From cfebdd863a219cdaa571684aae229dc9d7f88ea9 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Mon, 8 Jan 2024 09:48:51 +0100 Subject: [PATCH] Git - add open stash command (#201970) * Initial implementation * Add button to quick pick * Improve stash picker * Remove quick pick buttons --- extensions/git/package.json | 10 ++++++ extensions/git/package.nls.json | 1 + extensions/git/src/commands.ts | 30 ++++++++++++++-- extensions/git/src/git.ts | 60 +++++++++++++++++++++++++++----- extensions/git/src/repository.ts | 6 +++- 5 files changed, 96 insertions(+), 11 deletions(-) diff --git a/extensions/git/package.json b/extensions/git/package.json index 8744035090e..fbdcb00b462 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -674,6 +674,12 @@ "category": "Git", "enablement": "!operationInProgress" }, + { + "command": "git.stashOpen", + "title": "%command.stashOpen%", + "category": "Git", + "enablement": "!operationInProgress" + }, { "command": "git.timeline.openDiff", "title": "%command.timelineOpenDiff%", @@ -1257,6 +1263,10 @@ "command": "git.openRepositoriesInParentFolders", "when": "config.git.enabled && !git.missing && git.parentRepositoryCount != 0" }, + { + "command": "git.openStash", + "when": "config.git.enabled && !git.missing && config.multiDiffEditor.experimental.enabled" + }, { "command": "git.viewChanges", "when": "false" diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index 9b053671d0f..3c6a721b804 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -104,6 +104,7 @@ "command.stashApplyLatest": "Apply Latest Stash", "command.stashDrop": "Drop Stash...", "command.stashDropAll": "Drop All Stashes...", + "command.stashOpen": "Open Stash...", "command.timelineOpenDiff": "Open Changes", "command.timelineOpenCommit": "Open Commit", "command.timelineCopyCommitId": "Copy Commit ID", diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 50a9fbbb22f..78b52187a50 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -3535,6 +3535,31 @@ export class CommandCenter { await repository.dropStash(); } + @command('git.stashOpen', { repository: true }) + async stashOpen(repository: Repository): Promise { + const placeHolder = l10n.t('Pick a stash to open'); + const stash = await this.pickStash(repository, placeHolder); + + if (!stash) { + return; + } + + const stashFiles = await repository.showStash(stash.index); + + if (!stashFiles || stashFiles.length === 0) { + return; + } + + const args: [Uri, Uri | undefined, Uri | undefined][] = []; + + for (const file of stashFiles) { + const fileUri = Uri.file(path.join(repository.root, file)); + args.push([fileUri, toGitUri(fileUri, `stash@{${stash.index}}`), fileUri]); + } + + commands.executeCommand('vscode.changes', `Git Stash #${stash.index}: ${stash.description}`, args); + } + private async pickStash(repository: Repository, placeHolder: string): Promise { const stashes = await repository.getStashes(); @@ -3543,9 +3568,10 @@ export class CommandCenter { return; } - const picks = stashes.map(stash => ({ label: `#${stash.index}: ${stash.description}`, description: '', details: '', stash })); + const picks = stashes.map(stash => ({ label: `#${stash.index}: ${stash.description}`, description: stash.branchName, stash })); const result = await window.showQuickPick(picks, { placeHolder }); - return result && result.stash; + + return result?.stash; } @command('git.timeline.openDiff', { repository: false }) diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 6f768d36ea0..2c8be161784 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -37,6 +37,7 @@ export interface IFileStatus { export interface Stash { index: number; description: string; + branchName?: string; } interface MutableRemote extends Remote { @@ -964,6 +965,38 @@ export function parseLsFiles(raw: string): LsFilesElement[] { .map(([, mode, object, stage, file]) => ({ mode, object, stage, file })); } +function parseGitStashes(raw: string): Stash[] { + const result: Stash[] = []; + const regex = /^stash@{(\d+)}:(.+)$/; + const descriptionRegex = /(WIP\s)*on([^:]+):(.*)$/i; + + for (const stash of raw.split('\n').filter(s => !!s)) { + // Extract index and description + const match = regex.exec(stash); + if (!match) { + continue; + } + + const [, index, description] = match; + + // Extract branch name from description + const descriptionMatch = descriptionRegex.exec(description); + if (!descriptionMatch) { + result.push({ index: parseInt(index), description: description.trim() }); + continue; + } + + const [, wip, branchName, message] = descriptionMatch; + result.push({ + index: parseInt(index), + description: wip ? `WIP (${message.trim()})` : message.trim(), + branchName: branchName.trim() + }); + } + + return result; +} + export interface PullOptions { unshallow?: boolean; tags?: boolean; @@ -2114,6 +2147,24 @@ export class Repository { } } + async showStash(index: number): Promise { + const args = ['stash', 'show', `stash@{${index}}`, '--name-only']; + + try { + const result = await this.exec(args); + + return result.stdout.trim() + .split('\n') + .filter(line => !!line); + } catch (err) { + if (/No stash found/.test(err.stderr || '')) { + return undefined; + } + + throw err; + } + } + async getStatus(opts?: { limit?: number; ignoreSubmodules?: boolean; similarityThreshold?: number; untrackedChanges?: 'mixed' | 'separate' | 'hidden'; cancellationToken?: CancellationToken }): Promise<{ status: IFileStatus[]; statusLength: number; didHitLimit: boolean }> { if (opts?.cancellationToken && opts?.cancellationToken.isCancellationRequested) { throw new CancellationError(); @@ -2385,14 +2436,7 @@ export class Repository { async getStashes(): Promise { const result = await this.exec(['stash', 'list']); - const regex = /^stash@{(\d+)}:(.+)$/; - const rawStashes = result.stdout.trim().split('\n') - .filter(b => !!b) - .map(line => regex.exec(line) as RegExpExecArray) - .filter(g => !!g) - .map(([, index, description]: RegExpExecArray) => ({ index: parseInt(index), description })); - - return rawStashes; + return parseGitStashes(result.stdout.trim()); } async getRemotes(): Promise { diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 4855d754240..42f90428a2c 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -1931,7 +1931,7 @@ export class Repository implements Disposable { } async getStashes(): Promise { - return await this.repository.getStashes(); + return this.run(Operation.Stash, () => this.repository.getStashes()); } async createStash(message?: string, includeUntracked?: boolean, staged?: boolean): Promise { @@ -1958,6 +1958,10 @@ export class Repository implements Disposable { return await this.run(Operation.Stash, () => this.repository.applyStash(index)); } + async showStash(index: number): Promise { + return await this.run(Operation.Stash, () => this.repository.showStash(index)); + } + async getCommitTemplate(): Promise { return await this.run(Operation.GetCommitTemplate, async () => this.repository.getCommitTemplate()); }