Skip to content
Merged
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
19 changes: 16 additions & 3 deletions src/commands/git/reset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import type { GitLog } from '../../git/models/log';
import type { GitReference, GitRevisionReference, GitTagReference } from '../../git/models/reference';
import { getReferenceLabel } from '../../git/models/reference.utils';
import type { Repository } from '../../git/models/repository';
import { showGenericErrorMessage } from '../../messages';
import type { FlagsQuickPickItem } from '../../quickpicks/items/flags';
import { createFlagsQuickPickItem } from '../../quickpicks/items/flags';
import { Logger } from '../../system/logger';
import type { ViewsWithRepositoryFolders } from '../../views/viewBase';
import type {
PartialStepState,
Expand Down Expand Up @@ -69,8 +71,19 @@ export class ResetGitCommand extends QuickCommand<State> {
return this._canSkipConfirm;
}

execute(state: ResetStepState) {
state.repo.reset(...state.flags, state.reference.ref);
async execute(state: ResetStepState) {
try {
await state.repo.git.reset(
{
hard: state.flags.includes('--hard'),
soft: state.flags.includes('--soft'),
},
state.reference.ref,
);
} catch (ex) {
Logger.error(ex, this.title);
void showGenericErrorMessage(ex.message);
}
}

protected async *steps(state: PartialStepState<State>): StepGenerator {
Expand Down Expand Up @@ -156,7 +169,7 @@ export class ResetGitCommand extends QuickCommand<State> {
}

endSteps(state);
this.execute(state as ResetStepState);
await this.execute(state as ResetStepState);
}

return state.counter < 0 ? StepResultBreak : undefined;
Expand Down
68 changes: 55 additions & 13 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,
ResetError,
ResetErrorReason,
StashPushError,
StashPushErrorReason,
TagError,
Expand Down Expand Up @@ -73,42 +75,47 @@ const textDecoder = new TextDecoder('utf8');
const rootSha = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';

export const GitErrors = {
alreadyCheckedOut: /already checked out/i,
alreadyExists: /already exists/i,
ambiguousArgument: /fatal:\s*ambiguous argument ['"].+['"]: unknown revision or path not in the working tree/i,
badRevision: /bad revision '(.*?)'/i,
cantLockRef: /cannot lock ref|unable to update local ref/i,
changesWouldBeOverwritten:
/Your local changes to the following files would be overwritten|Your local changes would be overwritten/i,
commitChangesFirst: /Please, commit your changes before you can/i,
conflict: /^CONFLICT \([^)]+\): \b/m,
detachedHead: /You are in 'detached HEAD' state/i,
entryNotUpToDate: /error:\s*Entry ['"].+['"] not uptodate\. Cannot merge\./i,
failedToDeleteDirectoryNotEmpty: /failed to delete '(.*?)': Directory not empty/i,
invalidLineCount: /file .+? has only \d+ lines/i,
invalidObjectName: /invalid object name: (.*)\s/i,
invalidObjectNameList: /could not open object name list: (.*)\s/i,
invalidTagName: /invalid tag name/i,
mainWorkingTree: /is a main working tree/i,
noFastForward: /\(non-fast-forward\)/i,
noMergeBase: /no merge base/i,
noRemoteRepositorySpecified: /No remote repository specified\./i,
noUpstream: /^fatal: The current branch .* has no upstream branch/i,
notAValidObjectName: /Not a valid object name/i,
notAWorkingTree: /'(.*?)' is not a working tree/i,
noUserNameConfigured: /Please tell me who you are\./i,
invalidLineCount: /file .+? has only \d+ lines/i,
uncommittedChanges: /contains modified or untracked files/i,
alreadyExists: /already exists/i,
alreadyCheckedOut: /already checked out/i,
mainWorkingTree: /is a main working tree/i,
noUpstream: /^fatal: The current branch .* has no upstream branch/i,
noPausedOperation:
/no merge (?:in progress|to abort)|no cherry-pick(?: or revert)? in progress|no rebase in progress/i,
permissionDenied: /Permission.*denied/i,
pushRejected: /^error: failed to push some refs to\b/m,
rebaseMultipleBranches: /cannot rebase onto multiple branches/i,
refLocked: /fatal:\s*cannot lock ref ['"].+['"]: unable to create file/i,
remoteAhead: /rejected because the remote contains work/i,
remoteConnection: /Could not read from remote repository/i,
tagConflict: /! \[rejected\].*\(would clobber existing tag\)/m,
unmergedFiles: /is not possible because you have unmerged files|You have unmerged files/i,
unstagedChanges: /You have unstaged changes/i,
remoteRejected: /rejected because the remote contains work/i,
tagAlreadyExists: /tag .* already exists/i,
tagConflict: /! \[rejected\].*\(would clobber existing tag\)/m,
tagNotFound: /tag .* not found/i,
invalidTagName: /invalid tag name/i,
remoteRejected: /rejected because the remote contains work/i,
uncommittedChanges: /contains modified or untracked files/i,
unmergedChanges: /error:\s*you need to resolve your current index first/i,
unmergedFiles: /is not possible because you have unmerged files|You have unmerged files/i,
unresolvedConflicts: /You must edit all merge conflicts|Resolve all conflicts/i,
unstagedChanges: /You have unstaged changes/i,
};

const GitWarnings = {
Expand Down Expand Up @@ -177,6 +184,16 @@ const tagErrorAndReason: [RegExp, TagErrorReason][] = [
[GitErrors.remoteRejected, TagErrorReason.RemoteRejected],
];

const resetErrorAndReason: [RegExp, ResetErrorReason][] = [
[GitErrors.ambiguousArgument, ResetErrorReason.AmbiguousArgument],
[GitErrors.changesWouldBeOverwritten, ResetErrorReason.ChangesWouldBeOverwritten],
[GitErrors.detachedHead, ResetErrorReason.DetachedHead],
[GitErrors.entryNotUpToDate, ResetErrorReason.EntryNotUpToDate],
[GitErrors.permissionDenied, ResetErrorReason.PermissionDenied],
[GitErrors.refLocked, ResetErrorReason.RefLocked],
[GitErrors.unmergedChanges, ResetErrorReason.UnmergedChanges],
];

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 @@ -1627,8 +1644,33 @@ export class Git {
return this.exec<string>({ cwd: repoPath }, 'remote', 'get-url', remote);
}

reset(repoPath: string | undefined, pathspecs: string[]) {
return this.exec<string>({ cwd: repoPath }, 'reset', '-q', '--', ...pathspecs);
async reset(
repoPath: string,
pathspecs: string[],
options?: { hard?: boolean; soft?: never; ref?: string } | { soft?: boolean; hard?: never; ref?: string },
): Promise<void> {
try {
const flags = [];
if (options?.hard) {
flags.push('--hard');
} else if (options?.soft) {
flags.push('--soft');
}

if (options?.ref) {
flags.push(options.ref);
}
await this.exec<string>({ cwd: repoPath }, 'reset', '-q', ...flags, '--', ...pathspecs);
} catch (ex) {
const msg: string = ex?.toString() ?? '';
for (const [error, reason] of resetErrorAndReason) {
if (error.test(msg) || error.test(ex.stderr ?? '')) {
throw new ResetError(reason, ex);
}
}

throw new ResetError(ResetErrorReason.Other, ex);
}
}

async rev_list(
Expand Down
5 changes: 5 additions & 0 deletions src/env/node/git/localGitProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4942,6 +4942,11 @@ export class LocalGitProvider implements GitProvider, Disposable {
}
}

@log()
async reset(repoPath: string, ref: string, options?: { hard?: boolean } | { soft?: boolean }): Promise<void> {
await this.git.reset(repoPath, [], { ...options, ref: ref });
}

@log({ args: { 2: false } })
async runGitCommandViaTerminal(
repoPath: string,
Expand Down
12 changes: 9 additions & 3 deletions src/git/actions/repository.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type { ResetGitCommandArgs } from '../../commands/git/reset';
import { Container } from '../../container';
import type { ViewsWithRepositoryFolders } from '../../views/viewBase';
import { executeGitCommand } from '../actions';
Expand Down Expand Up @@ -41,11 +40,18 @@ export function rebase(repo?: string | Repository, ref?: GitReference, interacti
export function reset(
repo?: string | Repository,
ref?: GitRevisionReference | GitTagReference,
flags?: NonNullable<ResetGitCommandArgs['state']>['flags'],
options?: { hard?: boolean; soft?: never } | { hard?: never; soft?: boolean },
) {
const flags: Array<'--hard' | '--soft'> = [];
if (options?.hard) {
flags.push('--hard');
} else if (options?.soft) {
flags.push('--soft');
}

return executeGitCommand({
command: 'reset',
confirm: flags == null || flags.includes('--hard'),
confirm: options == null || options.hard,
state: { repo: repo, reference: ref, flags: flags },
});
}
Expand Down
63 changes: 63 additions & 0 deletions src/git/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -632,3 +632,66 @@ export class PausedOperationContinueError extends Error {
Error.captureStackTrace?.(this, PausedOperationContinueError);
}
}

export const enum ResetErrorReason {
AmbiguousArgument,
ChangesWouldBeOverwritten,
DetachedHead,
EntryNotUpToDate,
PermissionDenied,
RefLocked,
Other,
UnmergedChanges,
}

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

readonly original?: Error;
readonly reason: ResetErrorReason | undefined;
constructor(reason?: ResetErrorReason, original?: Error);
constructor(message?: string, original?: Error);
constructor(messageOrReason: string | ResetErrorReason | undefined, original?: Error) {
let message;
let reason: ResetErrorReason | undefined;
if (messageOrReason == null) {
message = 'Unable to reset';
} else if (typeof messageOrReason === 'string') {
message = messageOrReason;
reason = undefined;
} else {
reason = messageOrReason;
message = 'Unable to reset';
switch (reason) {
case ResetErrorReason.UnmergedChanges:
message = `${message} because there are unmerged changes`;
break;
case ResetErrorReason.AmbiguousArgument:
message = `${message} because the argument is ambiguous`;
break;
case ResetErrorReason.EntryNotUpToDate:
message = `${message} because the entry is not up to date`;
break;
case ResetErrorReason.RefLocked:
message = `${message} because the ref is locked`;
break;
case ResetErrorReason.PermissionDenied:
message = `${message} because you don't have permission to modify affected files`;
break;
case ResetErrorReason.DetachedHead:
message = `${message} because you are in a detached HEAD state`;
break;
case ResetErrorReason.ChangesWouldBeOverwritten:
message = `${message} because your local changes would be overwritten`;
break;
}
}
super(message);

this.original = original;
this.reason = reason;
Error.captureStackTrace?.(this, ResetError);
}
}
2 changes: 2 additions & 0 deletions src/git/gitProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ export interface GitProviderRepository {
pruneRemote?(repoPath: string, name: string): Promise<void>;
removeRemote?(repoPath: string, name: string): Promise<void>;

reset?(repoPath: string, ref: string, options?: { hard?: boolean } | { soft?: boolean }): Promise<void>;

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

@log()
async reset(
repoPath: string | Uri,
options: { hard?: boolean } | { soft?: boolean } = {},
ref: string,
): Promise<void> {
const { provider, path } = this.getProvider(repoPath);
if (provider.reset == null) throw new ProviderNotSupportedError(provider.descriptor.name);

return provider.reset(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 @@ -822,11 +822,6 @@ export class Repository implements Disposable {
);
}

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

resume() {
if (!this._suspended) return;

Expand Down
Loading