Skip to content

Commit 849821a

Browse files
Copilotalexr00
andauthored
Fix duplicate entries in .git/config for GitHub PRs (#8876)
* Initial plan * Prevent duplicate PR config entries Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Serialize PR config updates Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Attestation commit --------- 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 ccd87b1 commit 849821a

2 files changed

Lines changed: 94 additions & 2 deletions

File tree

src/github/pullRequestGitHelper.ts

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ export type BranchInfo = {
4242

4343
export class PullRequestGitHelper {
4444
static ID = 'PullRequestGitHelper';
45+
private static readonly configUpdates = new WeakMap<Repository, Map<string, Promise<void>>>();
46+
4547
static async checkoutFromFork(
4648
repository: Repository,
4749
pullRequest: PullRequestModel & IResolvedPullRequestModel,
@@ -307,6 +309,38 @@ export class PullRequestGitHelper {
307309
return `${owner}#${repository}#${baseBranch}`;
308310
}
309311

312+
private static async setConfig(repository: Repository, key: string, value: string): Promise<void> {
313+
let repositoryUpdates = PullRequestGitHelper.configUpdates.get(repository);
314+
if (!repositoryUpdates) {
315+
repositoryUpdates = new Map();
316+
PullRequestGitHelper.configUpdates.set(repository, repositoryUpdates);
317+
}
318+
319+
const previousUpdate = repositoryUpdates.get(key);
320+
const update = (previousUpdate ? previousUpdate.catch(() => undefined) : Promise.resolve()).then(async () => {
321+
const existingConfigs = (await repository.getConfigs()).filter(config => config.key === key);
322+
if (existingConfigs.some(config => config.value === value)) {
323+
return;
324+
}
325+
if (existingConfigs.length === 1 && repository.unsetConfig) {
326+
await repository.unsetConfig(key);
327+
}
328+
await repository.setConfig(key, value);
329+
});
330+
repositoryUpdates.set(key, update);
331+
332+
try {
333+
await update;
334+
} finally {
335+
if (repositoryUpdates.get(key) === update) {
336+
repositoryUpdates.delete(key);
337+
if (!repositoryUpdates.size) {
338+
PullRequestGitHelper.configUpdates.delete(repository);
339+
}
340+
}
341+
}
342+
}
343+
310344
static parsePullRequestMetadata(value: string): PullRequestMetadata | undefined {
311345
if (value) {
312346
const matches = /(.*)#(.*)#(.*)/g.exec(value);
@@ -434,7 +468,7 @@ export class PullRequestGitHelper {
434468
}
435469
const prConfigKey = `branch.${branchName}.${PullRequestMetadataKey}`;
436470
if (pullRequest) {
437-
await repository.setConfig(prConfigKey, PullRequestGitHelper.buildPullRequestMetadata(pullRequest));
471+
await PullRequestGitHelper.setConfig(repository, prConfigKey, PullRequestGitHelper.buildPullRequestMetadata(pullRequest));
438472
} else if (repository.unsetConfig) {
439473
await repository.unsetConfig(prConfigKey);
440474
}
@@ -458,7 +492,7 @@ export class PullRequestGitHelper {
458492
const prConfigKey = `branch.${branch}.${BaseBranchMetadataKey}`;
459493
if (base) {
460494
Logger.appendLine(`associate ${branch} with base branch ${base.owner}/${base.repo}#${base.branch}`, PullRequestGitHelper.ID);
461-
await repository.setConfig(prConfigKey, PullRequestGitHelper.buildBaseBranchMetadata(base.owner, base.repo, base.branch));
495+
await PullRequestGitHelper.setConfig(repository, prConfigKey, PullRequestGitHelper.buildBaseBranchMetadata(base.owner, base.repo, base.branch));
462496
} else if (repository.unsetConfig) {
463497
await repository.unsetConfig(prConfigKey);
464498
const vscodeBaseBranchConfigKey = `branch.${branch}.${VscodeBaseBranchMetadataKey}`;

src/test/github/pullRequestGitHelper.test.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,64 @@ describe('PullRequestGitHelper', function () {
189189
});
190190
});
191191

192+
describe('associateBranchWithPullRequest', function () {
193+
const pullRequest = (number: number) => ({
194+
number,
195+
base: {
196+
repositoryCloneUrl: {
197+
owner: 'owner',
198+
repositoryName: 'name',
199+
},
200+
},
201+
}) as PullRequestModel;
202+
203+
it('replaces pull request metadata instead of appending values', async function () {
204+
await PullRequestGitHelper.associateBranchWithPullRequest(repository, pullRequest(100), 'feature');
205+
await PullRequestGitHelper.associateBranchWithPullRequest(repository, pullRequest(100), 'feature');
206+
await PullRequestGitHelper.associateBranchWithPullRequest(repository, pullRequest(101), 'feature');
207+
208+
const key = 'branch.feature.github-pr-owner-number';
209+
assert.deepStrictEqual((await repository.getConfigs()).filter(config => config.key === key), [
210+
{ key, value: 'owner#name#101' },
211+
]);
212+
});
213+
214+
it('does not append metadata during concurrent associations', async function () {
215+
await Promise.all([
216+
PullRequestGitHelper.associateBranchWithPullRequest(repository, pullRequest(100), 'feature'),
217+
PullRequestGitHelper.associateBranchWithPullRequest(repository, pullRequest(100), 'feature'),
218+
]);
219+
220+
const key = 'branch.feature.github-pr-owner-number';
221+
assert.deepStrictEqual((await repository.getConfigs()).filter(config => config.key === key), [
222+
{ key, value: 'owner#name#100' },
223+
]);
224+
});
225+
226+
it('does not append to existing duplicate metadata', async function () {
227+
const key = 'branch.feature.github-pr-owner-number';
228+
await repository.setConfig(key, 'owner#name#100');
229+
await repository.setConfig(key, 'owner#name#100');
230+
231+
await PullRequestGitHelper.associateBranchWithPullRequest(repository, pullRequest(100), 'feature');
232+
233+
assert.strictEqual((await repository.getConfigs()).filter(config => config.key === key).length, 2);
234+
});
235+
});
236+
237+
describe('associateBaseBranchWithBranch', function () {
238+
it('replaces base branch metadata instead of appending values', async function () {
239+
await PullRequestGitHelper.associateBaseBranchWithBranch(repository, 'feature', { owner: 'owner', repo: 'name', branch: 'main' });
240+
await PullRequestGitHelper.associateBaseBranchWithBranch(repository, 'feature', { owner: 'owner', repo: 'name', branch: 'main' });
241+
await PullRequestGitHelper.associateBaseBranchWithBranch(repository, 'feature', { owner: 'owner', repo: 'name', branch: 'next' });
242+
243+
const key = 'branch.feature.github-pr-base-branch';
244+
assert.deepStrictEqual((await repository.getConfigs()).filter(config => config.key === key), [
245+
{ key, value: 'owner#name#next' },
246+
]);
247+
});
248+
});
249+
192250
describe('getMatchingPullRequestMetadataForBranch', function () {
193251
it('returns the highest-numbered PR when duplicate config entries exist for the branch', async function () {
194252
// Simulate the case where a branch name has been associated with multiple

0 commit comments

Comments
 (0)