Git - add "Stashes" node to the repositories view (#279400)

* WIP - Initial implementation

* Get author and committer date for a stash

* Add drop stash command

* More cleanup
This commit is contained in:
Ladislau Szomoru
2025-11-25 17:13:48 +00:00
committed by GitHub
parent 0432ed536c
commit f297f37463
9 changed files with 279 additions and 59 deletions

View File

@@ -14,7 +14,7 @@ import { Model } from './model';
import { GitResourceGroup, Repository, Resource, ResourceGroupType } from './repository';
import { DiffEditorSelectionHunkToolbarContext, LineChange, applyLineChanges, getIndexDiffInformation, getModifiedRange, getWorkingTreeDiffInformation, intersectDiffWithRange, invertLineChange, toLineChanges, toLineRanges, compareLineChanges } from './staging';
import { fromGitUri, toGitUri, isGitUri, toMergeUris, toMultiFileDiffEditorUris } from './uri';
import { DiagnosticSeverityConfig, dispose, fromNow, getHistoryItemDisplayName, grep, isDefined, isDescendant, isLinuxSnap, isRemote, isWindows, pathEquals, relativePath, subject, toDiagnosticSeverity, truncate } from './util';
import { DiagnosticSeverityConfig, dispose, fromNow, getHistoryItemDisplayName, getStashDescription, grep, isDefined, isDescendant, isLinuxSnap, isRemote, isWindows, pathEquals, relativePath, subject, toDiagnosticSeverity, truncate } from './util';
import { GitTimelineItem } from './timelineProvider';
import { ApiRepository } from './api/api1';
import { getRemoteSourceActions, pickRemoteSource } from './remoteSource';
@@ -333,7 +333,7 @@ class RepositoryItem implements QuickPickItem {
class StashItem implements QuickPickItem {
get label(): string { return `#${this.stash.index}: ${this.stash.description}`; }
get description(): string | undefined { return this.stash.branchName; }
get description(): string | undefined { return getStashDescription(this.stash); }
constructor(readonly stash: Stash) { }
}
@@ -4544,7 +4544,7 @@ export class CommandCenter {
return;
}
await this._stashDrop(repository, stash);
await this._stashDrop(repository, stash.index, stash.description);
}
@command('git.stashDropAll', { repository: true })
@@ -4577,15 +4577,15 @@ export class CommandCenter {
return;
}
if (await this._stashDrop(result.repository, result.stash)) {
if (await this._stashDrop(result.repository, result.stash.index, result.stash.description)) {
await commands.executeCommand('workbench.action.closeActiveEditor');
}
}
async _stashDrop(repository: Repository, stash: Stash): Promise<boolean> {
async _stashDrop(repository: Repository, index: number, description: string): Promise<boolean> {
const yes = l10n.t('Yes');
const result = await window.showWarningMessage(
l10n.t('Are you sure you want to drop the stash: {0}?', stash.description),
l10n.t('Are you sure you want to drop the stash: {0}?', description),
{ modal: true },
yes
);
@@ -4593,7 +4593,7 @@ export class CommandCenter {
return false;
}
await repository.dropStash(stash.index);
await repository.dropStash(index);
return true;
}
@@ -4606,36 +4606,7 @@ export class CommandCenter {
return;
}
const stashChanges = await repository.showStash(stash.index);
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) {
const isChangeUntracked = !!stashUntrackedFiles.find(f => pathEquals(f, change.uri.fsPath));
const modifiedUriRef = !isChangeUntracked ? stash.hash : stashUntrackedFilesParentCommit ?? stash.hash;
resources.push(toMultiFileDiffEditorUris(change, stashFirstParentCommit, modifiedUriRef));
}
commands.executeCommand('_workbench.openMultiDiffEditor', { multiDiffSourceUri, title, resources });
await this._viewStash(repository, stash);
}
private async pickStash(repository: Repository, placeHolder: string): Promise<Stash | undefined> {
@@ -4680,6 +4651,39 @@ export class CommandCenter {
return { repository, stash };
}
private async _viewStash(repository: Repository, stash: Stash): Promise<void> {
const stashChanges = await repository.showStash(stash.index);
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) {
const isChangeUntracked = !!stashUntrackedFiles.find(f => pathEquals(f, change.uri.fsPath));
const modifiedUriRef = !isChangeUntracked ? stash.hash : stashUntrackedFilesParentCommit ?? stash.hash;
resources.push(toMultiFileDiffEditorUris(change, stashFirstParentCommit, modifiedUriRef));
}
commands.executeCommand('_workbench.openMultiDiffEditor', { multiDiffSourceUri, title, resources });
}
@command('git.timeline.openDiff', { repository: false })
async timelineOpenDiff(item: TimelineItem, uri: Uri | undefined, _source: string) {
const cmd = this.resolveTimelineOpenDiffCommand(
@@ -5217,6 +5221,78 @@ export class CommandCenter {
await repository.deleteTag(artifact.name);
}
@command('git.repositories.stashView', { repository: true })
async artifactStashView(repository: Repository, artifact: SourceControlArtifact): Promise<void> {
if (!repository || !artifact) {
return;
}
// Extract stash index from artifact id
const regex = /^stash@\{(\d+)\}$/;
const match = regex.exec(artifact.id);
if (!match) {
return;
}
const stashes = await repository.getStashes();
const stash = stashes.find(s => s.index === parseInt(match[1]));
if (!stash) {
return;
}
await this._viewStash(repository, stash);
}
@command('git.repositories.stashApply', { repository: true })
async artifactStashApply(repository: Repository, artifact: SourceControlArtifact): Promise<void> {
if (!repository || !artifact) {
return;
}
// Extract stash index from artifact id (format: "stash@{index}")
const regex = /^stash@\{(\d+)\}$/;
const match = regex.exec(artifact.id);
if (!match) {
return;
}
const stashIndex = parseInt(match[1]);
await repository.applyStash(stashIndex);
}
@command('git.repositories.stashPop', { repository: true })
async artifactStashPop(repository: Repository, artifact: SourceControlArtifact): Promise<void> {
if (!repository || !artifact) {
return;
}
// Extract stash index from artifact id (format: "stash@{index}")
const regex = /^stash@\{(\d+)\}$/;
const match = regex.exec(artifact.id);
if (!match) {
return;
}
const stashIndex = parseInt(match[1]);
await repository.popStash(stashIndex);
}
@command('git.repositories.stashDrop', { repository: true })
async artifactStashDrop(repository: Repository, artifact: SourceControlArtifact): Promise<void> {
if (!repository || !artifact) {
return;
}
// Extract stash index from artifact id
const regex = /^stash@\{(\d+)\}$/;
const match = regex.exec(artifact.id);
if (!match) {
return;
}
await this._stashDrop(repository, parseInt(match[1]), artifact.name);
}
private createCommand(id: string, key: string, method: Function, options: ScmCommandOptions): (...args: any[]) => any {
const result = (...args: any[]) => {
let result: Promise<any>;