Git - Optimistic UI plumbing (#165237)

This commit is contained in:
Ladislau Szomoru 2022-11-04 12:52:28 +01:00 committed by GitHub
parent 2423a7a714
commit b208b8794d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 138 additions and 91 deletions

View file

@ -2543,6 +2543,15 @@
"default": false,
"markdownDescription": "%config.mergeEditor%",
"scope": "window"
},
"git.optimisticUpdate": {
"type": "boolean",
"default": true,
"markdownDescription": "%config.optimisticUpdate%",
"scope": "resource",
"tags": [
"experimental"
]
}
}
},

View file

@ -236,6 +236,7 @@
"config.repositoryScanMaxDepth": "Controls the depth used when scanning workspace folders for Git repositories when `#git.autoRepositoryDetection#` is set to `true` or `subFolders`. Can be set to `-1` for no limit.",
"config.useIntegratedAskPass": "Controls whether GIT_ASKPASS should be overwritten to use the integrated version.",
"config.mergeEditor": "Open the merge editor for files that are currently under conflict.",
"config.optimisticUpdate": "Controls whether to optimistically update the state of the Source Control view after running git commands.",
"submenu.explorer": "Git",
"submenu.commit": "Commit",
"submenu.commit.amend": "Amend",

View file

@ -1571,7 +1571,7 @@ export class CommandCenter {
repository: Repository,
getCommitMessage: () => Promise<string | undefined>,
opts: CommitOptions
): Promise<boolean> {
): Promise<void> {
const config = workspace.getConfiguration('git', Uri.file(repository.root));
let promptToSaveFilesBeforeCommit = config.get<'always' | 'staged' | 'never'>('promptToSaveFilesBeforeCommit');
@ -1611,7 +1611,7 @@ export class CommandCenter {
noStagedChanges = repository.indexGroup.resourceStates.length === 0;
noUnstagedChanges = repository.workingTreeGroup.resourceStates.length === 0;
} else if (pick !== commit) {
return false; // do not commit on cancel
return; // do not commit on cancel
}
}
}
@ -1621,7 +1621,7 @@ export class CommandCenter {
const suggestSmartCommit = config.get<boolean>('suggestSmartCommit') === true;
if (!suggestSmartCommit) {
return false;
return;
}
// prompt the user if we want to commit all or not
@ -1635,9 +1635,9 @@ export class CommandCenter {
config.update('enableSmartCommit', true, true);
} else if (pick === never) {
config.update('suggestSmartCommit', false, true);
return false;
return;
} else if (pick !== yes) {
return false; // do not commit on cancel
return; // do not commit on cancel
}
}
@ -1683,7 +1683,7 @@ export class CommandCenter {
const answer = await window.showInformationMessage(l10n.t('There are no changes to commit.'), commitAnyway);
if (answer !== commitAnyway) {
return false;
return;
}
opts.empty = true;
@ -1692,7 +1692,7 @@ export class CommandCenter {
if (opts.noVerify) {
if (!config.get<boolean>('allowNoVerifyCommit')) {
await window.showErrorMessage(l10n.t('Commits without verification are not allowed, please enable them with the "git.allowNoVerifyCommit" setting.'));
return false;
return;
}
if (config.get<boolean>('confirmNoVerifyCommit')) {
@ -1704,7 +1704,7 @@ export class CommandCenter {
if (pick === neverAgain) {
config.update('confirmNoVerifyCommit', false, true);
} else if (pick !== yes) {
return false;
return;
}
}
}
@ -1712,7 +1712,7 @@ export class CommandCenter {
const message = await getCommitMessage();
if (!message && !opts.amend && !opts.useEditor) {
return false;
return;
}
if (opts.all && smartCommitChanges === 'tracked') {
@ -1738,12 +1738,12 @@ export class CommandCenter {
}
if (!pick) {
return false;
return;
} else if (pick === commitToNewBranch) {
const branchName = await this.promptForBranchName(repository);
if (!branchName) {
return false;
return;
}
await repository.branch(branchName, true);
@ -1751,8 +1751,6 @@ export class CommandCenter {
}
await repository.commit(message, opts);
return true;
}
private async commitWithAnyInput(repository: Repository, opts: CommitOptions): Promise<void> {
@ -1790,11 +1788,7 @@ export class CommandCenter {
return _message;
};
const didCommit = await this.smartCommit(repository, getCommitMessage, opts);
if (message && didCommit) {
repository.inputBox.value = await repository.getInputTemplate();
}
await this.smartCommit(repository, getCommitMessage, opts);
}
@command('git.commit', { repository: true })

View file

@ -447,6 +447,13 @@ export interface GitResourceGroup extends SourceControlResourceGroup {
resourceStates: Resource[];
}
interface GitResourceGroups {
indexGroup?: Resource[];
mergeGroup?: Resource[];
untrackedGroup?: Resource[];
workingTreeGroup?: Resource[];
}
export interface OperationResult {
operation: Operation;
error: any;
@ -974,7 +981,7 @@ export class Repository implements Disposable {
|| e.affectsConfiguration('git.ignoreSubmodules', root)
|| e.affectsConfiguration('git.openDiffOnClick', root)
|| e.affectsConfiguration('git.showActionButton', root)
)(this.updateModelState, this, this.disposables);
)(() => this.updateModelState(), this, this.disposables);
const updateInputBoxVisibility = () => {
const config = workspace.getConfiguration('git', root);
@ -1247,40 +1254,53 @@ export class Repository implements Disposable {
}
async commit(message: string | undefined, opts: CommitOptions = Object.create(null)): Promise<void> {
const indexResources = [...this.indexGroup.resourceStates.map(r => r.resourceUri.fsPath)];
const workingGroupResources = opts.all && opts.all !== 'tracked' ?
[...this.workingTreeGroup.resourceStates.map(r => r.resourceUri.fsPath)] : [];
if (this.rebaseCommit) {
await this.run(Operation.RebaseContinue, async () => {
if (opts.all) {
const addOpts = opts.all === 'tracked' ? { update: true } : {};
await this.repository.add([], addOpts);
}
await this.run(
Operation.RebaseContinue,
async () => {
if (opts.all) {
const addOpts = opts.all === 'tracked' ? { update: true } : {};
await this.repository.add([], addOpts);
}
await this.repository.rebaseContinue();
this.closeDiffEditors(indexResources, workingGroupResources);
});
await this.repository.rebaseContinue();
await this.commitOperationCleanup(message, opts);
});
} else {
// Set post-commit command to render the correct action button
this.commitCommandCenter.postCommitCommand = opts.postCommitCommand;
await this.run(Operation.Commit, async () => {
if (opts.all) {
const addOpts = opts.all === 'tracked' ? { update: true } : {};
await this.repository.add([], addOpts);
}
await this.run(
Operation.Commit,
async () => {
if (opts.all) {
const addOpts = opts.all === 'tracked' ? { update: true } : {};
await this.repository.add([], addOpts);
}
delete opts.all;
delete opts.all;
if (opts.requireUserConfig === undefined || opts.requireUserConfig === null) {
const config = workspace.getConfiguration('git', Uri.file(this.root));
opts.requireUserConfig = config.get<boolean>('requireGitUserConfig');
}
if (opts.requireUserConfig === undefined || opts.requireUserConfig === null) {
const config = workspace.getConfiguration('git', Uri.file(this.root));
opts.requireUserConfig = config.get<boolean>('requireGitUserConfig');
}
await this.repository.commit(message, opts);
this.closeDiffEditors(indexResources, workingGroupResources);
});
await this.repository.commit(message, opts);
await this.commitOperationCleanup(message, opts);
},
(): GitResourceGroups => {
let untrackedGroup: Resource[] | undefined = undefined,
workingTreeGroup: Resource[] | undefined = undefined;
if (opts.all === 'tracked') {
workingTreeGroup = this.workingTreeGroup.resourceStates
.filter(r => r.type === Status.UNTRACKED);
} else if (opts.all) {
untrackedGroup = workingTreeGroup = [];
}
return { indexGroup: [], untrackedGroup, workingTreeGroup };
});
// Execute post-commit command
await this.run(Operation.PostCommitCommand, async () => {
@ -1289,6 +1309,18 @@ export class Repository implements Disposable {
}
}
private async commitOperationCleanup(message: string | undefined, opts: CommitOptions) {
if (message) {
this.inputBox.value = await this.getInputTemplate();
}
const indexResources = [...this.indexGroup.resourceStates.map(r => r.resourceUri.fsPath)];
const workingGroupResources = opts.all && opts.all !== 'tracked' ?
[...this.workingTreeGroup.resourceStates.map(r => r.resourceUri.fsPath)] : [];
this.closeDiffEditors(indexResources, workingGroupResources);
}
async clean(resources: Uri[]): Promise<void> {
await this.run(Operation.Clean, async () => {
const toClean: string[] = [];
@ -1869,7 +1901,10 @@ export class Repository implements Disposable {
}
}
private async run<T>(operation: Operation, runOperation: () => Promise<T> = () => Promise.resolve<any>(null)): Promise<T> {
private async run<T>(
operation: Operation,
runOperation: () => Promise<T> = () => Promise.resolve<any>(null),
getOptimisticResourceGroups: () => GitResourceGroups | undefined = () => undefined): Promise<T> {
if (this.state !== RepositoryState.Idle) {
throw new Error('Repository not initialized');
}
@ -1883,7 +1918,10 @@ export class Repository implements Disposable {
const result = await this.retryRun(operation, runOperation);
if (!isReadOnly(operation)) {
await this.updateModelState();
const scopedConfig = workspace.getConfiguration('git', Uri.file(this.repository.root));
const optimisticUpdate = scopedConfig.get<boolean>('optimisticUpdate') === true;
await this.updateModelState(optimisticUpdate ? getOptimisticResourceGroups() : undefined);
}
return result;
@ -1942,18 +1980,14 @@ export class Repository implements Disposable {
return folderPaths.filter(p => !ignored.has(p));
}
private async updateModelState() {
private async updateModelState(optimisticResourcesGroups?: GitResourceGroups) {
this.updateModelStateCancellationTokenSource?.cancel();
this.updateModelStateCancellationTokenSource = new CancellationTokenSource();
await this._updateModelState(this.updateModelStateCancellationTokenSource.token);
await this._updateModelState(optimisticResourcesGroups, this.updateModelStateCancellationTokenSource.token);
}
private async _updateModelState(cancellationToken?: CancellationToken): Promise<void> {
if (cancellationToken && cancellationToken.isCancellationRequested) {
return;
}
private async _updateModelState(optimisticResourcesGroups?: GitResourceGroups, cancellationToken?: CancellationToken): Promise<void> {
try {
const config = workspace.getConfiguration('git');
let sort = config.get<'alphabetically' | 'committerdate'>('branchSortOrder') || 'alphabetically';
@ -1961,13 +1995,12 @@ export class Repository implements Disposable {
sort = 'alphabetically';
}
const [HEAD, refs, remotes, submodules, status, rebaseCommit, mergeInProgress, commitTemplate] =
const [HEAD, refs, remotes, submodules, rebaseCommit, mergeInProgress, commitTemplate] =
await Promise.all([
this.repository.getHEADBranch(),
this.repository.getRefs({ sort }),
this.repository.getRemotes(),
this.repository.getSubmodules(),
this.getStatus(cancellationToken),
this.getRebaseCommit(),
this.isMergeInProgress(),
this.getInputTemplate()]);
@ -1979,18 +2012,15 @@ export class Repository implements Disposable {
this.rebaseCommit = rebaseCommit;
this.mergeInProgress = mergeInProgress;
// set resource groups
this.mergeGroup.resourceStates = status.merge;
this.indexGroup.resourceStates = status.index;
this.workingTreeGroup.resourceStates = status.workingTree;
this.untrackedGroup.resourceStates = status.untracked;
// set count badge
this.setCountBadge();
this._onDidChangeStatus.fire();
this._sourceControl.commitTemplate = commitTemplate;
// Optimistically update the resource states
if (optimisticResourcesGroups) {
this._updateResourceGroupsState(optimisticResourcesGroups);
}
// Update resource states based on status information
this._updateResourceGroupsState(await this.getStatus(cancellationToken));
}
catch (err) {
if (err instanceof CancellationError) {
@ -2001,7 +2031,20 @@ export class Repository implements Disposable {
}
}
private async getStatus(cancellationToken?: CancellationToken): Promise<{ index: Resource[]; workingTree: Resource[]; merge: Resource[]; untracked: Resource[] }> {
private _updateResourceGroupsState(resourcesGroups: GitResourceGroups): void {
// set resource groups
if (resourcesGroups.indexGroup) { this.indexGroup.resourceStates = resourcesGroups.indexGroup; }
if (resourcesGroups.mergeGroup) { this.mergeGroup.resourceStates = resourcesGroups.mergeGroup; }
if (resourcesGroups.untrackedGroup) { this.untrackedGroup.resourceStates = resourcesGroups.untrackedGroup; }
if (resourcesGroups.workingTreeGroup) { this.workingTreeGroup.resourceStates = resourcesGroups.workingTreeGroup; }
// set count badge
this.setCountBadge();
this._onDidChangeStatus.fire();
}
private async getStatus(cancellationToken?: CancellationToken): Promise<GitResourceGroups> {
if (cancellationToken && cancellationToken.isCancellationRequested) {
throw new CancellationError();
}
@ -2088,10 +2131,10 @@ export class Repository implements Disposable {
}
}
const index: Resource[] = [],
workingTree: Resource[] = [],
merge: Resource[] = [],
untracked: Resource[] = [];
const indexGroup: Resource[] = [],
mergeGroup: Resource[] = [],
untrackedGroup: Resource[] = [],
workingTreeGroup: Resource[] = [];
status.forEach(raw => {
const uri = Uri.file(path.join(this.repository.root, raw.path));
@ -2101,42 +2144,42 @@ export class Repository implements Disposable {
switch (raw.x + raw.y) {
case '??': switch (untrackedChanges) {
case 'mixed': return workingTree.push(new Resource(this.resourceCommandResolver, ResourceGroupType.WorkingTree, uri, Status.UNTRACKED, useIcons));
case 'separate': return untracked.push(new Resource(this.resourceCommandResolver, ResourceGroupType.Untracked, uri, Status.UNTRACKED, useIcons));
case 'mixed': return workingTreeGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.WorkingTree, uri, Status.UNTRACKED, useIcons));
case 'separate': return untrackedGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.Untracked, uri, Status.UNTRACKED, useIcons));
default: return undefined;
}
case '!!': switch (untrackedChanges) {
case 'mixed': return workingTree.push(new Resource(this.resourceCommandResolver, ResourceGroupType.WorkingTree, uri, Status.IGNORED, useIcons));
case 'separate': return untracked.push(new Resource(this.resourceCommandResolver, ResourceGroupType.Untracked, uri, Status.IGNORED, useIcons));
case 'mixed': return workingTreeGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.WorkingTree, uri, Status.IGNORED, useIcons));
case 'separate': return untrackedGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.Untracked, uri, Status.IGNORED, useIcons));
default: return undefined;
}
case 'DD': return merge.push(new Resource(this.resourceCommandResolver, ResourceGroupType.Merge, uri, Status.BOTH_DELETED, useIcons));
case 'AU': return merge.push(new Resource(this.resourceCommandResolver, ResourceGroupType.Merge, uri, Status.ADDED_BY_US, useIcons));
case 'UD': return merge.push(new Resource(this.resourceCommandResolver, ResourceGroupType.Merge, uri, Status.DELETED_BY_THEM, useIcons));
case 'UA': return merge.push(new Resource(this.resourceCommandResolver, ResourceGroupType.Merge, uri, Status.ADDED_BY_THEM, useIcons));
case 'DU': return merge.push(new Resource(this.resourceCommandResolver, ResourceGroupType.Merge, uri, Status.DELETED_BY_US, useIcons));
case 'AA': return merge.push(new Resource(this.resourceCommandResolver, ResourceGroupType.Merge, uri, Status.BOTH_ADDED, useIcons));
case 'UU': return merge.push(new Resource(this.resourceCommandResolver, ResourceGroupType.Merge, uri, Status.BOTH_MODIFIED, useIcons));
case 'DD': return mergeGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.Merge, uri, Status.BOTH_DELETED, useIcons));
case 'AU': return mergeGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.Merge, uri, Status.ADDED_BY_US, useIcons));
case 'UD': return mergeGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.Merge, uri, Status.DELETED_BY_THEM, useIcons));
case 'UA': return mergeGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.Merge, uri, Status.ADDED_BY_THEM, useIcons));
case 'DU': return mergeGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.Merge, uri, Status.DELETED_BY_US, useIcons));
case 'AA': return mergeGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.Merge, uri, Status.BOTH_ADDED, useIcons));
case 'UU': return mergeGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.Merge, uri, Status.BOTH_MODIFIED, useIcons));
}
switch (raw.x) {
case 'M': index.push(new Resource(this.resourceCommandResolver, ResourceGroupType.Index, uri, Status.INDEX_MODIFIED, useIcons)); break;
case 'A': index.push(new Resource(this.resourceCommandResolver, ResourceGroupType.Index, uri, Status.INDEX_ADDED, useIcons)); break;
case 'D': index.push(new Resource(this.resourceCommandResolver, ResourceGroupType.Index, uri, Status.INDEX_DELETED, useIcons)); break;
case 'R': index.push(new Resource(this.resourceCommandResolver, ResourceGroupType.Index, uri, Status.INDEX_RENAMED, useIcons, renameUri)); break;
case 'C': index.push(new Resource(this.resourceCommandResolver, ResourceGroupType.Index, uri, Status.INDEX_COPIED, useIcons, renameUri)); break;
case 'M': indexGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.Index, uri, Status.INDEX_MODIFIED, useIcons)); break;
case 'A': indexGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.Index, uri, Status.INDEX_ADDED, useIcons)); break;
case 'D': indexGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.Index, uri, Status.INDEX_DELETED, useIcons)); break;
case 'R': indexGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.Index, uri, Status.INDEX_RENAMED, useIcons, renameUri)); break;
case 'C': indexGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.Index, uri, Status.INDEX_COPIED, useIcons, renameUri)); break;
}
switch (raw.y) {
case 'M': workingTree.push(new Resource(this.resourceCommandResolver, ResourceGroupType.WorkingTree, uri, Status.MODIFIED, useIcons, renameUri)); break;
case 'D': workingTree.push(new Resource(this.resourceCommandResolver, ResourceGroupType.WorkingTree, uri, Status.DELETED, useIcons, renameUri)); break;
case 'A': workingTree.push(new Resource(this.resourceCommandResolver, ResourceGroupType.WorkingTree, uri, Status.INTENT_TO_ADD, useIcons, renameUri)); break;
case 'M': workingTreeGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.WorkingTree, uri, Status.MODIFIED, useIcons, renameUri)); break;
case 'D': workingTreeGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.WorkingTree, uri, Status.DELETED, useIcons, renameUri)); break;
case 'A': workingTreeGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.WorkingTree, uri, Status.INTENT_TO_ADD, useIcons, renameUri)); break;
}
return undefined;
});
return { index, workingTree, merge, untracked };
return { indexGroup, mergeGroup, untrackedGroup, workingTreeGroup };
}
private setCountBadge(): void {