mirror of
https://github.com/microsoft/vscode.git
synced 2025-12-23 03:39:23 +00:00
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:
@@ -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> {
|
||||
|
||||
Reference in New Issue
Block a user