Git - refactor create/delete worktree and expose extension API (#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>
This commit is contained in:
Ladislau Szomoru
2025-11-18 15:37:56 +00:00
committed by GitHub
parent d5f4606ce7
commit 5a1c4e6ca5
4 changed files with 161 additions and 156 deletions

View File

@@ -762,8 +762,6 @@ export class CommandCenter {
private disposables: Disposable[];
private commandErrors = new CommandErrorOutputTextDocumentContentProvider();
private static readonly WORKTREE_ROOT_KEY = 'worktreeRoot';
constructor(
private git: Git,
private model: Model,
@@ -3500,119 +3498,47 @@ export class CommandCenter {
});
}
@command('git.createWorktreeWithDefaults', { repository: true, repositoryFilter: ['repository'] })
async createWorktreeWithDefaults(
repository: Repository,
commitish: string = 'HEAD'
): Promise<string | undefined> {
const config = workspace.getConfiguration('git');
const branchPrefix = config.get<string>('branchPrefix', '');
// Generate branch name if not provided
let branch = await this.generateRandomBranchName(repository, '-');
if (!branch) {
// Fallback to timestamp-based name if random generation fails
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
branch = `${branchPrefix}worktree-${timestamp}`;
}
// Ensure branch name starts with prefix if configured
if (branchPrefix && !branch.startsWith(branchPrefix)) {
branch = branchPrefix + branch;
}
// Create worktree name from branch name
const worktreeName = branch.startsWith(branchPrefix)
? branch.substring(branchPrefix.length).replace(/\//g, '-')
: branch.replace(/\//g, '-');
// Determine default worktree path
const defaultWorktreeRoot = this.globalState.get<string>(`${CommandCenter.WORKTREE_ROOT_KEY}:${repository.root}`);
const defaultWorktreePath = defaultWorktreeRoot
? path.join(defaultWorktreeRoot, worktreeName)
: path.join(path.dirname(repository.root), `${path.basename(repository.root)}.worktrees`, worktreeName);
// Check if worktree already exists at this path
const existingWorktree = repository.worktrees.find(worktree =>
pathEquals(path.normalize(worktree.path), path.normalize(defaultWorktreePath))
);
if (existingWorktree) {
// Generate unique path by appending a number
let counter = 1;
let uniquePath = `${defaultWorktreePath}-${counter}`;
while (repository.worktrees.some(wt => pathEquals(path.normalize(wt.path), path.normalize(uniquePath)))) {
counter++;
uniquePath = `${defaultWorktreePath}-${counter}`;
}
const finalWorktreePath = uniquePath;
try {
await repository.addWorktree({ path: finalWorktreePath, branch, commitish });
// Update worktree root in global state
const worktreeRoot = path.dirname(finalWorktreePath);
if (worktreeRoot !== defaultWorktreeRoot) {
this.globalState.update(`${CommandCenter.WORKTREE_ROOT_KEY}:${repository.root}`, worktreeRoot);
}
return finalWorktreePath;
} catch (err) {
// Return undefined on failure
return undefined;
}
}
try {
await repository.addWorktree({ path: defaultWorktreePath, branch, commitish });
// Update worktree root in global state
const worktreeRoot = path.dirname(defaultWorktreePath);
if (worktreeRoot !== defaultWorktreeRoot) {
this.globalState.update(`${CommandCenter.WORKTREE_ROOT_KEY}:${repository.root}`, worktreeRoot);
}
return defaultWorktreePath;
} catch (err) {
// Return undefined on failure
return undefined;
}
}
@command('git.createWorktree', { repository: true })
@command('git.createWorktree', { repository: true, repositoryFilter: ['repository', 'submodule'] })
async createWorktree(repository?: Repository): Promise<void> {
if (!repository) {
// Single repository/submodule/worktree
if (this.model.repositories.length === 1) {
repository = this.model.repositories[0];
}
}
if (!repository) {
// Single repository/submodule
const repositories = this.model.repositories
.filter(r => r.kind === 'repository' || r.kind === 'submodule');
if (repositories.length === 1) {
repository = repositories[0];
}
}
if (!repository) {
// Multiple repositories/submodules
repository = await this.model.pickRepository(['repository', 'submodule']);
}
if (!repository) {
return;
}
await this._createWorktree(repository);
}
private async _createWorktree(repository: Repository): Promise<void> {
const config = workspace.getConfiguration('git');
const branchPrefix = config.get<string>('branchPrefix')!;
// Get commitish and branch for the new worktree
const worktreeDetails = await this.getWorktreeCommitishAndBranch(repository);
if (!worktreeDetails) {
return;
}
const { commitish, branch } = worktreeDetails;
const worktreeName = ((branch ?? commitish).startsWith(branchPrefix)
? (branch ?? commitish).substring(branchPrefix.length).replace(/\//g, '-')
: (branch ?? commitish).replace(/\//g, '-'));
// Get path for the new worktree
const worktreePath = await this.getWorktreePath(repository, worktreeName);
if (!worktreePath) {
return;
}
try {
await repository.createWorktree({ path: worktreePath, branch, commitish: commitish });
} catch (err) {
if (err instanceof GitError && err.gitErrorCode === GitErrorCodes.WorktreeAlreadyExists) {
await this.handleWorktreeAlreadyExists(err);
} else if (err instanceof GitError && err.gitErrorCode === GitErrorCodes.WorktreeBranchAlreadyUsed) {
await this.handleWorktreeBranchAlreadyUsed(err);
} else {
throw err;
}
}
}
private async getWorktreeCommitishAndBranch(repository: Repository): Promise<{ commitish: string; branch: string | undefined } | undefined> {
const config = workspace.getConfiguration('git', Uri.file(repository.root));
const showRefDetails = config.get<boolean>('showReferenceDetails') === true;
const createBranch = new CreateBranchItem();
@@ -3631,23 +3557,21 @@ export class CommandCenter {
const choice = await this.pickRef(getBranchPicks(), placeHolder);
if (!choice) {
return;
return undefined;
}
let branch: string | undefined = undefined;
let commitish: string;
if (choice === createBranch) {
branch = await this.promptForBranchName(repository);
// Create new branch
const branch = await this.promptForBranchName(repository);
if (!branch) {
return;
return undefined;
}
commitish = 'HEAD';
return { commitish: 'HEAD', branch };
} else {
// Existing reference
if (!(choice instanceof RefItem) || !choice.refName) {
return;
return undefined;
}
if (choice.refName === repository.HEAD?.name) {
@@ -3656,15 +3580,14 @@ export class CommandCenter {
const pick = await window.showWarningMessage(message, { modal: true }, createBranch);
if (pick === createBranch) {
branch = await this.promptForBranchName(repository);
const branch = await this.promptForBranchName(repository);
if (!branch) {
return;
return undefined;
}
commitish = 'HEAD';
return { commitish: 'HEAD', branch };
} else {
return;
return undefined;
}
} else {
// Check whether the selected branch is checked out in an existing worktree
@@ -3674,17 +3597,14 @@ export class CommandCenter {
await this.handleWorktreeConflict(worktree.path, message);
return;
}
commitish = choice.refName;
return { commitish: choice.refName, branch: undefined };
}
}
}
const worktreeName = ((branch ?? commitish).startsWith(branchPrefix)
? (branch ?? commitish).substring(branchPrefix.length).replace(/\//g, '-')
: (branch ?? commitish).replace(/\//g, '-'));
// If user selects folder button, they manually select the worktree path through folder picker
private async getWorktreePath(repository: Repository, worktreeName: string): Promise<string | undefined> {
const getWorktreePath = async (): Promise<string | undefined> => {
const worktreeRoot = this.globalState.get<string>(`${CommandCenter.WORKTREE_ROOT_KEY}:${repository.root}`);
const worktreeRoot = this.globalState.get<string>(`${Repository.WORKTREE_ROOT_STORAGE_KEY}:${repository.root}`);
const defaultUri = worktreeRoot ? Uri.file(worktreeRoot) : Uri.file(path.dirname(repository.root));
const uris = await window.showOpenDialog({
@@ -3720,7 +3640,7 @@ export class CommandCenter {
};
// Default worktree path is based on the last worktree location or a worktree folder for the repository
const defaultWorktreeRoot = this.globalState.get<string>(`${CommandCenter.WORKTREE_ROOT_KEY}:${repository.root}`);
const defaultWorktreeRoot = this.globalState.get<string>(`${Repository.WORKTREE_ROOT_STORAGE_KEY}:${repository.root}`);
const defaultWorktreePath = defaultWorktreeRoot
? path.join(defaultWorktreeRoot, worktreeName)
: path.join(path.dirname(repository.root), `${path.basename(repository.root)}.worktrees`, worktreeName);
@@ -3759,29 +3679,7 @@ export class CommandCenter {
dispose(disposables);
if (!worktreePath) {
return;
}
try {
await repository.addWorktree({ path: worktreePath, branch, commitish: commitish });
// Update worktree root in global state
const worktreeRoot = path.dirname(worktreePath);
if (worktreeRoot !== defaultWorktreeRoot) {
this.globalState.update(`${CommandCenter.WORKTREE_ROOT_KEY}:${repository.root}`, worktreeRoot);
}
} catch (err) {
if (err instanceof GitError && err.gitErrorCode === GitErrorCodes.WorktreeAlreadyExists) {
await this.handleWorktreeAlreadyExists(err);
} else if (err instanceof GitError && err.gitErrorCode === GitErrorCodes.WorktreeBranchAlreadyUsed) {
await this.handleWorktreeBranchAlreadyUsed(err);
} else {
throw err;
}
return;
}
return worktreePath;
}
private async handleWorktreeBranchAlreadyUsed(err: GitError): Promise<void> {