@ -0,0 +1,195 @@ | |||
'use strict'; | |||
import { Disposable, QuickInputButtons, QuickPickItem, window } from 'vscode'; | |||
import { command, Command, Commands } from './common'; | |||
import { log } from '../system'; | |||
import { CherryPickQuickCommand } from './quick/cherry-pick'; | |||
import { QuickCommandBase, QuickPickStep } from './quick/quickCommand'; | |||
import { 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 { RebaseQuickCommand } from './quick/rebase'; | |||
const sanitizeLabel = /\$\(.+?\)|\W/g; | |||
@command() | |||
export class GitCommandsCommand extends Command { | |||
constructor() { | |||
super(Commands.GitCommands); | |||
} | |||
@log({ args: false, correlate: true, singleLine: true, timed: false }) | |||
async execute() { | |||
const commands: QuickCommandBase[] = [ | |||
new CheckoutQuickCommand(), | |||
new CherryPickQuickCommand(), | |||
new MergeQuickCommand(), | |||
new FetchQuickCommand(), | |||
new PullQuickCommand(), | |||
new PushQuickCommand(), | |||
new RebaseQuickCommand() | |||
]; | |||
const quickpick = window.createQuickPick(); | |||
quickpick.ignoreFocusOut = true; | |||
let inCommand: QuickCommandBase | undefined; | |||
function showCommand(command: QuickPickStep | undefined) { | |||
if (command === undefined) { | |||
const previousLabel = inCommand && inCommand.label; | |||
inCommand = undefined; | |||
quickpick.buttons = []; | |||
quickpick.title = 'GitLens'; | |||
quickpick.placeholder = 'Select command...'; | |||
quickpick.canSelectMany = false; | |||
quickpick.items = commands; | |||
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); | |||
quickpick.items = command.items; | |||
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); | |||
} | |||
// // 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 = []; | |||
// } | |||
} | |||
} | |||
async function next(command: QuickCommandBase, items: QuickPickItem[] | undefined) { | |||
quickpick.busy = true; | |||
// quickpick.enabled = false; | |||
const next = await command.next(items); | |||
if (next.done) { | |||
return false; | |||
} | |||
quickpick.value = ''; | |||
showCommand(next.value); | |||
// quickpick.enabled = true; | |||
quickpick.busy = false; | |||
return true; | |||
} | |||
showCommand(undefined); | |||
const disposables: Disposable[] = []; | |||
try { | |||
void (await new Promise<void>(resolve => { | |||
disposables.push( | |||
quickpick.onDidHide(() => resolve()), | |||
quickpick.onDidTriggerButton(async e => { | |||
if (e === QuickInputButtons.Back) { | |||
quickpick.value = ''; | |||
if (inCommand !== undefined) { | |||
showCommand(await inCommand.previous()); | |||
} | |||
return; | |||
} | |||
const step = inCommand && inCommand.value; | |||
if (step === undefined || step.onDidClickButton === undefined) return; | |||
step.onDidClickButton(quickpick, e); | |||
}), | |||
quickpick.onDidChangeValue(async e => { | |||
if (quickpick.canSelectMany && e === ' ') { | |||
quickpick.value = ''; | |||
quickpick.selectedItems = | |||
quickpick.selectedItems.length === quickpick.items.length ? [] : quickpick.items; | |||
return; | |||
} | |||
if (e.endsWith(' ')) { | |||
if (quickpick.canSelectMany && quickpick.selectedItems.length !== 0) { | |||
return; | |||
} | |||
const cmd = quickpick.value.toLowerCase().trim(); | |||
let items; | |||
if (inCommand === undefined) { | |||
const command = commands.find( | |||
c => c.label.replace(sanitizeLabel, '').toLowerCase() === cmd | |||
); | |||
if (command === undefined) return; | |||
inCommand = command; | |||
} | |||
else { | |||
const step = inCommand.value; | |||
if (step === undefined) return; | |||
const item = step.items.find( | |||
i => i.label.replace(sanitizeLabel, '').toLowerCase() === cmd | |||
); | |||
if (item === undefined) return; | |||
items = [item]; | |||
} | |||
if (!(await next(inCommand, items))) { | |||
resolve(); | |||
} | |||
} | |||
}), | |||
quickpick.onDidAccept(async () => { | |||
let items = quickpick.selectedItems; | |||
if (items.length === 0) { | |||
if (!quickpick.canSelectMany || quickpick.activeItems.length === 0) return; | |||
items = quickpick.activeItems; | |||
} | |||
if (inCommand === undefined) { | |||
const command = items[0]; | |||
if (!QuickCommandBase.is(command)) return; | |||
inCommand = command; | |||
} | |||
if (!(await next(inCommand, items as QuickPickItem[]))) { | |||
resolve(); | |||
} | |||
}) | |||
); | |||
quickpick.show(); | |||
})); | |||
quickpick.hide(); | |||
} | |||
finally { | |||
quickpick.dispose(); | |||
disposables.forEach(d => d.dispose()); | |||
} | |||
} | |||
} |
@ -0,0 +1,161 @@ | |||
'use strict'; | |||
/* eslint-disable no-loop-func */ | |||
import { ProgressLocation, QuickInputButtons, window } from 'vscode'; | |||
import { Container } from '../../container'; | |||
import { Repository } from '../../git/gitService'; | |||
import { GlyphChars } from '../../constants'; | |||
import { GitCommandBase } from './gitCommand'; | |||
import { CommandAbortError, QuickPickStep } from './quickCommand'; | |||
import { ReferencesQuickPickItem, RepositoryQuickPickItem } from '../../quickpicks'; | |||
import { Strings } from '../../system'; | |||
interface State { | |||
repos: Repository[]; | |||
ref: string; | |||
} | |||
export class CheckoutQuickCommand extends GitCommandBase { | |||
constructor() { | |||
super('checkout', 'Checkout'); | |||
} | |||
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.ref}` | |||
}, | |||
() => Promise.all(state.repos.map(r => r.checkout(state.ref, { progress: false }))) | |||
)); | |||
} | |||
async *steps(): AsyncIterableIterator<QuickPickStep> { | |||
const state: Partial<State> & { counter: number } = { counter: 0 }; | |||
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.createStep<RepositoryQuickPickItem>({ | |||
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.ref === undefined || state.counter < 2) { | |||
const includeTags = showTags || state.repos.length === 1; | |||
const items = await this.getBranchesAndOrTags(state.repos, includeTags); | |||
const step = this.createStep<ReferencesQuickPickItem>({ | |||
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`, | |||
items: items, | |||
selectedItems: state.ref ? items.filter(ref => ref.label === state.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`; | |||
quickpick.buttons = [QuickInputButtons.Back]; | |||
quickpick.items = await this.getBranchesAndOrTags(state.repos!, showTags); | |||
quickpick.busy = false; | |||
quickpick.enabled = true; | |||
} | |||
}); | |||
const selection = yield step; | |||
if (!this.canMoveNext(step, state, selection)) { | |||
if (oneRepo) { | |||
break; | |||
} | |||
continue; | |||
} | |||
state.ref = selection[0].item.ref; | |||
} | |||
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 ${ | |||
state.repos.length === 1 | |||
? state.repos[0].formattedName | |||
: `${state.repos.length} repositories` | |||
} to ${state.ref}` | |||
} | |||
] | |||
); | |||
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; | |||
} | |||
} | |||
} | |||
} |
@ -0,0 +1,169 @@ | |||
'use strict'; | |||
/* eslint-disable no-loop-func */ | |||
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 { BranchQuickPickItem, CommitQuickPickItem, RepositoryQuickPickItem } from '../../quickpicks'; | |||
import { runGitCommandInTerminal } from '../../terminal'; | |||
interface State { | |||
repo: Repository; | |||
destination: GitBranch; | |||
source: GitBranch; | |||
commits: GitLogCommit[]; | |||
} | |||
export class CherryPickQuickCommand extends GitCommandBase { | |||
constructor() { | |||
super('cherry-pick', 'Cherry Pick'); | |||
} | |||
execute(state: State) { | |||
// 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); | |||
} | |||
async *steps(): AsyncIterableIterator<QuickPickStep> { | |||
const state: Partial<State> & { counter: number } = { counter: 0 }; | |||
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.createStep<RepositoryQuickPickItem>({ | |||
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.createStep<BranchQuickPickItem>({ | |||
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, { | |||
filterBranches: b => b.id !== destId | |||
}) | |||
}); | |||
const selection = yield step; | |||
if (!this.canMoveNext(step, state, selection)) { | |||
if (oneRepo) { | |||
break; | |||
} | |||
continue; | |||
} | |||
state.source = selection[0].item; | |||
} | |||
if (state.commits === undefined || state.counter < 3) { | |||
const log = await Container.git.getLog(state.source.repoPath, { | |||
ref: `${state.destination.ref}..${state.source.ref}`, | |||
merges: false | |||
}); | |||
const step = this.createStep<CommitQuickPickItem>({ | |||
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}`, | |||
[ | |||
{ | |||
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}` | |||
} | |||
] | |||
); | |||
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; | |||
} | |||
} | |||
} | |||
} |
@ -0,0 +1,125 @@ | |||
'use strict'; | |||
import { QuickPickItem } from 'vscode'; | |||
import { Container } from '../../container'; | |||
import { Repository } from '../../git/gitService'; | |||
import { CommandAbortError, QuickCommandBase, QuickPickStep } from './quickCommand'; | |||
import { RepositoryQuickPickItem } from '../../quickpicks'; | |||
import { Strings } from '../../system'; | |||
import { GlyphChars } from '../../constants'; | |||
interface State { | |||
repos: Repository[]; | |||
flags: string[]; | |||
} | |||
export class FetchQuickCommand extends QuickCommandBase { | |||
constructor() { | |||
super('fetch', 'Fetch'); | |||
} | |||
execute(state: State) { | |||
return Container.git.fetchAll(state.repos, { | |||
all: state.flags.includes('--all'), | |||
prune: state.flags.includes('--prune') | |||
}); | |||
} | |||
async *steps(): AsyncIterableIterator<QuickPickStep> { | |||
const state: Partial<State> & { counter: number } = { counter: 0 }; | |||
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.createStep<RepositoryQuickPickItem>({ | |||
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 | |||
}) | |||
) | |||
) | |||
}); | |||
const selection = yield step; | |||
if (!this.canMoveNext(step, state, selection)) { | |||
break; | |||
} | |||
state.repos = selection.map(i => i.item); | |||
} | |||
} | |||
const step = this.createConfirmStep<QuickPickItem & { item: string[] }>( | |||
`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; | |||
} | |||
} | |||
} | |||
} |
@ -0,0 +1,88 @@ | |||
'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<GitBranch[], GitTag[] | undefined>([ | |||
repo.getBranches({ filter: filterBranches, sort: true }), | |||
includeTags ? repo.getTags({ filter: filterTags, includeRefs: true, sort: true }) : undefined | |||
]); | |||
} | |||
else { | |||
const [branchesByRepo, tagsByRepo] = await Promise.all<GitBranch[][], GitTag[][] | undefined>([ | |||
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<BranchQuickPickItem | TagQuickPickItem>([ | |||
...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' | |||
}) | |||
) | |||
]); | |||
} | |||
} |
@ -0,0 +1,165 @@ | |||
'use strict'; | |||
import { QuickPickItem } from 'vscode'; | |||
import { Container } from '../../container'; | |||
import { GitBranch, Repository } from '../../git/gitService'; | |||
import { GlyphChars } from '../../constants'; | |||
import { CommandAbortError, QuickPickStep } from './quickCommand'; | |||
import { BranchQuickPickItem, RepositoryQuickPickItem } from '../../quickpicks'; | |||
import { Strings } from '../../system'; | |||
import { GitCommandBase } from './gitCommand'; | |||
import { runGitCommandInTerminal } from '../../terminal'; | |||
interface State { | |||
repo: Repository; | |||
destination: GitBranch; | |||
source: GitBranch; | |||
flags: string[]; | |||
} | |||
export class MergeQuickCommand extends GitCommandBase { | |||
constructor() { | |||
super('merge', 'Merge'); | |||
} | |||
execute(state: State) { | |||
runGitCommandInTerminal('merge', [...state.flags, state.source.ref].join(' '), state.repo.path); | |||
} | |||
async *steps(): AsyncIterableIterator<QuickPickStep> { | |||
const state: Partial<State> & { counter: number } = { counter: 0 }; | |||
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.createStep<RepositoryQuickPickItem>({ | |||
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.createStep<BranchQuickPickItem>({ | |||
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, { | |||
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<QuickPickItem & { item: string[] }>( | |||
`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; | |||
} | |||
} | |||
} | |||
} |
@ -0,0 +1,112 @@ | |||
'use strict'; | |||
import { QuickPickItem } from 'vscode'; | |||
import { Container } from '../../container'; | |||
import { Repository } from '../../git/gitService'; | |||
import { CommandAbortError, QuickCommandBase, QuickPickStep } from './quickCommand'; | |||
import { RepositoryQuickPickItem } from '../../quickpicks'; | |||
import { Strings } from '../../system'; | |||
import { GlyphChars } from '../../constants'; | |||
interface State { | |||
repos: Repository[]; | |||
flags: string[]; | |||
} | |||
export class PullQuickCommand extends QuickCommandBase { | |||
constructor() { | |||
super('pull', 'Pull'); | |||
} | |||
execute(state: State) { | |||
return Container.git.pullAll(state.repos, { rebase: state.flags.includes('--rebase') }); | |||
} | |||
async *steps(): AsyncIterableIterator<QuickPickStep> { | |||
const state: Partial<State> & { counter: number } = { counter: 0 }; | |||
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.createStep<RepositoryQuickPickItem>({ | |||
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 | |||
}) | |||
) | |||
) | |||
}); | |||
const selection = yield step; | |||
if (!this.canMoveNext(step, state, selection)) { | |||
break; | |||
} | |||
state.repos = selection.map(i => i.item); | |||
} | |||
} | |||
const step = this.createConfirmStep<QuickPickItem & { item: string[] }>( | |||
`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; | |||
} | |||
} | |||
} | |||
} |
@ -0,0 +1,97 @@ | |||
'use strict'; | |||
import { Container } from '../../container'; | |||
import { Repository } from '../../git/gitService'; | |||
import { CommandAbortError, QuickCommandBase, QuickPickStep } from './quickCommand'; | |||
import { RepositoryQuickPickItem } from '../../quickpicks'; | |||
import { Strings } from '../../system'; | |||
import { GlyphChars } from '../../constants'; | |||
interface State { | |||
repos: Repository[]; | |||
} | |||
export class PushQuickCommand extends QuickCommandBase { | |||
constructor() { | |||
super('push', 'Push'); | |||
} | |||
execute(state: State) { | |||
return Container.git.pushAll(state.repos); | |||
} | |||
async *steps(): AsyncIterableIterator<QuickPickStep> { | |||
const state: Partial<State> & { counter: number } = { counter: 0 }; | |||
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.createStep<RepositoryQuickPickItem>({ | |||
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 | |||
}) | |||
) | |||
) | |||
}); | |||
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 push ${ | |||
state.repos.length === 1 | |||
? state.repos[0].formattedName | |||
: `${state.repos.length} repositories` | |||
}` | |||
} | |||
] | |||
); | |||
const selection = yield step; | |||
if (!this.canMoveNext(step, state, selection)) { | |||
if (oneRepo) { | |||
break; | |||
} | |||
continue; | |||
} | |||
this.execute(state as State); | |||
break; | |||
} | |||
catch (ex) { | |||
if (ex instanceof CommandAbortError) break; | |||
throw ex; | |||
} | |||
} | |||
} | |||
} |
@ -0,0 +1,113 @@ | |||
'use strict'; | |||
import { QuickInputButton, QuickPick, QuickPickItem } from 'vscode'; | |||
export interface QuickPickStep<T extends QuickPickItem = any> { | |||
buttons?: QuickInputButton[]; | |||
selectedItems?: QuickPickItem[]; | |||
items: QuickPickItem[]; | |||
multiselect?: boolean; | |||
placeholder?: string; | |||
title?: string; | |||
onDidClickButton?(quickpick: QuickPick<T>, button: QuickInputButton): void; | |||
validate?(selection: T[]): boolean; | |||
} | |||
export class CommandAbortError extends Error { | |||
constructor() { | |||
super('Abort'); | |||
} | |||
} | |||
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 | undefined; | |||
private _stepsIterator: AsyncIterableIterator<QuickPickStep> | undefined; | |||
constructor( | |||
public readonly label: string, | |||
public readonly title: string, | |||
options: { | |||
description?: string; | |||
detail?: string; | |||
} = {} | |||
) { | |||
this.description = options.description; | |||
this.detail = options.detail; | |||
} | |||
abstract steps(): AsyncIterableIterator<QuickPickStep>; | |||
async previous(): Promise<QuickPickStep | undefined> { | |||
// Simulate going back, by having no selection | |||
return (await this.next([])).value; | |||
} | |||
async next(selection?: QuickPickItem[]): Promise<IteratorResult<QuickPickStep>> { | |||
if (this._stepsIterator === undefined) { | |||
this._stepsIterator = this.steps(); | |||
} | |||
const result = await this._stepsIterator.next(selection); | |||
this._current = result.value; | |||
if (result.done) { | |||
this._stepsIterator = undefined; | |||
} | |||
return result; | |||
} | |||
get value(): QuickPickStep | undefined { | |||
return this._current; | |||
} | |||
protected createConfirmStep<T extends QuickPickItem>( | |||
title: string, | |||
confirmations: T[], | |||
cancellable: boolean = true | |||
): QuickPickStep<T> { | |||
return this.createStep<T>({ | |||
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 createStep<T extends QuickPickItem>(step: QuickPickStep<T>): QuickPickStep<T> { | |||
return step; | |||
} | |||
protected canMoveNext<T extends QuickPickItem>( | |||
step: QuickPickStep<T>, | |||
state: { counter: number }, | |||
selection: T[] | undefined | |||
): selection is T[] { | |||
if (selection === undefined || selection.length === 0) { | |||
state.counter--; | |||
if (state.counter < 0) { | |||
state.counter = 0; | |||
} | |||
return false; | |||
} | |||
if (step.validate === undefined || step.validate(selection)) { | |||
state.counter++; | |||
return true; | |||
} | |||
return false; | |||
} | |||
} |
@ -0,0 +1,157 @@ | |||
'use strict'; | |||
import { QuickPickItem } from 'vscode'; | |||
import { Container } from '../../container'; | |||
import { GitBranch, Repository } from '../../git/gitService'; | |||
import { GlyphChars } from '../../constants'; | |||
import { CommandAbortError, QuickPickStep } from './quickCommand'; | |||
import { BranchQuickPickItem, RepositoryQuickPickItem } from '../../quickpicks'; | |||
import { Strings } from '../../system'; | |||
import { GitCommandBase } from './gitCommand'; | |||
import { runGitCommandInTerminal } from '../../terminal'; | |||
interface State { | |||
repo: Repository; | |||
destination: GitBranch; | |||
source: GitBranch; | |||
flags: string[]; | |||
} | |||
export class RebaseQuickCommand extends GitCommandBase { | |||
constructor() { | |||
super('rebase', 'Rebase'); | |||
} | |||
execute(state: State) { | |||
runGitCommandInTerminal('rebase', [...state.flags, state.source.ref].join(' '), state.repo.path); | |||
} | |||
async *steps(): AsyncIterableIterator<QuickPickStep> { | |||
const state: Partial<State> & { counter: number } = { counter: 0 }; | |||
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.createStep<RepositoryQuickPickItem>({ | |||
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.createStep<BranchQuickPickItem>({ | |||
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, { | |||
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<QuickPickItem & { item: string[] }>( | |||
`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; | |||
} | |||
} | |||
} | |||
} |