From 5c3f6b5b050a2150f31dd193237b57a6979ac316 Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Wed, 14 Aug 2019 00:45:28 -0400 Subject: [PATCH] Reorgs git commands Adds explicit directive for going back Adds command error logging Removes unneeded abort error --- src/commands/git/checkout.ts | 271 +++++++++++++++++++++++++++ src/commands/git/cherry-pick.ts | 200 ++++++++++++++++++++ src/commands/git/fetch.ts | 164 ++++++++++++++++ src/commands/git/merge.ts | 165 +++++++++++++++++ src/commands/git/pull.ts | 144 +++++++++++++++ src/commands/git/push.ts | 150 +++++++++++++++ src/commands/git/rebase.ts | 157 ++++++++++++++++ src/commands/gitCommands.ts | 58 +++--- src/commands/quick/checkout.ts | 277 ---------------------------- src/commands/quick/cherry-pick.ts | 201 -------------------- src/commands/quick/fetch.ts | 163 ---------------- src/commands/quick/merge.ts | 171 ----------------- src/commands/quick/pull.ts | 143 -------------- src/commands/quick/push.ts | 149 --------------- src/commands/quick/quickCommand.ts | 175 ------------------ src/commands/quick/quickCommands.helpers.ts | 85 --------- src/commands/quick/rebase.ts | 163 ---------------- src/commands/quickCommand.helpers.ts | 85 +++++++++ src/commands/quickCommand.ts | 177 ++++++++++++++++++ src/quickpicks/gitQuickPicks.ts | 34 +++- src/views/viewCommands.ts | 9 +- 21 files changed, 1582 insertions(+), 1559 deletions(-) create mode 100644 src/commands/git/checkout.ts create mode 100644 src/commands/git/cherry-pick.ts create mode 100644 src/commands/git/fetch.ts create mode 100644 src/commands/git/merge.ts create mode 100644 src/commands/git/pull.ts create mode 100644 src/commands/git/push.ts create mode 100644 src/commands/git/rebase.ts delete mode 100644 src/commands/quick/checkout.ts delete mode 100644 src/commands/quick/cherry-pick.ts delete mode 100644 src/commands/quick/fetch.ts delete mode 100644 src/commands/quick/merge.ts delete mode 100644 src/commands/quick/pull.ts delete mode 100644 src/commands/quick/push.ts delete mode 100644 src/commands/quick/quickCommand.ts delete mode 100644 src/commands/quick/quickCommands.helpers.ts delete mode 100644 src/commands/quick/rebase.ts create mode 100644 src/commands/quickCommand.helpers.ts create mode 100644 src/commands/quickCommand.ts diff --git a/src/commands/git/checkout.ts b/src/commands/git/checkout.ts new file mode 100644 index 0000000..6674228 --- /dev/null +++ b/src/commands/git/checkout.ts @@ -0,0 +1,271 @@ +'use strict'; +/* eslint-disable no-loop-func */ +import { ProgressLocation, QuickInputButtons, window } from 'vscode'; +import { Container } from '../../container'; +import { GitBranch, GitReference, GitTag, Repository } from '../../git/gitService'; +import { GlyphChars } from '../../constants'; +import { getBranchesAndOrTags, QuickCommandBase, QuickInputStep, QuickPickStep, StepState } from '../quickCommand'; +import { ReferencesQuickPickItem, RefQuickPickItem, RepositoryQuickPickItem } from '../../quickpicks'; +import { Strings } from '../../system'; +import { Logger } from '../../logger'; + +interface State { + repos: Repository[]; + branchOrTagOrRef: GitBranch | GitTag | GitReference; + createBranch?: string; +} + +export interface CommandArgs { + readonly command: 'checkout'; + state?: Partial; + + skipConfirmation?: boolean; +} + +export class CheckoutGitCommand 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) { + return void (await window.withProgress( + { + location: ProgressLocation.Notification, + title: `Checking out ${ + state.repos.length === 1 ? state.repos[0].formattedName : `${state.repos.length} repositories` + } to ${state.branchOrTagOrRef.ref}` + }, + () => + Promise.all( + state.repos.map(r => + r.checkout(state.branchOrTagOrRef.ref, { createBranch: state.createBranch, progress: false }) + ) + ) + )); + } + + protected async *steps(): AsyncIterableIterator { + const state: StepState = this._initialState === undefined ? { counter: 0 } : this._initialState; + let oneRepo = false; + let showTags = false; + + while (true) { + try { + if (state.repos === undefined || state.counter < 1) { + const repos = [...(await Container.git.getOrderedRepositories())]; + + if (repos.length === 1) { + oneRepo = true; + state.counter++; + state.repos = [repos[0]]; + } + else { + const step = this.createPickStep({ + multiselect: true, + title: this.title, + placeholder: 'Choose repositories', + items: await Promise.all( + repos.map(repo => + RepositoryQuickPickItem.create( + repo, + state.repos ? state.repos.some(r => r.id === repo.id) : undefined, + { branch: true, fetched: true, status: true } + ) + ) + ) + }); + const selection = yield step; + + if (!this.canMoveNext(step, state, selection)) { + break; + } + + state.repos = selection.map(i => i.item); + } + } + + if (state.branchOrTagOrRef === undefined || state.counter < 2) { + const includeTags = showTags || state.repos.length === 1; + + 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${GlyphChars.Space.repeat(3)}(select or enter a reference)`, + matchOnDescription: true, + items: items, + selectedItems: state.branchOrTagOrRef + ? items.filter(ref => ref.label === state.branchOrTagOrRef!.ref) + : undefined, + buttons: includeTags + ? [QuickInputButtons.Back] + : [ + QuickInputButtons.Back, + { + iconPath: { + dark: Container.context.asAbsolutePath('images/dark/icon-tag.svg') as any, + light: Container.context.asAbsolutePath('images/light/icon-tag.svg') as any + }, + tooltip: 'Show Tags' + } + ], + onDidClickButton: async (quickpick, button) => { + quickpick.busy = true; + quickpick.enabled = false; + + if (!showTags) { + showTags = true; + } + + 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 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); + // }, + onValidateValue: async (quickpick, value) => { + if (state.repos!.length !== 1) return false; + if (!(await Container.git.validateReference(state.repos![0].path, value))) return false; + + quickpick.items = [RefQuickPickItem.create(value, true, { ref: true })]; + return true; + } + }); + const selection = yield step; + + if (!this.canMoveNext(step, state, selection)) { + if (oneRepo) { + break; + } + + continue; + } + + state.branchOrTagOrRef = selection[0].item; + } + + if (GitBranch.is(state.branchOrTagOrRef) && 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` + }`, + placeholder: 'Please provide a 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; + } + + 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); + break; + } + catch (ex) { + Logger.error(ex, this.title); + + throw ex; + } + } + } +} diff --git a/src/commands/git/cherry-pick.ts b/src/commands/git/cherry-pick.ts new file mode 100644 index 0000000..55ceb1f --- /dev/null +++ b/src/commands/git/cherry-pick.ts @@ -0,0 +1,200 @@ +'use strict'; +/* eslint-disable no-loop-func */ +import { Container } from '../../container'; +import { GitBranch, GitLogCommit, GitReference, Repository } from '../../git/gitService'; +import { GlyphChars } from '../../constants'; +import { Iterables, Strings } from '../../system'; +import { getBranchesAndOrTags, QuickCommandBase, QuickInputStep, QuickPickStep, StepState } from '../quickCommand'; +import { + BackOrCancelQuickPickItem, + BranchQuickPickItem, + CommitQuickPickItem, + RefQuickPickItem, + RepositoryQuickPickItem +} from '../../quickpicks'; +import { runGitCommandInTerminal } from '../../terminal'; +import { Logger } from '../../logger'; + +interface State { + repo: Repository; + destination: GitBranch; + source: GitBranch | GitReference; + commits?: GitLogCommit[]; +} + +export class CherryPickGitCommand extends QuickCommandBase { + constructor() { + super('cherry-pick', 'Cherry Pick', { description: 'via Terminal' }); + } + + execute(state: State) { + if (state.commits !== undefined) { + // Ensure the commits are ordered with the oldest first + state.commits.sort((a, b) => a.date.getTime() - b.date.getTime()); + runGitCommandInTerminal('cherry-pick', state.commits.map(c => c.sha).join(' '), state.repo.path, true); + } + + runGitCommandInTerminal('cherry-pick', state.source.ref, state.repo.path, true); + } + + protected async *steps(): AsyncIterableIterator { + const state: StepState = this._initialState === undefined ? { counter: 0 } : this._initialState; + let oneRepo = false; + + while (true) { + try { + if (state.repo === undefined || state.counter < 1) { + const repos = [...(await Container.git.getOrderedRepositories())]; + + if (repos.length === 1) { + oneRepo = true; + state.counter++; + state.repo = repos[0]; + } + else { + const active = state.repo ? state.repo : await Container.git.getActiveRepository(); + + const step = this.createPickStep({ + title: this.title, + placeholder: 'Choose a repository', + items: await Promise.all( + repos.map(r => + RepositoryQuickPickItem.create(r, r.id === (active && active.id), { + branch: true, + fetched: true, + status: true + }) + ) + ) + }); + const selection = yield step; + + if (!this.canMoveNext(step, state, selection)) { + break; + } + + state.repo = selection[0].item; + } + } + + state.destination = await state.repo.getBranch(); + if (state.destination === undefined) break; + + if (state.source === undefined || state.counter < 2) { + const destId = state.destination.id; + + const step = this.createPickStep({ + title: `${this.title} into ${state.destination.name}${Strings.pad(GlyphChars.Dot, 2, 2)}${ + state.repo.formattedName + }`, + placeholder: `Choose a branch or tag to cherry-pick from${GlyphChars.Space.repeat( + 3 + )}(select or enter a reference)`, + matchOnDescription: true, + items: await getBranchesAndOrTags(state.repo, true, { + filterBranches: b => b.id !== destId + }), + onValidateValue: async (quickpick, value) => { + if (!(await Container.git.validateReference(state.repo!.path, value))) return false; + + quickpick.items = [RefQuickPickItem.create(value, true, { ref: true })]; + return true; + } + }); + const selection = yield step; + + if (!this.canMoveNext(step, state, selection)) { + if (oneRepo) { + break; + } + + continue; + } + + if (GitBranch.is(state.source)) { + state.source = selection[0].item; + } + else { + state.source = selection[0].item; + state.counter++; + } + } + + if (GitBranch.is(state.source) && (state.commits === undefined || state.counter < 3)) { + const log = await Container.git.getLog(state.repo.path, { + ref: `${state.destination.ref}..${state.source.ref}`, + merges: false + }); + + const step = this.createPickStep({ + title: `${this.title} onto ${state.destination.name}${Strings.pad(GlyphChars.Dot, 2, 2)}${ + state.repo.formattedName + }`, + multiselect: log !== undefined, + placeholder: + log === undefined + ? `${state.source.name} has no pickable commits` + : `Choose commits to cherry-pick onto ${state.destination.name}`, + items: + log === undefined + ? [BackOrCancelQuickPickItem.create(false, true), BackOrCancelQuickPickItem.create()] + : [ + ...Iterables.map(log.commits.values(), commit => + CommitQuickPickItem.create( + commit, + state.commits ? state.commits.some(c => c.sha === commit.sha) : undefined, + { compact: true } + ) + ) + ] + }); + const selection = yield step; + + if (!this.canMoveNext(step, state, selection)) { + continue; + } + + state.commits = selection.map(i => i.item); + } + + const step = this.createConfirmStep( + `Confirm ${this.title}${Strings.pad(GlyphChars.Dot, 2, 2)}${state.repo.formattedName}`, + [ + state.commits !== undefined + ? { + label: this.title, + description: `${ + state.commits.length === 1 + ? state.commits[0].shortSha + : `${state.commits.length} commits` + } onto ${state.destination.name}`, + detail: `Will apply ${ + state.commits.length === 1 + ? `commit ${state.commits[0].shortSha}` + : `${state.commits.length} commits` + } onto ${state.destination.name}` + } + : { + label: this.title, + description: `${state.source.name} onto ${state.destination.name}`, + detail: `Will apply commit ${state.source.name} onto ${state.destination.name}` + } + ] + ); + const selection = yield step; + + if (!this.canMoveNext(step, state, selection)) { + continue; + } + + this.execute(state as State); + break; + } + catch (ex) { + Logger.error(ex, this.title); + + throw ex; + } + } + } +} diff --git a/src/commands/git/fetch.ts b/src/commands/git/fetch.ts new file mode 100644 index 0000000..4261102 --- /dev/null +++ b/src/commands/git/fetch.ts @@ -0,0 +1,164 @@ +'use strict'; +import { QuickPickItem } from 'vscode'; +import { Container } from '../../container'; +import { Repository } from '../../git/gitService'; +import { QuickCommandBase, QuickInputStep, QuickPickStep, StepState } from '../quickCommand'; +import { RepositoryQuickPickItem } from '../../quickpicks'; +import { Strings } from '../../system'; +import { GlyphChars } from '../../constants'; +import { Logger } from '../../logger'; + +interface State { + repos: Repository[]; + flags: string[]; +} + +export interface CommandArgs { + readonly command: 'fetch'; + state?: Partial; + + skipConfirmation?: boolean; +} + +export class FetchGitCommand 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) { + return Container.git.fetchAll(state.repos, { + all: state.flags.includes('--all'), + prune: state.flags.includes('--prune') + }); + } + + protected async *steps(): AsyncIterableIterator { + const state: StepState = this._initialState === undefined ? { counter: 0 } : this._initialState; + let oneRepo = false; + + while (true) { + try { + if (state.repos === undefined || state.counter < 1) { + const repos = [...(await Container.git.getOrderedRepositories())]; + + if (repos.length === 1) { + oneRepo = true; + state.counter++; + state.repos = [repos[0]]; + } + else { + const step = this.createPickStep({ + multiselect: true, + title: this.title, + placeholder: 'Choose repositories', + items: await Promise.all( + repos.map(repo => + RepositoryQuickPickItem.create( + repo, + state.repos ? state.repos.some(r => r.id === repo.id) : undefined, + { + branch: true, + fetched: true, + status: true + } + ) + ) + ) + }); + const selection = yield step; + + if (!this.canMoveNext(step, state, selection)) { + break; + } + + state.repos = selection.map(i => i.item); + } + } + + 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; + } + + continue; + } + + state.flags = selection[0].item; + } + + this.execute(state as State); + break; + } + catch (ex) { + Logger.error(ex, this.title); + + throw ex; + } + } + } +} diff --git a/src/commands/git/merge.ts b/src/commands/git/merge.ts new file mode 100644 index 0000000..f4a1c50 --- /dev/null +++ b/src/commands/git/merge.ts @@ -0,0 +1,165 @@ +'use strict'; +import { QuickPickItem } from 'vscode'; +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 { Strings } from '../../system'; +import { runGitCommandInTerminal } from '../../terminal'; +import { Logger } from '../../logger'; + +interface State { + repo: Repository; + destination: GitBranch; + source: GitBranch | GitTag; + flags: string[]; +} + +export class MergeGitCommand extends QuickCommandBase { + constructor() { + super('merge', 'Merge', { description: 'via Terminal' }); + } + + execute(state: State) { + runGitCommandInTerminal('merge', [...state.flags, state.source.ref].join(' '), state.repo.path, true); + } + + protected async *steps(): AsyncIterableIterator { + const state: StepState = this._initialState === undefined ? { counter: 0 } : this._initialState; + let oneRepo = false; + + while (true) { + try { + if (state.repo === undefined || state.counter < 1) { + const repos = [...(await Container.git.getOrderedRepositories())]; + + if (repos.length === 1) { + oneRepo = true; + state.counter++; + state.repo = repos[0]; + } + else { + const active = state.repo ? state.repo : await Container.git.getActiveRepository(); + + const step = this.createPickStep({ + title: this.title, + placeholder: 'Choose a repository', + items: await Promise.all( + repos.map(r => + RepositoryQuickPickItem.create(r, r.id === (active && active.id), { + branch: true, + fetched: true, + status: true + }) + ) + ) + }); + const selection = yield step; + + if (!this.canMoveNext(step, state, selection)) { + break; + } + + state.repo = selection[0].item; + } + } + + state.destination = await state.repo.getBranch(); + if (state.destination === undefined) break; + + if (state.source === undefined || state.counter < 2) { + const destId = state.destination.id; + + const step = this.createPickStep({ + title: `${this.title} into ${state.destination.name}${Strings.pad(GlyphChars.Dot, 2, 2)}${ + state.repo.formattedName + }`, + placeholder: `Choose a branch or tag to merge into ${state.destination.name}`, + items: await getBranchesAndOrTags(state.repo, true, { + filterBranches: b => b.id !== destId, + picked: state.source && state.source.ref + }) + }); + const selection = yield step; + + if (!this.canMoveNext(step, state, selection)) { + if (oneRepo) { + break; + } + continue; + } + + state.source = selection[0].item; + } + + const count = + (await Container.git.getCommitCount(state.repo.path, [ + `${state.destination.name}..${state.source.name}` + ])) || 0; + if (count === 0) { + const step = this.createConfirmStep( + `Confirm ${this.title}${Strings.pad(GlyphChars.Dot, 2, 2)}${state.repo.formattedName}`, + [ + { + label: `Cancel ${this.title}`, + description: '', + detail: `${state.destination.name} is up to date with ${state.source.name}` + } + ], + false + ); + + yield step; + break; + } + + const step = this.createConfirmStep( + `Confirm ${this.title}${Strings.pad(GlyphChars.Dot, 2, 2)}${state.repo.formattedName}`, + [ + { + label: this.title, + description: `${state.source.name} into ${state.destination.name}`, + detail: `Will merge ${Strings.pluralize('commit', count)} from ${state.source.name} into ${ + state.destination.name + }`, + item: [] + }, + { + label: `Fast-forward ${this.title}`, + description: `--ff-only ${state.source.name} into ${state.destination.name}`, + detail: `Will fast-forward merge ${Strings.pluralize('commit', count)} from ${ + state.source.name + } into ${state.destination.name}`, + item: ['--ff-only'] + }, + { + label: `No Fast-forward ${this.title}`, + description: `--no-ff ${state.source.name} into ${state.destination.name}`, + detail: `Will create a merge commit when merging ${Strings.pluralize( + 'commit', + count + )} from ${state.source.name} into ${state.destination.name}`, + item: ['--no-ff'] + } + ] + ); + const selection = yield step; + + if (!this.canMoveNext(step, state, selection)) { + continue; + } + + state.flags = selection[0].item; + + this.execute(state as State); + break; + } + catch (ex) { + Logger.error(ex, this.title); + + throw ex; + } + } + } +} diff --git a/src/commands/git/pull.ts b/src/commands/git/pull.ts new file mode 100644 index 0000000..99ea3f3 --- /dev/null +++ b/src/commands/git/pull.ts @@ -0,0 +1,144 @@ +'use strict'; +import { QuickPickItem } from 'vscode'; +import { Container } from '../../container'; +import { Repository } from '../../git/gitService'; +import { QuickCommandBase, QuickInputStep, QuickPickStep, StepState } from '../quickCommand'; +import { RepositoryQuickPickItem } from '../../quickpicks'; +import { Strings } from '../../system'; +import { GlyphChars } from '../../constants'; +import { Logger } from '../../logger'; + +interface State { + repos: Repository[]; + flags: string[]; +} + +export interface CommandArgs { + readonly command: 'pull'; + state?: Partial; + + skipConfirmation?: boolean; +} + +export class PullGitCommand 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') }); + } + + protected async *steps(): AsyncIterableIterator { + const state: StepState = this._initialState === undefined ? { counter: 0 } : this._initialState; + let oneRepo = false; + + while (true) { + try { + if (state.repos === undefined || state.counter < 1) { + const repos = [...(await Container.git.getOrderedRepositories())]; + + if (repos.length === 1) { + oneRepo = true; + state.counter++; + state.repos = [repos[0]]; + } + else { + const step = this.createPickStep({ + multiselect: true, + title: this.title, + placeholder: 'Choose repositories', + items: await Promise.all( + repos.map(repo => + RepositoryQuickPickItem.create( + repo, + state.repos ? state.repos.some(r => r.id === repo.id) : undefined, + { + branch: true, + fetched: true, + status: true + } + ) + ) + ) + }); + const selection = yield step; + + if (!this.canMoveNext(step, state, selection)) { + break; + } + + state.repos = selection.map(i => i.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` + }`, + [ + { + label: this.title, + description: '', + detail: `Will pull ${ + state.repos.length === 1 + ? state.repos[0].formattedName + : `${state.repos.length} repositories` + }`, + item: [] + }, + { + label: `${this.title} with Rebase`, + description: '--rebase', + detail: `Will pull with rebase ${ + state.repos.length === 1 + ? state.repos[0].formattedName + : `${state.repos.length} repositories` + }`, + item: ['--rebase'] + } + ] + ); + const selection = yield step; + + if (!this.canMoveNext(step, state, selection)) { + if (oneRepo) { + break; + } + + continue; + } + + state.flags = selection[0].item; + + this.execute(state as State); + break; + } + catch (ex) { + Logger.error(ex, this.title); + + throw ex; + } + } + } +} diff --git a/src/commands/git/push.ts b/src/commands/git/push.ts new file mode 100644 index 0000000..f090ae9 --- /dev/null +++ b/src/commands/git/push.ts @@ -0,0 +1,150 @@ +'use strict'; +import { Container } from '../../container'; +import { Repository } from '../../git/gitService'; +import { QuickCommandBase, QuickInputStep, QuickPickStep, StepState } from '../quickCommand'; +import { RepositoryQuickPickItem } from '../../quickpicks'; +import { Strings } from '../../system'; +import { GlyphChars } from '../../constants'; +import { Logger } from '../../logger'; + +interface State { + repos: Repository[]; + flags: string[]; +} + +export interface CommandArgs { + readonly command: 'push'; + state?: Partial; + + skipConfirmation?: boolean; +} + +export class PushGitCommand 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, { force: state.flags.includes('--force') }); + } + + protected async *steps(): AsyncIterableIterator { + const state: StepState = this._initialState === undefined ? { counter: 0 } : this._initialState; + let oneRepo = false; + + while (true) { + try { + if (state.repos === undefined || state.counter < 1) { + const repos = [...(await Container.git.getOrderedRepositories())]; + + if (repos.length === 1) { + oneRepo = true; + state.counter++; + state.repos = [repos[0]]; + } + else { + const step = this.createPickStep({ + multiselect: true, + title: this.title, + placeholder: 'Choose repositories', + items: await Promise.all( + repos.map(repo => + RepositoryQuickPickItem.create( + repo, + state.repos ? state.repos.some(r => r.id === repo.id) : undefined, + { + branch: true, + fetched: true, + status: true + } + ) + ) + ) + }); + const selection = yield step; + + if (!this.canMoveNext(step, state, selection)) { + break; + } + + state.repos = selection.map(i => i.item); + } + } + + 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; + } + + continue; + } + + state.flags = selection[0].item; + } + + this.execute(state as State); + break; + } + catch (ex) { + Logger.error(ex, this.title); + + throw ex; + } + } + } +} diff --git a/src/commands/git/rebase.ts b/src/commands/git/rebase.ts new file mode 100644 index 0000000..4a86330 --- /dev/null +++ b/src/commands/git/rebase.ts @@ -0,0 +1,157 @@ +'use strict'; +import { QuickPickItem } from 'vscode'; +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 { Strings } from '../../system'; +import { runGitCommandInTerminal } from '../../terminal'; +import { Logger } from '../../logger'; + +interface State { + repo: Repository; + destination: GitBranch; + source: GitBranch | GitTag; + flags: string[]; +} + +export class RebaseGitCommand extends QuickCommandBase { + constructor() { + super('rebase', 'Rebase', { description: 'via Terminal' }); + } + + execute(state: State) { + runGitCommandInTerminal('rebase', [...state.flags, state.source.ref].join(' '), state.repo.path, true); + } + + protected async *steps(): AsyncIterableIterator { + const state: StepState = this._initialState === undefined ? { counter: 0 } : this._initialState; + let oneRepo = false; + + while (true) { + try { + if (state.repo === undefined || state.counter < 1) { + const repos = [...(await Container.git.getOrderedRepositories())]; + + if (repos.length === 1) { + oneRepo = true; + state.counter++; + state.repo = repos[0]; + } + else { + const active = state.repo ? state.repo : await Container.git.getActiveRepository(); + + const step = this.createPickStep({ + title: this.title, + placeholder: 'Choose a repository', + items: await Promise.all( + repos.map(r => + RepositoryQuickPickItem.create(r, r.id === (active && active.id), { + branch: true, + fetched: true, + status: true + }) + ) + ) + }); + const selection = yield step; + + if (!this.canMoveNext(step, state, selection)) { + break; + } + + state.repo = selection[0].item; + } + } + + state.destination = await state.repo.getBranch(); + if (state.destination === undefined) break; + + if (state.source === undefined || state.counter < 2) { + const destId = state.destination.id; + + const step = this.createPickStep({ + title: `${this.title} ${state.destination.name}${Strings.pad(GlyphChars.Dot, 2, 2)}${ + state.repo.formattedName + }`, + placeholder: `Choose a branch or tag to rebase ${state.destination.name} with`, + items: await getBranchesAndOrTags(state.repo, true, { + filterBranches: b => b.id !== destId, + picked: state.source && state.source.ref + }) + }); + const selection = yield step; + + if (!this.canMoveNext(step, state, selection)) { + if (oneRepo) { + break; + } + continue; + } + + state.source = selection[0].item; + } + + const count = + (await Container.git.getCommitCount(state.repo.path, [ + `${state.destination.name}..${state.source.name}` + ])) || 0; + if (count === 0) { + const step = this.createConfirmStep( + `Confirm ${this.title}${Strings.pad(GlyphChars.Dot, 2, 2)}${state.repo.formattedName}`, + [ + { + label: `Cancel ${this.title}`, + description: '', + detail: `${state.destination.name} is up to date with ${state.source.name}` + } + ], + false + ); + + yield step; + break; + } + + const step = this.createConfirmStep( + `Confirm ${this.title}${Strings.pad(GlyphChars.Dot, 2, 2)}${state.repo.formattedName}`, + [ + { + label: this.title, + description: `${state.destination.name} with ${state.source.name}`, + detail: `Will update ${state.destination.name} by applying ${Strings.pluralize( + 'commit', + count + )} on top of ${state.source.name}`, + item: [] + }, + { + label: `Interactive ${this.title}`, + description: `--interactive ${state.destination.name} with ${state.source.name}`, + detail: `Will interactively update ${ + state.destination.name + } by applying ${Strings.pluralize('commit', count)} on top of ${state.source.name}`, + item: ['--interactive'] + } + ] + ); + const selection = yield step; + + if (!this.canMoveNext(step, state, selection)) { + continue; + } + + state.flags = selection[0].item; + + this.execute(state as State); + break; + } + catch (ex) { + Logger.error(ex, this.title); + + throw ex; + } + } + } +} diff --git a/src/commands/gitCommands.ts b/src/commands/gitCommands.ts index ab23ff6..87a1d3c 100644 --- a/src/commands/gitCommands.ts +++ b/src/commands/gitCommands.ts @@ -2,20 +2,15 @@ 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 { CommandArgs as FetchCommandArgs, FetchQuickCommand } from './quick/fetch'; -import { MergeQuickCommand } from './quick/merge'; -import { CommandArgs as PullCommandArgs, PullQuickCommand } from './quick/pull'; -import { CommandArgs as PushCommandArgs, PushQuickCommand } from './quick/push'; -import { RebaseQuickCommand } from './quick/rebase'; +import { isQuickInputStep, isQuickPickStep, QuickCommandBase, QuickInputStep, QuickPickStep } from './quickCommand'; +import { BackOrCancelQuickPickItem } from '../quickpicks'; +import { CommandArgs as CheckoutCommandArgs, CheckoutGitCommand } from './git/checkout'; +import { CherryPickGitCommand } from './git/cherry-pick'; +import { CommandArgs as FetchCommandArgs, FetchGitCommand } from './git/fetch'; +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'; const sanitizeLabel = /\$\(.+?\)|\W/g; @@ -24,18 +19,18 @@ export type GitCommandsCommandArgs = CheckoutCommandArgs | FetchCommandArgs | Pu class PickCommandStep implements QuickPickStep { readonly buttons = []; readonly items: QuickCommandBase[]; - readonly placeholder = 'Select command...'; + readonly placeholder = 'Choose a git command'; readonly title = 'GitLens'; constructor(args?: GitCommandsCommandArgs) { this.items = [ - new CheckoutQuickCommand(args && args.command === 'checkout' ? args : undefined), - new CherryPickQuickCommand(), - new MergeQuickCommand(), - 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() + new CheckoutGitCommand(args && args.command === 'checkout' ? args : undefined), + new CherryPickGitCommand(), + new MergeGitCommand(), + 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() ]; } @@ -275,6 +270,25 @@ export class GitCommandsCommand extends Command { items = quickpick.activeItems; } + if (items.length === 1) { + const item = items[0]; + if (BackOrCancelQuickPickItem.is(item)) { + if (item.cancelled) { + resolve(); + + return; + } + + quickpick.value = ''; + if (commandsStep.command !== undefined) { + quickpick.busy = true; + resolve((await commandsStep.command.previous()) || commandsStep); + } + + return; + } + } + if (commandsStep.command === undefined) { const command = items[0]; if (!QuickCommandBase.is(command)) return; diff --git a/src/commands/quick/checkout.ts b/src/commands/quick/checkout.ts deleted file mode 100644 index f8a3b3a..0000000 --- a/src/commands/quick/checkout.ts +++ /dev/null @@ -1,277 +0,0 @@ -'use strict'; -/* eslint-disable no-loop-func */ -import { ProgressLocation, QuickInputButtons, window } from 'vscode'; -import { Container } from '../../container'; -import { GitBranch, GitReference, GitTag, Repository } from '../../git/gitService'; -import { GlyphChars } from '../../constants'; -import { - CommandAbortError, - getBranchesAndOrTags, - QuickCommandBase, - QuickInputStep, - QuickPickStep, - StepState -} from './quickCommand'; -import { ReferencesQuickPickItem, RefQuickPickItem, RepositoryQuickPickItem } from '../../quickpicks'; -import { Strings } from '../../system'; - -interface State { - repos: Repository[]; - branchOrTagOrRef: GitBranch | GitTag | GitReference; - createBranch?: string; -} - -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) { - return void (await window.withProgress( - { - location: ProgressLocation.Notification, - title: `Checking out ${ - state.repos.length === 1 ? state.repos[0].formattedName : `${state.repos.length} repositories` - } to ${state.branchOrTagOrRef.ref}` - }, - () => - Promise.all( - state.repos.map(r => - r.checkout(state.branchOrTagOrRef.ref, { createBranch: state.createBranch, progress: false }) - ) - ) - )); - } - - protected async *steps(): AsyncIterableIterator { - const state: StepState = this._initialState === undefined ? { counter: 0 } : this._initialState; - let oneRepo = false; - let showTags = false; - - while (true) { - try { - if (state.repos === undefined || state.counter < 1) { - const repos = [...(await Container.git.getOrderedRepositories())]; - - if (repos.length === 1) { - oneRepo = true; - state.counter++; - state.repos = [repos[0]]; - } - else { - const step = this.createPickStep({ - multiselect: true, - title: this.title, - placeholder: 'Choose repositories', - items: await Promise.all( - repos.map(repo => - RepositoryQuickPickItem.create( - repo, - state.repos ? state.repos.some(r => r.id === repo.id) : undefined, - { branch: true, fetched: true, status: true } - ) - ) - ) - }); - const selection = yield step; - - if (!this.canMoveNext(step, state, selection)) { - break; - } - - state.repos = selection.map(i => i.item); - } - } - - if (state.branchOrTagOrRef === undefined || state.counter < 2) { - const includeTags = showTags || state.repos.length === 1; - - 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${GlyphChars.Space.repeat(3)}(select or enter a reference)`, - matchOnDescription: true, - items: items, - selectedItems: state.branchOrTagOrRef - ? items.filter(ref => ref.label === state.branchOrTagOrRef!.ref) - : undefined, - buttons: includeTags - ? [QuickInputButtons.Back] - : [ - QuickInputButtons.Back, - { - iconPath: { - dark: Container.context.asAbsolutePath('images/dark/icon-tag.svg') as any, - light: Container.context.asAbsolutePath('images/light/icon-tag.svg') as any - }, - tooltip: 'Show Tags' - } - ], - onDidClickButton: async (quickpick, button) => { - quickpick.busy = true; - quickpick.enabled = false; - - if (!showTags) { - showTags = true; - } - - 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 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); - // }, - onValidateValue: async (quickpick, value) => { - if (state.repos!.length !== 1) return false; - if (!(await Container.git.validateReference(state.repos![0].path, value))) return false; - - quickpick.items = [RefQuickPickItem.create(value, true, { ref: true })]; - return true; - } - }); - const selection = yield step; - - if (!this.canMoveNext(step, state, selection)) { - if (oneRepo) { - break; - } - - continue; - } - - state.branchOrTagOrRef = selection[0].item; - } - - if (GitBranch.is(state.branchOrTagOrRef) && 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` - }`, - 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; - } - - 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); - break; - } - catch (ex) { - if (ex instanceof CommandAbortError) break; - - throw ex; - } - } - } -} diff --git a/src/commands/quick/cherry-pick.ts b/src/commands/quick/cherry-pick.ts deleted file mode 100644 index a50e177..0000000 --- a/src/commands/quick/cherry-pick.ts +++ /dev/null @@ -1,201 +0,0 @@ -'use strict'; -/* eslint-disable no-loop-func */ -import { Container } from '../../container'; -import { GitBranch, GitLogCommit, GitReference, Repository } from '../../git/gitService'; -import { GlyphChars } from '../../constants'; -import { Iterables, Strings } from '../../system'; -import { - CommandAbortError, - getBranchesAndOrTags, - QuickCommandBase, - QuickInputStep, - QuickPickStep, - StepState -} from './quickCommand'; -import { BranchQuickPickItem, CommitQuickPickItem, RepositoryQuickPickItem } from '../../quickpicks'; -import { runGitCommandInTerminal } from '../../terminal'; -import { RefQuickPickItem } from '../../quickpicks/gitQuickPicks'; - -interface State { - repo: Repository; - destination: GitBranch; - source: GitBranch | GitReference; - commits?: GitLogCommit[]; -} - -export class CherryPickQuickCommand extends QuickCommandBase { - constructor() { - super('cherry-pick', 'Cherry Pick', { description: 'via Terminal' }); - } - - execute(state: State) { - if (state.commits !== undefined) { - // Ensure the commits are ordered with the oldest first - state.commits.sort((a, b) => a.date.getTime() - b.date.getTime()); - runGitCommandInTerminal('cherry-pick', state.commits.map(c => c.sha).join(' '), state.repo.path, true); - } - - runGitCommandInTerminal('cherry-pick', state.source.ref, state.repo.path, true); - } - - protected async *steps(): AsyncIterableIterator { - const state: StepState = this._initialState === undefined ? { counter: 0 } : this._initialState; - let oneRepo = false; - - while (true) { - try { - if (state.repo === undefined || state.counter < 1) { - const repos = [...(await Container.git.getOrderedRepositories())]; - - if (repos.length === 1) { - oneRepo = true; - state.counter++; - state.repo = repos[0]; - } - else { - const active = state.repo ? state.repo : await Container.git.getActiveRepository(); - - const step = this.createPickStep({ - title: this.title, - placeholder: 'Choose a repository', - items: await Promise.all( - repos.map(r => - RepositoryQuickPickItem.create(r, r.id === (active && active.id), { - branch: true, - fetched: true, - status: true - }) - ) - ) - }); - const selection = yield step; - - if (!this.canMoveNext(step, state, selection)) { - break; - } - - state.repo = selection[0].item; - } - } - - state.destination = await state.repo.getBranch(); - if (state.destination === undefined) break; - - if (state.source === undefined || state.counter < 2) { - const destId = state.destination.id; - - 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${GlyphChars.Space.repeat( - 3 - )}(select or enter a reference)`, - matchOnDescription: true, - items: await getBranchesAndOrTags(state.repo, true, { - filterBranches: b => b.id !== destId - }), - onValidateValue: async (quickpick, value) => { - if (!(await Container.git.validateReference(state.repo!.path, value))) return false; - - quickpick.items = [RefQuickPickItem.create(value, true, { ref: true })]; - return true; - } - }); - const selection = yield step; - - if (!this.canMoveNext(step, state, selection)) { - if (oneRepo) { - break; - } - - continue; - } - - if (GitBranch.is(state.source)) { - state.source = selection[0].item; - } - else { - state.source = selection[0].item; - state.counter++; - } - } - - if (GitBranch.is(state.source) && (state.commits === undefined || state.counter < 3)) { - const log = await Container.git.getLog(state.repo.path, { - ref: `${state.destination.ref}..${state.source.ref}`, - merges: false - }); - - const step = this.createPickStep({ - title: `${this.title} onto ${state.destination.name}${Strings.pad(GlyphChars.Dot, 2, 2)}${ - state.repo.name - }`, - multiselect: log !== undefined, - placeholder: - log === undefined - ? `${state.source.name} has no pickable commits` - : `Choose commits to cherry-pick onto ${state.destination.name}`, - items: - log === undefined - ? [] - : [ - ...Iterables.map(log.commits.values(), commit => - CommitQuickPickItem.create( - commit, - state.commits ? state.commits.some(c => c.sha === commit.sha) : undefined, - { compact: true } - ) - ) - ] - }); - const selection = yield step; - - if (!this.canMoveNext(step, state, selection)) { - continue; - } - - state.commits = selection.map(i => i.item); - } - - const step = this.createConfirmStep( - `Confirm ${this.title}${Strings.pad(GlyphChars.Dot, 2, 2)}${state.repo.name}`, - [ - state.commits !== undefined - ? { - label: this.title, - description: `${ - state.commits.length === 1 - ? state.commits[0].shortSha - : `${state.commits.length} commits` - } onto ${state.destination.name}`, - detail: `Will apply ${ - state.commits.length === 1 - ? `commit ${state.commits[0].shortSha}` - : `${state.commits.length} commits` - } onto ${state.destination.name}` - } - : { - label: this.title, - description: `${state.source.name} onto ${state.destination.name}`, - detail: `Will apply commit ${state.source.name} onto ${state.destination.name}` - } - ] - ); - const selection = yield step; - - if (!this.canMoveNext(step, state, selection)) { - continue; - } - - this.execute(state as State); - break; - } - catch (ex) { - if (ex instanceof CommandAbortError) break; - - throw ex; - } - } - } -} diff --git a/src/commands/quick/fetch.ts b/src/commands/quick/fetch.ts deleted file mode 100644 index 010edfc..0000000 --- a/src/commands/quick/fetch.ts +++ /dev/null @@ -1,163 +0,0 @@ -'use strict'; -import { QuickPickItem } from 'vscode'; -import { Container } from '../../container'; -import { Repository } from '../../git/gitService'; -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 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) { - return Container.git.fetchAll(state.repos, { - all: state.flags.includes('--all'), - prune: state.flags.includes('--prune') - }); - } - - protected async *steps(): AsyncIterableIterator { - const state: StepState = this._initialState === undefined ? { counter: 0 } : this._initialState; - let oneRepo = false; - - while (true) { - try { - if (state.repos === undefined || state.counter < 1) { - const repos = [...(await Container.git.getOrderedRepositories())]; - - if (repos.length === 1) { - oneRepo = true; - state.counter++; - state.repos = [repos[0]]; - } - else { - const step = this.createPickStep({ - multiselect: true, - title: this.title, - placeholder: 'Choose repositories', - items: await Promise.all( - repos.map(repo => - RepositoryQuickPickItem.create( - repo, - state.repos ? state.repos.some(r => r.id === repo.id) : undefined, - { - branch: true, - fetched: true, - status: true - } - ) - ) - ) - }); - const selection = yield step; - - if (!this.canMoveNext(step, state, selection)) { - break; - } - - state.repos = selection.map(i => i.item); - } - } - - 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; - } - - continue; - } - - state.flags = selection[0].item; - } - - this.execute(state as State); - break; - } - catch (ex) { - if (ex instanceof CommandAbortError) break; - - throw ex; - } - } - } -} diff --git a/src/commands/quick/merge.ts b/src/commands/quick/merge.ts deleted file mode 100644 index d7f1d42..0000000 --- a/src/commands/quick/merge.ts +++ /dev/null @@ -1,171 +0,0 @@ -'use strict'; -import { QuickPickItem } from 'vscode'; -import { Container } from '../../container'; -import { GitBranch, Repository } from '../../git/gitService'; -import { GlyphChars } from '../../constants'; -import { - CommandAbortError, - getBranchesAndOrTags, - QuickCommandBase, - QuickInputStep, - QuickPickStep, - StepState -} from './quickCommand'; -import { BranchQuickPickItem, RepositoryQuickPickItem } from '../../quickpicks'; -import { Strings } from '../../system'; -import { runGitCommandInTerminal } from '../../terminal'; - -interface State { - repo: Repository; - destination: GitBranch; - source: GitBranch; - flags: string[]; -} - -export class MergeQuickCommand extends QuickCommandBase { - constructor() { - super('merge', 'Merge', { description: 'via Terminal' }); - } - - execute(state: State) { - runGitCommandInTerminal('merge', [...state.flags, state.source.ref].join(' '), state.repo.path, true); - } - - protected async *steps(): AsyncIterableIterator { - const state: StepState = this._initialState === undefined ? { counter: 0 } : this._initialState; - let oneRepo = false; - - while (true) { - try { - if (state.repo === undefined || state.counter < 1) { - const repos = [...(await Container.git.getOrderedRepositories())]; - - if (repos.length === 1) { - oneRepo = true; - state.counter++; - state.repo = repos[0]; - } - else { - const active = state.repo ? state.repo : await Container.git.getActiveRepository(); - - const step = this.createPickStep({ - title: this.title, - placeholder: 'Choose a repository', - items: await Promise.all( - repos.map(r => - RepositoryQuickPickItem.create(r, r.id === (active && active.id), { - branch: true, - fetched: true, - status: true - }) - ) - ) - }); - const selection = yield step; - - if (!this.canMoveNext(step, state, selection)) { - break; - } - - state.repo = selection[0].item; - } - } - - state.destination = await state.repo.getBranch(); - if (state.destination === undefined) break; - - if (state.source === undefined || state.counter < 2) { - const destId = state.destination.id; - - 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 getBranchesAndOrTags(state.repo, true, { - filterBranches: b => b.id !== destId, - picked: state.source && state.source.ref - }) - }); - const selection = yield step; - - if (!this.canMoveNext(step, state, selection)) { - if (oneRepo) { - break; - } - continue; - } - - state.source = selection[0].item; - } - - const count = - (await Container.git.getCommitCount(state.repo.path, [ - `${state.destination.name}..${state.source.name}` - ])) || 0; - if (count === 0) { - const step = this.createConfirmStep( - `Confirm ${this.title}${Strings.pad(GlyphChars.Dot, 2, 2)}${state.repo.name}`, - [ - { - label: `Cancel ${this.title}`, - description: '', - detail: `${state.destination.name} is up to date with ${state.source.name}` - } - ], - false - ); - - yield step; - break; - } - - const step = this.createConfirmStep( - `Confirm ${this.title}${Strings.pad(GlyphChars.Dot, 2, 2)}${state.repo.name}`, - [ - { - label: this.title, - description: `${state.source.name} into ${state.destination.name}`, - detail: `Will merge ${Strings.pluralize('commit', count)} from ${state.source.name} into ${ - state.destination.name - }`, - item: [] - }, - { - label: `Fast-forward ${this.title}`, - description: `--ff-only ${state.source.name} into ${state.destination.name}`, - detail: `Will fast-forward merge ${Strings.pluralize('commit', count)} from ${ - state.source.name - } into ${state.destination.name}`, - item: ['--ff-only'] - }, - { - label: `No Fast-forward ${this.title}`, - description: `--no-ff ${state.source.name} into ${state.destination.name}`, - detail: `Will create a merge commit when merging ${Strings.pluralize( - 'commit', - count - )} from ${state.source.name} into ${state.destination.name}`, - item: ['--no-ff'] - } - ] - ); - const selection = yield step; - - if (!this.canMoveNext(step, state, selection)) { - continue; - } - - state.flags = selection[0].item; - - this.execute(state as State); - break; - } - catch (ex) { - if (ex instanceof CommandAbortError) break; - - throw ex; - } - } - } -} diff --git a/src/commands/quick/pull.ts b/src/commands/quick/pull.ts deleted file mode 100644 index b7bc57e..0000000 --- a/src/commands/quick/pull.ts +++ /dev/null @@ -1,143 +0,0 @@ -'use strict'; -import { QuickPickItem } from 'vscode'; -import { Container } from '../../container'; -import { Repository } from '../../git/gitService'; -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 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') }); - } - - protected async *steps(): AsyncIterableIterator { - const state: StepState = this._initialState === undefined ? { counter: 0 } : this._initialState; - let oneRepo = false; - - while (true) { - try { - if (state.repos === undefined || state.counter < 1) { - const repos = [...(await Container.git.getOrderedRepositories())]; - - if (repos.length === 1) { - oneRepo = true; - state.counter++; - state.repos = [repos[0]]; - } - else { - const step = this.createPickStep({ - multiselect: true, - title: this.title, - placeholder: 'Choose repositories', - items: await Promise.all( - repos.map(repo => - RepositoryQuickPickItem.create( - repo, - state.repos ? state.repos.some(r => r.id === repo.id) : undefined, - { - branch: true, - fetched: true, - status: true - } - ) - ) - ) - }); - const selection = yield step; - - if (!this.canMoveNext(step, state, selection)) { - break; - } - - state.repos = selection.map(i => i.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` - }`, - [ - { - label: this.title, - description: '', - detail: `Will pull ${ - state.repos.length === 1 - ? state.repos[0].formattedName - : `${state.repos.length} repositories` - }`, - item: [] - }, - { - label: `${this.title} with Rebase`, - description: '--rebase', - detail: `Will pull with rebase ${ - state.repos.length === 1 - ? state.repos[0].formattedName - : `${state.repos.length} repositories` - }`, - item: ['--rebase'] - } - ] - ); - const selection = yield step; - - if (!this.canMoveNext(step, state, selection)) { - if (oneRepo) { - break; - } - - continue; - } - - state.flags = selection[0].item; - - this.execute(state as State); - break; - } - catch (ex) { - if (ex instanceof CommandAbortError) break; - - throw ex; - } - } - } -} diff --git a/src/commands/quick/push.ts b/src/commands/quick/push.ts deleted file mode 100644 index 5a23b41..0000000 --- a/src/commands/quick/push.ts +++ /dev/null @@ -1,149 +0,0 @@ -'use strict'; -import { Container } from '../../container'; -import { Repository } from '../../git/gitService'; -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 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, { force: state.flags.includes('--force') }); - } - - protected async *steps(): AsyncIterableIterator { - const state: StepState = this._initialState === undefined ? { counter: 0 } : this._initialState; - let oneRepo = false; - - while (true) { - try { - if (state.repos === undefined || state.counter < 1) { - const repos = [...(await Container.git.getOrderedRepositories())]; - - if (repos.length === 1) { - oneRepo = true; - state.counter++; - state.repos = [repos[0]]; - } - else { - const step = this.createPickStep({ - multiselect: true, - title: this.title, - placeholder: 'Choose repositories', - items: await Promise.all( - repos.map(repo => - RepositoryQuickPickItem.create( - repo, - state.repos ? state.repos.some(r => r.id === repo.id) : undefined, - { - branch: true, - fetched: true, - status: true - } - ) - ) - ) - }); - const selection = yield step; - - if (!this.canMoveNext(step, state, selection)) { - break; - } - - state.repos = selection.map(i => i.item); - } - } - - 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; - } - - continue; - } - - state.flags = selection[0].item; - } - - this.execute(state as State); - break; - } - catch (ex) { - if (ex instanceof CommandAbortError) break; - - throw ex; - } - } - } -} diff --git a/src/commands/quick/quickCommand.ts b/src/commands/quick/quickCommand.ts deleted file mode 100644 index 540c179..0000000 --- a/src/commands/quick/quickCommand.ts +++ /dev/null @@ -1,175 +0,0 @@ -'use strict'; -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[]; - selectedItems?: QuickPickItem[]; - items: QuickPickItem[]; - matchOnDescription?: boolean; - matchOnDetail?: boolean; - multiselect?: boolean; - placeholder?: string; - title?: string; - value?: string; - - onDidAccept?(quickpick: QuickPick): Promise; - onDidClickButton?(quickpick: QuickPick, button: QuickInputButton): void; - onValidateValue?(quickpick: QuickPick, value: string, items: T[]): Promise; - 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 { - constructor() { - super('Abort'); - } -} - -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; - } - - readonly description?: string; - readonly detail?: string; - - private _current: QuickPickStep | QuickInputStep | undefined; - private _stepsIterator: AsyncIterableIterator | undefined; - - constructor( - public readonly label: string, - public readonly title: string, - options: { - description?: string; - detail?: string; - } = {} - ) { - this.description = options.description; - this.detail = options.detail; - } - - private _picked: boolean = false; - get picked() { - return this._picked; - } - set picked(value: boolean) { - this._picked = value; - } - - protected _initialState?: StepState; - - protected abstract steps(): AsyncIterableIterator; - - async previous(): Promise { - // Simulate going back, by having no selection - return (await this.next([])).value; - } - - async next(value?: QuickPickItem[] | string): Promise> { - if (this._stepsIterator === undefined) { - this._stepsIterator = this.steps(); - } - - 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 | QuickInputStep | undefined { - return this._current; - } - - protected createConfirmStep( - title: string, - confirmations: T[], - cancellable: boolean = true - ): QuickPickStep { - return this.createPickStep({ - placeholder: `Confirm ${this.title}`, - title: title, - items: cancellable ? [...confirmations, { label: 'Cancel' }] : confirmations, - selectedItems: [confirmations[0]], - // eslint-disable-next-line no-loop-func - validate: (selection: T[]) => { - if (selection[0].label === 'Cancel') throw new CommandAbortError(); - return true; - } - }); - } - - protected createInputStep(step: QuickInputStep): QuickInputStep { - return step; - } - - protected createPickStep(step: QuickPickStep): QuickPickStep { - return step; - } - - protected canMoveNext( - step: QuickPickStep, - state: { counter: number }, - selection: T[] | undefined - ): 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; - } - return false; - } - - 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 deleted file mode 100644 index dce54c0..0000000 --- a/src/commands/quick/quickCommands.helpers.ts +++ /dev/null @@ -1,85 +0,0 @@ -'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 deleted file mode 100644 index e411eba..0000000 --- a/src/commands/quick/rebase.ts +++ /dev/null @@ -1,163 +0,0 @@ -'use strict'; -import { QuickPickItem } from 'vscode'; -import { Container } from '../../container'; -import { GitBranch, Repository } from '../../git/gitService'; -import { GlyphChars } from '../../constants'; -import { - CommandAbortError, - getBranchesAndOrTags, - QuickCommandBase, - QuickInputStep, - QuickPickStep, - StepState -} from './quickCommand'; -import { BranchQuickPickItem, RepositoryQuickPickItem } from '../../quickpicks'; -import { Strings } from '../../system'; -import { runGitCommandInTerminal } from '../../terminal'; - -interface State { - repo: Repository; - destination: GitBranch; - source: GitBranch; - flags: string[]; -} - -export class RebaseQuickCommand extends QuickCommandBase { - constructor() { - super('rebase', 'Rebase', { description: 'via Terminal' }); - } - - execute(state: State) { - runGitCommandInTerminal('rebase', [...state.flags, state.source.ref].join(' '), state.repo.path, true); - } - - protected async *steps(): AsyncIterableIterator { - const state: StepState = this._initialState === undefined ? { counter: 0 } : this._initialState; - let oneRepo = false; - - while (true) { - try { - if (state.repo === undefined || state.counter < 1) { - const repos = [...(await Container.git.getOrderedRepositories())]; - - if (repos.length === 1) { - oneRepo = true; - state.counter++; - state.repo = repos[0]; - } - else { - const active = state.repo ? state.repo : await Container.git.getActiveRepository(); - - const step = this.createPickStep({ - title: this.title, - placeholder: 'Choose a repository', - items: await Promise.all( - repos.map(r => - RepositoryQuickPickItem.create(r, r.id === (active && active.id), { - branch: true, - fetched: true, - status: true - }) - ) - ) - }); - const selection = yield step; - - if (!this.canMoveNext(step, state, selection)) { - break; - } - - state.repo = selection[0].item; - } - } - - state.destination = await state.repo.getBranch(); - if (state.destination === undefined) break; - - if (state.source === undefined || state.counter < 2) { - const destId = state.destination.id; - - 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 getBranchesAndOrTags(state.repo, true, { - filterBranches: b => b.id !== destId, - picked: state.source && state.source.ref - }) - }); - const selection = yield step; - - if (!this.canMoveNext(step, state, selection)) { - if (oneRepo) { - break; - } - continue; - } - - state.source = selection[0].item; - } - - const count = - (await Container.git.getCommitCount(state.repo.path, [ - `${state.destination.name}..${state.source.name}` - ])) || 0; - if (count === 0) { - const step = this.createConfirmStep( - `Confirm ${this.title}${Strings.pad(GlyphChars.Dot, 2, 2)}${state.repo.name}`, - [ - { - label: `Cancel ${this.title}`, - description: '', - detail: `${state.destination.name} is up to date with ${state.source.name}` - } - ], - false - ); - - yield step; - break; - } - - const step = this.createConfirmStep( - `Confirm ${this.title}${Strings.pad(GlyphChars.Dot, 2, 2)}${state.repo.name}`, - [ - { - label: this.title, - description: `${state.destination.name} with ${state.source.name}`, - detail: `Will update ${state.destination.name} by applying ${Strings.pluralize( - 'commit', - count - )} on top of ${state.source.name}`, - item: [] - }, - { - label: `Interactive ${this.title}`, - description: `--interactive ${state.destination.name} with ${state.source.name}`, - detail: `Will interactively update ${ - state.destination.name - } by applying ${Strings.pluralize('commit', count)} on top of ${state.source.name}`, - item: ['--interactive'] - } - ] - ); - const selection = yield step; - - if (!this.canMoveNext(step, state, selection)) { - continue; - } - - state.flags = selection[0].item; - - this.execute(state as State); - break; - } - catch (ex) { - if (ex instanceof CommandAbortError) break; - - throw ex; - } - } - } -} diff --git a/src/commands/quickCommand.helpers.ts b/src/commands/quickCommand.helpers.ts new file mode 100644 index 0000000..f4751e7 --- /dev/null +++ b/src/commands/quickCommand.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/quickCommand.ts b/src/commands/quickCommand.ts new file mode 100644 index 0000000..f72acae --- /dev/null +++ b/src/commands/quickCommand.ts @@ -0,0 +1,177 @@ +'use strict'; +import { InputBox, QuickInputButton, QuickPick, QuickPickItem } from 'vscode'; +import { Promises } from '../system'; +import { BackOrCancelQuickPickItem } from '../quickpicks'; + +export * from './quickCommand.helpers'; + +export enum Directive { + Back = 'back' +} + +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[]; + selectedItems?: QuickPickItem[]; + items: (BackOrCancelQuickPickItem | T)[] | BackOrCancelQuickPickItem[]; + matchOnDescription?: boolean; + matchOnDetail?: boolean; + multiselect?: boolean; + placeholder?: string; + title?: string; + value?: string; + + onDidAccept?(quickpick: QuickPick): Promise; + onDidClickButton?(quickpick: QuickPick, button: QuickInputButton): void; + onValidateValue?(quickpick: QuickPick, value: string, items: T[]): Promise; + validate?(selection: T[]): boolean | Promise; +} + +export function isQuickPickStep(item: QuickPickStep | QuickInputStep): item is QuickPickStep { + return (item as QuickPickStep).items !== undefined; +} + +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; + } + + readonly description?: string; + readonly detail?: string; + + private _current: QuickPickStep | QuickInputStep | undefined; + private _stepsIterator: AsyncIterableIterator | undefined; + + constructor( + public readonly label: string, + public readonly title: string, + options: { + description?: string; + detail?: string; + } = {} + ) { + this.description = options.description; + this.detail = options.detail; + } + + private _picked: boolean = false; + get picked() { + return this._picked; + } + set picked(value: boolean) { + this._picked = value; + } + + protected _initialState?: StepState; + + protected abstract steps(): AsyncIterableIterator; + + async previous(): Promise { + return (await this.next(Directive.Back)).value; + } + + async next(value?: QuickPickItem[] | string | Directive): Promise> { + if (this._stepsIterator === undefined) { + this._stepsIterator = this.steps(); + } + + 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 | QuickInputStep | undefined { + return this._current; + } + + protected createConfirmStep( + title: string, + confirmations: T[], + cancellable: boolean = true + ): QuickPickStep { + return this.createPickStep({ + placeholder: `Confirm ${this.title}`, + title: title, + items: cancellable ? [...confirmations, BackOrCancelQuickPickItem.create()] : confirmations, + selectedItems: [confirmations[0]] + }); + } + + protected createInputStep(step: QuickInputStep): QuickInputStep { + return step; + } + + protected createPickStep(step: QuickPickStep): QuickPickStep { + return step; + } + + protected canMoveNext( + step: QuickPickStep, + state: { counter: number }, + selection: T[] | Directive | undefined + ): selection is T[]; + protected canMoveNext( + step: QuickInputStep, + state: { counter: number }, + value: string | Directive | undefined + ): boolean | Promise; + protected canMoveNext( + step: QuickPickStep | QuickInputStep, + state: { counter: number }, + value: T[] | string | Directive | undefined + ) { + if (value === Directive.Back) { + state.counter--; + if (state.counter < 0) { + state.counter = 0; + } + return false; + } + + 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)) { + const [valid] = result; + if (valid) { + state.counter++; + } + return valid; + } + + return result.then(([valid]) => { + if (valid) { + state.counter++; + } + return valid; + }); + } + + return false; + } +} diff --git a/src/quickpicks/gitQuickPicks.ts b/src/quickpicks/gitQuickPicks.ts index f5fda70..7a14339 100644 --- a/src/quickpicks/gitQuickPicks.ts +++ b/src/quickpicks/gitQuickPicks.ts @@ -14,6 +14,26 @@ import { } from '../git/gitService'; import { emojify } from '../emojis'; +export interface BackOrCancelQuickPickItem extends QuickPickItem { + cancelled: boolean; +} + +export namespace BackOrCancelQuickPickItem { + export function create(cancelled: boolean = true, picked?: boolean, label?: string) { + const item: BackOrCancelQuickPickItem = { + label: label || (cancelled ? 'Cancel' : 'Back'), + picked: picked, + cancelled: cancelled + }; + + return item; + } + + export function is(item: QuickPickItem): item is BackOrCancelQuickPickItem { + return item != null && 'cancelled' in item; + } +} + export interface BranchQuickPickItem extends QuickPickItem { readonly item: GitBranch; readonly current: boolean; @@ -169,6 +189,13 @@ export namespace CommitQuickPickItem { } } +export interface RefQuickPickItem extends QuickPickItem { + readonly item: GitReference; + readonly current: boolean; + readonly ref: string; + readonly remote: boolean; +} + export namespace RefQuickPickItem { export function create(ref: string, picked?: boolean, options: { ref?: boolean } = {}) { const item: RefQuickPickItem = { @@ -185,13 +212,6 @@ export namespace RefQuickPickItem { } } -export interface RefQuickPickItem extends QuickPickItem { - readonly item: GitReference; - readonly current: boolean; - readonly ref: string; - readonly remote: boolean; -} - export interface RepositoryQuickPickItem extends QuickPickItem { readonly item: Repository; readonly repoPath: string; diff --git a/src/views/viewCommands.ts b/src/views/viewCommands.ts index bc9b6b5..955eb2e 100644 --- a/src/views/viewCommands.ts +++ b/src/views/viewCommands.ts @@ -201,7 +201,8 @@ export class ViewCommands { private fetch(node: RemoteNode | RepositoryNode) { if (node instanceof RemoteNode) return node.fetch(); if (node instanceof RepositoryNode) { - return commands.executeCommand(Commands.GitCommands, { command: 'fetch', state: { repos: [node.repo] } }); + const args: GitCommandsCommandArgs = { command: 'fetch', state: { repos: [node.repo] } }; + return commands.executeCommand(Commands.GitCommands, args); } return undefined; @@ -213,7 +214,8 @@ export class ViewCommands { } if (!(node instanceof RepositoryNode)) return undefined; - return commands.executeCommand(Commands.GitCommands, { command: 'pull', state: { repos: [node.repo] } }); + const args: GitCommandsCommandArgs = { command: 'pull', state: { repos: [node.repo] } }; + return commands.executeCommand(Commands.GitCommands, args); } private push(node: RepositoryNode | BranchTrackingStatusNode, force?: boolean) { @@ -222,7 +224,8 @@ export class ViewCommands { } if (!(node instanceof RepositoryNode)) return undefined; - return commands.executeCommand(Commands.GitCommands, { command: 'push', state: { repos: [node.repo] } }); + const args: GitCommandsCommandArgs = { command: 'push', state: { repos: [node.repo] } }; + return commands.executeCommand(Commands.GitCommands, args); } private async applyChanges(node: ViewRefFileNode) {