Skip to content

Commit 7a7da0f

Browse files
committed
Refactors Git provider into sub-providers
- Revison operations Removes unneeded Git abstractions
1 parent 864a1e1 commit 7a7da0f

23 files changed

+694
-1033
lines changed

src/env/node/git/git.ts

Lines changed: 64 additions & 736 deletions
Large diffs are not rendered by default.

src/env/node/git/localGitProvider.ts

Lines changed: 23 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { GlyphChars, Schemes } from '../../../constants';
1111
import type { Container } from '../../../container';
1212
import { Features } from '../../../features';
1313
import { GitCache } from '../../../git/cache';
14+
import { GitErrorHandling } from '../../../git/commandOptions';
1415
import {
1516
BlameIgnoreRevsFileBadRevisionError,
1617
BlameIgnoreRevsFileError,
@@ -42,11 +43,9 @@ import { RemoteResourceType } from '../../../git/models/remoteResource';
4243
import type { RepositoryChangeEvent } from '../../../git/models/repository';
4344
import { Repository, RepositoryChange, RepositoryChangeComparisonMode } from '../../../git/models/repository';
4445
import { deletedOrMissing } from '../../../git/models/revision';
45-
import type { GitTreeEntry } from '../../../git/models/tree';
4646
import { parseGitBlame } from '../../../git/parsers/blameParser';
4747
import { parseGitFileDiff } from '../../../git/parsers/diffParser';
4848
import { parseGitLogSimpleFormat, parseGitLogSimpleRenamed } from '../../../git/parsers/logParser';
49-
import { parseGitLsFiles, parseGitTree } from '../../../git/parsers/treeParser';
5049
import { getBranchNameAndRemote, getBranchTrackingWithoutRemote } from '../../../git/utils/branch.utils';
5150
import { isBranchReference } from '../../../git/utils/reference.utils';
5251
import { getVisibilityCacheKey } from '../../../git/utils/remote.utils';
@@ -65,7 +64,7 @@ import { getBestPath, isFolderUri, relative, splitPath } from '../../../system/-
6564
import { gate } from '../../../system/decorators/-webview/gate';
6665
import { debug, log } from '../../../system/decorators/log';
6766
import { debounce } from '../../../system/function';
68-
import { first } from '../../../system/iterable';
67+
import { first, join } from '../../../system/iterable';
6968
import { Logger } from '../../../system/logger';
7069
import type { LogScope } from '../../../system/logger.scope';
7170
import { getLogScope, setLogScopeExit } from '../../../system/logger.scope';
@@ -89,6 +88,7 @@ import { GraphGitSubProvider } from './sub-providers/graph';
8988
import { PatchGitSubProvider } from './sub-providers/patch';
9089
import { RefsGitSubProvider } from './sub-providers/refs';
9190
import { RemotesGitSubProvider } from './sub-providers/remotes';
91+
import { RevisionGitSubProvider } from './sub-providers/revision';
9292
import { StagingGitSubProvider } from './sub-providers/staging';
9393
import { StashGitSubProvider } from './sub-providers/stash';
9494
import { StatusGitSubProvider } from './sub-providers/status';
@@ -935,7 +935,7 @@ export class LocalGitProvider implements GitProvider, Disposable {
935935
let patch;
936936
try {
937937
patch = await this.git.diff(root, relativePath, ref1, ref2);
938-
void (await this.git.apply(root, patch));
938+
void (await this.git.exec({ cwd: root, stdin: patch }, 'apply', '--whitespace=warn'));
939939
} catch (ex) {
940940
const msg: string = ex?.toString() ?? '';
941941
if (patch && /patch does not apply/i.test(msg)) {
@@ -949,7 +949,7 @@ export class LocalGitProvider implements GitProvider, Disposable {
949949

950950
if (result.title === 'Yes') {
951951
try {
952-
void (await this.git.apply(root, patch, { allowConflicts: true }));
952+
void (await this.git.exec({ cwd: root, stdin: patch }, 'apply', '--whitespace=warn', '--3way'));
953953

954954
return;
955955
} catch (e) {
@@ -1007,7 +1007,12 @@ export class LocalGitProvider implements GitProvider, Disposable {
10071007
async excludeIgnoredUris(repoPath: string, uris: Uri[]): Promise<Uri[]> {
10081008
const paths = new Map<string, Uri>(uris.map(u => [normalizePath(u.fsPath), u]));
10091009

1010-
const data = await this.git.check_ignore(repoPath, ...paths.keys());
1010+
const data = await this.git.exec(
1011+
{ cwd: repoPath, errors: GitErrorHandling.Ignore, stdin: join(paths.keys(), '\0') },
1012+
'check-ignore',
1013+
'-z',
1014+
'--stdin',
1015+
);
10111016
if (data == null) return uris;
10121017

10131018
const ignored = data.split('\0').filter(<T>(i?: T): i is T => Boolean(i));
@@ -1134,11 +1139,12 @@ export class LocalGitProvider implements GitProvider, Disposable {
11341139

11351140
// Since Git can't setup remote tracking when publishing a new branch to a specific commit, do it now
11361141
if (setUpstream != null) {
1137-
await this.git.branch__set_upstream(
1138-
repoPath,
1142+
await this.git.exec(
1143+
{ cwd: repoPath },
1144+
'branch',
1145+
'--set-upstream-to',
1146+
`${setUpstream.remote}/${setUpstream.remoteBranch}`,
11391147
setUpstream.branch,
1140-
setUpstream.remote,
1141-
setUpstream.remoteBranch,
11421148
);
11431149
}
11441150

@@ -1898,49 +1904,6 @@ export class LocalGitProvider implements GitProvider, Disposable {
18981904
return undefined;
18991905
}
19001906

1901-
@gate()
1902-
@log()
1903-
getRevisionContent(repoPath: string, path: string, ref: string): Promise<Uint8Array | undefined> {
1904-
const [relativePath, root] = splitPath(path, repoPath);
1905-
1906-
return this.git.show__content<Buffer>(root, relativePath, ref, { encoding: 'buffer' }) as Promise<
1907-
Uint8Array | undefined
1908-
>;
1909-
}
1910-
1911-
@log()
1912-
async getTreeEntryForRevision(repoPath: string, path: string, ref: string): Promise<GitTreeEntry | undefined> {
1913-
if (repoPath == null || !path) return undefined;
1914-
1915-
const [relativePath, root] = splitPath(path, repoPath);
1916-
1917-
if (isUncommittedStaged(ref)) {
1918-
const data = await this.git.ls_files(root, relativePath, { ref: ref });
1919-
const [result] = parseGitLsFiles(data);
1920-
if (result == null) return undefined;
1921-
1922-
const size = await this.git.cat_file__size(repoPath, result.oid);
1923-
return {
1924-
ref: ref,
1925-
oid: result.oid,
1926-
path: relativePath,
1927-
size: size,
1928-
type: 'blob',
1929-
};
1930-
}
1931-
1932-
const data = await this.git.ls_tree(root, ref, relativePath);
1933-
return parseGitTree(data, ref)[0];
1934-
}
1935-
1936-
@log()
1937-
async getTreeForRevision(repoPath: string, ref: string): Promise<GitTreeEntry[]> {
1938-
if (repoPath == null) return [];
1939-
1940-
const data = await this.git.ls_tree(repoPath, ref);
1941-
return parseGitTree(data, ref);
1942-
}
1943-
19441907
hasUnsafeRepositories(): boolean {
19451908
return this.unsafePaths.size !== 0;
19461909
}
@@ -2049,10 +2012,10 @@ export class LocalGitProvider implements GitProvider, Disposable {
20492012
}
20502013

20512014
if (!tracked && ref && !isUncommitted(ref)) {
2052-
tracked = Boolean(await this.git.ls_files(repoPath, relativePath, { ref: ref }));
2015+
tracked = Boolean(await this.git.ls_files(repoPath, relativePath, { rev: ref }));
20532016
// If we still haven't found this file, make sure it wasn't deleted in that ref (i.e. check the previous)
20542017
if (!tracked) {
2055-
tracked = Boolean(await this.git.ls_files(repoPath, relativePath, { ref: `${ref}^` }));
2018+
tracked = Boolean(await this.git.ls_files(repoPath, relativePath, { rev: `${ref}^` }));
20562019
}
20572020
}
20582021

@@ -2148,6 +2111,11 @@ export class LocalGitProvider implements GitProvider, Disposable {
21482111
return (this._remotes ??= new RemotesGitSubProvider(this.container, this.git, this._cache, this));
21492112
}
21502113

2114+
private _revision: RevisionGitSubProvider | undefined;
2115+
get revision(): RevisionGitSubProvider {
2116+
return (this._revision ??= new RevisionGitSubProvider(this.container, this.git, this._cache, this));
2117+
}
2118+
21512119
private _staging: StagingGitSubProvider | undefined;
21522120
get staging(): StagingGitSubProvider | undefined {
21532121
return (this._staging ??= new StagingGitSubProvider(this.container, this.git));

src/env/node/git/sub-providers/branches.ts

Lines changed: 69 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import type {
1212
import { GitBranch } from '../../../../git/models/branch';
1313
import type { MergeConflict } from '../../../../git/models/mergeConflict';
1414
import type { GitBranchReference } from '../../../../git/models/reference';
15-
import { parseGitBranches } from '../../../../git/parsers/branchParser';
15+
import { parseGitBranches, parseGitBranchesDefaultFormat } from '../../../../git/parsers/branchParser';
1616
import { parseMergeTreeConflict } from '../../../../git/parsers/mergeTreeParser';
1717
import { getReferenceFromBranch } from '../../../../git/utils/-webview/reference.utils';
1818
import type { BranchSortOptions } from '../../../../git/utils/-webview/sorting';
@@ -28,7 +28,9 @@ import { getLogScope } from '../../../../system/logger.scope';
2828
import { PageableResult } from '../../../../system/paging';
2929
import { getSettledValue } from '../../../../system/promise';
3030
import type { Git } from '../git';
31+
import { GitErrors, gitLogDefaultConfigs } from '../git';
3132
import type { LocalGitProvider } from '../localGitProvider';
33+
import { RunError } from '../shell';
3234

3335
const emptyPagedResult: PagedResult<any> = Object.freeze({ values: [] });
3436

@@ -76,7 +78,16 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
7678

7779
const [pausedOpStatusResult, committerDateResult] = await Promise.allSettled([
7880
isDetachedHead(name) ? this.provider.status?.getPausedOperationStatus(repoPath) : undefined,
79-
this.git.log__recent_committerdate(repoPath, commitOrdering),
81+
this.git
82+
.exec(
83+
{ cwd: repoPath, configs: gitLogDefaultConfigs, errors: GitErrorHandling.Ignore },
84+
'log',
85+
'-n1',
86+
'--format=%ct',
87+
commitOrdering ? `--${commitOrdering}-order` : undefined,
88+
'--',
89+
)
90+
.then(data => (!data.length ? undefined : data.trim())),
8091
]);
8192

8293
const committerDate = getSettledValue(committerDateResult);
@@ -114,7 +125,13 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
114125
if (resultsPromise == null) {
115126
async function load(this: BranchesGitSubProvider): Promise<PagedResult<GitBranch>> {
116127
try {
117-
const data = await this.git.for_each_ref__branch(repoPath, { all: true });
128+
const data = await this.git.exec(
129+
{ cwd: repoPath },
130+
'for-each-ref',
131+
`--format=${parseGitBranchesDefaultFormat}`,
132+
'refs/heads',
133+
'refs/remotes',
134+
);
118135
// If we don't get any data, assume the repo doesn't have any commits yet so check if we have a current branch
119136
if (!data?.length) {
120137
const current = await this.getCurrentBranch(repoPath);
@@ -275,7 +292,7 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
275292

276293
if (remote) {
277294
try {
278-
const data = await this.git.ls_remote__HEAD(repoPath, remote);
295+
const data = await this.git.exec({ cwd: repoPath }, 'ls-remote', '--symref', remote, 'HEAD');
279296
if (data == null) return undefined;
280297

281298
const match = /ref:\s(\S+)\s+HEAD/m.exec(data);
@@ -287,7 +304,7 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
287304
}
288305

289306
try {
290-
const data = await this.git.symbolic_ref(repoPath, `refs/remotes/origin/HEAD`);
307+
const data = await this.git.exec({ cwd: repoPath }, 'symbolic-ref', '--short', 'refs/remotes/origin/HEAD');
291308
return data?.trim() || undefined;
292309
} catch {}
293310

@@ -304,8 +321,14 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
304321
const scope = getLogScope();
305322

306323
try {
307-
const data = await this.git.merge_base(repoPath, ref1, ref2, options);
308-
if (data == null) return undefined;
324+
const data = await this.git.exec(
325+
{ cwd: repoPath },
326+
'merge-base',
327+
options?.forkPoint ? '--fork-point' : undefined,
328+
ref1,
329+
ref2,
330+
);
331+
if (!data) return undefined;
309332

310333
return data.split('\n')[0].trim() || undefined;
311334
} catch (ex) {
@@ -316,7 +339,7 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
316339

317340
@log()
318341
async createBranch(repoPath: string, name: string, sha: string): Promise<void> {
319-
await this.git.branch(repoPath, name, sha);
342+
await this.git.exec({ cwd: repoPath }, 'branch', name, sha);
320343
}
321344

322345
@log()
@@ -371,14 +394,7 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
371394
} catch {}
372395

373396
// Cherry-pick detection (handles cherry-picks, rebases, etc)
374-
const data = await this.git.exec<string>(
375-
{ cwd: repoPath },
376-
'cherry',
377-
'--abbrev',
378-
'-v',
379-
into.name,
380-
branch.name,
381-
);
397+
const data = await this.git.exec({ cwd: repoPath }, 'cherry', '--abbrev', '-v', into.name, branch.name);
382398
// Check if there are no lines or all lines startwith a `-` (i.e. likely merged)
383399
if (!data || data.split('\n').every(l => l.startsWith('-'))) {
384400
return { merged: true, confidence: 'high' };
@@ -415,9 +431,37 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
415431

416432
let data;
417433
try {
418-
data = await this.git.merge_tree(repoPath, branch, targetBranch, '-z', '--name-only', '--no-messages');
434+
data = await this.git.exec(
435+
{ cwd: repoPath, errors: GitErrorHandling.Throw },
436+
'merge-tree',
437+
'-z',
438+
'--name-only',
439+
'--no-messages',
440+
branch,
441+
targetBranch,
442+
);
419443
} catch (ex) {
420-
Logger.error(ex, scope);
444+
const msg: string = ex?.toString() ?? '';
445+
if (GitErrors.notAValidObjectName.test(msg)) {
446+
Logger.error(
447+
ex,
448+
scope,
449+
`'${targetBranch}' or '${branch}' not found - ensure the branches exist and are fully qualified (e.g. 'refs/heads/main')`,
450+
);
451+
} else if (GitErrors.badRevision.test(msg)) {
452+
Logger.error(ex, scope, `Invalid branch name: ${msg.slice(msg.indexOf("'"))}`);
453+
} else if (GitErrors.noMergeBase.test(msg)) {
454+
Logger.error(
455+
ex,
456+
scope,
457+
`Unable to merge '${branch}' and '${targetBranch}' as they have no common ancestor`,
458+
);
459+
} else if (ex instanceof RunError) {
460+
data = ex.stdout;
461+
} else {
462+
Logger.error(ex, scope);
463+
debugger;
464+
}
421465
}
422466
if (!data) return undefined;
423467

@@ -496,7 +540,7 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
496540
options?: { upstream: true },
497541
): Promise<string | undefined> {
498542
try {
499-
let data = await this.git.reflog(repoPath, undefined, ref, '--grep-reflog=branch: Created from *.');
543+
let data = await this.git.exec({ cwd: repoPath }, 'reflog', ref, '--grep-reflog=branch: Created from *.');
500544

501545
let entries = data.split('\n').filter(entry => Boolean(entry));
502546
if (entries.length !== 1) return undefined;
@@ -517,12 +561,13 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
517561
}
518562

519563
// Check if branch was created from HEAD
520-
data = await this.git.reflog(
521-
repoPath,
522-
undefined,
564+
data = await this.git.exec(
565+
{ cwd: repoPath },
566+
'reflog',
523567
'HEAD',
524568
`--grep-reflog=checkout: moving from .* to ${ref.replace('refs/heads/', '')}`,
525569
);
570+
526571
entries = data.split('\n').filter(entry => Boolean(entry));
527572
if (!entries.length) return undefined;
528573

@@ -562,11 +607,11 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
562607

563608
@log()
564609
async renameBranch(repoPath: string, oldName: string, newName: string): Promise<void> {
565-
await this.git.branch(repoPath, '-m', oldName, newName);
610+
await this.git.exec({ cwd: repoPath }, 'branch', '-m', oldName, newName);
566611
}
567612

568613
private async getValidatedBranchName(repoPath: string, name: string): Promise<string | undefined> {
569-
const data = await this.git.exec<string>(
614+
const data = await this.git.exec(
570615
{ cwd: repoPath, errors: GitErrorHandling.Ignore },
571616
'rev-parse',
572617
'--verify',

0 commit comments

Comments
 (0)