From fe0632cbb2d6d3517676748a9601d3cb3dc36b01 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Fri, 26 Jan 2024 23:28:45 +0100 Subject: [PATCH] Git - handle stashes that contain untracked files (#203572) --- extensions/git/src/commands.ts | 39 ++++++++++++++++++++++++-------- extensions/git/src/git.ts | 11 ++++++--- extensions/git/src/operation.ts | 15 +++++++----- extensions/git/src/repository.ts | 6 ++++- 4 files changed, 51 insertions(+), 20 deletions(-) diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 33bd2ce2393..5ff4fc4f4a6 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -3709,25 +3709,44 @@ export class CommandCenter { } const stashChanges = await repository.showStash(stash.index); - const stashParentCommit = stash.parents.length > 0 ? stash.parents[0] : `${stash.hash}^`; - if (!stashChanges || stashChanges.length === 0) { return; } + // A stash commit can have up to 3 parents: + // 1. The first parent is the commit that was HEAD when the stash was created. + // 2. The second parent is the commit that represents the index when the stash was created. + // 3. The third parent (when present) represents the untracked files when the stash was created. + const stashFirstParentCommit = stash.parents.length > 0 ? stash.parents[0] : `${stash.hash}^`; + const stashUntrackedFilesParentCommit = stash.parents.length === 3 ? stash.parents[2] : undefined; + const stashUntrackedFiles: string[] = []; + + if (stashUntrackedFilesParentCommit) { + const untrackedFiles = await repository.getObjectFiles(stashUntrackedFilesParentCommit); + stashUntrackedFiles.push(...untrackedFiles.map(f => path.join(repository.root, f.file))); + } + const title = `Git Stash #${stash.index}: ${stash.description}`; const multiDiffSourceUri = toGitUri(Uri.file(repository.root), `stash@{${stash.index}}`, { scheme: 'git-stash' }); const resources: { originalUri: Uri | undefined; modifiedUri: Uri | undefined }[] = []; for (const change of stashChanges) { - if (change.status === Status.INDEX_ADDED) { - resources.push({ originalUri: undefined, modifiedUri: toGitUri(change.uri, stash.hash) }); - } else if (change.status === Status.DELETED) { - resources.push({ originalUri: toGitUri(change.uri, stashParentCommit), modifiedUri: undefined }); - } else if (change.status === Status.INDEX_RENAMED) { - resources.push({ originalUri: toGitUri(change.originalUri, stashParentCommit), modifiedUri: toGitUri(change.uri, stash.hash) }); - } else { - resources.push({ originalUri: toGitUri(change.uri, stashParentCommit), modifiedUri: toGitUri(change.uri, stash.hash) }); + const isChangeUntracked = !!stashUntrackedFiles.find(f => pathEquals(f, change.uri.fsPath)); + const modifiedUriRef = !isChangeUntracked ? stash.hash : stashUntrackedFilesParentCommit ?? stash.hash; + + switch (change.status) { + case Status.INDEX_ADDED: + resources.push({ originalUri: undefined, modifiedUri: toGitUri(change.uri, modifiedUriRef) }); + break; + case Status.DELETED: + resources.push({ originalUri: toGitUri(change.uri, stashFirstParentCommit), modifiedUri: undefined }); + break; + case Status.INDEX_RENAMED: + resources.push({ originalUri: toGitUri(change.originalUri, stashFirstParentCommit), modifiedUri: toGitUri(change.uri, modifiedUriRef) }); + break; + default: + resources.push({ originalUri: toGitUri(change.uri, stashFirstParentCommit), modifiedUri: toGitUri(change.uri, modifiedUriRef) }); + break; } } diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 119fce2815d..785a9cd2267 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -937,7 +937,7 @@ function parseGitDiffShortStat(data: string): CommitShortStat { return { files: parseInt(files), insertions: parseInt(insertions ?? '0'), deletions: parseInt(deletions ?? '0') }; } -interface LsTreeElement { +export interface LsTreeElement { mode: string; type: string; object: string; @@ -1294,8 +1294,13 @@ export class Repository { return { mode, object, size: parseInt(size) }; } - async lstree(treeish: string, path: string): Promise { - const { stdout } = await this.exec(['ls-tree', '-l', treeish, '--', sanitizePath(path)]); + async lstree(treeish: string, path?: string): Promise { + const args = ['ls-tree', '-l', treeish]; + if (path) { + args.push('--', sanitizePath(path)); + } + + const { stdout } = await this.exec(args); return parseLsTree(stdout); } diff --git a/extensions/git/src/operation.ts b/extensions/git/src/operation.ts index 15e494f5fab..ead345c0b06 100644 --- a/extensions/git/src/operation.ts +++ b/extensions/git/src/operation.ts @@ -28,6 +28,7 @@ export const enum OperationKind { GetBranches = 'GetBranches', GetCommitTemplate = 'GetCommitTemplate', GetObjectDetails = 'GetObjectDetails', + GetObjectFiles = 'GetObjectFiles', GetRefs = 'GetRefs', GetRemoteRefs = 'GetRemoteRefs', HashObject = 'HashObject', @@ -65,12 +66,12 @@ export const enum OperationKind { export type Operation = AddOperation | ApplyOperation | BlameOperation | BranchOperation | CheckIgnoreOperation | CherryPickOperation | CheckoutOperation | CheckoutTrackingOperation | CleanOperation | CommitOperation | ConfigOperation | DeleteBranchOperation | DeleteRefOperation | DeleteRemoteTagOperation | DeleteTagOperation | DiffOperation | FetchOperation | FindTrackingBranchesOperation | - GetBranchOperation | GetBranchesOperation | GetCommitTemplateOperation | GetObjectDetailsOperation | GetRefsOperation | GetRemoteRefsOperation | - HashObjectOperation | IgnoreOperation | LogOperation | LogFileOperation | MergeOperation | MergeAbortOperation | MergeBaseOperation | - MoveOperation | PostCommitCommandOperation | PullOperation | PushOperation | RemoteOperation | RenameBranchOperation | RemoveOperation | - ResetOperation | RebaseOperation | RebaseAbortOperation | RebaseContinueOperation | RefreshOperation | RevertFilesOperation | RevListOperation | - RevParseOperation | SetBranchUpstreamOperation | ShowOperation | StageOperation | StatusOperation | StashOperation | SubmoduleUpdateOperation | - SyncOperation | TagOperation; + GetBranchOperation | GetBranchesOperation | GetCommitTemplateOperation | GetObjectDetailsOperation | GetObjectFilesOperation | GetRefsOperation | + GetRemoteRefsOperation | HashObjectOperation | IgnoreOperation | LogOperation | LogFileOperation | MergeOperation | MergeAbortOperation | + MergeBaseOperation | MoveOperation | PostCommitCommandOperation | PullOperation | PushOperation | RemoteOperation | RenameBranchOperation | + RemoveOperation | ResetOperation | RebaseOperation | RebaseAbortOperation | RebaseContinueOperation | RefreshOperation | RevertFilesOperation | + RevListOperation | RevParseOperation | SetBranchUpstreamOperation | ShowOperation | StageOperation | StatusOperation | StashOperation | + SubmoduleUpdateOperation | SyncOperation | TagOperation; type BaseOperation = { kind: OperationKind; blocking: boolean; readOnly: boolean; remote: boolean; retry: boolean; showProgress: boolean }; export type AddOperation = BaseOperation & { kind: OperationKind.Add }; @@ -95,6 +96,7 @@ export type GetBranchOperation = BaseOperation & { kind: OperationKind.GetBranch export type GetBranchesOperation = BaseOperation & { kind: OperationKind.GetBranches }; export type GetCommitTemplateOperation = BaseOperation & { kind: OperationKind.GetCommitTemplate }; export type GetObjectDetailsOperation = BaseOperation & { kind: OperationKind.GetObjectDetails }; +export type GetObjectFilesOperation = BaseOperation & { kind: OperationKind.GetObjectFiles }; export type GetRefsOperation = BaseOperation & { kind: OperationKind.GetRefs }; export type GetRemoteRefsOperation = BaseOperation & { kind: OperationKind.GetRemoteRefs }; export type HashObjectOperation = BaseOperation & { kind: OperationKind.HashObject }; @@ -151,6 +153,7 @@ export const Operation = { GetBranches: { kind: OperationKind.GetBranches, blocking: false, readOnly: true, remote: false, retry: false, showProgress: true } as GetBranchesOperation, GetCommitTemplate: { kind: OperationKind.GetCommitTemplate, blocking: false, readOnly: true, remote: false, retry: false, showProgress: true } as GetCommitTemplateOperation, GetObjectDetails: { kind: OperationKind.GetObjectDetails, blocking: false, readOnly: true, remote: false, retry: false, showProgress: false } as GetObjectDetailsOperation, + GetObjectFiles: { kind: OperationKind.GetObjectFiles, blocking: false, readOnly: true, remote: false, retry: false, showProgress: false } as GetObjectFilesOperation, GetRefs: { kind: OperationKind.GetRefs, blocking: false, readOnly: true, remote: false, retry: false, showProgress: false } as GetRefsOperation, GetRemoteRefs: { kind: OperationKind.GetRemoteRefs, blocking: false, readOnly: true, remote: true, retry: false, showProgress: false } as GetRemoteRefsOperation, HashObject: { kind: OperationKind.HashObject, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as HashObjectOperation, diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 70e4e98c43a..89bdc37f624 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -11,7 +11,7 @@ import TelemetryReporter from '@vscode/extension-telemetry'; import { Branch, Change, ForcePushMode, GitErrorCodes, LogOptions, Ref, Remote, Status, CommitOptions, BranchQuery, FetchOptions, RefQuery, RefType } from './api/git'; import { AutoFetcher } from './autofetch'; import { debounce, memoize, throttle } from './decorators'; -import { Commit, GitError, Repository as BaseRepository, Stash, Submodule, LogFileOptions, PullOptions } from './git'; +import { Commit, GitError, Repository as BaseRepository, Stash, Submodule, LogFileOptions, PullOptions, LsTreeElement } from './git'; import { StatusBarCommands } from './statusbar'; import { toGitUri } from './uri'; 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 { }); } + getObjectFiles(ref: string): Promise { + return this.run(Operation.GetObjectFiles, () => this.repository.lstree(ref)); + } + getObjectDetails(ref: string, filePath: string): Promise<{ mode: string; object: string; size: number }> { return this.run(Operation.GetObjectDetails, () => this.repository.getObjectDetails(ref, filePath)); }