Skip to content

Commit

Permalink
add support to git rebase steps
Browse files Browse the repository at this point in the history
  • Loading branch information
sergiolms committed Dec 11, 2024
1 parent 753e82a commit 0545f2a
Show file tree
Hide file tree
Showing 7 changed files with 141 additions and 25 deletions.
51 changes: 40 additions & 11 deletions src/commands/git/rebase.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Container } from '../../container';
import type { RebaseOptions } from '../../git/gitProvider';
import type { GitBranch } from '../../git/models/branch';
import type { GitLog } from '../../git/models/log';
import type { GitReference } from '../../git/models/reference';
Expand Down Expand Up @@ -38,13 +39,9 @@ interface Context {
title: string;
}

type Flags = '--interactive';
type RebaseOptions = { interactive?: boolean };

interface State {
repo: string | Repository;
destination: GitReference;
flags: Flags[];
options: RebaseOptions;
}

Expand Down Expand Up @@ -90,7 +87,7 @@ export class RebaseGitCommand extends QuickCommand<State> {
}

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

if (state.options == null) {
state.options = {};
state.options = {
autostash: true,
};
}

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

state.options = Object.assign(state.options ?? {}, ...result);
state.options = Object.assign({ autostash: true }, ...result);

endSteps(state);
void this.execute(state as RebaseStepState);
Expand Down Expand Up @@ -255,9 +254,40 @@ export class RebaseGitCommand extends QuickCommand<State> {
return StepResultBreak;
}

const optionsArr: RebaseOptions[] = [];
try {
await state.repo.git.rebase(null, null, undefined, { checkActiveRebase: true });
} catch {
const step: QuickPickStep<FlagsQuickPickItem<RebaseOptions>> = this.createConfirmStep(
appendReposToTitle(title, state, context),
[
createFlagsQuickPickItem<RebaseOptions>([], [{ abort: true }], {
label: 'Abort Rebase',
description: '--abort',
detail: 'Will abort the current rebase',
}),
createFlagsQuickPickItem<RebaseOptions>([], [{ continue: true }], {
label: 'Continue Rebase',
description: '--continue',
detail: 'Will continue the current rebase',
}),
createFlagsQuickPickItem<RebaseOptions>([], [{ skip: true }], {
label: 'Skip Rebase',
description: '--skip',
detail: 'Will skip the current commit and continue the rebase',
}),
],
createDirectiveQuickPickItem(Directive.Cancel, true, {
label: 'Do nothing. A rebase is already in progress',
detail: "If that is not the case, you can run `rm -rf '.git/rebase-merge'` and try again",
}),
);

const selection: StepSelection<typeof step> = yield step;
return canPickStepContinue(step, state, selection) ? selection[0].item : StepResultBreak;
}

const rebaseItems = [
createFlagsQuickPickItem<RebaseOptions>(optionsArr, [{ interactive: true }], {
createFlagsQuickPickItem<RebaseOptions>([], [{ interactive: true }], {
label: `Interactive ${this.title}`,
description: '--interactive',
detail: `Will interactively update ${getReferenceLabel(context.branch, {
Expand All @@ -270,7 +300,7 @@ export class RebaseGitCommand extends QuickCommand<State> {

if (behind > 0) {
rebaseItems.unshift(
createFlagsQuickPickItem<RebaseOptions>(optionsArr, [{}], {
createFlagsQuickPickItem<RebaseOptions>([], [{}], {
label: this.title,
detail: `Will update ${getReferenceLabel(context.branch, {
label: false,
Expand All @@ -286,7 +316,6 @@ export class RebaseGitCommand extends QuickCommand<State> {
rebaseItems,
);

state.options = Object.assign(state.options, ...optionsArr);
const selection: StepSelection<typeof step> = yield step;
return canPickStepContinue(step, state, selection) ? selection[0].item : StepResultBreak;
}
Expand Down
42 changes: 41 additions & 1 deletion src/env/node/git/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ export const GitErrors = {
tagNotFound: /tag .* not found/i,
invalidTagName: /invalid tag name/i,
remoteRejected: /rejected because the remote contains work/i,
unresolvedConflicts: /^error: could not apply .*\n^hint: Resolve all conflicts.*$/im,
rebaseMergeInProgress: /^fatal: It seems that there is already a rebase-merge directory/i,
};

const GitWarnings = {
Expand Down Expand Up @@ -178,6 +180,8 @@ const tagErrorAndReason: [RegExp, TagErrorReason][] = [
const rebaseErrorAndReason: [RegExp, RebaseErrorReason][] = [
[GitErrors.uncommittedChanges, RebaseErrorReason.WorkingChanges],
[GitErrors.changesWouldBeOverwritten, RebaseErrorReason.OverwrittenChanges],
[GitErrors.unresolvedConflicts, RebaseErrorReason.UnresolvedConflicts],
[GitErrors.rebaseMergeInProgress, RebaseErrorReason.RebaseMergeInProgress],
];

export class Git {
Expand Down Expand Up @@ -1101,7 +1105,7 @@ export class Git {

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

async check_active_rebase(repoPath: string): Promise<boolean> {
try {
const data = await this.git<string>({ cwd: repoPath }, 'rev-parse', '--verify', 'REBASE_HEAD');
return Boolean(data.length);
} catch {
return false;
}
}

async check_active_cherry_pick(repoPath: string): Promise<boolean> {
try {
const data = await this.git<string>({ cwd: repoPath }, 'rev-parse', '--verify', 'CHERRY_PICK_HEAD');
return Boolean(data.length);
} catch (_ex) {
return true;
}
}

async check_active_merge(repoPath: string): Promise<boolean> {
try {
const data = await this.git<string>({ cwd: repoPath }, 'rev-parse', '--verify', 'MERGE_HEAD');
return Boolean(data.length);
} catch (_ex) {
return true;
}
}

async check_active_cherry_revert(repoPath: string): Promise<boolean> {
try {
const data = await this.git<string>({ cwd: repoPath }, 'rev-parse', '--verify', 'REVERT_HEAD');
return Boolean(data.length);
} catch (_ex) {
return true;
}
}

for_each_ref__branch(repoPath: string, options: { all: boolean } = { all: false }) {
const params = ['for-each-ref', `--format=${parseGitBranchesDefaultFormat}`, 'refs/heads'];
if (options.all) {
Expand Down
41 changes: 35 additions & 6 deletions src/env/node/git/localGitProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import type {
PagingOptions,
PreviousComparisonUrisResult,
PreviousLineComparisonUrisResult,
RebaseOptions,
RepositoryCloseEvent,
RepositoryInitWatcher,
RepositoryOpenEvent,
Expand Down Expand Up @@ -1661,22 +1662,50 @@ export class LocalGitProvider implements GitProvider, Disposable {
@log()
async rebase(
repoPath: string,
ref: string,
upstream: string | null,
ref: string | null,
configs?: { sequenceEditor?: string },
options?: { interactive?: boolean } = {},
options?: RebaseOptions = {},
): Promise<void> {
const configFlags = [];
const args = [];

if (options?.checkActiveRebase) {
if (await this.git.check_active_rebase(repoPath)) {
throw new RebaseError(RebaseErrorReason.RebaseMergeInProgress);
}

return;
}

if (configs?.sequenceEditor != null) {
configFlags.push('-c', `sequence.editor="${configs.sequenceEditor}"`);
}

if (options?.interactive) {
args.push('--interactive');
}
// These options can only be used on their own
if (options?.abort) {
args.push('--abort');
} else if (options?.continue) {
args.push('--continue');
} else if (options?.skip) {
args.push('--skip');
} else {
if (options?.autostash) {
args.push('--autostash');
}

if (options?.interactive) {
args.push('--interactive');
}

args.push(ref);
if (upstream) {
args.push(upstream);
}

if (ref) {
args.push(ref);
}
}

try {
await this.git.rebase(repoPath, args, configFlags);
Expand Down
2 changes: 1 addition & 1 deletion src/git/actions/repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export function push(repos?: string | string[] | Repository | Repository[], forc
export function rebase(repo?: string | Repository, ref?: GitReference, interactive: boolean = true) {
return executeGitCommand({
command: 'rebase',
state: { repo: repo, destination: ref, flags: interactive ? ['--interactive'] : [] },
state: { repo: repo, destination: ref, options: { interactive: interactive, autostash: true } },
});
}

Expand Down
6 changes: 6 additions & 0 deletions src/git/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,8 @@ export class TagError extends Error {
export const enum RebaseErrorReason {
WorkingChanges,
OverwrittenChanges,
UnresolvedConflicts,
RebaseMergeInProgress,
Other,
}

Expand All @@ -592,6 +594,10 @@ export class RebaseError extends Error {
return `${baseMessage} because there are uncommitted changes`;
case RebaseErrorReason.OverwrittenChanges:
return `${baseMessage} because some local changes would be overwritten`;
case RebaseErrorReason.UnresolvedConflicts:
return `${baseMessage} due to conflicts. Resolve the conflicts first and continue the rebase`;
case RebaseErrorReason.RebaseMergeInProgress:
return `${baseMessage} because a rebase is already in progress`;
default:
return baseMessage;
}
Expand Down
14 changes: 12 additions & 2 deletions src/git/gitProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,15 @@ export interface BranchContributorOverview {
readonly contributors?: GitContributor[];
}

export type RebaseOptions = {
abort?: boolean;
autostash?: boolean;
checkActiveRebase?: boolean;
continue?: boolean;
interactive?: boolean;
skip?: boolean;
};

export interface GitProviderRepository {
createBranch?(repoPath: string, name: string, ref: string): Promise<void>;
renameBranch?(repoPath: string, oldName: string, newName: string): Promise<void>;
Expand All @@ -127,9 +136,10 @@ export interface GitProviderRepository {
removeRemote?(repoPath: string, name: string): Promise<void>;
rebase?(
repoPath: string,
ref: string,
upstream: string | null,
ref: string | null,
configs?: { sequenceEditor?: string },
options?: { interactive?: boolean },
options?: RebaseOptions,
): Promise<void>;

applyUnreachableCommitForPatch?(
Expand Down
10 changes: 6 additions & 4 deletions src/git/gitProviderService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import type {
PagingOptions,
PreviousComparisonUrisResult,
PreviousLineComparisonUrisResult,
RebaseOptions,
RepositoryVisibility,
RepositoryVisibilityInfo,
ScmRepository,
Expand Down Expand Up @@ -1345,14 +1346,15 @@ export class GitProviderService implements Disposable {
@log()
rebase(
repoPath: string | Uri,
ref: string,
configs: { sequenceEditor?: string },
options: { interactive?: boolean } = {},
upstream: string | null,
ref: string | null,
configs?: { sequenceEditor?: string },
options: RebaseOptions = {},
): Promise<void> {
const { provider, path } = this.getProvider(repoPath);
if (provider.rebase == null) throw new ProviderNotSupportedError(provider.descriptor.name);

return provider.rebase(path, ref, configs, options);
return provider.rebase(path, upstream, ref, configs, options);
}

@log()
Expand Down

0 comments on commit 0545f2a

Please sign in to comment.