From d8bce253fb9e28fbfbb657483a9a3965a9e6dd21 Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Tue, 5 Jan 2021 02:38:24 -0500 Subject: [PATCH] Adds resiliency to reading .git files Adds caching to merge/rebase status --- src/git/git.ts | 38 +++++++- src/git/gitService.ts | 181 +++++++++++++++++++++--------------- src/git/models/rebase.ts | 4 +- src/views/nodes/rebaseStatusNode.ts | 16 +++- 4 files changed, 154 insertions(+), 85 deletions(-) diff --git a/src/git/git.ts b/src/git/git.ts index af850e8..7bea0e1 100644 --- a/src/git/git.ts +++ b/src/git/git.ts @@ -1,8 +1,9 @@ /* eslint-disable @typescript-eslint/naming-convention */ 'use strict'; import * as paths from 'path'; +import { TextDecoder } from 'util'; import * as iconv from 'iconv-lite'; -import { window } from 'vscode'; +import { Uri, window, workspace } from 'vscode'; import { GlyphChars } from '../constants'; import { Container } from '../container'; import { Logger } from '../logger'; @@ -26,6 +27,8 @@ const emptyObj = Object.freeze({}); const emptyStr = ''; const slash = '/'; +const textDecoder = new TextDecoder('utf8'); + // This is a root sha of all git repo's if using sha1 const rootSha = '4b825dc642cb6eb9a060e54bf8d69288fbee4904'; @@ -1382,4 +1385,37 @@ export namespace Git { export function tag(repoPath: string) { return git({ cwd: repoPath }, 'tag', '-l', `--format=${GitTagParser.defaultFormat}`); } + + export async function readDotGitFile( + repoPath: string, + paths: string[], + options?: { numeric?: false; throw?: boolean; trim?: boolean }, + ): Promise; + export async function readDotGitFile( + repoPath: string, + path: string[], + options?: { numeric: true; throw?: boolean; trim?: boolean }, + ): Promise; + export async function readDotGitFile( + repoPath: string, + pathParts: string[], + options?: { numeric?: boolean; throw?: boolean; trim?: boolean }, + ): Promise { + try { + const bytes = await workspace.fs.readFile(Uri.file(paths.join(...[repoPath, '.git', ...pathParts]))); + let contents = textDecoder.decode(bytes); + contents = options?.trim ?? true ? contents.trim() : contents; + + if (options?.numeric) { + const number = Number.parseInt(contents, 10); + return isNaN(number) ? undefined : number; + } + + return contents; + } catch (ex) { + if (options?.throw) throw ex; + + return undefined; + } + } } diff --git a/src/git/gitService.ts b/src/git/gitService.ts index 16aa1e8..7f87bde 100644 --- a/src/git/gitService.ts +++ b/src/git/gitService.ts @@ -1,7 +1,6 @@ 'use strict'; import * as fs from 'fs'; import * as paths from 'path'; -import { TextDecoder } from 'util'; import { ConfigurationChangeEvent, Disposable, @@ -121,8 +120,6 @@ const weightedDefaultBranches = new Map([ ['development', 1], ]); -const textDecoder = new TextDecoder('utf8'); - export class GitService implements Disposable { private _onDidChangeRepositories = new EventEmitter(); get onDidChangeRepositories(): Event { @@ -135,6 +132,8 @@ export class GitService implements Disposable { private readonly _branchesCache = new Map(); private readonly _contributorsCache = new Map(); + private readonly _mergeStatusCache = new Map(); + private readonly _rebaseStatusCache = new Map(); private readonly _remotesWithApiProviderCache = new Map | null>(); private readonly _stashesCache = new Map(); private readonly _tagsCache = new Map(); @@ -165,6 +164,8 @@ export class GitService implements Disposable { this._repositoryTree.forEach(r => r.dispose()); this._branchesCache.clear(); this._contributorsCache.clear(); + this._mergeStatusCache.clear(); + this._rebaseStatusCache.clear(); this._remotesWithApiProviderCache.clear(); this._stashesCache.clear(); this._tagsCache.clear(); @@ -203,14 +204,18 @@ export class GitService implements Disposable { this._branchesCache.delete(repo.path); this._contributorsCache.delete(repo.path); + this._mergeStatusCache.delete(repo.path); + this._rebaseStatusCache.delete(repo.path); + this._tagsCache.delete(repo.path); + this._trackedCache.clear(); + if (e.changed(RepositoryChange.Remotes)) { this._remotesWithApiProviderCache.clear(); } + if (e.changed(RepositoryChange.Stash)) { this._stashesCache.delete(repo.path); } - this._tagsCache.delete(repo.path); - this._trackedCache.clear(); if (e.changed(RepositoryChange.Config)) { this._userMapCache.delete(repo.path); @@ -2397,93 +2402,115 @@ export class GitService implements Disposable { } } + @gate() @log() async getMergeStatus(repoPath: string): Promise { - const merge = await Git.rev_parse__verify(repoPath, 'MERGE_HEAD'); - if (merge == null) return undefined; + let status = this.useCaching ? this._mergeStatusCache.get(repoPath) : undefined; + if (status === undefined) { + const merge = await Git.rev_parse__verify(repoPath, 'MERGE_HEAD'); + if (merge != null) { + const [branch, mergeBase, possibleSourceBranches] = await Promise.all([ + this.getBranch(repoPath), + this.getMergeBase(repoPath, 'MERGE_HEAD', 'HEAD'), + this.getCommitBranches(repoPath, 'MERGE_HEAD', { mode: 'pointsAt' }), + ]); + + status = { + type: 'merge', + repoPath: repoPath, + mergeBase: mergeBase, + HEAD: GitReference.create(merge, repoPath, { refType: 'revision' }), + current: GitReference.fromBranch(branch!), + incoming: + possibleSourceBranches?.length === 1 + ? GitReference.create(possibleSourceBranches[0], repoPath, { + refType: 'branch', + name: possibleSourceBranches[0], + remote: false, + }) + : undefined, + }; + } - const [branch, mergeBase, possibleSourceBranches] = await Promise.all([ - this.getBranch(repoPath), - this.getMergeBase(repoPath, 'MERGE_HEAD', 'HEAD'), - this.getCommitBranches(repoPath, 'MERGE_HEAD', { mode: 'pointsAt' }), - ]); + const repo = await this.getRepository(repoPath); + if (repo?.supportsChangeEvents) { + this._mergeStatusCache.set(repoPath, status ?? null); + } + } - return { - type: 'merge', - repoPath: repoPath, - mergeBase: mergeBase, - HEAD: GitReference.create(merge, repoPath, { refType: 'revision' }), - current: GitReference.fromBranch(branch!), - incoming: - possibleSourceBranches?.length === 1 - ? GitReference.create(possibleSourceBranches[0], repoPath, { - refType: 'branch', - name: possibleSourceBranches[0], - remote: false, - }) - : undefined, - }; + return status ?? undefined; } + @gate() @log() async getRebaseStatus(repoPath: string): Promise { - const rebase = await Git.rev_parse__verify(repoPath, 'REBASE_HEAD'); - if (rebase == null) return undefined; - - const [mergeBase, headNameBytes, ontoBytes, stepBytes, stepMessageBytes, stepsBytes] = await Promise.all([ - this.getMergeBase(repoPath, 'REBASE_HEAD', 'HEAD'), - workspace.fs.readFile(Uri.file(paths.join(repoPath, '.git', 'rebase-merge', 'head-name'))), - workspace.fs.readFile(Uri.file(paths.join(repoPath, '.git', 'rebase-merge', 'onto'))), - workspace.fs.readFile(Uri.file(paths.join(repoPath, '.git', 'rebase-merge', 'msgnum'))), - workspace.fs.readFile(Uri.file(paths.join(repoPath, '.git', 'rebase-merge', 'message'))), - workspace.fs.readFile(Uri.file(paths.join(repoPath, '.git', 'rebase-merge', 'end'))), - ]); + let status = this.useCaching ? this._rebaseStatusCache.get(repoPath) : undefined; + if (status === undefined) { + const rebase = await Git.rev_parse__verify(repoPath, 'REBASE_HEAD'); + if (rebase != null) { + // eslint-disable-next-line prefer-const + let [mergeBase, branch, onto, step, stepMessage, steps] = await Promise.all([ + this.getMergeBase(repoPath, 'REBASE_HEAD', 'HEAD'), + Git.readDotGitFile(repoPath, ['rebase-merge', 'head-name']), + Git.readDotGitFile(repoPath, ['rebase-merge', 'onto']), + Git.readDotGitFile(repoPath, ['rebase-merge', 'msgnum'], { numeric: true }), + Git.readDotGitFile(repoPath, ['rebase-merge', 'message'], { throw: true }).catch(() => + Git.readDotGitFile(repoPath, ['rebase-merge', 'message-squashed']), + ), + Git.readDotGitFile(repoPath, ['rebase-merge', 'end'], { numeric: true }), + ]); + + if (branch == null || onto == null) return undefined; + + if (branch.startsWith('refs/heads/')) { + branch = branch.substr(11).trim(); + } - let branch = textDecoder.decode(headNameBytes); - if (branch.startsWith('refs/heads/')) { - branch = branch.substr(11).trim(); - } + const possibleSourceBranches = await this.getCommitBranches(repoPath, onto, { mode: 'pointsAt' }); - const onto = textDecoder.decode(ontoBytes).trim(); - const step = Number.parseInt(textDecoder.decode(stepBytes).trim(), 10); - const steps = Number.parseInt(textDecoder.decode(stepsBytes).trim(), 10); + let possibleSourceBranch: string | undefined; + for (const b of possibleSourceBranches) { + if (b.startsWith('(no branch, rebasing')) continue; - const possibleSourceBranches = await this.getCommitBranches(repoPath, onto, { mode: 'pointsAt' }); + possibleSourceBranch = b; + break; + } - let possibleSourceBranch: string | undefined; - for (const b of possibleSourceBranches) { - if (b.startsWith('(no branch, rebasing')) continue; + status = { + type: 'rebase', + repoPath: repoPath, + mergeBase: mergeBase, + HEAD: GitReference.create(rebase, repoPath, { refType: 'revision' }), + current: + possibleSourceBranch != null + ? GitReference.create(possibleSourceBranch, repoPath, { + refType: 'branch', + name: possibleSourceBranch, + remote: false, + }) + : undefined, + + incoming: GitReference.create(branch, repoPath, { + refType: 'branch', + name: branch, + remote: false, + }), + step: step, + stepCurrent: GitReference.create(rebase, repoPath, { + refType: 'revision', + message: stepMessage, + }), + steps: steps, + }; + } - possibleSourceBranch = b; - break; + const repo = await this.getRepository(repoPath); + if (repo?.supportsChangeEvents) { + this._rebaseStatusCache.set(repoPath, status ?? null); + } } - return { - type: 'rebase', - repoPath: repoPath, - mergeBase: mergeBase, - HEAD: GitReference.create(rebase, repoPath, { refType: 'revision' }), - current: - possibleSourceBranch != null - ? GitReference.create(possibleSourceBranch, repoPath, { - refType: 'branch', - name: possibleSourceBranch, - remote: false, - }) - : undefined, - - incoming: GitReference.create(branch, repoPath, { - refType: 'branch', - name: branch, - remote: false, - }), - step: step, - stepCurrent: GitReference.create(rebase, repoPath, { - refType: 'revision', - message: textDecoder.decode(stepMessageBytes).trim(), - }), - steps: steps, - }; + return status ?? undefined; } @log() diff --git a/src/git/models/rebase.ts b/src/git/models/rebase.ts index 426980e..29952a5 100644 --- a/src/git/models/rebase.ts +++ b/src/git/models/rebase.ts @@ -9,7 +9,7 @@ export interface GitRebaseStatus { current: GitBranchReference | undefined; incoming: GitBranchReference; - step: number; + step: number | undefined; stepCurrent: GitRevisionReference; - steps: number; + steps: number | undefined; } diff --git a/src/views/nodes/rebaseStatusNode.ts b/src/views/nodes/rebaseStatusNode.ts index 85466ba..affe717 100644 --- a/src/views/nodes/rebaseStatusNode.ts +++ b/src/views/nodes/rebaseStatusNode.ts @@ -84,9 +84,13 @@ export class RebaseStatusNode extends ViewNode { const item = new TreeItem( `${this.status?.hasConflicts ? 'Resolve conflicts to continue rebasing' : 'Rebasing'} ${ this.rebaseStatus.incoming != null - ? `${GitReference.toString(this.rebaseStatus.incoming, { expand: false, icon: false })} ` + ? `${GitReference.toString(this.rebaseStatus.incoming, { expand: false, icon: false })}` : '' - }(${this.rebaseStatus.step}/${this.rebaseStatus.steps})`, + }${ + this.rebaseStatus.step != null && this.rebaseStatus.steps != null + ? ` (${this.rebaseStatus.step}/${this.rebaseStatus.steps})` + : '' + }`, TreeItemCollapsibleState.Expanded, ); item.id = this.id; @@ -100,9 +104,11 @@ export class RebaseStatusNode extends ViewNode { item.tooltip = new MarkdownString( `${`Rebasing ${ this.rebaseStatus.incoming != null ? GitReference.toString(this.rebaseStatus.incoming) : '' - }onto ${GitReference.toString(this.rebaseStatus.current)}`}\n\nStep ${this.rebaseStatus.step} of ${ - this.rebaseStatus.steps - }\\\nStopped at ${GitReference.toString(this.rebaseStatus.stepCurrent, { icon: true })}${ + }onto ${GitReference.toString(this.rebaseStatus.current)}`}${ + this.rebaseStatus.step != null && this.rebaseStatus.steps != null + ? `\n\nStep ${this.rebaseStatus.step} of ${this.rebaseStatus.steps}\\\n` + : '\n\n' + }Stopped at ${GitReference.toString(this.rebaseStatus.stepCurrent, { icon: true })}${ this.status?.hasConflicts ? `\n\n${Strings.pluralize('conflicted file', this.status.conflicts.length)}` : ''