diff --git a/package.json b/package.json index e5a2454..01236ab 100644 --- a/package.json +++ b/package.json @@ -475,6 +475,33 @@ "markdownDescription": "Specifies the style of the gravatar default (fallback) images", "scope": "window" }, + "gitlens.gitCommands.skipConfirmations": { + "type": "array", + "default": [ + "checkout", + "fetch" + ], + "items": { + "type": "string", + "enum": [ + "checkout", + "fetch", + "pull", + "push" + ], + "enumDescriptions": [ + "Skips checkout command confirmation", + "Skips fetch command confirmation", + "Skips pull command confirmation", + "Skips push command confirmation" + ] + }, + "minItems": 0, + "maxItems": 4, + "uniqueItems": true, + "markdownDescription": "Specifies which Git commands should have their confirmations skipped when executed from a GitLens view", + "scope": "window" + }, "gitlens.heatmap.ageThreshold": { "type": "string", "default": "90", diff --git a/src/commands/gitCommands.ts b/src/commands/gitCommands.ts index 8866568..6990063 100644 --- a/src/commands/gitCommands.ts +++ b/src/commands/gitCommands.ts @@ -1,121 +1,187 @@ 'use strict'; -import { Disposable, QuickInputButtons, QuickPickItem, window } from 'vscode'; +import { Disposable, InputBox, QuickInputButtons, QuickPick, QuickPickItem, window } from 'vscode'; import { command, Command, Commands } from './common'; import { log } from '../system'; +import { + isQuickInputStep, + isQuickPickStep, + QuickCommandBase, + QuickInputStep, + QuickPickStep +} from './quick/quickCommand'; +import { CommandArgs as CheckoutCommandArgs, CheckoutQuickCommand } from './quick/checkout'; import { CherryPickQuickCommand } from './quick/cherry-pick'; -import { QuickCommandBase, QuickPickStep } from './quick/quickCommand'; -import { FetchQuickCommand } from './quick/fetch'; +import { CommandArgs as FetchCommandArgs, FetchQuickCommand } from './quick/fetch'; import { MergeQuickCommand } from './quick/merge'; -import { PushQuickCommand } from './quick/push'; -import { PullQuickCommand } from './quick/pull'; -import { CheckoutQuickCommand } from './quick/checkout'; +import { CommandArgs as PullCommandArgs, PullQuickCommand } from './quick/pull'; +import { CommandArgs as PushCommandArgs, PushQuickCommand } from './quick/push'; import { RebaseQuickCommand } from './quick/rebase'; const sanitizeLabel = /\$\(.+?\)|\W/g; -@command() -export class GitCommandsCommand extends Command { - constructor() { - super(Commands.GitCommands); - } +export type GitCommandsCommandArgs = CheckoutCommandArgs | FetchCommandArgs | PullCommandArgs | PushCommandArgs; - @log({ args: false, correlate: true, singleLine: true, timed: false }) - async execute() { - const commands: QuickCommandBase[] = [ - new CheckoutQuickCommand(), +class PickCommandStep implements QuickPickStep { + readonly buttons = []; + readonly items: QuickCommandBase[]; + readonly placeholder = 'Select command...'; + readonly title = 'GitLens'; + + constructor(args?: GitCommandsCommandArgs) { + this.items = [ + new CheckoutQuickCommand(args && args.command === 'checkout' ? args : undefined), new CherryPickQuickCommand(), new MergeQuickCommand(), - new FetchQuickCommand(), - new PullQuickCommand(), - new PushQuickCommand(), + new FetchQuickCommand(args && args.command === 'fetch' ? args : undefined), + new PullQuickCommand(args && args.command === 'pull' ? args : undefined), + new PushQuickCommand(args && args.command === 'push' ? args : undefined), new RebaseQuickCommand() ]; + } - const quickpick = window.createQuickPick(); - quickpick.ignoreFocusOut = true; + private _active: QuickCommandBase | undefined; + get command(): QuickCommandBase | undefined { + return this._active; + } + set command(value: QuickCommandBase | undefined) { + if (this._active !== undefined) { + this._active.picked = false; + } - let inCommand: QuickCommandBase | undefined; + this._active = value; - function showCommand(command: QuickPickStep | undefined) { - if (command === undefined) { - const previousLabel = inCommand && inCommand.label; - inCommand = undefined; + if (this._active !== undefined) { + this._active.picked = true; + } + } - quickpick.buttons = []; - quickpick.title = 'GitLens'; - quickpick.placeholder = 'Select command...'; - quickpick.canSelectMany = false; - quickpick.items = commands; + find(commandName: string) { + const cmd = commandName.toLowerCase(); + return this.items.find(c => c.label.replace(sanitizeLabel, '').toLowerCase() === cmd); + } +} - if (previousLabel) { - const active = quickpick.items.find(i => i.label === previousLabel); - if (active) { - quickpick.activeItems = [active]; - } - } - } - else { - quickpick.buttons = command.buttons || [QuickInputButtons.Back]; - quickpick.title = command.title; - quickpick.placeholder = command.placeholder; - quickpick.canSelectMany = Boolean(command.multiselect); +@command() +export class GitCommandsCommand extends Command { + constructor() { + super(Commands.GitCommands); + } - quickpick.items = command.items; + @log({ args: false, correlate: true, singleLine: true, timed: false }) + async execute(args?: GitCommandsCommandArgs) { + const commandsStep = new PickCommandStep(args); - if (quickpick.canSelectMany) { - quickpick.selectedItems = command.selectedItems || quickpick.items.filter(i => i.picked); - quickpick.activeItems = quickpick.selectedItems; - } - else { - quickpick.activeItems = command.selectedItems || quickpick.items.filter(i => i.picked); - } + let step: QuickPickStep | QuickInputStep | undefined = commandsStep; - // // BUG: https://github.com/microsoft/vscode/issues/75046 - // // If we can multiselect, then ensure the selectedItems gets reset (otherwise it could end up included the current selected items) - // if (quickpick.canSelectMany && quickpick.selectedItems.length !== 0) { - // quickpick.selectedItems = []; - // } + if (args) { + const command = commandsStep.find(args.command); + if (command !== undefined) { + commandsStep.command = command; + + const next = await command.next(); + if (next.done) return; + + step = next.value; } } - async function next(command: QuickCommandBase, items: QuickPickItem[] | undefined) { - quickpick.busy = true; - // quickpick.enabled = false; + while (step !== undefined) { + if (isQuickPickStep(step)) { + step = await this.showPickStep(step, commandsStep); + continue; + } - const next = await command.next(items); - if (next.done) { - return false; + if (isQuickInputStep(step)) { + step = await this.showInputStep(step, commandsStep); + continue; } - quickpick.value = ''; - showCommand(next.value); + break; + } + } + + private async showInputStep(step: QuickInputStep, commandsStep: PickCommandStep) { + const input = window.createInputBox(); + input.ignoreFocusOut = true; - // quickpick.enabled = true; - quickpick.busy = false; + const disposables: Disposable[] = []; - return true; + try { + return await new Promise(resolve => { + disposables.push( + input.onDidHide(() => resolve()), + input.onDidTriggerButton(async e => { + if (e === QuickInputButtons.Back) { + input.value = ''; + if (commandsStep.command !== undefined) { + input.busy = true; + resolve((await commandsStep.command.previous()) || commandsStep); + } + + return; + } + + const step = commandsStep.command && commandsStep.command.value; + if (step === undefined || !isQuickInputStep(step) || step.onDidClickButton === undefined) + return; + + step.onDidClickButton(input, e); + }), + input.onDidChangeValue(async e => { + if (step.validate === undefined) return; + + const [, message] = await step.validate(e); + input.validationMessage = message; + }), + input.onDidAccept(async () => { + resolve(await this.nextStep(input, commandsStep.command!, input.value)); + }) + ); + + input.buttons = step.buttons || [QuickInputButtons.Back]; + input.title = step.title; + input.placeholder = step.placeholder; + if (step.value !== undefined) { + input.value = step.value; + } + + // If we are starting over clear the previously active command + if (commandsStep.command !== undefined && step === commandsStep) { + commandsStep.command = undefined; + } + + input.show(); + }); } + finally { + input.dispose(); + disposables.forEach(d => d.dispose()); + } + } - showCommand(undefined); + private async showPickStep(step: QuickPickStep, commandsStep: PickCommandStep) { + const quickpick = window.createQuickPick(); + quickpick.ignoreFocusOut = true; const disposables: Disposable[] = []; try { - void (await new Promise(resolve => { + return await new Promise(resolve => { disposables.push( quickpick.onDidHide(() => resolve()), quickpick.onDidTriggerButton(async e => { if (e === QuickInputButtons.Back) { quickpick.value = ''; - if (inCommand !== undefined) { - showCommand(await inCommand.previous()); + if (commandsStep.command !== undefined) { + quickpick.busy = true; + resolve((await commandsStep.command.previous()) || commandsStep); } return; } - const step = inCommand && inCommand.value; - if (step === undefined || step.onDidClickButton === undefined) return; + const step = commandsStep.command && commandsStep.command.value; + if (step === undefined || !isQuickPickStep(step) || step.onDidClickButton === undefined) return; step.onDidClickButton(quickpick, e); }), @@ -133,21 +199,18 @@ export class GitCommandsCommand extends Command { return; } - const cmd = quickpick.value.toLowerCase().trim(); - let items; - if (inCommand === undefined) { - const command = commands.find( - c => c.label.replace(sanitizeLabel, '').toLowerCase() === cmd - ); + if (commandsStep.command === undefined) { + const command = commandsStep.find(quickpick.value.trim()); if (command === undefined) return; - inCommand = command; + commandsStep.command = command; } else { - const step = inCommand.value; - if (step === undefined) return; + const step = commandsStep.command.value; + if (step === undefined || !isQuickPickStep(step)) return; + const cmd = quickpick.value.trim().toLowerCase(); const item = step.items.find( i => i.label.replace(sanitizeLabel, '').toLowerCase() === cmd ); @@ -156,40 +219,89 @@ export class GitCommandsCommand extends Command { items = [item]; } - if (!(await next(inCommand, items))) { - resolve(); - } + resolve(await this.nextStep(quickpick, commandsStep.command, items)); } }), quickpick.onDidAccept(async () => { let items = quickpick.selectedItems; if (items.length === 0) { - if (!quickpick.canSelectMany || quickpick.activeItems.length === 0) return; + if (!quickpick.canSelectMany || quickpick.activeItems.length === 0) { + const value = quickpick.value.trim(); + if (value.length === 0) return; + + const step = commandsStep.command && commandsStep.command.value; + if (step === undefined || !isQuickPickStep(step) || step.onDidAccept === undefined) + return; + + quickpick.busy = true; + + if (await step.onDidAccept(quickpick)) { + resolve(await this.nextStep(quickpick, commandsStep.command!, value)); + } + + quickpick.busy = false; + return; + } items = quickpick.activeItems; } - if (inCommand === undefined) { + if (commandsStep.command === undefined) { const command = items[0]; if (!QuickCommandBase.is(command)) return; - inCommand = command; + commandsStep.command = command; } - if (!(await next(inCommand, items as QuickPickItem[]))) { - resolve(); - } + resolve(await this.nextStep(quickpick, commandsStep.command, items as QuickPickItem[])); }) ); - quickpick.show(); - })); + quickpick.buttons = step.buttons || [QuickInputButtons.Back]; + quickpick.title = step.title; + quickpick.placeholder = step.placeholder; + quickpick.canSelectMany = Boolean(step.multiselect); + + quickpick.items = step.items; + + if (quickpick.canSelectMany) { + quickpick.selectedItems = step.selectedItems || quickpick.items.filter(i => i.picked); + quickpick.activeItems = quickpick.selectedItems; + } + else { + quickpick.activeItems = step.selectedItems || quickpick.items.filter(i => i.picked); + } + + // If we are starting over clear the previously active command + if (commandsStep.command !== undefined && step === commandsStep) { + commandsStep.command = undefined; + } + + if (step.value !== undefined) { + quickpick.value = step.value; + } - quickpick.hide(); + quickpick.show(); + }); } finally { quickpick.dispose(); disposables.forEach(d => d.dispose()); } } + + private async nextStep( + quickInput: QuickPick | InputBox, + command: QuickCommandBase, + value: QuickPickItem[] | string | undefined + ) { + quickInput.busy = true; + // quickInput.enabled = false; + + const next = await command.next(value); + if (next.done) return undefined; + + quickInput.value = ''; + return next.value; + } } diff --git a/src/commands/quick/checkout.ts b/src/commands/quick/checkout.ts index c6856a4..6f950ba 100644 --- a/src/commands/quick/checkout.ts +++ b/src/commands/quick/checkout.ts @@ -2,21 +2,59 @@ /* eslint-disable no-loop-func */ import { ProgressLocation, QuickInputButtons, window } from 'vscode'; import { Container } from '../../container'; -import { Repository } from '../../git/gitService'; +import { GitBranch, GitReference, GitService, GitTag, Repository } from '../../git/gitService'; import { GlyphChars } from '../../constants'; -import { GitCommandBase } from './gitCommand'; -import { CommandAbortError, QuickPickStep } from './quickCommand'; +import { + CommandAbortError, + getBranchesAndOrTags, + QuickCommandBase, + QuickInputStep, + QuickPickStep, + StepState +} from './quickCommand'; import { ReferencesQuickPickItem, RepositoryQuickPickItem } from '../../quickpicks'; import { Strings } from '../../system'; interface State { repos: Repository[]; - ref: string; + branchOrTagOrRef: GitBranch | GitTag | GitReference; + createBranch?: string; } -export class CheckoutQuickCommand extends GitCommandBase { - constructor() { +export interface CommandArgs { + readonly command: 'checkout'; + state?: Partial; + + skipConfirmation?: boolean; +} + +export class CheckoutQuickCommand extends QuickCommandBase { + constructor(args?: CommandArgs) { super('checkout', 'Checkout'); + + if (args === undefined || args.state === undefined) return; + + let counter = 0; + if (args.state.repos !== undefined && args.state.repos.length !== 0) { + counter++; + } + + if (args.state.branchOrTagOrRef !== undefined) { + counter++; + } + + if ( + args.skipConfirmation === undefined && + Container.config.gitCommands.skipConfirmations.includes(this.label) + ) { + args.skipConfirmation = true; + } + + this._initialState = { + counter: counter, + skipConfirmation: counter > 1 && args.skipConfirmation, + ...args.state + }; } async execute(state: State) { @@ -25,14 +63,19 @@ export class CheckoutQuickCommand extends GitCommandBase { location: ProgressLocation.Notification, title: `Checking out ${ state.repos.length === 1 ? state.repos[0].formattedName : `${state.repos.length} repositories` - } to ${state.ref}` + } to ${state.branchOrTagOrRef.ref}` }, - () => Promise.all(state.repos.map(r => r.checkout(state.ref, { progress: false }))) + () => + Promise.all( + state.repos.map(r => + r.checkout(state.branchOrTagOrRef.ref, { createBranch: state.createBranch, progress: false }) + ) + ) )); } - async *steps(): AsyncIterableIterator { - const state: Partial & { counter: number } = { counter: 0 }; + protected async *steps(): AsyncIterableIterator { + const state: StepState = this._initialState === undefined ? { counter: 0 } : this._initialState; let oneRepo = false; let showTags = false; @@ -47,7 +90,7 @@ export class CheckoutQuickCommand extends GitCommandBase { state.repos = [repos[0]]; } else { - const step = this.createStep({ + const step = this.createPickStep({ multiselect: true, title: this.title, placeholder: 'Choose repositories', @@ -71,19 +114,27 @@ export class CheckoutQuickCommand extends GitCommandBase { } } - if (state.ref === undefined || state.counter < 2) { + if (state.branchOrTagOrRef === undefined || state.counter < 2) { const includeTags = showTags || state.repos.length === 1; - const items = await this.getBranchesAndOrTags(state.repos, includeTags); - const step = this.createStep({ + const items = await getBranchesAndOrTags( + state.repos, + includeTags, + state.repos.length === 1 ? undefined : { filterBranches: b => !b.remote } + ); + const step = this.createPickStep({ title: `${this.title}${Strings.pad(GlyphChars.Dot, 2, 2)}${ state.repos.length === 1 ? state.repos[0].formattedName : `${state.repos.length} repositories` }`, - placeholder: `Choose a branch${includeTags ? ' or tag' : ''} to checkout to`, + placeholder: `Choose a branch${ + includeTags ? ' or tag' : '' + } to checkout to${GlyphChars.Space.repeat(3)}(select or enter a reference)`, items: items, - selectedItems: state.ref ? items.filter(ref => ref.label === state.ref) : undefined, + selectedItems: state.branchOrTagOrRef + ? items.filter(ref => ref.label === state.branchOrTagOrRef!.ref) + : undefined, buttons: includeTags ? [QuickInputButtons.Back] : [ @@ -104,13 +155,21 @@ export class CheckoutQuickCommand extends GitCommandBase { showTags = true; } - quickpick.placeholder = `Choose a branch${showTags ? ' or tag' : ''} to checkout to`; + quickpick.placeholder = `Choose a branch${ + showTags ? ' or tag' : '' + } to checkout to${GlyphChars.Space.repeat(3)}(select or enter a reference)`; quickpick.buttons = [QuickInputButtons.Back]; - quickpick.items = await this.getBranchesAndOrTags(state.repos!, showTags); + quickpick.items = await getBranchesAndOrTags(state.repos!, showTags); quickpick.busy = false; quickpick.enabled = true; + }, + onDidAccept: (quickpick): Promise => { + const ref = quickpick.value.trim(); + if (ref.length === 0 || state.repos!.length !== 1) return Promise.resolve(false); + + return Container.git.validateReference(state.repos![0].path, ref); } }); const selection = yield step; @@ -123,29 +182,81 @@ export class CheckoutQuickCommand extends GitCommandBase { continue; } - state.ref = selection[0].item.ref; + state.branchOrTagOrRef = + typeof selection === 'string' + ? { name: GitService.shortenSha(selection), ref: selection } + : selection[0].item; } - const step = this.createConfirmStep( - `Confirm ${this.title}${Strings.pad(GlyphChars.Dot, 2, 2)}${ - state.repos.length === 1 ? state.repos[0].formattedName : `${state.repos.length} repositories` - } to ${state.ref}`, - [ - { - label: this.title, - description: `${state.ref}`, - detail: `Will checkout ${ + if (state.branchOrTagOrRef instanceof GitBranch && state.branchOrTagOrRef.remote) { + const branches = await Container.git.getBranches(state.branchOrTagOrRef.repoPath, { + filter: b => { + return b.tracking === state.branchOrTagOrRef!.name; + } + }); + + if (branches.length === 0) { + const step = this.createInputStep({ + title: `${this.title} new branch to ${state.branchOrTagOrRef.ref}${Strings.pad( + GlyphChars.Dot, + 2, + 2 + )}${ state.repos.length === 1 ? state.repos[0].formattedName : `${state.repos.length} repositories` - } to ${state.ref}` + }`, + placeholder: 'Choose name for the local branch', + value: state.branchOrTagOrRef.getName(), + validate: async (value: string | undefined): Promise<[boolean, string | undefined]> => { + if (value == null) return [false, undefined]; + + value = value.trim(); + if (value.length === 0) return [false, 'Please enter a valid branch name']; + + const valid = Boolean(await Container.git.validateBranchName(value!)); + return [valid, valid ? undefined : `'${value}' isn't a valid branch name`]; + } + }); + + const value = yield step; + + if (!(await this.canMoveNext(step, state, value))) { + continue; } - ] - ); - const selection = yield step; - if (!this.canMoveNext(step, state, selection)) { - continue; + state.createBranch = value; + } + } + + if (!state.skipConfirmation) { + const step = this.createConfirmStep( + `Confirm ${this.title}${Strings.pad(GlyphChars.Dot, 2, 2)}${ + state.repos.length === 1 + ? state.repos[0].formattedName + : `${state.repos.length} repositories` + }`, + [ + { + label: this.title, + description: `${state.createBranch ? `${state.createBranch} to ` : ''}${ + state.branchOrTagOrRef.name + }`, + detail: `Will ${ + state.createBranch ? `create ${state.createBranch} and` : '' + } checkout to ${state.branchOrTagOrRef.name} in ${ + state.repos.length === 1 + ? state.repos[0].formattedName + : `${state.repos.length} repositories` + }` + } + ] + ); + const selection = yield step; + + if (!this.canMoveNext(step, state, selection)) { + continue; + } } this.execute(state as State); diff --git a/src/commands/quick/cherry-pick.ts b/src/commands/quick/cherry-pick.ts index 6fae621..7f85c77 100644 --- a/src/commands/quick/cherry-pick.ts +++ b/src/commands/quick/cherry-pick.ts @@ -4,8 +4,14 @@ import { Container } from '../../container'; import { GitBranch, GitLogCommit, Repository } from '../../git/gitService'; import { GlyphChars } from '../../constants'; import { Iterables, Strings } from '../../system'; -import { GitCommandBase } from './gitCommand'; -import { CommandAbortError, QuickPickStep } from './quickCommand'; +import { + CommandAbortError, + getBranchesAndOrTags, + QuickCommandBase, + QuickInputStep, + QuickPickStep, + StepState +} from './quickCommand'; import { BranchQuickPickItem, CommitQuickPickItem, RepositoryQuickPickItem } from '../../quickpicks'; import { runGitCommandInTerminal } from '../../terminal'; @@ -16,7 +22,7 @@ interface State { commits: GitLogCommit[]; } -export class CherryPickQuickCommand extends GitCommandBase { +export class CherryPickQuickCommand extends QuickCommandBase { constructor() { super('cherry-pick', 'Cherry Pick', { description: 'via Terminal' }); } @@ -27,8 +33,8 @@ export class CherryPickQuickCommand extends GitCommandBase { runGitCommandInTerminal('cherry-pick', state.commits.map(c => c.sha).join(' '), state.repo.path, true); } - async *steps(): AsyncIterableIterator { - const state: Partial & { counter: number } = { counter: 0 }; + protected async *steps(): AsyncIterableIterator { + const state: StepState = this._initialState === undefined ? { counter: 0 } : this._initialState; let oneRepo = false; while (true) { @@ -44,7 +50,7 @@ export class CherryPickQuickCommand extends GitCommandBase { else { const active = state.repo ? state.repo : await Container.git.getActiveRepository(); - const step = this.createStep({ + const step = this.createPickStep({ title: this.title, placeholder: 'Choose a repository', items: await Promise.all( @@ -73,14 +79,20 @@ export class CherryPickQuickCommand extends GitCommandBase { if (state.source === undefined || state.counter < 2) { const destId = state.destination.id; - const step = this.createStep({ + const step = this.createPickStep({ title: `${this.title} into ${state.destination.name}${Strings.pad(GlyphChars.Dot, 2, 2)}${ state.repo.name }`, placeholder: 'Choose a branch or tag to cherry-pick from', - items: await this.getBranchesAndOrTags(state.repo, true, { + items: await getBranchesAndOrTags(state.repo, true, { filterBranches: b => b.id !== destId }) + // onDidAccept: (quickpick): Promise => { + // const ref = quickpick.value.trim(); + // if (ref.length === 0) return Promise.resolve(false); + + // return Container.git.validateReference(state.repo!.path, ref); + // } }); const selection = yield step; @@ -92,16 +104,22 @@ export class CherryPickQuickCommand extends GitCommandBase { continue; } + // TODO: Allow pasting in commit id + // if (typeof selection === 'string') { + + // } + // else { state.source = selection[0].item; + // } } if (state.commits === undefined || state.counter < 3) { - const log = await Container.git.getLog(state.source.repoPath, { + const log = await Container.git.getLog(state.repo.path, { ref: `${state.destination.ref}..${state.source.ref}`, merges: false }); - const step = this.createStep({ + const step = this.createPickStep({ title: `${this.title} onto ${state.destination.name}${Strings.pad(GlyphChars.Dot, 2, 2)}${ state.repo.name }`, diff --git a/src/commands/quick/fetch.ts b/src/commands/quick/fetch.ts index f0dacfe..010edfc 100644 --- a/src/commands/quick/fetch.ts +++ b/src/commands/quick/fetch.ts @@ -2,7 +2,7 @@ import { QuickPickItem } from 'vscode'; import { Container } from '../../container'; import { Repository } from '../../git/gitService'; -import { CommandAbortError, QuickCommandBase, QuickPickStep } from './quickCommand'; +import { CommandAbortError, QuickCommandBase, QuickInputStep, QuickPickStep, StepState } from './quickCommand'; import { RepositoryQuickPickItem } from '../../quickpicks'; import { Strings } from '../../system'; import { GlyphChars } from '../../constants'; @@ -12,9 +12,36 @@ interface State { flags: string[]; } -export class FetchQuickCommand extends QuickCommandBase { - constructor() { +export interface CommandArgs { + readonly command: 'fetch'; + state?: Partial; + + skipConfirmation?: boolean; +} + +export class FetchQuickCommand extends QuickCommandBase { + constructor(args?: CommandArgs) { super('fetch', 'Fetch'); + + if (args === undefined || args.state === undefined) return; + + let counter = 0; + if (args.state.repos !== undefined && args.state.repos.length !== 0) { + counter++; + } + + if ( + args.skipConfirmation === undefined && + Container.config.gitCommands.skipConfirmations.includes(this.label) + ) { + args.skipConfirmation = true; + } + + this._initialState = { + counter: counter, + skipConfirmation: counter > 0 && args.skipConfirmation, + ...args.state + }; } execute(state: State) { @@ -24,8 +51,8 @@ export class FetchQuickCommand extends QuickCommandBase { }); } - async *steps(): AsyncIterableIterator { - const state: Partial & { counter: number } = { counter: 0 }; + protected async *steps(): AsyncIterableIterator { + const state: StepState = this._initialState === undefined ? { counter: 0 } : this._initialState; let oneRepo = false; while (true) { @@ -39,17 +66,21 @@ export class FetchQuickCommand extends QuickCommandBase { state.repos = [repos[0]]; } else { - const step = this.createStep({ + const step = this.createPickStep({ multiselect: true, title: this.title, placeholder: 'Choose repositories', items: await Promise.all( - repos.map(r => - RepositoryQuickPickItem.create(r, undefined, { - branch: true, - fetched: true, - status: true - }) + repos.map(repo => + RepositoryQuickPickItem.create( + repo, + state.repos ? state.repos.some(r => r.id === repo.id) : undefined, + { + branch: true, + fetched: true, + status: true + } + ) ) ) }); @@ -63,55 +94,62 @@ export class FetchQuickCommand extends QuickCommandBase { } } - const step = this.createConfirmStep( - `Confirm ${this.title}${Strings.pad(GlyphChars.Dot, 2, 2)}${ - state.repos.length === 1 ? state.repos[0].formattedName : `${state.repos.length} repositories` - }`, - [ - { - label: this.title, - description: '', - detail: `Will fetch ${ - state.repos.length === 1 - ? state.repos[0].formattedName - : `${state.repos.length} repositories` - }`, - item: [] - }, - { - label: `${this.title} & Prune`, - description: '--prune', - detail: `Will fetch and prune ${ - state.repos.length === 1 - ? state.repos[0].formattedName - : `${state.repos.length} repositories` - }`, - item: ['--prune'] - }, - { - label: `${this.title} All`, - description: '--all', - detail: `Will fetch all remotes of ${ - state.repos.length === 1 - ? state.repos[0].formattedName - : `${state.repos.length} repositories` - }`, - item: ['--all'] + if (state.skipConfirmation) { + state.flags = []; + } + else { + const step = this.createConfirmStep( + `Confirm ${this.title}${Strings.pad(GlyphChars.Dot, 2, 2)}${ + state.repos.length === 1 + ? state.repos[0].formattedName + : `${state.repos.length} repositories` + }`, + [ + { + label: this.title, + description: '', + detail: `Will fetch ${ + state.repos.length === 1 + ? state.repos[0].formattedName + : `${state.repos.length} repositories` + }`, + item: [] + }, + { + label: `${this.title} & Prune`, + description: '--prune', + detail: `Will fetch and prune ${ + state.repos.length === 1 + ? state.repos[0].formattedName + : `${state.repos.length} repositories` + }`, + item: ['--prune'] + }, + { + label: `${this.title} All`, + description: '--all', + detail: `Will fetch all remotes of ${ + state.repos.length === 1 + ? state.repos[0].formattedName + : `${state.repos.length} repositories` + }`, + item: ['--all'] + } + ] + ); + const selection = yield step; + + if (!this.canMoveNext(step, state, selection)) { + if (oneRepo) { + break; } - ] - ); - const selection = yield step; - if (!this.canMoveNext(step, state, selection)) { - if (oneRepo) { - break; + continue; } - continue; + state.flags = selection[0].item; } - state.flags = selection[0].item; - this.execute(state as State); break; } diff --git a/src/commands/quick/gitCommand.ts b/src/commands/quick/gitCommand.ts deleted file mode 100644 index 19f4558..0000000 --- a/src/commands/quick/gitCommand.ts +++ /dev/null @@ -1,88 +0,0 @@ -'use strict'; -import { intersectionWith } from 'lodash'; -import { QuickCommandBase } from './quickCommand'; -import { GitBranch, GitTag, Repository } from '../../git/git'; -import { BranchQuickPickItem, TagQuickPickItem } from '../../quickpicks'; - -export abstract class GitCommandBase extends QuickCommandBase { - protected async getBranchesAndOrTags( - repos: Repository | Repository[], - includeTags: boolean, - { - filterBranches, - filterTags, - picked - }: { filterBranches?: (b: GitBranch) => boolean; filterTags?: (t: GitTag) => boolean; picked?: string } = {} - ) { - let branches: GitBranch[]; - let tags: GitTag[] | undefined; - - let singleRepo = false; - if (repos instanceof Repository || repos.length === 1) { - singleRepo = true; - const repo = repos instanceof Repository ? repos : repos[0]; - - [branches, tags] = await Promise.all([ - repo.getBranches({ filter: filterBranches, sort: true }), - includeTags ? repo.getTags({ filter: filterTags, includeRefs: true, sort: true }) : undefined - ]); - } - else { - const [branchesByRepo, tagsByRepo] = await Promise.all([ - Promise.all(repos.map(r => r.getBranches({ filter: filterBranches, sort: true }))), - includeTags - ? Promise.all(repos.map(r => r.getTags({ filter: filterTags, includeRefs: true, sort: true }))) - : undefined - ]); - - branches = GitBranch.sort( - intersectionWith(...branchesByRepo, ((b1: GitBranch, b2: GitBranch) => b1.name === b2.name) as any) - ); - - if (includeTags) { - tags = GitTag.sort( - intersectionWith(...tagsByRepo!, ((t1: GitTag, t2: GitTag) => t1.name === t2.name) as any) - ); - } - } - - if (!includeTags) { - return Promise.all( - branches.map(b => - BranchQuickPickItem.create(b, undefined, { - current: singleRepo ? 'checkmark' : false, - ref: singleRepo, - status: singleRepo, - type: 'remote' - }) - ) - ); - } - - return Promise.all([ - ...branches! - .filter(b => !b.remote) - .map(b => - BranchQuickPickItem.create(b, picked != null && b.ref === picked, { - current: singleRepo ? 'checkmark' : false, - ref: singleRepo, - status: singleRepo - }) - ), - ...tags!.map(t => - TagQuickPickItem.create(t, picked != null && t.ref === picked, { - ref: singleRepo, - type: true - }) - ), - ...branches! - .filter(b => b.remote) - .map(b => - BranchQuickPickItem.create(b, picked != null && b.ref === picked, { - current: singleRepo ? 'checkmark' : false, - type: 'remote' - }) - ) - ]); - } -} diff --git a/src/commands/quick/merge.ts b/src/commands/quick/merge.ts index 3e83e7d..d7f1d42 100644 --- a/src/commands/quick/merge.ts +++ b/src/commands/quick/merge.ts @@ -3,10 +3,16 @@ import { QuickPickItem } from 'vscode'; import { Container } from '../../container'; import { GitBranch, Repository } from '../../git/gitService'; import { GlyphChars } from '../../constants'; -import { CommandAbortError, QuickPickStep } from './quickCommand'; +import { + CommandAbortError, + getBranchesAndOrTags, + QuickCommandBase, + QuickInputStep, + QuickPickStep, + StepState +} from './quickCommand'; import { BranchQuickPickItem, RepositoryQuickPickItem } from '../../quickpicks'; import { Strings } from '../../system'; -import { GitCommandBase } from './gitCommand'; import { runGitCommandInTerminal } from '../../terminal'; interface State { @@ -16,7 +22,7 @@ interface State { flags: string[]; } -export class MergeQuickCommand extends GitCommandBase { +export class MergeQuickCommand extends QuickCommandBase { constructor() { super('merge', 'Merge', { description: 'via Terminal' }); } @@ -25,8 +31,8 @@ export class MergeQuickCommand extends GitCommandBase { runGitCommandInTerminal('merge', [...state.flags, state.source.ref].join(' '), state.repo.path, true); } - async *steps(): AsyncIterableIterator { - const state: Partial & { counter: number } = { counter: 0 }; + protected async *steps(): AsyncIterableIterator { + const state: StepState = this._initialState === undefined ? { counter: 0 } : this._initialState; let oneRepo = false; while (true) { @@ -42,7 +48,7 @@ export class MergeQuickCommand extends GitCommandBase { else { const active = state.repo ? state.repo : await Container.git.getActiveRepository(); - const step = this.createStep({ + const step = this.createPickStep({ title: this.title, placeholder: 'Choose a repository', items: await Promise.all( @@ -71,12 +77,12 @@ export class MergeQuickCommand extends GitCommandBase { if (state.source === undefined || state.counter < 2) { const destId = state.destination.id; - const step = this.createStep({ + const step = this.createPickStep({ title: `${this.title} into ${state.destination.name}${Strings.pad(GlyphChars.Dot, 2, 2)}${ state.repo.name }`, placeholder: `Choose a branch or tag to merge into ${state.destination.name}`, - items: await this.getBranchesAndOrTags(state.repo, true, { + items: await getBranchesAndOrTags(state.repo, true, { filterBranches: b => b.id !== destId, picked: state.source && state.source.ref }) diff --git a/src/commands/quick/pull.ts b/src/commands/quick/pull.ts index 7708c23..b7bc57e 100644 --- a/src/commands/quick/pull.ts +++ b/src/commands/quick/pull.ts @@ -2,7 +2,7 @@ import { QuickPickItem } from 'vscode'; import { Container } from '../../container'; import { Repository } from '../../git/gitService'; -import { CommandAbortError, QuickCommandBase, QuickPickStep } from './quickCommand'; +import { CommandAbortError, QuickCommandBase, QuickInputStep, QuickPickStep, StepState } from './quickCommand'; import { RepositoryQuickPickItem } from '../../quickpicks'; import { Strings } from '../../system'; import { GlyphChars } from '../../constants'; @@ -12,17 +12,44 @@ interface State { flags: string[]; } -export class PullQuickCommand extends QuickCommandBase { - constructor() { +export interface CommandArgs { + readonly command: 'pull'; + state?: Partial; + + skipConfirmation?: boolean; +} + +export class PullQuickCommand extends QuickCommandBase { + constructor(args?: CommandArgs) { super('pull', 'Pull'); + + if (args === undefined || args.state === undefined) return; + + let counter = 0; + if (args.state.repos !== undefined && args.state.repos.length !== 0) { + counter++; + } + + if ( + args.skipConfirmation === undefined && + Container.config.gitCommands.skipConfirmations.includes(this.label) + ) { + args.skipConfirmation = true; + } + + this._initialState = { + counter: counter, + skipConfirmation: counter > 0 && args.skipConfirmation, + ...args.state + }; } execute(state: State) { return Container.git.pullAll(state.repos, { rebase: state.flags.includes('--rebase') }); } - async *steps(): AsyncIterableIterator { - const state: Partial & { counter: number } = { counter: 0 }; + protected async *steps(): AsyncIterableIterator { + const state: StepState = this._initialState === undefined ? { counter: 0 } : this._initialState; let oneRepo = false; while (true) { @@ -36,17 +63,21 @@ export class PullQuickCommand extends QuickCommandBase { state.repos = [repos[0]]; } else { - const step = this.createStep({ + const step = this.createPickStep({ multiselect: true, title: this.title, placeholder: 'Choose repositories', items: await Promise.all( - repos.map(r => - RepositoryQuickPickItem.create(r, undefined, { - branch: true, - fetched: true, - status: true - }) + repos.map(repo => + RepositoryQuickPickItem.create( + repo, + state.repos ? state.repos.some(r => r.id === repo.id) : undefined, + { + branch: true, + fetched: true, + status: true + } + ) ) ) }); diff --git a/src/commands/quick/push.ts b/src/commands/quick/push.ts index 3edec30..5a23b41 100644 --- a/src/commands/quick/push.ts +++ b/src/commands/quick/push.ts @@ -1,26 +1,54 @@ 'use strict'; import { Container } from '../../container'; import { Repository } from '../../git/gitService'; -import { CommandAbortError, QuickCommandBase, QuickPickStep } from './quickCommand'; +import { CommandAbortError, QuickCommandBase, QuickInputStep, QuickPickStep, StepState } from './quickCommand'; import { RepositoryQuickPickItem } from '../../quickpicks'; import { Strings } from '../../system'; import { GlyphChars } from '../../constants'; interface State { repos: Repository[]; + flags: string[]; } -export class PushQuickCommand extends QuickCommandBase { - constructor() { +export interface CommandArgs { + readonly command: 'push'; + state?: Partial; + + skipConfirmation?: boolean; +} + +export class PushQuickCommand extends QuickCommandBase { + constructor(args?: CommandArgs) { super('push', 'Push'); + + if (args === undefined || args.state === undefined) return; + + let counter = 0; + if (args.state.repos !== undefined && args.state.repos.length !== 0) { + counter++; + } + + if ( + args.skipConfirmation === undefined && + Container.config.gitCommands.skipConfirmations.includes(this.label) + ) { + args.skipConfirmation = true; + } + + this._initialState = { + counter: counter, + skipConfirmation: counter > 0 && args.skipConfirmation, + ...args.state + }; } execute(state: State) { - return Container.git.pushAll(state.repos); + return Container.git.pushAll(state.repos, { force: state.flags.includes('--force') }); } - async *steps(): AsyncIterableIterator { - const state: Partial & { counter: number } = { counter: 0 }; + protected async *steps(): AsyncIterableIterator { + const state: StepState = this._initialState === undefined ? { counter: 0 } : this._initialState; let oneRepo = false; while (true) { @@ -34,17 +62,21 @@ export class PushQuickCommand extends QuickCommandBase { state.repos = [repos[0]]; } else { - const step = this.createStep({ + const step = this.createPickStep({ multiselect: true, title: this.title, placeholder: 'Choose repositories', items: await Promise.all( - repos.map(r => - RepositoryQuickPickItem.create(r, undefined, { - branch: true, - fetched: true, - status: true - }) + repos.map(repo => + RepositoryQuickPickItem.create( + repo, + state.repos ? state.repos.some(r => r.id === repo.id) : undefined, + { + branch: true, + fetched: true, + status: true + } + ) ) ) }); @@ -58,30 +90,50 @@ export class PushQuickCommand extends QuickCommandBase { } } - const step = this.createConfirmStep( - `Confirm ${this.title}${Strings.pad(GlyphChars.Dot, 2, 2)}${ - state.repos.length === 1 ? state.repos[0].formattedName : `${state.repos.length} repositories` - }`, - [ - { - label: this.title, - description: '', - detail: `Will push ${ - state.repos.length === 1 - ? state.repos[0].formattedName - : `${state.repos.length} repositories` - }` + if (state.skipConfirmation) { + state.flags = []; + } + else { + const step = this.createConfirmStep( + `Confirm ${this.title}${Strings.pad(GlyphChars.Dot, 2, 2)}${ + state.repos.length === 1 + ? state.repos[0].formattedName + : `${state.repos.length} repositories` + }`, + [ + { + label: this.title, + description: '', + detail: `Will push ${ + state.repos.length === 1 + ? state.repos[0].formattedName + : `${state.repos.length} repositories` + }`, + item: [] + }, + { + label: `Force ${this.title}`, + description: '', + detail: `Will force push ${ + state.repos.length === 1 + ? state.repos[0].formattedName + : `${state.repos.length} repositories` + }`, + item: ['--force'] + } + ] + ); + const selection = yield step; + + if (!this.canMoveNext(step, state, selection)) { + if (oneRepo) { + break; } - ] - ); - const selection = yield step; - if (!this.canMoveNext(step, state, selection)) { - if (oneRepo) { - break; + continue; } - continue; + state.flags = selection[0].item; } this.execute(state as State); diff --git a/src/commands/quick/quickCommand.ts b/src/commands/quick/quickCommand.ts index 9a4a352..08e5be9 100644 --- a/src/commands/quick/quickCommand.ts +++ b/src/commands/quick/quickCommand.ts @@ -1,5 +1,22 @@ 'use strict'; -import { QuickInputButton, QuickPick, QuickPickItem } from 'vscode'; +import { InputBox, QuickInputButton, QuickPick, QuickPickItem } from 'vscode'; +import { Promises } from '../../system/promise'; + +export * from './quickCommands.helpers'; + +export interface QuickInputStep { + buttons?: QuickInputButton[]; + placeholder?: string; + title?: string; + value?: string; + + onDidClickButton?(input: InputBox, button: QuickInputButton): void; + validate?(value: string | undefined): [boolean, string | undefined] | Promise<[boolean, string | undefined]>; +} + +export function isQuickInputStep(item: QuickPickStep | QuickInputStep): item is QuickInputStep { + return (item as QuickPickStep).items === undefined; +} export interface QuickPickStep { buttons?: QuickInputButton[]; @@ -8,9 +25,15 @@ export interface QuickPickStep { multiselect?: boolean; placeholder?: string; title?: string; + value?: string; + onDidAccept?(quickpick: QuickPick): Promise; onDidClickButton?(quickpick: QuickPick, button: QuickInputButton): void; - validate?(selection: T[]): boolean; + validate?(selection: T[]): boolean | Promise; +} + +export function isQuickPickStep(item: QuickPickStep | QuickInputStep): item is QuickPickStep { + return (item as QuickPickStep).items !== undefined; } export class CommandAbortError extends Error { @@ -19,7 +42,9 @@ export class CommandAbortError extends Error { } } -export abstract class QuickCommandBase implements QuickPickItem { +export type StepState = Partial & { counter: number; skipConfirmation?: boolean }; + +export abstract class QuickCommandBase implements QuickPickItem { static is(item: QuickPickItem): item is QuickCommandBase { return item instanceof QuickCommandBase; } @@ -27,8 +52,8 @@ export abstract class QuickCommandBase implements QuickPickItem { readonly description?: string; readonly detail?: string; - private _current: QuickPickStep | undefined; - private _stepsIterator: AsyncIterableIterator | undefined; + private _current: QuickPickStep | QuickInputStep | undefined; + private _stepsIterator: AsyncIterableIterator | undefined; constructor( public readonly label: string, @@ -42,29 +67,40 @@ export abstract class QuickCommandBase implements QuickPickItem { this.detail = options.detail; } - abstract steps(): AsyncIterableIterator; + private _picked: boolean = false; + get picked() { + return this._picked; + } + set picked(value: boolean) { + this._picked = value; + } + + protected _initialState?: StepState; - async previous(): Promise { + protected abstract steps(): AsyncIterableIterator; + + async previous(): Promise { // Simulate going back, by having no selection return (await this.next([])).value; } - async next(selection?: QuickPickItem[]): Promise> { + async next(value?: QuickPickItem[] | string): Promise> { if (this._stepsIterator === undefined) { this._stepsIterator = this.steps(); } - const result = await this._stepsIterator.next(selection); + const result = await this._stepsIterator.next(value); this._current = result.value; if (result.done) { + this._initialState = undefined; this._stepsIterator = undefined; } return result; } - get value(): QuickPickStep | undefined { + get value(): QuickPickStep | QuickInputStep | undefined { return this._current; } @@ -73,7 +109,7 @@ export abstract class QuickCommandBase implements QuickPickItem { confirmations: T[], cancellable: boolean = true ): QuickPickStep { - return this.createStep({ + return this.createPickStep({ placeholder: `Confirm ${this.title}`, title: title, items: cancellable ? [...confirmations, { label: 'Cancel' }] : confirmations, @@ -86,7 +122,11 @@ export abstract class QuickCommandBase implements QuickPickItem { }); } - protected createStep(step: QuickPickStep): QuickPickStep { + protected createInputStep(step: QuickInputStep): QuickInputStep { + return step; + } + + protected createPickStep(step: QuickPickStep): QuickPickStep { return step; } @@ -94,8 +134,18 @@ export abstract class QuickCommandBase implements QuickPickItem { step: QuickPickStep, state: { counter: number }, selection: T[] | undefined - ): selection is T[] { - if (selection === undefined || selection.length === 0) { + ): selection is T[]; + protected canMoveNext( + step: QuickInputStep, + state: { counter: number }, + value: string | undefined + ): boolean | Promise; + protected canMoveNext( + step: QuickPickStep | QuickInputStep, + state: { counter: number }, + value: T[] | string | undefined + ) { + if (value === undefined || value.length === 0) { state.counter--; if (state.counter < 0) { state.counter = 0; @@ -103,11 +153,20 @@ export abstract class QuickCommandBase implements QuickPickItem { return false; } - if (step.validate === undefined || step.validate(selection)) { + if (step.validate === undefined || (isQuickPickStep(step) && step.validate!(value as T[]))) { state.counter++; return true; } + if (isQuickInputStep(step)) { + const result = step.validate!(value as string); + if (!Promises.isPromise(result)) { + return result[0]; + } + + return result.then(([valid]) => valid); + } + return false; } } diff --git a/src/commands/quick/quickCommands.helpers.ts b/src/commands/quick/quickCommands.helpers.ts new file mode 100644 index 0000000..dce54c0 --- /dev/null +++ b/src/commands/quick/quickCommands.helpers.ts @@ -0,0 +1,85 @@ +'use strict'; +import { intersectionWith } from 'lodash-es'; +import { GitBranch, GitTag, Repository } from '../../git/git'; +import { BranchQuickPickItem, TagQuickPickItem } from '../../quickpicks'; + +export async function getBranchesAndOrTags( + repos: Repository | Repository[], + includeTags: boolean, + { + filterBranches, + filterTags, + picked + }: { filterBranches?: (b: GitBranch) => boolean; filterTags?: (t: GitTag) => boolean; picked?: string } = {} +) { + let branches: GitBranch[]; + let tags: GitTag[] | undefined; + + let singleRepo = false; + if (repos instanceof Repository || repos.length === 1) { + singleRepo = true; + const repo = repos instanceof Repository ? repos : repos[0]; + + [branches, tags] = await Promise.all([ + repo.getBranches({ filter: filterBranches, sort: true }), + includeTags ? repo.getTags({ filter: filterTags, includeRefs: true, sort: true }) : undefined + ]); + } + else { + const [branchesByRepo, tagsByRepo] = await Promise.all([ + Promise.all(repos.map(r => r.getBranches({ filter: filterBranches, sort: true }))), + includeTags + ? Promise.all(repos.map(r => r.getTags({ filter: filterTags, includeRefs: true, sort: true }))) + : undefined + ]); + + branches = GitBranch.sort( + intersectionWith(...branchesByRepo, ((b1: GitBranch, b2: GitBranch) => b1.name === b2.name) as any) + ); + + if (includeTags) { + tags = GitTag.sort( + intersectionWith(...tagsByRepo!, ((t1: GitTag, t2: GitTag) => t1.name === t2.name) as any) + ); + } + } + + if (!includeTags) { + return Promise.all( + branches.map(b => + BranchQuickPickItem.create(b, undefined, { + current: singleRepo ? 'checkmark' : false, + ref: singleRepo, + status: singleRepo, + type: 'remote' + }) + ) + ); + } + + return Promise.all([ + ...branches! + .filter(b => !b.remote) + .map(b => + BranchQuickPickItem.create(b, picked != null && b.ref === picked, { + current: singleRepo ? 'checkmark' : false, + ref: singleRepo, + status: singleRepo + }) + ), + ...tags!.map(t => + TagQuickPickItem.create(t, picked != null && t.ref === picked, { + ref: singleRepo, + type: true + }) + ), + ...branches! + .filter(b => b.remote) + .map(b => + BranchQuickPickItem.create(b, picked != null && b.ref === picked, { + current: singleRepo ? 'checkmark' : false, + type: 'remote' + }) + ) + ]); +} diff --git a/src/commands/quick/rebase.ts b/src/commands/quick/rebase.ts index c1e427d..e411eba 100644 --- a/src/commands/quick/rebase.ts +++ b/src/commands/quick/rebase.ts @@ -3,10 +3,16 @@ import { QuickPickItem } from 'vscode'; import { Container } from '../../container'; import { GitBranch, Repository } from '../../git/gitService'; import { GlyphChars } from '../../constants'; -import { CommandAbortError, QuickPickStep } from './quickCommand'; +import { + CommandAbortError, + getBranchesAndOrTags, + QuickCommandBase, + QuickInputStep, + QuickPickStep, + StepState +} from './quickCommand'; import { BranchQuickPickItem, RepositoryQuickPickItem } from '../../quickpicks'; import { Strings } from '../../system'; -import { GitCommandBase } from './gitCommand'; import { runGitCommandInTerminal } from '../../terminal'; interface State { @@ -16,7 +22,7 @@ interface State { flags: string[]; } -export class RebaseQuickCommand extends GitCommandBase { +export class RebaseQuickCommand extends QuickCommandBase { constructor() { super('rebase', 'Rebase', { description: 'via Terminal' }); } @@ -25,8 +31,8 @@ export class RebaseQuickCommand extends GitCommandBase { runGitCommandInTerminal('rebase', [...state.flags, state.source.ref].join(' '), state.repo.path, true); } - async *steps(): AsyncIterableIterator { - const state: Partial & { counter: number } = { counter: 0 }; + protected async *steps(): AsyncIterableIterator { + const state: StepState = this._initialState === undefined ? { counter: 0 } : this._initialState; let oneRepo = false; while (true) { @@ -42,7 +48,7 @@ export class RebaseQuickCommand extends GitCommandBase { else { const active = state.repo ? state.repo : await Container.git.getActiveRepository(); - const step = this.createStep({ + const step = this.createPickStep({ title: this.title, placeholder: 'Choose a repository', items: await Promise.all( @@ -71,12 +77,12 @@ export class RebaseQuickCommand extends GitCommandBase { if (state.source === undefined || state.counter < 2) { const destId = state.destination.id; - const step = this.createStep({ + const step = this.createPickStep({ title: `${this.title} ${state.destination.name}${Strings.pad(GlyphChars.Dot, 2, 2)}${ state.repo.name }`, placeholder: `Choose a branch or tag to rebase ${state.destination.name} with`, - items: await this.getBranchesAndOrTags(state.repo, true, { + items: await getBranchesAndOrTags(state.repo, true, { filterBranches: b => b.id !== destId, picked: state.source && state.source.ref }) diff --git a/src/commands/repositories.ts b/src/commands/repositories.ts index eb5b9d8..2b9e287 100644 --- a/src/commands/repositories.ts +++ b/src/commands/repositories.ts @@ -1,6 +1,8 @@ 'use strict'; +import { commands } from 'vscode'; import { Container } from '../container'; import { command, Command, Commands } from './common'; +import { GitCommandsCommandArgs } from '../commands'; @command() export class FetchRepositoriesCommand extends Command { @@ -8,8 +10,11 @@ export class FetchRepositoriesCommand extends Command { super(Commands.FetchRepositories); } - execute() { - return Container.git.fetchAll(); + async execute() { + const repositories = await Container.git.getOrderedRepositories(); + + const args: GitCommandsCommandArgs = { command: 'fetch', state: { repos: repositories } }; + return commands.executeCommand(Commands.GitCommands, args); } } @@ -19,8 +24,11 @@ export class PullRepositoriesCommand extends Command { super(Commands.PullRepositories); } - execute() { - return Container.git.pullAll(); + async execute() { + const repositories = await Container.git.getOrderedRepositories(); + + const args: GitCommandsCommandArgs = { command: 'pull', state: { repos: repositories } }; + return commands.executeCommand(Commands.GitCommands, args); } } @@ -30,7 +38,10 @@ export class PushRepositoriesCommand extends Command { super(Commands.PushRepositories); } - execute() { - return Container.git.pushAll(); + async execute() { + const repositories = await Container.git.getOrderedRepositories(); + + const args: GitCommandsCommandArgs = { command: 'push', state: { repos: repositories } }; + return commands.executeCommand(Commands.GitCommands, args); } } diff --git a/src/config.ts b/src/config.ts index b45c403..170434e 100644 --- a/src/config.ts +++ b/src/config.ts @@ -32,6 +32,9 @@ export interface Config { defaultDateSource: DateSource; defaultDateStyle: DateStyle; defaultGravatarsStyle: GravatarDefaultStyle; + gitCommands: { + skipConfirmations: string[]; + }; heatmap: { ageThreshold: number; coldColor: string; diff --git a/src/git/git.ts b/src/git/git.ts index c43af87..9032cec 100644 --- a/src/git/git.ts +++ b/src/git/git.ts @@ -449,6 +449,28 @@ export class Git { return git({ cwd: repoPath, errors: GitErrorHandling.Ignore, local: true }, 'check-mailmap', author); } + static async check_ref_format(ref: string, repoPath?: string, options: { branch?: boolean } = { branch: true }) { + const params = ['check-ref-format']; + if (options.branch) { + params.push('--branch'); + } + else { + params.push('--normalize'); + } + + try { + const data = await git( + { cwd: repoPath || emptyStr, errors: GitErrorHandling.Throw, local: true }, + ...params, + ref + ); + return data.trim(); + } + catch { + return false; + } + } + static checkout( repoPath: string, ref: string, diff --git a/src/git/gitService.ts b/src/git/gitService.ts index eb861ec..1f97d72 100644 --- a/src/git/gitService.ts +++ b/src/git/gitService.ts @@ -595,14 +595,14 @@ export class GitService implements Disposable { 0: (repos?: Repository[]) => (repos === undefined ? false : repos.map(r => r.name).join(', ')) } }) - async pushAll(repositories?: Repository[]) { + async pushAll(repositories?: Repository[], options: { force?: boolean } = {}) { if (repositories === undefined) { repositories = await this.getOrderedRepositories(); } if (repositories.length === 0) return; if (repositories.length === 1) { - repositories[0].push(); + repositories[0].push(options); return; } @@ -612,7 +612,7 @@ export class GitService implements Disposable { location: ProgressLocation.Notification, title: `Pushing ${repositories.length} repositories` }, - () => Promise.all(repositories!.map(r => r.push({ progress: false }))) + () => Promise.all(repositories!.map(r => r.push({ progress: false, ...options }))) ); } @@ -2683,6 +2683,11 @@ export class GitService implements Disposable { } @log() + validateBranchName(ref: string, repoPath?: string) { + return Git.check_ref_format(ref, repoPath); + } + + @log() validateReference(repoPath: string, ref: string) { return Git.cat_file__validate(repoPath, ref); } diff --git a/src/git/models/repository.ts b/src/git/models/repository.ts index e33b13d..51eb291 100644 --- a/src/git/models/repository.ts +++ b/src/git/models/repository.ts @@ -237,9 +237,9 @@ export class Repository implements Disposable { @gate() @log() - async checkout(ref: string, options: { progress?: boolean } = {}) { - const { progress } = { progress: true, ...options }; - if (!progress) return this.checkoutCore(ref); + async checkout(ref: string, options: { createBranch?: string | undefined; progress?: boolean } = {}) { + const { progress, ...opts } = { progress: true, ...options }; + if (!progress) return this.checkoutCore(ref, opts); return void (await window.withProgress( { @@ -247,13 +247,13 @@ export class Repository implements Disposable { title: `Checking out ${this.formattedName} to ${ref}...`, cancellable: false }, - () => this.checkoutCore(ref) + () => this.checkoutCore(ref, opts) )); } - private async checkoutCore(ref: string, options: { remote?: string } = {}) { + private async checkoutCore(ref: string, options: { createBranch?: string } = {}) { try { - void (await Container.git.checkout(this.path, ref)); + void (await Container.git.checkout(this.path, ref, options)); this.fireChange(RepositoryChange.Repository); } diff --git a/src/views/viewCommands.ts b/src/views/viewCommands.ts index d971137..803717b 100644 --- a/src/views/viewCommands.ts +++ b/src/views/viewCommands.ts @@ -7,6 +7,7 @@ import { DiffWithCommandArgsRevision, DiffWithPreviousCommandArgs, DiffWithWorkingCommandArgs, + GitCommandsCommandArgs, openEditor, OpenFileInRemoteCommandArgs, OpenFileRevisionCommandArgs, @@ -45,7 +46,6 @@ import { } from './nodes'; import { Strings } from '../system/string'; import { runGitCommandInTerminal } from '../terminal'; -import { ReferencesQuickPick } from '../quickpicks'; interface CompareSelectedInfo { ref: string; @@ -199,7 +199,9 @@ export class ViewCommands { private fetch(node: RemoteNode | RepositoryNode) { if (node instanceof RemoteNode) return node.fetch(); - if (node instanceof RepositoryNode) return node.fetch(); + if (node instanceof RepositoryNode) { + return commands.executeCommand(Commands.GitCommands, { command: 'fetch', state: { repos: [node.repo] } }); + } return undefined; } @@ -210,7 +212,7 @@ export class ViewCommands { } if (!(node instanceof RepositoryNode)) return undefined; - return node.pull(); + return commands.executeCommand(Commands.GitCommands, { command: 'pull', state: { repos: [node.repo] } }); } private push(node: RepositoryNode | BranchTrackingStatusNode, force?: boolean) { @@ -219,7 +221,7 @@ export class ViewCommands { } if (!(node instanceof RepositoryNode)) return undefined; - return node.push({ force: force }); + return commands.executeCommand(Commands.GitCommands, { command: 'push', state: { repos: [node.repo] } }); } private async applyChanges(node: ViewRefFileNode) { @@ -245,45 +247,26 @@ export class ViewCommands { return Container.git.checkout(node.repoPath, node.ref, { fileName: node.fileName }); } - if (node instanceof BranchNode) { - let branch = node.branch; - if (branch.current) { - const pick = await new ReferencesQuickPick(node.repoPath).show('Choose a branch to check out to', { - checkmarks: false, - filterBranches: b => !b.current, - include: 'branches' - }); - if (pick === undefined) return undefined; - - branch = pick.item; - } - - if (branch.remote) { - const branches = await Container.git.getBranches(node.repoPath, { - filter: b => { - return b.tracking === branch.name; - } - }); + const repo = await Container.git.getRepository(node.repoPath); - if (branches.length !== 0) { - return Container.git.checkout(node.repoPath, branches[0].ref); - } - - const name = await window.showInputBox({ - prompt: 'Please provide a name for the local branch', - placeHolder: 'Local branch name', - value: branch.getName(), - ignoreFocusOut: true - }); - if (name === undefined || name.length === 0) return undefined; - - return Container.git.checkout(node.repoPath, branch.ref, { createBranch: name }); - } - - return Container.git.checkout(branch.repoPath, branch.ref); + let args: GitCommandsCommandArgs; + if (node instanceof BranchNode) { + args = { + command: 'checkout', + state: { repos: [repo!], branchOrTagOrRef: node.branch.current ? undefined : node.branch } + }; + } + else if (node instanceof TagNode) { + args = { command: 'checkout', state: { repos: [repo!], branchOrTagOrRef: node.tag } }; + } + else { + args = { + command: 'checkout', + state: { repos: [repo!], branchOrTagOrRef: { name: node.ref, ref: node.ref } } + }; } - return Container.git.checkout(node.repoPath, node.ref); + return commands.executeCommand(Commands.GitCommands, args); } private async addRemote(node: RemoteNode) { diff --git a/src/vsls/host.ts b/src/vsls/host.ts index d69628f..a3d7f0d 100644 --- a/src/vsls/host.ts +++ b/src/vsls/host.ts @@ -23,6 +23,7 @@ const gitWhitelist = new Map boolean>([ ['branch', args => args[1] === '--contains'], ['cat-file', defaultWhitelistFn], ['check-mailmap', defaultWhitelistFn], + ['check-ref-format', defaultWhitelistFn], ['config', args => args[1] === '--get' || args[1] === '--get-regex'], ['diff', defaultWhitelistFn], ['difftool', defaultWhitelistFn],