Skip to content

Commit

Permalink
Adds resiliency to reading .git files
Browse files Browse the repository at this point in the history
Adds caching to merge/rebase status
  • Loading branch information
eamodio committed Jan 6, 2021
1 parent 14d45f4 commit d8bce25
Show file tree
Hide file tree
Showing 4 changed files with 154 additions and 85 deletions.
38 changes: 37 additions & 1 deletion src/git/git.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';

Expand Down Expand Up @@ -1382,4 +1385,37 @@ export namespace Git {
export function tag(repoPath: string) {
return git<string>({ cwd: repoPath }, 'tag', '-l', `--format=${GitTagParser.defaultFormat}`);
}

export async function readDotGitFile(
repoPath: string,
paths: string[],
options?: { numeric?: false; throw?: boolean; trim?: boolean },
): Promise<string | undefined>;
export async function readDotGitFile(
repoPath: string,
path: string[],
options?: { numeric: true; throw?: boolean; trim?: boolean },
): Promise<number | undefined>;
export async function readDotGitFile(
repoPath: string,
pathParts: string[],
options?: { numeric?: boolean; throw?: boolean; trim?: boolean },
): Promise<string | number | undefined> {
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;
}
}
}
181 changes: 104 additions & 77 deletions src/git/gitService.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
'use strict';
import * as fs from 'fs';
import * as paths from 'path';
import { TextDecoder } from 'util';
import {
ConfigurationChangeEvent,
Disposable,
Expand Down Expand Up @@ -121,8 +120,6 @@ const weightedDefaultBranches = new Map<string, number>([
['development', 1],
]);

const textDecoder = new TextDecoder('utf8');

export class GitService implements Disposable {
private _onDidChangeRepositories = new EventEmitter<void>();
get onDidChangeRepositories(): Event<void> {
Expand All @@ -135,6 +132,8 @@ export class GitService implements Disposable {

private readonly _branchesCache = new Map<string, GitBranch[]>();
private readonly _contributorsCache = new Map<string, GitContributor[]>();
private readonly _mergeStatusCache = new Map<string, GitMergeStatus | null>();
private readonly _rebaseStatusCache = new Map<string, GitRebaseStatus | null>();
private readonly _remotesWithApiProviderCache = new Map<string, GitRemote<RichRemoteProvider> | null>();
private readonly _stashesCache = new Map<string, GitStash | null>();
private readonly _tagsCache = new Map<string, GitTag[]>();
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -2397,93 +2402,115 @@ export class GitService implements Disposable {
}
}

@gate()
@log()
async getMergeStatus(repoPath: string): Promise<GitMergeStatus | undefined> {
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<GitRebaseStatus | undefined> {
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()
Expand Down
4 changes: 2 additions & 2 deletions src/git/models/rebase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export interface GitRebaseStatus {
current: GitBranchReference | undefined;
incoming: GitBranchReference;

step: number;
step: number | undefined;
stepCurrent: GitRevisionReference;
steps: number;
steps: number | undefined;
}
16 changes: 11 additions & 5 deletions src/views/nodes/rebaseStatusNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,13 @@ export class RebaseStatusNode extends ViewNode<ViewsWithCommits> {
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;
Expand All @@ -100,9 +104,11 @@ export class RebaseStatusNode extends ViewNode<ViewsWithCommits> {
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)}`
: ''
Expand Down

0 comments on commit d8bce25

Please sign in to comment.