238 lines
6.9 KiB

'use strict';
import { Container } from '../../container';
import { GitBranch, GitLog, GitReference, GitRevision, Repository } from '../../git/git';
import { FlagsQuickPickItem } from '../../quickpicks';
import { ViewsWithRepositoryFolders } from '../../views/viewBase';
import {
appendReposToTitle,
PartialStepState,
pickBranchOrTagStep,
pickCommitsStep,
pickRepositoryStep,
QuickCommand,
QuickPickStep,
StepGenerator,
StepResult,
StepResultGenerator,
StepSelection,
StepState,
} from '../quickCommand';
interface Context {
repos: Repository[];
associatedView: ViewsWithRepositoryFolders;
cache: Map<string, Promise<GitLog | undefined>>;
destination: GitBranch;
selectedBranchOrTag: GitReference | undefined;
showTags: boolean;
title: string;
}
type Flags = '--edit' | '--no-commit';
interface State<Refs = GitReference | GitReference[]> {
repo: string | Repository;
references: Refs;
flags: Flags[];
}
export interface CherryPickGitCommandArgs {
readonly command: 'cherry-pick';
state?: Partial<State>;
}
type CherryPickStepState<T extends State = State> = ExcludeSome<StepState<T>, 'repo', string>;
export class CherryPickGitCommand extends QuickCommand<State> {
constructor(args?: CherryPickGitCommandArgs) {
super('cherry-pick', 'cherry-pick', 'Cherry Pick', {
description: 'integrates changes from specified commits into the current branch',
});
let counter = 0;
if (args?.state?.repo != null) {
counter++;
}
if (
args?.state?.references != null &&
(!Array.isArray(args.state.references) || args.state.references.length !== 0)
) {
counter++;
}
this.initialState = {
counter: counter,
confirm: true,
...args?.state,
};
}
override get canSkipConfirm(): boolean {
return false;
}
execute(state: CherryPickStepState<State<GitReference[]>>) {
state.repo.cherryPick(...state.flags, ...state.references.map(c => c.ref).reverse());
}
override isFuzzyMatch(name: string) {
return super.isFuzzyMatch(name) || name === 'cherry';
}
protected async *steps(state: PartialStepState<State>): StepGenerator {
const context: Context = {
repos: Container.instance.git.openRepositories,
associatedView: Container.instance.commitsView,
cache: new Map<string, Promise<GitLog | undefined>>(),
destination: undefined!,
selectedBranchOrTag: undefined,
showTags: true,
title: this.title,
};
if (state.flags == null) {
state.flags = [];
}
if (state.references != null && !Array.isArray(state.references)) {
state.references = [state.references];
}
let skippedStepOne = false;
while (this.canStepsContinue(state)) {
context.title = this.title;
if (state.counter < 1 || state.repo == null || typeof state.repo === 'string') {
skippedStepOne = false;
if (context.repos.length === 1) {
skippedStepOne = true;
if (state.repo == null) {
state.counter++;
}
state.repo = context.repos[0];
} else {
const result = yield* pickRepositoryStep(state, context);
// Always break on the first step (so we will go back)
if (result === StepResult.Break) break;
state.repo = result;
}
}
if (context.destination == null) {
const branch = await state.repo.getBranch();
if (branch == null) break;
context.destination = branch;
}
context.title = `${this.title} into ${GitReference.toString(context.destination, { icon: false })}`;
if (state.counter < 2 || state.references == null || state.references.length === 0) {
const result: StepResult<GitReference> = yield* pickBranchOrTagStep(
state as CherryPickStepState,
context,
{
filter: { branches: b => b.id !== context.destination.id },
placeholder: context =>
`Choose a branch${context.showTags ? ' or tag' : ''} to cherry-pick from`,
picked: context.selectedBranchOrTag?.ref,
value: context.selectedBranchOrTag == null ? state.references?.[0]?.ref : undefined,
},
);
if (result === StepResult.Break) {
// If we skipped the previous step, make sure we back up past it
if (skippedStepOne) {
state.counter--;
}
continue;
}
if (GitReference.isRevision(result)) {
state.references = [result];
context.selectedBranchOrTag = undefined;
} else {
context.selectedBranchOrTag = result;
}
}
if (state.counter < 3 && context.selectedBranchOrTag != null) {
const ref = GitRevision.createRange(context.destination.ref, context.selectedBranchOrTag.ref);
let log = context.cache.get(ref);
if (log == null) {
log = Container.instance.git.getLog(state.repo.path, { ref: ref, merges: false });
context.cache.set(ref, log);
}
const result: StepResult<GitReference[]> = yield* pickCommitsStep(
state as CherryPickStepState,
context,
{
log: await log,
onDidLoadMore: log => context.cache.set(ref, Promise.resolve(log)),
picked: state.references?.map(r => r.ref),
placeholder: (context, log) =>
log == null
? `No pickable commits found on ${GitReference.toString(context.selectedBranchOrTag, {
icon: false,
})}`
: `Choose commits to cherry-pick into ${GitReference.toString(context.destination, {
icon: false,
})}`,
},
);
if (result === StepResult.Break) continue;
state.references = result;
}
if (this.confirm(state.confirm)) {
const result = yield* this.confirmStep(state as CherryPickStepState, context);
if (result === StepResult.Break) continue;
state.flags = result;
}
QuickCommand.endSteps(state);
this.execute(state as CherryPickStepState<State<GitReference[]>>);
}
return state.counter < 0 ? StepResult.Break : undefined;
}
private *confirmStep(state: CherryPickStepState, context: Context): StepResultGenerator<Flags[]> {
const step: QuickPickStep<FlagsQuickPickItem<Flags>> = QuickCommand.createConfirmStep(
appendReposToTitle(`Confirm ${context.title}`, state, context),
[
FlagsQuickPickItem.create<Flags>(state.flags, [], {
label: this.title,
detail: `Will apply ${GitReference.toString(state.references)} to ${GitReference.toString(
context.destination,
)}`,
}),
FlagsQuickPickItem.create<Flags>(state.flags, ['--edit'], {
label: `${this.title} & Edit`,
description: '--edit',
detail: `Will edit and apply ${GitReference.toString(state.references)} to ${GitReference.toString(
context.destination,
)}`,
}),
FlagsQuickPickItem.create<Flags>(state.flags, ['--no-commit'], {
label: `${this.title} without Committing`,
description: '--no-commit',
detail: `Will apply ${GitReference.toString(state.references)} to ${GitReference.toString(
context.destination,
)} without Committing`,
}),
],
context,
);
const selection: StepSelection<typeof step> = yield step;
return QuickCommand.canPickStepContinue(step, state, selection) ? selection[0].item : StepResult.Break;
}
}