Skip to content

Commit 0a71aa6

Browse files
authored
3529 Change tag terminal-run commands into normal commands w/ proper error handling (#3670)
* change git tag terminal-run cmds into normal cmds * customize tag errors with action * rebase & lint errors
1 parent 70c9e2e commit 0a71aa6

File tree

7 files changed

+170
-30
lines changed

7 files changed

+170
-30
lines changed

src/commands/git/tag.ts

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@ import {
88
isTagReference,
99
} from '../../git/models/reference';
1010
import type { Repository } from '../../git/models/repository';
11+
import { showGenericErrorMessage } from '../../messages';
1112
import type { QuickPickItemOfT } from '../../quickpicks/items/common';
1213
import type { FlagsQuickPickItem } from '../../quickpicks/items/flags';
1314
import { createFlagsQuickPickItem } from '../../quickpicks/items/flags';
15+
import { Logger } from '../../system/logger';
1416
import { pluralize } from '../../system/string';
1517
import type { ViewsWithRepositoryFolders } from '../../views/viewBase';
1618
import type {
@@ -293,12 +295,12 @@ export class TagGitCommand extends QuickCommand<State> {
293295
}
294296

295297
endSteps(state);
296-
state.repo.tag(
297-
...state.flags,
298-
...(state.message.length !== 0 ? [`"${state.message}"`] : []),
299-
state.name,
300-
state.reference.ref,
301-
);
298+
try {
299+
await state.repo.git.createTag(state.name, state.reference.ref, state.message);
300+
} catch (ex) {
301+
Logger.error(ex, context.title);
302+
void showGenericErrorMessage(ex);
303+
}
302304
}
303305
}
304306

@@ -356,7 +358,7 @@ export class TagGitCommand extends QuickCommand<State> {
356358
return canPickStepContinue(step, state, selection) ? selection[0].item : StepResultBreak;
357359
}
358360

359-
private *deleteCommandSteps(state: DeleteStepState, context: Context): StepGenerator {
361+
private async *deleteCommandSteps(state: DeleteStepState, context: Context): StepGenerator {
360362
while (this.canStepsContinue(state)) {
361363
if (state.references != null && !Array.isArray(state.references)) {
362364
state.references = [state.references];
@@ -381,7 +383,14 @@ export class TagGitCommand extends QuickCommand<State> {
381383
if (result === StepResultBreak) continue;
382384

383385
endSteps(state);
384-
state.repo.tagDelete(state.references);
386+
for (const { ref } of state.references) {
387+
try {
388+
await state.repo.git.deleteTag(ref);
389+
} catch (ex) {
390+
Logger.error(ex, context.title);
391+
void showGenericErrorMessage(ex);
392+
}
393+
}
385394
}
386395
}
387396

src/env/node/git/git.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import {
2222
PushErrorReason,
2323
StashPushError,
2424
StashPushErrorReason,
25+
TagError,
26+
TagErrorReason,
2527
WorkspaceUntrustedError,
2628
} from '../../../git/errors';
2729
import type { GitDir } from '../../../git/gitProvider';
@@ -32,7 +34,6 @@ import type { GitUser } from '../../../git/models/user';
3234
import { parseGitBranchesDefaultFormat } from '../../../git/parsers/branchParser';
3335
import { parseGitLogAllFormat, parseGitLogDefaultFormat } from '../../../git/parsers/logParser';
3436
import { parseGitRemoteUrl } from '../../../git/parsers/remoteParser';
35-
import { parseGitTagsDefaultFormat } from '../../../git/parsers/tagParser';
3637
import { splitAt } from '../../../system/array';
3738
import { log } from '../../../system/decorators/log';
3839
import { join } from '../../../system/iterable';
@@ -100,6 +101,10 @@ export const GitErrors = {
100101
tagConflict: /! \[rejected\].*\(would clobber existing tag\)/m,
101102
unmergedFiles: /is not possible because you have unmerged files/i,
102103
unstagedChanges: /You have unstaged changes/i,
104+
tagAlreadyExists: /tag .* already exists/i,
105+
tagNotFound: /tag .* not found/i,
106+
invalidTagName: /invalid tag name/i,
107+
remoteRejected: /rejected because the remote contains work/i,
103108
};
104109

105110
const GitWarnings = {
@@ -160,6 +165,14 @@ function getStdinUniqueKey(): number {
160165
type ExitCodeOnlyGitCommandOptions = GitCommandOptions & { exitCodeOnly: true };
161166
export type PushForceOptions = { withLease: true; ifIncludes?: boolean } | { withLease: false; ifIncludes?: never };
162167

168+
const tagErrorAndReason: [RegExp, TagErrorReason][] = [
169+
[GitErrors.tagAlreadyExists, TagErrorReason.TagAlreadyExists],
170+
[GitErrors.tagNotFound, TagErrorReason.TagNotFound],
171+
[GitErrors.invalidTagName, TagErrorReason.InvalidTagName],
172+
[GitErrors.permissionDenied, TagErrorReason.PermissionDenied],
173+
[GitErrors.remoteRejected, TagErrorReason.RemoteRejected],
174+
];
175+
163176
export class Git {
164177
/** Map of running git commands -- avoids running duplicate overlaping commands */
165178
private readonly pendingCommands = new Map<string, Promise<string | Buffer>>();
@@ -2078,8 +2091,19 @@ export class Git {
20782091
return this.git<string>({ cwd: repoPath }, 'symbolic-ref', '--short', ref);
20792092
}
20802093

2081-
tag(repoPath: string) {
2082-
return this.git<string>({ cwd: repoPath }, 'tag', '-l', `--format=${parseGitTagsDefaultFormat}`);
2094+
async tag(repoPath: string, ...args: string[]) {
2095+
try {
2096+
const output = await this.git<string>({ cwd: repoPath }, 'tag', ...args);
2097+
return output;
2098+
} catch (ex) {
2099+
const msg: string = ex?.toString() ?? '';
2100+
for (const [error, reason] of tagErrorAndReason) {
2101+
if (error.test(msg) || error.test(ex.stderr ?? '')) {
2102+
throw new TagError(reason, ex);
2103+
}
2104+
}
2105+
throw new TagError(TagErrorReason.Other, ex);
2106+
}
20832107
}
20842108

20852109
worktree__add(

src/env/node/git/localGitProvider.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
StashApplyError,
3535
StashApplyErrorReason,
3636
StashPushError,
37+
TagError,
3738
WorktreeCreateError,
3839
WorktreeCreateErrorReason,
3940
WorktreeDeleteError,
@@ -161,7 +162,7 @@ import {
161162
import { parseGitRefLog, parseGitRefLogDefaultFormat } from '../../../git/parsers/reflogParser';
162163
import { parseGitRemotes } from '../../../git/parsers/remoteParser';
163164
import { parseGitStatus } from '../../../git/parsers/statusParser';
164-
import { parseGitTags } from '../../../git/parsers/tagParser';
165+
import { parseGitTags, parseGitTagsDefaultFormat } from '../../../git/parsers/tagParser';
165166
import { parseGitLsFiles, parseGitTree } from '../../../git/parsers/treeParser';
166167
import { parseGitWorktrees } from '../../../git/parsers/worktreeParser';
167168
import { getRemoteProviderMatcher, loadRemoteProviders } from '../../../git/remotes/remoteProviders';
@@ -1263,6 +1264,32 @@ export class LocalGitProvider implements GitProvider, Disposable {
12631264
await this.git.branch(repoPath, '-m', oldName, newName);
12641265
}
12651266

1267+
@log()
1268+
async createTag(repoPath: string, name: string, ref: string, message?: string): Promise<void> {
1269+
try {
1270+
await this.git.tag(repoPath, name, ref, ...(message != null && message.length > 0 ? ['-m', message] : []));
1271+
} catch (ex) {
1272+
if (ex instanceof TagError) {
1273+
throw ex.WithTag(name).WithAction('create');
1274+
}
1275+
1276+
throw ex;
1277+
}
1278+
}
1279+
1280+
@log()
1281+
async deleteTag(repoPath: string, name: string): Promise<void> {
1282+
try {
1283+
await this.git.tag(repoPath, '-d', name);
1284+
} catch (ex) {
1285+
if (ex instanceof TagError) {
1286+
throw ex.WithTag(name).WithAction('delete');
1287+
}
1288+
1289+
throw ex;
1290+
}
1291+
}
1292+
12661293
@log()
12671294
async checkout(
12681295
repoPath: string,
@@ -5117,7 +5144,7 @@ export class LocalGitProvider implements GitProvider, Disposable {
51175144
if (resultsPromise == null) {
51185145
async function load(this: LocalGitProvider): Promise<PagedResult<GitTag>> {
51195146
try {
5120-
const data = await this.git.tag(repoPath!);
5147+
const data = await this.git.tag(repoPath!, '-l', `--format=${parseGitTagsDefaultFormat}`);
51215148
return { values: parseGitTags(data, repoPath!) };
51225149
} catch (_ex) {
51235150
this._tagsCache.delete(repoPath!);

src/git/errors.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,3 +489,81 @@ export class WorktreeDeleteError extends Error {
489489
Error.captureStackTrace?.(this, WorktreeDeleteError);
490490
}
491491
}
492+
493+
export const enum TagErrorReason {
494+
TagAlreadyExists,
495+
TagNotFound,
496+
InvalidTagName,
497+
PermissionDenied,
498+
RemoteRejected,
499+
Other,
500+
}
501+
502+
export class TagError extends Error {
503+
static is(ex: unknown, reason?: TagErrorReason): ex is TagError {
504+
return ex instanceof TagError && (reason == null || ex.reason === reason);
505+
}
506+
507+
readonly original?: Error;
508+
readonly reason: TagErrorReason | undefined;
509+
action?: string;
510+
tag?: string;
511+
512+
private static buildTagErrorMessage(reason?: TagErrorReason, tag?: string, action?: string): string {
513+
let baseMessage: string;
514+
if (action != null) {
515+
baseMessage = `Unable to ${action} tag ${tag ? `'${tag}'` : ''}`;
516+
} else {
517+
baseMessage = `Unable to perform action${tag ? ` with tag '${tag}'` : 'on tag'}`;
518+
}
519+
520+
switch (reason) {
521+
case TagErrorReason.TagAlreadyExists:
522+
return `${baseMessage} because it already exists`;
523+
case TagErrorReason.TagNotFound:
524+
return `${baseMessage} because it does not exist`;
525+
case TagErrorReason.InvalidTagName:
526+
return `${baseMessage} because the tag name is invalid`;
527+
case TagErrorReason.PermissionDenied:
528+
return `${baseMessage} because you don't have permission to push to this remote repository.`;
529+
case TagErrorReason.RemoteRejected:
530+
return `${baseMessage} because the remote repository rejected the push.`;
531+
default:
532+
return baseMessage;
533+
}
534+
}
535+
536+
constructor(reason?: TagErrorReason, original?: Error, tag?: string, action?: string);
537+
constructor(message?: string, original?: Error);
538+
constructor(messageOrReason: string | TagErrorReason | undefined, original?: Error, tag?: string, action?: string) {
539+
let reason: TagErrorReason | undefined;
540+
if (typeof messageOrReason !== 'string') {
541+
reason = messageOrReason as TagErrorReason;
542+
} else {
543+
super(messageOrReason);
544+
}
545+
const message =
546+
typeof messageOrReason === 'string'
547+
? messageOrReason
548+
: TagError.buildTagErrorMessage(messageOrReason as TagErrorReason, tag, action);
549+
super(message);
550+
551+
this.original = original;
552+
this.reason = reason;
553+
this.tag = tag;
554+
this.action = action;
555+
Error.captureStackTrace?.(this, TagError);
556+
}
557+
558+
WithTag(tag: string) {
559+
this.tag = tag;
560+
this.message = TagError.buildTagErrorMessage(this.reason, tag, this.action);
561+
return this;
562+
}
563+
564+
WithAction(action: string) {
565+
this.action = action;
566+
this.message = TagError.buildTagErrorMessage(this.reason, this.tag, action);
567+
return this;
568+
}
569+
}

src/git/gitProvider.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,8 @@ export interface BranchContributorOverview {
120120
export interface GitProviderRepository {
121121
createBranch?(repoPath: string, name: string, ref: string): Promise<void>;
122122
renameBranch?(repoPath: string, oldName: string, newName: string): Promise<void>;
123-
123+
createTag?(repoPath: string, name: string, ref: string, message?: string): Promise<void>;
124+
deleteTag?(repoPath: string, name: string): Promise<void>;
124125
addRemote?(repoPath: string, name: string, url: string, options?: { fetch?: boolean }): Promise<void>;
125126
pruneRemote?(repoPath: string, name: string): Promise<void>;
126127
removeRemote?(repoPath: string, name: string): Promise<void>;

src/git/gitProviderService.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1377,6 +1377,22 @@ export class GitProviderService implements Disposable {
13771377
return provider.renameBranch(path, oldName, newName);
13781378
}
13791379

1380+
@log()
1381+
createTag(repoPath: string | Uri, name: string, ref: string, message?: string): Promise<void> {
1382+
const { provider, path } = this.getProvider(repoPath);
1383+
if (provider.createTag == null) throw new ProviderNotSupportedError(provider.descriptor.name);
1384+
1385+
return provider.createTag(path, name, ref, message);
1386+
}
1387+
1388+
@log()
1389+
deleteTag(repoPath: string | Uri, name: string): Promise<void> {
1390+
const { provider, path } = this.getProvider(repoPath);
1391+
if (provider.deleteTag == null) throw new ProviderNotSupportedError(provider.descriptor.name);
1392+
1393+
return provider.deleteTag(path, name);
1394+
}
1395+
13801396
@log()
13811397
checkout(
13821398
repoPath: string | Uri,

src/git/models/repository.ts

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import type { GitProviderDescriptor, GitProviderRepository } from '../gitProvide
2727
import type { GitProviderService } from '../gitProviderService';
2828
import type { GitBranch } from './branch';
2929
import { getBranchNameWithoutRemote, getRemoteNameFromBranchName } from './branch';
30-
import type { GitBranchReference, GitReference, GitTagReference } from './reference';
30+
import type { GitBranchReference, GitReference } from './reference';
3131
import { getNameWithoutRemote, isBranchReference } from './reference';
3232
import type { GitRemote } from './remote';
3333
import type { GitWorktree } from './worktree';
@@ -992,21 +992,6 @@ export class Repository implements Disposable {
992992
this._suspended = true;
993993
}
994994

995-
@log()
996-
tag(...args: string[]) {
997-
void this.runTerminalCommand('tag', ...args);
998-
}
999-
1000-
@log()
1001-
tagDelete(tags: GitTagReference | GitTagReference[]) {
1002-
if (!Array.isArray(tags)) {
1003-
tags = [tags];
1004-
}
1005-
1006-
const args = ['--delete'];
1007-
void this.runTerminalCommand('tag', ...args, ...tags.map(t => t.ref));
1008-
}
1009-
1010995
private _fsWatcherDisposable: Disposable | undefined;
1011996
private _fsWatchers = new Map<string, number>();
1012997
private _fsChangeDelay: number = defaultFileSystemChangeDelay;

0 commit comments

Comments
 (0)