Skip to content

Commit 40d6d90

Browse files
Copilotalexr00
andcommitted
feat: add checkout pull request in worktree option
Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com>
1 parent cf08027 commit 40d6d90

File tree

3 files changed

+129
-0
lines changed

3 files changed

+129
-0
lines changed

package.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -988,6 +988,12 @@
988988
"category": "%command.pull.request.category%",
989989
"icon": "$(cloud)"
990990
},
991+
{
992+
"command": "pr.pickInWorktree",
993+
"title": "%command.pr.pickInWorktree.title%",
994+
"category": "%command.pull.request.category%",
995+
"icon": "$(folder-library)"
996+
},
991997
{
992998
"command": "pr.exit",
993999
"title": "%command.pr.exit.title%",
@@ -2059,6 +2065,10 @@
20592065
"command": "pr.pickOnCodespaces",
20602066
"when": "false"
20612067
},
2068+
{
2069+
"command": "pr.pickInWorktree",
2070+
"when": "false"
2071+
},
20622072
{
20632073
"command": "pr.exit",
20642074
"when": "github:inReviewMode"
@@ -2857,6 +2867,11 @@
28572867
"when": "view == pr:github && viewItem =~ /pullrequest(:local)?:nonactive/ && (!isWeb || remoteName != codespaces && virtualWorkspace != vscode-vfs)",
28582868
"group": "1_pullrequest@3"
28592869
},
2870+
{
2871+
"command": "pr.pickInWorktree",
2872+
"when": "view == pr:github && viewItem =~ /pullrequest(:local)?:nonactive/ && !isWeb",
2873+
"group": "1_pullrequest@4"
2874+
},
28602875
{
28612876
"command": "pr.openChanges",
28622877
"when": "view =~ /(pr|prStatus):github/ && viewItem =~ /(pullrequest|description)/",

package.nls.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@
208208
"command.pr.openChanges.title": "Open Changes",
209209
"command.pr.pickOnVscodeDev.title": "Checkout Pull Request on vscode.dev",
210210
"command.pr.pickOnCodespaces.title": "Checkout Pull Request on Codespaces",
211+
"command.pr.pickInWorktree.title": "Checkout Pull Request in Worktree",
211212
"command.pr.exit.title": "Checkout Default Branch",
212213
"command.pr.dismissNotification.title": "Dismiss Notification",
213214
"command.pr.markAllCopilotNotificationsAsRead.title": "Dismiss All Copilot Notifications",

src/commands.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -825,6 +825,119 @@ export function registerCommands(
825825
),
826826
);
827827

828+
context.subscriptions.push(
829+
vscode.commands.registerCommand('pr.pickInWorktree', async (pr: PRNode | RepositoryChangesNode | PullRequestModel) => {
830+
if (pr === undefined) {
831+
Logger.error('Unexpectedly received undefined when picking a PR for worktree checkout.', logId);
832+
return vscode.window.showErrorMessage(vscode.l10n.t('No pull request was selected to checkout, please try again.'));
833+
}
834+
835+
let pullRequestModel: PullRequestModel;
836+
let repository: Repository | undefined;
837+
838+
if (pr instanceof PRNode || pr instanceof RepositoryChangesNode) {
839+
pullRequestModel = pr.pullRequestModel;
840+
repository = pr.repository;
841+
} else {
842+
pullRequestModel = pr;
843+
}
844+
845+
// Validate that the PR has a valid head branch
846+
if (!pullRequestModel.head) {
847+
return vscode.window.showErrorMessage(vscode.l10n.t('Unable to checkout pull request: missing head branch information.'));
848+
}
849+
850+
// Get the folder manager to access the repository
851+
const folderManager = reposManager.getManagerForIssueModel(pullRequestModel);
852+
if (!folderManager) {
853+
return vscode.window.showErrorMessage(vscode.l10n.t('Unable to find repository for this pull request.'));
854+
}
855+
856+
const repositoryToUse = repository || folderManager.repository;
857+
858+
/* __GDPR__
859+
"pr.checkoutInWorktree" : {}
860+
*/
861+
telemetry.sendTelemetryEvent('pr.checkoutInWorktree');
862+
863+
return vscode.window.withProgress(
864+
{
865+
location: vscode.ProgressLocation.Notification,
866+
title: vscode.l10n.t('Checking out Pull Request #{0} in worktree', pullRequestModel.number),
867+
},
868+
async (progress) => {
869+
// Generate a branch name for the worktree
870+
const branchName = pullRequestModel.head!.ref;
871+
const remoteName = pullRequestModel.remote.remoteName;
872+
873+
// Fetch the PR branch first
874+
progress.report({ message: vscode.l10n.t('Fetching branch {0}...', branchName) });
875+
try {
876+
await repositoryToUse.fetch({ remote: remoteName, ref: branchName });
877+
} catch (e) {
878+
Logger.appendLine(`Failed to fetch branch ${branchName}: ${e}`, logId);
879+
// Continue even if fetch fails - the branch might already be available locally
880+
}
881+
882+
// Ask user for worktree location
883+
const repoRootPath = repositoryToUse.rootUri.fsPath;
884+
const parentDir = pathLib.dirname(repoRootPath);
885+
const defaultWorktreePath = pathLib.join(parentDir, `pr-${pullRequestModel.number}`);
886+
887+
const worktreeUri = await vscode.window.showSaveDialog({
888+
defaultUri: vscode.Uri.file(defaultWorktreePath),
889+
title: vscode.l10n.t('Select Worktree Location'),
890+
saveLabel: vscode.l10n.t('Create Worktree'),
891+
});
892+
893+
if (!worktreeUri) {
894+
return; // User cancelled
895+
}
896+
897+
const worktreePath = worktreeUri.fsPath;
898+
899+
// Create the worktree using git command
900+
progress.report({ message: vscode.l10n.t('Creating worktree at {0}...', worktreePath) });
901+
902+
const trackedBranchName = `${remoteName}/${branchName}`;
903+
const localBranchName = `pr-${pullRequestModel.number}/${branchName}`;
904+
905+
try {
906+
// Execute git worktree add command
907+
const terminal = vscode.window.createTerminal({
908+
name: vscode.l10n.t('Git Worktree'),
909+
cwd: repoRootPath,
910+
hideFromUser: true,
911+
});
912+
913+
// Create worktree with a new local branch tracking the remote
914+
terminal.sendText(`git worktree add -b "${localBranchName}" "${worktreePath}" "${trackedBranchName}" && exit`);
915+
916+
// Wait a bit for the command to complete
917+
await new Promise(resolve => setTimeout(resolve, 2000));
918+
919+
terminal.dispose();
920+
921+
// Ask user if they want to open the worktree
922+
const openAction = vscode.l10n.t('Open in New Window');
923+
const result = await vscode.window.showInformationMessage(
924+
vscode.l10n.t('Worktree created for Pull Request #{0}', pullRequestModel.number),
925+
openAction
926+
);
927+
928+
if (result === openAction) {
929+
await commands.openFolder(worktreeUri, { forceNewWindow: true });
930+
}
931+
} catch (e) {
932+
const errorMessage = e instanceof Error ? e.message : String(e);
933+
Logger.error(`Failed to create worktree: ${errorMessage}`, logId);
934+
return vscode.window.showErrorMessage(vscode.l10n.t('Failed to create worktree: {0}', errorMessage));
935+
}
936+
}
937+
);
938+
}),
939+
);
940+
828941
context.subscriptions.push(vscode.commands.registerCommand('pr.checkoutOnVscodeDevFromDescription', async (context: BaseContext | undefined) => {
829942
if (!context) {
830943
return vscode.window.showErrorMessage(vscode.l10n.t('No pull request context provided for checkout.'));

0 commit comments

Comments
 (0)