diff --git a/src/commands/git/cherry-pick.ts b/src/commands/git/cherry-pick.ts index ad24cfb94bc30..a98b7234c5988 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, @@ -80,8 +82,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.flags); + } catch (ex) { + Logger.error(ex, this.title); + void showGenericErrorMessage(ex.message); + } + } } override isFuzzyMatch(name: string) { @@ -225,7 +234,7 @@ export class CherryPickGitCommand extends QuickCommand { } endSteps(state); - this.execute(state as CherryPickStepState>); + await this.execute(state as CherryPickStepState>); } return state.counter < 0 ? StepResultBreak : undefined; diff --git a/src/env/node/git/git.ts b/src/env/node/git/git.ts index f109e990e0532..c4df912709fa7 100644 --- a/src/env/node/git/git.ts +++ b/src/env/node/git/git.ts @@ -78,6 +78,7 @@ 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, + conflictsInWip: /hint: After resolving the conflicts, mark them/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 +166,12 @@ function getStdinUniqueKey(): number { type ExitCodeOnlyGitCommandOptions = GitCommandOptions & { exitCodeOnly: true }; export type PushForceOptions = { withLease: true; ifIncludes?: boolean } | { withLease: false; ifIncludes?: never }; +const cherryPickErrorAndReason = [ + [GitErrors.changesWouldBeOverwritten, CherryPickErrorReason.AbortedWouldOverwrite], + [GitErrors.conflict, CherryPickErrorReason.Conflicts], + [GitErrors.conflictsInWip, CherryPickErrorReason.Conflicts], +]; + const tagErrorAndReason: [RegExp, TagErrorReason][] = [ [GitErrors.tagAlreadyExists, TagErrorReason.TagAlreadyExists], [GitErrors.tagNotFound, TagErrorReason.TagNotFound], @@ -617,28 +624,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, args: string[]) { try { - await this.git({ cwd: repoPath, errors: options?.errors }, ...params); + await this.git({ cwd: repoPath }, '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 ed099e9b94008..2a9680d84715c 100644 --- a/src/env/node/git/localGitProvider.ts +++ b/src/env/node/git/localGitProvider.ts @@ -1076,6 +1076,30 @@ 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 }): 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, 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(); @@ -1225,7 +1249,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, ref, { noCommit: true, errors: GitErrorHandling.Throw }); } catch (ex) { Logger.error(ex, scope); if (ex instanceof CherryPickError) { diff --git a/src/git/errors.ts b/src/git/errors.ts index e1ef081fdfb25..ae491599a55d1 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 74450e2a2a829..c3021710166b6 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 aeb41768e11f0..3426a1c42ee52 100644 --- a/src/git/gitProviderService.ts +++ b/src/git/gitProviderService.ts @@ -42,6 +42,7 @@ import { registerCommand } from '../system/vscode/command'; import { configuration } from '../system/vscode/configuration'; import { setContext } from '../system/vscode/context'; import { getBestPath } from '../system/vscode/path'; +import type { GitErrorHandling } from './commandOptions'; import type { BranchContributorOverview, GitCaches, @@ -1334,6 +1335,26 @@ export class GitProviderService implements Disposable { return provider.removeRemote(path, name); } + @log() + cherryPick(repoPath: string | Uri, ref: string, flags: string[] | undefined = []): Promise { + const { provider, path } = this.getProvider(repoPath); + if (provider.cherryPick == null) throw new ProviderNotSupportedError(provider.descriptor.name); + + const options: { noCommit?: boolean; edit?: boolean; errors?: GitErrorHandling } = {}; + for (const flag of flags) { + switch (flag) { + case '--no-commit': + options.noCommit = true; + break; + case '--edit': + options.edit = true; + break; + } + } + + 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 a536ad7b622fe..92bf160850f77 100644 --- a/src/git/models/repository.ts +++ b/src/git/models/repository.ts @@ -633,11 +633,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); }