Skip to content

Commit 9cb5bc8

Browse files
committed
add support to git rebase steps
1 parent 4bb6cbb commit 9cb5bc8

File tree

7 files changed

+141
-25
lines changed

7 files changed

+141
-25
lines changed

src/commands/git/rebase.ts

+40-11
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Container } from '../../container';
2+
import type { RebaseOptions } 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';
@@ -38,13 +39,9 @@ interface Context {
3839
title: string;
3940
}
4041

41-
type Flags = '--interactive';
42-
type RebaseOptions = { interactive?: boolean };
43-
4442
interface State {
4543
repo: string | Repository;
4644
destination: GitReference;
47-
flags: Flags[];
4845
options: RebaseOptions;
4946
}
5047

@@ -90,7 +87,7 @@ export class RebaseGitCommand extends QuickCommand<State> {
9087
}
9188

9289
try {
93-
await state.repo.git.rebase(state.destination.ref, configs, state.options);
90+
await state.repo.git.rebase(null, state.destination.ref, configs, state.options);
9491
} catch (ex) {
9592
Logger.error(ex, this.title);
9693
void showGenericErrorMessage(ex);
@@ -111,7 +108,9 @@ export class RebaseGitCommand extends QuickCommand<State> {
111108
};
112109

113110
if (state.options == null) {
114-
state.options = {};
111+
state.options = {
112+
autostash: true,
113+
};
115114
}
116115

117116
let skippedStepOne = false;
@@ -214,7 +213,7 @@ export class RebaseGitCommand extends QuickCommand<State> {
214213
const result = yield* this.confirmStep(state as RebaseStepState, context);
215214
if (result === StepResultBreak) continue;
216215

217-
state.options = Object.assign(state.options ?? {}, ...result);
216+
state.options = Object.assign({ autostash: true }, ...result);
218217

219218
endSteps(state);
220219
void this.execute(state as RebaseStepState);
@@ -255,9 +254,40 @@ export class RebaseGitCommand extends QuickCommand<State> {
255254
return StepResultBreak;
256255
}
257256

258-
const optionsArr: RebaseOptions[] = [];
257+
try {
258+
await state.repo.git.rebase(null, null, undefined, { checkActiveRebase: true });
259+
} catch {
260+
const step: QuickPickStep<FlagsQuickPickItem<RebaseOptions>> = this.createConfirmStep(
261+
appendReposToTitle(title, state, context),
262+
[
263+
createFlagsQuickPickItem<RebaseOptions>([], [{ abort: true }], {
264+
label: 'Abort Rebase',
265+
description: '--abort',
266+
detail: 'Will abort the current rebase',
267+
}),
268+
createFlagsQuickPickItem<RebaseOptions>([], [{ continue: true }], {
269+
label: 'Continue Rebase',
270+
description: '--continue',
271+
detail: 'Will continue the current rebase',
272+
}),
273+
createFlagsQuickPickItem<RebaseOptions>([], [{ skip: true }], {
274+
label: 'Skip Rebase',
275+
description: '--skip',
276+
detail: 'Will skip the current commit and continue the rebase',
277+
}),
278+
],
279+
createDirectiveQuickPickItem(Directive.Cancel, true, {
280+
label: 'Do nothing. A rebase is already in progress',
281+
detail: "If that is not the case, you can run `rm -rf '.git/rebase-merge'` and try again",
282+
}),
283+
);
284+
285+
const selection: StepSelection<typeof step> = yield step;
286+
return canPickStepContinue(step, state, selection) ? selection[0].item : StepResultBreak;
287+
}
288+
259289
const rebaseItems = [
260-
createFlagsQuickPickItem<RebaseOptions>(optionsArr, [{ interactive: true }], {
290+
createFlagsQuickPickItem<RebaseOptions>([], [{ interactive: true }], {
261291
label: `Interactive ${this.title}`,
262292
description: '--interactive',
263293
detail: `Will interactively update ${getReferenceLabel(context.branch, {
@@ -270,7 +300,7 @@ export class RebaseGitCommand extends QuickCommand<State> {
270300

271301
if (behind > 0) {
272302
rebaseItems.unshift(
273-
createFlagsQuickPickItem<RebaseOptions>(optionsArr, [{}], {
303+
createFlagsQuickPickItem<RebaseOptions>([], [{}], {
274304
label: this.title,
275305
detail: `Will update ${getReferenceLabel(context.branch, {
276306
label: false,
@@ -286,7 +316,6 @@ export class RebaseGitCommand extends QuickCommand<State> {
286316
rebaseItems,
287317
);
288318

289-
state.options = Object.assign(state.options, ...optionsArr);
290319
const selection: StepSelection<typeof step> = yield step;
291320
return canPickStepContinue(step, state, selection) ? selection[0].item : StepResultBreak;
292321
}

src/env/node/git/git.ts

+41-1
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ export const GitErrors = {
107107
tagNotFound: /tag .* not found/i,
108108
invalidTagName: /invalid tag name/i,
109109
remoteRejected: /rejected because the remote contains work/i,
110+
unresolvedConflicts: /^error: could not apply .*\n^hint: Resolve all conflicts.*$/im,
111+
rebaseMergeInProgress: /^fatal: It seems that there is already a rebase-merge directory/i,
110112
};
111113

112114
const GitWarnings = {
@@ -178,6 +180,8 @@ const tagErrorAndReason: [RegExp, TagErrorReason][] = [
178180
const rebaseErrorAndReason: [RegExp, RebaseErrorReason][] = [
179181
[GitErrors.uncommittedChanges, RebaseErrorReason.WorkingChanges],
180182
[GitErrors.changesWouldBeOverwritten, RebaseErrorReason.OverwrittenChanges],
183+
[GitErrors.unresolvedConflicts, RebaseErrorReason.UnresolvedConflicts],
184+
[GitErrors.rebaseMergeInProgress, RebaseErrorReason.RebaseMergeInProgress],
181185
];
182186

183187
export class Git {
@@ -1101,7 +1105,7 @@ export class Git {
11011105

11021106
async rebase(repoPath: string, args: string[] | undefined = [], configs: string[] | undefined = []): Promise<void> {
11031107
try {
1104-
void (await this.git<string>({ cwd: repoPath }, ...configs, 'rebase', '--autostash', ...args));
1108+
void (await this.git<string>({ cwd: repoPath }, ...configs, 'rebase', ...args));
11051109
} catch (ex) {
11061110
const msg: string = ex?.toString() ?? '';
11071111
for (const [regex, reason] of rebaseErrorAndReason) {
@@ -1114,6 +1118,42 @@ export class Git {
11141118
}
11151119
}
11161120

1121+
async check_active_rebase(repoPath: string): Promise<boolean> {
1122+
try {
1123+
const data = await this.git<string>({ cwd: repoPath }, 'rev-parse', '--verify', 'REBASE_HEAD');
1124+
return Boolean(data.length);
1125+
} catch {
1126+
return false;
1127+
}
1128+
}
1129+
1130+
async check_active_cherry_pick(repoPath: string): Promise<boolean> {
1131+
try {
1132+
const data = await this.git<string>({ cwd: repoPath }, 'rev-parse', '--verify', 'CHERRY_PICK_HEAD');
1133+
return Boolean(data.length);
1134+
} catch (_ex) {
1135+
return true;
1136+
}
1137+
}
1138+
1139+
async check_active_merge(repoPath: string): Promise<boolean> {
1140+
try {
1141+
const data = await this.git<string>({ cwd: repoPath }, 'rev-parse', '--verify', 'MERGE_HEAD');
1142+
return Boolean(data.length);
1143+
} catch (_ex) {
1144+
return true;
1145+
}
1146+
}
1147+
1148+
async check_active_cherry_revert(repoPath: string): Promise<boolean> {
1149+
try {
1150+
const data = await this.git<string>({ cwd: repoPath }, 'rev-parse', '--verify', 'REVERT_HEAD');
1151+
return Boolean(data.length);
1152+
} catch (_ex) {
1153+
return true;
1154+
}
1155+
}
1156+
11171157
for_each_ref__branch(repoPath: string, options: { all: boolean } = { all: false }) {
11181158
const params = ['for-each-ref', `--format=${parseGitBranchesDefaultFormat}`, 'refs/heads'];
11191159
if (options.all) {

src/env/node/git/localGitProvider.ts

+35-6
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import type {
5353
PagingOptions,
5454
PreviousComparisonUrisResult,
5555
PreviousLineComparisonUrisResult,
56+
RebaseOptions,
5657
RepositoryCloseEvent,
5758
RepositoryInitWatcher,
5859
RepositoryOpenEvent,
@@ -1662,22 +1663,50 @@ export class LocalGitProvider implements GitProvider, Disposable {
16621663
@log()
16631664
async rebase(
16641665
repoPath: string,
1665-
ref: string,
1666+
upstream: string | null,
1667+
ref: string | null,
16661668
configs?: { sequenceEditor?: string },
1667-
options?: { interactive?: boolean } = {},
1669+
options?: RebaseOptions = {},
16681670
): Promise<void> {
16691671
const configFlags = [];
16701672
const args = [];
16711673

1674+
if (options?.checkActiveRebase) {
1675+
if (await this.git.check_active_rebase(repoPath)) {
1676+
throw new RebaseError(RebaseErrorReason.RebaseMergeInProgress);
1677+
}
1678+
1679+
return;
1680+
}
1681+
16721682
if (configs?.sequenceEditor != null) {
16731683
configFlags.push('-c', `sequence.editor="${configs.sequenceEditor}"`);
16741684
}
16751685

1676-
if (options?.interactive) {
1677-
args.push('--interactive');
1678-
}
1686+
// These options can only be used on their own
1687+
if (options?.abort) {
1688+
args.push('--abort');
1689+
} else if (options?.continue) {
1690+
args.push('--continue');
1691+
} else if (options?.skip) {
1692+
args.push('--skip');
1693+
} else {
1694+
if (options?.autostash) {
1695+
args.push('--autostash');
1696+
}
1697+
1698+
if (options?.interactive) {
1699+
args.push('--interactive');
1700+
}
16791701

1680-
args.push(ref);
1702+
if (upstream) {
1703+
args.push(upstream);
1704+
}
1705+
1706+
if (ref) {
1707+
args.push(ref);
1708+
}
1709+
}
16811710

16821711
try {
16831712
await this.git.rebase(repoPath, args, configFlags);

src/git/actions/repository.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export function push(repos?: string | string[] | Repository | Repository[], forc
3434
export function rebase(repo?: string | Repository, ref?: GitReference, interactive: boolean = true) {
3535
return executeGitCommand({
3636
command: 'rebase',
37-
state: { repo: repo, destination: ref, flags: interactive ? ['--interactive'] : [] },
37+
state: { repo: repo, destination: ref, options: { interactive: interactive, autostash: true } },
3838
});
3939
}
4040

src/git/errors.ts

+6
Original file line numberDiff line numberDiff line change
@@ -571,6 +571,8 @@ export class TagError extends Error {
571571
export const enum RebaseErrorReason {
572572
WorkingChanges,
573573
OverwrittenChanges,
574+
UnresolvedConflicts,
575+
RebaseMergeInProgress,
574576
Other,
575577
}
576578

@@ -592,6 +594,10 @@ export class RebaseError extends Error {
592594
return `${baseMessage} because there are uncommitted changes`;
593595
case RebaseErrorReason.OverwrittenChanges:
594596
return `${baseMessage} because some local changes would be overwritten`;
597+
case RebaseErrorReason.UnresolvedConflicts:
598+
return `${baseMessage} due to conflicts. Resolve the conflicts first and continue the rebase`;
599+
case RebaseErrorReason.RebaseMergeInProgress:
600+
return `${baseMessage} because a rebase is already in progress`;
595601
default:
596602
return baseMessage;
597603
}

src/git/gitProvider.ts

+12-2
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,15 @@ export interface BranchContributorOverview {
117117
readonly contributors?: GitContributor[];
118118
}
119119

120+
export type RebaseOptions = {
121+
abort?: boolean;
122+
autostash?: boolean;
123+
checkActiveRebase?: boolean;
124+
continue?: boolean;
125+
interactive?: boolean;
126+
skip?: boolean;
127+
};
128+
120129
export interface GitProviderRepository {
121130
createBranch?(repoPath: string, name: string, ref: string): Promise<void>;
122131
renameBranch?(repoPath: string, oldName: string, newName: string): Promise<void>;
@@ -127,9 +136,10 @@ export interface GitProviderRepository {
127136
removeRemote?(repoPath: string, name: string): Promise<void>;
128137
rebase?(
129138
repoPath: string,
130-
ref: string,
139+
upstream: string | null,
140+
ref: string | null,
131141
configs?: { sequenceEditor?: string },
132-
options?: { interactive?: boolean },
142+
options?: RebaseOptions,
133143
): Promise<void>;
134144

135145
applyUnreachableCommitForPatch?(

src/git/gitProviderService.ts

+6-4
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import type {
5555
PagingOptions,
5656
PreviousComparisonUrisResult,
5757
PreviousLineComparisonUrisResult,
58+
RebaseOptions,
5859
RepositoryVisibility,
5960
RepositoryVisibilityInfo,
6061
ScmRepository,
@@ -1345,14 +1346,15 @@ export class GitProviderService implements Disposable {
13451346
@log()
13461347
rebase(
13471348
repoPath: string | Uri,
1348-
ref: string,
1349-
configs: { sequenceEditor?: string },
1350-
options: { interactive?: boolean } = {},
1349+
upstream: string | null,
1350+
ref: string | null,
1351+
configs?: { sequenceEditor?: string },
1352+
options: RebaseOptions = {},
13511353
): Promise<void> {
13521354
const { provider, path } = this.getProvider(repoPath);
13531355
if (provider.rebase == null) throw new ProviderNotSupportedError(provider.descriptor.name);
13541356

1355-
return provider.rebase(path, ref, configs, options);
1357+
return provider.rebase(path, upstream, ref, configs, options);
13561358
}
13571359

13581360
@log()

0 commit comments

Comments
 (0)