Skip to content

Commit fe0632c

Browse files
authored
Git - handle stashes that contain untracked files (microsoft#203572)
1 parent c0de87c commit fe0632c

File tree

4 files changed

+51
-20
lines changed

4 files changed

+51
-20
lines changed

extensions/git/src/commands.ts

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3709,25 +3709,44 @@ export class CommandCenter {
37093709
}
37103710

37113711
const stashChanges = await repository.showStash(stash.index);
3712-
const stashParentCommit = stash.parents.length > 0 ? stash.parents[0] : `${stash.hash}^`;
3713-
37143712
if (!stashChanges || stashChanges.length === 0) {
37153713
return;
37163714
}
37173715

3716+
// A stash commit can have up to 3 parents:
3717+
// 1. The first parent is the commit that was HEAD when the stash was created.
3718+
// 2. The second parent is the commit that represents the index when the stash was created.
3719+
// 3. The third parent (when present) represents the untracked files when the stash was created.
3720+
const stashFirstParentCommit = stash.parents.length > 0 ? stash.parents[0] : `${stash.hash}^`;
3721+
const stashUntrackedFilesParentCommit = stash.parents.length === 3 ? stash.parents[2] : undefined;
3722+
const stashUntrackedFiles: string[] = [];
3723+
3724+
if (stashUntrackedFilesParentCommit) {
3725+
const untrackedFiles = await repository.getObjectFiles(stashUntrackedFilesParentCommit);
3726+
stashUntrackedFiles.push(...untrackedFiles.map(f => path.join(repository.root, f.file)));
3727+
}
3728+
37183729
const title = `Git Stash #${stash.index}: ${stash.description}`;
37193730
const multiDiffSourceUri = toGitUri(Uri.file(repository.root), `stash@{${stash.index}}`, { scheme: 'git-stash' });
37203731

37213732
const resources: { originalUri: Uri | undefined; modifiedUri: Uri | undefined }[] = [];
37223733
for (const change of stashChanges) {
3723-
if (change.status === Status.INDEX_ADDED) {
3724-
resources.push({ originalUri: undefined, modifiedUri: toGitUri(change.uri, stash.hash) });
3725-
} else if (change.status === Status.DELETED) {
3726-
resources.push({ originalUri: toGitUri(change.uri, stashParentCommit), modifiedUri: undefined });
3727-
} else if (change.status === Status.INDEX_RENAMED) {
3728-
resources.push({ originalUri: toGitUri(change.originalUri, stashParentCommit), modifiedUri: toGitUri(change.uri, stash.hash) });
3729-
} else {
3730-
resources.push({ originalUri: toGitUri(change.uri, stashParentCommit), modifiedUri: toGitUri(change.uri, stash.hash) });
3734+
const isChangeUntracked = !!stashUntrackedFiles.find(f => pathEquals(f, change.uri.fsPath));
3735+
const modifiedUriRef = !isChangeUntracked ? stash.hash : stashUntrackedFilesParentCommit ?? stash.hash;
3736+
3737+
switch (change.status) {
3738+
case Status.INDEX_ADDED:
3739+
resources.push({ originalUri: undefined, modifiedUri: toGitUri(change.uri, modifiedUriRef) });
3740+
break;
3741+
case Status.DELETED:
3742+
resources.push({ originalUri: toGitUri(change.uri, stashFirstParentCommit), modifiedUri: undefined });
3743+
break;
3744+
case Status.INDEX_RENAMED:
3745+
resources.push({ originalUri: toGitUri(change.originalUri, stashFirstParentCommit), modifiedUri: toGitUri(change.uri, modifiedUriRef) });
3746+
break;
3747+
default:
3748+
resources.push({ originalUri: toGitUri(change.uri, stashFirstParentCommit), modifiedUri: toGitUri(change.uri, modifiedUriRef) });
3749+
break;
37313750
}
37323751
}
37333752

extensions/git/src/git.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -937,7 +937,7 @@ function parseGitDiffShortStat(data: string): CommitShortStat {
937937
return { files: parseInt(files), insertions: parseInt(insertions ?? '0'), deletions: parseInt(deletions ?? '0') };
938938
}
939939

940-
interface LsTreeElement {
940+
export interface LsTreeElement {
941941
mode: string;
942942
type: string;
943943
object: string;
@@ -1294,8 +1294,13 @@ export class Repository {
12941294
return { mode, object, size: parseInt(size) };
12951295
}
12961296

1297-
async lstree(treeish: string, path: string): Promise<LsTreeElement[]> {
1298-
const { stdout } = await this.exec(['ls-tree', '-l', treeish, '--', sanitizePath(path)]);
1297+
async lstree(treeish: string, path?: string): Promise<LsTreeElement[]> {
1298+
const args = ['ls-tree', '-l', treeish];
1299+
if (path) {
1300+
args.push('--', sanitizePath(path));
1301+
}
1302+
1303+
const { stdout } = await this.exec(args);
12991304
return parseLsTree(stdout);
13001305
}
13011306

extensions/git/src/operation.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export const enum OperationKind {
2828
GetBranches = 'GetBranches',
2929
GetCommitTemplate = 'GetCommitTemplate',
3030
GetObjectDetails = 'GetObjectDetails',
31+
GetObjectFiles = 'GetObjectFiles',
3132
GetRefs = 'GetRefs',
3233
GetRemoteRefs = 'GetRemoteRefs',
3334
HashObject = 'HashObject',
@@ -65,12 +66,12 @@ export const enum OperationKind {
6566
export type Operation = AddOperation | ApplyOperation | BlameOperation | BranchOperation | CheckIgnoreOperation | CherryPickOperation |
6667
CheckoutOperation | CheckoutTrackingOperation | CleanOperation | CommitOperation | ConfigOperation | DeleteBranchOperation |
6768
DeleteRefOperation | DeleteRemoteTagOperation | DeleteTagOperation | DiffOperation | FetchOperation | FindTrackingBranchesOperation |
68-
GetBranchOperation | GetBranchesOperation | GetCommitTemplateOperation | GetObjectDetailsOperation | GetRefsOperation | GetRemoteRefsOperation |
69-
HashObjectOperation | IgnoreOperation | LogOperation | LogFileOperation | MergeOperation | MergeAbortOperation | MergeBaseOperation |
70-
MoveOperation | PostCommitCommandOperation | PullOperation | PushOperation | RemoteOperation | RenameBranchOperation | RemoveOperation |
71-
ResetOperation | RebaseOperation | RebaseAbortOperation | RebaseContinueOperation | RefreshOperation | RevertFilesOperation | RevListOperation |
72-
RevParseOperation | SetBranchUpstreamOperation | ShowOperation | StageOperation | StatusOperation | StashOperation | SubmoduleUpdateOperation |
73-
SyncOperation | TagOperation;
69+
GetBranchOperation | GetBranchesOperation | GetCommitTemplateOperation | GetObjectDetailsOperation | GetObjectFilesOperation | GetRefsOperation |
70+
GetRemoteRefsOperation | HashObjectOperation | IgnoreOperation | LogOperation | LogFileOperation | MergeOperation | MergeAbortOperation |
71+
MergeBaseOperation | MoveOperation | PostCommitCommandOperation | PullOperation | PushOperation | RemoteOperation | RenameBranchOperation |
72+
RemoveOperation | ResetOperation | RebaseOperation | RebaseAbortOperation | RebaseContinueOperation | RefreshOperation | RevertFilesOperation |
73+
RevListOperation | RevParseOperation | SetBranchUpstreamOperation | ShowOperation | StageOperation | StatusOperation | StashOperation |
74+
SubmoduleUpdateOperation | SyncOperation | TagOperation;
7475

7576
type BaseOperation = { kind: OperationKind; blocking: boolean; readOnly: boolean; remote: boolean; retry: boolean; showProgress: boolean };
7677
export type AddOperation = BaseOperation & { kind: OperationKind.Add };
@@ -95,6 +96,7 @@ export type GetBranchOperation = BaseOperation & { kind: OperationKind.GetBranch
9596
export type GetBranchesOperation = BaseOperation & { kind: OperationKind.GetBranches };
9697
export type GetCommitTemplateOperation = BaseOperation & { kind: OperationKind.GetCommitTemplate };
9798
export type GetObjectDetailsOperation = BaseOperation & { kind: OperationKind.GetObjectDetails };
99+
export type GetObjectFilesOperation = BaseOperation & { kind: OperationKind.GetObjectFiles };
98100
export type GetRefsOperation = BaseOperation & { kind: OperationKind.GetRefs };
99101
export type GetRemoteRefsOperation = BaseOperation & { kind: OperationKind.GetRemoteRefs };
100102
export type HashObjectOperation = BaseOperation & { kind: OperationKind.HashObject };
@@ -151,6 +153,7 @@ export const Operation = {
151153
GetBranches: { kind: OperationKind.GetBranches, blocking: false, readOnly: true, remote: false, retry: false, showProgress: true } as GetBranchesOperation,
152154
GetCommitTemplate: { kind: OperationKind.GetCommitTemplate, blocking: false, readOnly: true, remote: false, retry: false, showProgress: true } as GetCommitTemplateOperation,
153155
GetObjectDetails: { kind: OperationKind.GetObjectDetails, blocking: false, readOnly: true, remote: false, retry: false, showProgress: false } as GetObjectDetailsOperation,
156+
GetObjectFiles: { kind: OperationKind.GetObjectFiles, blocking: false, readOnly: true, remote: false, retry: false, showProgress: false } as GetObjectFilesOperation,
154157
GetRefs: { kind: OperationKind.GetRefs, blocking: false, readOnly: true, remote: false, retry: false, showProgress: false } as GetRefsOperation,
155158
GetRemoteRefs: { kind: OperationKind.GetRemoteRefs, blocking: false, readOnly: true, remote: true, retry: false, showProgress: false } as GetRemoteRefsOperation,
156159
HashObject: { kind: OperationKind.HashObject, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as HashObjectOperation,

extensions/git/src/repository.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import TelemetryReporter from '@vscode/extension-telemetry';
1111
import { Branch, Change, ForcePushMode, GitErrorCodes, LogOptions, Ref, Remote, Status, CommitOptions, BranchQuery, FetchOptions, RefQuery, RefType } from './api/git';
1212
import { AutoFetcher } from './autofetch';
1313
import { debounce, memoize, throttle } from './decorators';
14-
import { Commit, GitError, Repository as BaseRepository, Stash, Submodule, LogFileOptions, PullOptions } from './git';
14+
import { Commit, GitError, Repository as BaseRepository, Stash, Submodule, LogFileOptions, PullOptions, LsTreeElement } from './git';
1515
import { StatusBarCommands } from './statusbar';
1616
import { toGitUri } from './uri';
1717
import { anyEvent, combinedDisposable, debounceEvent, dispose, EmptyDisposable, eventToPromise, filterEvent, find, IDisposable, isDescendant, onceEvent, pathEquals, relativePath } from './util';
@@ -1955,6 +1955,10 @@ export class Repository implements Disposable {
19551955
});
19561956
}
19571957

1958+
getObjectFiles(ref: string): Promise<LsTreeElement[]> {
1959+
return this.run(Operation.GetObjectFiles, () => this.repository.lstree(ref));
1960+
}
1961+
19581962
getObjectDetails(ref: string, filePath: string): Promise<{ mode: string; object: string; size: number }> {
19591963
return this.run(Operation.GetObjectDetails, () => this.repository.getObjectDetails(ref, filePath));
19601964
}

0 commit comments

Comments
 (0)