Skip to content

Commit 87b29d1

Browse files
committed
add git revert command
1 parent cb71cee commit 87b29d1

File tree

8 files changed

+187
-8
lines changed

8 files changed

+187
-8
lines changed

src/commands/git/revert.ts

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
1+
import { Commands } from '../../constants.commands';
12
import type { Container } from '../../container';
3+
import { RevertError, RevertErrorReason } from '../../git/errors';
24
import type { GitBranch } from '../../git/models/branch';
35
import type { GitLog } from '../../git/models/log';
46
import type { GitRevisionReference } from '../../git/models/reference';
57
import { getReferenceLabel } from '../../git/models/reference';
68
import type { Repository } from '../../git/models/repository';
9+
import { showGenericErrorMessage, showShouldCommitOrStashPrompt } from '../../messages';
710
import type { FlagsQuickPickItem } from '../../quickpicks/items/flags';
811
import { createFlagsQuickPickItem } from '../../quickpicks/items/flags';
12+
import { Logger } from '../../system/logger';
13+
import { executeCommand, executeCoreCommand } from '../../system/vscode/command';
914
import type { ViewsWithRepositoryFolders } from '../../views/viewBase';
1015
import type {
1116
PartialStepState,
@@ -71,8 +76,42 @@ export class RevertGitCommand extends QuickCommand<State> {
7176
return false;
7277
}
7378

74-
execute(state: RevertStepState<State<GitRevisionReference[]>>) {
75-
state.repo.revert(...state.flags, ...state.references.map(c => c.ref).reverse());
79+
async execute(state: RevertStepState<State<GitRevisionReference[]>>) {
80+
for (const ref of state.references.reverse()) {
81+
try {
82+
await state.repo.git.revert(ref.ref, state.flags);
83+
} catch (ex) {
84+
if (ex instanceof RevertError) {
85+
let shouldRetry = false;
86+
if (ex.reason === RevertErrorReason.LocalChangesWouldBeOverwritten) {
87+
const response = await showShouldCommitOrStashPrompt();
88+
if (response === 'Stash') {
89+
await executeCommand(Commands.GitCommandsStashPush);
90+
shouldRetry = true;
91+
} else if (response === 'Commit') {
92+
await executeCoreCommand('workbench.view.scm');
93+
shouldRetry = true;
94+
} else {
95+
continue;
96+
}
97+
}
98+
99+
if (shouldRetry) {
100+
try {
101+
await state.repo.git.revert(ref.ref, state.flags);
102+
} catch (ex) {
103+
Logger.error(ex, this.title);
104+
void showGenericErrorMessage(ex.message);
105+
}
106+
}
107+
108+
continue;
109+
}
110+
111+
Logger.error(ex, this.title);
112+
void showGenericErrorMessage(ex.message);
113+
}
114+
}
76115
}
77116

78117
protected async *steps(state: PartialStepState<State>): StepGenerator {
@@ -160,7 +199,7 @@ export class RevertGitCommand extends QuickCommand<State> {
160199
state.flags = result;
161200

162201
endSteps(state);
163-
this.execute(state as RevertStepState<State<GitRevisionReference[]>>);
202+
await this.execute(state as RevertStepState<State<GitRevisionReference[]>>);
164203
}
165204

166205
return state.counter < 0 ? StepResultBreak : undefined;

src/env/node/git/git.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import {
2020
PullErrorReason,
2121
PushError,
2222
PushErrorReason,
23+
RevertError,
24+
RevertErrorReason,
2325
StashPushError,
2426
StashPushErrorReason,
2527
TagError,
@@ -105,6 +107,7 @@ export const GitErrors = {
105107
tagNotFound: /tag .* not found/i,
106108
invalidTagName: /invalid tag name/i,
107109
remoteRejected: /rejected because the remote contains work/i,
110+
revertHasConflicts: /(error: could not revert .*) (hint: After resolving the conflicts)/gi,
108111
};
109112

110113
const GitWarnings = {
@@ -173,6 +176,13 @@ const tagErrorAndReason: [RegExp, TagErrorReason][] = [
173176
[GitErrors.remoteRejected, TagErrorReason.RemoteRejected],
174177
];
175178

179+
const revertErrorAndReason = [
180+
[GitErrors.badRevision, RevertErrorReason.BadRevision],
181+
[GitErrors.invalidObjectName, RevertErrorReason.InvalidObjectName],
182+
[GitErrors.revertHasConflicts, RevertErrorReason.Conflict],
183+
[GitErrors.changesWouldBeOverwritten, RevertErrorReason.LocalChangesWouldBeOverwritten],
184+
];
185+
176186
export class Git {
177187
/** Map of running git commands -- avoids running duplicate overlaping commands */
178188
private readonly pendingCommands = new Map<string, Promise<string | Buffer>>();
@@ -1588,6 +1598,21 @@ export class Git {
15881598
return this.git<string>({ cwd: repoPath }, 'reset', '-q', '--', ...pathspecs);
15891599
}
15901600

1601+
async revert(repoPath: string, ...args: string[]) {
1602+
try {
1603+
await this.git<string>({ cwd: repoPath }, 'revert', ...args);
1604+
} catch (ex) {
1605+
const msg: string = ex?.toString() ?? '';
1606+
for (const [error, reason] of revertErrorAndReason) {
1607+
if (error.test(msg) || error.test(ex.stderr ?? '')) {
1608+
throw new RevertError(reason, ex);
1609+
}
1610+
}
1611+
1612+
throw new RevertError(RevertErrorReason.Other, ex);
1613+
}
1614+
}
1615+
15911616
async rev_list(
15921617
repoPath: string,
15931618
ref: string,

src/env/node/git/localGitProvider.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
PullError,
3232
PushError,
3333
PushErrorReason,
34+
RevertError,
3435
StashApplyError,
3536
StashApplyErrorReason,
3637
StashPushError,
@@ -6044,6 +6045,24 @@ export class LocalGitProvider implements GitProvider, Disposable {
60446045
return worktrees;
60456046
}
60466047

6048+
@log()
6049+
async revert(repoPath: string, ref: string, options?: { edit?: boolean }): Promise<void> {
6050+
const args = [];
6051+
if (options.edit !== undefined) {
6052+
args.push(options.edit ? '--edit' : '--no-edit');
6053+
}
6054+
6055+
try {
6056+
await this.git.revert(repoPath, ...args, ref);
6057+
} catch (ex) {
6058+
if (ex instanceof RevertError) {
6059+
throw ex.WithRef(ref);
6060+
}
6061+
6062+
throw ex;
6063+
}
6064+
}
6065+
60476066
@log()
60486067
// eslint-disable-next-line @typescript-eslint/require-await
60496068
async getWorktreesDefaultUri(repoPath: string): Promise<Uri | undefined> {

src/git/errors.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,3 +567,65 @@ export class TagError extends Error {
567567
return this;
568568
}
569569
}
570+
571+
export const enum RevertErrorReason {
572+
BadRevision,
573+
InvalidObjectName,
574+
Conflict,
575+
LocalChangesWouldBeOverwritten,
576+
Other,
577+
}
578+
579+
export class RevertError extends Error {
580+
static is(ex: unknown, reason?: RevertErrorReason): ex is RevertError {
581+
return ex instanceof RevertError && (reason == null || ex.reason === reason);
582+
}
583+
584+
readonly original?: Error;
585+
readonly reason: RevertErrorReason | undefined;
586+
ref?: string;
587+
588+
constructor(reason?: RevertErrorReason, original?: Error, ref?: string);
589+
constructor(message?: string, original?: Error);
590+
constructor(messageOrReason: string | RevertErrorReason | undefined, original?: Error, ref?: string) {
591+
let message;
592+
let reason: RevertErrorReason | undefined;
593+
if (messageOrReason == null) {
594+
message = 'Unable to revert';
595+
} else if (typeof messageOrReason === 'string') {
596+
message = messageOrReason;
597+
reason = undefined;
598+
} else {
599+
reason = messageOrReason;
600+
message = RevertError.buildRevertErrorMessage(reason, ref);
601+
}
602+
super(message);
603+
604+
this.original = original;
605+
this.reason = reason;
606+
this.ref = ref;
607+
Error.captureStackTrace?.(this, RevertError);
608+
}
609+
610+
WithRef(ref: string): this {
611+
this.ref = ref;
612+
this.message = RevertError.buildRevertErrorMessage(this.reason, ref);
613+
return this;
614+
}
615+
616+
private static buildRevertErrorMessage(reason?: RevertErrorReason, ref?: string): string {
617+
const baseMessage = `Unable to revert${ref ? ` revision '${ref}'` : ''}`;
618+
switch (reason) {
619+
case RevertErrorReason.BadRevision:
620+
return `${baseMessage} because it is not a valid revision.`;
621+
case RevertErrorReason.InvalidObjectName:
622+
return `${baseMessage} because it is not a valid object name.`;
623+
case RevertErrorReason.Conflict:
624+
return `${baseMessage} it has unresolved conflicts. Resolve the conflicts and try again.`;
625+
case RevertErrorReason.LocalChangesWouldBeOverwritten:
626+
return `${baseMessage} because local changes would be overwritten. Commit or stash your changes first.`;
627+
default:
628+
return `${baseMessage}.`;
629+
}
630+
}
631+
}

src/git/gitProvider.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ export interface GitProviderRepository {
125125
addRemote?(repoPath: string, name: string, url: string, options?: { fetch?: boolean }): Promise<void>;
126126
pruneRemote?(repoPath: string, name: string): Promise<void>;
127127
removeRemote?(repoPath: string, name: string): Promise<void>;
128+
revert?(repoPath: string, ref: string, options?: { edit?: boolean }): Promise<void>;
128129

129130
applyUnreachableCommitForPatch?(
130131
repoPath: string,

src/git/gitProviderService.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1334,6 +1334,28 @@ export class GitProviderService implements Disposable {
13341334
return provider.removeRemote(path, name);
13351335
}
13361336

1337+
@log()
1338+
async revert(repoPath: string | Uri, ref: string, flags: string[] | undefined = []): Promise<void> {
1339+
const { provider, path } = this.getProvider(repoPath);
1340+
if (provider.revert == null) throw new ProviderNotSupportedError(provider.descriptor.name);
1341+
1342+
const options: { edit?: boolean } = {};
1343+
for (const flag of flags) {
1344+
switch (flag) {
1345+
case '--edit':
1346+
options.edit = true;
1347+
break;
1348+
case '--no-edit':
1349+
options.edit = false;
1350+
break;
1351+
default:
1352+
break;
1353+
}
1354+
}
1355+
1356+
return provider.revert(path, ref, options);
1357+
}
1358+
13371359
@log()
13381360
applyChangesToWorkingFile(uri: GitUri, ref1?: string, ref2?: string): Promise<void> {
13391361
const { provider } = this.getProvider(uri);

src/git/models/repository.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -870,11 +870,6 @@ export class Repository implements Disposable {
870870
}
871871
}
872872

873-
@log()
874-
revert(...args: string[]) {
875-
void this.runTerminalCommand('revert', ...args);
876-
}
877-
878873
async setRemoteAsDefault(remote: GitRemote, value: boolean = true) {
879874
await this.container.storage.storeWorkspace('remote:default', value ? remote.name : undefined);
880875

src/messages.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,22 @@ export function showIntegrationRequestTimedOutWarningMessage(providerName: strin
230230
);
231231
}
232232

233+
export async function showShouldCommitOrStashPrompt(): Promise<string | undefined> {
234+
const stash = { title: 'Stash' };
235+
const commit = { title: 'Commit' };
236+
const cancel = { title: 'Cancel', isCloseAffordance: true };
237+
const result = await showMessage(
238+
'warn',
239+
'You have changes in your working tree. Commit or stash them before reverting',
240+
undefined,
241+
null,
242+
stash,
243+
commit,
244+
cancel,
245+
);
246+
return result?.title;
247+
}
248+
233249
export async function showWhatsNewMessage(majorVersion: string) {
234250
const confirm = { title: 'OK', isCloseAffordance: true };
235251
const releaseNotes = { title: 'View Release Notes' };

0 commit comments

Comments
 (0)