Skip to content

Commit e0a643b

Browse files
authored
Changes delete branch command to avoid terminal (#4359)
1 parent a44d84b commit e0a643b

File tree

7 files changed

+266
-55
lines changed

7 files changed

+266
-55
lines changed

src/commands/git/branch.ts

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
import { QuickInputButtons } from 'vscode';
1+
import { QuickInputButtons, window } from 'vscode';
22
import type { Container } from '../../container';
3+
import { BranchError, BranchErrorReason } from '../../git/errors';
34
import type { IssueShape } from '../../git/models/issue';
45
import type { GitBranchReference, GitReference } from '../../git/models/reference';
56
import { Repository } from '../../git/models/repository';
67
import type { GitWorktree } from '../../git/models/worktree';
78
import { addAssociatedIssueToBranch } from '../../git/utils/-webview/branch.issue.utils';
89
import { getWorktreesByBranch } from '../../git/utils/-webview/worktree.utils';
10+
import { getBranchNameAndRemote } from '../../git/utils/branch.utils';
911
import {
1012
getReferenceLabel,
1113
getReferenceNameWithoutRemote,
@@ -430,7 +432,7 @@ export class BranchGitCommand extends QuickCommand {
430432
try {
431433
await state.repo.git.branches.createBranch?.(state.name, state.reference.ref);
432434
} catch (ex) {
433-
Logger.error(ex);
435+
Logger.error(ex, context.title);
434436
// TODO likely need some better error handling here
435437
return showGenericErrorMessage('Unable to create branch');
436438
}
@@ -571,10 +573,47 @@ export class BranchGitCommand extends QuickCommand {
571573
state.flags = result;
572574

573575
endSteps(state);
574-
state.repo.branchDelete(state.references, {
575-
force: state.flags.includes('--force'),
576-
remote: state.flags.includes('--remotes'),
577-
});
576+
577+
for (const ref of state.references) {
578+
const [name, remote] = getBranchNameAndRemote(ref);
579+
try {
580+
if (ref.remote) {
581+
await state.repo.git.branches.deleteRemoteBranch?.(name, remote!);
582+
} else {
583+
await state.repo.git.branches.deleteLocalBranch?.(name, {
584+
force: state.flags.includes('--force'),
585+
});
586+
if (state.flags.includes('--remotes') && remote) {
587+
await state.repo.git.branches.deleteRemoteBranch?.(name, remote);
588+
}
589+
}
590+
} catch (ex) {
591+
if (BranchError.is(ex, BranchErrorReason.BranchNotFullyMerged)) {
592+
const confirm = { title: 'Delete Branch' };
593+
const cancel = { title: 'Cancel', isCloseAffordance: true };
594+
const result = await window.showWarningMessage(
595+
`Unable to delete branch '${name}' as it is not fully merged. Do you want to delete it anyway?`,
596+
{ modal: true },
597+
confirm,
598+
cancel,
599+
);
600+
601+
if (result === confirm) {
602+
try {
603+
await state.repo.git.branches.deleteLocalBranch?.(name, { force: true });
604+
} catch (ex) {
605+
Logger.error(ex, context.title);
606+
void showGenericErrorMessage(ex);
607+
}
608+
}
609+
610+
continue;
611+
}
612+
613+
Logger.error(ex, context.title);
614+
void showGenericErrorMessage(ex);
615+
}
616+
}
578617
}
579618
}
580619

@@ -680,7 +719,7 @@ export class BranchGitCommand extends QuickCommand {
680719
try {
681720
await state.repo.git.branches.renameBranch?.(state.reference.ref, state.name);
682721
} catch (ex) {
683-
Logger.error(ex);
722+
Logger.error(ex, context.title);
684723
// TODO likely need some better error handling here
685724
return showGenericErrorMessage('Unable to rename branch');
686725
}

src/commands/git/tag.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,7 @@ export class TagGitCommand extends QuickCommand<State> {
383383
if (result === StepResultBreak) continue;
384384

385385
endSteps(state);
386+
386387
for (const { ref } of state.references) {
387388
try {
388389
await state.repo.git.tags.deleteTag?.(ref);

src/env/node/git/git.ts

Lines changed: 53 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import { GitErrorHandling } from '../../../git/commandOptions';
1616
import {
1717
BlameIgnoreRevsFileBadRevisionError,
1818
BlameIgnoreRevsFileError,
19+
BranchError,
20+
BranchErrorReason,
1921
FetchError,
2022
FetchErrorReason,
2123
PullError,
@@ -75,6 +77,8 @@ export const GitErrors = {
7577
alreadyExists: /already exists/i,
7678
ambiguousArgument: /fatal:\s*ambiguous argument ['"].+['"]: unknown revision or path not in the working tree/i,
7779
badRevision: /bad revision '(.*?)'/i,
80+
branchAlreadyExists: /fatal: A branch named '.+?' already exists/i,
81+
branchNotFullyMerged: /error: The branch '.+?' is not fully merged/i,
7882
cantLockRef: /cannot lock ref|unable to update local ref/i,
7983
changesWouldBeOverwritten:
8084
/Your local changes to the following files would be overwritten|Your local changes would be overwritten/i,
@@ -84,13 +88,15 @@ export const GitErrors = {
8488
emptyPreviousCherryPick: /The previous cherry-pick is now empty/i,
8589
entryNotUpToDate: /error:\s*Entry ['"].+['"] not uptodate\. Cannot merge\./i,
8690
failedToDeleteDirectoryNotEmpty: /failed to delete '(.*?)': Directory not empty/i,
91+
invalidBranchName: /fatal: '.+?' is not a valid branch name/i,
8792
invalidLineCount: /file .+? has only (\d+) lines/i,
8893
invalidObjectName: /invalid object name: (.*)\s/i,
8994
invalidObjectNameList: /could not open object name list: (.*)\s/i,
9095
invalidTagName: /invalid tag name/i,
9196
mainWorkingTree: /is a main working tree/i,
9297
noFastForward: /\(non-fast-forward\)/i,
9398
noMergeBase: /no merge base/i,
99+
noRemoteReference: /unable to delete '.+?': remote ref does not exist/i,
94100
noRemoteRepositorySpecified: /No remote repository specified\./i,
95101
noUpstream: /^fatal: The current branch .* has no upstream branch/i,
96102
notAValidObjectName: /Not a valid object name/i,
@@ -169,15 +175,14 @@ const uniqueCounterForStdin = getScopedCounter();
169175
type ExitCodeOnlyGitCommandOptions = GitCommandOptions & { exitCodeOnly: true };
170176
export type PushForceOptions = { withLease: true; ifIncludes?: boolean } | { withLease: false; ifIncludes?: never };
171177

172-
const tagErrorAndReason: [RegExp, TagErrorReason][] = [
173-
[GitErrors.tagAlreadyExists, TagErrorReason.TagAlreadyExists],
174-
[GitErrors.tagNotFound, TagErrorReason.TagNotFound],
175-
[GitErrors.invalidTagName, TagErrorReason.InvalidTagName],
176-
[GitErrors.permissionDenied, TagErrorReason.PermissionDenied],
177-
[GitErrors.remoteRejected, TagErrorReason.RemoteRejected],
178+
const branchErrorsToReasons: [RegExp, BranchErrorReason][] = [
179+
[GitErrors.noRemoteReference, BranchErrorReason.NoRemoteReference],
180+
[GitErrors.invalidBranchName, BranchErrorReason.InvalidBranchName],
181+
[GitErrors.branchAlreadyExists, BranchErrorReason.BranchAlreadyExists],
182+
[GitErrors.branchNotFullyMerged, BranchErrorReason.BranchNotFullyMerged],
178183
];
179184

180-
const resetErrorAndReason: [RegExp, ResetErrorReason][] = [
185+
const resetErrorsToReasons: [RegExp, ResetErrorReason][] = [
181186
[GitErrors.ambiguousArgument, ResetErrorReason.AmbiguousArgument],
182187
[GitErrors.changesWouldBeOverwritten, ResetErrorReason.ChangesWouldBeOverwritten],
183188
[GitErrors.detachedHead, ResetErrorReason.DetachedHead],
@@ -187,6 +192,14 @@ const resetErrorAndReason: [RegExp, ResetErrorReason][] = [
187192
[GitErrors.unmergedChanges, ResetErrorReason.UnmergedChanges],
188193
];
189194

195+
const tagErrorsToReasons: [RegExp, TagErrorReason][] = [
196+
[GitErrors.tagAlreadyExists, TagErrorReason.TagAlreadyExists],
197+
[GitErrors.tagNotFound, TagErrorReason.TagNotFound],
198+
[GitErrors.invalidTagName, TagErrorReason.InvalidTagName],
199+
[GitErrors.permissionDenied, TagErrorReason.PermissionDenied],
200+
[GitErrors.remoteRejected, TagErrorReason.RemoteRejected],
201+
];
202+
190203
export class GitError extends Error {
191204
readonly cmd: string | undefined;
192205
readonly exitCode: number | string | undefined;
@@ -705,6 +718,21 @@ export class Git implements Disposable {
705718
}
706719
}
707720

721+
async branch(repoPath: string, ...args: string[]): Promise<GitResult<string>> {
722+
try {
723+
const result = await this.exec({ cwd: repoPath }, 'branch', ...args);
724+
return result;
725+
} catch (ex) {
726+
const msg: string = ex?.toString() ?? '';
727+
for (const [error, reason] of branchErrorsToReasons) {
728+
if (error.test(msg) || error.test(ex.stderr ?? '')) {
729+
throw new BranchError(reason, ex);
730+
}
731+
}
732+
throw new BranchError(BranchErrorReason.Other, ex);
733+
}
734+
}
735+
708736
async branchOrTag__containsOrPointsAt(
709737
repoPath: string,
710738
refs: string[],
@@ -991,6 +1019,10 @@ export class Git implements Disposable {
9911019
publish?: boolean;
9921020
remote?: string;
9931021
upstream?: string;
1022+
delete?: {
1023+
remote: string;
1024+
branch: string;
1025+
};
9941026
},
9951027
): Promise<void> {
9961028
const params = ['push'];
@@ -1018,6 +1050,8 @@ export class Git implements Disposable {
10181050
}
10191051
} else if (options.remote) {
10201052
params.push(options.remote);
1053+
} else if (options.delete) {
1054+
params.push(options.delete.remote, `:${options.delete.branch}`);
10211055
}
10221056

10231057
try {
@@ -1038,12 +1072,16 @@ export class Git implements Disposable {
10381072
/! \[rejected\].*\(remote ref updated since checkout\)/m.test(ex.stderr || '')
10391073
) {
10401074
reason = PushErrorReason.PushRejectedWithLeaseIfIncludes;
1075+
} else if (/error: unable to delete '(.*?)': remote ref does not exist/m.test(ex.stderr || '')) {
1076+
reason = PushErrorReason.PushRejectedRefNotExists;
10411077
} else {
10421078
reason = PushErrorReason.PushRejected;
10431079
}
10441080
} else {
10451081
reason = PushErrorReason.PushRejected;
10461082
}
1083+
} else if (/error: unable to delete '(.*?)': remote ref does not exist/m.test(ex.stderr || '')) {
1084+
reason = PushErrorReason.PushRejectedRefNotExists;
10471085
} else if (GitErrors.permissionDenied.test(msg) || GitErrors.permissionDenied.test(ex.stderr ?? '')) {
10481086
reason = PushErrorReason.PermissionDenied;
10491087
} else if (GitErrors.remoteConnection.test(msg) || GitErrors.remoteConnection.test(ex.stderr ?? '')) {
@@ -1052,7 +1090,12 @@ export class Git implements Disposable {
10521090
reason = PushErrorReason.NoUpstream;
10531091
}
10541092

1055-
throw new PushError(reason, ex, options?.branch, options?.remote);
1093+
throw new PushError(
1094+
reason,
1095+
ex,
1096+
options?.branch || options?.delete?.branch,
1097+
options?.remote || options?.delete?.remote,
1098+
);
10561099
}
10571100
}
10581101

@@ -1126,7 +1169,7 @@ export class Git implements Disposable {
11261169
await this.exec({ cwd: repoPath }, 'reset', '-q', ...flags, '--', ...pathspecs);
11271170
} catch (ex) {
11281171
const msg: string = ex?.toString() ?? '';
1129-
for (const [error, reason] of resetErrorAndReason) {
1172+
for (const [error, reason] of resetErrorsToReasons) {
11301173
if (error.test(msg) || error.test(ex.stderr ?? '')) {
11311174
throw new ResetError(reason, ex);
11321175
}
@@ -1542,7 +1585,7 @@ export class Git implements Disposable {
15421585
return result;
15431586
} catch (ex) {
15441587
const msg: string = ex?.toString() ?? '';
1545-
for (const [error, reason] of tagErrorAndReason) {
1588+
for (const [error, reason] of tagErrorsToReasons) {
15461589
if (error.test(msg) || error.test(ex.stderr ?? '')) {
15471590
throw new TagError(reason, ex);
15481591
}

src/env/node/git/sub-providers/branches.ts

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { Container } from '../../../../container';
33
import { CancellationError, isCancellationError } from '../../../../errors';
44
import type { GitCache } from '../../../../git/cache';
55
import { GitErrorHandling } from '../../../../git/commandOptions';
6+
import { BranchError } from '../../../../git/errors';
67
import type {
78
BranchContributionsOverview,
89
GitBranchesSubProvider,
@@ -441,7 +442,41 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
441442

442443
@log()
443444
async createBranch(repoPath: string, name: string, sha: string): Promise<void> {
444-
await this.git.exec({ cwd: repoPath }, 'branch', name, sha);
445+
try {
446+
await this.git.branch(repoPath, name, sha);
447+
} catch (ex) {
448+
if (ex instanceof BranchError) {
449+
throw ex.update({ branch: name, action: 'create' });
450+
}
451+
452+
throw ex;
453+
}
454+
}
455+
456+
@log()
457+
async deleteLocalBranch(repoPath: string, name: string, options?: { force?: boolean }): Promise<void> {
458+
try {
459+
await this.git.branch(repoPath, options?.force ? '-D' : '-d', name);
460+
} catch (ex) {
461+
if (ex instanceof BranchError) {
462+
throw ex.update({ branch: name, action: 'delete' });
463+
}
464+
465+
throw ex;
466+
}
467+
}
468+
469+
@log()
470+
async deleteRemoteBranch(repoPath: string, name: string, remote: string): Promise<void> {
471+
try {
472+
await this.git.exec({ cwd: repoPath }, 'push', '-d', remote, name);
473+
} catch (ex) {
474+
if (ex instanceof BranchError) {
475+
throw ex.update({ branch: name, action: 'delete' });
476+
}
477+
478+
throw ex;
479+
}
445480
}
446481

447482
@log()
@@ -781,7 +816,15 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
781816

782817
@log()
783818
async renameBranch(repoPath: string, oldName: string, newName: string): Promise<void> {
784-
await this.git.exec({ cwd: repoPath }, 'branch', '-m', oldName, newName);
819+
try {
820+
await this.git.branch(repoPath, 'branch', '-m', oldName, newName);
821+
} catch (ex) {
822+
if (ex instanceof BranchError) {
823+
throw ex.update({ branch: oldName, action: 'rename' });
824+
}
825+
826+
throw ex;
827+
}
785828
}
786829

787830
@log()

src/env/node/git/sub-providers/tags.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ export class TagsGitSubProvider implements GitTagsSubProvider {
134134
await this.git.tag(repoPath, name, sha, ...(message != null && message.length > 0 ? ['-m', message] : []));
135135
} catch (ex) {
136136
if (ex instanceof TagError) {
137-
throw ex.withTag(name).withAction('create');
137+
throw ex.update({ tag: name, action: 'create' });
138138
}
139139

140140
throw ex;
@@ -147,7 +147,7 @@ export class TagsGitSubProvider implements GitTagsSubProvider {
147147
await this.git.tag(repoPath, '-d', name);
148148
} catch (ex) {
149149
if (ex instanceof TagError) {
150-
throw ex.withTag(name).withAction('delete');
150+
throw ex.update({ tag: name, action: 'delete' });
151151
}
152152

153153
throw ex;

0 commit comments

Comments
 (0)