Skip to content

Commit 26ce8cc

Browse files
authored
Support applying coding agent changes to local. (#7690)
1 parent 1f76dc1 commit 26ce8cc

4 files changed

Lines changed: 116 additions & 0 deletions

File tree

package.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1323,6 +1323,12 @@
13231323
"category": "%command.pull.request.category%",
13241324
"icon": "$(git-compare)"
13251325
},
1326+
{
1327+
"command": "pr.applyChangesFromDescription",
1328+
"title": "%command.pr.applyChangesFromDescription.title%",
1329+
"category": "%command.pull.request.category%",
1330+
"icon": "$(git-stash-apply)"
1331+
},
13261332
{
13271333
"command": "pr.checkoutOnVscodeDevFromDescription",
13281334
"title": "%command.pr.checkoutOnVscodeDevFromDescription.title%",
@@ -3443,6 +3449,10 @@
34433449
{
34443450
"command": "pr.checkoutFromDescription",
34453451
"when": "chatSessionType == copilot-swe-agent"
3452+
},
3453+
{
3454+
"command": "pr.applyChangesFromDescription",
3455+
"when": "chatSessionType == copilot-swe-agent"
34463456
}
34473457
]
34483458
},

package.nls.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,7 @@
275275
"command.pr.toggleEditorCommentingOn.title": "Toggle Editor Commenting On",
276276
"command.pr.toggleEditorCommentingOff.title": "Toggle Editor Commenting Off",
277277
"command.pr.checkoutFromDescription.title": "Checkout",
278+
"command.pr.applyChangesFromDescription.title": "Apply Changes",
278279
"command.pr.checkoutOnVscodeDevFromDescription.title": "Checkout on vscode.dev",
279280
"command.pr.openSessionLogFromDescription.title": "View Session",
280281
"command.issue.openDescription.title": "View Issue Description",

src/commands.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -651,6 +651,63 @@ export function registerCommands(
651651
return { folderManager, pr };
652652
};
653653

654+
const applyPullRequestChanges = async (folderManager: FolderRepositoryManager, pullRequest: PullRequestModel): Promise<void> => {
655+
let patch: string | undefined;
656+
try {
657+
patch = await pullRequest.getPatch();
658+
659+
if (!patch.trim()) {
660+
vscode.window.showErrorMessage(vscode.l10n.t('No patch data available for pull request #{0}', pullRequest.number.toString()));
661+
return;
662+
}
663+
664+
const tempFilePath = pathLib.join(
665+
folderManager.repository.rootUri.fsPath,
666+
'.git',
667+
`pr-${pullRequest.number}.patch`,
668+
);
669+
const encoder = new TextEncoder();
670+
const tempUri = vscode.Uri.file(tempFilePath);
671+
672+
await vscode.window.withProgress(
673+
{
674+
location: vscode.ProgressLocation.Notification,
675+
title: vscode.l10n.t('Applying changes from PR #{0}', pullRequest.number.toString()),
676+
cancellable: false
677+
},
678+
async (task) => {
679+
await vscode.workspace.fs.writeFile(tempUri, encoder.encode(patch));
680+
try {
681+
await folderManager.repository.apply(tempFilePath, false);
682+
task.report({ message: vscode.l10n.t('Successfully applied changes from pull request #{0}', pullRequest.number.toString()), increment: 100 });
683+
} finally {
684+
await vscode.workspace.fs.delete(tempUri);
685+
}
686+
}
687+
);
688+
689+
} catch (error) {
690+
const errorMessage = formatError(error);
691+
Logger.error(`Failed to apply PR changes: ${errorMessage}`, 'Commands');
692+
693+
const copyGitApply = vscode.l10n.t('Copy git apply');
694+
const result = await vscode.window.showErrorMessage(
695+
vscode.l10n.t('Failed to apply changes from pull request: {0}', errorMessage),
696+
copyGitApply
697+
);
698+
699+
if (result === copyGitApply) {
700+
if (patch) {
701+
const gitApplyCommand = `git apply --3way <<'EOF'\n${patch}\nEOF`;
702+
await vscode.env.clipboard.writeText(gitApplyCommand);
703+
vscode.window.showInformationMessage(vscode.l10n.t('Git apply command copied to clipboard'));
704+
} else {
705+
vscode.window.showErrorMessage(vscode.l10n.t('Unable to copy git apply command - patch content is not available'));
706+
}
707+
}
708+
}
709+
};
710+
654711
function contextHasPath(ctx: OverviewContext | { path: string } | undefined): ctx is { path: string } {
655712
const contextAsPath: Partial<{ path: string }> = (ctx as { path: string });
656713
return !!contextAsPath.path;
@@ -684,6 +741,34 @@ export function registerCommands(
684741

685742
}));
686743

744+
context.subscriptions.push(vscode.commands.registerCommand('pr.applyChangesFromDescription', async (ctx: OverviewContext | { path: string } | undefined) => {
745+
if (!ctx) {
746+
return vscode.window.showErrorMessage(vscode.l10n.t('No pull request context provided for applying changes.'));
747+
}
748+
749+
if (contextHasPath(ctx)) {
750+
const { path } = ctx;
751+
const prNumber = Number(Buffer.from(path.substring(1), 'base64').toString('utf8'));
752+
if (Number.isNaN(prNumber)) {
753+
return vscode.window.showErrorMessage(vscode.l10n.t('Unable to parse pull request number.'));
754+
}
755+
const folderManager = reposManager.folderManagers[0];
756+
const pullRequest = await folderManager.fetchById(folderManager.gitHubRepositories[0], Number(prNumber));
757+
if (!pullRequest) {
758+
return vscode.window.showErrorMessage(vscode.l10n.t('Unable to find pull request #{0}', prNumber.toString()));
759+
}
760+
761+
return applyPullRequestChanges(folderManager, pullRequest);
762+
}
763+
764+
const resolved = await resolvePr(ctx);
765+
if (!resolved) {
766+
return vscode.window.showErrorMessage(vscode.l10n.t('Unable to resolve pull request for applying changes.'));
767+
}
768+
return applyPullRequestChanges(resolved.folderManager, resolved.pr);
769+
770+
}));
771+
687772
context.subscriptions.push(
688773
vscode.commands.registerCommand('pr.openChanges', async (pr: PRNode | RepositoryChangesNode | PullRequestModel | OverviewContext | undefined) => {
689774
if (pr === undefined) {

src/github/pullRequestModel.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1483,6 +1483,26 @@ export class PullRequestModel extends IssueModel<PullRequest> implements IPullRe
14831483
return parsed;
14841484
}
14851485

1486+
async getPatch(): Promise<string> {
1487+
const githubRepository = this.githubRepository;
1488+
const { octokit, remote } = await githubRepository.ensure();
1489+
1490+
const { data } = await octokit.call(octokit.api.pulls.get, {
1491+
owner: remote.owner,
1492+
repo: remote.repositoryName,
1493+
pull_number: this.number,
1494+
mediaType: {
1495+
format: 'diff'
1496+
}
1497+
});
1498+
1499+
if (typeof data === 'string') {
1500+
return data;
1501+
} else {
1502+
throw new Error('Expected diff data to be a string');
1503+
}
1504+
}
1505+
14861506
public static async getChangeModels(folderManager: FolderRepositoryManager, pullRequestModel: PullRequestModel): Promise<(RemoteFileChangeModel | InMemFileChangeModel)[]> {
14871507
const isCurrentPR = folderManager.activePullRequest?.number === pullRequestModel.number;
14881508
const changes = pullRequestModel.fileChanges.size > 0 ? pullRequestModel.fileChanges.values() : await pullRequestModel.getFileChangesInfo();

0 commit comments

Comments
 (0)