Skip to content

Commit fc8a931

Browse files
committed
convert git merge into git cmd
1 parent 00a56b2 commit fc8a931

File tree

8 files changed

+155
-23
lines changed

8 files changed

+155
-23
lines changed

src/commands/git/merge.ts

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import type { Container } from '../../container';
2+
import type { MergeOptions } from '../../git/gitProvider';
23
import type { GitBranch } from '../../git/models/branch';
34
import type { GitLog } from '../../git/models/log';
45
import type { GitReference } from '../../git/models/reference';
56
import { createRevisionRange, getReferenceLabel, isRevisionReference } from '../../git/models/reference';
67
import type { Repository } from '../../git/models/repository';
8+
import { showGenericErrorMessage } from '../../messages';
79
import type { DirectiveQuickPickItem } from '../../quickpicks/items/directive';
810
import { createDirectiveQuickPickItem, Directive } from '../../quickpicks/items/directive';
911
import type { FlagsQuickPickItem } from '../../quickpicks/items/flags';
1012
import { createFlagsQuickPickItem } from '../../quickpicks/items/flags';
13+
import { Logger } from '../../system/logger';
1114
import { pluralize } from '../../system/string';
1215
import type { ViewsWithRepositoryFolders } from '../../views/viewBase';
1316
import type {
@@ -35,12 +38,10 @@ interface Context {
3538
title: string;
3639
}
3740

38-
type Flags = '--ff-only' | '--no-ff' | '--squash' | '--no-commit';
39-
4041
interface State {
4142
repo: string | Repository;
4243
reference: GitReference;
43-
flags: Flags[];
44+
options: MergeOptions;
4445
}
4546

4647
export interface MergeGitCommandArgs {
@@ -76,8 +77,13 @@ export class MergeGitCommand extends QuickCommand<State> {
7677
return false;
7778
}
7879

79-
execute(state: MergeStepState) {
80-
state.repo.merge(...state.flags, state.reference.ref);
80+
async execute(state: MergeStepState) {
81+
try {
82+
await state.repo.git.merge(state.reference.ref, state.options);
83+
} catch (ex) {
84+
Logger.error(ex, this.title);
85+
void showGenericErrorMessage(ex);
86+
}
8187
}
8288

8389
protected async *steps(state: PartialStepState<State>): StepGenerator {
@@ -93,8 +99,8 @@ export class MergeGitCommand extends QuickCommand<State> {
9399
title: this.title,
94100
};
95101

96-
if (state.flags == null) {
97-
state.flags = [];
102+
if (state.options == null) {
103+
state.options = {};
98104
}
99105

100106
let skippedStepOne = false;
@@ -197,16 +203,16 @@ export class MergeGitCommand extends QuickCommand<State> {
197203
const result = yield* this.confirmStep(state as MergeStepState, context);
198204
if (result === StepResultBreak) continue;
199205

200-
state.flags = result;
206+
state.options = Object.assign({}, ...result);
201207

202208
endSteps(state);
203-
this.execute(state as MergeStepState);
209+
await this.execute(state as MergeStepState);
204210
}
205211

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

209-
private async *confirmStep(state: MergeStepState, context: Context): AsyncStepResultGenerator<Flags[]> {
215+
private async *confirmStep(state: MergeStepState, context: Context): AsyncStepResultGenerator<MergeOptions[]> {
210216
const counts = await this.container.git.getLeftRightCommitCount(
211217
state.repo.path,
212218
createRevisionRange(context.destination.ref, state.reference.ref, '...'),
@@ -240,31 +246,31 @@ export class MergeGitCommand extends QuickCommand<State> {
240246
return StepResultBreak;
241247
}
242248

243-
const step: QuickPickStep<FlagsQuickPickItem<Flags>> = this.createConfirmStep(
249+
const step: QuickPickStep<FlagsQuickPickItem<MergeOptions>> = this.createConfirmStep(
244250
appendReposToTitle(`Confirm ${title}`, state, context),
245251
[
246-
createFlagsQuickPickItem<Flags>(state.flags, [], {
252+
createFlagsQuickPickItem<MergeOptions>([], [], {
247253
label: this.title,
248254
detail: `Will merge ${pluralize('commit', count)} from ${getReferenceLabel(state.reference, {
249255
label: false,
250256
})} into ${getReferenceLabel(context.destination, { label: false })}`,
251257
}),
252-
createFlagsQuickPickItem<Flags>(state.flags, ['--ff-only'], {
258+
createFlagsQuickPickItem<MergeOptions>([], [{ fastForwardOnly: true }], {
253259
label: `Fast-forward ${this.title}`,
254260
description: '--ff-only',
255261
detail: `Will fast-forward merge ${pluralize('commit', count)} from ${getReferenceLabel(
256262
state.reference,
257263
{ label: false },
258264
)} into ${getReferenceLabel(context.destination, { label: false })}`,
259265
}),
260-
createFlagsQuickPickItem<Flags>(state.flags, ['--squash'], {
266+
createFlagsQuickPickItem<MergeOptions>([], [{ squash: true }], {
261267
label: `Squash ${this.title}`,
262268
description: '--squash',
263269
detail: `Will squash ${pluralize('commit', count)} from ${getReferenceLabel(state.reference, {
264270
label: false,
265271
})} into one when merging into ${getReferenceLabel(context.destination, { label: false })}`,
266272
}),
267-
createFlagsQuickPickItem<Flags>(state.flags, ['--no-ff'], {
273+
createFlagsQuickPickItem<MergeOptions>([], [{ noFastForward: true }], {
268274
label: `No Fast-forward ${this.title}`,
269275
description: '--no-ff',
270276
detail: `Will create a merge commit when merging ${pluralize(
@@ -275,7 +281,7 @@ export class MergeGitCommand extends QuickCommand<State> {
275281
{ label: false },
276282
)}`,
277283
}),
278-
createFlagsQuickPickItem<Flags>(state.flags, ['--no-ff', '--no-commit'], {
284+
createFlagsQuickPickItem<MergeOptions>([], [{ noCommit: true, noFastForward: true }], {
279285
label: `Don't Commit ${this.title}`,
280286
description: '--no-commit --no-ff',
281287
detail: `Will pause before committing the merge of ${pluralize(

src/commands/git/switch.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ export class SwitchGitCommand extends QuickCommand<State> {
104104
);
105105

106106
if (state.fastForwardTo != null) {
107-
state.repos[0].merge('--ff-only', state.fastForwardTo.ref);
107+
await state.repos[0].git.merge(state.fastForwardTo.ref, { fastForwardOnly: true });
108108
}
109109
}
110110

@@ -211,7 +211,7 @@ export class SwitchGitCommand extends QuickCommand<State> {
211211
);
212212
if (worktree != null && !worktree.isDefault) {
213213
if (state.fastForwardTo != null) {
214-
state.repos[0].merge('--ff-only', state.fastForwardTo.ref);
214+
await state.repos[0].git.merge(state.fastForwardTo.ref, { fastForwardOnly: true });
215215
}
216216

217217
const worktreeResult = yield* getSteps(

src/env/node/git/git.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import {
1616
CherryPickErrorReason,
1717
FetchError,
1818
FetchErrorReason,
19+
MergeError,
20+
MergeErrorReason,
1921
PullError,
2022
PullErrorReason,
2123
PushError,
@@ -173,6 +175,12 @@ const tagErrorAndReason: [RegExp, TagErrorReason][] = [
173175
[GitErrors.remoteRejected, TagErrorReason.RemoteRejected],
174176
];
175177

178+
const mergeErrorAndReason: [RegExp, MergeErrorReason][] = [
179+
[GitErrors.conflict, MergeErrorReason.Conflict],
180+
[GitErrors.unmergedFiles, MergeErrorReason.UnmergedFiles],
181+
[GitErrors.unstagedChanges, MergeErrorReason.UnstagedChanges],
182+
];
183+
176184
export class Git {
177185
/** Map of running git commands -- avoids running duplicate overlaping commands */
178186
private readonly pendingCommands = new Map<string, Promise<string | Buffer>>();
@@ -1092,6 +1100,21 @@ export class Git {
10921100
}
10931101
}
10941102

1103+
async merge(repoPath: string, args: string[]) {
1104+
try {
1105+
await this.git<string>({ cwd: repoPath }, 'merge', ...args);
1106+
} catch (ex) {
1107+
const msg: string = ex?.toString() ?? '';
1108+
for (const [error, reason] of mergeErrorAndReason) {
1109+
if (error.test(msg) || error.test(ex.stderr ?? '')) {
1110+
throw new MergeError(reason, ex);
1111+
}
1112+
}
1113+
1114+
throw new MergeError(MergeErrorReason.Other, ex);
1115+
}
1116+
}
1117+
10951118
for_each_ref__branch(repoPath: string, options: { all: boolean } = { all: false }) {
10961119
const params = ['for-each-ref', `--format=${parseGitBranchesDefaultFormat}`, 'refs/heads'];
10971120
if (options.all) {

src/env/node/git/localGitProvider.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import type {
4747
GitProvider,
4848
GitProviderDescriptor,
4949
LeftRightCommitCountResult,
50+
MergeOptions,
5051
NextComparisonUrisResult,
5152
PagedResult,
5253
PagingOptions,
@@ -1097,6 +1098,29 @@ export class LocalGitProvider implements GitProvider, Disposable {
10971098
this.container.events.fire('git:cache:reset', { repoPath: repoPath, caches: ['remotes'] });
10981099
}
10991100

1101+
@log()
1102+
async merge(repoPath: string, ref: string, options?: MergeOptions): Promise<void> {
1103+
const args: string[] = [];
1104+
1105+
if (options?.fastForwardOnly) {
1106+
args.push('--ff-only');
1107+
} else if (options?.noFastForward) {
1108+
args.push('--no-ff');
1109+
}
1110+
1111+
if (options?.noCommit) {
1112+
args.push('--no-commit');
1113+
}
1114+
1115+
if (options?.squash) {
1116+
args.push('--squash');
1117+
}
1118+
1119+
args.push(ref);
1120+
1121+
await this.git.merge(repoPath, args);
1122+
}
1123+
11001124
@log()
11011125
async applyChangesToWorkingFile(uri: GitUri, ref1?: string, ref2?: string) {
11021126
const scope = getLogScope();

src/git/errors.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,3 +567,70 @@ export class TagError extends Error {
567567
return this;
568568
}
569569
}
570+
571+
export const enum MergeErrorReason {
572+
Conflict,
573+
UnmergedFiles,
574+
UnstagedChanges,
575+
Other,
576+
}
577+
578+
export class MergeError extends Error {
579+
static is(ex: unknown, reason?: MergeErrorReason): ex is MergeError {
580+
return ex instanceof MergeError && (reason == null || ex.reason === reason);
581+
}
582+
583+
readonly original?: Error;
584+
readonly reason: MergeErrorReason | undefined;
585+
ref?: string;
586+
587+
private static buildMergeErrorMessage(reason?: MergeErrorReason, ref?: string): string {
588+
let baseMessage: string;
589+
if (ref != null) {
590+
baseMessage = `Unable to merge ${ref}`;
591+
} else {
592+
baseMessage = `Unable to merge`;
593+
}
594+
595+
switch (reason) {
596+
case MergeErrorReason.Conflict:
597+
return `${baseMessage} due to conflicts`;
598+
case MergeErrorReason.UnmergedFiles:
599+
return `${baseMessage} because you have unmerged files`;
600+
case MergeErrorReason.UnstagedChanges:
601+
return `${baseMessage} because you have unstaged changes`;
602+
default:
603+
return baseMessage;
604+
}
605+
606+
return baseMessage;
607+
}
608+
609+
constructor(reason?: MergeErrorReason, original?: Error, ref?: string);
610+
constructor(message?: string, original?: Error);
611+
constructor(messageOrReason: string | MergeErrorReason | undefined, original?: Error, ref?: string) {
612+
let reason: MergeErrorReason | undefined;
613+
if (typeof messageOrReason !== 'string') {
614+
reason = messageOrReason as MergeErrorReason;
615+
} else {
616+
super(messageOrReason);
617+
}
618+
619+
const message =
620+
typeof messageOrReason === 'string'
621+
? messageOrReason
622+
: MergeError.buildMergeErrorMessage(messageOrReason as MergeErrorReason, ref);
623+
super(message);
624+
625+
this.original = original;
626+
this.reason = reason;
627+
this.ref = ref;
628+
Error.captureStackTrace?.(this, MergeError);
629+
}
630+
631+
WithRef(ref: string) {
632+
this.ref = ref;
633+
this.message = MergeError.buildMergeErrorMessage(this.reason, ref);
634+
return this;
635+
}
636+
}

src/git/gitProvider.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,13 @@ export interface BranchContributorOverview {
117117
readonly contributors?: GitContributor[];
118118
}
119119

120+
export type MergeOptions = {
121+
fastForwardOnly?: boolean;
122+
noFastForward?: boolean;
123+
noCommit?: boolean;
124+
squash?: boolean;
125+
};
126+
120127
export interface GitProviderRepository {
121128
createBranch?(repoPath: string, name: string, ref: string): Promise<void>;
122129
renameBranch?(repoPath: string, oldName: string, newName: string): Promise<void>;
@@ -125,6 +132,7 @@ export interface GitProviderRepository {
125132
addRemote?(repoPath: string, name: string, url: string, options?: { fetch?: boolean }): Promise<void>;
126133
pruneRemote?(repoPath: string, name: string): Promise<void>;
127134
removeRemote?(repoPath: string, name: string): Promise<void>;
135+
merge?(repoPath: string, ref: string, options?: MergeOptions): Promise<void>;
128136

129137
applyUnreachableCommitForPatch?(
130138
repoPath: string,

src/git/gitProviderService.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import type {
5050
GitProviderDescriptor,
5151
GitProviderId,
5252
LeftRightCommitCountResult,
53+
MergeOptions,
5354
NextComparisonUrisResult,
5455
PagedResult,
5556
PagingOptions,
@@ -1334,6 +1335,14 @@ export class GitProviderService implements Disposable {
13341335
return provider.removeRemote(path, name);
13351336
}
13361337

1338+
@log()
1339+
merge(repoPath: string, ref: string, options: MergeOptions = {}): Promise<void> {
1340+
const { provider, path } = this.getProvider(repoPath);
1341+
if (provider.merge == null) throw new ProviderNotSupportedError(provider.descriptor.name);
1342+
1343+
return provider.merge(path, ref, options);
1344+
}
1345+
13371346
@log()
13381347
applyChangesToWorkingFile(uri: GitUri, ref1?: string, ref2?: string): Promise<void> {
13391348
const { provider } = this.getProvider(uri);

src/git/models/repository.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -734,11 +734,6 @@ export class Repository implements Disposable {
734734
return this.git.getWorktree(w => w.uri.toString() === url);
735735
}
736736

737-
@log()
738-
merge(...args: string[]) {
739-
void this.runTerminalCommand('merge', ...args);
740-
}
741-
742737
@gate()
743738
@log()
744739
async pull(options?: { progress?: boolean; rebase?: boolean }) {

0 commit comments

Comments
 (0)