Skip to content

Commit 43ac905

Browse files
Copilotalexr00
andauthored
Use GraphQL UpdatePullRequestBranch mutation for conflict-free branch updates (#8419)
* Initial plan * Use GraphQL UpdatePullRequestBranch mutation when there are no conflicts Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Use explicit mergeable state check for GraphQL branch update Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Add validation for head SHA and clarify handling of all mergeable states Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Show update branch button when PR is not checked out (no conflicts) Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Check for conflicts in isUpdateBranchWithGitHubEnabled Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Revert proposed changes * Pull local branch after GraphQL update when PR is checked out Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Only pull after GraphQL update, not after REST API conflict resolution Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Clean up * Revert api files * Update string --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com>
1 parent 974d80a commit 43ac905

File tree

7 files changed

+108
-10
lines changed

7 files changed

+108
-10
lines changed

src/github/folderRepositoryManager.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2453,6 +2453,7 @@ export class FolderRepositoryManager extends Disposable {
24532453
}
24542454

24552455
const isBrowser = (vscode.env.appHost === 'vscode.dev' || vscode.env.appHost === 'github.dev');
2456+
24562457
if (!pullRequest.isActive || isBrowser) {
24572458
const conflictModel = await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, title: vscode.l10n.t('Finding conflicts...') }, () => createConflictResolutionModel(pullRequest));
24582459
if (conflictModel === undefined) {
@@ -2474,6 +2475,21 @@ export class FolderRepositoryManager extends Disposable {
24742475
}
24752476
}
24762477

2478+
if (pullRequest.item.mergeable !== PullRequestMergeability.Conflict) {
2479+
const result = await vscode.window.withProgress(
2480+
{ location: vscode.ProgressLocation.Notification, title: vscode.l10n.t('Updating branch...') },
2481+
async () => {
2482+
const success = await pullRequest.updateBranchWithGraphQL();
2483+
if (success && pullRequest.isActive) {
2484+
await this.repository.pull();
2485+
}
2486+
return success;
2487+
}
2488+
);
2489+
return result;
2490+
}
2491+
2492+
24772493
if (this.repository.state.workingTreeChanges.length > 0 || this.repository.state.indexChanges.length > 0) {
24782494
await vscode.window.showErrorMessage(vscode.l10n.t('The pull request branch cannot be updated when the there changed files in the working tree or index. Stash or commit all change and then try again.'), { modal: true });
24792495
return false;
@@ -3053,7 +3069,7 @@ export const byRemoteName = (name: string): Predicate<GitHubRepository> => ({ re
30533069
/**
30543070
* Unwraps lines that were wrapped for conventional commit message formatting (typically at 72 characters).
30553071
* Similar to GitHub's behavior when converting commit messages to PR descriptions.
3056-
*
3072+
*
30573073
* Rules:
30583074
* - Preserves blank lines as paragraph breaks
30593075
* - Preserves fenced code blocks (```)

src/github/graphql.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -545,6 +545,15 @@ export interface EnqueuePullRequestResponse {
545545
}
546546
}
547547

548+
export interface UpdatePullRequestBranchResponse {
549+
updatePullRequestBranch: {
550+
pullRequest: {
551+
id: string;
552+
headRefOid: string;
553+
}
554+
}
555+
}
556+
548557
export interface SubmittedReview extends Review {
549558
comments: {
550559
nodes: ReviewComment[];

src/github/pullRequestModel.ts

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import {
4444
TimelineEventsResponse,
4545
UnresolveReviewThreadResponse,
4646
UpdateIssueResponse,
47+
UpdatePullRequestBranchResponse,
4748
} from './graphql';
4849
import {
4950
AccountType,
@@ -1201,11 +1202,55 @@ export class PullRequestModel extends IssueModel<PullRequest> implements IPullRe
12011202
}
12021203

12031204
async updateBranch(model: ConflictResolutionModel): Promise<boolean> {
1204-
if (this.item.mergeable === PullRequestMergeability.Conflict && (!model.resolvedConflicts || model.resolvedConflicts.size === 0)) {
1205+
if (!model.resolvedConflicts || model.resolvedConflicts.size === 0) {
12051206
throw new Error('Pull Request has conflicts but no resolutions were provided.');
12061207
}
1207-
12081208
Logger.debug(`Updating branch ${model.prHeadBranchName} to ${model.prBaseBranchName} - enter`, GitHubRepository.ID);
1209+
// For Conflict state, use the REST API approach with conflict resolution.
1210+
// For Unknown or NotMergeable states, the REST API approach will also be used as a fallback,
1211+
// though these states may fail for other reasons (e.g., blocked by branch protection).
1212+
return this.updateBranchWithConflictResolution(model!);
1213+
}
1214+
1215+
/**
1216+
* Update the branch using the GitHub GraphQL API's UpdatePullRequestBranch mutation.
1217+
* This is used when there are no conflicts between the head and base branches.
1218+
*/
1219+
public async updateBranchWithGraphQL(): Promise<boolean> {
1220+
Logger.debug(`Updating branch using GraphQL UpdatePullRequestBranch mutation - enter`, GitHubRepository.ID);
1221+
1222+
if (!this.head?.sha) {
1223+
Logger.error(`Cannot update branch: head SHA is not available`, GitHubRepository.ID);
1224+
return false;
1225+
}
1226+
1227+
try {
1228+
const { mutate, schema } = await this.githubRepository.ensure();
1229+
1230+
await mutate<UpdatePullRequestBranchResponse>({
1231+
mutation: schema.UpdatePullRequestBranch,
1232+
variables: {
1233+
input: {
1234+
pullRequestId: this.graphNodeId,
1235+
expectedHeadOid: this.head.sha
1236+
}
1237+
}
1238+
});
1239+
1240+
Logger.debug(`Updating branch using GraphQL UpdatePullRequestBranch mutation - done`, GitHubRepository.ID);
1241+
return true;
1242+
} catch (e) {
1243+
Logger.error(`Updating branch using GraphQL UpdatePullRequestBranch mutation failed: ${e}`, GitHubRepository.ID);
1244+
return false;
1245+
}
1246+
}
1247+
1248+
/**
1249+
* Update the branch with conflict resolution using the REST API.
1250+
* This is used when there are conflicts between the head and base branches.
1251+
*/
1252+
private async updateBranchWithConflictResolution(model: ConflictResolutionModel): Promise<boolean> {
1253+
Logger.debug(`Updating branch ${model.prHeadBranchName} with conflict resolution - enter`, GitHubRepository.ID);
12091254
try {
12101255
const { octokit } = await this.githubRepository.ensure();
12111256

@@ -1225,10 +1270,10 @@ export class PullRequestModel extends IssueModel<PullRequest> implements IPullRe
12251270
await octokit.call(octokit.api.git.updateRef, { owner: model.prHeadOwner, repo: model.repositoryName, ref: `heads/${model.prHeadBranchName}`, sha: newCommitSha });
12261271

12271272
} catch (e) {
1228-
Logger.error(`Updating branch ${model.prHeadBranchName} to ${model.prBaseBranchName} failed: ${e}`, GitHubRepository.ID);
1273+
Logger.error(`Updating branch ${model.prHeadBranchName} with conflict resolution failed: ${e}`, GitHubRepository.ID);
12291274
return false;
12301275
}
1231-
Logger.debug(`Updating branch ${model.prHeadBranchName} to ${model.prBaseBranchName} - done`, GitHubRepository.ID);
1276+
Logger.debug(`Updating branch ${model.prHeadBranchName} with conflict resolution - done`, GitHubRepository.ID);
12321277
return true;
12331278
}
12341279

src/github/pullRequestOverview.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
ITeam,
1818
MergeMethod,
1919
MergeMethodsAvailability,
20+
PullRequestMergeability,
2021
ReviewEventEnum,
2122
ReviewState,
2223
} from './interface';
@@ -220,7 +221,13 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode
220221
}
221222

222223
private isUpdateBranchWithGitHubEnabled(): boolean {
223-
return this._item.isActive || vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE).get('experimentalUpdateBranchWithGitHub', false);
224+
// With the GraphQL UpdatePullRequestBranch API, we can update branches even when not checked out
225+
// (as long as there are no conflicts). If there are conflicts, we need the branch to be checked out.
226+
const hasConflicts = this._item.item.mergeable === PullRequestMergeability.Conflict;
227+
if (hasConflicts) {
228+
return this._item.isActive;
229+
}
230+
return true;
224231
}
225232

226233
protected override continueOnGitHub() {

src/github/pullRequestReviewCommon.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -162,13 +162,16 @@ export namespace PullRequestReviewCommon {
162162
refreshAfterUpdate: () => Promise<void>,
163163
checkUpdateEnabled?: () => boolean
164164
): Promise<void> {
165-
if (checkUpdateEnabled && !checkUpdateEnabled()) {
166-
await vscode.window.showErrorMessage(vscode.l10n.t('The pull request branch must be checked out to be updated.'), { modal: true });
165+
// When there are conflicts and the PR is not checked out, we need local checkout to resolve them
166+
const hasConflicts = ctx.item.item.mergeable === PullRequestMergeability.Conflict;
167+
if (hasConflicts && checkUpdateEnabled && !checkUpdateEnabled()) {
168+
await vscode.window.showErrorMessage(vscode.l10n.t('The pull request branch must be checked out to resolve conflicts.'), { modal: true });
167169
return ctx.replyMessage(message, {});
168170
}
169171

170-
if (ctx.folderRepositoryManager.repository.state.workingTreeChanges.length > 0 || ctx.folderRepositoryManager.repository.state.indexChanges.length > 0) {
171-
await vscode.window.showErrorMessage(vscode.l10n.t('The pull request branch cannot be updated when the there changed files in the working tree or index. Stash or commit all change and then try again.'), { modal: true });
172+
// Working tree/index checks only apply when the PR is checked out
173+
if (ctx.item.isActive && (ctx.folderRepositoryManager.repository.state.workingTreeChanges.length > 0 || ctx.folderRepositoryManager.repository.state.indexChanges.length > 0)) {
174+
await vscode.window.showErrorMessage(vscode.l10n.t('The pull request branch cannot be updated when there are changed files in the working tree or index. Stash or commit all change and then try again.'), { modal: true });
172175
return ctx.replyMessage(message, {});
173176
}
174177
const mergeSucceeded = await ctx.folderRepositoryManager.tryMergeBaseIntoHead(ctx.item, true);

src/github/queries.gql

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -763,4 +763,13 @@ mutation ReplaceActorsForAssignable($input: ReplaceActorsForAssignableInput!) {
763763
}
764764
}
765765
}
766+
}
767+
768+
mutation UpdatePullRequestBranch($input: UpdatePullRequestBranchInput!) {
769+
updatePullRequestBranch(input: $input) {
770+
pullRequest {
771+
id
772+
headRefOid
773+
}
774+
}
766775
}

src/github/queriesExtra.gql

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -806,4 +806,13 @@ mutation EnqueuePullRequest($input: EnqueuePullRequestInput!) {
806806
...MergeQueueEntryFragment
807807
}
808808
}
809+
}
810+
811+
mutation UpdatePullRequestBranch($input: UpdatePullRequestBranchInput!) {
812+
updatePullRequestBranch(input: $input) {
813+
pullRequest {
814+
id
815+
headRefOid
816+
}
817+
}
809818
}

0 commit comments

Comments
 (0)