From f4f72b0eee026357559093c99fa73eb50592d811 Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Wed, 15 Jul 2026 14:53:15 +0200 Subject: [PATCH] Fix the "update with merge commit" button for non-checked-out PRs --- src/github/folderRepositoryManager.ts | 29 +++++------ src/github/pullRequestReviewCommon.ts | 4 +- .../github/folderRepositoryManager.test.ts | 51 +++++++++++++++++++ webviews/common/context.tsx | 1 + 4 files changed, 67 insertions(+), 18 deletions(-) diff --git a/src/github/folderRepositoryManager.ts b/src/github/folderRepositoryManager.ts index 9b4497d244..5d691cc2b2 100644 --- a/src/github/folderRepositoryManager.ts +++ b/src/github/folderRepositoryManager.ts @@ -2554,6 +2554,20 @@ export class FolderRepositoryManager extends Disposable { const isBrowser = (vscode.env.appHost === 'vscode.dev' || vscode.env.appHost === 'github.dev'); + if (pullRequest.item.mergeable !== PullRequestMergeability.Conflict && !pullRequest.githubRepository.remote.isEnterprise) { + const result = await vscode.window.withProgress( + { location: vscode.ProgressLocation.Notification, title: vscode.l10n.t('Updating branch...') }, + async () => { + const success = await pullRequest.updateBranchWithGraphQL(); + if (success && pullRequest.isActive) { + await this.repository.pull(); + } + return success; + } + ); + return result; + } + if (!pullRequest.isActive || isBrowser) { const conflictModel = await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, title: vscode.l10n.t('Finding conflicts...') }, () => createConflictResolutionModel(pullRequest)); if (conflictModel === undefined) { @@ -2576,21 +2590,6 @@ export class FolderRepositoryManager extends Disposable { } } - if (pullRequest.item.mergeable !== PullRequestMergeability.Conflict && !pullRequest.githubRepository.remote.isEnterprise) { - const result = await vscode.window.withProgress( - { location: vscode.ProgressLocation.Notification, title: vscode.l10n.t('Updating branch...') }, - async () => { - const success = await pullRequest.updateBranchWithGraphQL(); - if (success && pullRequest.isActive) { - await this.repository.pull(); - } - return success; - } - ); - return result; - } - - if (this.repository.state.workingTreeChanges.length > 0 || this.repository.state.indexChanges.length > 0) { 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 }); return false; diff --git a/src/github/pullRequestReviewCommon.ts b/src/github/pullRequestReviewCommon.ts index 8d52d05619..143f627702 100644 --- a/src/github/pullRequestReviewCommon.ts +++ b/src/github/pullRequestReviewCommon.ts @@ -179,9 +179,6 @@ export namespace PullRequestReviewCommon { return ctx.replyMessage(message, {}); } const mergeSucceeded = await ctx.folderRepositoryManager.tryMergeBaseIntoHead(ctx.item, true); - if (!mergeSucceeded) { - ctx.replyMessage(message, {}); - } // The mergability of the PR doesn't update immediately. Poll. let mergability = PullRequestMergeability.Unknown; let attemptsRemaining = 5; @@ -194,6 +191,7 @@ export namespace PullRequestReviewCommon { const result: Partial = { events: await ctx.getTimeline(), mergeable: mergability, + canUpdateBranch: !mergeSucceeded, }; await refreshAfterUpdate(); diff --git a/src/test/github/folderRepositoryManager.test.ts b/src/test/github/folderRepositoryManager.test.ts index ef746bf208..7e51d692ad 100644 --- a/src/test/github/folderRepositoryManager.test.ts +++ b/src/test/github/folderRepositoryManager.test.ts @@ -24,6 +24,10 @@ import { GitHubServerType } from '../../common/authentication'; import { CreatePullRequestHelper } from '../../view/createPullRequestHelper'; import { RepositoriesManager } from '../../github/repositoriesManager'; import { MockThemeWatcher } from '../mocks/mockThemeWatcher'; +import { PullRequestReviewCommon, ReviewContext } from '../../github/pullRequestReviewCommon'; +import { IRequestMessage } from '../../common/webview'; +import { PullRequestMergeability } from '../../github/interface'; +import { PullRequest } from '../../github/views'; describe('PullRequestManager', function () { let sinon: SinonSandbox; @@ -68,6 +72,53 @@ describe('PullRequestManager', function () { assert.deepStrictEqual(manager.activePullRequest, pr); }); }); + + describe('tryMergeBaseIntoHead', function () { + it('updates a conflict-free pull request that is not checked out using GraphQL', async function () { + const url = 'https://github.com/aaa/bbb.git'; + const remote = new GitHubRemote('origin', url, new Protocol(url), GitHubServerType.GitHubDotCom); + const repository = new GitHubRepository(1, remote, Uri.file('C:\\users\\test\\repo'), manager.credentialStore, telemetry); + const prItem = convertRESTPullRequestToRawPullRequest(new PullRequestBuilder().build(), repository); + const pr = new PullRequestModel(manager.credentialStore, telemetry, repository, remote, prItem); + sinon.stub(manager, 'isHeadUpToDateWithBase').resolves(false); + const updateBranchWithGraphQL = sinon.stub(pr, 'updateBranchWithGraphQL').resolves(true); + const updateBranch = sinon.stub(pr, 'updateBranch').resolves(true); + + const result = await manager.tryMergeBaseIntoHead(pr, true); + + assert.strictEqual(result, true); + assert.strictEqual(updateBranchWithGraphQL.calledOnce, true); + assert.strictEqual(updateBranch.notCalled, true); + }); + }); + + describe('updateBranch', function () { + it('reports that the branch is up to date after a successful update', async function () { + const url = 'https://github.com/aaa/bbb.git'; + const remote = new GitHubRemote('origin', url, new Protocol(url), GitHubServerType.GitHubDotCom); + const repository = new GitHubRepository(1, remote, Uri.file('C:\\users\\test\\repo'), manager.credentialStore, telemetry); + const prItem = convertRESTPullRequestToRawPullRequest(new PullRequestBuilder().build(), repository); + const pr = new PullRequestModel(manager.credentialStore, telemetry, repository, remote, prItem); + sinon.stub(manager, 'tryMergeBaseIntoHead').resolves(true); + sinon.stub(pr, 'getMergeability').resolves({ mergeability: PullRequestMergeability.Mergeable }); + let reply: Partial | undefined; + const context: ReviewContext = { + item: pr, + folderRepositoryManager: manager, + existingReviewers: [], + postMessage: sinon.stub().resolves(), + replyMessage: (_message, response) => reply = response, + throwError: sinon.stub(), + getTimeline: sinon.stub().resolves([]), + }; + const message: IRequestMessage = { req: '1', command: 'pr.update-branch', args: '' }; + + await PullRequestReviewCommon.updateBranch(context, message, sinon.stub().resolves()); + + assert.strictEqual(reply?.canUpdateBranch, false); + assert.strictEqual(reply?.mergeable, PullRequestMergeability.Mergeable); + }); + }); }); describe('titleAndBodyFrom', function () { diff --git a/webviews/common/context.tsx b/webviews/common/context.tsx index 5d34894694..dc05951156 100644 --- a/webviews/common/context.tsx +++ b/webviews/common/context.tsx @@ -452,6 +452,7 @@ export class PRContext { const result: Partial = await this.postMessage({ command: 'pr.update-branch' }); state.events = result.events ?? state.events; state.mergeable = result.mergeable ?? state.mergeable; + state.canUpdateBranch = result.canUpdateBranch ?? state.canUpdateBranch; this.updatePR(state); };