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