Skip to content

Commit 6f23b56

Browse files
create git reset command (#3676)
rebase & lint fixes check also stderr for errors Co-authored-by: Ramin Tadayon <[email protected]>
1 parent 12c62fc commit 6f23b56

File tree

8 files changed

+162
-24
lines changed

8 files changed

+162
-24
lines changed

src/commands/git/reset.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ import type { GitLog } from '../../git/models/log';
44
import type { GitReference, GitRevisionReference, GitTagReference } from '../../git/models/reference';
55
import { getReferenceLabel } from '../../git/models/reference.utils';
66
import type { Repository } from '../../git/models/repository';
7+
import { showGenericErrorMessage } from '../../messages';
78
import type { FlagsQuickPickItem } from '../../quickpicks/items/flags';
89
import { createFlagsQuickPickItem } from '../../quickpicks/items/flags';
10+
import { Logger } from '../../system/logger';
911
import type { ViewsWithRepositoryFolders } from '../../views/viewBase';
1012
import type {
1113
PartialStepState,
@@ -69,8 +71,19 @@ export class ResetGitCommand extends QuickCommand<State> {
6971
return this._canSkipConfirm;
7072
}
7173

72-
execute(state: ResetStepState) {
73-
state.repo.reset(...state.flags, state.reference.ref);
74+
async execute(state: ResetStepState) {
75+
try {
76+
await state.repo.git.reset(
77+
{
78+
hard: state.flags.includes('--hard'),
79+
soft: state.flags.includes('--soft'),
80+
},
81+
state.reference.ref,
82+
);
83+
} catch (ex) {
84+
Logger.error(ex, this.title);
85+
void showGenericErrorMessage(ex.message);
86+
}
7487
}
7588

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

158171
endSteps(state);
159-
this.execute(state as ResetStepState);
172+
await this.execute(state as ResetStepState);
160173
}
161174

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

src/env/node/git/git.ts

Lines changed: 55 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import {
2020
PullErrorReason,
2121
PushError,
2222
PushErrorReason,
23+
ResetError,
24+
ResetErrorReason,
2325
StashPushError,
2426
StashPushErrorReason,
2527
TagError,
@@ -73,42 +75,47 @@ const textDecoder = new TextDecoder('utf8');
7375
const rootSha = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
7476

7577
export const GitErrors = {
78+
alreadyCheckedOut: /already checked out/i,
79+
alreadyExists: /already exists/i,
80+
ambiguousArgument: /fatal:\s*ambiguous argument ['"].+['"]: unknown revision or path not in the working tree/i,
7681
badRevision: /bad revision '(.*?)'/i,
7782
cantLockRef: /cannot lock ref|unable to update local ref/i,
7883
changesWouldBeOverwritten:
7984
/Your local changes to the following files would be overwritten|Your local changes would be overwritten/i,
8085
commitChangesFirst: /Please, commit your changes before you can/i,
8186
conflict: /^CONFLICT \([^)]+\): \b/m,
87+
detachedHead: /You are in 'detached HEAD' state/i,
88+
entryNotUpToDate: /error:\s*Entry ['"].+['"] not uptodate\. Cannot merge\./i,
8289
failedToDeleteDirectoryNotEmpty: /failed to delete '(.*?)': Directory not empty/i,
90+
invalidLineCount: /file .+? has only \d+ lines/i,
8391
invalidObjectName: /invalid object name: (.*)\s/i,
8492
invalidObjectNameList: /could not open object name list: (.*)\s/i,
93+
invalidTagName: /invalid tag name/i,
94+
mainWorkingTree: /is a main working tree/i,
8595
noFastForward: /\(non-fast-forward\)/i,
8696
noMergeBase: /no merge base/i,
8797
noRemoteRepositorySpecified: /No remote repository specified\./i,
98+
noUpstream: /^fatal: The current branch .* has no upstream branch/i,
8899
notAValidObjectName: /Not a valid object name/i,
89100
notAWorkingTree: /'(.*?)' is not a working tree/i,
90101
noUserNameConfigured: /Please tell me who you are\./i,
91-
invalidLineCount: /file .+? has only \d+ lines/i,
92-
uncommittedChanges: /contains modified or untracked files/i,
93-
alreadyExists: /already exists/i,
94-
alreadyCheckedOut: /already checked out/i,
95-
mainWorkingTree: /is a main working tree/i,
96-
noUpstream: /^fatal: The current branch .* has no upstream branch/i,
97102
noPausedOperation:
98103
/no merge (?:in progress|to abort)|no cherry-pick(?: or revert)? in progress|no rebase in progress/i,
99104
permissionDenied: /Permission.*denied/i,
100105
pushRejected: /^error: failed to push some refs to\b/m,
101106
rebaseMultipleBranches: /cannot rebase onto multiple branches/i,
107+
refLocked: /fatal:\s*cannot lock ref ['"].+['"]: unable to create file/i,
102108
remoteAhead: /rejected because the remote contains work/i,
103109
remoteConnection: /Could not read from remote repository/i,
104-
tagConflict: /! \[rejected\].*\(would clobber existing tag\)/m,
105-
unmergedFiles: /is not possible because you have unmerged files|You have unmerged files/i,
106-
unstagedChanges: /You have unstaged changes/i,
110+
remoteRejected: /rejected because the remote contains work/i,
107111
tagAlreadyExists: /tag .* already exists/i,
112+
tagConflict: /! \[rejected\].*\(would clobber existing tag\)/m,
108113
tagNotFound: /tag .* not found/i,
109-
invalidTagName: /invalid tag name/i,
110-
remoteRejected: /rejected because the remote contains work/i,
114+
uncommittedChanges: /contains modified or untracked files/i,
115+
unmergedChanges: /error:\s*you need to resolve your current index first/i,
116+
unmergedFiles: /is not possible because you have unmerged files|You have unmerged files/i,
111117
unresolvedConflicts: /You must edit all merge conflicts|Resolve all conflicts/i,
118+
unstagedChanges: /You have unstaged changes/i,
112119
};
113120

114121
const GitWarnings = {
@@ -177,6 +184,16 @@ const tagErrorAndReason: [RegExp, TagErrorReason][] = [
177184
[GitErrors.remoteRejected, TagErrorReason.RemoteRejected],
178185
];
179186

187+
const resetErrorAndReason: [RegExp, ResetErrorReason][] = [
188+
[GitErrors.ambiguousArgument, ResetErrorReason.AmbiguousArgument],
189+
[GitErrors.changesWouldBeOverwritten, ResetErrorReason.ChangesWouldBeOverwritten],
190+
[GitErrors.detachedHead, ResetErrorReason.DetachedHead],
191+
[GitErrors.entryNotUpToDate, ResetErrorReason.EntryNotUpToDate],
192+
[GitErrors.permissionDenied, ResetErrorReason.PermissionDenied],
193+
[GitErrors.refLocked, ResetErrorReason.RefLocked],
194+
[GitErrors.unmergedChanges, ResetErrorReason.UnmergedChanges],
195+
];
196+
180197
export class Git {
181198
/** Map of running git commands -- avoids running duplicate overlaping commands */
182199
private readonly pendingCommands = new Map<string, Promise<string | Buffer>>();
@@ -1627,8 +1644,33 @@ export class Git {
16271644
return this.exec<string>({ cwd: repoPath }, 'remote', 'get-url', remote);
16281645
}
16291646

1630-
reset(repoPath: string | undefined, pathspecs: string[]) {
1631-
return this.exec<string>({ cwd: repoPath }, 'reset', '-q', '--', ...pathspecs);
1647+
async reset(
1648+
repoPath: string,
1649+
pathspecs: string[],
1650+
options?: { hard?: boolean; soft?: never; ref?: string } | { soft?: boolean; hard?: never; ref?: string },
1651+
): Promise<void> {
1652+
try {
1653+
const flags = [];
1654+
if (options?.hard) {
1655+
flags.push('--hard');
1656+
} else if (options?.soft) {
1657+
flags.push('--soft');
1658+
}
1659+
1660+
if (options?.ref) {
1661+
flags.push(options.ref);
1662+
}
1663+
await this.exec<string>({ cwd: repoPath }, 'reset', '-q', ...flags, '--', ...pathspecs);
1664+
} catch (ex) {
1665+
const msg: string = ex?.toString() ?? '';
1666+
for (const [error, reason] of resetErrorAndReason) {
1667+
if (error.test(msg) || error.test(ex.stderr ?? '')) {
1668+
throw new ResetError(reason, ex);
1669+
}
1670+
}
1671+
1672+
throw new ResetError(ResetErrorReason.Other, ex);
1673+
}
16321674
}
16331675

16341676
async rev_list(

src/env/node/git/localGitProvider.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4942,6 +4942,11 @@ export class LocalGitProvider implements GitProvider, Disposable {
49424942
}
49434943
}
49444944

4945+
@log()
4946+
async reset(repoPath: string, ref: string, options?: { hard?: boolean } | { soft?: boolean }): Promise<void> {
4947+
await this.git.reset(repoPath, [], { ...options, ref: ref });
4948+
}
4949+
49454950
@log({ args: { 2: false } })
49464951
async runGitCommandViaTerminal(
49474952
repoPath: string,

src/git/actions/repository.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import type { ResetGitCommandArgs } from '../../commands/git/reset';
21
import { Container } from '../../container';
32
import type { ViewsWithRepositoryFolders } from '../../views/viewBase';
43
import { executeGitCommand } from '../actions';
@@ -41,11 +40,18 @@ export function rebase(repo?: string | Repository, ref?: GitReference, interacti
4140
export function reset(
4241
repo?: string | Repository,
4342
ref?: GitRevisionReference | GitTagReference,
44-
flags?: NonNullable<ResetGitCommandArgs['state']>['flags'],
43+
options?: { hard?: boolean; soft?: never } | { hard?: never; soft?: boolean },
4544
) {
45+
const flags: Array<'--hard' | '--soft'> = [];
46+
if (options?.hard) {
47+
flags.push('--hard');
48+
} else if (options?.soft) {
49+
flags.push('--soft');
50+
}
51+
4652
return executeGitCommand({
4753
command: 'reset',
48-
confirm: flags == null || flags.includes('--hard'),
54+
confirm: options == null || options.hard,
4955
state: { repo: repo, reference: ref, flags: flags },
5056
});
5157
}

src/git/errors.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -632,3 +632,66 @@ export class PausedOperationContinueError extends Error {
632632
Error.captureStackTrace?.(this, PausedOperationContinueError);
633633
}
634634
}
635+
636+
export const enum ResetErrorReason {
637+
AmbiguousArgument,
638+
ChangesWouldBeOverwritten,
639+
DetachedHead,
640+
EntryNotUpToDate,
641+
PermissionDenied,
642+
RefLocked,
643+
Other,
644+
UnmergedChanges,
645+
}
646+
647+
export class ResetError extends Error {
648+
static is(ex: unknown, reason?: ResetErrorReason): ex is ResetError {
649+
return ex instanceof ResetError && (reason == null || ex.reason === reason);
650+
}
651+
652+
readonly original?: Error;
653+
readonly reason: ResetErrorReason | undefined;
654+
constructor(reason?: ResetErrorReason, original?: Error);
655+
constructor(message?: string, original?: Error);
656+
constructor(messageOrReason: string | ResetErrorReason | undefined, original?: Error) {
657+
let message;
658+
let reason: ResetErrorReason | undefined;
659+
if (messageOrReason == null) {
660+
message = 'Unable to reset';
661+
} else if (typeof messageOrReason === 'string') {
662+
message = messageOrReason;
663+
reason = undefined;
664+
} else {
665+
reason = messageOrReason;
666+
message = 'Unable to reset';
667+
switch (reason) {
668+
case ResetErrorReason.UnmergedChanges:
669+
message = `${message} because there are unmerged changes`;
670+
break;
671+
case ResetErrorReason.AmbiguousArgument:
672+
message = `${message} because the argument is ambiguous`;
673+
break;
674+
case ResetErrorReason.EntryNotUpToDate:
675+
message = `${message} because the entry is not up to date`;
676+
break;
677+
case ResetErrorReason.RefLocked:
678+
message = `${message} because the ref is locked`;
679+
break;
680+
case ResetErrorReason.PermissionDenied:
681+
message = `${message} because you don't have permission to modify affected files`;
682+
break;
683+
case ResetErrorReason.DetachedHead:
684+
message = `${message} because you are in a detached HEAD state`;
685+
break;
686+
case ResetErrorReason.ChangesWouldBeOverwritten:
687+
message = `${message} because your local changes would be overwritten`;
688+
break;
689+
}
690+
}
691+
super(message);
692+
693+
this.original = original;
694+
this.reason = reason;
695+
Error.captureStackTrace?.(this, ResetError);
696+
}
697+
}

src/git/gitProvider.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,8 @@ export interface GitProviderRepository {
134134
pruneRemote?(repoPath: string, name: string): Promise<void>;
135135
removeRemote?(repoPath: string, name: string): Promise<void>;
136136

137+
reset?(repoPath: string, ref: string, options?: { hard?: boolean } | { soft?: boolean }): Promise<void>;
138+
137139
checkout?(
138140
repoPath: string,
139141
ref: string,

src/git/gitProviderService.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1349,6 +1349,18 @@ export class GitProviderService implements Disposable {
13491349
return provider.removeRemote(path, name);
13501350
}
13511351

1352+
@log()
1353+
async reset(
1354+
repoPath: string | Uri,
1355+
options: { hard?: boolean } | { soft?: boolean } = {},
1356+
ref: string,
1357+
): Promise<void> {
1358+
const { provider, path } = this.getProvider(repoPath);
1359+
if (provider.reset == null) throw new ProviderNotSupportedError(provider.descriptor.name);
1360+
1361+
return provider.reset(path, ref, options);
1362+
}
1363+
13521364
@log()
13531365
applyChangesToWorkingFile(uri: GitUri, ref1?: string, ref2?: string): Promise<void> {
13541366
const { provider } = this.getProvider(uri);

src/git/models/repository.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -822,11 +822,6 @@ export class Repository implements Disposable {
822822
);
823823
}
824824

825-
@log()
826-
reset(...args: string[]) {
827-
void this.runTerminalCommand('reset', ...args);
828-
}
829-
830825
resume() {
831826
if (!this._suspended) return;
832827

0 commit comments

Comments
 (0)