Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

3530 Change revert terminal-run commands into normal commands w/ proper error handling #3789

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19568,29 +19568,29 @@
"bundle": "webpack --mode production",
"bundle:extension": "webpack --mode production --config-name extension:node",
"clean": "pnpx rimraf --glob dist out .vscode-test .vscode-test-web .eslintcache* tsconfig*.tsbuildinfo",
"clean:all": "pnpm run clean && pnpm run clean:deps",
"clean:all": "npm run clean && npm run clean:deps",
"clean:lint": "pnpx rimraf .eslintcache",
"clean:deps": "pnpx rimraf node_modules",
"copy:images": "webpack --config webpack.config.images.mjs",
"graph:link": "pnpm link @gitkraken/gitkraken-components",
"graph:link:main": "pushd \"../GitKrakenComponents\" && pnpm link && popd && pnpm graph:link",
"graph:unlink": "pnpm unlink @gitkraken/gitkraken-components && pnpm install --force",
"graph:unlink:main": "pnpm graph:unlink && pushd \"../GitKrakenComponents\" && pnpm unlink && popd",
"graph:link": "npm link @gitkraken/gitkraken-components",
"graph:link:main": "pushd \"../GitKrakenComponents\" && npm link && popd && npm graph:link",
"graph:unlink": "npm unlink @gitkraken/gitkraken-components && npm install --force",
"graph:unlink:main": "npm graph:unlink && pushd \"../GitKrakenComponents\" && npm unlink && popd",
"icons:apply": "node ./scripts/applyIconsContribution.mjs",
"icons:svgo": "svgo -q -f ./images/icons/ --config svgo.config.js",
"lint": "pnpm run clean:lint && eslint .",
"lint:fix": "pnpm run clean:lint && eslint . --fix",
"lint:webviews": "pnpm run clean:lint && eslint \"src/webviews/apps/**/*.ts?(x)\"",
"lint": "npm run clean:lint && eslint .",
"lint:fix": "npm run clean:lint && eslint . --fix",
"lint:webviews": "npm run clean:lint && eslint \"src/webviews/apps/**/*.ts?(x)\"",
"package": "vsce package --no-dependencies",
"package-pre": "pnpm run patch-pre && pnpm run package --pre-release",
"package-pre": "npm run patch-pre && npm run package --pre-release",
"patch-pre": "node ./scripts/applyPreReleasePatch.mjs",
"prep-release": "node ./scripts/prep-release.mjs",
"pretty": "prettier --config .prettierrc --write .",
"pretty:check": "prettier --config .prettierrc --check .",
"pub": "vsce publish --no-dependencies",
"pub-pre": "vsce publish --no-dependencies --pre-release",
"rebuild": "pnpm run reset && pnpm run build",
"reset": "pnpm run clean && pnpm install --force",
"rebuild": "npm run reset && npm run build",
"reset": "npm run clean && npm install --force",
"test": "vscode-test",
"test:e2e": "playwright test -c tests/e2e/playwright.config.ts",
"watch": "webpack --watch --mode development",
Expand Down
45 changes: 42 additions & 3 deletions src/commands/git/revert.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { Commands } from '../../constants.commands';
import type { Container } from '../../container';
import { RevertError, RevertErrorReason } from '../../git/errors';
import type { GitBranch } from '../../git/models/branch';
import type { GitLog } from '../../git/models/log';
import type { GitRevisionReference } from '../../git/models/reference';
import { getReferenceLabel } from '../../git/models/reference';
import type { Repository } from '../../git/models/repository';
import { showGenericErrorMessage, showShouldCommitOrStashPrompt } from '../../messages';
import type { FlagsQuickPickItem } from '../../quickpicks/items/flags';
import { createFlagsQuickPickItem } from '../../quickpicks/items/flags';
import { Logger } from '../../system/logger';
import { executeCommand, executeCoreCommand } from '../../system/vscode/command';
import type { ViewsWithRepositoryFolders } from '../../views/viewBase';
import type {
PartialStepState,
Expand Down Expand Up @@ -71,8 +76,42 @@ export class RevertGitCommand extends QuickCommand<State> {
return false;
}

execute(state: RevertStepState<State<GitRevisionReference[]>>) {
state.repo.revert(...state.flags, ...state.references.map(c => c.ref).reverse());
async execute(state: RevertStepState<State<GitRevisionReference[]>>) {
for (const ref of state.references.reverse()) {
try {
await state.repo.git.revert(ref.ref, state.flags);
} catch (ex) {
if (ex instanceof RevertError) {
let shouldRetry = false;
if (ex.reason === RevertErrorReason.LocalChangesWouldBeOverwritten) {
const response = await showShouldCommitOrStashPrompt();
if (response === 'Stash') {
await executeCommand(Commands.GitCommandsStashPush);
shouldRetry = true;
} else if (response === 'Commit') {
await executeCoreCommand('workbench.view.scm');
shouldRetry = true;
} else {
continue;
}
}

if (shouldRetry) {
try {
await state.repo.git.revert(ref.ref, state.flags);
} catch (ex) {
Logger.error(ex, this.title);
void showGenericErrorMessage(ex.message);
}
}

continue;
}

Logger.error(ex, this.title);
void showGenericErrorMessage(ex.message);
}
}
}

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

endSteps(state);
this.execute(state as RevertStepState<State<GitRevisionReference[]>>);
await this.execute(state as RevertStepState<State<GitRevisionReference[]>>);
}

return state.counter < 0 ? StepResultBreak : undefined;
Expand Down
25 changes: 25 additions & 0 deletions src/env/node/git/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import {
PullErrorReason,
PushError,
PushErrorReason,
RevertError,
RevertErrorReason,
StashPushError,
StashPushErrorReason,
TagError,
Expand Down Expand Up @@ -105,6 +107,7 @@ export const GitErrors = {
tagNotFound: /tag .* not found/i,
invalidTagName: /invalid tag name/i,
remoteRejected: /rejected because the remote contains work/i,
revertHasConflicts: /(error: could not revert .*) (hint: After resolving the conflicts)/gi,
};

const GitWarnings = {
Expand Down Expand Up @@ -173,6 +176,13 @@ const tagErrorAndReason: [RegExp, TagErrorReason][] = [
[GitErrors.remoteRejected, TagErrorReason.RemoteRejected],
];

const revertErrorAndReason = [
[GitErrors.badRevision, RevertErrorReason.BadRevision],
[GitErrors.invalidObjectName, RevertErrorReason.InvalidObjectName],
[GitErrors.revertHasConflicts, RevertErrorReason.Conflict],
[GitErrors.changesWouldBeOverwritten, RevertErrorReason.LocalChangesWouldBeOverwritten],
];

export class Git {
/** Map of running git commands -- avoids running duplicate overlaping commands */
private readonly pendingCommands = new Map<string, Promise<string | Buffer>>();
Expand Down Expand Up @@ -1588,6 +1598,21 @@ export class Git {
return this.git<string>({ cwd: repoPath }, 'reset', '-q', '--', ...pathspecs);
}

async revert(repoPath: string, ...args: string[]) {
try {
await this.git<string>({ cwd: repoPath }, 'revert', ...args);
} catch (ex) {
const msg: string = ex?.toString() ?? '';
for (const [error, reason] of revertErrorAndReason) {
if (error.test(msg) || error.test(ex.stderr ?? '')) {
throw new RevertError(reason, ex);
}
}

throw new RevertError(RevertErrorReason.Other, ex);
}
}

async rev_list(
repoPath: string,
ref: string,
Expand Down
19 changes: 19 additions & 0 deletions src/env/node/git/localGitProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
PullError,
PushError,
PushErrorReason,
RevertError,
StashApplyError,
StashApplyErrorReason,
StashPushError,
Expand Down Expand Up @@ -6082,6 +6083,24 @@ export class LocalGitProvider implements GitProvider, Disposable {
return worktrees;
}

@log()
async revert(repoPath: string, ref: string, options?: { edit?: boolean }): Promise<void> {
const args = [];
if (options.edit !== undefined) {
args.push(options.edit ? '--edit' : '--no-edit');
}

try {
await this.git.revert(repoPath, ...args, ref);
} catch (ex) {
if (ex instanceof RevertError) {
throw ex.WithRef(ref);
}

throw ex;
}
}

@log()
// eslint-disable-next-line @typescript-eslint/require-await
async getWorktreesDefaultUri(repoPath: string): Promise<Uri | undefined> {
Expand Down
62 changes: 62 additions & 0 deletions src/git/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -567,3 +567,65 @@ export class TagError extends Error {
return this;
}
}

export const enum RevertErrorReason {
BadRevision,
InvalidObjectName,
Conflict,
LocalChangesWouldBeOverwritten,
Other,
}

export class RevertError extends Error {
static is(ex: unknown, reason?: RevertErrorReason): ex is RevertError {
return ex instanceof RevertError && (reason == null || ex.reason === reason);
}

readonly original?: Error;
readonly reason: RevertErrorReason | undefined;
ref?: string;

constructor(reason?: RevertErrorReason, original?: Error, ref?: string);
constructor(message?: string, original?: Error);
constructor(messageOrReason: string | RevertErrorReason | undefined, original?: Error, ref?: string) {
let message;
let reason: RevertErrorReason | undefined;
if (messageOrReason == null) {
message = 'Unable to revert';
} else if (typeof messageOrReason === 'string') {
message = messageOrReason;
reason = undefined;
} else {
reason = messageOrReason;
message = RevertError.buildRevertErrorMessage(reason, ref);
}
super(message);

this.original = original;
this.reason = reason;
this.ref = ref;
Error.captureStackTrace?.(this, RevertError);
}

WithRef(ref: string): this {
this.ref = ref;
this.message = RevertError.buildRevertErrorMessage(this.reason, ref);
return this;
}

private static buildRevertErrorMessage(reason?: RevertErrorReason, ref?: string): string {
const baseMessage = `Unable to revert${ref ? ` revision '${ref}'` : ''}`;
switch (reason) {
case RevertErrorReason.BadRevision:
return `${baseMessage} because it is not a valid revision.`;
case RevertErrorReason.InvalidObjectName:
return `${baseMessage} because it is not a valid object name.`;
case RevertErrorReason.Conflict:
return `${baseMessage} it has unresolved conflicts. Resolve the conflicts and try again.`;
case RevertErrorReason.LocalChangesWouldBeOverwritten:
return `${baseMessage} because local changes would be overwritten. Commit or stash your changes first.`;
default:
return `${baseMessage}.`;
}
}
}
1 change: 1 addition & 0 deletions src/git/gitProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ export interface GitProviderRepository {
addRemote?(repoPath: string, name: string, url: string, options?: { fetch?: boolean }): Promise<void>;
pruneRemote?(repoPath: string, name: string): Promise<void>;
removeRemote?(repoPath: string, name: string): Promise<void>;
revert?(repoPath: string, ref: string, options?: { edit?: boolean }): Promise<void>;

applyUnreachableCommitForPatch?(
repoPath: string,
Expand Down
22 changes: 22 additions & 0 deletions src/git/gitProviderService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1334,6 +1334,28 @@ export class GitProviderService implements Disposable {
return provider.removeRemote(path, name);
}

@log()
async revert(repoPath: string | Uri, ref: string, flags: string[] | undefined = []): Promise<void> {
const { provider, path } = this.getProvider(repoPath);
if (provider.revert == null) throw new ProviderNotSupportedError(provider.descriptor.name);

const options: { edit?: boolean } = {};
for (const flag of flags) {
switch (flag) {
case '--edit':
options.edit = true;
break;
case '--no-edit':
options.edit = false;
break;
default:
break;
}
}

return provider.revert(path, ref, options);
}

@log()
applyChangesToWorkingFile(uri: GitUri, ref1?: string, ref2?: string): Promise<void> {
const { provider } = this.getProvider(uri);
Expand Down
5 changes: 0 additions & 5 deletions src/git/models/repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -870,11 +870,6 @@ export class Repository implements Disposable {
}
}

@log()
revert(...args: string[]) {
void this.runTerminalCommand('revert', ...args);
}

async setRemoteAsDefault(remote: GitRemote, value: boolean = true) {
await this.container.storage.storeWorkspace('remote:default', value ? remote.name : undefined);

Expand Down
16 changes: 16 additions & 0 deletions src/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,22 @@ export function showIntegrationRequestTimedOutWarningMessage(providerName: strin
);
}

export async function showShouldCommitOrStashPrompt(): Promise<string | undefined> {
const stash = { title: 'Stash' };
const commit = { title: 'Commit' };
const cancel = { title: 'Cancel', isCloseAffordance: true };
const result = await showMessage(
'warn',
'You have changes in your working tree. Commit or stash them before reverting',
undefined,
null,
stash,
commit,
cancel,
);
return result?.title;
}

export async function showWhatsNewMessage(majorVersion: string) {
const confirm = { title: 'OK', isCloseAffordance: true };
const releaseNotes = { title: 'View Release Notes' };
Expand Down