diff --git a/package.json b/package.json index 8aedd29..b2be40a 100644 --- a/package.json +++ b/package.json @@ -5374,13 +5374,20 @@ "enablement": "!operationInProgress" }, { - "command": "gitlens.views.deleteStash", - "title": "Delete Stash...", + "command": "gitlens.views.stash.delete", + "title": "Drop Stash...", "category": "GitLens", "icon": "$(trash)", "enablement": "!operationInProgress" }, { + "command": "gitlens.views.stash.rename", + "title": "Rename Stash...", + "category": "GitLens", + "icon": "$(edit)", + "enablement": "!operationInProgress" + }, + { "command": "gitlens.stashSave", "title": "Stash All Changes", "category": "GitLens", @@ -7094,13 +7101,20 @@ "enablement": "!operationInProgress" }, { - "command": "gitlens.graph.deleteStash", - "title": "Delete Stash...", + "command": "gitlens.graph.stash.delete", + "title": "Drop Stash...", "category": "GitLens", "icon": "$(trash)", "enablement": "!operationInProgress" }, { + "command": "gitlens.graph.stash.rename", + "title": "Rename Stash...", + "category": "GitLens", + "icon": "$(edit)", + "enablement": "!operationInProgress" + }, + { "command": "gitlens.graph.createTag", "title": "Create Tag...", "category": "GitLens", @@ -8085,7 +8099,11 @@ "when": "gitlens:enabled && !gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders" }, { - "command": "gitlens.views.deleteStash", + "command": "gitlens.views.stash.delete", + "when": "false" + }, + { + "command": "gitlens.views.stash.rename", "when": "false" }, { @@ -9249,7 +9267,11 @@ "when": "false" }, { - "command": "gitlens.graph.deleteStash", + "command": "gitlens.graph.stash.delete", + "when": "false" + }, + { + "command": "gitlens.graph.stash.rename", "when": "false" }, { @@ -11681,7 +11703,12 @@ "group": "inline@1" }, { - "command": "gitlens.views.deleteStash", + "command": "gitlens.views.stash.rename", + "when": "!gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders && viewItem == gitlens:stash", + "group": "inline@98" + }, + { + "command": "gitlens.views.stash.delete", "when": "!gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders && viewItem == gitlens:stash", "group": "inline@99" }, @@ -11691,11 +11718,16 @@ "group": "1_gitlens_actions@1" }, { - "command": "gitlens.views.deleteStash", + "command": "gitlens.views.stash.rename", "when": "!gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders && viewItem == gitlens:stash", "group": "1_gitlens_actions@2" }, { + "command": "gitlens.views.stash.delete", + "when": "!gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders && viewItem == gitlens:stash", + "group": "1_gitlens_actions@3" + }, + { "command": "gitlens.views.createTag", "when": "!gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders && viewItem =~ /gitlens:tags\\b/", "group": "inline@1" @@ -12007,11 +12039,16 @@ "group": "1_gitlens_actions@1" }, { - "command": "gitlens.graph.deleteStash", + "command": "gitlens.graph.stash.rename", "when": "!gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders && webviewItem == gitlens:stash", "group": "1_gitlens_actions@2" }, { + "command": "gitlens.graph.stash.delete", + "when": "!gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders && webviewItem == gitlens:stash", + "group": "1_gitlens_actions@3" + }, + { "command": "gitlens.graph.saveStash", "when": "!gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders && webviewItem == gitlens:wip", "group": "1_gitlens_actions@3" diff --git a/src/commands/git/stash.ts b/src/commands/git/stash.ts index 67ccfb3..07b5a4c 100644 --- a/src/commands/git/stash.ts +++ b/src/commands/git/stash.ts @@ -86,13 +86,21 @@ interface PushState { flags: PushFlags[]; } -type State = ApplyState | DropState | ListState | PopState | PushState; +interface RenameState { + subcommand: 'rename'; + repo: string | Repository; + reference: GitStashReference; + message: string; +} + +type State = ApplyState | DropState | ListState | PopState | PushState | RenameState; type StashStepState = SomeNonNullable, 'subcommand'>; type ApplyStepState = StashStepState>; type DropStepState = StashStepState>; type ListStepState = StashStepState>; type PopStepState = StashStepState>; type PushStepState = StashStepState>; +type RenameStepState = StashStepState>; const subcommandToTitleMap = new Map([ ['apply', 'Apply'], @@ -100,6 +108,7 @@ const subcommandToTitleMap = new Map([ ['list', 'List'], ['pop', 'Pop'], ['push', 'Push'], + ['rename', 'Rename'], ]); function getTitle(title: string, subcommand: State['subcommand'] | undefined) { return subcommand == null ? title : `${subcommandToTitleMap.get(subcommand)} ${title}`; @@ -131,13 +140,21 @@ export class StashGitCommand extends QuickCommand { counter++; } break; - case 'push': if (args.state.message != null) { counter++; } break; + case 'rename': + if (args.state.reference != null) { + counter++; + } + + if (args.state.message != null) { + counter++; + } + break; } } @@ -229,6 +246,11 @@ export class StashGitCommand extends QuickCommand { case 'push': yield* this.pushCommandSteps(state as PushStepState, context); break; + case 'rename': + yield* this.renameCommandSteps(state as RenameStepState, context); + // Clear any chosen message, since we are exiting this subcommand + state.message = undefined!; + break; default: endSteps(state); break; @@ -280,6 +302,12 @@ export class StashGitCommand extends QuickCommand { picked: state.subcommand === 'push', item: 'push', }, + { + label: 'rename', + description: 'renames the specified stash', + picked: state.subcommand === 'rename', + item: 'rename', + }, ], buttons: [QuickInputButtons.Back], }); @@ -641,4 +669,99 @@ export class StashGitCommand extends QuickCommand { const selection: StepSelection = yield step; return canPickStepContinue(step, state, selection) ? selection[0].item : StepResultBreak; } + + private async *renameCommandSteps(state: RenameStepState, context: Context): StepGenerator { + while (this.canStepsContinue(state)) { + if (state.counter < 3 || state.reference == null) { + const result: StepResult = yield* pickStashStep(state, context, { + stash: await this.container.git.getStash(state.repo.path), + placeholder: (context, stash) => + stash == null ? `No stashes found in ${state.repo.formattedName}` : 'Choose a stash to rename', + picked: state.reference?.ref, + }); + // Always break on the first step (so we will go back) + if (result === StepResultBreak) break; + + state.reference = result; + } + + if (state.counter < 4 || state.message == null) { + const result: StepResult = yield* this.renameCommandInputMessageStep(state, context); + if (result === StepResultBreak) continue; + + state.message = result; + } + + if (this.confirm(state.confirm)) { + const result = yield* this.renameCommandConfirmStep(state, context); + if (result === StepResultBreak) continue; + } + + endSteps(state); + + try { + await state.repo.stashRename( + state.reference.name, + state.reference.ref, + state.message, + state.reference.stashOnRef, + ); + } catch (ex) { + Logger.error(ex, context.title); + void showGenericErrorMessage(ex.message); + } + } + } + + private async *renameCommandInputMessageStep( + state: RenameStepState, + context: Context, + ): AsyncStepResultGenerator { + const step = createInputStep({ + title: appendReposToTitle(context.title, state, context), + placeholder: `Please provide a new message for ${getReferenceLabel(state.reference, { icon: false })}`, + value: state.message ?? state.reference?.message, + prompt: 'Enter new stash message', + }); + + const value: StepSelection = yield step; + if (!canStepContinue(step, state, value) || !(await canInputStepContinue(step, state, value))) { + return StepResultBreak; + } + + return value; + } + + private *renameCommandConfirmStep(state: RenameStepState, context: Context): StepResultGenerator<'rename'> { + const step = this.createConfirmStep( + appendReposToTitle(`Confirm ${context.title}`, state, context), + [ + { + label: context.title, + detail: `Will rename ${getReferenceLabel(state.reference)}`, + item: state.subcommand, + }, + ], + undefined, + { + placeholder: `Confirm ${context.title}`, + additionalButtons: [ShowDetailsViewQuickInputButton, RevealInSideBarQuickInputButton], + onDidClickButton: (quickpick, button) => { + if (button === ShowDetailsViewQuickInputButton) { + void showDetailsView(state.reference, { + pin: false, + preserveFocus: true, + }); + } else if (button === RevealInSideBarQuickInputButton) { + void reveal(state.reference, { + select: true, + expand: true, + }); + } + }, + }, + ); + const selection: StepSelection = yield step; + return canPickStepContinue(step, state, selection) ? selection[0].item : StepResultBreak; + } } diff --git a/src/env/node/git/git.ts b/src/env/node/git/git.ts index 5a49ef2..8b0be87 100644 --- a/src/env/node/git/git.ts +++ b/src/env/node/git/git.ts @@ -1803,6 +1803,18 @@ export class Git { return this.git({ cwd: repoPath }, 'stash', deleteAfter ? 'pop' : 'apply', stashName); } + async stash__rename(repoPath: string, stashName: string, ref: string, message: string, stashOnRef?: string) { + await this.stash__delete(repoPath, stashName, ref); + return this.git( + { cwd: repoPath }, + 'stash', + 'store', + '-m', + stashOnRef ? `On ${stashOnRef}: ${message}` : message, + ref, + ); + } + async stash__delete(repoPath: string, stashName: string, ref?: string) { if (!stashName) return undefined; diff --git a/src/env/node/git/localGitProvider.ts b/src/env/node/git/localGitProvider.ts index e7beeaa..a98816b 100644 --- a/src/env/node/git/localGitProvider.ts +++ b/src/env/node/git/localGitProvider.ts @@ -3895,7 +3895,7 @@ export class LocalGitProvider implements GitProvider, Disposable { committedDate: '%ct', parents: '%P', stashName: '%gd', - summary: '%B', + summary: '%gs', }); const data = await this.git.stash__list(repoPath, { args: parser.arguments, @@ -4738,6 +4738,18 @@ export class LocalGitProvider implements GitProvider, Disposable { this.container.events.fire('git:cache:reset', { repoPath: repoPath, caches: ['stashes'] }); } + @log() + async stashRename( + repoPath: string, + stashName: string, + ref: string, + message: string, + stashOnRef?: string, + ): Promise { + await this.git.stash__rename(repoPath, stashName, ref, message, stashOnRef); + this.container.events.fire('git:cache:reset', { repoPath: repoPath, caches: ['stashes'] }); + } + @log({ args: { 2: uris => uris?.length } }) async stashSave( repoPath: string, diff --git a/src/git/actions/stash.ts b/src/git/actions/stash.ts index c11ffc6..75b5a43 100644 --- a/src/git/actions/stash.ts +++ b/src/git/actions/stash.ts @@ -20,6 +20,13 @@ export function drop(repo?: string | Repository, ref?: GitStashReference) { }); } +export function rename(repo?: string | Repository, ref?: GitStashReference, message?: string) { + return executeGitCommand({ + command: 'stash', + state: { subcommand: 'rename', repo: repo, reference: ref, message: message }, + }); +} + export function pop(repo?: string | Repository, ref?: GitStashReference) { return executeGitCommand({ command: 'stash', diff --git a/src/git/gitProvider.ts b/src/git/gitProvider.ts index cbf523e..dc8f813 100644 --- a/src/git/gitProvider.ts +++ b/src/git/gitProvider.ts @@ -453,9 +453,10 @@ export interface GitProvider extends Disposable { unStageFile(repoPath: string, pathOrUri: string | Uri): Promise; unStageDirectory(repoPath: string, directoryOrUri: string | Uri): Promise; - stashApply(repoPath: string, stashName: string, options?: { deleteAfter?: boolean | undefined }): Promise; - stashDelete(repoPath: string, stashName: string, ref?: string): Promise; - stashSave( + stashApply?(repoPath: string, stashName: string, options?: { deleteAfter?: boolean | undefined }): Promise; + stashDelete?(repoPath: string, stashName: string, ref?: string): Promise; + stashRename?(repoPath: string, stashName: string, ref: string, message: string, stashOnRef?: string): Promise; + stashSave?( repoPath: string, message?: string, uris?: Uri[], diff --git a/src/git/gitProviderService.ts b/src/git/gitProviderService.ts index 96bc2ed..a748163 100644 --- a/src/git/gitProviderService.ts +++ b/src/git/gitProviderService.ts @@ -2697,26 +2697,38 @@ export class GitProviderService implements Disposable { } @log() - stashApply(repoPath: string | Uri, stashName: string, options?: { deleteAfter?: boolean }): Promise { + async stashApply(repoPath: string | Uri, stashName: string, options?: { deleteAfter?: boolean }): Promise { const { provider, path } = this.getProvider(repoPath); - return provider.stashApply(path, stashName, options); + return provider.stashApply?.(path, stashName, options); } @log() - stashDelete(repoPath: string | Uri, stashName: string, ref?: string): Promise { + async stashDelete(repoPath: string | Uri, stashName: string, ref?: string): Promise { const { provider, path } = this.getProvider(repoPath); - return provider.stashDelete(path, stashName, ref); + return provider.stashDelete?.(path, stashName, ref); + } + + @log() + async stashRename( + repoPath: string | Uri, + stashName: string, + ref: string, + message: string, + stashOnRef?: string, + ): Promise { + const { provider, path } = this.getProvider(repoPath); + return provider.stashRename?.(path, stashName, ref, message, stashOnRef); } @log({ args: { 2: uris => uris?.length } }) - stashSave( + async stashSave( repoPath: string | Uri, message?: string, uris?: Uri[], options?: { includeUntracked?: boolean; keepIndex?: boolean; onlyStaged?: boolean }, ): Promise { const { provider, path } = this.getProvider(repoPath); - return provider.stashSave(path, message, uris, options); + return provider.stashSave?.(path, message, uris, options); } @log() diff --git a/src/git/models/reference.ts b/src/git/models/reference.ts index 7e87d11..1624c39 100644 --- a/src/git/models/reference.ts +++ b/src/git/models/reference.ts @@ -128,6 +128,7 @@ export interface GitStashReference { number: string | undefined; message?: string | undefined; + stashOnRef?: string | undefined; } export interface GitTagReference { @@ -159,7 +160,7 @@ export function createReference( export function createReference( ref: string, repoPath: string, - options: { refType: 'stash'; name: string; number: string | undefined; message?: string }, + options: { refType: 'stash'; name: string; number: string | undefined; message?: string; stashOnRef?: string }, ): GitStashReference; export function createReference( ref: string, @@ -178,7 +179,7 @@ export function createReference( upstream?: { name: string; missing: boolean }; } | { refType?: 'revision'; name?: string; message?: string } - | { refType: 'stash'; name: string; number: string | undefined; message?: string } + | { refType: 'stash'; name: string; number: string | undefined; message?: string; stashOnRef?: string } | { id?: string; refType: 'tag'; name: string } = { refType: 'revision' }, ): GitReference { switch (options.refType) { @@ -200,6 +201,7 @@ export function createReference( name: options.name, number: options.number, message: options.message, + stashOnRef: options.stashOnRef, }; case 'tag': return { @@ -329,7 +331,7 @@ export function getReferenceLabel( if (isStashReference(ref)) { let message; if (options.expand && ref.message) { - message = `${ref.number != null ? `${ref.number}: ` : ''}${ + message = `${ref.number != null ? `#${ref.number}: ` : ''}${ ref.message.length > 20 ? `${ref.message.substring(0, 20).trimRight()}${GlyphChars.Ellipsis}` : ref.message @@ -339,7 +341,7 @@ export function getReferenceLabel( result = `${options.label ? 'stash ' : ''}${ options.icon ? `$(archive)${GlyphChars.Space}${message ?? ref.name}` - : `${message ?? ref.number ?? ref.name}` + : `${message ?? (ref.number ? `#${ref.number}` : ref.name)}` }`; } else if (isRevisionRange(ref.ref)) { result = refName; diff --git a/src/git/models/repository.ts b/src/git/models/repository.ts index 3d629e5..3a55e37 100644 --- a/src/git/models/repository.ts +++ b/src/git/models/repository.ts @@ -1017,6 +1017,14 @@ export class Repository implements Disposable { @gate() @log() + async stashRename(stashName: string, ref: string, message: string, stashOnRef?: string) { + await this.container.git.stashRename(this.path, stashName, ref, message, stashOnRef); + + this.fireChange(RepositoryChange.Stash); + } + + @gate() + @log() async stashSave( message?: string, uris?: Uri[], diff --git a/src/plus/github/githubGitProvider.ts b/src/plus/github/githubGitProvider.ts index c61f119..74ab779 100644 --- a/src/plus/github/githubGitProvider.ts +++ b/src/plus/github/githubGitProvider.ts @@ -3068,20 +3068,6 @@ export class GitHubGitProvider implements GitProvider, Disposable { @log() async unStageDirectory(_repoPath: string, _directoryOrUri: string | Uri): Promise {} - @log() - async stashApply(_repoPath: string, _stashName: string, _options?: { deleteAfter?: boolean }): Promise {} - - @log() - async stashDelete(_repoPath: string, _stashName: string, _ref?: string): Promise {} - - @log({ args: { 2: uris => uris?.length } }) - async stashSave( - _repoPath: string, - _message?: string, - _uris?: Uri[], - _options?: { includeUntracked?: boolean; keepIndex?: boolean; onlyStaged?: boolean }, - ): Promise {} - @gate() private async ensureRepositoryContext( repoPath: string, diff --git a/src/plus/webviews/graph/graphWebview.ts b/src/plus/webviews/graph/graphWebview.ts index 6651d3e..218a61d 100644 --- a/src/plus/webviews/graph/graphWebview.ts +++ b/src/plus/webviews/graph/graphWebview.ts @@ -352,7 +352,8 @@ export class GraphWebviewProvider implements WebviewProvider { registerCommand('gitlens.graph.saveStash', this.saveStash, this), registerCommand('gitlens.graph.applyStash', this.applyStash, this), - registerCommand('gitlens.graph.deleteStash', this.deleteStash, this), + registerCommand('gitlens.graph.stash.delete', this.deleteStash, this), + registerCommand('gitlens.graph.stash.rename', this.renameStash, this), registerCommand('gitlens.graph.createTag', this.createTag, this), registerCommand('gitlens.graph.deleteTag', this.deleteTag, this), @@ -2292,6 +2293,14 @@ export class GraphWebviewProvider implements WebviewProvider { } @debug() + private renameStash(item?: GraphItemContext) { + const ref = this.getGraphItemRef(item, 'stash'); + if (ref == null) return Promise.resolve(); + + return StashActions.rename(ref.repoPath, ref); + } + + @debug() private async createTag(item?: GraphItemContext) { const ref = this.getGraphItemRef(item); if (ref == null) return Promise.resolve(); diff --git a/src/views/nodes/stashNode.ts b/src/views/nodes/stashNode.ts index 13d10d7..8121a39 100644 --- a/src/views/nodes/stashNode.ts +++ b/src/views/nodes/stashNode.ts @@ -80,7 +80,7 @@ export class StashNode extends ViewRefNode this.applyStash()); - registerViewCommand('gitlens.views.deleteStash', this.deleteStash, this, ViewCommandMultiSelectMode.Custom); + registerViewCommand('gitlens.views.stash.delete', this.deleteStash, this, ViewCommandMultiSelectMode.Custom); + registerViewCommand('gitlens.views.stash.rename', this.renameStash, this); registerViewCommand('gitlens.views.title.createTag', () => this.createTag()); registerViewCommand('gitlens.views.createTag', this.createTag, this); @@ -295,7 +296,6 @@ export class ViewCommands { this, ); } - @debug() private addAuthors(node?: ViewNode) { return ContributorActions.addAuthors(getNodeRepoPath(node)); @@ -472,6 +472,13 @@ export class ViewCommands { } @debug() + private renameStash(node: StashNode) { + if (!(node instanceof StashNode)) return Promise.resolve(); + + return StashActions.rename(node.repoPath, node.commit); + } + + @debug() private deleteTag(node: TagNode) { if (!(node instanceof TagNode)) return Promise.resolve();