diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 33cc63aa3ea..1e34aa2ee4e 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -270,7 +270,8 @@ export const GitErrorCodes = { GitNotFound: 'GitNotFound', CantCreatePipe: 'CantCreatePipe', CantAccessRemote: 'CantAccessRemote', - RepositoryNotFound: 'RepositoryNotFound' + RepositoryNotFound: 'RepositoryNotFound', + RepositoryIsLocked: 'RepositoryIsLocked' }; export class Git { @@ -333,7 +334,9 @@ export class Git { if (result.exitCode) { let gitErrorCode: string | undefined = void 0; - if (/Authentication failed/.test(result.stderr)) { + if (/Another git process seems to be running in this repository|If no other git process is currently running/.test(result.stderr)) { + gitErrorCode = GitErrorCodes.RepositoryIsLocked; + } else if (/Authentication failed/.test(result.stderr)) { gitErrorCode = GitErrorCodes.AuthenticationFailed; } else if (/Not a git repository/.test(result.stderr)) { gitErrorCode = GitErrorCodes.NotAGitRepository; diff --git a/extensions/git/src/model.ts b/extensions/git/src/model.ts index dbb0461d87e..c061cd103d1 100644 --- a/extensions/git/src/model.ts +++ b/extensions/git/src/model.ts @@ -11,11 +11,9 @@ import { anyEvent, eventToPromise, filterEvent, mapEvent, EmptyDisposable, combi import { memoize, throttle, debounce } from './decorators'; import { watch } from './watch'; import * as path from 'path'; -import * as fs from 'fs'; import * as nls from 'vscode-nls'; const timeout = (millis: number) => new Promise(c => setTimeout(c, millis)); -const exists = (path: string) => new Promise(c => fs.exists(path, c)); const localize = nls.loadMessageBundle(); const iconsRootPath = path.join(path.dirname(__dirname), 'resources', 'icons'); @@ -364,21 +362,6 @@ export class Model implements Disposable { } } - /** - * Returns promise which resolves when there is no `.git/index.lock` file, - * or when it has attempted way too many times. Back off mechanism. - */ - async whenUnlocked(): Promise { - let millis = 100; - let retries = 0; - - while (retries < 10 && await exists(path.join(this.repository.root, '.git', 'index.lock'))) { - retries += 1; - millis *= 1.4; - await timeout(millis); - } - } - @throttle async init(): Promise { if (this.state !== State.NotAGitRepository) { @@ -394,23 +377,19 @@ export class Model implements Disposable { await this.run(Operation.Status); } - @throttle async add(...resources: Resource[]): Promise { await this.run(Operation.Add, () => this.repository.add(resources.map(r => r.resourceUri.fsPath))); } - @throttle async stage(uri: Uri, contents: string): Promise { const relativePath = path.relative(this.repository.root, uri.fsPath).replace(/\\/g, '/'); await this.run(Operation.Stage, () => this.repository.stage(relativePath, contents)); } - @throttle async revertFiles(...resources: Resource[]): Promise { await this.run(Operation.RevertFiles, () => this.repository.revertFiles('HEAD', resources.map(r => r.resourceUri.fsPath))); } - @throttle async commit(message: string, opts: CommitOptions = Object.create(null)): Promise { await this.run(Operation.Commit, async () => { if (opts.all) { @@ -421,7 +400,6 @@ export class Model implements Disposable { }); } - @throttle async clean(...resources: Resource[]): Promise { await this.run(Operation.Clean, async () => { const toClean: string[] = []; @@ -454,22 +432,18 @@ export class Model implements Disposable { }); } - @throttle async branch(name: string): Promise { await this.run(Operation.Branch, () => this.repository.branch(name, true)); } - @throttle async checkout(treeish: string): Promise { await this.run(Operation.Checkout, () => this.repository.checkout(treeish, [])); } - @throttle async getCommit(ref: string): Promise { return await this.repository.getCommit(ref); } - @throttle async reset(treeish: string, hard?: boolean): Promise { await this.run(Operation.Reset, () => this.repository.reset(treeish, hard)); } @@ -479,12 +453,10 @@ export class Model implements Disposable { await this.run(Operation.Fetch, () => this.repository.fetch()); } - @throttle async pull(rebase?: boolean): Promise { await this.run(Operation.Pull, () => this.repository.pull(rebase)); } - @throttle async push(remote?: string, name?: string, options?: PushOptions): Promise { await this.run(Operation.Push, () => this.repository.push(remote, name, options)); } @@ -503,9 +475,6 @@ export class Model implements Disposable { } async show(ref: string, uri: Uri): Promise { - // TODO@Joao: should we make this a general concept? - await this.whenIdle(); - return await this.run(Operation.Show, async () => { const relativePath = path.relative(this.repository.root, uri.fsPath).replace(/\\/g, '/'); const result = await this.repository.git.exec(this.repository.root, ['show', `${ref}:${relativePath}`]); @@ -532,11 +501,11 @@ export class Model implements Disposable { try { await this.assertIdleState(); - await this.whenUnlocked(); - const result = await runOperation(); + + const result = await this.retryRun(runOperation); if (!isReadOnly(operation)) { - await this.update(); + await this.updateModelState(); } return result; @@ -559,6 +528,24 @@ export class Model implements Disposable { }); } + private async retryRun(runOperation: () => Promise = () => Promise.resolve(null)): Promise { + let attempt = 0; + + while (true) { + try { + attempt++; + return await runOperation(); + } catch (err) { + if (err.gitErrorCode === GitErrorCodes.RepositoryIsLocked && attempt <= 10) { + // quatratic backoff + await timeout(Math.pow(attempt, 2) * 50); + } else { + throw err; + } + } + } + } + /* We use the native Node `watch` for faster, non debounced events. * That way we hopefully get the events during the operations we're * performing, thus sparing useless `git status` calls to refresh @@ -592,7 +579,7 @@ export class Model implements Disposable { } @throttle - private async update(): Promise { + private async updateModelState(): Promise { const status = await this.repository.getStatus(); let HEAD: Branch | undefined;