diff --git a/package.json b/package.json index 3cd08d9..98cbe21 100644 --- a/package.json +++ b/package.json @@ -485,7 +485,8 @@ "type": "array", "default": [ "checkout", - "fetch" + "fetch", + "stash-push" ], "items": { "type": "string", @@ -493,13 +494,19 @@ "checkout", "fetch", "pull", - "push" + "push", + "stash-apply", + "stash-pop", + "stash-push" ], "enumDescriptions": [ "Skips checkout command confirmation", "Skips fetch command confirmation", "Skips pull command confirmation", - "Skips push command confirmation" + "Skips push command confirmation", + "Skips stash apply command confirmation", + "Skips stash pop command confirmation", + "Skips stash push command confirmation" ] }, "minItems": 0, diff --git a/src/commands/git/merge.ts b/src/commands/git/merge.ts index f4a1c50..f5944da 100644 --- a/src/commands/git/merge.ts +++ b/src/commands/git/merge.ts @@ -4,7 +4,12 @@ import { Container } from '../../container'; import { GitBranch, GitTag, Repository } from '../../git/gitService'; import { GlyphChars } from '../../constants'; import { getBranchesAndOrTags, QuickCommandBase, QuickInputStep, QuickPickStep, StepState } from '../quickCommand'; -import { BranchQuickPickItem, RepositoryQuickPickItem, TagQuickPickItem } from '../../quickpicks'; +import { + BackOrCancelQuickPickItem, + BranchQuickPickItem, + RepositoryQuickPickItem, + TagQuickPickItem +} from '../../quickpicks'; import { Strings } from '../../system'; import { runGitCommandInTerminal } from '../../terminal'; import { Logger } from '../../logger'; @@ -100,14 +105,13 @@ export class MergeGitCommand extends QuickCommandBase { if (count === 0) { const step = this.createConfirmStep( `Confirm ${this.title}${Strings.pad(GlyphChars.Dot, 2, 2)}${state.repo.formattedName}`, - [ - { + [], + { + cancel: BackOrCancelQuickPickItem.create(true, true, { label: `Cancel ${this.title}`, - description: '', detail: `${state.destination.name} is up to date with ${state.source.name}` - } - ], - false + }) + } ); yield step; diff --git a/src/commands/git/rebase.ts b/src/commands/git/rebase.ts index 4a86330..ce258a0 100644 --- a/src/commands/git/rebase.ts +++ b/src/commands/git/rebase.ts @@ -4,7 +4,12 @@ import { Container } from '../../container'; import { GitBranch, GitTag, Repository } from '../../git/gitService'; import { GlyphChars } from '../../constants'; import { getBranchesAndOrTags, QuickCommandBase, QuickInputStep, QuickPickStep, StepState } from '../quickCommand'; -import { BranchQuickPickItem, RepositoryQuickPickItem, TagQuickPickItem } from '../../quickpicks'; +import { + BackOrCancelQuickPickItem, + BranchQuickPickItem, + RepositoryQuickPickItem, + TagQuickPickItem +} from '../../quickpicks'; import { Strings } from '../../system'; import { runGitCommandInTerminal } from '../../terminal'; import { Logger } from '../../logger'; @@ -100,14 +105,13 @@ export class RebaseGitCommand extends QuickCommandBase { if (count === 0) { const step = this.createConfirmStep( `Confirm ${this.title}${Strings.pad(GlyphChars.Dot, 2, 2)}${state.repo.formattedName}`, - [ - { + [], + { + cancel: BackOrCancelQuickPickItem.create(true, true, { label: `Cancel ${this.title}`, - description: '', detail: `${state.destination.name} is up to date with ${state.source.name}` - } - ], - false + }) + } ); yield step; diff --git a/src/commands/git/stash.ts b/src/commands/git/stash.ts new file mode 100644 index 0000000..d3c47e5 --- /dev/null +++ b/src/commands/git/stash.ts @@ -0,0 +1,503 @@ +'use strict'; +import { QuickPickItem, Uri, window } from 'vscode'; +import { Container } from '../../container'; +import { GitStashCommit, GitUri, Repository } from '../../git/gitService'; +import { BreakQuickCommand, QuickCommandBase, QuickInputStep, QuickPickStep, StepState } from '../quickCommand'; +import { BackOrCancelQuickPickItem, CommitQuickPickItem, RepositoryQuickPickItem } from '../../quickpicks'; +import { Iterables, Strings } from '../../system'; +import { GlyphChars } from '../../constants'; +import { Logger } from '../../logger'; +import { Messages } from '../../messages'; + +interface ApplyState { + subcommand: 'apply'; + repo: Repository; + stash: { stashName: string; message: string; repoPath: string }; + flags: string[]; +} + +interface DropState { + subcommand: 'drop'; + repo: Repository; + stash: { stashName: string; message: string; repoPath: string }; + flags: string[]; +} + +interface PopState { + subcommand: 'pop'; + repo: Repository; + stash: { stashName: string; message: string; repoPath: string }; + flags: string[]; +} + +interface PushState { + subcommand: 'push'; + repo: Repository; + message?: string; + uris?: Uri[]; + flags: string[]; +} + +type State = ApplyState | DropState | PopState | PushState; +type StashStepState = Partial & { counter: number; repo: Repository; skipConfirmation?: boolean }; + +interface StashSubcommandQuickPickItem extends QuickPickItem { + item: State['subcommand']; +} + +export interface CommandArgs { + readonly command: 'stash'; + state?: Partial; + + skipConfirmation?: boolean; +} + +export class StashGitCommand extends QuickCommandBase { + constructor(args?: CommandArgs) { + super('stash', 'Stash'); + + if (args === undefined || args.state === undefined) return; + + let counter = 0; + if (args.state.subcommand !== undefined) { + counter++; + } + + if (args.state.repo !== undefined) { + counter++; + } + + switch (args.state.subcommand) { + case 'apply': + case 'drop': + case 'pop': + if (args.state.stash !== undefined) { + counter++; + } + break; + + case 'push': + if (args.state.message !== undefined) { + counter++; + } + + break; + } + + if ( + args.skipConfirmation === undefined && + Container.config.gitCommands.skipConfirmations.includes(`${this.label}-${args.state.subcommand}`) + ) { + args.skipConfirmation = true; + } + + this._initialState = { + counter: counter, + skipConfirmation: counter > 0 && args.skipConfirmation, + ...args.state + }; + } + + protected async *steps(): AsyncIterableIterator { + const state: StepState = this._initialState === undefined ? { counter: 0 } : this._initialState; + let oneRepo = false; + + while (true) { + try { + if (state.subcommand === undefined || state.counter < 1) { + const step = this.createPickStep({ + title: this.title, + placeholder: `Choose a ${this.label} command`, + items: [ + { + label: 'apply', + picked: state.subcommand === 'apply', + item: 'apply' + }, + { + label: 'drop', + picked: state.subcommand === 'drop', + item: 'drop' + }, + { + label: 'pop', + picked: state.subcommand === 'pop', + item: 'pop' + }, + { + label: 'push', + picked: state.subcommand === 'push', + item: 'push' + } + ] + }); + const selection = yield step; + + if (!this.canMoveNext(step, state, selection)) { + break; + } + + state.subcommand = selection[0].item; + } + + if (state.repo === undefined || state.counter < 2) { + const repos = [...(await Container.git.getOrderedRepositories())]; + + if (repos.length === 1) { + oneRepo = true; + state.counter++; + state.repo = repos[0]; + } + else { + const step = this.createPickStep({ + title: `${this.title} ${state.subcommand}`, + placeholder: 'Choose a repository', + items: await Promise.all( + repos.map(r => + RepositoryQuickPickItem.create(r, r.id === (state.repo && state.repo.id), { + branch: true, + fetched: true, + status: true + }) + ) + ) + }); + const selection = yield step; + + if (!this.canMoveNext(step, state, selection)) { + continue; + } + + state.repo = selection[0].item; + } + } + + switch (state.subcommand) { + case 'apply': + case 'pop': + yield* this.applyOrPop(state as StashStepState); + break; + case 'drop': + yield* this.drop(state as StashStepState); + break; + case 'push': + yield* this.push(state as StashStepState); + break; + default: + return; + } + + if (oneRepo) { + state.counter--; + } + continue; + } + catch (ex) { + if (ex instanceof BreakQuickCommand) return; + + Logger.error(ex, `${this.title}.${state.subcommand}`); + + switch (state.subcommand) { + case 'apply': + case 'pop': + if ( + ex.message.includes( + 'Your local changes to the following files would be overwritten by merge' + ) + ) { + void window.showWarningMessage( + 'Unable to apply stash. Your working tree changes would be overwritten' + ); + + return; + } + else if (ex.message.includes('Auto-merging') && ex.message.includes('CONFLICT')) { + void window.showInformationMessage('Stash applied with conflicts'); + + return; + } + + void Messages.showGenericErrorMessage( + `Unable to apply stash \u2014 ${ex.message.trim().replace(/\n+?/g, '; ')}` + ); + + return; + + case 'drop': + void Messages.showGenericErrorMessage('Unable to delete stash'); + + return; + + case 'push': + if (ex.message.includes('newer version of Git')) { + void window.showErrorMessage(`Unable to stash changes. ${ex.message}`); + + return; + } + + void Messages.showGenericErrorMessage('Unable to stash changes'); + + return; + } + + throw ex; + } + } + } + + private async *applyOrPop( + state: StashStepState | StashStepState + ): AsyncIterableIterator { + while (true) { + if (state.stash === undefined || state.counter < 3) { + const stash = await Container.git.getStashList(state.repo.path); + + const step = this.createPickStep>({ + title: `${this.title} ${state.subcommand}${Strings.pad(GlyphChars.Dot, 2, 2)}${ + state.repo.formattedName + }`, + placeholder: + stash === undefined + ? `${state.repo.formattedName} has no stashed changes` + : 'Choose a stash to apply to your working tree', + items: + stash === undefined + ? [BackOrCancelQuickPickItem.create(false, true), BackOrCancelQuickPickItem.create()] + : [ + ...Iterables.map(stash.commits.values(), c => + CommitQuickPickItem.create( + c, + c.stashName === (state.stash && state.stash.stashName), + { + compact: true + } + ) + ) + ] + }); + const selection = yield step; + + if (!this.canMoveNext(step, state, selection)) { + break; + } + + state.stash = selection[0].item; + } + + if (state.skipConfirmation) { + state.flags = []; + } + else { + const message = + state.stash.message.length > 80 + ? `${state.stash.message.substring(0, 80)}${GlyphChars.Ellipsis}` + : state.stash.message; + + const step = this.createConfirmStep( + `Confirm ${this.title} ${state.subcommand}${Strings.pad(GlyphChars.Dot, 2, 2)}${ + state.repo.formattedName + }`, + [ + { + label: `${this.title} ${state.subcommand}`, + description: `${state.stash.stashName}${Strings.pad(GlyphChars.Dash, 2, 2)}${message}`, + detail: + state.subcommand === 'pop' + ? `Will delete ${ + state.stash!.stashName + } and apply the changes to the working tree of ${state.repo.formattedName}` + : `Will apply the changes from ${state.stash!.stashName} to the working tree of ${ + state.repo.formattedName + }`, + command: state.subcommand!, + item: [] + }, + // Alternate confirmation (if pop then apply, and vice versa) + { + label: `${this.title} ${state.subcommand === 'pop' ? 'apply' : 'pop'}`, + description: `${state.stash!.stashName}${Strings.pad(GlyphChars.Dash, 2, 2)}${message}`, + detail: + state.subcommand === 'pop' + ? `Will apply the changes from ${state.stash!.stashName} to the working tree of ${ + state.repo.formattedName + }` + : `Will delete ${ + state.stash!.stashName + } and apply the changes to the working tree of ${state.repo.formattedName}`, + command: state.subcommand === 'pop' ? 'apply' : 'pop', + item: [] + } + ], + { placeholder: `Confirm ${this.title} ${state.subcommand}` } + ); + const selection = yield step; + + if (!this.canMoveNext(step, state, selection)) { + break; + } + + state.subcommand = selection[0].command; + state.flags = selection[0].item; + } + + void Container.git.stashApply(state.repo.path, state.stash!.stashName, state.subcommand === 'pop'); + + throw new BreakQuickCommand(); + } + } + + private async *drop(state: StashStepState): AsyncIterableIterator { + while (true) { + if (state.stash === undefined || state.counter < 3) { + const stash = await Container.git.getStashList(state.repo.path); + + const step = this.createPickStep>({ + title: `${this.title} ${state.subcommand}${Strings.pad(GlyphChars.Dot, 2, 2)}${ + state.repo.formattedName + }`, + placeholder: + stash === undefined + ? `${state.repo.formattedName} has no stashed changes` + : 'Choose a stash to delete', + items: + stash === undefined + ? [BackOrCancelQuickPickItem.create(false, true), BackOrCancelQuickPickItem.create()] + : [ + ...Iterables.map(stash.commits.values(), c => + CommitQuickPickItem.create( + c, + c.stashName === (state.stash && state.stash.stashName), + { + compact: true + } + ) + ) + ] + }); + const selection = yield step; + + if (!this.canMoveNext(step, state, selection)) { + break; + } + + state.stash = selection[0].item; + } + + if (state.skipConfirmation) { + state.flags = []; + } + else { + const message = + state.stash.message.length > 80 + ? `${state.stash.message.substring(0, 80)}${GlyphChars.Ellipsis}` + : state.stash.message; + + const step = this.createConfirmStep( + `Confirm ${this.title} ${state.subcommand}${Strings.pad(GlyphChars.Dot, 2, 2)}${ + state.repo.formattedName + }`, + [ + { + label: `${this.title} ${state.subcommand}`, + description: `${state.stash.stashName}${Strings.pad(GlyphChars.Dash, 2, 2)}${message}`, + detail: `Will delete ${state.stash!.stashName}` + } + ], + { placeholder: `Confirm ${this.title} ${state.subcommand}` } + ); + const selection = yield step; + + if (!this.canMoveNext(step, state, selection)) { + break; + } + } + + void Container.git.stashDelete(state.repo.path, state.stash!.stashName); + + throw new BreakQuickCommand(); + } + } + + // eslint-disable-next-line require-await + private async *push(state: StashStepState): AsyncIterableIterator { + while (true) { + if (state.message === undefined || state.counter < 3) { + const step = this.createInputStep({ + title: `${this.title} ${state.subcommand}${Strings.pad(GlyphChars.Dot, 2, 2)}${ + state.repo.formattedName + }`, + placeholder: 'Please provide a stash message', + value: state.message + // validate: (value: string | undefined): [boolean, string | undefined] => [value != null, undefined] + }); + + const value = yield step; + + if (!this.canMoveNext(step, state, value)) { + break; + } + + state.message = value; + } + + if (state.skipConfirmation) { + state.flags = []; + } + else { + const step = this.createConfirmStep( + `Confirm ${this.title} ${state.subcommand}${Strings.pad(GlyphChars.Dot, 2, 2)}${ + state.repo.formattedName + }`, + state.uris === undefined || state.uris.length === 0 + ? [ + { + label: `${this.title} ${state.subcommand}`, + description: state.message, + detail: 'Will stash uncommitted changes', + item: [] + }, + { + label: `${this.title} ${state.subcommand}`, + description: state.message, + detail: 'Will stash uncommitted changes, including untracked files', + item: ['--include-untracked'] + }, + { + label: `${this.title} ${state.subcommand}`, + description: state.message, + detail: 'Will stash uncommitted changes, but will keep staged files intact', + item: ['--keep-index'] + } + ] + : [ + { + label: `${this.title} ${state.subcommand}`, + description: state.message, + detail: `Will stash changes in ${ + state.uris.length === 1 + ? GitUri.getFormattedPath(state.uris[0], { relativeTo: state.repo.path }) + : `${state.uris.length} files` + }`, + item: [] + } + ], + { placeholder: `Confirm ${this.title} ${state.subcommand}` } + ); + const selection = yield step; + + if (!this.canMoveNext(step, state, selection)) { + break; + } + + state.flags = selection[0].item; + } + + void Container.git.stashSave(state.repo.path, state.message, state.uris, { + includeUntracked: state.flags.includes('--include-untracked'), + keepIndex: state.flags.includes('--keep-index') + }); + + throw new BreakQuickCommand(); + } + } +} diff --git a/src/commands/gitCommands.ts b/src/commands/gitCommands.ts index 87a1d3c..14d9198 100644 --- a/src/commands/gitCommands.ts +++ b/src/commands/gitCommands.ts @@ -11,10 +11,16 @@ import { MergeGitCommand } from './git/merge'; import { CommandArgs as PullCommandArgs, PullGitCommand } from './git/pull'; import { CommandArgs as PushCommandArgs, PushGitCommand } from './git/push'; import { RebaseGitCommand } from './git/rebase'; +import { CommandArgs as StashCommandArgs, StashGitCommand } from './git/stash'; const sanitizeLabel = /\$\(.+?\)|\W/g; -export type GitCommandsCommandArgs = CheckoutCommandArgs | FetchCommandArgs | PullCommandArgs | PushCommandArgs; +export type GitCommandsCommandArgs = + | CheckoutCommandArgs + | FetchCommandArgs + | PullCommandArgs + | PushCommandArgs + | StashCommandArgs; class PickCommandStep implements QuickPickStep { readonly buttons = []; @@ -30,7 +36,8 @@ class PickCommandStep implements QuickPickStep { new FetchGitCommand(args && args.command === 'fetch' ? args : undefined), new PullGitCommand(args && args.command === 'pull' ? args : undefined), new PushGitCommand(args && args.command === 'push' ? args : undefined), - new RebaseGitCommand() + new RebaseGitCommand(), + new StashGitCommand(args && args.command === 'stash' ? args : undefined) ]; } diff --git a/src/commands/quickCommand.ts b/src/commands/quickCommand.ts index f72acae..dab7f1f 100644 --- a/src/commands/quickCommand.ts +++ b/src/commands/quickCommand.ts @@ -5,6 +5,12 @@ import { BackOrCancelQuickPickItem } from '../quickpicks'; export * from './quickCommand.helpers'; +export class BreakQuickCommand extends Error { + constructor() { + super('break'); + } +} + export enum Directive { Back = 'back' } @@ -108,12 +114,18 @@ export abstract class QuickCommandBase implements QuickPickItem { protected createConfirmStep( title: string, confirmations: T[], - cancellable: boolean = true + { + cancel, + placeholder + }: { + cancel?: BackOrCancelQuickPickItem; + placeholder?: string; + } = {} ): QuickPickStep { return this.createPickStep({ - placeholder: `Confirm ${this.title}`, + placeholder: placeholder || `Confirm ${this.title}`, title: title, - items: cancellable ? [...confirmations, BackOrCancelQuickPickItem.create()] : confirmations, + items: [...confirmations, cancel || BackOrCancelQuickPickItem.create()], selectedItems: [confirmations[0]] }); } diff --git a/src/commands/stashApply.ts b/src/commands/stashApply.ts index 671b105..75b0491 100644 --- a/src/commands/stashApply.ts +++ b/src/commands/stashApply.ts @@ -1,23 +1,19 @@ 'use strict'; -import { window } from 'vscode'; -import { GlyphChars } from '../constants'; +import { commands } from 'vscode'; import { Container } from '../container'; import { GitStashCommit } from '../git/gitService'; -import { Logger } from '../logger'; -import { Messages } from '../messages'; -import { CommandQuickPickItem, StashListQuickPick } from '../quickpicks'; +import { CommandQuickPickItem } from '../quickpicks'; import { command, Command, CommandContext, Commands, - getRepoPathOrPrompt, isCommandViewContextWithCommit, isCommandViewContextWithRepo } from './common'; +import { GitCommandsCommandArgs } from '../commands'; export interface StashApplyCommandArgs { - confirm?: boolean; deleteAfter?: boolean; repoPath?: string; stashItem?: { stashName: string; message: string; repoPath: string }; @@ -31,11 +27,10 @@ export class StashApplyCommand extends Command { super(Commands.StashApply); } - protected preExecute(context: CommandContext, args: StashApplyCommandArgs = { confirm: true, deleteAfter: false }) { + protected preExecute(context: CommandContext, args: StashApplyCommandArgs = { deleteAfter: false }) { if (isCommandViewContextWithCommit(context)) { args = { ...args }; args.stashItem = context.node.commit; - return this.execute(args); } else if (isCommandViewContextWithRepo(context)) { args = { ...args }; @@ -45,90 +40,20 @@ export class StashApplyCommand extends Command { return this.execute(args); } - async execute(args: StashApplyCommandArgs = { confirm: true, deleteAfter: false }) { - args = { ...args }; - - if (args.stashItem === undefined || args.stashItem.stashName === undefined) { - if (args.repoPath === undefined) { - args.repoPath = await getRepoPathOrPrompt( - `Apply stashed changes from which repository${GlyphChars.Ellipsis}`, - args.goBackCommand - ); - } - if (!args.repoPath) return undefined; - - const progressCancellation = StashListQuickPick.showProgress('apply'); - - try { - const stash = await Container.git.getStashList(args.repoPath); - if (stash === undefined) return window.showInformationMessage('There are no stashed changes'); - - if (progressCancellation.token.isCancellationRequested) return undefined; - - const currentCommand = new CommandQuickPickItem( - { - label: `go back ${GlyphChars.ArrowBack}`, - description: 'to apply stashed changes' - }, - Commands.StashApply, - [args] - ); - - const pick = await StashListQuickPick.show( - stash, - 'apply', - progressCancellation, - args.goBackCommand, - currentCommand - ); - if (pick instanceof CommandQuickPickItem) return pick.execute(); - if (pick === undefined) { - return args.goBackCommand === undefined ? undefined : args.goBackCommand.execute(); - } - - args.goBackCommand = currentCommand; - args.stashItem = pick.item; - } - finally { - progressCancellation.cancel(); - } + async execute(args: StashApplyCommandArgs = { deleteAfter: false }) { + let repo; + if (args.stashItem !== undefined || args.repoPath !== undefined) { + repo = await Container.git.getRepository((args.stashItem && args.stashItem.repoPath) || args.repoPath!); } - try { - if (args.confirm) { - const message = - args.stashItem.message.length > 80 - ? `${args.stashItem.message.substring(0, 80)}${GlyphChars.Ellipsis}` - : args.stashItem.message; - const result = await window.showWarningMessage( - `Apply stashed changes '${message}' to your working tree?`, - { title: 'Yes, delete after applying' }, - { title: 'Yes' }, - { title: 'No', isCloseAffordance: true } - ); - if (result === undefined || result.title === 'No') { - return args.goBackCommand === undefined ? undefined : args.goBackCommand.execute(); - } - - args.deleteAfter = result.title !== 'Yes'; - } - - return await Container.git.stashApply(args.stashItem.repoPath, args.stashItem.stashName, args.deleteAfter); - } - catch (ex) { - Logger.error(ex, 'StashApplyCommand'); - if (ex.message.includes('Your local changes to the following files would be overwritten by merge')) { - return window.showWarningMessage( - 'Unable to apply stash. Your working tree changes would be overwritten.' - ); - } - else if (ex.message.includes('Auto-merging') && ex.message.includes('CONFLICT')) { - return window.showInformationMessage('Stash applied with conflicts'); + const gitCommandArgs: GitCommandsCommandArgs = { + command: 'stash', + state: { + subcommand: args.deleteAfter ? 'pop' : 'apply', + repo: repo, + stash: args.stashItem } - - return Messages.showGenericErrorMessage( - `Unable to apply stash \u2014 ${ex.message.trim().replace(/\n+?/g, '; ')}` - ); - } + }; + return commands.executeCommand(Commands.GitCommands, gitCommandArgs); } } diff --git a/src/commands/stashDelete.ts b/src/commands/stashDelete.ts index bf2b22b..1587b6b 100644 --- a/src/commands/stashDelete.ts +++ b/src/commands/stashDelete.ts @@ -1,15 +1,13 @@ 'use strict'; -import { window } from 'vscode'; -import { GlyphChars } from '../constants'; +import { commands } from 'vscode'; import { Container } from '../container'; import { GitStashCommit } from '../git/gitService'; -import { Logger } from '../logger'; -import { Messages } from '../messages'; import { CommandQuickPickItem } from '../quickpicks'; import { command, Command, CommandContext, Commands, isCommandViewContextWithCommit } from './common'; +import { GitCommandsCommandArgs } from '../commands'; export interface StashDeleteCommandArgs { - confirm?: boolean; + repoPath?: string; stashItem?: { stashName: string; message: string; repoPath: string }; goBackCommand?: CommandQuickPickItem; @@ -21,51 +19,29 @@ export class StashDeleteCommand extends Command { super(Commands.StashDelete); } - protected preExecute(context: CommandContext, args: StashDeleteCommandArgs = { confirm: true }) { + protected preExecute(context: CommandContext, args: StashDeleteCommandArgs = {}) { if (isCommandViewContextWithCommit(context)) { args = { ...args }; args.stashItem = context.node.commit; - return this.execute(args); } return this.execute(args); } - async execute(args: StashDeleteCommandArgs = { confirm: true }) { - args = { ...args }; - if ( - args.stashItem === undefined || - args.stashItem.stashName === undefined || - args.stashItem.repoPath === undefined - ) { - return undefined; + async execute(args: StashDeleteCommandArgs = {}) { + let repo; + if (args.stashItem !== undefined || args.repoPath !== undefined) { + repo = await Container.git.getRepository((args.stashItem && args.stashItem.repoPath) || args.repoPath!); } - if (args.confirm === undefined) { - args.confirm = true; - } - - try { - if (args.confirm) { - const message = - args.stashItem.message.length > 80 - ? `${args.stashItem.message.substring(0, 80)}${GlyphChars.Ellipsis}` - : args.stashItem.message; - const result = await window.showWarningMessage( - `Delete stashed changes '${message}'?`, - { title: 'Yes' }, - { title: 'No', isCloseAffordance: true } - ); - if (result === undefined || result.title !== 'Yes') { - return args.goBackCommand === undefined ? undefined : args.goBackCommand.execute(); - } + const gitCommandArgs: GitCommandsCommandArgs = { + command: 'stash', + state: { + subcommand: 'drop', + repo: repo, + stash: args.stashItem } - - return await Container.git.stashDelete(args.stashItem.repoPath, args.stashItem.stashName); - } - catch (ex) { - Logger.error(ex, 'StashDeleteCommand'); - return Messages.showGenericErrorMessage('Unable to delete stash'); - } + }; + return commands.executeCommand(Commands.GitCommands, gitCommandArgs); } } diff --git a/src/commands/stashSave.ts b/src/commands/stashSave.ts index 7a3a437..6b0d83f 100644 --- a/src/commands/stashSave.ts +++ b/src/commands/stashSave.ts @@ -1,21 +1,18 @@ 'use strict'; -import { Uri, window } from 'vscode'; -import { GlyphChars } from '../constants'; +import { commands, Uri } from 'vscode'; import { Container } from '../container'; import { GitUri } from '../git/gitUri'; -import { Logger } from '../logger'; -import { Messages } from '../messages'; import { CommandQuickPickItem } from '../quickpicks'; import { command, Command, CommandContext, Commands, - getRepoPathOrPrompt, isCommandViewContextWithFile, isCommandViewContextWithRepo, isCommandViewContextWithRepoPath } from './common'; +import { GitCommandsCommandArgs } from '../commands'; export interface StashSaveCommandArgs { message?: string; @@ -34,7 +31,8 @@ export class StashSaveCommand extends Command { protected preExecute(context: CommandContext, args: StashSaveCommandArgs = {}) { if (isCommandViewContextWithFile(context)) { args = { ...args }; - args.uris = [GitUri.fromFile(context.node.file, context.node.file.repoPath || context.node.repoPath)]; + args.repoPath = context.node.file.repoPath || context.node.repoPath; + args.uris = [GitUri.fromFile(context.node.file, args.repoPath)]; } else if (isCommandViewContextWithRepo(context)) { args = { ...args }; @@ -60,39 +58,20 @@ export class StashSaveCommand extends Command { } async execute(args: StashSaveCommandArgs = {}) { - args = { ...args }; - - const uri = args.uris !== undefined && args.uris.length !== 0 ? args.uris[0] : undefined; - if (args.repoPath === undefined) { - args.repoPath = await getRepoPathOrPrompt( - `Stash changes for which repository${GlyphChars.Ellipsis}`, - args.goBackCommand, - uri - ); + let repo; + if (args.uris !== undefined || args.repoPath !== undefined) { + repo = await Container.git.getRepository((args.uris && args.uris[0]) || args.repoPath!); } - if (!args.repoPath) return undefined; - try { - if (args.message == null) { - args.message = await window.showInputBox({ - prompt: 'Please provide a stash message', - placeHolder: 'Stash message' - }); - if (args.message === undefined) { - return args.goBackCommand === undefined ? undefined : args.goBackCommand.execute(); - } + const gitCommandArgs: GitCommandsCommandArgs = { + command: 'stash', + state: { + subcommand: 'push', + repo: repo, + message: args.message, + uris: args.uris } - - return await Container.git.stashSave(args.repoPath, args.message, args.uris); - } - catch (ex) { - Logger.error(ex, 'StashSaveCommand'); - - const msg = ex && ex.message; - if (msg.includes('newer version of Git')) { - return window.showErrorMessage(`Unable to save stash. ${msg}`); - } - return Messages.showGenericErrorMessage('Unable to save stash'); - } + }; + return commands.executeCommand(Commands.GitCommands, gitCommandArgs); } } diff --git a/src/git/git.ts b/src/git/git.ts index 2308ae6..62bf1e6 100644 --- a/src/git/git.ts +++ b/src/git/git.ts @@ -1067,19 +1067,34 @@ export class Git { ); } - static stash__push(repoPath: string, pathspecs: string[], message?: string) { - const params = ['stash', 'push', '-u']; + static stash__push( + repoPath: string, + message?: string, + { + includeUntracked, + keepIndex, + pathspecs + }: { includeUntracked?: boolean; keepIndex?: boolean; pathspecs?: string[] } = {} + ) { + const params = ['stash', 'push']; + + if (includeUntracked || (pathspecs !== undefined && pathspecs.length !== 0)) { + params.push('-u'); + } + + if (keepIndex) { + params.push('-k'); + } + if (message) { params.push('-m', message); } - return git({ cwd: repoPath }, ...params, '--', ...pathspecs); - } - static stash__save(repoPath: string, message?: string) { - const params = ['stash', 'save', '-u']; - if (message) { - params.push(message); + params.push('--'); + if (pathspecs !== undefined && pathspecs.length !== 0) { + params.push(...pathspecs); } + return git({ cwd: repoPath }, ...params); } diff --git a/src/git/gitService.ts b/src/git/gitService.ts index 873f859..913c6f8 100644 --- a/src/git/gitService.ts +++ b/src/git/gitService.ts @@ -2752,13 +2752,18 @@ export class GitService implements Disposable { } @log() - stashSave(repoPath: string, message?: string, uris?: Uri[]) { - if (uris === undefined) return Git.stash__save(repoPath, message); + stashSave( + repoPath: string, + message?: string, + uris?: Uri[], + options: { includeUntracked?: boolean; keepIndex?: boolean } = {} + ) { + if (uris === undefined) return Git.stash__push(repoPath, message, options); GitService.ensureGitVersion('2.13.2', 'Stashing individual files'); - const pathspecs = uris.map(u => Git.splitPath(u.fsPath, repoPath)[0]); - return Git.stash__push(repoPath, pathspecs, message); + const pathspecs = uris.map(u => `./${Git.splitPath(u.fsPath, repoPath)[0]}`); + return Git.stash__push(repoPath, message, { ...options, pathspecs: pathspecs }); } static compareGitVersion(version: string) { diff --git a/src/quickpicks/commitQuickPick.ts b/src/quickpicks/commitQuickPick.ts index 84741d8..4491a68 100644 --- a/src/quickpicks/commitQuickPick.ts +++ b/src/quickpicks/commitQuickPick.ts @@ -250,7 +250,6 @@ export class CommitQuickPick { let remotes; if (stash) { const stashApplyCommmandArgs: StashApplyCommandArgs = { - confirm: true, deleteAfter: false, stashItem: commit as GitStashCommit, goBackCommand: options.currentCommand @@ -269,7 +268,6 @@ export class CommitQuickPick { ); const stashDeleteCommmandArgs: StashDeleteCommandArgs = { - confirm: true, stashItem: commit as GitStashCommit, goBackCommand: options.currentCommand }; diff --git a/src/quickpicks/gitQuickPicks.ts b/src/quickpicks/gitQuickPicks.ts index 7a14339..e82a219 100644 --- a/src/quickpicks/gitQuickPicks.ts +++ b/src/quickpicks/gitQuickPicks.ts @@ -19,9 +19,15 @@ export interface BackOrCancelQuickPickItem extends QuickPickItem { } export namespace BackOrCancelQuickPickItem { - export function create(cancelled: boolean = true, picked?: boolean, label?: string) { + export function create( + cancelled: boolean = true, + picked?: boolean, + options: { label?: string; description?: string; detail?: string } = {} + ) { const item: BackOrCancelQuickPickItem = { - label: label || (cancelled ? 'Cancel' : 'Back'), + label: options.label || (cancelled ? 'Cancel' : 'Back'), + description: options.description || '', + detail: options.detail, picked: picked, cancelled: cancelled };