Skip to content

Commit ed22158

Browse files
Copilotalexr00
andauthored
refactor: extract worktree checkout logic into standalone utility function
Agent-Logs-Url: https://github.com/microsoft/vscode-pull-request-github/sessions/ddf40003-0929-42c3-abfb-5abf98150f00 Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com>
1 parent af07a44 commit ed22158

3 files changed

Lines changed: 138 additions & 107 deletions

File tree

src/commands.ts

Lines changed: 3 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { chooseItem } from './github/quickPicks';
3232
import { RepositoriesManager } from './github/repositoriesManager';
3333
import { codespacesPrLink, getIssuesUrl, getPullsUrl, isInCodespaces, ISSUE_OR_URL_EXPRESSION, parseIssueExpressionOutput, vscodeDevPrLink } from './github/utils';
3434
import { BaseContext, OverviewContext } from './github/views';
35+
import { checkoutPRInWorktree } from './github/worktree';
3536
import { IssueChatContextItem } from './lm/issueContextProvider';
3637
import { PRChatContextItem } from './lm/pullRequestContextProvider';
3738
import { isNotificationTreeItem, NotificationTreeItem } from './notifications/notificationItem';
@@ -845,117 +846,13 @@ export function registerCommands(
845846
return vscode.window.showErrorMessage(vscode.l10n.t('No pull request was selected to checkout, please try again.'));
846847
}
847848

848-
// Validate that the PR has a valid head branch
849-
if (!pullRequestModel.head) {
850-
return vscode.window.showErrorMessage(vscode.l10n.t('Unable to checkout pull request: missing head branch information.'));
851-
}
852-
853-
// Store validated head to avoid non-null assertions later
854-
const prHead = pullRequestModel.head;
855-
856849
// Get the folder manager to access the repository
857850
const folderManager = reposManager.getManagerForIssueModel(pullRequestModel);
858851
if (!folderManager) {
859852
return vscode.window.showErrorMessage(vscode.l10n.t('Unable to find repository for this pull request.'));
860853
}
861854

862-
const repositoryToUse = repository || folderManager.repository;
863-
864-
/* __GDPR__
865-
"pr.checkoutInWorktree" : {}
866-
*/
867-
telemetry.sendTelemetryEvent('pr.checkoutInWorktree');
868-
869-
// Prepare for operations
870-
const repoRootPath = repositoryToUse.rootUri.fsPath;
871-
const parentDir = pathLib.dirname(repoRootPath);
872-
const defaultWorktreePath = pathLib.join(parentDir, `pr-${pullRequestModel.number}`);
873-
const branchName = prHead.ref;
874-
const remoteName = pullRequestModel.remote.remoteName;
875-
876-
// Ask user for worktree location first (not in progress)
877-
const worktreeUri = await vscode.window.showSaveDialog({
878-
defaultUri: vscode.Uri.file(defaultWorktreePath),
879-
title: vscode.l10n.t('Select Worktree Location'),
880-
saveLabel: vscode.l10n.t('Create Worktree'),
881-
});
882-
883-
if (!worktreeUri) {
884-
return; // User cancelled
885-
}
886-
887-
const worktreePath = worktreeUri.fsPath;
888-
const trackedBranchName = `${remoteName}/${branchName}`;
889-
890-
try {
891-
// Check if the createWorktree API is available
892-
if (!repositoryToUse.createWorktree) {
893-
throw new Error(vscode.l10n.t('Git worktree API is not available. Please update VS Code to the latest version.'));
894-
}
895-
896-
// Start progress for fetch and worktree creation
897-
await vscode.window.withProgress(
898-
{
899-
location: vscode.ProgressLocation.Notification,
900-
title: vscode.l10n.t('Creating worktree for Pull Request #{0}...', pullRequestModel.number),
901-
},
902-
async () => {
903-
// Fetch the PR branch first
904-
try {
905-
await repositoryToUse.fetch({ remote: remoteName, ref: branchName });
906-
} catch (e) {
907-
const errorMessage = e instanceof Error ? e.message : String(e);
908-
Logger.appendLine(`Failed to fetch branch ${branchName}: ${errorMessage}`, logId);
909-
// Continue even if fetch fails - the branch might already be available locally
910-
}
911-
912-
// Check if the branch already exists locally
913-
let branchExists = false;
914-
try {
915-
await repositoryToUse.getBranch(branchName);
916-
branchExists = true;
917-
} catch {
918-
// Branch doesn't exist locally, we'll create it
919-
branchExists = false;
920-
}
921-
922-
// Use the git extension's createWorktree API
923-
// If branch already exists, don't specify the branch parameter to avoid "branch already exists" error
924-
if (branchExists) {
925-
await repositoryToUse.createWorktree!({
926-
path: worktreePath,
927-
commitish: branchName
928-
});
929-
} else {
930-
await repositoryToUse.createWorktree!({
931-
path: worktreePath,
932-
commitish: trackedBranchName,
933-
branch: branchName
934-
});
935-
}
936-
}
937-
);
938-
939-
// Ask user how they want to open the worktree (modal dialog)
940-
const openInNewWindow = vscode.l10n.t('New Window');
941-
const openInCurrentWindow = vscode.l10n.t('Current Window');
942-
const result = await vscode.window.showInformationMessage(
943-
vscode.l10n.t('Worktree created for Pull Request #{0}. How would you like to open it?', pullRequestModel.number),
944-
{ modal: true },
945-
openInNewWindow,
946-
openInCurrentWindow
947-
);
948-
949-
if (result === openInNewWindow) {
950-
await commands.openFolder(worktreeUri, { forceNewWindow: true });
951-
} else if (result === openInCurrentWindow) {
952-
await commands.openFolder(worktreeUri, { forceNewWindow: false });
953-
}
954-
} catch (e) {
955-
const errorMessage = e instanceof Error ? e.message : String(e);
956-
Logger.error(`Failed to create worktree: ${errorMessage}`, logId);
957-
return vscode.window.showErrorMessage(vscode.l10n.t('Failed to create worktree: {0}', errorMessage));
958-
}
855+
return checkoutPRInWorktree(telemetry, folderManager, pullRequestModel, repository);
959856
}),
960857
);
961858

@@ -967,7 +864,7 @@ export function registerCommands(
967864
if (!resolved) {
968865
return vscode.window.showErrorMessage(vscode.l10n.t('Unable to resolve pull request for checkout.'));
969866
}
970-
return vscode.commands.executeCommand('pr.pickInWorktree', resolved.pr);
867+
return checkoutPRInWorktree(telemetry, resolved.folderManager, resolved.pr, undefined);
971868
}));
972869

973870
context.subscriptions.push(vscode.commands.registerCommand('pr.checkoutOnVscodeDevFromDescription', async (context: BaseContext | undefined) => {

src/github/pullRequestOverview.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { PullRequestReviewCommon, ReviewContext } from './pullRequestReviewCommo
2929
import { branchPicks, pickEmail, reviewersQuickPick } from './quickPicks';
3030
import { parseReviewers, processDiffLinks, processPermalinks } from './utils';
3131
import { CancelCodingAgentReply, ChangeBaseReply, ChangeReviewersReply, DeleteReviewResult, MergeArguments, MergeResult, PullRequest, ReadyForReviewAndMergeContext, ReadyForReviewContext, ReviewCommentContext, ReviewType, UnresolvedIdentity } from './views';
32+
import { checkoutPRInWorktree } from './worktree';
3233
import { debounce } from '../common/async';
3334
import { COPILOT_ACCOUNTS, IComment } from '../common/comment';
3435
import { COPILOT_REVIEWER, COPILOT_REVIEWER_ACCOUNT, COPILOT_SWE_AGENT, copilotEventToStatus, CopilotPRStatus, mostRecentCopilotEvent } from '../common/copilot';
@@ -830,7 +831,7 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode
830831
}
831832

832833
private checkoutPullRequestInWorktree(message: IRequestMessage<any>): void {
833-
vscode.commands.executeCommand('pr.pickInWorktree', this._item).then(
834+
checkoutPRInWorktree(this._telemetry, this._folderRepositoryManager, this._item, undefined).then(
834835
() => {
835836
this._replyMessage(message, {});
836837
},

src/github/worktree.ts

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import * as path from 'path';
7+
import * as vscode from 'vscode';
8+
import { FolderRepositoryManager } from './folderRepositoryManager';
9+
import { PullRequestModel } from './pullRequestModel';
10+
import { Repository } from '../api/api';
11+
import Logger from '../common/logger';
12+
import { ITelemetry } from '../common/telemetry';
13+
14+
const logId = 'Worktree';
15+
16+
/**
17+
* Checks out a pull request in a new git worktree.
18+
* @param telemetry Telemetry instance for tracking usage
19+
* @param folderManager The folder repository manager
20+
* @param pullRequestModel The pull request to checkout
21+
* @param repository Optional repository to use (if not provided, uses folderManager.repository)
22+
*/
23+
export async function checkoutPRInWorktree(
24+
telemetry: ITelemetry,
25+
folderManager: FolderRepositoryManager,
26+
pullRequestModel: PullRequestModel,
27+
repository: Repository | undefined
28+
): Promise<void> {
29+
// Validate that the PR has a valid head branch
30+
if (!pullRequestModel.head) {
31+
vscode.window.showErrorMessage(vscode.l10n.t('Unable to checkout pull request: missing head branch information.'));
32+
return;
33+
}
34+
35+
const prHead = pullRequestModel.head;
36+
const repositoryToUse = repository || folderManager.repository;
37+
38+
/* __GDPR__
39+
"pr.checkoutInWorktree" : {}
40+
*/
41+
telemetry.sendTelemetryEvent('pr.checkoutInWorktree');
42+
43+
// Prepare for operations
44+
const repoRootPath = repositoryToUse.rootUri.fsPath;
45+
const parentDir = path.dirname(repoRootPath);
46+
const defaultWorktreePath = path.join(parentDir, `pr-${pullRequestModel.number}`);
47+
const branchName = prHead.ref;
48+
const remoteName = pullRequestModel.remote.remoteName;
49+
50+
// Ask user for worktree location first (not in progress)
51+
const worktreeUri = await vscode.window.showSaveDialog({
52+
defaultUri: vscode.Uri.file(defaultWorktreePath),
53+
title: vscode.l10n.t('Select Worktree Location'),
54+
saveLabel: vscode.l10n.t('Create Worktree'),
55+
});
56+
57+
if (!worktreeUri) {
58+
return; // User cancelled
59+
}
60+
61+
const worktreePath = worktreeUri.fsPath;
62+
const trackedBranchName = `${remoteName}/${branchName}`;
63+
64+
try {
65+
// Check if the createWorktree API is available
66+
if (!repositoryToUse.createWorktree) {
67+
throw new Error(vscode.l10n.t('Git worktree API is not available. Please update VS Code to the latest version.'));
68+
}
69+
70+
// Start progress for fetch and worktree creation
71+
await vscode.window.withProgress(
72+
{
73+
location: vscode.ProgressLocation.Notification,
74+
title: vscode.l10n.t('Creating worktree for Pull Request #{0}...', pullRequestModel.number),
75+
},
76+
async () => {
77+
// Fetch the PR branch first
78+
try {
79+
await repositoryToUse.fetch({ remote: remoteName, ref: branchName });
80+
} catch (e) {
81+
const errorMessage = e instanceof Error ? e.message : String(e);
82+
Logger.appendLine(`Failed to fetch branch ${branchName}: ${errorMessage}`, logId);
83+
// Continue even if fetch fails - the branch might already be available locally
84+
}
85+
86+
// Check if the branch already exists locally
87+
let branchExists = false;
88+
try {
89+
await repositoryToUse.getBranch(branchName);
90+
branchExists = true;
91+
} catch {
92+
// Branch doesn't exist locally, we'll create it
93+
branchExists = false;
94+
}
95+
96+
// Use the git extension's createWorktree API
97+
// If branch already exists, don't specify the branch parameter to avoid "branch already exists" error
98+
if (branchExists) {
99+
await repositoryToUse.createWorktree!({
100+
path: worktreePath,
101+
commitish: branchName
102+
});
103+
} else {
104+
await repositoryToUse.createWorktree!({
105+
path: worktreePath,
106+
commitish: trackedBranchName,
107+
branch: branchName
108+
});
109+
}
110+
}
111+
);
112+
113+
// Ask user how they want to open the worktree (modal dialog)
114+
const openInNewWindow = vscode.l10n.t('New Window');
115+
const openInCurrentWindow = vscode.l10n.t('Current Window');
116+
const result = await vscode.window.showInformationMessage(
117+
vscode.l10n.t('Worktree created for Pull Request #{0}. How would you like to open it?', pullRequestModel.number),
118+
{ modal: true },
119+
openInNewWindow,
120+
openInCurrentWindow
121+
);
122+
123+
if (result === openInNewWindow) {
124+
await vscode.commands.executeCommand('vscode.openFolder', worktreeUri, { forceNewWindow: true });
125+
} else if (result === openInCurrentWindow) {
126+
await vscode.commands.executeCommand('vscode.openFolder', worktreeUri, { forceNewWindow: false });
127+
}
128+
} catch (e) {
129+
const errorMessage = e instanceof Error ? e.message : String(e);
130+
Logger.error(`Failed to create worktree: ${errorMessage}`, logId);
131+
vscode.window.showErrorMessage(vscode.l10n.t('Failed to create worktree: {0}', errorMessage));
132+
}
133+
}

0 commit comments

Comments
 (0)