Browse Source

Closes #2538 adds ability to rename stashes

Renames Delete Stash to Drop Stash
main
Eric Amodio 1 year ago
parent
commit
c3a6a21455
13 changed files with 259 additions and 43 deletions
  1. +46
    -9
      package.json
  2. +125
    -2
      src/commands/git/stash.ts
  3. +12
    -0
      src/env/node/git/git.ts
  4. +13
    -1
      src/env/node/git/localGitProvider.ts
  5. +7
    -0
      src/git/actions/stash.ts
  6. +4
    -3
      src/git/gitProvider.ts
  7. +18
    -6
      src/git/gitProviderService.ts
  8. +6
    -4
      src/git/models/reference.ts
  9. +8
    -0
      src/git/models/repository.ts
  10. +0
    -14
      src/plus/github/githubGitProvider.ts
  11. +10
    -1
      src/plus/webviews/graph/graphWebview.ts
  12. +1
    -1
      src/views/nodes/stashNode.ts
  13. +9
    -2
      src/views/viewCommands.ts

+ 46
- 9
package.json View File

@ -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"

+ 125
- 2
src/commands/git/stash.ts View File

@ -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<T extends State> = SomeNonNullable<StepState<T>, 'subcommand'>;
type ApplyStepState<T extends ApplyState = ApplyState> = StashStepState<ExcludeSome<T, 'repo', string>>;
type DropStepState<T extends DropState = DropState> = StashStepState<ExcludeSome<T, 'repo', string>>;
type ListStepState<T extends ListState = ListState> = StashStepState<ExcludeSome<T, 'repo', string>>;
type PopStepState<T extends PopState = PopState> = StashStepState<ExcludeSome<T, 'repo', string>>;
type PushStepState<T extends PushState = PushState> = StashStepState<ExcludeSome<T, 'repo', string>>;
type RenameStepState<T extends RenameState = RenameState> = StashStepState<ExcludeSome<T, 'repo', string>>;
const subcommandToTitleMap = new Map<State['subcommand'], string>([
['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<typeof step> = 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<GitStashReference> = 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<string> = 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<string> {
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<typeof step> = 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<typeof step> = yield step;
return canPickStepContinue(step, state, selection) ? selection[0].item : StepResultBreak;
}
}

+ 12
- 0
src/env/node/git/git.ts View File

@ -1803,6 +1803,18 @@ export class Git {
return this.git<string>({ 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<string>(
{ cwd: repoPath },
'stash',
'store',
'-m',
stashOnRef ? `On ${stashOnRef}: ${message}` : message,
ref,
);
}
async stash__delete(repoPath: string, stashName: string, ref?: string) {
if (!stashName) return undefined;

+ 13
- 1
src/env/node/git/localGitProvider.ts View File

@ -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<void> {
await this.git.stash__rename(repoPath, stashName, ref, message, stashOnRef);
this.container.events.fire('git:cache:reset', { repoPath: repoPath, caches: ['stashes'] });
}
@log<LocalGitProvider['stashSave']>({ args: { 2: uris => uris?.length } })
async stashSave(
repoPath: string,

+ 7
- 0
src/git/actions/stash.ts View File

@ -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',

+ 4
- 3
src/git/gitProvider.ts View File

@ -453,9 +453,10 @@ export interface GitProvider extends Disposable {
unStageFile(repoPath: string, pathOrUri: string | Uri): Promise<void>;
unStageDirectory(repoPath: string, directoryOrUri: string | Uri): Promise<void>;
stashApply(repoPath: string, stashName: string, options?: { deleteAfter?: boolean | undefined }): Promise<void>;
stashDelete(repoPath: string, stashName: string, ref?: string): Promise<void>;
stashSave(
stashApply?(repoPath: string, stashName: string, options?: { deleteAfter?: boolean | undefined }): Promise<void>;
stashDelete?(repoPath: string, stashName: string, ref?: string): Promise<void>;
stashRename?(repoPath: string, stashName: string, ref: string, message: string, stashOnRef?: string): Promise<void>;
stashSave?(
repoPath: string,
message?: string,
uris?: Uri[],

+ 18
- 6
src/git/gitProviderService.ts View File

@ -2697,26 +2697,38 @@ export class GitProviderService implements Disposable {
}
@log()
stashApply(repoPath: string | Uri, stashName: string, options?: { deleteAfter?: boolean }): Promise<void> {
async stashApply(repoPath: string | Uri, stashName: string, options?: { deleteAfter?: boolean }): Promise<void> {
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<void> {
async stashDelete(repoPath: string | Uri, stashName: string, ref?: string): Promise<void> {
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<void> {
const { provider, path } = this.getProvider(repoPath);
return provider.stashRename?.(path, stashName, ref, message, stashOnRef);
}
@log<GitProviderService['stashSave']>({ args: { 2: uris => uris?.length } })
stashSave(
async stashSave(
repoPath: string | Uri,
message?: string,
uris?: Uri[],
options?: { includeUntracked?: boolean; keepIndex?: boolean; onlyStaged?: boolean },
): Promise<void> {
const { provider, path } = this.getProvider(repoPath);
return provider.stashSave(path, message, uris, options);
return provider.stashSave?.(path, message, uris, options);
}
@log()

+ 6
- 4
src/git/models/reference.ts View File

@ -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;

+ 8
- 0
src/git/models/repository.ts View File

@ -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[],

+ 0
- 14
src/plus/github/githubGitProvider.ts View File

@ -3068,20 +3068,6 @@ export class GitHubGitProvider implements GitProvider, Disposable {
@log()
async unStageDirectory(_repoPath: string, _directoryOrUri: string | Uri): Promise<void> {}
@log()
async stashApply(_repoPath: string, _stashName: string, _options?: { deleteAfter?: boolean }): Promise<void> {}
@log()
async stashDelete(_repoPath: string, _stashName: string, _ref?: string): Promise<void> {}
@log<GitHubGitProvider['stashSave']>({ args: { 2: uris => uris?.length } })
async stashSave(
_repoPath: string,
_message?: string,
_uris?: Uri[],
_options?: { includeUntracked?: boolean; keepIndex?: boolean; onlyStaged?: boolean },
): Promise<void> {}
@gate()
private async ensureRepositoryContext(
repoPath: string,

+ 10
- 1
src/plus/webviews/graph/graphWebview.ts View File

@ -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();

+ 1
- 1
src/views/nodes/stashNode.ts View File

@ -80,7 +80,7 @@ export class StashNode extends ViewRefNode
});
item.contextValue = ContextValues.Stash;
if (this.options?.icon) {
item.iconPath = new ThemeIcon('gitlens-stashes');
item.iconPath = new ThemeIcon('archive');
}
item.tooltip = CommitFormatter.fromTemplate(
`\${'On 'stashOnRef\n}\${ago} (\${date})\n\n\${message}`,

+ 9
- 2
src/views/viewCommands.ts View File

@ -263,7 +263,8 @@ export class ViewCommands {
registerViewCommand('gitlens.views.renameBranch', this.renameBranch, this);
registerViewCommand('gitlens.views.title.applyStash', () => 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();

Loading…
Cancel
Save