From 612d9888e45bfeb15b2b22bdf944920a88edb4cd Mon Sep 17 00:00:00 2001 From: Sergio Date: Mon, 2 Dec 2024 14:12:52 +0100 Subject: [PATCH] convert git merge into git cmd --- src/commands/git/merge.ts | 13 ++++-- src/commands/git/switch.ts | 4 +- src/env/node/git/git.ts | 21 +++++++++ src/env/node/git/localGitProvider.ts | 26 +++++++++++ src/git/errors.ts | 67 ++++++++++++++++++++++++++++ src/git/gitProvider.ts | 5 +++ src/git/gitProviderService.ts | 26 +++++++++++ src/git/models/repository.ts | 5 --- 8 files changed, 157 insertions(+), 10 deletions(-) diff --git a/src/commands/git/merge.ts b/src/commands/git/merge.ts index 4fcdaa09052dd..fff3b90a6dd73 100644 --- a/src/commands/git/merge.ts +++ b/src/commands/git/merge.ts @@ -4,10 +4,12 @@ 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 { DirectiveQuickPickItem } from '../../quickpicks/items/directive'; import { createDirectiveQuickPickItem, Directive } from '../../quickpicks/items/directive'; import type { FlagsQuickPickItem } from '../../quickpicks/items/flags'; import { createFlagsQuickPickItem } from '../../quickpicks/items/flags'; +import { Logger } from '../../system/logger'; import { pluralize } from '../../system/string'; import type { ViewsWithRepositoryFolders } from '../../views/viewBase'; import type { @@ -76,8 +78,13 @@ export class MergeGitCommand extends QuickCommand { return false; } - execute(state: MergeStepState) { - state.repo.merge(...state.flags, state.reference.ref); + async execute(state: MergeStepState) { + try { + await state.repo.git.merge(state.reference.ref, state.flags); + } catch (ex) { + Logger.error(ex, this.title); + void showGenericErrorMessage(ex); + } } protected async *steps(state: PartialStepState): StepGenerator { @@ -200,7 +207,7 @@ export class MergeGitCommand extends QuickCommand { state.flags = result; endSteps(state); - this.execute(state as MergeStepState); + await this.execute(state as MergeStepState); } return state.counter < 0 ? StepResultBreak : undefined; diff --git a/src/commands/git/switch.ts b/src/commands/git/switch.ts index eeb62a6624afc..08970b691fd8b 100644 --- a/src/commands/git/switch.ts +++ b/src/commands/git/switch.ts @@ -104,7 +104,7 @@ export class SwitchGitCommand extends QuickCommand { ); if (state.fastForwardTo != null) { - state.repos[0].merge('--ff-only', state.fastForwardTo.ref); + await state.repos[0].git.merge(state.fastForwardTo.ref, ['--ff-only']); } } @@ -211,7 +211,7 @@ export class SwitchGitCommand extends QuickCommand { ); if (worktree != null && !worktree.isDefault) { if (state.fastForwardTo != null) { - state.repos[0].merge('--ff-only', state.fastForwardTo.ref); + await state.repos[0].git.merge(state.fastForwardTo.ref, ['--ff-only']); } const worktreeResult = yield* getSteps( diff --git a/src/env/node/git/git.ts b/src/env/node/git/git.ts index f109e990e0532..0af4192e076f6 100644 --- a/src/env/node/git/git.ts +++ b/src/env/node/git/git.ts @@ -173,6 +173,12 @@ const tagErrorAndReason: [RegExp, TagErrorReason][] = [ [GitErrors.remoteRejected, TagErrorReason.RemoteRejected], ]; +const mergeErrorAndReason: [RegExp, MergeErrorReason][] = [ + [GitErrors.conflict, MergeErrorReason.Conflict], + [GitErrors.unmergedFiles, MergeErrorReason.UnmergedFiles], + [GitErrors.unstagedChanges, MergeErrorReason.UnstagedChanges], +]; + export class Git { /** Map of running git commands -- avoids running duplicate overlaping commands */ private readonly pendingCommands = new Map>(); @@ -1092,6 +1098,21 @@ export class Git { } } + async merge(repoPath: string, args: string[]) { + try { + await this.git({ cwd: repoPath }, 'merge', ...args); + } catch (ex) { + const msg: string = ex?.toString() ?? ''; + for (const [error, reason] of mergeErrorAndReason) { + if (error.test(msg) || error.test(ex.stderr ?? '')) { + throw new MergeError(reason, ex); + } + } + + throw new MergeError(MergeErrorReason.Other, ex); + } + } + for_each_ref__branch(repoPath: string, options: { all: boolean } = { all: false }) { const params = ['for-each-ref', `--format=${parseGitBranchesDefaultFormat}`, 'refs/heads']; if (options.all) { diff --git a/src/env/node/git/localGitProvider.ts b/src/env/node/git/localGitProvider.ts index ed099e9b94008..69876894975f1 100644 --- a/src/env/node/git/localGitProvider.ts +++ b/src/env/node/git/localGitProvider.ts @@ -1076,6 +1076,32 @@ export class LocalGitProvider implements GitProvider, Disposable { this.container.events.fire('git:cache:reset', { repoPath: repoPath, caches: ['remotes'] }); } + @log() + async merge( + repoPath: string, + ref: string, + options?: { fastForwardOnly?: boolean; noFastForward?: boolean; noCommit?: boolean; squash?: boolean }, + ): Promise { + const args: string[] = []; + if (options?.fastForwardOnly) { + args.push('--ff-only'); + } else if (options?.noFastForward) { + args.push('--no-ff'); + } + + if (options?.noCommit) { + args.push('--no-commit'); + } + + if (options?.squash) { + args.push('--squash'); + } + + args.push(ref); + + await this.git.merge(repoPath, args); + } + @log() async applyChangesToWorkingFile(uri: GitUri, ref1?: string, ref2?: string) { const scope = getLogScope(); diff --git a/src/git/errors.ts b/src/git/errors.ts index e1ef081fdfb25..d96dae932a492 100644 --- a/src/git/errors.ts +++ b/src/git/errors.ts @@ -567,3 +567,70 @@ export class TagError extends Error { return this; } } + +export const enum MergeErrorReason { + Conflict, + UnmergedFiles, + UnstagedChanges, + Other, +} + +export class MergeError extends Error { + static is(ex: unknown, reason?: MergeErrorReason): ex is MergeError { + return ex instanceof MergeError && (reason == null || ex.reason === reason); + } + + readonly original?: Error; + readonly reason: MergeErrorReason | undefined; + ref?: string; + + private static buildMergeErrorMessage(reason?: MergeErrorReason, ref?: string): string { + let baseMessage: string; + if (ref != null) { + baseMessage = `Unable to merge ${ref}`; + } else { + baseMessage = `Unable to merge`; + } + + switch (reason) { + case MergeErrorReason.Conflict: + return `${baseMessage} due to conflicts`; + case MergeErrorReason.UnmergedFiles: + return `${baseMessage} because you have unmerged files`; + case MergeErrorReason.UnstagedChanges: + return `${baseMessage} because you have unstaged changes`; + default: + return baseMessage; + } + + return baseMessage; + } + + constructor(reason?: MergeErrorReason, original?: Error, ref?: string); + constructor(message?: string, original?: Error); + constructor(messageOrReason: string | MergeErrorReason | undefined, original?: Error, ref?: string) { + let reason: MergeErrorReason | undefined; + if (typeof messageOrReason !== 'string') { + reason = messageOrReason as MergeErrorReason; + } else { + super(messageOrReason); + } + + const message = + typeof messageOrReason === 'string' + ? messageOrReason + : MergeError.buildMergeErrorMessage(messageOrReason as MergeErrorReason, ref); + super(message); + + this.original = original; + this.reason = reason; + this.ref = ref; + Error.captureStackTrace?.(this, MergeError); + } + + WithRef(ref: string) { + this.ref = ref; + this.message = MergeError.buildMergeErrorMessage(this.reason, ref); + return this; + } +} diff --git a/src/git/gitProvider.ts b/src/git/gitProvider.ts index 74450e2a2a829..e659abbec766c 100644 --- a/src/git/gitProvider.ts +++ b/src/git/gitProvider.ts @@ -125,6 +125,11 @@ export interface GitProviderRepository { addRemote?(repoPath: string, name: string, url: string, options?: { fetch?: boolean }): Promise; pruneRemote?(repoPath: string, name: string): Promise; removeRemote?(repoPath: string, name: string): Promise; + merge?( + repoPath: string, + ref: string, + options?: { fastForwardOnly?: boolean; noFastForward?: boolean; noCommit?: boolean; squash?: boolean }, + ): Promise; applyUnreachableCommitForPatch?( repoPath: string, diff --git a/src/git/gitProviderService.ts b/src/git/gitProviderService.ts index aeb41768e11f0..7eef9ad3a209a 100644 --- a/src/git/gitProviderService.ts +++ b/src/git/gitProviderService.ts @@ -1334,6 +1334,32 @@ export class GitProviderService implements Disposable { return provider.removeRemote(path, name); } + @log() + merge(repoPath: string, ref: string, flags: string[] | undefined = []): Promise { + const { provider, path } = this.getProvider(repoPath); + if (provider.merge == null) throw new ProviderNotSupportedError(provider.descriptor.name); + const options: { fastForwardOnly?: boolean; noFastForward?: boolean; noCommit?: boolean; squash?: boolean } = + {}; + for (const flag of flags) { + switch (flag) { + case '--ff-only': + options.fastForwardOnly = true; + break; + case '--no-ff': + options.noFastForward = true; + break; + case '--squash': + options.squash = true; + break; + case '--no-commit': + options.noCommit = true; + break; + } + } + + return provider.merge(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..2d60d25993afb 100644 --- a/src/git/models/repository.ts +++ b/src/git/models/repository.ts @@ -733,11 +733,6 @@ export class Repository implements Disposable { return this.git.getWorktree(w => w.uri.toString() === url); } - @log() - merge(...args: string[]) { - void this.runTerminalCommand('merge', ...args); - } - @gate() @log() async pull(options?: { progress?: boolean; rebase?: boolean }) {