Skip to content

Commit 5a1c4e6

Browse files
lszomoruCopilot
andauthored
Git - refactor create/delete worktree and expose extension API (microsoft#278107)
* Git - refactor create/delete worktree and expose extension API * Pull request feedback * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent d5f4606 commit 5a1c4e6

File tree

4 files changed

+155
-150
lines changed

4 files changed

+155
-150
lines changed

extensions/git/src/api/api1.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,14 @@ export class ApiRepository implements Repository {
318318
dropStash(index?: number): Promise<void> {
319319
return this.#repository.dropStash(index);
320320
}
321+
322+
createWorktree(options?: { path?: string; commitish?: string; branch?: string }): Promise<string> {
323+
return this.#repository.createWorktree(options);
324+
}
325+
326+
deleteWorktree(path: string, options?: { force?: boolean }): Promise<void> {
327+
return this.#repository.deleteWorktree(path, options);
328+
}
321329
}
322330

323331
export class ApiGit implements Git {

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,9 @@ export interface Repository {
289289
applyStash(index?: number): Promise<void>;
290290
popStash(index?: number): Promise<void>;
291291
dropStash(index?: number): Promise<void>;
292+
293+
createWorktree(options?: { path?: string; commitish?: string; branch?: string }): Promise<string>;
294+
deleteWorktree(path: string, options?: { force?: boolean }): Promise<void>;
292295
}
293296

294297
export interface RemoteSource {

extensions/git/src/commands.ts

Lines changed: 45 additions & 147 deletions
Original file line numberDiff line numberDiff line change
@@ -762,8 +762,6 @@ export class CommandCenter {
762762
private disposables: Disposable[];
763763
private commandErrors = new CommandErrorOutputTextDocumentContentProvider();
764764

765-
private static readonly WORKTREE_ROOT_KEY = 'worktreeRoot';
766-
767765
constructor(
768766
private git: Git,
769767
private model: Model,
@@ -3500,119 +3498,47 @@ export class CommandCenter {
35003498
});
35013499
}
35023500

3503-
@command('git.createWorktreeWithDefaults', { repository: true, repositoryFilter: ['repository'] })
3504-
async createWorktreeWithDefaults(
3505-
repository: Repository,
3506-
commitish: string = 'HEAD'
3507-
): Promise<string | undefined> {
3508-
const config = workspace.getConfiguration('git');
3509-
const branchPrefix = config.get<string>('branchPrefix', '');
3510-
3511-
// Generate branch name if not provided
3512-
let branch = await this.generateRandomBranchName(repository, '-');
3513-
if (!branch) {
3514-
// Fallback to timestamp-based name if random generation fails
3515-
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
3516-
branch = `${branchPrefix}worktree-${timestamp}`;
3517-
}
3518-
3519-
// Ensure branch name starts with prefix if configured
3520-
if (branchPrefix && !branch.startsWith(branchPrefix)) {
3521-
branch = branchPrefix + branch;
3522-
}
3523-
3524-
// Create worktree name from branch name
3525-
const worktreeName = branch.startsWith(branchPrefix)
3526-
? branch.substring(branchPrefix.length).replace(/\//g, '-')
3527-
: branch.replace(/\//g, '-');
3528-
3529-
// Determine default worktree path
3530-
const defaultWorktreeRoot = this.globalState.get<string>(`${CommandCenter.WORKTREE_ROOT_KEY}:${repository.root}`);
3531-
const defaultWorktreePath = defaultWorktreeRoot
3532-
? path.join(defaultWorktreeRoot, worktreeName)
3533-
: path.join(path.dirname(repository.root), `${path.basename(repository.root)}.worktrees`, worktreeName);
3534-
3535-
// Check if worktree already exists at this path
3536-
const existingWorktree = repository.worktrees.find(worktree =>
3537-
pathEquals(path.normalize(worktree.path), path.normalize(defaultWorktreePath))
3538-
);
3539-
3540-
if (existingWorktree) {
3541-
// Generate unique path by appending a number
3542-
let counter = 1;
3543-
let uniquePath = `${defaultWorktreePath}-${counter}`;
3544-
while (repository.worktrees.some(wt => pathEquals(path.normalize(wt.path), path.normalize(uniquePath)))) {
3545-
counter++;
3546-
uniquePath = `${defaultWorktreePath}-${counter}`;
3547-
}
3548-
const finalWorktreePath = uniquePath;
3549-
3550-
try {
3551-
await repository.addWorktree({ path: finalWorktreePath, branch, commitish });
3552-
3553-
// Update worktree root in global state
3554-
const worktreeRoot = path.dirname(finalWorktreePath);
3555-
if (worktreeRoot !== defaultWorktreeRoot) {
3556-
this.globalState.update(`${CommandCenter.WORKTREE_ROOT_KEY}:${repository.root}`, worktreeRoot);
3557-
}
3558-
3559-
return finalWorktreePath;
3560-
} catch (err) {
3561-
// Return undefined on failure
3562-
return undefined;
3563-
}
3564-
}
3565-
3566-
try {
3567-
await repository.addWorktree({ path: defaultWorktreePath, branch, commitish });
3568-
3569-
// Update worktree root in global state
3570-
const worktreeRoot = path.dirname(defaultWorktreePath);
3571-
if (worktreeRoot !== defaultWorktreeRoot) {
3572-
this.globalState.update(`${CommandCenter.WORKTREE_ROOT_KEY}:${repository.root}`, worktreeRoot);
3573-
}
3574-
3575-
return defaultWorktreePath;
3576-
} catch (err) {
3577-
// Return undefined on failure
3578-
return undefined;
3579-
}
3580-
}
3581-
3582-
@command('git.createWorktree', { repository: true })
3501+
@command('git.createWorktree', { repository: true, repositoryFilter: ['repository', 'submodule'] })
35833502
async createWorktree(repository?: Repository): Promise<void> {
35843503
if (!repository) {
3585-
// Single repository/submodule/worktree
3586-
if (this.model.repositories.length === 1) {
3587-
repository = this.model.repositories[0];
3588-
}
3504+
return;
35893505
}
35903506

3591-
if (!repository) {
3592-
// Single repository/submodule
3593-
const repositories = this.model.repositories
3594-
.filter(r => r.kind === 'repository' || r.kind === 'submodule');
3507+
const config = workspace.getConfiguration('git');
3508+
const branchPrefix = config.get<string>('branchPrefix')!;
35953509

3596-
if (repositories.length === 1) {
3597-
repository = repositories[0];
3598-
}
3510+
// Get commitish and branch for the new worktree
3511+
const worktreeDetails = await this.getWorktreeCommitishAndBranch(repository);
3512+
if (!worktreeDetails) {
3513+
return;
35993514
}
36003515

3601-
if (!repository) {
3602-
// Multiple repositories/submodules
3603-
repository = await this.model.pickRepository(['repository', 'submodule']);
3604-
}
3516+
const { commitish, branch } = worktreeDetails;
3517+
const worktreeName = ((branch ?? commitish).startsWith(branchPrefix)
3518+
? (branch ?? commitish).substring(branchPrefix.length).replace(/\//g, '-')
3519+
: (branch ?? commitish).replace(/\//g, '-'));
36053520

3606-
if (!repository) {
3521+
// Get path for the new worktree
3522+
const worktreePath = await this.getWorktreePath(repository, worktreeName);
3523+
if (!worktreePath) {
36073524
return;
36083525
}
36093526

3610-
await this._createWorktree(repository);
3527+
try {
3528+
await repository.createWorktree({ path: worktreePath, branch, commitish: commitish });
3529+
} catch (err) {
3530+
if (err instanceof GitError && err.gitErrorCode === GitErrorCodes.WorktreeAlreadyExists) {
3531+
await this.handleWorktreeAlreadyExists(err);
3532+
} else if (err instanceof GitError && err.gitErrorCode === GitErrorCodes.WorktreeBranchAlreadyUsed) {
3533+
await this.handleWorktreeBranchAlreadyUsed(err);
3534+
} else {
3535+
throw err;
3536+
}
3537+
}
36113538
}
36123539

3613-
private async _createWorktree(repository: Repository): Promise<void> {
3614-
const config = workspace.getConfiguration('git');
3615-
const branchPrefix = config.get<string>('branchPrefix')!;
3540+
private async getWorktreeCommitishAndBranch(repository: Repository): Promise<{ commitish: string; branch: string | undefined } | undefined> {
3541+
const config = workspace.getConfiguration('git', Uri.file(repository.root));
36163542
const showRefDetails = config.get<boolean>('showReferenceDetails') === true;
36173543

36183544
const createBranch = new CreateBranchItem();
@@ -3631,23 +3557,21 @@ export class CommandCenter {
36313557
const choice = await this.pickRef(getBranchPicks(), placeHolder);
36323558

36333559
if (!choice) {
3634-
return;
3560+
return undefined;
36353561
}
36363562

3637-
let branch: string | undefined = undefined;
3638-
let commitish: string;
3639-
36403563
if (choice === createBranch) {
3641-
branch = await this.promptForBranchName(repository);
3642-
3564+
// Create new branch
3565+
const branch = await this.promptForBranchName(repository);
36433566
if (!branch) {
3644-
return;
3567+
return undefined;
36453568
}
36463569

3647-
commitish = 'HEAD';
3570+
return { commitish: 'HEAD', branch };
36483571
} else {
3572+
// Existing reference
36493573
if (!(choice instanceof RefItem) || !choice.refName) {
3650-
return;
3574+
return undefined;
36513575
}
36523576

36533577
if (choice.refName === repository.HEAD?.name) {
@@ -3656,15 +3580,14 @@ export class CommandCenter {
36563580
const pick = await window.showWarningMessage(message, { modal: true }, createBranch);
36573581

36583582
if (pick === createBranch) {
3659-
branch = await this.promptForBranchName(repository);
3660-
3583+
const branch = await this.promptForBranchName(repository);
36613584
if (!branch) {
3662-
return;
3585+
return undefined;
36633586
}
36643587

3665-
commitish = 'HEAD';
3588+
return { commitish: 'HEAD', branch };
36663589
} else {
3667-
return;
3590+
return undefined;
36683591
}
36693592
} else {
36703593
// Check whether the selected branch is checked out in an existing worktree
@@ -3674,17 +3597,14 @@ export class CommandCenter {
36743597
await this.handleWorktreeConflict(worktree.path, message);
36753598
return;
36763599
}
3677-
commitish = choice.refName;
3600+
return { commitish: choice.refName, branch: undefined };
36783601
}
36793602
}
3603+
}
36803604

3681-
const worktreeName = ((branch ?? commitish).startsWith(branchPrefix)
3682-
? (branch ?? commitish).substring(branchPrefix.length).replace(/\//g, '-')
3683-
: (branch ?? commitish).replace(/\//g, '-'));
3684-
3685-
// If user selects folder button, they manually select the worktree path through folder picker
3605+
private async getWorktreePath(repository: Repository, worktreeName: string): Promise<string | undefined> {
36863606
const getWorktreePath = async (): Promise<string | undefined> => {
3687-
const worktreeRoot = this.globalState.get<string>(`${CommandCenter.WORKTREE_ROOT_KEY}:${repository.root}`);
3607+
const worktreeRoot = this.globalState.get<string>(`${Repository.WORKTREE_ROOT_STORAGE_KEY}:${repository.root}`);
36883608
const defaultUri = worktreeRoot ? Uri.file(worktreeRoot) : Uri.file(path.dirname(repository.root));
36893609

36903610
const uris = await window.showOpenDialog({
@@ -3720,7 +3640,7 @@ export class CommandCenter {
37203640
};
37213641

37223642
// Default worktree path is based on the last worktree location or a worktree folder for the repository
3723-
const defaultWorktreeRoot = this.globalState.get<string>(`${CommandCenter.WORKTREE_ROOT_KEY}:${repository.root}`);
3643+
const defaultWorktreeRoot = this.globalState.get<string>(`${Repository.WORKTREE_ROOT_STORAGE_KEY}:${repository.root}`);
37243644
const defaultWorktreePath = defaultWorktreeRoot
37253645
? path.join(defaultWorktreeRoot, worktreeName)
37263646
: path.join(path.dirname(repository.root), `${path.basename(repository.root)}.worktrees`, worktreeName);
@@ -3759,29 +3679,7 @@ export class CommandCenter {
37593679

37603680
dispose(disposables);
37613681

3762-
if (!worktreePath) {
3763-
return;
3764-
}
3765-
3766-
try {
3767-
await repository.addWorktree({ path: worktreePath, branch, commitish: commitish });
3768-
3769-
// Update worktree root in global state
3770-
const worktreeRoot = path.dirname(worktreePath);
3771-
if (worktreeRoot !== defaultWorktreeRoot) {
3772-
this.globalState.update(`${CommandCenter.WORKTREE_ROOT_KEY}:${repository.root}`, worktreeRoot);
3773-
}
3774-
} catch (err) {
3775-
if (err instanceof GitError && err.gitErrorCode === GitErrorCodes.WorktreeAlreadyExists) {
3776-
await this.handleWorktreeAlreadyExists(err);
3777-
} else if (err instanceof GitError && err.gitErrorCode === GitErrorCodes.WorktreeBranchAlreadyUsed) {
3778-
await this.handleWorktreeBranchAlreadyUsed(err);
3779-
} else {
3780-
throw err;
3781-
}
3782-
3783-
return;
3784-
}
3682+
return worktreePath;
37853683
}
37863684

37873685
private async handleWorktreeBranchAlreadyUsed(err: GitError): Promise<void> {

0 commit comments

Comments
 (0)