Skip to content

Commit 1297e26

Browse files
Handle worktree errors (microsoft#258867)
* throw err when choosing already checked out branch * fix two separate worktree error handling * Validate branch as soon as it is selected * Working path validation upfront * normalize paths --------- Co-authored-by: Ladislau Szomoru <[email protected]>
1 parent 8eb09a0 commit 1297e26

File tree

3 files changed

+62
-35
lines changed

3 files changed

+62
-35
lines changed

extensions/git/src/api/git.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -442,5 +442,6 @@ export const enum GitErrorCodes {
442442
CherryPickEmpty = 'CherryPickEmpty',
443443
CherryPickConflict = 'CherryPickConflict',
444444
WorktreeContainsChanges = 'WorktreeContainsChanges',
445-
WorktreeAlreadyExists = 'WorktreeAlreadyExists'
445+
WorktreeAlreadyExists = 'WorktreeAlreadyExists',
446+
WorktreeBranchAlreadyUsed = 'WorktreeBranchAlreadyUsed'
446447
}

extensions/git/src/commands.ts

Lines changed: 57 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,16 @@ class RefItem implements QuickPickItem {
110110
return undefined;
111111
}
112112

113+
get refId(): string {
114+
switch (this.ref.type) {
115+
case RefType.Head:
116+
return `refs/heads/${this.ref.name}`;
117+
case RefType.RemoteHead:
118+
return `refs/remotes/${this.ref.remote}/${this.ref.name}`;
119+
case RefType.Tag:
120+
return `refs/tags/${this.ref.name}`;
121+
}
122+
}
113123
get refName(): string | undefined { return this.ref.name; }
114124
get refRemote(): string | undefined { return this.ref.remote; }
115125
get shortCommit(): string { return (this.ref.commit || '').substring(0, this.shortCommitLength); }
@@ -2923,12 +2933,12 @@ export class CommandCenter {
29232933
try {
29242934
await item.run(repository, opts);
29252935
} catch (err) {
2926-
if (err.gitErrorCode !== GitErrorCodes.DirtyWorkTree && err.gitErrorCode !== GitErrorCodes.WorktreeAlreadyExists) {
2936+
if (err.gitErrorCode !== GitErrorCodes.DirtyWorkTree && err.gitErrorCode !== GitErrorCodes.WorktreeBranchAlreadyUsed) {
29272937
throw err;
29282938
}
29292939

2930-
if (err.gitErrorCode === GitErrorCodes.WorktreeAlreadyExists) {
2931-
this.handleWorktreeError(err);
2940+
if (err.gitErrorCode === GitErrorCodes.WorktreeBranchAlreadyUsed) {
2941+
this.handleWorktreeBranchAlreadyUsed(err);
29322942
return false;
29332943
}
29342944

@@ -3470,16 +3480,14 @@ export class CommandCenter {
34703480
return;
34713481
}
34723482

3473-
// If the worktree is locked, we prompt to create a new branch
3474-
// otherwise we can use the existing selected branch or tag
3475-
const isWorktreeLocked = await this.isWorktreeLocked(repository, choice);
3476-
if (isWorktreeLocked) {
3477-
branch = await this.promptForBranchName(repository);
3478-
3479-
if (!branch) {
3480-
return;
3481-
}
3483+
// Check whether the selected branch is checked out in an existing worktree
3484+
const worktree = repository.worktrees.find(worktree => worktree.ref === choice.refId);
3485+
if (worktree) {
3486+
const message = l10n.t('Branch "{0}" is already checked out in the worktree at "{1}".', choice.refName, worktree.path);
3487+
await this.handleWorktreeConflict(worktree.path, message);
3488+
return;
34823489
}
3490+
34833491
commitish = choice.refName;
34843492
}
34853493

@@ -3516,6 +3524,14 @@ export class CommandCenter {
35163524
return [start, value.length];
35173525
};
35183526

3527+
const getValidationMessage = (value: string): InputBoxValidationMessage | undefined => {
3528+
const worktree = repository.worktrees.find(worktree => pathEquals(path.normalize(worktree.path), path.normalize(value)));
3529+
return worktree ? {
3530+
message: l10n.t('A worktree already exists at "{0}".', value),
3531+
severity: InputBoxValidationSeverity.Warning
3532+
} : undefined;
3533+
};
3534+
35193535
// Default worktree path is based on the last worktree location or a worktree folder for the repository
35203536
const defaultWorktreeRoot = this.globalState.get<string>(`${CommandCenter.WORKTREE_ROOT_KEY}:${repository.root}`);
35213537
const defaultWorktreePath = defaultWorktreeRoot
@@ -3530,6 +3546,7 @@ export class CommandCenter {
35303546
inputBox.prompt = l10n.t('Please provide a worktree path');
35313547
inputBox.value = defaultWorktreePath;
35323548
inputBox.valueSelection = getValueSelection(inputBox.value);
3549+
inputBox.validationMessage = getValidationMessage(inputBox.value);
35333550
inputBox.ignoreFocusOut = true;
35343551
inputBox.buttons = [
35353552
{
@@ -3544,6 +3561,9 @@ export class CommandCenter {
35443561
const worktreePath = await new Promise<string | undefined>((resolve) => {
35453562
disposables.push(inputBox.onDidHide(() => resolve(undefined)));
35463563
disposables.push(inputBox.onDidAccept(() => resolve(inputBox.value)));
3564+
disposables.push(inputBox.onDidChangeValue(value => {
3565+
inputBox.validationMessage = getValidationMessage(value);
3566+
}));
35473567
disposables.push(inputBox.onDidTriggerButton(async () => {
35483568
inputBox.value = await getWorktreePath() ?? '';
35493569
inputBox.valueSelection = getValueSelection(inputBox.value);
@@ -3565,57 +3585,62 @@ export class CommandCenter {
35653585
this.globalState.update(`${CommandCenter.WORKTREE_ROOT_KEY}:${repository.root}`, worktreeRoot);
35663586
}
35673587
} catch (err) {
3568-
if (err.gitErrorCode !== GitErrorCodes.WorktreeAlreadyExists) {
3588+
if (err.gitErrorCode === GitErrorCodes.WorktreeAlreadyExists) {
3589+
await this.handleWorktreeAlreadyExists(err);
3590+
} else if (err.gitErrorCode === GitErrorCodes.WorktreeBranchAlreadyUsed) {
3591+
await this.handleWorktreeBranchAlreadyUsed(err);
3592+
} else {
35693593
throw err;
35703594
}
35713595

3572-
this.handleWorktreeError(err);
35733596
return;
35743597
}
35753598
}
35763599

3577-
// If the user picks a branch that is present in any of the worktrees or the current branch, return true
3578-
// Otherwise, return false.
3579-
private async isWorktreeLocked(repository: Repository, choice: RefItem): Promise<boolean> {
3580-
if (!choice.refName) {
3581-
return false;
3582-
}
3583-
3584-
const worktrees = await repository.getWorktrees();
3600+
private async handleWorktreeBranchAlreadyUsed(err: any): Promise<void> {
3601+
const match = err.stderr.match(/fatal: '([^']+)' is already used by worktree at '([^']+)'/);
35853602

3586-
const isInWorktree = worktrees.some(worktree => worktree.ref === `refs/heads/${choice.refName}`);
3587-
const isCurrentBranch = repository.HEAD?.name === choice.refName;
3603+
if (!match) {
3604+
return;
3605+
}
35883606

3589-
return isInWorktree || isCurrentBranch;
3607+
const [, branch, path] = match;
3608+
const message = l10n.t('Branch "{0}" is already checked out in the worktree at "{1}".', branch, path);
3609+
await this.handleWorktreeConflict(path, message);
35903610
}
35913611

3592-
private async handleWorktreeError(err: any): Promise<void> {
3593-
const match = err.stderr.match(/^fatal: '([^']+)' is already used by worktree at '([^']+)'/);
3612+
private async handleWorktreeAlreadyExists(err: any): Promise<void> {
3613+
const match = err.stderr.match(/fatal: '([^']+)'/);
3614+
35943615
if (!match) {
35953616
return;
35963617
}
35973618

3598-
const [, branch, path] = match;
3619+
const [, path] = match;
3620+
const message = l10n.t('A worktree already exists at "{0}".', path);
3621+
await this.handleWorktreeConflict(path, message);
3622+
}
3623+
3624+
private async handleWorktreeConflict(path: string, message: string): Promise<void> {
35993625
const worktreeRepository = this.model.getRepository(path);
36003626

36013627
if (!worktreeRepository) {
36023628
return;
36033629
}
36043630

3605-
const openWorktree = l10n.t('Open in Current Window');
3606-
const openWorktreeInNewWindow = l10n.t('Open in New Window');
3607-
const message = l10n.t('Branch \'{0}\' is already checked out in the worktree at \'{1}\'.', branch, path);
3631+
const openWorktree = l10n.t('Open Worktree in Current Window');
3632+
const openWorktreeInNewWindow = l10n.t('Open Worktree in New Window');
36083633
const choice = await window.showWarningMessage(message, { modal: true }, openWorktree, openWorktreeInNewWindow);
36093634

36103635
if (choice === openWorktree) {
36113636
await this.openWorktreeInCurrentWindow(worktreeRepository);
36123637
} else if (choice === openWorktreeInNewWindow) {
36133638
await this.openWorktreeInNewWindow(worktreeRepository);
36143639
}
3615-
36163640
return;
36173641
}
36183642

3643+
36193644
@command('git.deleteWorktree', { repository: true, repositoryFilter: ['worktree'] })
36203645
async deleteWorktree(repository: Repository): Promise<void> {
36213646
if (!repository.dotGit.commonPath) {

extensions/git/src/git.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -351,10 +351,11 @@ function getGitErrorCode(stderr: string): string | undefined {
351351
return GitErrorCodes.NotASafeGitRepository;
352352
} else if (/contains modified or untracked files|use --force to delete it/.test(stderr)) {
353353
return GitErrorCodes.WorktreeContainsChanges;
354-
} else if (/is already used by worktree at|already exists/.test(stderr)) {
354+
} else if (/fatal: '[^']+' already exists/.test(stderr)) {
355355
return GitErrorCodes.WorktreeAlreadyExists;
356+
} else if (/is already used by worktree at/.test(stderr)) {
357+
return GitErrorCodes.WorktreeBranchAlreadyUsed;
356358
}
357-
358359
return undefined;
359360
}
360361

0 commit comments

Comments
 (0)