Skip to content

Commit 29ab4ab

Browse files
committed
Fixes issues with stash files untracked & stats
1 parent 839131a commit 29ab4ab

File tree

8 files changed

+217
-105
lines changed

8 files changed

+217
-105
lines changed

src/env/node/git/git.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2067,6 +2067,10 @@ export class Git {
20672067
}
20682068
}
20692069

2070+
stash(repoPath: string, ...args: string[]) {
2071+
return this.git<string>({ cwd: repoPath }, 'stash', ...args);
2072+
}
2073+
20702074
async status(
20712075
repoPath: string,
20722076
porcelainVersion: number = 1,

src/env/node/git/localGitProvider.ts

Lines changed: 86 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ import type {
8686
GitDiffShortStat,
8787
} from '../../../git/models/diff';
8888
import type { GitFile, GitFileStatus } from '../../../git/models/file';
89-
import { GitFileChange } from '../../../git/models/file';
89+
import { GitFileChange, GitFileWorkingTreeStatus, mapFilesWithStats } from '../../../git/models/file';
9090
import type {
9191
GitGraph,
9292
GitGraphRow,
@@ -143,10 +143,11 @@ import {
143143
parseGitDiffShortStat,
144144
parseGitFileDiff,
145145
} from '../../../git/parsers/diffParser';
146+
import type { ParserWithFilesAndMaybeStats } from '../../../git/parsers/logParser';
146147
import {
147148
createLogParserSingle,
148149
createLogParserWithFiles,
149-
createLogParserWithFileStats,
150+
createLogParserWithFilesAndStats,
150151
getContributorsParser,
151152
getGraphParser,
152153
getGraphStatsParser,
@@ -2362,7 +2363,7 @@ export class LocalGitProvider implements GitProvider, Disposable {
23622363

23632364
@log()
23642365
async getCommitFileStats(repoPath: string, ref: string): Promise<GitFileChange[] | undefined> {
2365-
const parser = createLogParserWithFileStats<{ sha: string }>({ sha: '%H' });
2366+
const parser = createLogParserWithFilesAndStats<{ sha: string }>({ sha: '%H' });
23662367

23672368
const data = await this.git.log(repoPath, { ref: ref }, '--max-count=1', ...parser.arguments);
23682369
if (data == null) return undefined;
@@ -5152,6 +5153,88 @@ export class LocalGitProvider implements GitProvider, Disposable {
51525153
return gitStash ?? undefined;
51535154
}
51545155

5156+
@log()
5157+
async getStashCommitFiles(
5158+
repoPath: string,
5159+
ref: string,
5160+
options?: { include?: { stats?: boolean } },
5161+
): Promise<GitFileChange[]> {
5162+
const [stashFilesResult, stashUntrackedFilesResult, stashFilesStatsResult] = await Promise.allSettled([
5163+
// Don't include untracked files here, because we won't be able to tell them apart from added (and we need the untracked status)
5164+
this.getStashCommitFilesCore(repoPath, ref, { untracked: false }),
5165+
// Check for any untracked files -- since git doesn't return them via `git stash list` :(
5166+
// See https://stackoverflow.com/questions/12681529/
5167+
this.getStashCommitFilesCore(repoPath, ref, { untracked: 'only' }),
5168+
options?.include?.stats
5169+
? this.getStashCommitFilesCore(repoPath, ref, { untracked: true, stats: true })
5170+
: undefined,
5171+
]);
5172+
5173+
let files = getSettledValue(stashFilesResult);
5174+
const untrackedFiles = getSettledValue(stashUntrackedFilesResult);
5175+
5176+
if (files?.length && untrackedFiles?.length) {
5177+
files.push(...untrackedFiles);
5178+
} else {
5179+
files = files ?? untrackedFiles;
5180+
}
5181+
5182+
files ??= [];
5183+
5184+
if (stashFilesStatsResult.status === 'fulfilled' && stashFilesStatsResult.value != null) {
5185+
files = mapFilesWithStats(files, stashFilesStatsResult.value);
5186+
}
5187+
5188+
return files;
5189+
}
5190+
5191+
private async getStashCommitFilesCore(
5192+
repoPath: string,
5193+
ref: string,
5194+
options?: { untracked?: boolean | 'only'; stats?: boolean },
5195+
): Promise<GitFileChange[] | undefined> {
5196+
const args = ['show'];
5197+
if (options?.untracked) {
5198+
args.push(options?.untracked === 'only' ? '--only-untracked' : '--include-untracked');
5199+
}
5200+
5201+
const similarityThreshold = configuration.get('advanced.similarityThreshold');
5202+
if (similarityThreshold != null) {
5203+
args.push(`-M${similarityThreshold}%`);
5204+
}
5205+
5206+
const parser: ParserWithFilesAndMaybeStats<object> = options?.stats
5207+
? createLogParserWithFilesAndStats()
5208+
: createLogParserWithFiles();
5209+
const data = await this.git.stash(repoPath, ...args, ...parser.arguments, ref);
5210+
5211+
for (const s of parser.parse(data)) {
5212+
return (
5213+
s.files?.map(
5214+
f =>
5215+
new GitFileChange(
5216+
repoPath,
5217+
f.path,
5218+
(options?.untracked === 'only'
5219+
? GitFileWorkingTreeStatus.Untracked
5220+
: f.status) as GitFileStatus,
5221+
f.originalPath,
5222+
undefined,
5223+
f.additions || f.deletions
5224+
? {
5225+
additions: f.additions ?? 0,
5226+
deletions: f.deletions ?? 0,
5227+
changes: 0,
5228+
}
5229+
: undefined,
5230+
),
5231+
) ?? []
5232+
);
5233+
}
5234+
5235+
return undefined;
5236+
}
5237+
51555238
@log()
51565239
async getStatusForFile(repoPath: string, pathOrUri: string | Uri): Promise<GitStatusFile | undefined> {
51575240
const status = await this.getStatus(repoPath);

src/git/gitProvider.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,11 @@ export interface GitProviderRepository {
353353
): Promise<GitRemote[]>;
354354
getRevisionContent(repoPath: string, path: string, ref: string): Promise<Uint8Array | undefined>;
355355
getStash?(repoPath: string | undefined): Promise<GitStash | undefined>;
356+
getStashCommitFiles?(
357+
repoPath: string,
358+
ref: string,
359+
options?: { include?: { stats?: boolean } },
360+
): Promise<GitFileChange[]>;
356361
getStatus(repoPath: string | undefined): Promise<GitStatus | undefined>;
357362
getStatusForFile(repoPath: string, uri: Uri): Promise<GitStatusFile | undefined>;
358363
getStatusForFiles(repoPath: string, pathOrGlob: Uri): Promise<GitStatusFile[] | undefined>;

src/git/gitProviderService.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2611,6 +2611,17 @@ export class GitProviderService implements Disposable {
26112611
return provider.getStash?.(path);
26122612
}
26132613

2614+
@gate()
2615+
@log()
2616+
async getStashCommitFiles(
2617+
repoPath: string | Uri,
2618+
ref: string,
2619+
options?: { include?: { stats?: boolean } },
2620+
): Promise<GitFileChange[]> {
2621+
const { provider, path } = this.getProvider(repoPath);
2622+
return provider.getStashCommitFiles?.(path, ref, options) ?? [];
2623+
}
2624+
26142625
@log()
26152626
async getStatus(repoPath: string | Uri | undefined): Promise<GitStatus | undefined> {
26162627
if (repoPath == null) return undefined;

src/git/models/commit.ts

Lines changed: 42 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,14 @@ import { formatDate, fromNow } from '../../system/date';
88
import { gate } from '../../system/decorators/gate';
99
import { memoize } from '../../system/decorators/memoize';
1010
import { getLoggableName } from '../../system/logger';
11+
import { getSettledValue } from '../../system/promise';
1112
import { pluralize } from '../../system/string';
1213
import type { PreviousLineComparisonUrisResult } from '../gitProvider';
1314
import { GitUri } from '../gitUri';
1415
import type { RemoteProvider } from '../remotes/remoteProvider';
1516
import { uncommitted, uncommittedStaged } from './constants';
1617
import type { GitFile } from './file';
17-
import { GitFileChange, GitFileWorkingTreeStatus } from './file';
18+
import { GitFileChange, mapFilesWithStats } from './file';
1819
import type { PullRequest } from './pullRequest';
1920
import type { GitReference, GitRevisionReference, GitStashReference } from './reference';
2021
import { isSha, isUncommitted, isUncommittedParent, isUncommittedStaged } from './reference';
@@ -226,77 +227,49 @@ export class GitCommit implements GitRevisionReference {
226227
return;
227228
}
228229

229-
const [commitResult, untrackedResult, commitFilesStatsResult] = await Promise.allSettled([
230-
this.container.git.getCommit(this.repoPath, this.refType === 'stash' ? `${this.stashName}^2` : this.sha),
231-
// Check for any untracked files -- since git doesn't return them via `git stash list` :(
232-
// See https://stackoverflow.com/questions/12681529/
233-
this.refType === 'stash' && !this._stashUntrackedFilesLoaded
234-
? this.container.git.getCommit(this.repoPath, `${this.stashName}^3`)
235-
: undefined,
236-
options?.include?.stats
237-
? this.container.git.getCommitFileStats(
238-
this.repoPath,
239-
this.refType === 'stash' ? `${this.stashName}^2` : this.sha,
240-
)
241-
: undefined,
242-
this.getPreviousSha(),
243-
]);
244-
245-
let commit;
246-
247-
if (commitResult.status === 'fulfilled' && commitResult.value != null) {
248-
commit = commitResult.value;
249-
this.parents.push(...(commit.parents ?? []));
250-
this._summary = commit.summary;
251-
this._message = commit.message;
252-
this._files = commit.files as GitFileChange[];
253-
254-
if (commitFilesStatsResult.status === 'fulfilled' && commitFilesStatsResult.value != null) {
255-
this._files = this._files.map(file => {
256-
const fileWithStats = commitFilesStatsResult.value!.find(f => f.path === file.path);
257-
return fileWithStats != null
258-
? new GitFileChange(
259-
file.repoPath,
260-
file.path,
261-
file.status,
262-
file.originalPath,
263-
file.previousSha,
264-
fileWithStats.stats,
265-
)
266-
: file;
267-
});
230+
if (this.refType === 'stash') {
231+
const [stashFilesResult] = await Promise.allSettled([
232+
this.container.git.getStashCommitFiles(this.repoPath, this.sha, options),
233+
this.getPreviousSha(),
234+
]);
235+
236+
const stashFiles = getSettledValue(stashFilesResult);
237+
if (stashFiles?.length) {
238+
this._files = stashFiles;
239+
}
240+
this._stashUntrackedFilesLoaded = true;
241+
} else {
242+
const [commitResult, commitFilesStatsResult] = await Promise.allSettled([
243+
this.container.git.getCommit(this.repoPath, this.sha),
244+
options?.include?.stats ? this.container.git.getCommitFileStats(this.repoPath, this.sha) : undefined,
245+
this.getPreviousSha(),
246+
]);
247+
248+
const commit = getSettledValue(commitResult);
249+
if (commit != null) {
250+
this.parents.push(...(commit.parents ?? []));
251+
this._summary = commit.summary;
252+
this._message = commit.message;
253+
this._files = (commit.files ?? []) as Mutable<typeof commit.files>;
268254
}
269255

270-
if (this._file != null) {
271-
const file = this._files.find(f => f.path === this._file!.path);
272-
if (file != null) {
273-
this._file = new GitFileChange(
274-
file.repoPath,
275-
file.path,
276-
file.status,
277-
file.originalPath ?? this._file.originalPath,
278-
file.previousSha ?? this._file.previousSha,
279-
file.stats ?? this._file.stats,
280-
);
281-
}
256+
const commitFilesStats = getSettledValue(commitFilesStatsResult);
257+
if (commitFilesStats?.length && this._files?.length) {
258+
this._files = mapFilesWithStats(this._files, commitFilesStats);
282259
}
283260
}
284261

285-
if (untrackedResult.status === 'fulfilled' && untrackedResult.value != null) {
286-
this._stashUntrackedFilesLoaded = true;
287-
288-
commit = untrackedResult.value;
289-
if (commit?.files != null && commit.files.length !== 0) {
290-
// Since these files are untracked -- make them look that way
291-
const files = commit.files.map(
292-
f => new GitFileChange(this.repoPath, f.path, GitFileWorkingTreeStatus.Untracked, f.originalPath),
262+
if (this._files != null && this._file != null) {
263+
const file = this._files.find(f => f.path === this._file!.path);
264+
if (file != null) {
265+
this._file = new GitFileChange(
266+
file.repoPath,
267+
file.path,
268+
file.status,
269+
file.originalPath ?? this._file.originalPath,
270+
file.previousSha ?? this._file.previousSha,
271+
file.stats ?? this._file.stats,
293272
);
294-
295-
if (this._files == null) {
296-
this._files = files;
297-
} else {
298-
this._files.push(...files);
299-
}
300273
}
301274
}
302275

@@ -627,13 +600,13 @@ export class GitCommit implements GitRevisionReference {
627600
return this.container.git.hasCommitBeenPushed(this.repoPath, this.ref);
628601
}
629602

630-
with(changes: {
603+
with<T extends GitCommit>(changes: {
631604
sha?: string;
632605
parents?: string[];
633606
files?: { file?: GitFileChange | null; files?: GitFileChange[] | null } | null;
634607
lines?: GitCommitLine[];
635608
stats?: GitCommitStats;
636-
}): GitCommit {
609+
}): T {
637610
let files;
638611
if (changes.files != null) {
639612
files = { file: this._file, files: this._files };
@@ -668,7 +641,7 @@ export class GitCommit implements GitRevisionReference {
668641
this.tips,
669642
this.stashName,
670643
this.stashOnRef,
671-
);
644+
) as T;
672645
}
673646

674647
protected getChangedValue<T>(change: T | null | undefined, original: T | undefined): T | undefined {

src/git/models/file.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,3 +312,20 @@ export class GitFileChange implements GitFileChangeShape {
312312
export function isGitFileChange(file: any): file is GitFileChange {
313313
return file instanceof GitFileChange;
314314
}
315+
316+
export function mapFilesWithStats(files: GitFileChange[], filesWithStats: GitFileChange[]): GitFileChange[] {
317+
return files.map(file => {
318+
const stats = filesWithStats.find(f => f.path === file.path)?.stats;
319+
return stats != null
320+
? new GitFileChange(
321+
file.repoPath,
322+
file.path,
323+
file.status,
324+
file.originalPath,
325+
file.previousSha,
326+
stats,
327+
file.staged,
328+
)
329+
: file;
330+
});
331+
}

0 commit comments

Comments
 (0)