@command('git.unstage')
	async unstage(...resourceStates: SourceControlResourceState[]): Promise<void> {
		resourceStates = resourceStates.filter(s => !!s);

		if (resourceStates.length === 0 || (resourceStates[0] && !(resourceStates[0].resourceUri instanceof Uri))) {
			const resource = this.getSCMResource();

			if (!resource) {
				return;
			}

			resourceStates = [resource];
		}

		const scmResources = resourceStates
			.filter(s => s instanceof Resource && s.resourceGroupType === ResourceGroupType.Index) as Resource[];

		if (!scmResources.length) {
			return;
		}

		const resources = scmResources.map(r => r.resourceUri);
		await this.runByRepository(resources, async (repository, resources) => repository.revert(resources));
	}


@command('git.unstageAll', { repository: true })
	async unstageAll(repository: Repository): Promise<void> {
		await repository.revert([]);
	}


@command('git.unstageSelectedRanges', { diff: true })
	async unstageSelectedRanges(diffs: LineChange[]): Promise<void> {
		const textEditor = window.activeTextEditor;

		if (!textEditor) {
			return;
		}

		const modifiedDocument = textEditor.document;
		const modifiedUri = modifiedDocument.uri;

		if (!isGitUri(modifiedUri)) {
			return;
		}

		const { ref } = fromGitUri(modifiedUri);

		if (ref !== '') {
			return;
		}

		const originalUri = toGitUri(modifiedUri, 'HEAD');
		const originalDocument = await workspace.openTextDocument(originalUri);
		const selectedLines = toLineRanges(textEditor.selections, modifiedDocument);
		const selectedDiffs = diffs
			.map(diff => selectedLines.reduce<LineChange | null>((result, range) => result || intersectDiffWithRange(modifiedDocument, diff, range), null))
			.filter(d => !!d) as LineChange[];

		if (!selectedDiffs.length) {
			return;
		}

		const invertedDiffs = selectedDiffs.map(invertLineChange);
		const result = applyLineChanges(modifiedDocument, originalDocument, invertedDiffs);

		await this.runByRepository(modifiedUri, async (repository, resource) => await repository.stage(resource, result));
	}


@command('git.clean')
	async clean(...resourceStates: SourceControlResourceState[]): Promise<void> {
		resourceStates = resourceStates.filter(s => !!s);

		if (resourceStates.length === 0 || (resourceStates[0] && !(resourceStates[0].resourceUri instanceof Uri))) {
			const resource = this.getSCMResource();

			if (!resource) {
				return;
			}

			resourceStates = [resource];
		}

		const scmResources = resourceStates.filter(s => s instanceof Resource
			&& (s.resourceGroupType === ResourceGroupType.WorkingTree || s.resourceGroupType === ResourceGroupType.Untracked)) as Resource[];

		if (!scmResources.length) {
			return;
		}

		const untrackedCount = scmResources.reduce((s, r) => s + (r.type === Status.UNTRACKED ? 1 : 0), 0);
		let message: string;
		let yes = localize('discard', "Discard Changes");

		if (scmResources.length === 1) {
			if (untrackedCount > 0) {
				message = localize('confirm delete', "Are you sure you want to DELETE {0}?\nThis is IRREVERSIBLE!\nThis file will be FOREVER LOST if you proceed.", path.basename(scmResources[0].resourceUri.fsPath));
				yes = localize('delete file', "Delete file");
			} else {
				if (scmResources[0].type === Status.DELETED) {
					yes = localize('restore file', "Restore file");
					message = localize('confirm restore', "Are you sure you want to restore {0}?", path.basename(scmResources[0].resourceUri.fsPath));
				} else {
					message = localize('confirm discard', "Are you sure you want to discard changes in {0}?", path.basename(scmResources[0].resourceUri.fsPath));
				}
			}
		} else {
			if (scmResources.every(resource => resource.type === Status.DELETED)) {
				yes = localize('restore files', "Restore files");
				message = localize('confirm restore multiple', "Are you sure you want to restore {0} files?", scmResources.length);
			} else {
				message = localize('confirm discard multiple', "Are you sure you want to discard changes in {0} files?", scmResources.length);
			}

			if (untrackedCount > 0) {
				message = `${message}\n\n${localize('warn untracked', "This will DELETE {0} untracked files!\nThis is IRREVERSIBLE!\nThese files will be FOREVER LOST.", untrackedCount)}`;
			}
		}

		const pick = await window.showWarningMessage(message, { modal: true }, yes);

		if (pick !== yes) {
			return;
		}

		const resources = scmResources.map(r => r.resourceUri);
		await this.runByRepository(resources, async (repository, resources) => repository.clean(resources));
	}


@command('git.cleanAll', { repository: true })
	async cleanAll(repository: Repository): Promise<void> {
		let resources = repository.workingTreeGroup.resourceStates;

		if (resources.length === 0) {
			return;
		}

		const trackedResources = resources.filter(r => r.type !== Status.UNTRACKED && r.type !== Status.IGNORED);
		const untrackedResources = resources.filter(r => r.type === Status.UNTRACKED || r.type === Status.IGNORED);

		if (untrackedResources.length === 0) {
			await this._cleanTrackedChanges(repository, resources);
		} else if (resources.length === 1) {
			await this._cleanUntrackedChange(repository, resources[0]);
		} else if (trackedResources.length === 0) {
			await this._cleanUntrackedChanges(repository, resources);
		} else { // resources.length > 1 && untrackedResources.length > 0 && trackedResources.length > 0
			const untrackedMessage = untrackedResources.length === 1
				? localize('there are untracked files single', "The following untracked file will be DELETED FROM DISK if discarded: {0}.", path.basename(untrackedResources[0].resourceUri.fsPath))
				: localize('there are untracked files', "There are {0} untracked files which will be DELETED FROM DISK if discarded.", untrackedResources.length);

			const message = localize('confirm discard all 2', "{0}\n\nThis is IRREVERSIBLE, your current working set will be FOREVER LOST.", untrackedMessage, resources.length);

			const yesTracked = trackedResources.length === 1
				? localize('yes discard tracked', "Discard 1 Tracked File", trackedResources.length)
				: localize('yes discard tracked multiple', "Discard {0} Tracked Files", trackedResources.length);

			const yesAll = localize('discardAll', "Discard All {0} Files", resources.length);
			const pick = await window.showWarningMessage(message, { modal: true }, yesTracked, yesAll);

			if (pick === yesTracked) {
				resources = trackedResources;
			} else if (pick !== yesAll) {
				return;
			}

			await repository.clean(resources.map(r => r.resourceUri));
		}
	}


@command('git.cleanAllTracked', { repository: true })
	async cleanAllTracked(repository: Repository): Promise<void> {
		const resources = repository.workingTreeGroup.resourceStates
			.filter(r => r.type !== Status.UNTRACKED && r.type !== Status.IGNORED);

		if (resources.length === 0) {
			return;
		}

		await this._cleanTrackedChanges(repository, resources);
	}


@command('git.cleanAllUntracked', { repository: true })
	async cleanAllUntracked(repository: Repository): Promise<void> {
		const resources = [...repository.workingTreeGroup.resourceStates, ...repository.untrackedGroup.resourceStates]
			.filter(r => r.type === Status.UNTRACKED || r.type === Status.IGNORED);

		if (resources.length === 0) {
			return;
		}

		if (resources.length === 1) {
			await this._cleanUntrackedChange(repository, resources[0]);
		} else {
			await this._cleanUntrackedChanges(repository, resources);
		}
	}


private async _cleanTrackedChanges(repository: Repository, resources: Resource[]): Promise<void> {
		const message = resources.length === 1
			? localize('confirm discard all single', "Are you sure you want to discard changes in {0}?", path.basename(resources[0].resourceUri.fsPath))
			: localize('confirm discard all', "Are you sure you want to discard ALL changes in {0} files?\nThis is IRREVERSIBLE!\nYour current working set will be FOREVER LOST if you proceed.", resources.length);
		const yes = resources.length === 1
			? localize('discardAll multiple', "Discard 1 File")
			: localize('discardAll', "Discard All {0} Files", resources.length);
		const pick = await window.showWarningMessage(message, { modal: true }, yes);

		if (pick !== yes) {
			return;
		}

		await repository.clean(resources.map(r => r.resourceUri));
	}


private async _cleanUntrackedChange(repository: Repository, resource: Resource): Promise<void> {
		const message = localize('confirm delete', "Are you sure you want to DELETE {0}?\nThis is IRREVERSIBLE!\nThis file will be FOREVER LOST if you proceed.", path.basename(resource.resourceUri.fsPath));
		const yes = localize('delete file', "Delete file");
		const pick = await window.showWarningMessage(message, { modal: true }, yes);

		if (pick !== yes) {
			return;
		}

		await repository.clean([resource.resourceUri]);
	}


private async _cleanUntrackedChanges(repository: Repository, resources: Resource[]): Promise<void> {
		const message = localize('confirm delete multiple', "Are you sure you want to DELETE {0} files?\nThis is IRREVERSIBLE!\nThese files will be FOREVER LOST if you proceed.", resources.length);
		const yes = localize('delete files', "Delete Files");
		const pick = await window.showWarningMessage(message, { modal: true }, yes);

		if (pick !== yes) {
			return;
		}

		await repository.clean(resources.map(r => r.resourceUri));
	}


private async smartCommit(
		repository: Repository,
		getCommitMessage: () => Promise<string | undefined>,
		opts?: CommitOptions
	): Promise<boolean> {
		const config = workspace.getConfiguration('git', Uri.file(repository.root));
		let promptToSaveFilesBeforeCommit = config.get<'always' | 'staged' | 'never'>('promptToSaveFilesBeforeCommit');

		// migration
		if (promptToSaveFilesBeforeCommit as any === true) {
			promptToSaveFilesBeforeCommit = 'always';
		} else if (promptToSaveFilesBeforeCommit as any === false) {
			promptToSaveFilesBeforeCommit = 'never';
		}

		const enableSmartCommit = config.get<boolean>('enableSmartCommit') === true;
		const enableCommitSigning = config.get<boolean>('enableCommitSigning') === true;
		let noStagedChanges = repository.indexGroup.resourceStates.length === 0;
		let noUnstagedChanges = repository.workingTreeGroup.resourceStates.length === 0;

		if (promptToSaveFilesBeforeCommit !== 'never') {
			let documents = workspace.textDocuments
				.filter(d => !d.isUntitled && d.isDirty && isDescendant(repository.root, d.uri.fsPath));

			if (promptToSaveFilesBeforeCommit === 'staged' || repository.indexGroup.resourceStates.length > 0) {
				documents = documents
					.filter(d => repository.indexGroup.resourceStates.some(s => pathEquals(s.resourceUri.fsPath, d.uri.fsPath)));
			}

			if (documents.length > 0) {
				const message = documents.length === 1
					? localize('unsaved files single', "The following file has unsaved changes which won't be included in the commit if you proceed: {0}.\n\nWould you like to save it before committing?", path.basename(documents[0].uri.fsPath))
					: localize('unsaved files', "There are {0} unsaved files.\n\nWould you like to save them before committing?", documents.length);
				const saveAndCommit = localize('save and commit', "Save All & Commit");
				const commit = localize('commit', "Commit Staged Changes");
				const pick = await window.showWarningMessage(message, { modal: true }, saveAndCommit, commit);

				if (pick === saveAndCommit) {
					await Promise.all(documents.map(d => d.save()));
					await repository.add(documents.map(d => d.uri));

					noStagedChanges = repository.indexGroup.resourceStates.length === 0;
					noUnstagedChanges = repository.workingTreeGroup.resourceStates.length === 0;
				} else if (pick !== commit) {
					return false; // do not commit on cancel
				}
			}
		}

		if (!opts) {
			opts = { all: noStagedChanges };
		} else if (!opts.all && noStagedChanges && !opts.empty) {
			opts = { ...opts, all: true };
		}

		// no changes, and the user has not configured to commit all in this case
		if (!noUnstagedChanges && noStagedChanges && !enableSmartCommit && !opts.empty) {
			const suggestSmartCommit = config.get<boolean>('suggestSmartCommit') === true;

			if (!suggestSmartCommit) {
				return false;
			}

			// prompt the user if we want to commit all or not
			const message = localize('no staged changes', "There are no staged changes to commit.\n\nWould you like to stage all your changes and commit them directly?");
			const yes = localize('yes', "Yes");
			const always = localize('always', "Always");
			const never = localize('never', "Never");
			const pick = await window.showWarningMessage(message, { modal: true }, yes, always, never);

			if (pick === always) {
				config.update('enableSmartCommit', true, true);
			} else if (pick === never) {
				config.update('suggestSmartCommit', false, true);
				return false;
			} else if (pick !== yes) {
				return false; // do not commit on cancel
			}
		}

		// enable signing of commits if configured
		opts.signCommit = enableCommitSigning;

		if (config.get<boolean>('alwaysSignOff')) {
			opts.signoff = true;
		}

		const smartCommitChanges = config.get<'all' | 'tracked'>('smartCommitChanges');

		if (
			(
				// no changes
				(noStagedChanges && noUnstagedChanges)
				// or no staged changes and not `all`
				|| (!opts.all && noStagedChanges)
				// no staged changes and no tracked unstaged changes
				|| (noStagedChanges && smartCommitChanges === 'tracked' && repository.workingTreeGroup.resourceStates.every(r => r.type === Status.UNTRACKED))
			)
			// amend allows changing only the commit message
			&& !opts.amend
			&& !opts.empty
		) {
			const commitAnyway = localize('commit anyway', "Create Empty Commit");
			const answer = await window.showInformationMessage(localize('no changes', "There are no changes to commit."), commitAnyway);

			if (answer !== commitAnyway) {
				return false;
			}

			opts.empty = true;
		}

		if (opts.noVerify) {
			if (!config.get<boolean>('allowNoVerifyCommit')) {
				await window.showErrorMessage(localize('no verify commit not allowed', "Commits without verification are not allowed, please enable them with the 'git.allowNoVerifyCommit' setting."));
				return false;
			}

			if (config.get<boolean>('confirmNoVerifyCommit')) {
				const message = localize('confirm no verify commit', "You are about to commit your changes without verification, this skips pre-commit hooks and can be undesirable.\n\nAre you sure to continue?");
				const yes = localize('ok', "OK");
				const neverAgain = localize('never ask again', "OK, Don't Ask Again");
				const pick = await window.showWarningMessage(message, { modal: true }, yes, neverAgain);

				if (pick === neverAgain) {
					config.update('confirmNoVerifyCommit', false, true);
				} else if (pick !== yes) {
					return false;
				}
			}
		}

		let message = await getCommitMessage();

		if (!message && !opts.amend) {
			return false;
		}

		if (opts.all && smartCommitChanges === 'tracked') {
			opts.all = 'tracked';
		}

		if (opts.all && config.get<'mixed' | 'separate' | 'hidden'>('untrackedChanges') !== 'mixed') {
			opts.all = 'tracked';
		}

		await repository.commit(message, opts);

		const postCommitCommand = config.get<'none' | 'push' | 'sync'>('postCommitCommand');

		switch (postCommitCommand) {
			case 'push':
				await this._push(repository, { pushType: PushType.Push, silent: true });
				break;
			case 'sync':
				await this.sync(repository);
				break;
		}

		return true;
	}


private async commitWithAnyInput(repository: Repository, opts?: CommitOptions): Promise<void> {
		const message = repository.inputBox.value;
		const getCommitMessage = async () => {
			let _message: string | undefined = message;

			if (!_message) {
				let value: string | undefined = undefined;

				if (opts && opts.amend && repository.HEAD && repository.HEAD.commit) {
					return undefined;
				}

				const branchName = repository.headShortName;
				let placeHolder: string;

				if (branchName) {
					placeHolder = localize('commitMessageWithHeadLabel2', "Message (commit on '{0}')", branchName);
				} else {
					placeHolder = localize('commit message', "Commit message");
				}

				_message = await window.showInputBox({
					value,
					placeHolder,
					prompt: localize('provide commit message', "Please provide a commit message"),
					ignoreFocusOut: true
				});
			}

			return _message;
		};

		const didCommit = await this.smartCommit(repository, getCommitMessage, opts);

		if (message && didCommit) {
			repository.inputBox.value = await repository.getInputTemplate();
		}
	}


@command('git.commit', { repository: true })
	async commit(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository);
	}


@command('git.commitStaged', { repository: true })
	async commitStaged(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository, { all: false });
	}


@command('git.commitStagedSigned', { repository: true })
	async commitStagedSigned(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository, { all: false, signoff: true });
	}


@command('git.commitStagedAmend', { repository: true })
	async commitStagedAmend(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository, { all: false, amend: true });
	}


@command('git.commitAll', { repository: true })
	async commitAll(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository, { all: true });
	}


@command('git.commitAllSigned', { repository: true })
	async commitAllSigned(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository, { all: true, signoff: true });
	}


@command('git.commitAllAmend', { repository: true })
	async commitAllAmend(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository, { all: true, amend: true });
	}


private async _commitEmpty(repository: Repository, noVerify?: boolean): Promise<void> {
		const root = Uri.file(repository.root);
		const config = workspace.getConfiguration('git', root);
		const shouldPrompt = config.get<boolean>('confirmEmptyCommits') === true;

		if (shouldPrompt) {
			const message = localize('confirm emtpy commit', "Are you sure you want to create an empty commit?");
			const yes = localize('yes', "Yes");
			const neverAgain = localize('yes never again', "Yes, Don't Show Again");
			const pick = await window.showWarningMessage(message, { modal: true }, yes, neverAgain);

			if (pick === neverAgain) {
				await config.update('confirmEmptyCommits', false, true);
			} else if (pick !== yes) {
				return;
			}
		}

		await this.commitWithAnyInput(repository, { empty: true, noVerify });
	}


@command('git.commitEmpty', { repository: true })
	async commitEmpty(repository: Repository): Promise<void> {
		await this._commitEmpty(repository);
	}


@command('git.commitNoVerify', { repository: true })
	async commitNoVerify(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository, { noVerify: true });
	}


@command('git.commitStagedNoVerify', { repository: true })
	async commitStagedNoVerify(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository, { all: false, noVerify: true });
	}


@command('git.commitStagedSignedNoVerify', { repository: true })
	async commitStagedSignedNoVerify(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository, { all: false, signoff: true, noVerify: true });
	}


@command('git.commitStagedAmendNoVerify', { repository: true })
	async commitStagedAmendNoVerify(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository, { all: false, amend: true, noVerify: true });
	}


@command('git.commitAllNoVerify', { repository: true })
	async commitAllNoVerify(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository, { all: true, noVerify: true });
	}


@command('git.commitAllSignedNoVerify', { repository: true })
	async commitAllSignedNoVerify(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository, { all: true, signoff: true, noVerify: true });
	}


@command('git.commitAllAmendNoVerify', { repository: true })
	async commitAllAmendNoVerify(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository, { all: true, amend: true, noVerify: true });
	}


@command('git.commitEmptyNoVerify', { repository: true })
	async commitEmptyNoVerify(repository: Repository): Promise<void> {
		await this._commitEmpty(repository, true);
	}


@command('git.restoreCommitTemplate', { repository: true })
	async restoreCommitTemplate(repository: Repository): Promise<void> {
		repository.inputBox.value = await repository.getCommitTemplate();
	}


@command('git.undoCommit', { repository: true })
	async undoCommit(repository: Repository): Promise<void> {
		const HEAD = repository.HEAD;

		if (!HEAD || !HEAD.commit) {
			window.showWarningMessage(localize('no more', "Can't undo because HEAD doesn't point to any commit."));
			return;
		}

		const commit = await repository.getCommit('HEAD');

		if (commit.parents.length > 1) {
			const yes = localize('undo commit', "Undo merge commit");
			const result = await window.showWarningMessage(localize('merge commit', "The last commit was a merge commit. Are you sure you want to undo it?"), { modal: true }, yes);

			if (result !== yes) {
				return;
			}
		}

		if (commit.parents.length > 0) {
			await repository.reset('HEAD~');
		} else {
			await repository.deleteRef('HEAD');
			await this.unstageAll(repository);
		}

		repository.inputBox.value = commit.message;
	}


@command('git.checkout', { repository: true })
	async checkout(repository: Repository, treeish?: string): Promise<boolean> {
		return this._checkout(repository, { treeish });
	}


@command('git.checkoutDetached', { repository: true })
	async checkoutDetached(repository: Repository, treeish?: string): Promise<boolean> {
		return this._checkout(repository, { detached: true, treeish });
	}


private async _checkout(repository: Repository, opts?: { detached?: boolean; treeish?: string }): Promise<boolean> {
		if (typeof opts?.treeish === 'string') {
			await repository.checkout(opts?.treeish, opts);
			return true;
		}

		const createBranch = new CreateBranchItem();
		const createBranchFrom = new CreateBranchFromItem();
		const checkoutDetached = new CheckoutDetachedItem();
		const picks: QuickPickItem[] = [];

		if (!opts?.detached) {
			picks.push(createBranch, createBranchFrom, checkoutDetached);
		}

		picks.push(...createCheckoutItems(repository));

		const quickpick = window.createQuickPick();
		quickpick.items = picks;
		quickpick.placeholder = opts?.detached
			? localize('select a ref to checkout detached', 'Select a ref to checkout in detached mode')
			: localize('select a ref to checkout', 'Select a ref to checkout');

		quickpick.show();

		const choice = await new Promise<QuickPickItem | undefined>(c => quickpick.onDidAccept(() => c(quickpick.activeItems[0])));
		quickpick.hide();

		if (!choice) {
			return false;
		}

		if (choice === createBranch) {
			await this._branch(repository, quickpick.value);
		} else if (choice === createBranchFrom) {
			await this._branch(repository, quickpick.value, true);
		} else if (choice === checkoutDetached) {
			return this._checkout(repository, { detached: true });
		} else {
			const item = choice as CheckoutItem;

			try {
				await item.run(repository, opts);
			} catch (err) {
				if (err.gitErrorCode !== GitErrorCodes.DirtyWorkTree) {
					throw err;
				}

				const force = localize('force', "Force Checkout");
				const stash = localize('stashcheckout', "Stash & Checkout");
				const choice = await window.showWarningMessage(localize('local changes', "Your local changes would be overwritten by checkout."), { modal: true }, force, stash);

				if (choice === force) {
					await this.cleanAll(repository);
					await item.run(repository, opts);
				} else if (choice === stash) {
					await this.stash(repository);
					await item.run(repository, opts);
					await this.stashPopLatest(repository);
				}
			}
		}

		return true;
	}


@command('git.branch', { repository: true })
	async branch(repository: Repository): Promise<void> {
		await this._branch(repository);
	}


@command('git.branchFrom', { repository: true })
	async branchFrom(repository: Repository): Promise<void> {
		await this._branch(repository, undefined, true);
	}


private generateRandomBranchName(repository: Repository, separator: string): string {
		const config = workspace.getConfiguration('git');
		const branchRandomNameDictionary = config.get<string[]>('branchRandomName.dictionary')!;

		const dictionaries: string[][] = [];
		for (const dictionary of branchRandomNameDictionary) {
			if (dictionary.toLowerCase() === 'adjectives') {
				dictionaries.push(adjectives);
			}
			if (dictionary.toLowerCase() === 'animals') {
				dictionaries.push(animals);
			}
			if (dictionary.toLowerCase() === 'colors') {
				dictionaries.push(colors);
			}
			if (dictionary.toLowerCase() === 'numbers') {
				dictionaries.push(NumberDictionary.generate({ length: 3 }));
			}
		}

		if (dictionaries.length === 0) {
			return '';
		}

		// 5 attempts to generate a random branch name
		for (let index = 0; index < 5; index++) {
			const randomName = uniqueNamesGenerator({
				dictionaries,
				length: dictionaries.length,
				separator
			});

			// Check for local ref conflict
			if (!repository.refs.find(r => r.type === RefType.Head && r.name === randomName)) {
				return randomName;
			}
		}

		return '';
	}


private async promptForBranchName(repository: Repository, defaultName?: string, initialValue?: string): Promise<string> {
		const config = workspace.getConfiguration('git');
		const branchPrefix = config.get<string>('branchPrefix')!;
		const branchWhitespaceChar = config.get<string>('branchWhitespaceChar')!;
		const branchValidationRegex = config.get<string>('branchValidationRegex')!;
		const sanitize = (name: string) => name ?
			name.trim().replace(/^-+/, '').replace(/^\.|\/\.|\.\.|~|\^|:|\/$|\.lock$|\.lock\/|\\|\*|\s|^\s*$|\.$|\[|\]$/g, branchWhitespaceChar)
			: name;

		let rawBranchName = defaultName;

		if (!rawBranchName) {
			// Branch name
			if (!initialValue) {
				const branchRandomNameEnabled = config.get<boolean>('branchRandomName.enable', false);
				initialValue = `${branchPrefix}${branchRandomNameEnabled ? this.generateRandomBranchName(repository, branchWhitespaceChar) : ''}`;
			}

			// Branch name selection
			const initialValueSelection: [number, number] | undefined =
				initialValue.startsWith(branchPrefix) ? [branchPrefix.length, initialValue.length] : undefined;

			rawBranchName = await window.showInputBox({
				placeHolder: localize('branch name', "Branch name"),
				prompt: localize('provide branch name', "Please provide a new branch name"),
				value: initialValue,
				valueSelection: initialValueSelection,
				ignoreFocusOut: true,
				validateInput: (name: string) => {
					const validateName = new RegExp(branchValidationRegex);
					const sanitizedName = sanitize(name);
					if (validateName.test(sanitizedName)) {
						// If the sanitized name that we will use is different than what is
						// in the input box, show an info message to the user informing them
						// the branch name that will be used.
						return name === sanitizedName
							? null
							: {
								message: localize('branch name does not match sanitized', "The new branch will be '{0}'", sanitizedName),
								severity: InputBoxValidationSeverity.Info
							};
					}

					return localize('branch name format invalid', "Branch name needs to match regex: {0}", branchValidationRegex);
				}
			});
		}

		return sanitize(rawBranchName || '');
	}


private async _branch(repository: Repository, defaultName?: string, from = false): Promise<void> {
		const branchName = await this.promptForBranchName(repository, defaultName);

		if (!branchName) {
			return;
		}

		let target = 'HEAD';

		if (from) {
			const picks = [new HEADItem(repository), ...createCheckoutItems(repository)];
			const placeHolder = localize('select a ref to create a new branch from', 'Select a ref to create the \'{0}\' branch from', branchName);
			const choice = await window.showQuickPick(picks, { placeHolder });

			if (!choice) {
				return;
			}

			target = choice.label;
		}

		await repository.branch(branchName, true, target);
	}


@command('git.deleteBranch', { repository: true })
	async deleteBranch(repository: Repository, name: string, force?: boolean): Promise<void> {
		let run: (force?: boolean) => Promise<void>;
		if (typeof name === 'string') {
			run = force => repository.deleteBranch(name, force);
		} else {
			const currentHead = repository.HEAD && repository.HEAD.name;
			const heads = repository.refs.filter(ref => ref.type === RefType.Head && ref.name !== currentHead)
				.map(ref => new BranchDeleteItem(ref));

			const placeHolder = localize('select branch to delete', 'Select a branch to delete');
			const choice = await window.showQuickPick<BranchDeleteItem>(heads, { placeHolder });

			if (!choice || !choice.branchName) {
				return;
			}
			name = choice.branchName;
			run = force => choice.run(repository, force);
		}

		try {
			await run(force);
		} catch (err) {
			if (err.gitErrorCode !== GitErrorCodes.BranchNotFullyMerged) {
				throw err;
			}

			const message = localize('confirm force delete branch', "The branch '{0}' is not fully merged. Delete anyway?", name);
			const yes = localize('delete branch', "Delete Branch");
			const pick = await window.showWarningMessage(message, { modal: true }, yes);

			if (pick === yes) {
				await run(true);
			}
		}
	}


@command('git.renameBranch', { repository: true })
	async renameBranch(repository: Repository): Promise<void> {
		const currentBranchName = repository.HEAD && repository.HEAD.name;
		const branchName = await this.promptForBranchName(repository, undefined, currentBranchName);

		if (!branchName) {
			return;
		}

		try {
			await repository.renameBranch(branchName);
		} catch (err) {
			switch (err.gitErrorCode) {
				case GitErrorCodes.InvalidBranchName:
					window.showErrorMessage(localize('invalid branch name', 'Invalid branch name'));
					return;
				case GitErrorCodes.BranchAlreadyExists:
					window.showErrorMessage(localize('branch already exists', "A branch named '{0}' already exists", branchName));
					return;
				default:
					throw err;
			}
		}
	}


@command('git.merge', { repository: true })
	async merge(repository: Repository): Promise<void> {
		const config = workspace.getConfiguration('git');
		const checkoutType = config.get<string | string[]>('checkoutType');
		const includeRemotes = checkoutType === 'all' || checkoutType === 'remote' || checkoutType?.includes('remote');

		const heads = repository.refs.filter(ref => ref.type === RefType.Head)
			.filter(ref => ref.name || ref.commit)
			.map(ref => new MergeItem(ref as Branch));

		const remoteHeads = (includeRemotes ? repository.refs.filter(ref => ref.type === RefType.RemoteHead) : [])
			.filter(ref => ref.name || ref.commit)
			.map(ref => new MergeItem(ref as Branch));

		const picks = [...heads, ...remoteHeads];
		const placeHolder = localize('select a branch to merge from', 'Select a branch to merge from');
		const choice = await window.showQuickPick<MergeItem>(picks, { placeHolder });

		if (!choice) {
			return;
		}

		await choice.run(repository);
	}


@command('git.rebase', { repository: true })
	async rebase(repository: Repository): Promise<void> {
		const config = workspace.getConfiguration('git');
		const checkoutType = config.get<string | string[]>('checkoutType');
		const includeRemotes = checkoutType === 'all' || checkoutType === 'remote' || checkoutType?.includes('remote');

		const heads = repository.refs.filter(ref => ref.type === RefType.Head)
			.filter(ref => ref.name !== repository.HEAD?.name)
			.filter(ref => ref.name || ref.commit);

		const remoteHeads = (includeRemotes ? repository.refs.filter(ref => ref.type === RefType.RemoteHead) : [])
			.filter(ref => ref.name || ref.commit);

		const picks = [...heads, ...remoteHeads]
			.map(ref => new RebaseItem(ref));

		// set upstream branch as first
		if (repository.HEAD?.upstream) {
			const upstreamName = `${repository.HEAD?.upstream.remote}/${repository.HEAD?.upstream.name}`;
			const index = picks.findIndex(e => e.ref.name === upstreamName);

			if (index > -1) {
				const [ref] = picks.splice(index, 1);
				ref.description = '(upstream)';
				picks.unshift(ref);
			}
		}

		const placeHolder = localize('select a branch to rebase onto', 'Select a branch to rebase onto');
		const choice = await window.showQuickPick<RebaseItem>(picks, { placeHolder });

		if (!choice) {
			return;
		}

		await choice.run(repository);
	}


@command('git.createTag', { repository: true })
	async createTag(repository: Repository): Promise<void> {
		const inputTagName = await window.showInputBox({
			placeHolder: localize('tag name', "Tag name"),
			prompt: localize('provide tag name', "Please provide a tag name"),
			ignoreFocusOut: true
		});

		if (!inputTagName) {
			return;
		}

		const inputMessage = await window.showInputBox({
			placeHolder: localize('tag message', "Message"),
			prompt: localize('provide tag message', "Please provide a message to annotate the tag"),
			ignoreFocusOut: true
		});

		const name = inputTagName.replace(/^\.|\/\.|\.\.|~|\^|:|\/$|\.lock$|\.lock\/|\\|\*|\s|^\s*$|\.$/g, '-');
		await repository.tag(name, inputMessage);
	}


@command('git.deleteTag', { repository: true })
	async deleteTag(repository: Repository): Promise<void> {
		const picks = repository.refs.filter(ref => ref.type === RefType.Tag)
			.map(ref => new TagItem(ref));

		if (picks.length === 0) {
			window.showWarningMessage(localize('no tags', "This repository has no tags."));
			return;
		}

		const placeHolder = localize('select a tag to delete', 'Select a tag to delete');
		const choice = await window.showQuickPick(picks, { placeHolder });

		if (!choice) {
			return;
		}

		await repository.deleteTag(choice.label);
	}


@command('git.fetch', { repository: true })
	async fetch(repository: Repository): Promise<void> {
		if (repository.remotes.length === 0) {
			window.showWarningMessage(localize('no remotes to fetch', "This repository has no remotes configured to fetch from."));
			return;
		}

		await repository.fetchDefault();
	}


@command('git.fetchPrune', { repository: true })
	async fetchPrune(repository: Repository): Promise<void> {
		if (repository.remotes.length === 0) {
			window.showWarningMessage(localize('no remotes to fetch', "This repository has no remotes configured to fetch from."));
			return;
		}

		await repository.fetchPrune();
	}


@command('git.fetchAll', { repository: true })
	async fetchAll(repository: Repository): Promise<void> {
		if (repository.remotes.length === 0) {
			window.showWarningMessage(localize('no remotes to fetch', "This repository has no remotes configured to fetch from."));
			return;
		}

		await repository.fetchAll();
	}


@command('git.pullFrom', { repository: true })
	async pullFrom(repository: Repository): Promise<void> {
		const remotes = repository.remotes;

		if (remotes.length === 0) {
			window.showWarningMessage(localize('no remotes to pull', "Your repository has no remotes configured to pull from."));
			return;
		}

		const remotePicks = remotes.filter(r => r.fetchUrl !== undefined).map(r => ({ label: r.name, description: r.fetchUrl! }));
		const placeHolder = localize('pick remote pull repo', "Pick a remote to pull the branch from");
		const remotePick = await window.showQuickPick(remotePicks, { placeHolder });

		if (!remotePick) {
			return;
		}

		const remoteRefs = repository.refs;
		const remoteRefsFiltered = remoteRefs.filter(r => (r.remote === remotePick.label));
		const branchPicks = remoteRefsFiltered.map(r => ({ label: r.name! }));
		const branchPlaceHolder = localize('pick branch pull', "Pick a branch to pull from");
		const branchPick = await window.showQuickPick(branchPicks, { placeHolder: branchPlaceHolder });

		if (!branchPick) {
			return;
		}

		const remoteCharCnt = remotePick.label.length;

		await repository.pullFrom(false, remotePick.label, branchPick.label.slice(remoteCharCnt + 1));
	}


@command('git.pull', { repository: true })
	async pull(repository: Repository): Promise<void> {
		const remotes = repository.remotes;

		if (remotes.length === 0) {
			window.showWarningMessage(localize('no remotes to pull', "Your repository has no remotes configured to pull from."));
			return;
		}

		await repository.pull(repository.HEAD);
	}


@command('git.pullRebase', { repository: true })
	async pullRebase(repository: Repository): Promise<void> {
		const remotes = repository.remotes;

		if (remotes.length === 0) {
			window.showWarningMessage(localize('no remotes to pull', "Your repository has no remotes configured to pull from."));
			return;
		}

		await repository.pullWithRebase(repository.HEAD);
	}


private async _push(repository: Repository, pushOptions: PushOptions) {
		const remotes = repository.remotes;

		if (remotes.length === 0) {
			if (pushOptions.silent) {
				return;
			}

			const addRemote = localize('addremote', 'Add Remote');
			const result = await window.showWarningMessage(localize('no remotes to push', "Your repository has no remotes configured to push to."), addRemote);

			if (result === addRemote) {
				await this.addRemote(repository);
			}

			return;
		}

		const config = workspace.getConfiguration('git', Uri.file(repository.root));
		let forcePushMode: ForcePushMode | undefined = undefined;

		if (pushOptions.forcePush) {
			if (!config.get<boolean>('allowForcePush')) {
				await window.showErrorMessage(localize('force push not allowed', "Force push is not allowed, please enable it with the 'git.allowForcePush' setting."));
				return;
			}

			forcePushMode = config.get<boolean>('useForcePushWithLease') === true ? ForcePushMode.ForceWithLease : ForcePushMode.Force;

			if (config.get<boolean>('confirmForcePush')) {
				const message = localize('confirm force push', "You are about to force push your changes, this can be destructive and could inadvertently overwrite changes made by others.\n\nAre you sure to continue?");
				const yes = localize('ok', "OK");
				const neverAgain = localize('never ask again', "OK, Don't Ask Again");
				const pick = await window.showWarningMessage(message, { modal: true }, yes, neverAgain);

				if (pick === neverAgain) {
					config.update('confirmForcePush', false, true);
				} else if (pick !== yes) {
					return;
				}
			}
		}

		if (pushOptions.pushType === PushType.PushFollowTags) {
			await repository.pushFollowTags(undefined, forcePushMode);
			return;
		}

		if (pushOptions.pushType === PushType.PushTags) {
			await repository.pushTags(undefined, forcePushMode);
		}

		if (!repository.HEAD || !repository.HEAD.name) {
			if (!pushOptions.silent) {
				window.showWarningMessage(localize('nobranch', "Please check out a branch to push to a remote."));
			}
			return;
		}

		if (pushOptions.pushType === PushType.Push) {
			try {
				await repository.push(repository.HEAD, forcePushMode);
			} catch (err) {
				if (err.gitErrorCode !== GitErrorCodes.NoUpstreamBranch) {
					throw err;
				}

				if (pushOptions.silent) {
					return;
				}

				const branchName = repository.HEAD.name;
				const message = localize('confirm publish branch', "The branch '{0}' has no remote branch. Would you like to publish this branch?", branchName);
				const yes = localize('ok', "OK");
				const pick = await window.showWarningMessage(message, { modal: true }, yes);

				if (pick === yes) {
					await this.publish(repository);
				}
			}
		} else {
			const branchName = repository.HEAD.name;
			if (!pushOptions.pushTo?.remote) {
				const addRemote = new AddRemoteItem(this);
				const picks = [...remotes.filter(r => r.pushUrl !== undefined).map(r => ({ label: r.name, description: r.pushUrl })), addRemote];
				const placeHolder = localize('pick remote', "Pick a remote to publish the branch '{0}' to:", branchName);
				const choice = await window.showQuickPick(picks, { placeHolder });

				if (!choice) {
					return;
				}

				if (choice === addRemote) {
					const newRemote = await this.addRemote(repository);

					if (newRemote) {
						await repository.pushTo(newRemote, branchName, undefined, forcePushMode);
					}
				} else {
					await repository.pushTo(choice.label, branchName, undefined, forcePushMode);
				}
			} else {
				await repository.pushTo(pushOptions.pushTo.remote, pushOptions.pushTo.refspec || branchName, pushOptions.pushTo.setUpstream, forcePushMode);
			}
		}
	}


@command('git.push', { repository: true })
	async push(repository: Repository): Promise<void> {
		await this._push(repository, { pushType: PushType.Push });
	}


@command('git.pushForce', { repository: true })
	async pushForce(repository: Repository): Promise<void> {
		await this._push(repository, { pushType: PushType.Push, forcePush: true });
	}


@command('git.pushWithTags', { repository: true })
	async pushFollowTags(repository: Repository): Promise<void> {
		await this._push(repository, { pushType: PushType.PushFollowTags });
	}


@command('git.pushWithTagsForce', { repository: true })
	async pushFollowTagsForce(repository: Repository): Promise<void> {
		await this._push(repository, { pushType: PushType.PushFollowTags, forcePush: true });
	}


@command('git.cherryPick', { repository: true })
	async cherryPick(repository: Repository): Promise<void> {
		const hash = await window.showInputBox({
			placeHolder: localize('commit hash', "Commit Hash"),
			prompt: localize('provide commit hash', "Please provide the commit hash"),
			ignoreFocusOut: true
		});

		if (!hash) {
			return;
		}

		await repository.cherryPick(hash);
	}


@command('git.pushTo', { repository: true })
	async pushTo(repository: Repository, remote?: string, refspec?: string, setUpstream?: boolean): Promise<void> {
		await this._push(repository, { pushType: PushType.PushTo, pushTo: { remote: remote, refspec: refspec, setUpstream: setUpstream } });
	}


@command('git.pushToForce', { repository: true })
	async pushToForce(repository: Repository, remote?: string, refspec?: string, setUpstream?: boolean): Promise<void> {
		await this._push(repository, { pushType: PushType.PushTo, pushTo: { remote: remote, refspec: refspec, setUpstream: setUpstream }, forcePush: true });
	}


@command('git.pushTags', { repository: true })
	async pushTags(repository: Repository): Promise<void> {
		await this._push(repository, { pushType: PushType.PushTags });
	}


@command('git.addRemote', { repository: true })
	async addRemote(repository: Repository): Promise<string | undefined> {
		const url = await pickRemoteSource({
			providerLabel: provider => localize('addfrom', "Add remote from {0}", provider.name),
			urlLabel: localize('addFrom', "Add remote from URL")
		});

		if (!url) {
			return;
		}

		const resultName = await window.showInputBox({
			placeHolder: localize('remote name', "Remote name"),
			prompt: localize('provide remote name', "Please provide a remote name"),
			ignoreFocusOut: true,
			validateInput: (name: string) => {
				if (!sanitizeRemoteName(name)) {
					return localize('remote name format invalid', "Remote name format invalid");
				} else if (repository.remotes.find(r => r.name === name)) {
					return localize('remote already exists', "Remote '{0}' already exists.", name);
				}

				return null;
			}
		});

		const name = sanitizeRemoteName(resultName || '');

		if (!name) {
			return;
		}

		await repository.addRemote(name, url.trim());
		await repository.fetch({ remote: name });
		return name;
	}


@command('git.removeRemote', { repository: true })
	async removeRemote(repository: Repository): Promise<void> {
		const remotes = repository.remotes;

		if (remotes.length === 0) {
			window.showErrorMessage(localize('no remotes added', "Your repository has no remotes."));
			return;
		}

		const picks = remotes.map(r => r.name);
		const placeHolder = localize('remove remote', "Pick a remote to remove");

		const remoteName = await window.showQuickPick(picks, { placeHolder });

		if (!remoteName) {
			return;
		}

		await repository.removeRemote(remoteName);
	}


private async _sync(repository: Repository, rebase: boolean): Promise<void> {
		const HEAD = repository.HEAD;

		if (!HEAD) {
			return;
		} else if (!HEAD.upstream) {
			const branchName = HEAD.name;
			const message = localize('confirm publish branch', "The branch '{0}' has no remote branch. Would you like to publish this branch?", branchName);
			const yes = localize('ok', "OK");
			const pick = await window.showWarningMessage(message, { modal: true }, yes);

			if (pick === yes) {
				await this.publish(repository);
			}
			return;
		}

		const remoteName = HEAD.remote || HEAD.upstream.remote;
		const remote = repository.remotes.find(r => r.name === remoteName);
		const isReadonly = remote && remote.isReadOnly;

		const config = workspace.getConfiguration('git');
		const shouldPrompt = !isReadonly && config.get<boolean>('confirmSync') === true;

		if (shouldPrompt) {
			const message = localize('sync is unpredictable', "This action will pull and push commits from and to '{0}/{1}'.", HEAD.upstream.remote, HEAD.upstream.name);
			const yes = localize('ok', "OK");
			const neverAgain = localize('never again', "OK, Don't Show Again");
			const pick = await window.showWarningMessage(message, { modal: true }, yes, neverAgain);

			if (pick === neverAgain) {
				await config.update('confirmSync', false, true);
			} else if (pick !== yes) {
				return;
			}
		}

		if (rebase) {
			await repository.syncRebase(HEAD);
		} else {
			await repository.sync(HEAD);
		}
	}


@command('git.sync', { repository: true })
	async sync(repository: Repository): Promise<void> {
		try {
			await this._sync(repository, false);
		} catch (err) {
			if (/Cancelled/i.test(err && (err.message || err.stderr || ''))) {
				return;
			}

			throw err;
		}
	}


@command('git._syncAll')
	async syncAll(): Promise<void> {
		await Promise.all(this.model.repositories.map(async repository => {
			const HEAD = repository.HEAD;

			if (!HEAD || !HEAD.upstream) {
				return;
			}

			await repository.sync(HEAD);
		}));
	}


@command('git.syncRebase', { repository: true })
	async syncRebase(repository: Repository): Promise<void> {
		try {
			await this._sync(repository, true);
		} catch (err) {
			if (/Cancelled/i.test(err && (err.message || err.stderr || ''))) {
				return;
			}

			throw err;
		}
	}


@command('git.publish', { repository: true })
	async publish(repository: Repository): Promise<void> {
		const branchName = repository.HEAD && repository.HEAD.name || '';
		const remotes = repository.remotes;

		if (remotes.length === 0) {
			const publishers = this.model.getRemoteSourcePublishers();

			if (publishers.length === 0) {
				window.showWarningMessage(localize('no remotes to publish', "Your repository has no remotes configured to publish to."));
				return;
			}

			let publisher: RemoteSourcePublisher;

			if (publishers.length === 1) {
				publisher = publishers[0];
			} else {
				const picks = publishers
					.map(provider => ({ label: (provider.icon ? `$(${provider.icon}) ` : '') + localize('publish to', "Publish to {0}", provider.name), alwaysShow: true, provider }));
				const placeHolder = localize('pick provider', "Pick a provider to publish the branch '{0}' to:", branchName);
				const choice = await window.showQuickPick(picks, { placeHolder });

				if (!choice) {
					return;
				}

				publisher = choice.provider;
			}

			await publisher.publishRepository(new ApiRepository(repository));
			this.model.firePublishEvent(repository, branchName);

			return;
		}

		if (remotes.length === 1) {
			await repository.pushTo(remotes[0].name, branchName, true);
			this.model.firePublishEvent(repository, branchName);

			return;
		}

		const addRemote = new AddRemoteItem(this);
		const picks = [...repository.remotes.map(r => ({ label: r.name, description: r.pushUrl })), addRemote];
		const placeHolder = localize('pick remote', "Pick a remote to publish the branch '{0}' to:", branchName);
		const choice = await window.showQuickPick(picks, { placeHolder });

		if (!choice) {
			return;
		}

		if (choice === addRemote) {
			const newRemote = await this.addRemote(repository);

			if (newRemote) {
				await repository.pushTo(newRemote, branchName, true);

				this.model.firePublishEvent(repository, branchName);
			}
		} else {
			await repository.pushTo(choice.label, branchName, true);

			this.model.firePublishEvent(repository, branchName);
		}
	}


@command('git.ignore')
	async ignore(...resourceStates: SourceControlResourceState[]): Promise<void> {
		resourceStates = resourceStates.filter(s => !!s);

		if (resourceStates.length === 0 || (resourceStates[0] && !(resourceStates[0].resourceUri instanceof Uri))) {
			const resource = this.getSCMResource();

			if (!resource) {
				return;
			}

			resourceStates = [resource];
		}

		const resources = resourceStates
			.filter(s => s instanceof Resource)
			.map(r => r.resourceUri);

		if (!resources.length) {
			return;
		}

		await this.runByRepository(resources, async (repository, resources) => repository.ignore(resources));
	}


@command('git.revealInExplorer')
	async revealInExplorer(resourceState: SourceControlResourceState): Promise<void> {
		if (!resourceState) {
			return;
		}

		if (!(resourceState.resourceUri instanceof Uri)) {
			return;
		}

		await commands.executeCommand('revealInExplorer', resourceState.resourceUri);
	}


private async _stash(repository: Repository, includeUntracked = false): Promise<void> {
		const noUnstagedChanges = repository.workingTreeGroup.resourceStates.length === 0
			&& (!includeUntracked || repository.untrackedGroup.resourceStates.length === 0);
		const noStagedChanges = repository.indexGroup.resourceStates.length === 0;

		if (noUnstagedChanges && noStagedChanges) {
			window.showInformationMessage(localize('no changes stash', "There are no changes to stash."));
			return;
		}

		const config = workspace.getConfiguration('git', Uri.file(repository.root));
		const promptToSaveFilesBeforeStashing = config.get<'always' | 'staged' | 'never'>('promptToSaveFilesBeforeStash');

		if (promptToSaveFilesBeforeStashing !== 'never') {
			let documents = workspace.textDocuments
				.filter(d => !d.isUntitled && d.isDirty && isDescendant(repository.root, d.uri.fsPath));

			if (promptToSaveFilesBeforeStashing === 'staged' || repository.indexGroup.resourceStates.length > 0) {
				documents = documents
					.filter(d => repository.indexGroup.resourceStates.some(s => pathEquals(s.resourceUri.fsPath, d.uri.fsPath)));
			}

			if (documents.length > 0) {
				const message = documents.length === 1
					? localize('unsaved stash files single', "The following file has unsaved changes which won't be included in the stash if you proceed: {0}.\n\nWould you like to save it before stashing?", path.basename(documents[0].uri.fsPath))
					: localize('unsaved stash files', "There are {0} unsaved files.\n\nWould you like to save them before stashing?", documents.length);
				const saveAndStash = localize('save and stash', "Save All & Stash");
				const stash = localize('stash', "Stash Anyway");
				const pick = await window.showWarningMessage(message, { modal: true }, saveAndStash, stash);

				if (pick === saveAndStash) {
					await Promise.all(documents.map(d => d.save()));
				} else if (pick !== stash) {
					return; // do not stash on cancel
				}
			}
		}

		let message: string | undefined;

		if (config.get<boolean>('useCommitInputAsStashMessage') && (!repository.sourceControl.commitTemplate || repository.inputBox.value !== repository.sourceControl.commitTemplate)) {
			message = repository.inputBox.value;
		}

		message = await window.showInputBox({
			value: message,
			prompt: localize('provide stash message', "Optionally provide a stash message"),
			placeHolder: localize('stash message', "Stash message")
		});

		if (typeof message === 'undefined') {
			return;
		}

		await repository.createStash(message, includeUntracked);
	}


@command('git.stash', { repository: true })
	stash(repository: Repository): Promise<void> {
		return this._stash(repository);
	}


@command('git.stashIncludeUntracked', { repository: true })
	stashIncludeUntracked(repository: Repository): Promise<void> {
		return this._stash(repository, true);
	}


@command('git.stashPop', { repository: true })
	async stashPop(repository: Repository): Promise<void> {
		const placeHolder = localize('pick stash to pop', "Pick a stash to pop");
		const stash = await this.pickStash(repository, placeHolder);

		if (!stash) {
			return;
		}

		await repository.popStash(stash.index);
	}


@command('git.stashPopLatest', { repository: true })
	async stashPopLatest(repository: Repository): Promise<void> {
		const stashes = await repository.getStashes();

		if (stashes.length === 0) {
			window.showInformationMessage(localize('no stashes', "There are no stashes in the repository."));
			return;
		}

		await repository.popStash();
	}


@command('git.stashApply', { repository: true })
	async stashApply(repository: Repository): Promise<void> {
		const placeHolder = localize('pick stash to apply', "Pick a stash to apply");
		const stash = await this.pickStash(repository, placeHolder);

		if (!stash) {
			return;
		}

		await repository.applyStash(stash.index);
	}


@command('git.stashApplyLatest', { repository: true })
	async stashApplyLatest(repository: Repository): Promise<void> {
		const stashes = await repository.getStashes();

		if (stashes.length === 0) {
			window.showInformationMessage(localize('no stashes', "There are no stashes in the repository."));
			return;
		}

		await repository.applyStash();
	}


@command('git.stashDrop', { repository: true })
	async stashDrop(repository: Repository): Promise<void> {
		const placeHolder = localize('pick stash to drop', "Pick a stash to drop");
		const stash = await this.pickStash(repository, placeHolder);

		if (!stash) {
			return;
		}

		// request confirmation for the operation
		const yes = localize('yes', "Yes");
		const result = await window.showWarningMessage(
			localize('sure drop', "Are you sure you want to drop the stash: {0}?", stash.description),
			yes
		);
		if (result !== yes) {
			return;
		}

		await repository.dropStash(stash.index);
	}


@command('git.stashDropAll', { repository: true })
	async stashDropAll(repository: Repository): Promise<void> {
		const stashes = await repository.getStashes();

		if (stashes.length === 0) {
			window.showInformationMessage(localize('no stashes', "There are no stashes in the repository."));
			return;
		}

		// request confirmation for the operation
		const yes = localize('yes', "Yes");
		const question = stashes.length === 1 ?
			localize('drop one stash', "Are you sure you want to drop ALL stashes? There is 1 stash that will be subject to pruning, and MAY BE IMPOSSIBLE TO RECOVER.") :
			localize('drop all stashes', "Are you sure you want to drop ALL stashes? There are {0} stashes that will be subject to pruning, and MAY BE IMPOSSIBLE TO RECOVER.", stashes.length);

		const result = await window.showWarningMessage(question, yes);
		if (result !== yes) {
			return;
		}

		await repository.dropStash();
	}


private async pickStash(repository: Repository, placeHolder: string): Promise<Stash | undefined> {
		const stashes = await repository.getStashes();

		if (stashes.length === 0) {
			window.showInformationMessage(localize('no stashes', "There are no stashes in the repository."));
			return;
		}

		const picks = stashes.map(stash => ({ label: `#${stash.index}:  ${stash.description}`, description: '', details: '', stash }));
		const result = await window.showQuickPick(picks, { placeHolder });
		return result && result.stash;
	}


@command('git.timeline.openDiff', { repository: false })
	async timelineOpenDiff(item: TimelineItem, uri: Uri | undefined, _source: string) {
		const cmd = this.resolveTimelineOpenDiffCommand(
			item, uri,
			{
				preserveFocus: true,
				preview: true,
				viewColumn: ViewColumn.Active
			},
		);
		if (cmd === undefined) {
			return undefined;
		}

		return commands.executeCommand(cmd.command, ...(cmd.arguments ?? []));
	}


resolveTimelineOpenDiffCommand(item: TimelineItem, uri: Uri | undefined, options?: TextDocumentShowOptions): Command | undefined {
		if (uri === undefined || uri === null || !GitTimelineItem.is(item)) {
			return undefined;
		}

		const basename = path.basename(uri.fsPath);

		let title;
		if ((item.previousRef === 'HEAD' || item.previousRef === '~') && item.ref === '') {
			title = localize('git.title.workingTree', '{0} (Working Tree)', basename);
		}
		else if (item.previousRef === 'HEAD' && item.ref === '~') {
			title = localize('git.title.index', '{0} (Index)', basename);
		} else {
			title = localize('git.title.diffRefs', '{0} ({1}) ↔ {0} ({2})', basename, item.shortPreviousRef, item.shortRef);
		}

		return {
			command: 'vscode.diff',
			title: localize('git.timeline.openDiffCommand', "Open Comparison"),
			arguments: [toGitUri(uri, item.previousRef), item.ref === '' ? uri : toGitUri(uri, item.ref), title, options]
		};
	}


@command('git.timeline.copyCommitId', { repository: false })
	async timelineCopyCommitId(item: TimelineItem, _uri: Uri | undefined, _source: string) {
		if (!GitTimelineItem.is(item)) {
			return;
		}

		env.clipboard.writeText(item.ref);
	}


@command('git.timeline.copyCommitMessage', { repository: false })
	async timelineCopyCommitMessage(item: TimelineItem, _uri: Uri | undefined, _source: string) {
		if (!GitTimelineItem.is(item)) {
			return;
		}

		env.clipboard.writeText(item.message);
	}


@command('git.timeline.selectForCompare', { repository: false })
	async timelineSelectForCompare(item: TimelineItem, uri: Uri | undefined, _source: string) {
		if (!GitTimelineItem.is(item) || !uri) {
			return;
		}

		this._selectedForCompare = { uri, item };
		await commands.executeCommand('setContext', 'git.timeline.selectedForCompare', true);
	}


@command('git.timeline.compareWithSelected', { repository: false })
	async timelineCompareWithSelected(item: TimelineItem, uri: Uri | undefined, _source: string) {
		if (!GitTimelineItem.is(item) || !uri || !this._selectedForCompare || uri.toString() !== this._selectedForCompare.uri.toString()) {
			return;
		}

		const { item: selected } = this._selectedForCompare;

		const basename = path.basename(uri.fsPath);
		let leftTitle;
		if ((selected.previousRef === 'HEAD' || selected.previousRef === '~') && selected.ref === '') {
			leftTitle = localize('git.title.workingTree', '{0} (Working Tree)', basename);
		}
		else if (selected.previousRef === 'HEAD' && selected.ref === '~') {
			leftTitle = localize('git.title.index', '{0} (Index)', basename);
		} else {
			leftTitle = localize('git.title.ref', '{0} ({1})', basename, selected.shortRef);
		}

		let rightTitle;
		if ((item.previousRef === 'HEAD' || item.previousRef === '~') && item.ref === '') {
			rightTitle = localize('git.title.workingTree', '{0} (Working Tree)', basename);
		}
		else if (item.previousRef === 'HEAD' && item.ref === '~') {
			rightTitle = localize('git.title.index', '{0} (Index)', basename);
		} else {
			rightTitle = localize('git.title.ref', '{0} ({1})', basename, item.shortRef);
		}

		const title = localize('git.title.diff', '{0} ↔ {1}', leftTitle, rightTitle);
		await commands.executeCommand('vscode.diff', selected.ref === '' ? uri : toGitUri(uri, selected.ref), item.ref === '' ? uri : toGitUri(uri, item.ref), title);
	}


@command('git.rebaseAbort', { repository: true })
	async rebaseAbort(repository: Repository): Promise<void> {
		if (repository.rebaseCommit) {
			await repository.rebaseAbort();
		} else {
			await window.showInformationMessage(localize('no rebase', "No rebase in progress."));
		}
	}


@command('git.closeAllDiffEditors', { repository: true })
	closeDiffEditors(repository: Repository): void {
		const resources = [
			...repository.indexGroup.resourceStates.map(r => r.resourceUri.fsPath),
			...repository.workingTreeGroup.resourceStates.map(r => r.resourceUri.fsPath),
			...repository.untrackedGroup.resourceStates.map(r => r.resourceUri.fsPath)
		];

		repository.closeDiffEditors(resources, resources, true);
	}


private createCommand(id: string, key: string, method: Function, options: ScmCommandOptions): (...args: any[]) => any {
		const result = (...args: any[]) => {
			let result: Promise<any>;

			if (!options.repository) {
				result = Promise.resolve(method.apply(this, args));
			} else {
				// try to guess the repository based on the first argument
				const repository = this.model.getRepository(args[0]);
				let repositoryPromise: Promise<Repository | undefined>;

				if (repository) {
					repositoryPromise = Promise.resolve(repository);
				} else if (this.model.repositories.length === 1) {
					repositoryPromise = Promise.resolve(this.model.repositories[0]);
				} else {
					repositoryPromise = this.model.pickRepository();
				}

				result = repositoryPromise.then(repository => {
					if (!repository) {
						return Promise.resolve();
					}

					return Promise.resolve(method.apply(this, [repository, ...args.slice(1)]));
				});
			}

			/* __GDPR__
				"git.command" : {
					"command" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
				}
			*/
			this.telemetryReporter.sendTelemetryEvent('git.command', { command: id });

			return result.catch(async err => {
				const options: MessageOptions = {
					modal: true
				};

				let message: string;
				let type: 'error' | 'warning' = 'error';

				const choices = new Map<string, () => void>();
				const openOutputChannelChoice = localize('open git log', "Open Git Log");
				const outputChannelLogger = this.outputChannelLogger;
				choices.set(openOutputChannelChoice, () => outputChannelLogger.showOutputChannel());

				const showCommandOutputChoice = localize('show command output', "Show Command Output");
				if (err.stderr) {
					choices.set(showCommandOutputChoice, async () => {
						const timestamp = new Date().getTime();
						const uri = Uri.parse(`git-output:/git-error-${timestamp}`);

						let command = 'git';

						if (err.gitArgs) {
							command = `${command} ${err.gitArgs.join(' ')}`;
						} else if (err.gitCommand) {
							command = `${command} ${err.gitCommand}`;
						}

						this.commandErrors.set(uri, `> ${command}\n${err.stderr}`);

						try {
							const doc = await workspace.openTextDocument(uri);
							await window.showTextDocument(doc);
						} finally {
							this.commandErrors.delete(uri);
						}
					});
				}

				switch (err.gitErrorCode) {
					case GitErrorCodes.DirtyWorkTree:
						message = localize('clean repo', "Please clean your repository working tree before checkout.");
						break;
					case GitErrorCodes.PushRejected:
						message = localize('cant push', "Can't push refs to remote. Try running 'Pull' first to integrate your changes.");
						break;
					case GitErrorCodes.Conflict:
						message = localize('merge conflicts', "There are merge conflicts. Resolve them before committing.");
						type = 'warning';
						options.modal = false;
						break;
					case GitErrorCodes.StashConflict:
						message = localize('stash merge conflicts', "There were merge conflicts while applying the stash.");
						type = 'warning';
						options.modal = false;
						break;
					case GitErrorCodes.AuthenticationFailed: {
						const regex = /Authentication failed for '(.*)'/i;
						const match = regex.exec(err.stderr || String(err));

						message = match
							? localize('auth failed specific', "Failed to authenticate to git remote:\n\n{0}", match[1])
							: localize('auth failed', "Failed to authenticate to git remote.");
						break;
					}
					case GitErrorCodes.NoUserNameConfigured:
					case GitErrorCodes.NoUserEmailConfigured:
						message = localize('missing user info', "Make sure you configure your 'user.name' and 'user.email' in git.");
						choices.set(localize('learn more', "Learn More"), () => commands.executeCommand('vscode.open', Uri.parse('https://aka.ms/vscode-setup-git')));
						break;
					default: {
						const hint = (err.stderr || err.message || String(err))
							.replace(/^error: /mi, '')
							.replace(/^> husky.*$/mi, '')
							.split(/[\r\n]/)
							.filter((line: string) => !!line)
						[0];

						message = hint
							? localize('git error details', "Git: {0}", hint)
							: localize('git error', "Git error");

						break;
					}
				}

				if (!message) {
					console.error(err);
					return;
				}

				const allChoices = Array.from(choices.keys());
				const result = type === 'error'
					? await window.showErrorMessage(message, options, ...allChoices)
					: await window.showWarningMessage(message, options, ...allChoices);

				if (result) {
					const resultFn = choices.get(result);

					if (resultFn) {
						resultFn();
					}
				}
			});
		};

		// patch this object, so people can call methods directly
		(this as any)[key] = result;

		return result;
	}


private getSCMResource(uri?: Uri): Resource | undefined {
		uri = uri ? uri : (window.activeTextEditor && window.activeTextEditor.document.uri);

		this.outputChannelLogger.logDebug(`git.getSCMResource.uri ${uri && uri.toString()}`);

		for (const r of this.model.repositories.map(r => r.root)) {
			this.outputChannelLogger.logDebug(`repo root ${r}`);
		}

		if (!uri) {
			return undefined;
		}

		if (isGitUri(uri)) {
			const { path } = fromGitUri(uri);
			uri = Uri.file(path);
		}

		if (uri.scheme === 'file') {
			const uriString = uri.toString();
			const repository = this.model.getRepository(uri);

			if (!repository) {
				return undefined;
			}

			return repository.workingTreeGroup.resourceStates.filter(r => r.resourceUri.toString() === uriString)[0]
				|| repository.indexGroup.resourceStates.filter(r => r.resourceUri.toString() === uriString)[0];
		}
		return undefined;
	}


private runByRepository<T>(resource: Uri, fn: (repository: Repository, resource: Uri) => Promise<T>): Promise<T[]>;


private runByRepository<T>(resources: Uri[], fn: (repository: Repository, resources: Uri[]) => Promise<T>): Promise<T[]>;


private async runByRepository<T>(arg: Uri | Uri[], fn: (repository: Repository, resources: any) => Promise<T>): Promise<T[]> {
		const resources = arg instanceof Uri ? [arg] : arg;
		const isSingleResource = arg instanceof Uri;

		const groups = resources.reduce((result, resource) => {
			let repository = this.model.getRepository(resource);

			if (!repository) {
				console.warn('Could not find git repository for ', resource);
				return result;
			}

			// Could it be a submodule?
			if (pathEquals(resource.fsPath, repository.root)) {
				repository = this.model.getRepositoryForSubmodule(resource) || repository;
			}

			const tuple = result.filter(p => p.repository === repository)[0];

			if (tuple) {
				tuple.resources.push(resource);
			} else {
				result.push({ repository, resources: [resource] });
			}

			return result;
		}, [] as { repository: Repository; resources: Uri[] }[]);

		const promises = groups
			.map(({ repository, resources }) => fn(repository as Repository, isSingleResource ? resources[0] : resources));

		return Promise.all(promises);
	}


dispose(): void {
		this.disposables.forEach(d => d.dispose());
	}


export function listScreenInfo() {
  const stout = execSync(`zsh -l -c 'PATH=${PATH} displayplacer list'`);
  const result = stout
    .toString()
    .split("\n")
    .reduce<DisplayPlacerList>(
      (data, line) => {
        if (line.startsWith("displayplacer ")) {
          data.currentCommand = line;
        }
        return data;
      },
      { currentCommand: null }
    );

  console.log(result);

  return result;
}


export function switchSettings(favorite: Favorite) {
  if (!favorite.command) return;
  try {
    execSync(`zsh -l -c 'PATH=${PATH}:/opt/homebrew/bin ${favorite.command}'`);
  } catch (e: any) {
    let error = true;
    const lines = e.toString().split("\n");
    lines.forEach((line: string) => {
      console.log(line);
      if (line.search(/skipping changes for that screen/)) {
        error = false;
      }
    });

    if (error) throw new Error("Unknown");
  }
}


(data, line) => {
        if (line.startsWith("displayplacer ")) {
          data.currentCommand = line;
        }
        return data;
      }


(line: string) => {
      console.log(line);
      if (line.search(/skipping changes for that screen/)) {
        error = false;
      }
    }


(props) => {
    const { className, ...rest } = props;
    return (
        <div className={["r-footer", className].filter(Boolean).join(' ')} {...rest}>
            {props.children}
        </div>


interface FooterProps extends React.HTMLAttributes<HTMLElement> {

}


r => filterCondition.isVisible(r)


f => f.relativePriority


r => r


r => filter.isVisible(r)


(rv, v) => { rv.push(...v); return rv; }


r => this.allowedRoomsByFilters.has(r)


r => r.roomId === room.roomId ? room : r


t => !isNullOrUndefined(this.cachedRooms[t])


interface IStickyRoom {
    room: Room;
    position: number;
    tag: TagID;
}


/**
     * Awaitable version of the sticky room setter.
     * @param val The new room to sticky.
     */
public async setStickyRoom(val: Room) {
        await this.updateStickyRoom(val);
    }


public getTagSorting(tagId: TagID): SortAlgorithm {
        if (!this.sortAlgorithms) return null;
        return this.sortAlgorithms[tagId];
    }


public async setTagSorting(tagId: TagID, sort: SortAlgorithm) {
        if (!tagId) throw new Error("Tag ID must be defined");
        if (!sort) throw new Error("Algorithm must be defined");
        this.sortAlgorithms[tagId] = sort;

        const algorithm: OrderingAlgorithm = this.algorithms[tagId];
        await algorithm.setSortAlgorithm(sort);
        this._cachedRooms[tagId] = algorithm.orderedRooms;
        this.recalculateFilteredRoomsForTag(tagId); // update filter to re-sort the list
        this.recalculateStickyRoom(tagId); // update sticky room to make sure it appears if needed
    }


public getListOrdering(tagId: TagID): ListAlgorithm {
        if (!this.listAlgorithms) return null;
        return this.listAlgorithms[tagId];
    }


public async setListOrdering(tagId: TagID, order: ListAlgorithm) {
        if (!tagId) throw new Error("Tag ID must be defined");
        if (!order) throw new Error("Algorithm must be defined");
        this.listAlgorithms[tagId] = order;

        const algorithm = getListAlgorithmInstance(order, tagId, this.sortAlgorithms[tagId]);
        this.algorithms[tagId] = algorithm;

        await algorithm.setRooms(this._cachedRooms[tagId]);
        this._cachedRooms[tagId] = algorithm.orderedRooms;
        this.recalculateFilteredRoomsForTag(tagId); // update filter to re-sort the list
        this.recalculateStickyRoom(tagId); // update sticky room to make sure it appears if needed
    }


public addFilterCondition(filterCondition: IFilterCondition): void {
        // Populate the cache of the new filter
        this.allowedByFilter.set(filterCondition, this.rooms.filter(r => filterCondition.isVisible(r)));
        this.recalculateFilteredRooms();
        filterCondition.on(FILTER_CHANGED, this.handleFilterChange.bind(this));
    }


public removeFilterCondition(filterCondition: IFilterCondition): void {
        filterCondition.off(FILTER_CHANGED, this.handleFilterChange.bind(this));
        if (this.allowedByFilter.has(filterCondition)) {
            this.allowedByFilter.delete(filterCondition);
            this.recalculateFilteredRooms();

            // If we removed the last filter, tell consumers that we've "updated" our filtered
            // view. This will trick them into getting the complete room list.
            if (!this.hasFilters) {
                this.emit(LIST_UPDATED_EVENT);
            }
        }
    }


private async handleFilterChange() {
        await this.recalculateFilteredRooms();

        // re-emit the update so the list store can fire an off-cycle update if needed
        this.emit(FILTER_CHANGED);
    }


private async updateStickyRoom(val: Room) {
        try {
            return await this.doUpdateStickyRoom(val);
        } finally {
            this._lastStickyRoom = null; // clear to indicate we're done changing
        }
    }


private async doUpdateStickyRoom(val: Room) {
        // Note throughout: We need async so we can wait for handleRoomUpdate() to do its thing,
        // otherwise we risk duplicating rooms.

        if (val && !VisibilityProvider.instance.isRoomVisible(val)) {
            val = null; // the room isn't visible - lie to the rest of this function
        }

        // Set the last sticky room to indicate that we're in a change. The code throughout the
        // class can safely handle a null room, so this should be safe to do as a backup.
        this._lastStickyRoom = this._stickyRoom || <IStickyRoom>{};

        // It's possible to have no selected room. In that case, clear the sticky room
        if (!val) {
            if (this._stickyRoom) {
                const stickyRoom = this._stickyRoom.room;
                this._stickyRoom = null; // clear before we go to update the algorithm

                // Lie to the algorithm and re-add the room to the algorithm
                await this.handleRoomUpdate(stickyRoom, RoomUpdateCause.NewRoom);
                return;
            }
            return;
        }

        // When we do have a room though, we expect to be able to find it
        let tag = this.roomIdsToTags[val.roomId][0];
        if (!tag) throw new Error(`${val.roomId} does not belong to a tag and cannot be sticky`);

        // We specifically do NOT use the ordered rooms set as it contains the sticky room, which
        // means we'll be off by 1 when the user is switching rooms. This leads to visual jumping
        // when the user is moving south in the list (not north, because of math).
        const tagList = this.getOrderedRoomsWithoutSticky()[tag] || []; // can be null if filtering
        let position = tagList.indexOf(val);

        // We do want to see if a tag change happened though - if this did happen then we'll want
        // to force the position to zero (top) to ensure we can properly handle it.
        const wasSticky = this._lastStickyRoom.room ? this._lastStickyRoom.room.roomId === val.roomId : false;
        if (this._lastStickyRoom.tag && tag !== this._lastStickyRoom.tag && wasSticky && position < 0) {
            console.warn(`Sticky room ${val.roomId} changed tags during sticky room handling`);
            position = 0;
        }

        // Sanity check the position to make sure the room is qualified for being sticky
        if (position < 0) throw new Error(`${val.roomId} does not appear to be known and cannot be sticky`);

        // 🐉 Here be dragons.
        // Before we can go through with lying to the underlying algorithm about a room
        // we need to ensure that when we do we're ready for the inevitable sticky room
        // update we'll receive. To prepare for that, we first remove the sticky room and
        // recalculate the state ourselves so that when the underlying algorithm calls for
        // the same thing it no-ops. After we're done calling the algorithm, we'll issue
        // a new update for ourselves.
        const lastStickyRoom = this._stickyRoom;
        this._stickyRoom = null; // clear before we update the algorithm
        this.recalculateStickyRoom();

        // When we do have the room, re-add the old room (if needed) to the algorithm
        // and remove the sticky room from the algorithm. This is so the underlying
        // algorithm doesn't try and confuse itself with the sticky room concept.
        // We don't add the new room if the sticky room isn't changing because that's
        // an easy way to cause duplication. We have to do room ID checks instead of
        // referential checks as the references can differ through the lifecycle.
        if (lastStickyRoom && lastStickyRoom.room && lastStickyRoom.room.roomId !== val.roomId) {
            // Lie to the algorithm and re-add the room to the algorithm
            await this.handleRoomUpdate(lastStickyRoom.room, RoomUpdateCause.NewRoom);
        }
        // Lie to the algorithm and remove the room from it's field of view
        await this.handleRoomUpdate(val, RoomUpdateCause.RoomRemoved);

        // Check for tag & position changes while we're here. We also check the room to ensure
        // it is still the same room.
        if (this._stickyRoom) {
            if (this._stickyRoom.room !== val) {
                // Check the room IDs just in case
                if (this._stickyRoom.room.roomId === val.roomId) {
                    console.warn("Sticky room changed references");
                } else {
                    throw new Error("Sticky room changed while the sticky room was changing");
                }
            }

            console.warn(`Sticky room changed tag & position from ${tag} / ${position} `
                + `to ${this._stickyRoom.tag} / ${this._stickyRoom.position}`);

            tag = this._stickyRoom.tag;
            position = this._stickyRoom.position;
        }

        // Now that we're done lying to the algorithm, we need to update our position
        // marker only if the user is moving further down the same list. If they're switching
        // lists, or moving upwards, the position marker will splice in just fine but if
        // they went downwards in the same list we'll be off by 1 due to the shifting rooms.
        if (lastStickyRoom && lastStickyRoom.tag === tag && lastStickyRoom.position <= position) {
            position++;
        }

        this._stickyRoom = {
            room: val,
            position: position,
            tag: tag,
        };

        // We update the filtered rooms just in case, as otherwise users will end up visiting
        // a room while filtering and it'll disappear. We don't update the filter earlier in
        // this function simply because we don't have to.
        this.recalculateFilteredRoomsForTag(tag);
        if (lastStickyRoom && lastStickyRoom.tag !== tag) this.recalculateFilteredRoomsForTag(lastStickyRoom.tag);
        this.recalculateStickyRoom();

        // Finally, trigger an update
        this.emit(LIST_UPDATED_EVENT);
    }


protected recalculateFilteredRooms() {
        if (!this.hasFilters) {
            return;
        }

        console.warn("Recalculating filtered room list");
        const filters = Array.from(this.allowedByFilter.keys());
        const orderedFilters = new ArrayUtil(filters)
            .groupBy(f => f.relativePriority)
            .orderBy(getEnumValues(FilterPriority))
            .value;
        const newMap: ITagMap = {};
        for (const tagId of Object.keys(this.cachedRooms)) {
            // Cheaply clone the rooms so we can more easily do operations on the list.
            // We optimize our lookups by trying to reduce sample size as much as possible
            // to the rooms we know will be deduped by the Set.
            const rooms = this.cachedRooms[tagId].map(r => r); // cheap clone
            this.tryInsertStickyRoomToFilterSet(rooms, tagId);
            let remainingRooms = rooms.map(r => r);
            let allowedRoomsInThisTag = [];
            let lastFilterPriority = orderedFilters[0].relativePriority;
            for (const filter of orderedFilters) {
                if (filter.relativePriority !== lastFilterPriority) {
                    // Every time the filter changes priority, we want more specific filtering.
                    // To accomplish that, reset the variables to make it look like the process
                    // has started over, but using the filtered rooms as the seed.
                    remainingRooms = allowedRoomsInThisTag;
                    allowedRoomsInThisTag = [];
                    lastFilterPriority = filter.relativePriority;
                }
                const filteredRooms = remainingRooms.filter(r => filter.isVisible(r));
                for (const room of filteredRooms) {
                    const idx = remainingRooms.indexOf(room);
                    if (idx >= 0) remainingRooms.splice(idx, 1);
                    allowedRoomsInThisTag.push(room);
                }
            }
            newMap[tagId] = allowedRoomsInThisTag;

            if (SettingsStore.getValue("advancedRoomListLogging")) {
                // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
                console.log(`[DEBUG] ${newMap[tagId].length}/${rooms.length} rooms filtered into ${tagId}`);
            }
        }

        const allowedRooms = Object.values(newMap).reduce((rv, v) => { rv.push(...v); return rv; }, <Room[]>[]);
        this.allowedRoomsByFilters = new Set(allowedRooms);
        this.filteredRooms = newMap;
        this.emit(LIST_UPDATED_EVENT);
    }


protected recalculateFilteredRoomsForTag(tagId: TagID): void {
        if (!this.hasFilters) return; // don't bother doing work if there's nothing to do

        if (SettingsStore.getValue("advancedRoomListLogging")) {
            // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
            console.log(`Recalculating filtered rooms for ${tagId}`);
        }
        delete this.filteredRooms[tagId];
        const rooms = this.cachedRooms[tagId].map(r => r); // cheap clone
        this.tryInsertStickyRoomToFilterSet(rooms, tagId);
        const filteredRooms = rooms.filter(r => this.allowedRoomsByFilters.has(r));
        if (filteredRooms.length > 0) {
            this.filteredRooms[tagId] = filteredRooms;
        }

        if (SettingsStore.getValue("advancedRoomListLogging")) {
            // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
            console.log(`[DEBUG] ${filteredRooms.length}/${rooms.length} rooms filtered into ${tagId}`);
        }
    }


protected tryInsertStickyRoomToFilterSet(rooms: Room[], tagId: TagID) {
        if (!this._stickyRoom || !this._stickyRoom.room || this._stickyRoom.tag !== tagId) return;

        const position = this._stickyRoom.position;
        if (position >= rooms.length) {
            rooms.push(this._stickyRoom.room);
        } else {
            rooms.splice(position, 0, this._stickyRoom.room);
        }
    }


/**
     * Recalculate the sticky room position. If this is being called in relation to
     * a specific tag being updated, it should be given to this function to optimize
     * the call.
     * @param updatedTag The tag that was updated, if possible.
     */
protected recalculateStickyRoom(updatedTag: TagID = null): void {
        // 🐉 Here be dragons.
        // This function does far too much for what it should, and is called by many places.
        // Not only is this responsible for ensuring the sticky room is held in place at all
        // times, it is also responsible for ensuring our clone of the cachedRooms is up to
        // date. If either of these desyncs, we see weird behaviour like duplicated rooms,
        // outdated lists, and other nonsensical issues that aren't necessarily obvious.

        if (!this._stickyRoom) {
            // If there's no sticky room, just do nothing useful.
            if (!!this._cachedStickyRooms) {
                // Clear the cache if we won't be needing it
                this._cachedStickyRooms = null;
                this.emit(LIST_UPDATED_EVENT);
            }
            return;
        }

        if (!this._cachedStickyRooms || !updatedTag) {
            if (SettingsStore.getValue("advancedRoomListLogging")) {
                // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
                console.log(`Generating clone of cached rooms for sticky room handling`);
            }
            const stickiedTagMap: ITagMap = {};
            for (const tagId of Object.keys(this.cachedRooms)) {
                stickiedTagMap[tagId] = this.cachedRooms[tagId].map(r => r); // shallow clone
            }
            this._cachedStickyRooms = stickiedTagMap;
        }

        if (updatedTag) {
            // Update the tag indicated by the caller, if possible. This is mostly to ensure
            // our cache is up to date.
            if (SettingsStore.getValue("advancedRoomListLogging")) {
                // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
                console.log(`Replacing cached sticky rooms for ${updatedTag}`);
            }
            this._cachedStickyRooms[updatedTag] = this.cachedRooms[updatedTag].map(r => r); // shallow clone
        }

        // Now try to insert the sticky room, if we need to.
        // We need to if there's no updated tag (we regenned the whole cache) or if the tag
        // we might have updated from the cache is also our sticky room.
        const sticky = this._stickyRoom;
        if (!updatedTag || updatedTag === sticky.tag) {
            if (SettingsStore.getValue("advancedRoomListLogging")) {
                // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
                console.log(
                    `Inserting sticky room ${sticky.room.roomId} at position ${sticky.position} in ${sticky.tag}`,
                );
            }
            this._cachedStickyRooms[sticky.tag].splice(sticky.position, 0, sticky.room);
        }

        // Finally, trigger an update
        this.emit(LIST_UPDATED_EVENT);
    }


/**
     * Asks the Algorithm to regenerate all lists, using the tags given
     * as reference for which lists to generate and which way to generate
     * them.
     * @param {ITagSortingMap} tagSortingMap The tags to generate.
     * @param {IListOrderingMap} listOrderingMap The ordering of those tags.
     * @returns {Promise<*>} A promise which resolves when complete.
     */
public async populateTags(tagSortingMap: ITagSortingMap, listOrderingMap: IListOrderingMap): Promise<any> {
        if (!tagSortingMap) throw new Error(`Sorting map cannot be null or empty`);
        if (!listOrderingMap) throw new Error(`Ordering ma cannot be null or empty`);
        if (arrayHasDiff(Object.keys(tagSortingMap), Object.keys(listOrderingMap))) {
            throw new Error(`Both maps must contain the exact same tags`);
        }
        this.sortAlgorithms = tagSortingMap;
        this.listAlgorithms = listOrderingMap;
        this.algorithms = {};
        for (const tag of Object.keys(tagSortingMap)) {
            this.algorithms[tag] = getListAlgorithmInstance(this.listAlgorithms[tag], tag, this.sortAlgorithms[tag]);
        }
        return this.setKnownRooms(this.rooms);
    }


/**
     * Gets an ordered set of rooms for the all known tags, filtered.
     * @returns {ITagMap} The cached list of rooms, ordered,
     * for each tag. May be empty, but never null/undefined.
     */
public getOrderedRooms(): ITagMap {
        if (!this.hasFilters) {
            return this._cachedStickyRooms || this.cachedRooms;
        }
        return this.filteredRooms;
    }


public getUnfilteredRooms(): ITagMap {
        return this._cachedStickyRooms || this.cachedRooms;
    }


/**
     * This returns the same as getOrderedRooms(), but without the sticky room
     * map as it causes issues for sticky room handling (see sticky room handling
     * for more information).
     * @returns {ITagMap} The cached list of rooms, ordered,
     * for each tag. May be empty, but never null/undefined.
     */
private getOrderedRoomsWithoutSticky(): ITagMap {
        if (!this.hasFilters) {
            return this.cachedRooms;
        }
        return this.filteredRooms;
    }


/**
     * Seeds the Algorithm with a set of rooms. The algorithm will discard all
     * previously known information and instead use these rooms instead.
     * @param {Room[]} rooms The rooms to force the algorithm to use.
     * @returns {Promise<*>} A promise which resolves when complete.
     */
public async setKnownRooms(rooms: Room[]): Promise<any> {
        if (isNullOrUndefined(rooms)) throw new Error(`Array of rooms cannot be null`);
        if (!this.sortAlgorithms) throw new Error(`Cannot set known rooms without a tag sorting map`);

        console.warn("Resetting known rooms, initiating regeneration");

        // Before we go any further we need to clear (but remember) the sticky room to
        // avoid accidentally duplicating it in the list.
        const oldStickyRoom = this._stickyRoom;
        await this.updateStickyRoom(null);

        this.rooms = rooms;

        const newTags: ITagMap = {};
        for (const tagId in this.sortAlgorithms) {
            // noinspection JSUnfilteredForInLoop
            newTags[tagId] = [];
        }

        // If we can avoid doing work, do so.
        if (!rooms.length) {
            await this.generateFreshTags(newTags); // just in case it wants to do something
            this.cachedRooms = newTags;
            return;
        }

        // Split out the easy rooms first (leave and invite)
        const memberships = splitRoomsByMembership(rooms);
        for (const room of memberships[EffectiveMembership.Invite]) {
            newTags[DefaultTagID.Invite].push(room);
        }
        for (const room of memberships[EffectiveMembership.Leave]) {
            newTags[DefaultTagID.Archived].push(room);
        }

        // Now process all the joined rooms. This is a bit more complicated
        for (const room of memberships[EffectiveMembership.Join]) {
            const tags = this.getTagsOfJoinedRoom(room);
            let inTag = false;
            if (tags.length > 0) {
                for (const tag of tags) {
                    if (!isNullOrUndefined(newTags[tag])) {
                        if (tag === DefaultTagID.ServerNotice) {
                            newTags[DefaultTagID.DM].push(room);
                        } else {
                            newTags[tag].push(room);
                        }
                        inTag = true;
                    }
                }
            }

            if (!inTag) {
                if (DMRoomMap.shared().getUserIdForRoomId(room.roomId)) {
                    newTags[DefaultTagID.DM].push(room);
                } else {
                    newTags[DefaultTagID.Untagged].push(room);
                }
            }
        }

        await this.generateFreshTags(newTags);

        this.cachedRooms = newTags;
        this.updateTagsFromCache();
        this.recalculateFilteredRooms();

        // Now that we've finished generation, we need to update the sticky room to what
        // it was. It's entirely possible that it changed lists though, so if it did then
        // we also have to update the position of it.
        if (oldStickyRoom && oldStickyRoom.room) {
            await this.updateStickyRoom(oldStickyRoom.room);
            if (this._stickyRoom && this._stickyRoom.room) { // just in case the update doesn't go according to plan
                if (this._stickyRoom.tag !== oldStickyRoom.tag) {
                    // We put the sticky room at the top of the list to treat it as an obvious tag change.
                    this._stickyRoom.position = 0;
                    this.recalculateStickyRoom(this._stickyRoom.tag);
                }
            }
        }
    }


public getTagsForRoom(room: Room): TagID[] {
        const tags: TagID[] = [];

        const membership = getEffectiveMembership(room.getMyMembership());
        if (membership === EffectiveMembership.Invite) {
            tags.push(DefaultTagID.Invite);
        } else if (membership === EffectiveMembership.Leave) {
            tags.push(DefaultTagID.Archived);
        } else {
            tags.push(...this.getTagsOfJoinedRoom(room));
        }

        if (!tags.length) tags.push(DefaultTagID.Untagged);

        return tags;
    }


private getTagsOfJoinedRoom(room: Room): TagID[] {
        let tags = Object.keys(room.tags || {});

        if (tags.length === 0) {
            // Check to see if it's a DM if it isn't anything else
            if (DMRoomMap.shared().getUserIdForRoomId(room.roomId)) {
                tags = [DefaultTagID.DM];
            }
        }

        return tags;
    }


/**
     * Updates the roomsToTags map
     */
private updateTagsFromCache() {
        const newMap = {};

        const tags = Object.keys(this.cachedRooms);
        for (const tagId of tags) {
            const rooms = this.cachedRooms[tagId];
            for (const room of rooms) {
                if (!newMap[room.roomId]) newMap[room.roomId] = [];
                newMap[room.roomId].push(tagId);
            }
        }

        this.roomIdsToTags = newMap;
    }


/**
     * Called when the Algorithm believes a complete regeneration of the existing
     * lists is needed.
     * @param {ITagMap} updatedTagMap The tag map which needs populating. Each tag
     * will already have the rooms which belong to it - they just need ordering. Must
     * be mutated in place.
     * @returns {Promise<*>} A promise which resolves when complete.
     */
private async generateFreshTags(updatedTagMap: ITagMap): Promise<any> {
        if (!this.algorithms) throw new Error("Not ready: no algorithms to determine tags from");

        for (const tag of Object.keys(updatedTagMap)) {
            const algorithm: OrderingAlgorithm = this.algorithms[tag];
            if (!algorithm) throw new Error(`No algorithm for ${tag}`);

            await algorithm.setRooms(updatedTagMap[tag]);
            updatedTagMap[tag] = algorithm.orderedRooms;
        }
    }


() => {
      this.table('profile').add({
        directory: 1
      });
      this.table('directory').bulkAdd([{ id: 1 }, { id: 2 }]);
      this.table('item').bulkAdd(items);
      this.table('file').bulkAdd([
        {
          id: 1,
          path: '::directory/subdirectory/subsubdirectory/',
          file_name: 'filename1.jpg',
          size: 4576543,
          format: FileFormatType.JPG,
          type: 'jpg'
        },
        {
          id: 2,
          path: '::directory/subdirectory/subsubdirectory/',
          file_name: 'filename2.jpg',
          size: 4576543,
          format: FileFormatType.JPG,
          type: 'jpg'
        },
        {
          id: 3,
          path: '::directory/subdirectory/subsubdirectory/',
          file_name: 'filename3.pdf',
          size: 4576543,
          format: FileFormatType.PDF,
          type: 'jpg'
        }
      ]);
    }


() => {
  let dexie: DexieService;
  let item: ItemService;
  let file: FileService;
  let mockDatabase: DATABASE;

  beforeEach(async () => {
    mockDatabase = new DATABASE();
    const bed = TestBed.configureTestingModule(testBedSetup);
    TestBed.overrideProvider(DexieService, { useValue: mockDatabase });
    dexie = bed.get(DexieService);
    item = bed.get(ItemService);
    file = bed.get(FileService);
  });

  afterAll( async() => {
    mockDatabase.delete();
    mockDatabase = new DATABASE();
    TestBed.overrideProvider(DexieService, {useValue: mockDatabase});
  });

  it('GIVEN three items in a directory THEN it should retrieve all the items', async () => {
    const items = await item.getItemsByDirectoryId(1);
    expect(items.length).toBe(3);
    expect(items[0].document_type).toBe(PortfolioType.DIAGNOSIS);
    expect(items[1].document_type).toBe(PortfolioType.DIAGNOSIS);
    expect(items[2].document_type).toBe(PortfolioType.DIAGNOSIS);
  });

  it('GIVEN an invalid directory ID THEN it should retrieve no items', async () => {
    const items = await item.getItemsByDirectoryId(88);
    expect(items.length).toBe(0);
  });

  it('GIVEN a diary entry with an attached file THEN it should create an item', async () => {
    const countBefore = await mockDatabase.table('item').count();
    const diaryFile: NativeFile = {
      id: 4,
      path: '::directory/subdirectory/subsubdirectory/',
      format: FileFormatType.JPG,
      file_name: 'dry_skin.jpg',
      size: 808
    };
    const creation_date = moment().format('YYYY-MM-DD');
    const document_name = 'Dry Skin on Arm';
    const specificValues = {
      page: PageType.Diary
    };
    const created_item = await item.createItemAsFile(
      diaryFile,
      creation_date,
      1,
      document_name,
      specificValues
    );

    expect(created_item.directory_id).toBe(1);
    expect(created_item.page).toBe(specificValues.page);
    expect(created_item.title).toBe(document_name);
    expect(created_item.file).toBe(diaryFile);
    expect(created_item.file_id).toBe(diaryFile.id);
    expect(created_item.chosen_date).toBe(creation_date);
    const countAfter = await mockDatabase.table('item').count();
    expect(countAfter).toBe(countBefore + 1);
  });

  it('GIVEN a medical document THEN it should create an item', async () => {
    const countBefore = await mockDatabase.table('item').count();
    const medicalDoc: NativeFile = {
      id: 5,
      path: '::directory/subdirectory/subsubdirectory2/',
      format: FileFormatType.PDF,
      file_name: 'lab_test.jpg',
      size: 8869
    };
    const creation_date = moment().format('YYYY-MM-DD');
    const document_name = 'Blood Test';
    const specificValues = {
      page: PageType.Portfolio
    };
    const created_item = await item.createItemAsFile(
      medicalDoc,
      creation_date,
      1,
      document_name,
      specificValues
    );

    expect(created_item.directory_id).toBe(1);
    expect(created_item.page).toBe(specificValues.page);
    expect(created_item.title).toBe(document_name);
    expect(created_item.file).toBe(medicalDoc);
    expect(created_item.file_id).toBe(medicalDoc.id);
    expect(created_item.chosen_date).toBe(creation_date);
    const countAfter = await mockDatabase.table('item').count();
    expect(countAfter).toBe(countBefore + 1);
  });

  it('GIVEN a diary entry without an attached file THEN it should create an item', async () => {
    const countBefore = await mockDatabase.table('item').count();
    const new_item: Item = {
      title: 'Blood Pressure Measurement',
      description: 'Morning measurement',
      chosen_date: moment().format('YYYY-MM-DD'),
      page: PageType.Diary,
      directory_id: 1
    };
    const created_item = await item.addItemToDB(new_item);

    expect(created_item.directory_id).toBe(1);
    expect(created_item.page).toBe(new_item.page);
    expect(created_item.title).toBe(new_item.title);
    expect(created_item.file).toBe(undefined);
    expect(created_item.file_id).toBe(undefined);
    expect(created_item.chosen_date).toBe(new_item.chosen_date);
    const countAfter = await mockDatabase.table('item').count();
    expect(countAfter).toBe(countBefore + 1);
  });
}


async () => {
    mockDatabase = new DATABASE();
    const bed = TestBed.configureTestingModule(testBedSetup);
    TestBed.overrideProvider(DexieService, { useValue: mockDatabase });
    dexie = bed.get(DexieService);
    item = bed.get(ItemService);
    file = bed.get(FileService);
  }


async() => {
    mockDatabase.delete();
    mockDatabase = new DATABASE();
    TestBed.overrideProvider(DexieService, {useValue: mockDatabase});
  }


async () => {
    const items = await item.getItemsByDirectoryId(1);
    expect(items.length).toBe(3);
    expect(items[0].document_type).toBe(PortfolioType.DIAGNOSIS);
    expect(items[1].document_type).toBe(PortfolioType.DIAGNOSIS);
    expect(items[2].document_type).toBe(PortfolioType.DIAGNOSIS);
  }


async () => {
    const items = await item.getItemsByDirectoryId(88);
    expect(items.length).toBe(0);
  }