Skip to content

Commit

Permalink
convert cherry pick into git cmd
Browse files Browse the repository at this point in the history
  • Loading branch information
sergiolms committed Dec 12, 2024
1 parent 00a56b2 commit e08e553
Show file tree
Hide file tree
Showing 7 changed files with 114 additions and 55 deletions.
38 changes: 25 additions & 13 deletions src/commands/git/cherry-pick.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import type { GitLog } from '../../git/models/log';
import type { GitReference } from '../../git/models/reference';
import { createRevisionRange, getReferenceLabel, isRevisionReference } from '../../git/models/reference';
import type { Repository } from '../../git/models/repository';
import { showGenericErrorMessage } from '../../messages';
import type { FlagsQuickPickItem } from '../../quickpicks/items/flags';
import { createFlagsQuickPickItem } from '../../quickpicks/items/flags';
import { Logger } from '../../system/logger';
import type { ViewsWithRepositoryFolders } from '../../views/viewBase';
import type {
PartialStepState,
Expand All @@ -29,12 +31,15 @@ interface Context {
title: string;
}

type Flags = '--edit' | '--no-commit';
type CherryPickOptions = {
noCommit?: boolean;
edit?: boolean;
};

interface State<Refs = GitReference | GitReference[]> {
repo: string | Repository;
references: Refs;
flags: Flags[];
options: CherryPickOptions;
}

export interface CherryPickGitCommandArgs {
Expand Down Expand Up @@ -80,8 +85,15 @@ export class CherryPickGitCommand extends QuickCommand<State> {
return false;
}

execute(state: CherryPickStepState<State<GitReference[]>>) {
state.repo.cherryPick(...state.flags, ...state.references.map(c => c.ref).reverse());
async execute(state: CherryPickStepState<State<GitReference[]>>) {
for (const ref of state.references.map(c => c.ref).reverse()) {
try {
await state.repo.git.cherryPick(ref, state.options);
} catch (ex) {
Logger.error(ex, this.title);
void showGenericErrorMessage(ex.message);
}
}
}

override isFuzzyMatch(name: string) {
Expand All @@ -99,8 +111,8 @@ export class CherryPickGitCommand extends QuickCommand<State> {
title: this.title,
};

if (state.flags == null) {
state.flags = [];
if (state.options == null) {
state.options = {};
}

if (state.references != null && !Array.isArray(state.references)) {
Expand Down Expand Up @@ -221,35 +233,35 @@ export class CherryPickGitCommand extends QuickCommand<State> {
const result = yield* this.confirmStep(state as CherryPickStepState, context);
if (result === StepResultBreak) continue;

state.flags = result;
state.options = Object.assign({}, ...result);
}

endSteps(state);
this.execute(state as CherryPickStepState<State<GitReference[]>>);
await this.execute(state as CherryPickStepState<State<GitReference[]>>);
}

return state.counter < 0 ? StepResultBreak : undefined;
}

private *confirmStep(state: CherryPickStepState, context: Context): StepResultGenerator<Flags[]> {
const step: QuickPickStep<FlagsQuickPickItem<Flags>> = createConfirmStep(
private *confirmStep(state: CherryPickStepState, context: Context): StepResultGenerator<CherryPickOptions[]> {
const step: QuickPickStep<FlagsQuickPickItem<CherryPickOptions>> = createConfirmStep(
appendReposToTitle(`Confirm ${context.title}`, state, context),
[
createFlagsQuickPickItem<Flags>(state.flags, [], {
createFlagsQuickPickItem<CherryPickOptions>([], [], {
label: this.title,
detail: `Will apply ${getReferenceLabel(state.references, { label: false })} to ${getReferenceLabel(
context.destination,
{ label: false },
)}`,
}),
createFlagsQuickPickItem<Flags>(state.flags, ['--edit'], {
createFlagsQuickPickItem<CherryPickOptions>([], [{ edit: true }], {
label: `${this.title} & Edit`,
description: '--edit',
detail: `Will edit and apply ${getReferenceLabel(state.references, {
label: false,
})} to ${getReferenceLabel(context.destination, { label: false })}`,
}),
createFlagsQuickPickItem<Flags>(state.flags, ['--no-commit'], {
createFlagsQuickPickItem<CherryPickOptions>([], [{ noCommit: true }], {
label: `${this.title} without Committing`,
description: '--no-commit',
detail: `Will apply ${getReferenceLabel(state.references, { label: false })} to ${getReferenceLabel(
Expand Down
32 changes: 15 additions & 17 deletions src/env/node/git/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ export const GitErrors = {
changesWouldBeOverwritten: /Your local changes to the following files would be overwritten/i,
commitChangesFirst: /Please, commit your changes before you can/i,
conflict: /^CONFLICT \([^)]+\): \b/m,
cherryPickUnmerged:
/error: Cherry-picking.*unmerged files\.\nhint:.*\nhint:.*make a commit\.\nfatal: cherry-pick failed/i,
failedToDeleteDirectoryNotEmpty: /failed to delete '(.*?)': Directory not empty/i,
invalidObjectName: /invalid object name: (.*)\s/i,
invalidObjectNameList: /could not open object name list: (.*)\s/i,
Expand Down Expand Up @@ -165,6 +167,12 @@ function getStdinUniqueKey(): number {
type ExitCodeOnlyGitCommandOptions = GitCommandOptions & { exitCodeOnly: true };
export type PushForceOptions = { withLease: true; ifIncludes?: boolean } | { withLease: false; ifIncludes?: never };

const cherryPickErrorAndReason: [RegExp, CherryPickErrorReason][] = [
[GitErrors.changesWouldBeOverwritten, CherryPickErrorReason.AbortedWouldOverwrite],
[GitErrors.conflict, CherryPickErrorReason.Conflicts],
[GitErrors.cherryPickUnmerged, CherryPickErrorReason.Conflicts],
];

const tagErrorAndReason: [RegExp, TagErrorReason][] = [
[GitErrors.tagAlreadyExists, TagErrorReason.TagAlreadyExists],
[GitErrors.tagNotFound, TagErrorReason.TagNotFound],
Expand Down Expand Up @@ -617,28 +625,18 @@ export class Git {
return this.git<string>({ cwd: repoPath }, ...params);
}

async cherrypick(repoPath: string, sha: string, options: { noCommit?: boolean; errors?: GitErrorHandling } = {}) {
const params = ['cherry-pick'];
if (options?.noCommit) {
params.push('-n');
}
params.push(sha);

async cherryPick(repoPath: string, options: { errors?: GitErrorHandling } = {}, args: string[]) {
try {
await this.git<string>({ cwd: repoPath, errors: options?.errors }, ...params);
await this.git<string>({ cwd: repoPath, errors: options?.errors }, 'cherry-pick', ...args);
} catch (ex) {
const msg: string = ex?.toString() ?? '';
let reason: CherryPickErrorReason = CherryPickErrorReason.Other;
if (
GitErrors.changesWouldBeOverwritten.test(msg) ||
GitErrors.changesWouldBeOverwritten.test(ex.stderr ?? '')
) {
reason = CherryPickErrorReason.AbortedWouldOverwrite;
} else if (GitErrors.conflict.test(msg) || GitErrors.conflict.test(ex.stdout ?? '')) {
reason = CherryPickErrorReason.Conflicts;
for (const [error, reason] of cherryPickErrorAndReason) {
if (error.test(msg) || error.test(ex.stderr ?? '')) {
throw new CherryPickError(reason, ex);
}
}

throw new CherryPickError(reason, ex, sha);
throw new CherryPickError(CherryPickErrorReason.Other, ex);
}
}

Expand Down
30 changes: 29 additions & 1 deletion src/env/node/git/localGitProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1097,6 +1097,34 @@ export class LocalGitProvider implements GitProvider, Disposable {
this.container.events.fire('git:cache:reset', { repoPath: repoPath, caches: ['remotes'] });
}

@log()
async cherryPick(
repoPath: string,
ref: string,
options: { noCommit?: boolean; edit?: boolean; errors?: GitErrorHandling },
): Promise<void> {
const args: string[] = [];
if (options?.noCommit) {
args.push('-n');
}

if (options?.edit) {
args.push('-e');
}

args.push(ref);

try {
await this.git.cherryPick(repoPath, undefined, args);
} catch (ex) {
if (ex instanceof CherryPickError) {
throw ex.WithRef(ref);
}

throw ex;
}
}

@log()
async applyChangesToWorkingFile(uri: GitUri, ref1?: string, ref2?: string) {
const scope = getLogScope();
Expand Down Expand Up @@ -1246,7 +1274,7 @@ export class LocalGitProvider implements GitProvider, Disposable {

// Apply the patch using a cherry pick without committing
try {
await this.git.cherrypick(targetPath, ref, { noCommit: true, errors: GitErrorHandling.Throw });
await this.git.cherryPick(targetPath, undefined, [ref, '--no-commit']);
} catch (ex) {
Logger.error(ex, scope);
if (ex instanceof CherryPickError) {
Expand Down
50 changes: 31 additions & 19 deletions src/git/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -364,37 +364,49 @@ export class CherryPickError extends Error {

readonly original?: Error;
readonly reason: CherryPickErrorReason | undefined;
ref?: string;

private static buildCherryPickErrorMessage(reason: CherryPickErrorReason | undefined, ref?: string) {
let baseMessage = `Unable to cherry-pick${ref ? ` commit '${ref}'` : ''}`;
switch (reason) {
case CherryPickErrorReason.AbortedWouldOverwrite:
baseMessage += ' as some local changes would be overwritten';
break;
case CherryPickErrorReason.Conflicts:
baseMessage += ' due to conflicts';
break;
}
return baseMessage;
}

constructor(reason?: CherryPickErrorReason, original?: Error, sha?: string);
constructor(message?: string, original?: Error);
constructor(messageOrReason: string | CherryPickErrorReason | undefined, original?: Error, sha?: string) {
let message;
const baseMessage = `Unable to cherry-pick${sha ? ` commit '${sha}'` : ''}`;
constructor(messageOrReason: string | CherryPickErrorReason | undefined, original?: Error, ref?: string) {
let reason: CherryPickErrorReason | undefined;
if (messageOrReason == null) {
message = baseMessage;
} else if (typeof messageOrReason === 'string') {
message = messageOrReason;
reason = undefined;
if (typeof messageOrReason !== 'string') {
reason = messageOrReason as CherryPickErrorReason;
} else {
reason = messageOrReason;
switch (reason) {
case CherryPickErrorReason.AbortedWouldOverwrite:
message = `${baseMessage} as some local changes would be overwritten.`;
break;
case CherryPickErrorReason.Conflicts:
message = `${baseMessage} due to conflicts.`;
break;
default:
message = baseMessage;
}
super(messageOrReason);
}

const message =
typeof messageOrReason === 'string'
? messageOrReason
: CherryPickError.buildCherryPickErrorMessage(messageOrReason as CherryPickErrorReason, ref);
super(message);

this.original = original;
this.reason = reason;
this.ref = ref;

Error.captureStackTrace?.(this, CherryPickError);
}

WithRef(ref: string) {
this.ref = ref;
this.message = CherryPickError.buildCherryPickErrorMessage(this.reason, ref);
return this;
}
}

export class WorkspaceUntrustedError extends Error {
Expand Down
2 changes: 2 additions & 0 deletions src/git/gitProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ export interface GitProviderRepository {
pruneRemote?(repoPath: string, name: string): Promise<void>;
removeRemote?(repoPath: string, name: string): Promise<void>;

cherryPick?(repoPath: string, ref: string, options: { noCommit?: boolean; edit?: boolean }): Promise<void>;

applyUnreachableCommitForPatch?(
repoPath: string,
ref: string,
Expand Down
12 changes: 12 additions & 0 deletions src/git/gitProviderService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1334,6 +1334,18 @@ export class GitProviderService implements Disposable {
return provider.removeRemote(path, name);
}

@log()
cherryPick(
repoPath: string | Uri,
ref: string,
options: { noCommit?: boolean; edit?: boolean } = {},
): Promise<void> {
const { provider, path } = this.getProvider(repoPath);
if (provider.cherryPick == null) throw new ProviderNotSupportedError(provider.descriptor.name);

return provider.cherryPick(path, ref, options);
}

@log()
applyChangesToWorkingFile(uri: GitUri, ref1?: string, ref2?: string): Promise<void> {
const { provider } = this.getProvider(uri);
Expand Down
5 changes: 0 additions & 5 deletions src/git/models/repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -634,11 +634,6 @@ export class Repository implements Disposable {
}
}

@log()
cherryPick(...args: string[]) {
void this.runTerminalCommand('cherry-pick', ...args);
}

containsUri(uri: Uri) {
return this === this.container.git.getRepository(uri);
}
Expand Down

0 comments on commit e08e553

Please sign in to comment.