Skip to content

Commit e08e553

Browse files
committed
convert cherry pick into git cmd
1 parent 00a56b2 commit e08e553

File tree

7 files changed

+114
-55
lines changed

7 files changed

+114
-55
lines changed

src/commands/git/cherry-pick.ts

+25-13
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ import type { GitLog } from '../../git/models/log';
44
import type { GitReference } from '../../git/models/reference';
55
import { createRevisionRange, getReferenceLabel, isRevisionReference } from '../../git/models/reference';
66
import type { Repository } from '../../git/models/repository';
7+
import { showGenericErrorMessage } from '../../messages';
78
import type { FlagsQuickPickItem } from '../../quickpicks/items/flags';
89
import { createFlagsQuickPickItem } from '../../quickpicks/items/flags';
10+
import { Logger } from '../../system/logger';
911
import type { ViewsWithRepositoryFolders } from '../../views/viewBase';
1012
import type {
1113
PartialStepState,
@@ -29,12 +31,15 @@ interface Context {
2931
title: string;
3032
}
3133

32-
type Flags = '--edit' | '--no-commit';
34+
type CherryPickOptions = {
35+
noCommit?: boolean;
36+
edit?: boolean;
37+
};
3338

3439
interface State<Refs = GitReference | GitReference[]> {
3540
repo: string | Repository;
3641
references: Refs;
37-
flags: Flags[];
42+
options: CherryPickOptions;
3843
}
3944

4045
export interface CherryPickGitCommandArgs {
@@ -80,8 +85,15 @@ export class CherryPickGitCommand extends QuickCommand<State> {
8085
return false;
8186
}
8287

83-
execute(state: CherryPickStepState<State<GitReference[]>>) {
84-
state.repo.cherryPick(...state.flags, ...state.references.map(c => c.ref).reverse());
88+
async execute(state: CherryPickStepState<State<GitReference[]>>) {
89+
for (const ref of state.references.map(c => c.ref).reverse()) {
90+
try {
91+
await state.repo.git.cherryPick(ref, state.options);
92+
} catch (ex) {
93+
Logger.error(ex, this.title);
94+
void showGenericErrorMessage(ex.message);
95+
}
96+
}
8597
}
8698

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

102-
if (state.flags == null) {
103-
state.flags = [];
114+
if (state.options == null) {
115+
state.options = {};
104116
}
105117

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

224-
state.flags = result;
236+
state.options = Object.assign({}, ...result);
225237
}
226238

227239
endSteps(state);
228-
this.execute(state as CherryPickStepState<State<GitReference[]>>);
240+
await this.execute(state as CherryPickStepState<State<GitReference[]>>);
229241
}
230242

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

234-
private *confirmStep(state: CherryPickStepState, context: Context): StepResultGenerator<Flags[]> {
235-
const step: QuickPickStep<FlagsQuickPickItem<Flags>> = createConfirmStep(
246+
private *confirmStep(state: CherryPickStepState, context: Context): StepResultGenerator<CherryPickOptions[]> {
247+
const step: QuickPickStep<FlagsQuickPickItem<CherryPickOptions>> = createConfirmStep(
236248
appendReposToTitle(`Confirm ${context.title}`, state, context),
237249
[
238-
createFlagsQuickPickItem<Flags>(state.flags, [], {
250+
createFlagsQuickPickItem<CherryPickOptions>([], [], {
239251
label: this.title,
240252
detail: `Will apply ${getReferenceLabel(state.references, { label: false })} to ${getReferenceLabel(
241253
context.destination,
242254
{ label: false },
243255
)}`,
244256
}),
245-
createFlagsQuickPickItem<Flags>(state.flags, ['--edit'], {
257+
createFlagsQuickPickItem<CherryPickOptions>([], [{ edit: true }], {
246258
label: `${this.title} & Edit`,
247259
description: '--edit',
248260
detail: `Will edit and apply ${getReferenceLabel(state.references, {
249261
label: false,
250262
})} to ${getReferenceLabel(context.destination, { label: false })}`,
251263
}),
252-
createFlagsQuickPickItem<Flags>(state.flags, ['--no-commit'], {
264+
createFlagsQuickPickItem<CherryPickOptions>([], [{ noCommit: true }], {
253265
label: `${this.title} without Committing`,
254266
description: '--no-commit',
255267
detail: `Will apply ${getReferenceLabel(state.references, { label: false })} to ${getReferenceLabel(

src/env/node/git/git.ts

+15-17
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ export const GitErrors = {
7878
changesWouldBeOverwritten: /Your local changes to the following files would be overwritten/i,
7979
commitChangesFirst: /Please, commit your changes before you can/i,
8080
conflict: /^CONFLICT \([^)]+\): \b/m,
81+
cherryPickUnmerged:
82+
/error: Cherry-picking.*unmerged files\.\nhint:.*\nhint:.*make a commit\.\nfatal: cherry-pick failed/i,
8183
failedToDeleteDirectoryNotEmpty: /failed to delete '(.*?)': Directory not empty/i,
8284
invalidObjectName: /invalid object name: (.*)\s/i,
8385
invalidObjectNameList: /could not open object name list: (.*)\s/i,
@@ -165,6 +167,12 @@ function getStdinUniqueKey(): number {
165167
type ExitCodeOnlyGitCommandOptions = GitCommandOptions & { exitCodeOnly: true };
166168
export type PushForceOptions = { withLease: true; ifIncludes?: boolean } | { withLease: false; ifIncludes?: never };
167169

170+
const cherryPickErrorAndReason: [RegExp, CherryPickErrorReason][] = [
171+
[GitErrors.changesWouldBeOverwritten, CherryPickErrorReason.AbortedWouldOverwrite],
172+
[GitErrors.conflict, CherryPickErrorReason.Conflicts],
173+
[GitErrors.cherryPickUnmerged, CherryPickErrorReason.Conflicts],
174+
];
175+
168176
const tagErrorAndReason: [RegExp, TagErrorReason][] = [
169177
[GitErrors.tagAlreadyExists, TagErrorReason.TagAlreadyExists],
170178
[GitErrors.tagNotFound, TagErrorReason.TagNotFound],
@@ -617,28 +625,18 @@ export class Git {
617625
return this.git<string>({ cwd: repoPath }, ...params);
618626
}
619627

620-
async cherrypick(repoPath: string, sha: string, options: { noCommit?: boolean; errors?: GitErrorHandling } = {}) {
621-
const params = ['cherry-pick'];
622-
if (options?.noCommit) {
623-
params.push('-n');
624-
}
625-
params.push(sha);
626-
628+
async cherryPick(repoPath: string, options: { errors?: GitErrorHandling } = {}, args: string[]) {
627629
try {
628-
await this.git<string>({ cwd: repoPath, errors: options?.errors }, ...params);
630+
await this.git<string>({ cwd: repoPath, errors: options?.errors }, 'cherry-pick', ...args);
629631
} catch (ex) {
630632
const msg: string = ex?.toString() ?? '';
631-
let reason: CherryPickErrorReason = CherryPickErrorReason.Other;
632-
if (
633-
GitErrors.changesWouldBeOverwritten.test(msg) ||
634-
GitErrors.changesWouldBeOverwritten.test(ex.stderr ?? '')
635-
) {
636-
reason = CherryPickErrorReason.AbortedWouldOverwrite;
637-
} else if (GitErrors.conflict.test(msg) || GitErrors.conflict.test(ex.stdout ?? '')) {
638-
reason = CherryPickErrorReason.Conflicts;
633+
for (const [error, reason] of cherryPickErrorAndReason) {
634+
if (error.test(msg) || error.test(ex.stderr ?? '')) {
635+
throw new CherryPickError(reason, ex);
636+
}
639637
}
640638

641-
throw new CherryPickError(reason, ex, sha);
639+
throw new CherryPickError(CherryPickErrorReason.Other, ex);
642640
}
643641
}
644642

src/env/node/git/localGitProvider.ts

+29-1
Original file line numberDiff line numberDiff line change
@@ -1097,6 +1097,34 @@ export class LocalGitProvider implements GitProvider, Disposable {
10971097
this.container.events.fire('git:cache:reset', { repoPath: repoPath, caches: ['remotes'] });
10981098
}
10991099

1100+
@log()
1101+
async cherryPick(
1102+
repoPath: string,
1103+
ref: string,
1104+
options: { noCommit?: boolean; edit?: boolean; errors?: GitErrorHandling },
1105+
): Promise<void> {
1106+
const args: string[] = [];
1107+
if (options?.noCommit) {
1108+
args.push('-n');
1109+
}
1110+
1111+
if (options?.edit) {
1112+
args.push('-e');
1113+
}
1114+
1115+
args.push(ref);
1116+
1117+
try {
1118+
await this.git.cherryPick(repoPath, undefined, args);
1119+
} catch (ex) {
1120+
if (ex instanceof CherryPickError) {
1121+
throw ex.WithRef(ref);
1122+
}
1123+
1124+
throw ex;
1125+
}
1126+
}
1127+
11001128
@log()
11011129
async applyChangesToWorkingFile(uri: GitUri, ref1?: string, ref2?: string) {
11021130
const scope = getLogScope();
@@ -1246,7 +1274,7 @@ export class LocalGitProvider implements GitProvider, Disposable {
12461274

12471275
// Apply the patch using a cherry pick without committing
12481276
try {
1249-
await this.git.cherrypick(targetPath, ref, { noCommit: true, errors: GitErrorHandling.Throw });
1277+
await this.git.cherryPick(targetPath, undefined, [ref, '--no-commit']);
12501278
} catch (ex) {
12511279
Logger.error(ex, scope);
12521280
if (ex instanceof CherryPickError) {

src/git/errors.ts

+31-19
Original file line numberDiff line numberDiff line change
@@ -364,37 +364,49 @@ export class CherryPickError extends Error {
364364

365365
readonly original?: Error;
366366
readonly reason: CherryPickErrorReason | undefined;
367+
ref?: string;
368+
369+
private static buildCherryPickErrorMessage(reason: CherryPickErrorReason | undefined, ref?: string) {
370+
let baseMessage = `Unable to cherry-pick${ref ? ` commit '${ref}'` : ''}`;
371+
switch (reason) {
372+
case CherryPickErrorReason.AbortedWouldOverwrite:
373+
baseMessage += ' as some local changes would be overwritten';
374+
break;
375+
case CherryPickErrorReason.Conflicts:
376+
baseMessage += ' due to conflicts';
377+
break;
378+
}
379+
return baseMessage;
380+
}
367381

368382
constructor(reason?: CherryPickErrorReason, original?: Error, sha?: string);
369383
constructor(message?: string, original?: Error);
370-
constructor(messageOrReason: string | CherryPickErrorReason | undefined, original?: Error, sha?: string) {
371-
let message;
372-
const baseMessage = `Unable to cherry-pick${sha ? ` commit '${sha}'` : ''}`;
384+
constructor(messageOrReason: string | CherryPickErrorReason | undefined, original?: Error, ref?: string) {
373385
let reason: CherryPickErrorReason | undefined;
374-
if (messageOrReason == null) {
375-
message = baseMessage;
376-
} else if (typeof messageOrReason === 'string') {
377-
message = messageOrReason;
378-
reason = undefined;
386+
if (typeof messageOrReason !== 'string') {
387+
reason = messageOrReason as CherryPickErrorReason;
379388
} else {
380-
reason = messageOrReason;
381-
switch (reason) {
382-
case CherryPickErrorReason.AbortedWouldOverwrite:
383-
message = `${baseMessage} as some local changes would be overwritten.`;
384-
break;
385-
case CherryPickErrorReason.Conflicts:
386-
message = `${baseMessage} due to conflicts.`;
387-
break;
388-
default:
389-
message = baseMessage;
390-
}
389+
super(messageOrReason);
391390
}
391+
392+
const message =
393+
typeof messageOrReason === 'string'
394+
? messageOrReason
395+
: CherryPickError.buildCherryPickErrorMessage(messageOrReason as CherryPickErrorReason, ref);
392396
super(message);
393397

394398
this.original = original;
395399
this.reason = reason;
400+
this.ref = ref;
401+
396402
Error.captureStackTrace?.(this, CherryPickError);
397403
}
404+
405+
WithRef(ref: string) {
406+
this.ref = ref;
407+
this.message = CherryPickError.buildCherryPickErrorMessage(this.reason, ref);
408+
return this;
409+
}
398410
}
399411

400412
export class WorkspaceUntrustedError extends Error {

src/git/gitProvider.ts

+2
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@ export interface GitProviderRepository {
126126
pruneRemote?(repoPath: string, name: string): Promise<void>;
127127
removeRemote?(repoPath: string, name: string): Promise<void>;
128128

129+
cherryPick?(repoPath: string, ref: string, options: { noCommit?: boolean; edit?: boolean }): Promise<void>;
130+
129131
applyUnreachableCommitForPatch?(
130132
repoPath: string,
131133
ref: string,

src/git/gitProviderService.ts

+12
Original file line numberDiff line numberDiff line change
@@ -1334,6 +1334,18 @@ export class GitProviderService implements Disposable {
13341334
return provider.removeRemote(path, name);
13351335
}
13361336

1337+
@log()
1338+
cherryPick(
1339+
repoPath: string | Uri,
1340+
ref: string,
1341+
options: { noCommit?: boolean; edit?: boolean } = {},
1342+
): Promise<void> {
1343+
const { provider, path } = this.getProvider(repoPath);
1344+
if (provider.cherryPick == null) throw new ProviderNotSupportedError(provider.descriptor.name);
1345+
1346+
return provider.cherryPick(path, ref, options);
1347+
}
1348+
13371349
@log()
13381350
applyChangesToWorkingFile(uri: GitUri, ref1?: string, ref2?: string): Promise<void> {
13391351
const { provider } = this.getProvider(uri);

src/git/models/repository.ts

-5
Original file line numberDiff line numberDiff line change
@@ -634,11 +634,6 @@ export class Repository implements Disposable {
634634
}
635635
}
636636

637-
@log()
638-
cherryPick(...args: string[]) {
639-
void this.runTerminalCommand('cherry-pick', ...args);
640-
}
641-
642637
containsUri(uri: Uri) {
643638
return this === this.container.git.getRepository(uri);
644639
}

0 commit comments

Comments
 (0)