diff --git a/src/commands/git/cherry-pick.ts b/src/commands/git/cherry-pick.ts index ad24cfb94bc30..03adde250002d 100644 --- a/src/commands/git/cherry-pick.ts +++ b/src/commands/git/cherry-pick.ts @@ -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, @@ -29,12 +31,15 @@ interface Context { title: string; } -type Flags = '--edit' | '--no-commit'; +type CherryPickOptions = { + noCommit?: boolean; + edit?: boolean; +}; interface State { repo: string | Repository; references: Refs; - flags: Flags[]; + options: CherryPickOptions; } export interface CherryPickGitCommandArgs { @@ -80,8 +85,15 @@ export class CherryPickGitCommand extends QuickCommand { return false; } - execute(state: CherryPickStepState>) { - state.repo.cherryPick(...state.flags, ...state.references.map(c => c.ref).reverse()); + async execute(state: CherryPickStepState>) { + 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) { @@ -99,8 +111,8 @@ export class CherryPickGitCommand extends QuickCommand { title: this.title, }; - if (state.flags == null) { - state.flags = []; + if (state.options == null) { + state.options = {}; } if (state.references != null && !Array.isArray(state.references)) { @@ -221,35 +233,35 @@ export class CherryPickGitCommand extends QuickCommand { 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>); + await this.execute(state as CherryPickStepState>); } return state.counter < 0 ? StepResultBreak : undefined; } - private *confirmStep(state: CherryPickStepState, context: Context): StepResultGenerator { - const step: QuickPickStep> = createConfirmStep( + private *confirmStep(state: CherryPickStepState, context: Context): StepResultGenerator { + const step: QuickPickStep> = createConfirmStep( appendReposToTitle(`Confirm ${context.title}`, state, context), [ - createFlagsQuickPickItem(state.flags, [], { + createFlagsQuickPickItem([], [], { label: this.title, detail: `Will apply ${getReferenceLabel(state.references, { label: false })} to ${getReferenceLabel( context.destination, { label: false }, )}`, }), - createFlagsQuickPickItem(state.flags, ['--edit'], { + createFlagsQuickPickItem([], [{ 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(state.flags, ['--no-commit'], { + createFlagsQuickPickItem([], [{ noCommit: true }], { label: `${this.title} without Committing`, description: '--no-commit', detail: `Will apply ${getReferenceLabel(state.references, { label: false })} to ${getReferenceLabel( diff --git a/src/env/node/git/git.ts b/src/env/node/git/git.ts index 6f9b571ea868c..28c961213a6fb 100644 --- a/src/env/node/git/git.ts +++ b/src/env/node/git/git.ts @@ -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, @@ -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], @@ -617,28 +625,18 @@ export class Git { return this.git({ 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({ cwd: repoPath, errors: options?.errors }, ...params); + await this.git({ 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); } } diff --git a/src/env/node/git/localGitProvider.ts b/src/env/node/git/localGitProvider.ts index 75f5e9b1cbfd3..f2fd8ce15b1ba 100644 --- a/src/env/node/git/localGitProvider.ts +++ b/src/env/node/git/localGitProvider.ts @@ -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 { + 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(); @@ -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) { diff --git a/src/git/errors.ts b/src/git/errors.ts index e1ef081fdfb25..443847615a15d 100644 --- a/src/git/errors.ts +++ b/src/git/errors.ts @@ -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 { diff --git a/src/git/gitProvider.ts b/src/git/gitProvider.ts index 162279a814baf..fb0288f15238a 100644 --- a/src/git/gitProvider.ts +++ b/src/git/gitProvider.ts @@ -126,6 +126,8 @@ export interface GitProviderRepository { pruneRemote?(repoPath: string, name: string): Promise; removeRemote?(repoPath: string, name: string): Promise; + cherryPick?(repoPath: string, ref: string, options: { noCommit?: boolean; edit?: boolean }): Promise; + applyUnreachableCommitForPatch?( repoPath: string, ref: string, diff --git a/src/git/gitProviderService.ts b/src/git/gitProviderService.ts index 479657d170dd1..484533af774aa 100644 --- a/src/git/gitProviderService.ts +++ b/src/git/gitProviderService.ts @@ -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 { + 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 { const { provider } = this.getProvider(uri); diff --git a/src/git/models/repository.ts b/src/git/models/repository.ts index 49c0a3c4a972c..0c8e321f5d15d 100644 --- a/src/git/models/repository.ts +++ b/src/git/models/repository.ts @@ -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); }