Skip to content

Commit aeb49a3

Browse files
eamodiod13
authored andcommitted
Fixes #4201 adds git version check to for-each-ref
Refactors Git feature version detection - Consolidates version requirements into a single source of truth - Standardizes feature capability detection across commands
1 parent 5b90b67 commit aeb49a3

File tree

13 files changed

+109
-70
lines changed

13 files changed

+109
-70
lines changed

src/commands/git/push.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ export class PushGitCommand extends QuickCommand<State> {
157157
const useForceIfIncludes =
158158
useForceWithLease &&
159159
(configuration.getCore('git.useForcePushIfIncludes') ?? true) &&
160-
(await this.container.git.supports(state.repos[0].uri, 'forceIfIncludes'));
160+
(await this.container.git.supports(state.repos[0].uri, 'git:push:force-if-includes'));
161161

162162
let step: QuickPickStep<FlagsQuickPickItem<Flags>>;
163163

src/commands/stashSave.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ export class StashSaveCommand extends GlCommandBase {
7676
const repo = await this.container.git.getOrOpenRepository(uris[0]);
7777

7878
args.repoPath = repo?.path;
79-
args.onlyStaged = repo != null && hasOnlyStaged ? await repo.git.supports('stashOnlyStaged') : false;
79+
args.onlyStaged = repo != null && hasOnlyStaged ? await repo.git.supports('git:stash:push:staged') : false;
8080
if (args.keepStaged == null && !hasStaged) {
8181
args.keepStaged = true;
8282
}
@@ -115,7 +115,7 @@ export class StashSaveCommand extends GlCommandBase {
115115
const repo = await this.container.git.getOrOpenRepository(uris[0]);
116116

117117
args.repoPath = repo?.path;
118-
args.onlyStaged = repo != null && hasOnlyStaged ? await repo.git.supports('stashOnlyStaged') : false;
118+
args.onlyStaged = repo != null && hasOnlyStaged ? await repo.git.supports('git:stash:push:staged') : false;
119119
if (args.keepStaged == null && !hasStaged) {
120120
args.keepStaged = true;
121121
}

src/env/node/git/git.ts

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import type { CancellationToken, OutputChannel } from 'vscode';
77
import { env, Uri, window, workspace } from 'vscode';
88
import { hrtime } from '@env/hrtime';
99
import { GlyphChars } from '../../../constants';
10+
import type { GitFeature } from '../../../features';
11+
import { gitFeaturesByVersion } from '../../../features';
1012
import type { GitCommandOptions, GitSpawnOptions } from '../../../git/commandOptions';
1113
import { GitErrorHandling } from '../../../git/commandOptions';
1214
import {
@@ -41,6 +43,7 @@ import { Logger } from '../../../system/logger';
4143
import { slowCallWarningThreshold } from '../../../system/logger.constants';
4244
import { getLoggableScopeBlockOverride, getLogScope } from '../../../system/logger.scope';
4345
import { dirname, isAbsolute, isFolderGlob, joinPaths, normalizePath } from '../../../system/path';
46+
import { isPromise } from '../../../system/promise';
4447
import { getDurationMilliseconds } from '../../../system/string';
4548
import { compare, fromString } from '../../../system/version';
4649
import { ensureGitTerminal } from '../../../terminal';
@@ -362,27 +365,29 @@ export class Git {
362365
return (await this.getLocation()).path;
363366
}
364367

365-
async version(): Promise<string> {
366-
return (await this.getLocation()).version;
367-
}
368+
async ensureSupports(feature: GitFeature, prefix: string, suffix: string): Promise<void> {
369+
const version = gitFeaturesByVersion.get(feature);
370+
if (version == null) return;
368371

369-
async ensureGitVersion(version: string, prefix: string, suffix: string): Promise<void> {
370-
if (await this.isAtLeastVersion(version)) return;
372+
const gitVersion = await this.version();
373+
if (compare(fromString(gitVersion), fromString(version)) !== -1) return;
371374

372375
throw new Error(
373-
`${prefix} requires a newer version of Git (>= ${version}) than is currently installed (${await this.version()}).${suffix}`,
376+
`${prefix} requires a newer version of Git (>= ${version}) than is currently installed (${gitVersion}).${suffix}`,
374377
);
375378
}
376379

377-
async isAtLeastVersion(minimum: string): Promise<boolean> {
378-
const result = compare(fromString(await this.version()), fromString(minimum));
379-
return result !== -1;
380-
}
380+
supports(feature: GitFeature): boolean | Promise<boolean> {
381+
const version = gitFeaturesByVersion.get(feature);
382+
if (version == null) return true;
381383

382-
maybeIsAtLeastVersion(minimum: string): boolean | undefined {
383384
return this._gitLocation != null
384-
? compare(fromString(this._gitLocation.version), fromString(minimum)) !== -1
385-
: undefined;
385+
? compare(fromString(this._gitLocation.version), fromString(version)) !== -1
386+
: this.version().then(v => compare(fromString(v), fromString(version)) !== -1);
387+
}
388+
389+
async version(): Promise<string> {
390+
return (await this.getLocation()).version;
386391
}
387392

388393
// Git commands
@@ -427,10 +432,10 @@ export class Git {
427432
}
428433

429434
// Ensure the version of Git supports the --ignore-revs-file flag, otherwise the blame will fail
430-
let supportsIgnoreRevsFile = this.maybeIsAtLeastVersion('2.23');
431-
if (supportsIgnoreRevsFile === undefined) {
432-
supportsIgnoreRevsFile = await this.isAtLeastVersion('2.23');
433-
}
435+
const supportsIgnoreRevsFileResult = this.supports('git:ignoreRevsFile');
436+
let supportsIgnoreRevsFile = isPromise(supportsIgnoreRevsFileResult)
437+
? await supportsIgnoreRevsFileResult
438+
: supportsIgnoreRevsFileResult;
434439

435440
const ignoreRevsIndex = params.indexOf('--ignore-revs-file');
436441

@@ -825,7 +830,7 @@ export class Git {
825830
if (options.force.withLease) {
826831
params.push('--force-with-lease');
827832
if (options.force.ifIncludes) {
828-
if (await this.isAtLeastVersion('2.30.0')) {
833+
if (await this.supports('git:push:force-if-includes')) {
829834
params.push('--force-if-includes');
830835
}
831836
}
@@ -1531,10 +1536,14 @@ export class Git {
15311536
}
15321537

15331538
if (options?.onlyStaged) {
1534-
if (await this.isAtLeastVersion('2.35')) {
1539+
if (await this.supports('git:stash:push:staged')) {
15351540
params.push('--staged');
15361541
} else {
1537-
throw new Error('Git version 2.35 or higher is required for --staged');
1542+
throw new Error(
1543+
`Git version ${gitFeaturesByVersion.get(
1544+
'git:stash:push:staged',
1545+
)}}2.35 or higher is required for --staged`,
1546+
);
15381547
}
15391548
}
15401549

@@ -1584,7 +1593,7 @@ export class Git {
15841593
'--branch',
15851594
'-u',
15861595
];
1587-
if (await this.isAtLeastVersion('2.18')) {
1596+
if (await this.supports('git:status:find-renames')) {
15881597
params.push(
15891598
`--find-renames${options?.similarityThreshold == null ? '' : `=${options.similarityThreshold}%`}`,
15901599
);

src/env/node/git/localGitProvider.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -463,21 +463,21 @@ export class LocalGitProvider implements GitProvider, Disposable {
463463
if (supported != null) return supported;
464464

465465
switch (feature) {
466-
case 'worktrees' satisfies Features:
467-
supported = await this.git.isAtLeastVersion('2.17.0');
468-
this._supportedFeatures.set(feature, supported);
469-
return supported;
470-
case 'stashOnlyStaged' satisfies Features:
471-
supported = await this.git.isAtLeastVersion('2.35.0');
472-
this._supportedFeatures.set(feature, supported);
473-
return supported;
474-
case 'forceIfIncludes' satisfies Features:
475-
supported = await this.git.isAtLeastVersion('2.30.0');
476-
this._supportedFeatures.set(feature, supported);
477-
return supported;
466+
case 'stashes' satisfies Features:
467+
case 'timeline' satisfies Features:
468+
supported = true;
469+
break;
478470
default:
479-
return true;
471+
if (feature.startsWith('git:')) {
472+
supported = await this.git.supports(feature);
473+
} else {
474+
supported = true;
475+
}
476+
break;
480477
}
478+
479+
this._supportedFeatures.set(feature, supported);
480+
return supported;
481481
}
482482

483483
@debug<LocalGitProvider['visibility']>({ exit: r => `returned ${r[0]}` })

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
133133
if (resultsPromise == null) {
134134
async function load(this: BranchesGitSubProvider): Promise<PagedResult<GitBranch>> {
135135
try {
136-
const parser = getBranchParser();
136+
const parser = getBranchParser(await this.git.supports('git:for-each-ref:worktreePath'));
137137

138138
const data = await this.git.exec(
139139
{ cwd: repoPath },
@@ -167,7 +167,7 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
167167
hasCurrent = true;
168168
}
169169

170-
const worktreePath = normalizePath(entry.worktreePath);
170+
const worktreePath = entry.worktreePath ? normalizePath(entry.worktreePath) : undefined;
171171

172172
branches.push(
173173
new GitBranch(
@@ -511,7 +511,7 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
511511

512512
try {
513513
// If we have don't have Git v2.33+, just return
514-
if (!(await this.git.isAtLeastVersion('2.33'))) {
514+
if (!(await this.git.supports('git:merge-tree'))) {
515515
return undefined;
516516
}
517517

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ export class RefsGitSubProvider implements GitRefsSubProvider {
9999

100100
@log()
101101
async getSymbolicReferenceName(repoPath: string, ref: string): Promise<string | undefined> {
102-
const supportsEndOfOptions = await this.git.isAtLeastVersion('2.30');
102+
const supportsEndOfOptions = await this.git.supports('git:rev-parse:end-of-options');
103103

104104
const data = await this.git.exec(
105105
{ cwd: repoPath, errors: GitErrorHandling.Ignore },
@@ -200,7 +200,7 @@ export class RefsGitSubProvider implements GitRefsSubProvider {
200200
if (!ref) return undefined;
201201
if (ref === deletedOrMissing || isUncommitted(ref)) return ref;
202202

203-
const supportsEndOfOptions = await this.git.isAtLeastVersion('2.30');
203+
const supportsEndOfOptions = await this.git.supports('git:rev-parse:end-of-options');
204204

205205
const data = await this.git.exec(
206206
{ cwd: repoPath, errors: GitErrorHandling.Ignore },

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

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -368,25 +368,24 @@ export class StashGitSubProvider implements GitStashSubProvider {
368368
return;
369369
}
370370

371-
await this.git.ensureGitVersion(
372-
'2.13.2',
371+
await this.git.ensureSupports(
372+
'git:stash:push:pathspecs',
373373
'Stashing individual files',
374374
' Please retry by stashing everything or install a more recent version of Git and try again.',
375375
);
376376

377377
const pathspecs = uris.map(u => `./${splitPath(u, repoPath)[0]}`);
378378

379-
const stdinVersion = '2.30.0';
380-
let stdin = await this.git.isAtLeastVersion(stdinVersion);
379+
let stdin = await this.git.supports('git:stash:push:stdin');
381380
if (stdin && options?.onlyStaged && uris.length) {
382381
// Since Git doesn't support --staged with --pathspec-from-file try to pass them in directly
383382
stdin = false;
384383
}
385384

386385
// If we don't support stdin, then error out if we are over the maximum allowed git cli length
387386
if (!stdin && countStringLength(pathspecs) > maxGitCliLength) {
388-
await this.git.ensureGitVersion(
389-
stdinVersion,
387+
await this.git.ensureSupports(
388+
'git:stash:push:stdin',
390389
`Stashing so many files (${pathspecs.length}) at once`,
391390
' Please retry by stashing fewer files or install a more recent version of Git and try again.',
392391
);

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -493,7 +493,7 @@ export class StatusGitSubProvider implements GitStatusSubProvider {
493493
async getStatus(repoPath: string | undefined): Promise<GitStatus | undefined> {
494494
if (repoPath == null) return undefined;
495495

496-
const porcelainVersion = (await this.git.isAtLeastVersion('2.11')) ? 2 : 1;
496+
const porcelainVersion = (await this.git.supports('git:status:porcelain-v2')) ? 2 : 1;
497497

498498
const data = await this.git.status(repoPath, porcelainVersion, {
499499
similarityThreshold: configuration.get('advanced.similarityThreshold') ?? undefined,
@@ -529,7 +529,7 @@ export class StatusGitSubProvider implements GitStatusSubProvider {
529529
async getStatusForPath(repoPath: string, pathOrGlob: string | Uri): Promise<GitStatusFile[] | undefined> {
530530
const [relativePath] = splitPath(pathOrGlob, repoPath);
531531

532-
const porcelainVersion = (await this.git.isAtLeastVersion('2.11')) ? 2 : 1;
532+
const porcelainVersion = (await this.git.supports('git:status:porcelain-v2')) ? 2 : 1;
533533

534534
const data = await this.git.status(
535535
repoPath,

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,8 @@ export class WorktreesGitSubProvider implements GitWorktreesSubProvider {
9191

9292
@log()
9393
async getWorktrees(repoPath: string): Promise<GitWorktree[]> {
94-
await this.git.ensureGitVersion(
95-
'2.7.6',
94+
await this.git.ensureSupports(
95+
'git:worktrees:list',
9696
'Displaying worktrees',
9797
' Please install a more recent version of Git and try again.',
9898
);
@@ -158,8 +158,8 @@ export class WorktreesGitSubProvider implements GitWorktreesSubProvider {
158158
async deleteWorktree(repoPath: string, path: string | Uri, options?: { force?: boolean }): Promise<void> {
159159
const scope = getLogScope();
160160

161-
await this.git.ensureGitVersion(
162-
'2.17.0',
161+
await this.git.ensureSupports(
162+
'git:worktrees:delete',
163163
'Deleting worktrees',
164164
' Please install a more recent version of Git and try again.',
165165
);

src/features.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,39 @@ import type { RepositoryVisibility } from './git/gitProvider';
44
import type { RequiredSubscriptionPlans, Subscription } from './plus/gk/models/subscription';
55
import { capitalize } from './system/string';
66

7-
export type Features = 'stashes' | 'timeline' | 'worktrees' | 'stashOnlyStaged' | 'forceIfIncludes';
7+
// GitFeature's must start with `git:` to be recognized in all usages
8+
export type GitFeature =
9+
| 'git:for-each-ref:worktreePath'
10+
| 'git:ignoreRevsFile'
11+
| 'git:merge-tree'
12+
| 'git:push:force-if-includes'
13+
| 'git:rev-parse:end-of-options'
14+
| 'git:stash:push:pathspecs'
15+
| 'git:stash:push:staged'
16+
| 'git:stash:push:stdin'
17+
| 'git:status:find-renames'
18+
| 'git:status:porcelain-v2'
19+
| 'git:worktrees'
20+
| 'git:worktrees:delete'
21+
| 'git:worktrees:list';
22+
23+
export const gitFeaturesByVersion = new Map<GitFeature, string>([
24+
['git:for-each-ref:worktreePath', '2.23'],
25+
['git:ignoreRevsFile', '2.23'],
26+
['git:merge-tree', '2.33'],
27+
['git:push:force-if-includes', '2.30.0'],
28+
['git:rev-parse:end-of-options', '2.30'],
29+
['git:stash:push:pathspecs', '2.13.2'],
30+
['git:stash:push:staged', '2.35.0'],
31+
['git:stash:push:stdin', '2.30.0'],
32+
['git:status:find-renames', '2.18'],
33+
['git:status:porcelain-v2', '2.11'],
34+
['git:worktrees', '2.17.0'],
35+
['git:worktrees:delete', '2.17.0'],
36+
['git:worktrees:list', '2.7.6'],
37+
]);
38+
39+
export type Features = 'stashes' | 'timeline' | GitFeature;
840

941
export type FeatureAccess =
1042
| {

0 commit comments

Comments
 (0)