From 911750d1050db3f83b550aa0b5c4cff37bbeb1a5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 5 Aug 2026 10:31:23 +0000 Subject: [PATCH 1/4] Initial plan From 94a21f918c7baeaaceb26c7d67dcc688ac97d681 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 5 Aug 2026 10:44:24 +0000 Subject: [PATCH 2/4] Prevent duplicate PR config entries Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> --- src/github/pullRequestGitHelper.ts | 15 ++++++- src/test/github/pullRequestGitHelper.test.ts | 46 ++++++++++++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/src/github/pullRequestGitHelper.ts b/src/github/pullRequestGitHelper.ts index 6d93d2b176..343377ded0 100644 --- a/src/github/pullRequestGitHelper.ts +++ b/src/github/pullRequestGitHelper.ts @@ -307,6 +307,17 @@ export class PullRequestGitHelper { return `${owner}#${repository}#${baseBranch}`; } + private static async setConfig(repository: Repository, key: string, value: string): Promise { + const existingConfigs = (await repository.getConfigs()).filter(config => config.key === key); + if (existingConfigs.some(config => config.value === value)) { + return; + } + if (existingConfigs.length === 1 && repository.unsetConfig) { + await repository.unsetConfig(key); + } + await repository.setConfig(key, value); + } + static parsePullRequestMetadata(value: string): PullRequestMetadata | undefined { if (value) { const matches = /(.*)#(.*)#(.*)/g.exec(value); @@ -434,7 +445,7 @@ export class PullRequestGitHelper { } const prConfigKey = `branch.${branchName}.${PullRequestMetadataKey}`; if (pullRequest) { - await repository.setConfig(prConfigKey, PullRequestGitHelper.buildPullRequestMetadata(pullRequest)); + await PullRequestGitHelper.setConfig(repository, prConfigKey, PullRequestGitHelper.buildPullRequestMetadata(pullRequest)); } else if (repository.unsetConfig) { await repository.unsetConfig(prConfigKey); } @@ -458,7 +469,7 @@ export class PullRequestGitHelper { const prConfigKey = `branch.${branch}.${BaseBranchMetadataKey}`; if (base) { Logger.appendLine(`associate ${branch} with base branch ${base.owner}/${base.repo}#${base.branch}`, PullRequestGitHelper.ID); - await repository.setConfig(prConfigKey, PullRequestGitHelper.buildBaseBranchMetadata(base.owner, base.repo, base.branch)); + await PullRequestGitHelper.setConfig(repository, prConfigKey, PullRequestGitHelper.buildBaseBranchMetadata(base.owner, base.repo, base.branch)); } else if (repository.unsetConfig) { await repository.unsetConfig(prConfigKey); const vscodeBaseBranchConfigKey = `branch.${branch}.${VscodeBaseBranchMetadataKey}`; diff --git a/src/test/github/pullRequestGitHelper.test.ts b/src/test/github/pullRequestGitHelper.test.ts index 8c51164df0..9c50b09e11 100644 --- a/src/test/github/pullRequestGitHelper.test.ts +++ b/src/test/github/pullRequestGitHelper.test.ts @@ -189,6 +189,52 @@ describe('PullRequestGitHelper', function () { }); }); + describe('associateBranchWithPullRequest', function () { + const pullRequest = (number: number) => ({ + number, + base: { + repositoryCloneUrl: { + owner: 'owner', + repositoryName: 'name', + }, + }, + }) as PullRequestModel; + + it('replaces pull request metadata instead of appending values', async function () { + await PullRequestGitHelper.associateBranchWithPullRequest(repository, pullRequest(100), 'feature'); + await PullRequestGitHelper.associateBranchWithPullRequest(repository, pullRequest(100), 'feature'); + await PullRequestGitHelper.associateBranchWithPullRequest(repository, pullRequest(101), 'feature'); + + const key = 'branch.feature.github-pr-owner-number'; + assert.deepStrictEqual((await repository.getConfigs()).filter(config => config.key === key), [ + { key, value: 'owner#name#101' }, + ]); + }); + + it('does not append to existing duplicate metadata', async function () { + const key = 'branch.feature.github-pr-owner-number'; + await repository.setConfig(key, 'owner#name#100'); + await repository.setConfig(key, 'owner#name#100'); + + await PullRequestGitHelper.associateBranchWithPullRequest(repository, pullRequest(100), 'feature'); + + assert.strictEqual((await repository.getConfigs()).filter(config => config.key === key).length, 2); + }); + }); + + describe('associateBaseBranchWithBranch', function () { + it('replaces base branch metadata instead of appending values', async function () { + await PullRequestGitHelper.associateBaseBranchWithBranch(repository, 'feature', { owner: 'owner', repo: 'name', branch: 'main' }); + await PullRequestGitHelper.associateBaseBranchWithBranch(repository, 'feature', { owner: 'owner', repo: 'name', branch: 'main' }); + await PullRequestGitHelper.associateBaseBranchWithBranch(repository, 'feature', { owner: 'owner', repo: 'name', branch: 'next' }); + + const key = 'branch.feature.github-pr-base-branch'; + assert.deepStrictEqual((await repository.getConfigs()).filter(config => config.key === key), [ + { key, value: 'owner#name#next' }, + ]); + }); + }); + describe('getMatchingPullRequestMetadataForBranch', function () { it('returns the highest-numbered PR when duplicate config entries exist for the branch', async function () { // Simulate the case where a branch name has been associated with multiple From 4fca727b09c8dc18bc55c59c7fcef1b21f985b99 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 11 Aug 2026 11:16:50 +0000 Subject: [PATCH 3/4] Serialize PR config updates Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> --- src/github/pullRequestGitHelper.ts | 35 ++++++++++++++++---- src/test/github/pullRequestGitHelper.test.ts | 12 +++++++ 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/src/github/pullRequestGitHelper.ts b/src/github/pullRequestGitHelper.ts index 343377ded0..d990401155 100644 --- a/src/github/pullRequestGitHelper.ts +++ b/src/github/pullRequestGitHelper.ts @@ -42,6 +42,8 @@ export type BranchInfo = { export class PullRequestGitHelper { static ID = 'PullRequestGitHelper'; + private static readonly configUpdates = new WeakMap>>(); + static async checkoutFromFork( repository: Repository, pullRequest: PullRequestModel & IResolvedPullRequestModel, @@ -308,14 +310,35 @@ export class PullRequestGitHelper { } private static async setConfig(repository: Repository, key: string, value: string): Promise { - const existingConfigs = (await repository.getConfigs()).filter(config => config.key === key); - if (existingConfigs.some(config => config.value === value)) { - return; + let repositoryUpdates = PullRequestGitHelper.configUpdates.get(repository); + if (!repositoryUpdates) { + repositoryUpdates = new Map(); + PullRequestGitHelper.configUpdates.set(repository, repositoryUpdates); } - if (existingConfigs.length === 1 && repository.unsetConfig) { - await repository.unsetConfig(key); + + const previousUpdate = repositoryUpdates.get(key); + const update = (previousUpdate ? previousUpdate.catch(() => undefined) : Promise.resolve()).then(async () => { + const existingConfigs = (await repository.getConfigs()).filter(config => config.key === key); + if (existingConfigs.some(config => config.value === value)) { + return; + } + if (existingConfigs.length === 1 && repository.unsetConfig) { + await repository.unsetConfig(key); + } + await repository.setConfig(key, value); + }); + repositoryUpdates.set(key, update); + + try { + await update; + } finally { + if (repositoryUpdates.get(key) === update) { + repositoryUpdates.delete(key); + if (!repositoryUpdates.size) { + PullRequestGitHelper.configUpdates.delete(repository); + } + } } - await repository.setConfig(key, value); } static parsePullRequestMetadata(value: string): PullRequestMetadata | undefined { diff --git a/src/test/github/pullRequestGitHelper.test.ts b/src/test/github/pullRequestGitHelper.test.ts index 9c50b09e11..03d796579b 100644 --- a/src/test/github/pullRequestGitHelper.test.ts +++ b/src/test/github/pullRequestGitHelper.test.ts @@ -211,6 +211,18 @@ describe('PullRequestGitHelper', function () { ]); }); + it('does not append metadata during concurrent associations', async function () { + await Promise.all([ + PullRequestGitHelper.associateBranchWithPullRequest(repository, pullRequest(100), 'feature'), + PullRequestGitHelper.associateBranchWithPullRequest(repository, pullRequest(100), 'feature'), + ]); + + const key = 'branch.feature.github-pr-owner-number'; + assert.deepStrictEqual((await repository.getConfigs()).filter(config => config.key === key), [ + { key, value: 'owner#name#100' }, + ]); + }); + it('does not append to existing duplicate metadata', async function () { const key = 'branch.feature.github-pr-owner-number'; await repository.setConfig(key, 'owner#name#100'); From 83a0c92c1a206b80607fb990df63909c920c76c7 Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Wed, 12 Aug 2026 12:02:42 +0200 Subject: [PATCH 4/4] Attestation commit