From 4dea68b94682d4facdc1d87cd61980f09e8fdf29 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Fri, 16 Dec 2022 11:08:38 +0100 Subject: [PATCH] Git - Add the capability to "attach" state to operations (#169371) --- extensions/git/src/actionButton.ts | 14 +- extensions/git/src/autofetch.ts | 6 +- extensions/git/src/model.ts | 16 +- extensions/git/src/postCommitCommands.ts | 6 +- extensions/git/src/repository.ts | 287 +++++++++++++---------- extensions/git/src/statusbar.ts | 33 +-- 6 files changed, 199 insertions(+), 163 deletions(-) diff --git a/extensions/git/src/actionButton.ts b/extensions/git/src/actionButton.ts index aa49d179c8a..faf96100265 100644 --- a/extensions/git/src/actionButton.ts +++ b/extensions/git/src/actionButton.ts @@ -6,7 +6,7 @@ import { Command, Disposable, Event, EventEmitter, SourceControlActionButton, Uri, workspace, l10n } from 'vscode'; import { Branch, Status } from './api/git'; import { CommitCommandsCenter } from './postCommitCommands'; -import { Repository, Operation } from './repository'; +import { Repository, OperationKind } from './repository'; import { dispose } from './util'; interface ActionButtonState { @@ -181,14 +181,14 @@ export class ActionButtonCommand { private onDidChangeOperations(): void { const isCommitInProgress = - this.repository.operations.isRunning(Operation.Commit) || - this.repository.operations.isRunning(Operation.PostCommitCommand) || - this.repository.operations.isRunning(Operation.RebaseContinue); + this.repository.operations.isRunning(OperationKind.Commit) || + this.repository.operations.isRunning(OperationKind.PostCommitCommand) || + this.repository.operations.isRunning(OperationKind.RebaseContinue); const isSyncInProgress = - this.repository.operations.isRunning(Operation.Sync) || - this.repository.operations.isRunning(Operation.Push) || - this.repository.operations.isRunning(Operation.Pull); + this.repository.operations.isRunning(OperationKind.Sync) || + this.repository.operations.isRunning(OperationKind.Push) || + this.repository.operations.isRunning(OperationKind.Pull); this.state = { ...this.state, isCommitInProgress, isSyncInProgress }; } diff --git a/extensions/git/src/autofetch.ts b/extensions/git/src/autofetch.ts index 6c1271f24b9..836f31f475c 100644 --- a/extensions/git/src/autofetch.ts +++ b/extensions/git/src/autofetch.ts @@ -4,12 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import { workspace, Disposable, EventEmitter, Memento, window, MessageItem, ConfigurationTarget, Uri, ConfigurationChangeEvent, l10n } from 'vscode'; -import { Repository, Operation } from './repository'; +import { Repository, OperationKind } from './repository'; import { eventToPromise, filterEvent, onceEvent } from './util'; import { GitErrorCodes } from './api/git'; -function isRemoteOperation(operation: Operation): boolean { - return operation === Operation.Pull || operation === Operation.Push || operation === Operation.Sync || operation === Operation.Fetch; +function isRemoteOperation(operation: OperationKind): boolean { + return operation === OperationKind.Pull || operation === OperationKind.Push || operation === OperationKind.Sync || operation === OperationKind.Fetch; } export class AutoFetcher { diff --git a/extensions/git/src/model.ts b/extensions/git/src/model.ts index 849dcdab06b..090fbcb4986 100644 --- a/extensions/git/src/model.ts +++ b/extensions/git/src/model.ts @@ -5,7 +5,7 @@ import { workspace, WorkspaceFoldersChangeEvent, Uri, window, Event, EventEmitter, QuickPickItem, Disposable, SourceControl, SourceControlResourceGroup, TextEditor, Memento, commands, LogOutputChannel, l10n, ProgressLocation } from 'vscode'; import TelemetryReporter from '@vscode/extension-telemetry'; -import { Operation, Repository, RepositoryState } from './repository'; +import { OperationKind, Repository, RepositoryState } from './repository'; import { memoize, sequentialize, debounce } from './decorators'; import { dispose, anyEvent, filterEvent, isDescendant, pathEquals, toDisposable, eventToPromise } from './util'; import { Git } from './git'; @@ -533,19 +533,19 @@ export class Model implements IRemoteSourcePublisherRegistry, IPostCommitCommand let commitInProgress = false; let operationInProgress = false; for (const { repository } of this.openRepositories.values()) { - if (repository.operations.isRunning(Operation.Commit)) { + if (repository.operations.isRunning(OperationKind.Commit)) { commitInProgress = true; } // When one of the following operations is running, we want to // disable most commands in order to avoid multiple commands // running at the same time. - if (repository.operations.isRunning(Operation.Checkout) || - repository.operations.isRunning(Operation.CheckoutTracking) || - repository.operations.isRunning(Operation.Commit) || - repository.operations.isRunning(Operation.Pull) || - repository.operations.isRunning(Operation.Push) || - repository.operations.isRunning(Operation.Sync)) { + if (repository.operations.isRunning(OperationKind.Checkout) || + repository.operations.isRunning(OperationKind.CheckoutTracking) || + repository.operations.isRunning(OperationKind.Commit) || + repository.operations.isRunning(OperationKind.Pull) || + repository.operations.isRunning(OperationKind.Push) || + repository.operations.isRunning(OperationKind.Sync)) { operationInProgress = true; } } diff --git a/extensions/git/src/postCommitCommands.ts b/extensions/git/src/postCommitCommands.ts index a335040e94c..3b40cee53c3 100644 --- a/extensions/git/src/postCommitCommands.ts +++ b/extensions/git/src/postCommitCommands.ts @@ -5,7 +5,7 @@ import { Command, commands, Disposable, Event, EventEmitter, Memento, Uri, workspace, l10n } from 'vscode'; import { PostCommitCommandsProvider } from './api/git'; -import { Operation, Repository } from './repository'; +import { OperationKind, Repository } from './repository'; import { ApiRepository } from './api/api1'; import { dispose } from './util'; @@ -28,7 +28,7 @@ export class GitPostCommitCommandsProvider implements PostCommitCommandsProvider // Icon const repository = apiRepository.repository; - const isCommitInProgress = repository.operations.isRunning(Operation.Commit) || repository.operations.isRunning(Operation.PostCommitCommand); + const isCommitInProgress = repository.operations.isRunning(OperationKind.Commit) || repository.operations.isRunning(OperationKind.PostCommitCommand); const icon = isCommitInProgress ? '$(sync~spin)' : alwaysPrompt ? '$(lock)' : alwaysCommitToNewBranch ? '$(git-branch)' : undefined; // Tooltip (default) @@ -178,7 +178,7 @@ export class CommitCommandsCenter { l10n.t('Commit Changes to New Branch'); // Tooltip (in progress) - if (this.repository.operations.isRunning(Operation.Commit)) { + if (this.repository.operations.isRunning(OperationKind.Commit)) { tooltip = !alwaysCommitToNewBranch ? l10n.t('Committing Changes...') : l10n.t('Committing Changes to New Branch...'); diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index c41802a5de8..e174a058eb9 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -302,7 +302,7 @@ export class Resource implements SourceControlResourceState { } } -export const enum Operation { +export const enum OperationKind { Status = 'Status', Config = 'Config', Diff = 'Diff', @@ -358,70 +358,91 @@ export const enum Operation { Move = 'Move' } -function isReadOnly(operation: Operation): boolean { +function isReadOnly(operation: OperationKind): boolean { switch (operation) { - case Operation.Blame: - case Operation.CheckIgnore: - case Operation.Diff: - case Operation.FindTrackingBranches: - case Operation.GetBranch: - case Operation.GetCommitTemplate: - case Operation.GetObjectDetails: - case Operation.Log: - case Operation.LogFile: - case Operation.MergeBase: - case Operation.Show: + case OperationKind.Blame: + case OperationKind.CheckIgnore: + case OperationKind.Diff: + case OperationKind.FindTrackingBranches: + case OperationKind.GetBranch: + case OperationKind.GetCommitTemplate: + case OperationKind.GetObjectDetails: + case OperationKind.Log: + case OperationKind.LogFile: + case OperationKind.MergeBase: + case OperationKind.Show: return true; default: return false; } } -function shouldShowProgress(operation: Operation): boolean { +function shouldShowProgress(operation: OperationKind): boolean { switch (operation) { - case Operation.AddNoProgress: - case Operation.CleanNoProgress: - case Operation.FetchNoProgress: - case Operation.RevertFilesNoProgress: - case Operation.CheckIgnore: - case Operation.GetObjectDetails: - case Operation.Show: + case OperationKind.AddNoProgress: + case OperationKind.CleanNoProgress: + case OperationKind.FetchNoProgress: + case OperationKind.RevertFilesNoProgress: + case OperationKind.CheckIgnore: + case OperationKind.GetObjectDetails: + case OperationKind.Show: return false; default: return true; } } +export interface BaseOperation { + readonly kind: OperationKind; +} + +export interface CheckoutOperation extends BaseOperation { + readonly kind: OperationKind.Checkout | OperationKind.CheckoutTracking; + readonly refLabel: string; +} + export interface Operations { isIdle(): boolean; + getOperations(operationKind: OperationKind): BaseOperation[]; shouldShowProgress(): boolean; - isRunning(operation: Operation): boolean; + isRunning(operationKind: OperationKind): boolean; } class OperationsImpl implements Operations { - private operations = new Map(); + private operations = new Map>(); constructor(private readonly logger: LogOutputChannel) { } - start(operation: Operation): void { - this.logger.trace(`Operation start: ${operation}`); - this.operations.set(operation, (this.operations.get(operation) || 0) + 1); - } - - end(operation: Operation): void { - this.logger.trace(`Operation end: ${operation}`); - const count = (this.operations.get(operation) || 0) - 1; - - if (count <= 0) { - this.operations.delete(operation); + start(operation: BaseOperation): void { + if (this.operations.has(operation.kind)) { + this.operations.get(operation.kind)!.add(operation); } else { - this.operations.set(operation, count); + this.operations.set(operation.kind, new Set([operation])); } + + this.logger.trace(`Operation start: ${operation.kind}`); } - isRunning(operation: Operation): boolean { - return this.operations.has(operation); + end(operation: BaseOperation): void { + const operationSet = this.operations.get(operation.kind); + if (operationSet) { + operationSet.delete(operation); + if (operationSet.size === 0) { + this.operations.delete(operation.kind); + } + } + + this.logger.trace(`Operation end: ${operation.kind}`); + } + + getOperations(operationKind: OperationKind): BaseOperation[] { + const operationSet = this.operations.get(operationKind); + return operationSet ? Array.from(operationSet) : []; + } + + isRunning(operationKind: OperationKind): boolean { + return this.operations.has(operationKind); } isIdle(): boolean { @@ -461,7 +482,7 @@ interface GitResourceGroups { } export interface OperationResult { - operation: Operation; + operation: OperationKind; error: any; } @@ -477,7 +498,7 @@ class ProgressManager { this.repository.onDidChangeOperations(() => { // Disable input box when the commit operation is running - this.repository.sourceControl.inputBox.enabled = !this.repository.operations.isRunning(Operation.Commit); + this.repository.sourceControl.inputBox.enabled = !this.repository.operations.isRunning(OperationKind.Commit); }); } @@ -769,8 +790,8 @@ export class Repository implements Disposable { private _onDidChangeOriginalResource = new EventEmitter(); readonly onDidChangeOriginalResource: Event = this._onDidChangeOriginalResource.event; - private _onRunOperation = new EventEmitter(); - readonly onRunOperation: Event = this._onRunOperation.event; + private _onRunOperation = new EventEmitter(); + readonly onRunOperation: Event = this._onRunOperation.event; private _onDidRunOperation = new EventEmitter(); readonly onDidRunOperation: Event = this._onDidRunOperation.event; @@ -901,12 +922,6 @@ export class Repository implements Disposable { return this.repository.dotGit; } - // TODO@lszomoru - Move this to operation state - private _checkoutRef: string | undefined; - get checkoutRef(): string | undefined { - return this._checkoutRef; - } - private isRepositoryHuge: false | { limit: number } = false; private didWarnAboutLimit = false; @@ -1023,7 +1038,7 @@ export class Repository implements Disposable { } // https://github.com/microsoft/vscode/issues/39039 - const onSuccessfulPush = filterEvent(this.onDidRunOperation, e => e.operation === Operation.Push && !e.error); + const onSuccessfulPush = filterEvent(this.onDidRunOperation, e => e.operation === OperationKind.Push && !e.error); onSuccessfulPush(() => { const gitConfig = workspace.getConfiguration('git'); @@ -1155,89 +1170,89 @@ export class Repository implements Disposable { } getConfigs(): Promise<{ key: string; value: string }[]> { - return this.run(Operation.Config, () => this.repository.getConfigs('local')); + return this.run(OperationKind.Config, () => this.repository.getConfigs('local')); } getConfig(key: string): Promise { - return this.run(Operation.Config, () => this.repository.config('local', key)); + return this.run(OperationKind.Config, () => this.repository.config('local', key)); } getGlobalConfig(key: string): Promise { - return this.run(Operation.Config, () => this.repository.config('global', key)); + return this.run(OperationKind.Config, () => this.repository.config('global', key)); } setConfig(key: string, value: string): Promise { - return this.run(Operation.Config, () => this.repository.config('local', key, value)); + return this.run(OperationKind.Config, () => this.repository.config('local', key, value)); } log(options?: LogOptions): Promise { - return this.run(Operation.Log, () => this.repository.log(options)); + return this.run(OperationKind.Log, () => this.repository.log(options)); } logFile(uri: Uri, options?: LogFileOptions): Promise { // TODO: This probably needs per-uri granularity - return this.run(Operation.LogFile, () => this.repository.logFile(uri, options)); + return this.run(OperationKind.LogFile, () => this.repository.logFile(uri, options)); } @throttle async status(): Promise { - await this.run(Operation.Status); + await this.run(OperationKind.Status); } diff(cached?: boolean): Promise { - return this.run(Operation.Diff, () => this.repository.diff(cached)); + return this.run(OperationKind.Diff, () => this.repository.diff(cached)); } diffWithHEAD(): Promise; diffWithHEAD(path: string): Promise; diffWithHEAD(path?: string | undefined): Promise; diffWithHEAD(path?: string | undefined): Promise { - return this.run(Operation.Diff, () => this.repository.diffWithHEAD(path)); + return this.run(OperationKind.Diff, () => this.repository.diffWithHEAD(path)); } diffWith(ref: string): Promise; diffWith(ref: string, path: string): Promise; diffWith(ref: string, path?: string | undefined): Promise; diffWith(ref: string, path?: string): Promise { - return this.run(Operation.Diff, () => this.repository.diffWith(ref, path)); + return this.run(OperationKind.Diff, () => this.repository.diffWith(ref, path)); } diffIndexWithHEAD(): Promise; diffIndexWithHEAD(path: string): Promise; diffIndexWithHEAD(path?: string | undefined): Promise; diffIndexWithHEAD(path?: string): Promise { - return this.run(Operation.Diff, () => this.repository.diffIndexWithHEAD(path)); + return this.run(OperationKind.Diff, () => this.repository.diffIndexWithHEAD(path)); } diffIndexWith(ref: string): Promise; diffIndexWith(ref: string, path: string): Promise; diffIndexWith(ref: string, path?: string | undefined): Promise; diffIndexWith(ref: string, path?: string): Promise { - return this.run(Operation.Diff, () => this.repository.diffIndexWith(ref, path)); + return this.run(OperationKind.Diff, () => this.repository.diffIndexWith(ref, path)); } diffBlobs(object1: string, object2: string): Promise { - return this.run(Operation.Diff, () => this.repository.diffBlobs(object1, object2)); + return this.run(OperationKind.Diff, () => this.repository.diffBlobs(object1, object2)); } diffBetween(ref1: string, ref2: string): Promise; diffBetween(ref1: string, ref2: string, path: string): Promise; diffBetween(ref1: string, ref2: string, path?: string | undefined): Promise; diffBetween(ref1: string, ref2: string, path?: string): Promise { - return this.run(Operation.Diff, () => this.repository.diffBetween(ref1, ref2, path)); + return this.run(OperationKind.Diff, () => this.repository.diffBetween(ref1, ref2, path)); } getMergeBase(ref1: string, ref2: string): Promise { - return this.run(Operation.MergeBase, () => this.repository.getMergeBase(ref1, ref2)); + return this.run(OperationKind.MergeBase, () => this.repository.getMergeBase(ref1, ref2)); } async hashObject(data: string): Promise { - return this.run(Operation.HashObject, () => this.repository.hashObject(data)); + return this.run(OperationKind.HashObject, () => this.repository.hashObject(data)); } async add(resources: Uri[], opts?: { update?: boolean }): Promise { await this.run( - this.optimisticUpdateEnabled() ? Operation.AddNoProgress : Operation.Add, + this.optimisticUpdateEnabled() ? OperationKind.AddNoProgress : OperationKind.Add, async () => { await this.repository.add(resources.map(r => r.fsPath), opts); this.closeDiffEditors([], [...resources.map(r => r.fsPath)]); @@ -1274,12 +1289,12 @@ export class Repository implements Disposable { } async rm(resources: Uri[]): Promise { - await this.run(Operation.Remove, () => this.repository.rm(resources.map(r => r.fsPath))); + await this.run(OperationKind.Remove, () => this.repository.rm(resources.map(r => r.fsPath))); } async stage(resource: Uri, contents: string): Promise { const path = relativePath(this.repository.root, resource.fsPath).replace(/\\/g, '/'); - await this.run(Operation.Stage, async () => { + await this.run(OperationKind.Stage, async () => { await this.repository.stage(path, contents); this.closeDiffEditors([], [...resource.fsPath]); }); @@ -1288,7 +1303,7 @@ export class Repository implements Disposable { async revert(resources: Uri[]): Promise { await this.run( - this.optimisticUpdateEnabled() ? Operation.RevertFilesNoProgress : Operation.RevertFiles, + this.optimisticUpdateEnabled() ? OperationKind.RevertFilesNoProgress : OperationKind.RevertFiles, async () => { await this.repository.revert('HEAD', resources.map(r => r.fsPath)); this.closeDiffEditors([...resources.length !== 0 ? @@ -1335,7 +1350,7 @@ export class Repository implements Disposable { async commit(message: string | undefined, opts: CommitOptions = Object.create(null)): Promise { if (this.rebaseCommit) { await this.run( - Operation.RebaseContinue, + OperationKind.RebaseContinue, async () => { if (opts.all) { const addOpts = opts.all === 'tracked' ? { update: true } : {}; @@ -1351,7 +1366,7 @@ export class Repository implements Disposable { this.commitCommandCenter.postCommitCommand = opts.postCommitCommand; await this.run( - Operation.Commit, + OperationKind.Commit, async () => { if (opts.all) { const addOpts = opts.all === 'tracked' ? { update: true } : {}; @@ -1371,7 +1386,7 @@ export class Repository implements Disposable { () => this.commitOperationGetOptimisticResourceGroups(opts)); // Execute post-commit command - await this.run(Operation.PostCommitCommand, async () => { + await this.run(OperationKind.PostCommitCommand, async () => { await this.commitCommandCenter.executePostCommitCommand(opts.postCommitCommand); }); } @@ -1405,7 +1420,7 @@ export class Repository implements Disposable { async clean(resources: Uri[]): Promise { await this.run( - this.optimisticUpdateEnabled() ? Operation.CleanNoProgress : Operation.Clean, + this.optimisticUpdateEnabled() ? OperationKind.CleanNoProgress : OperationKind.Clean, async () => { const toClean: string[] = []; const toCheckout: string[] = []; @@ -1487,15 +1502,15 @@ export class Repository implements Disposable { } async branch(name: string, _checkout: boolean, _ref?: string): Promise { - await this.run(Operation.Branch, () => this.repository.branch(name, _checkout, _ref)); + await this.run(OperationKind.Branch, () => this.repository.branch(name, _checkout, _ref)); } async deleteBranch(name: string, force?: boolean): Promise { - await this.run(Operation.DeleteBranch, () => this.repository.deleteBranch(name, force)); + await this.run(OperationKind.DeleteBranch, () => this.repository.deleteBranch(name, force)); } async renameBranch(name: string): Promise { - await this.run(Operation.RenameBranch, () => this.repository.renameBranch(name)); + await this.run(OperationKind.RenameBranch, () => this.repository.renameBranch(name)); } @throttle @@ -1509,7 +1524,7 @@ export class Repository implements Disposable { try { // Fast-forward the branch if possible const options = { remote: branch.upstream.remote, ref: `${branch.upstream.name}:${branch.name}` }; - await this.run(Operation.Fetch, async () => this.repository.fetch(options)); + await this.run(OperationKind.Fetch, async () => this.repository.fetch(options)); } catch (err) { if (err.gitErrorCode === GitErrorCodes.BranchFastForwardRejected) { return; @@ -1520,50 +1535,51 @@ export class Repository implements Disposable { } async cherryPick(commitHash: string): Promise { - await this.run(Operation.CherryPick, () => this.repository.cherryPick(commitHash)); + await this.run(OperationKind.CherryPick, () => this.repository.cherryPick(commitHash)); } async move(from: string, to: string): Promise { - await this.run(Operation.Move, () => this.repository.move(from, to)); + await this.run(OperationKind.Move, () => this.repository.move(from, to)); } async getBranch(name: string): Promise { - return await this.run(Operation.GetBranch, () => this.repository.getBranch(name)); + return await this.run(OperationKind.GetBranch, () => this.repository.getBranch(name)); } async getBranches(query: BranchQuery): Promise { - return await this.run(Operation.GetBranches, () => this.repository.getBranches(query)); + return await this.run(OperationKind.GetBranches, () => this.repository.getBranches(query)); } async setBranchUpstream(name: string, upstream: string): Promise { - await this.run(Operation.SetBranchUpstream, () => this.repository.setBranchUpstream(name, upstream)); + await this.run(OperationKind.SetBranchUpstream, () => this.repository.setBranchUpstream(name, upstream)); } async merge(ref: string): Promise { - await this.run(Operation.Merge, () => this.repository.merge(ref)); + await this.run(OperationKind.Merge, () => this.repository.merge(ref)); } async mergeAbort(): Promise { - await this.run(Operation.MergeAbort, async () => await this.repository.mergeAbort()); + await this.run(OperationKind.MergeAbort, async () => await this.repository.mergeAbort()); } async rebase(branch: string): Promise { - await this.run(Operation.Rebase, () => this.repository.rebase(branch)); + await this.run(OperationKind.Rebase, () => this.repository.rebase(branch)); } async tag(name: string, message?: string): Promise { - await this.run(Operation.Tag, () => this.repository.tag(name, message)); + await this.run(OperationKind.Tag, () => this.repository.tag(name, message)); } async deleteTag(name: string): Promise { - await this.run(Operation.DeleteTag, () => this.repository.deleteTag(name)); + await this.run(OperationKind.DeleteTag, () => this.repository.deleteTag(name)); } async checkout(treeish: string, opts?: { detached?: boolean; pullBeforeCheckout?: boolean }): Promise { - this._checkoutRef = treeish; + const refLabel = this.checkoutRefLabel(treeish, opts?.detached); + const operation: CheckoutOperation = { kind: OperationKind.Checkout, refLabel }; - try { - await this.run(Operation.Checkout, async () => { + await this.run(operation, + async () => { if (opts?.pullBeforeCheckout) { try { await this.fastForwardBranch(treeish); @@ -1575,17 +1591,17 @@ export class Repository implements Disposable { await this.repository.checkout(treeish, [], opts); }); - } finally { - this._checkoutRef = undefined; - } } async checkoutTracking(treeish: string, opts: { detached?: boolean } = {}): Promise { - await this.run(Operation.CheckoutTracking, () => this.repository.checkout(treeish, [], { ...opts, track: true })); + const refLabel = this.checkoutRefLabel(treeish, opts?.detached); + const operation: CheckoutOperation = { kind: OperationKind.CheckoutTracking, refLabel }; + + await this.run(operation, () => this.repository.checkout(treeish, [], { ...opts, track: true })); } async findTrackingBranches(upstreamRef: string): Promise { - return await this.run(Operation.FindTrackingBranches, () => this.repository.findTrackingBranches(upstreamRef)); + return await this.run(OperationKind.FindTrackingBranches, () => this.repository.findTrackingBranches(upstreamRef)); } async getCommit(ref: string): Promise { @@ -1593,23 +1609,23 @@ export class Repository implements Disposable { } async reset(treeish: string, hard?: boolean): Promise { - await this.run(Operation.Reset, () => this.repository.reset(treeish, hard)); + await this.run(OperationKind.Reset, () => this.repository.reset(treeish, hard)); } async deleteRef(ref: string): Promise { - await this.run(Operation.DeleteRef, () => this.repository.deleteRef(ref)); + await this.run(OperationKind.DeleteRef, () => this.repository.deleteRef(ref)); } async addRemote(name: string, url: string): Promise { - await this.run(Operation.Remote, () => this.repository.addRemote(name, url)); + await this.run(OperationKind.Remote, () => this.repository.addRemote(name, url)); } async removeRemote(name: string): Promise { - await this.run(Operation.Remote, () => this.repository.removeRemote(name)); + await this.run(OperationKind.Remote, () => this.repository.removeRemote(name)); } async renameRemote(name: string, newName: string): Promise { - await this.run(Operation.Remote, () => this.repository.renameRemote(name, newName)); + await this.run(OperationKind.Remote, () => this.repository.renameRemote(name, newName)); } @throttle @@ -1638,7 +1654,7 @@ export class Repository implements Disposable { options.prune = prune; } - const operation = options.silent === true ? Operation.FetchNoProgress : Operation.Fetch; + const operation = options.silent === true ? OperationKind.FetchNoProgress : OperationKind.Fetch; await this.run(operation, async () => this.repository.fetch(options)); } @@ -1669,7 +1685,7 @@ export class Repository implements Disposable { } async pullFrom(rebase?: boolean, remote?: string, branch?: string, unshallow?: boolean): Promise { - await this.run(Operation.Pull, async () => { + await this.run(OperationKind.Pull, async () => { await this.maybeAutoStash(async () => { const config = workspace.getConfiguration('git', Uri.file(this.root)); const fetchOnPull = config.get('fetchOnPull'); @@ -1713,23 +1729,23 @@ export class Repository implements Disposable { branch = `${head.name}:${head.upstream.name}`; } - await this.run(Operation.Push, () => this._push(remote, branch, undefined, undefined, forcePushMode)); + await this.run(OperationKind.Push, () => this._push(remote, branch, undefined, undefined, forcePushMode)); } async pushTo(remote?: string, name?: string, setUpstream = false, forcePushMode?: ForcePushMode): Promise { - await this.run(Operation.Push, () => this._push(remote, name, setUpstream, undefined, forcePushMode)); + await this.run(OperationKind.Push, () => this._push(remote, name, setUpstream, undefined, forcePushMode)); } async pushFollowTags(remote?: string, forcePushMode?: ForcePushMode): Promise { - await this.run(Operation.Push, () => this._push(remote, undefined, false, true, forcePushMode)); + await this.run(OperationKind.Push, () => this._push(remote, undefined, false, true, forcePushMode)); } async pushTags(remote?: string, forcePushMode?: ForcePushMode): Promise { - await this.run(Operation.Push, () => this._push(remote, undefined, false, false, forcePushMode, true)); + await this.run(OperationKind.Push, () => this._push(remote, undefined, false, false, forcePushMode, true)); } async blame(path: string): Promise { - return await this.run(Operation.Blame, () => this.repository.blame(path)); + return await this.run(OperationKind.Blame, () => this.repository.blame(path)); } @throttle @@ -1748,7 +1764,7 @@ export class Repository implements Disposable { pushBranch = `${head.name}:${head.upstream.name}`; } - await this.run(Operation.Sync, async () => { + await this.run(OperationKind.Sync, async () => { await this.maybeAutoStash(async () => { const config = workspace.getConfiguration('git', Uri.file(this.root)); const fetchOnPull = config.get('fetchOnPull'); @@ -1802,7 +1818,7 @@ export class Repository implements Disposable { return true; } - const maybeRebased = await this.run(Operation.Log, async () => { + const maybeRebased = await this.run(OperationKind.Log, async () => { try { const result = await this.repository.exec(['log', '--oneline', '--cherry', `${currentBranch ?? ''}...${currentBranch ?? ''}@{upstream}`, '--']); if (result.exitCode) { @@ -1843,7 +1859,7 @@ export class Repository implements Disposable { } async show(ref: string, filePath: string): Promise { - return await this.run(Operation.Show, async () => { + return await this.run(OperationKind.Show, async () => { const path = relativePath(this.repository.root, filePath).replace(/\\/g, '/'); const configFiles = workspace.getConfiguration('files', Uri.file(filePath)); const defaultEncoding = configFiles.get('encoding'); @@ -1863,22 +1879,22 @@ export class Repository implements Disposable { } async buffer(ref: string, filePath: string): Promise { - return this.run(Operation.Show, () => { + return this.run(OperationKind.Show, () => { const path = relativePath(this.repository.root, filePath).replace(/\\/g, '/'); return this.repository.buffer(`${ref}:${path}`); }); } getObjectDetails(ref: string, filePath: string): Promise<{ mode: string; object: string; size: number }> { - return this.run(Operation.GetObjectDetails, () => this.repository.getObjectDetails(ref, filePath)); + return this.run(OperationKind.GetObjectDetails, () => this.repository.getObjectDetails(ref, filePath)); } detectObjectType(object: string): Promise<{ mimetype: string; encoding?: string }> { - return this.run(Operation.Show, () => this.repository.detectObjectType(object)); + return this.run(OperationKind.Show, () => this.repository.detectObjectType(object)); } async apply(patch: string, reverse?: boolean): Promise { - return await this.run(Operation.Apply, () => this.repository.apply(patch, reverse)); + return await this.run(OperationKind.Apply, () => this.repository.apply(patch, reverse)); } async getStashes(): Promise { @@ -1891,30 +1907,30 @@ export class Repository implements Disposable { ...this.workingTreeGroup.resourceStates.map(r => r.resourceUri.fsPath), ...includeUntracked ? this.untrackedGroup.resourceStates.map(r => r.resourceUri.fsPath) : []]; - return await this.run(Operation.Stash, async () => { + return await this.run(OperationKind.Stash, async () => { this.repository.createStash(message, includeUntracked); this.closeDiffEditors(indexResources, workingGroupResources); }); } async popStash(index?: number): Promise { - return await this.run(Operation.Stash, () => this.repository.popStash(index)); + return await this.run(OperationKind.Stash, () => this.repository.popStash(index)); } async dropStash(index?: number): Promise { - return await this.run(Operation.Stash, () => this.repository.dropStash(index)); + return await this.run(OperationKind.Stash, () => this.repository.dropStash(index)); } async applyStash(index?: number): Promise { - return await this.run(Operation.Stash, () => this.repository.applyStash(index)); + return await this.run(OperationKind.Stash, () => this.repository.applyStash(index)); } async getCommitTemplate(): Promise { - return await this.run(Operation.GetCommitTemplate, async () => this.repository.getCommitTemplate()); + return await this.run(OperationKind.GetCommitTemplate, async () => this.repository.getCommitTemplate()); } async ignore(files: Uri[]): Promise { - return await this.run(Operation.Ignore, async () => { + return await this.run(OperationKind.Ignore, async () => { const ignoreFile = `${this.repository.root}${path.sep}.gitignore`; const textToAppend = files .map(uri => relativePath(this.repository.root, uri.fsPath).replace(/\\/g, '/')) @@ -1937,11 +1953,11 @@ export class Repository implements Disposable { } async rebaseAbort(): Promise { - await this.run(Operation.RebaseAbort, async () => await this.repository.rebaseAbort()); + await this.run(OperationKind.RebaseAbort, async () => await this.repository.rebaseAbort()); } checkIgnore(filePaths: string[]): Promise> { - return this.run(Operation.CheckIgnore, () => { + return this.run(OperationKind.CheckIgnore, () => { return new Promise>((resolve, reject) => { filePaths = filePaths @@ -2032,9 +2048,17 @@ export class Repository implements Disposable { } private async run( - operation: Operation, + operation: OperationKind | BaseOperation, runOperation: () => Promise = () => Promise.resolve(null), getOptimisticResourceGroups: () => GitResourceGroups | undefined = () => undefined): Promise { + return this._run(typeof operation === 'object' ? operation : { kind: operation }, runOperation, getOptimisticResourceGroups); + } + + private async _run( + operation: BaseOperation, + runOperation: () => Promise, + getOptimisticResourceGroups: () => GitResourceGroups | undefined): Promise { + if (this.state !== RepositoryState.Idle) { throw new Error('Repository not initialized'); } @@ -2042,12 +2066,12 @@ export class Repository implements Disposable { let error: any = null; this._operations.start(operation); - this._onRunOperation.fire(operation); + this._onRunOperation.fire(operation.kind); try { - const result = await this.retryRun(operation, runOperation); + const result = await this._retryRun(operation.kind, runOperation); - if (!isReadOnly(operation)) { + if (!isReadOnly(operation.kind)) { await this.updateModelState(this.optimisticUpdateEnabled() ? getOptimisticResourceGroups() : undefined); } @@ -2062,11 +2086,11 @@ export class Repository implements Disposable { throw err; } finally { this._operations.end(operation); - this._onDidRunOperation.fire({ operation, error }); + this._onDidRunOperation.fire({ operation: operation.kind, error }); } } - private async retryRun(operation: Operation, runOperation: () => Promise = () => Promise.resolve(null)): Promise { + private async _retryRun(operation: OperationKind, runOperation: () => Promise = () => Promise.resolve(null)): Promise { let attempt = 0; while (true) { @@ -2076,7 +2100,7 @@ export class Repository implements Disposable { } catch (err) { const shouldRetry = attempt <= 10 && ( (err.gitErrorCode === GitErrorCodes.RepositoryIsLocked) - || ((operation === Operation.Pull || operation === Operation.Sync || operation === Operation.Fetch) && (err.gitErrorCode === GitErrorCodes.CantLockRef || err.gitErrorCode === GitErrorCodes.CantRebaseMultipleBranches)) + || ((operation === OperationKind.Pull || operation === OperationKind.Sync || operation === OperationKind.Fetch) && (err.gitErrorCode === GitErrorCodes.CantLockRef || err.gitErrorCode === GitErrorCodes.CantRebaseMultipleBranches)) ); if (shouldRetry) { @@ -2547,6 +2571,13 @@ export class Repository implements Disposable { return true; } + private checkoutRefLabel(treeish: string, detached?: boolean): string { + if (!detached) { return treeish; } + + const ref = this.refs.filter(r => r.name === treeish); + return ref[0]?.commit?.substring(0, 8) ?? treeish; + } + public isBranchProtected(name = this.HEAD?.name ?? ''): boolean { return this.isBranchProtectedMatcher ? this.isBranchProtectedMatcher(name) : false; } diff --git a/extensions/git/src/statusbar.ts b/extensions/git/src/statusbar.ts index c4bddd1de39..a62027ffdaa 100644 --- a/extensions/git/src/statusbar.ts +++ b/extensions/git/src/statusbar.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable, Command, EventEmitter, Event, workspace, Uri, l10n } from 'vscode'; -import { Repository, Operation } from './repository'; +import { Repository, OperationKind, CheckoutOperation } from './repository'; import { anyEvent, dispose, filterEvent } from './util'; import { Branch, RefType, RemoteSourcePublisher } from './api/git'; import { IRemoteSourcePublisherRegistry } from './remotePublisher'; @@ -40,8 +40,13 @@ class CheckoutStatusBar { } get command(): Command | undefined { + const operationData = [ + ...this.repository.operations.getOperations(OperationKind.Checkout) as CheckoutOperation[], + ...this.repository.operations.getOperations(OperationKind.CheckoutTracking) as CheckoutOperation[] + ]; + const rebasing = !!this.repository.rebaseCommit; - const label = this.repository.checkoutRef ?? `${this.repository.headLabel}${rebasing ? ` (${l10n.t('Rebasing')})` : ''}`; + const label = operationData[0]?.refLabel ?? `${this.repository.headLabel}${rebasing ? ` (${l10n.t('Rebasing')})` : ''}`; const command = (this.state.isCheckoutRunning || this.state.isCommitRunning || this.state.isSyncRunning) ? '' : 'git.checkout'; return { @@ -94,12 +99,12 @@ class CheckoutStatusBar { } private onDidChangeOperations(): void { - const isCommitRunning = this.repository.operations.isRunning(Operation.Commit); - const isCheckoutRunning = this.repository.operations.isRunning(Operation.Checkout) || - this.repository.operations.isRunning(Operation.CheckoutTracking); - const isSyncRunning = this.repository.operations.isRunning(Operation.Sync) || - this.repository.operations.isRunning(Operation.Push) || - this.repository.operations.isRunning(Operation.Pull); + const isCommitRunning = this.repository.operations.isRunning(OperationKind.Commit); + const isCheckoutRunning = this.repository.operations.isRunning(OperationKind.Checkout) || + this.repository.operations.isRunning(OperationKind.CheckoutTracking); + const isSyncRunning = this.repository.operations.isRunning(OperationKind.Sync) || + this.repository.operations.isRunning(OperationKind.Push) || + this.repository.operations.isRunning(OperationKind.Pull); this.state = { ...this.state, isCheckoutRunning, isCommitRunning, isSyncRunning }; } @@ -162,12 +167,12 @@ class SyncStatusBar { } private onDidChangeOperations(): void { - const isCommitRunning = this.repository.operations.isRunning(Operation.Commit); - const isCheckoutRunning = this.repository.operations.isRunning(Operation.Checkout) || - this.repository.operations.isRunning(Operation.CheckoutTracking); - const isSyncRunning = this.repository.operations.isRunning(Operation.Sync) || - this.repository.operations.isRunning(Operation.Push) || - this.repository.operations.isRunning(Operation.Pull); + const isCommitRunning = this.repository.operations.isRunning(OperationKind.Commit); + const isCheckoutRunning = this.repository.operations.isRunning(OperationKind.Checkout) || + this.repository.operations.isRunning(OperationKind.CheckoutTracking); + const isSyncRunning = this.repository.operations.isRunning(OperationKind.Sync) || + this.repository.operations.isRunning(OperationKind.Push) || + this.repository.operations.isRunning(OperationKind.Pull); this.state = { ...this.state, isCheckoutRunning, isCommitRunning, isSyncRunning }; }