From 85458975d15f11cc3209ea3666d184a9d2e1c761 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Jul 2026 10:11:02 +0000 Subject: [PATCH 1/2] Initial plan From cde3c7a35ebbdf89fa9420635b8b73841cd72811 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Jul 2026 10:19:12 +0000 Subject: [PATCH 2/2] Remove leftover branch git config when deleting local branches Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> --- src/github/folderRepositoryManager.ts | 14 ++++++++ .../github/folderRepositoryManager.test.ts | 35 ++++++++++++++++++- src/test/mocks/mockRepository.ts | 10 +++++- 3 files changed, 57 insertions(+), 2 deletions(-) diff --git a/src/github/folderRepositoryManager.ts b/src/github/folderRepositoryManager.ts index 9b4497d244..f66ffd78f4 100644 --- a/src/github/folderRepositoryManager.ts +++ b/src/github/folderRepositoryManager.ts @@ -2099,6 +2099,20 @@ export class FolderRepositoryManager extends Disposable { const deleteConfig = async (branch: string) => { await PullRequestGitHelper.associateBaseBranchWithBranch(this.repository, branch, undefined); await PullRequestGitHelper.associateBranchWithPullRequest(this.repository, undefined, branch); + // Git normally removes the entire [branch ""] section when a branch is deleted, but if the + // branch no longer exists the leftover section keeps the branch showing up in this list. Remove any + // remaining branch..* config entries so already-deleted branches don't reappear. + if (this.repository.unsetConfig) { + const prefix = `branch.${branch}.`; + const remaining = (await this.repository.getConfigs()).filter(config => config.key.startsWith(prefix)); + for (const config of remaining) { + try { + await this.repository.unsetConfig(config.key); + } catch (e) { + Logger.error(`Failed to remove leftover git config ${config.key}: ${e}`, this.id); + } + } + } }; // delete configs first since that can't be parallelized diff --git a/src/test/github/folderRepositoryManager.test.ts b/src/test/github/folderRepositoryManager.test.ts index ef746bf208..30056da274 100644 --- a/src/test/github/folderRepositoryManager.test.ts +++ b/src/test/github/folderRepositoryManager.test.ts @@ -30,6 +30,7 @@ describe('PullRequestManager', function () { let manager: FolderRepositoryManager; let telemetry: MockTelemetry; let mockThemeWatcher: MockThemeWatcher; + let repository: MockRepository; beforeEach(function () { sinon = createSandbox(); @@ -37,7 +38,7 @@ describe('PullRequestManager', function () { telemetry = new MockTelemetry(); mockThemeWatcher = new MockThemeWatcher(); - const repository = new MockRepository(); + repository = new MockRepository(); const context = new MockExtensionContext(); const credentialStore = new CredentialStore(telemetry, context); const repositoriesManager = new RepositoriesManager(credentialStore, telemetry); @@ -68,6 +69,38 @@ describe('PullRequestManager', function () { assert.deepStrictEqual(manager.activePullRequest, pr); }); }); + + describe('deleteBranches', function () { + const noopProgress = { report: () => { } }; + + it('removes leftover branch config after deleting a branch', async function () { + await repository.createBranch('feature', false, 'commit-hash'); + await repository.setConfig('branch.feature.remote', 'origin'); + await repository.setConfig('branch.feature.merge', 'refs/heads/feature'); + await repository.setConfig('branch.feature.github-pr-owner-number', 'owner#repo#1'); + + const nonExistant = new Set(); + await (manager as any).deleteBranches([{ label: 'feature' }], nonExistant, noopProgress, 1, 0, []); + + const configs = await repository.getConfigs(); + assert.strictEqual(configs.filter(c => c.key.startsWith('branch.feature.')).length, 0); + assert.strictEqual(nonExistant.has('feature'), false); + }); + + it('removes leftover branch config for a branch that no longer exists', async function () { + // The branch ref is already gone, but stale [branch "gone"] config remains. + await repository.setConfig('branch.gone.remote', 'origin'); + await repository.setConfig('branch.gone.merge', 'refs/heads/gone'); + await repository.setConfig('branch.gone.github-pr-owner-number', 'owner#repo#2'); + + const nonExistant = new Set(); + await (manager as any).deleteBranches([{ label: 'gone' }], nonExistant, noopProgress, 1, 0, []); + + const configs = await repository.getConfigs(); + assert.strictEqual(configs.filter(c => c.key.startsWith('branch.gone.')).length, 0); + assert.strictEqual(nonExistant.has('gone'), true); + }); + }); }); describe('titleAndBodyFrom', function () { diff --git a/src/test/mocks/mockRepository.ts b/src/test/mocks/mockRepository.ts index 9206853fdd..b482e810dc 100644 --- a/src/test/mocks/mockRepository.ts +++ b/src/test/mocks/mockRepository.ts @@ -115,6 +115,12 @@ export class MockRepository implements Repository { return oldValue; } + async unsetConfig(key: string): Promise { + const oldValue = this._config.get(key) || ''; + this._config.delete(key); + return oldValue; + } + getObjectDetails(treeish: string, treePath: string): Promise<{ mode: string; object: string; size: number }> { return Promise.reject(new Error(`Unexpected getObjectDetails(${treeish}, ${treePath})`)); } @@ -181,7 +187,9 @@ export class MockRepository implements Repository { async deleteBranch(name: string, force?: boolean | undefined): Promise { const index = this._branches.findIndex(b => b.name === name); if (index === -1) { - throw new Error(`Attempt to delete nonexistent branch ${name}`); + const error: Error & { stderr?: string } = new Error(`Attempt to delete nonexistent branch ${name}`); + error.stderr = `error: branch '${name}' not found.`; + throw error; } this._branches.splice(index, 1); }