From d5a94335fcdd943396fd03d331708374684bd0b0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Dec 2025 10:24:25 +0000 Subject: [PATCH 1/8] Initial plan From 0b3e7597a2341c9919101abaaebf32065b98758c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Dec 2025 10:31:30 +0000 Subject: [PATCH 2/8] Initial plan for fixing force-push pull conflicts Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> --- src/@types/vscode.proposed.chatParticipantAdditions.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/@types/vscode.proposed.chatParticipantAdditions.d.ts b/src/@types/vscode.proposed.chatParticipantAdditions.d.ts index 71520fa1ec..aa7001a3d2 100644 --- a/src/@types/vscode.proposed.chatParticipantAdditions.d.ts +++ b/src/@types/vscode.proposed.chatParticipantAdditions.d.ts @@ -105,6 +105,7 @@ declare module 'vscode' { isComplete?: boolean; toolSpecificData?: ChatTerminalToolInvocationData; fromSubAgent?: boolean; + presentation?: 'hidden' | 'hiddenAfterComplete' | undefined; constructor(toolName: string, toolCallId: string, isError?: boolean); } From a3320a89c7ba8e89a09cb925b0958e8c8bcaa87b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Dec 2025 10:38:46 +0000 Subject: [PATCH 3/8] Detect and handle force-pushed branches in pull notification Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> --- src/github/folderRepositoryManager.ts | 55 +++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/src/github/folderRepositoryManager.ts b/src/github/folderRepositoryManager.ts index 9dd9002c84..ad9d37bba5 100644 --- a/src/github/folderRepositoryManager.ts +++ b/src/github/folderRepositoryManager.ts @@ -2492,6 +2492,61 @@ export class FolderRepositoryManager extends Disposable { private async pullBranch(branch: Branch) { if (this._repository.state.HEAD?.name === branch.name) { + // Check if the branch has diverged (ahead > 0 && behind > 0) + // This typically happens when the remote has been force-pushed or rebased + if (branch.ahead !== undefined && branch.ahead > 0 && branch.behind !== undefined && branch.behind > 0) { + const resetToRemote = vscode.l10n.t('Reset to Remote'); + const cancel = vscode.l10n.t('Cancel'); + const result = await vscode.window.showWarningMessage( + vscode.l10n.t('The pull request branch has diverged from the remote (you have {0} local commit(s), remote has {1} new commit(s)).\n\nThis usually happens when the remote branch has been force-pushed or rebased. You can reset your local branch to match the remote (this will discard your {0} local commit(s)), or cancel and resolve manually.', branch.ahead, branch.behind), + { modal: true }, + resetToRemote, + cancel + ); + + if (result === resetToRemote) { + try { + if (branch.upstream) { + // Fetch to ensure we have the latest remote state + await this._repository.fetch(branch.upstream.remote, branch.name); + + // Get the remote branch reference + const remoteBranchRef = `refs/remotes/${branch.upstream.remote}/${branch.upstream.name}`; + const remoteBranch = await this._repository.getBranch(remoteBranchRef); + const currentBranchName = branch.name!; + + // Create a temp branch at the remote commit + const tempBranchName = `temp-pr-update-${Date.now()}`; + await this._repository.createBranch(tempBranchName, false, remoteBranch.commit); + await this._repository.setBranchUpstream(tempBranchName, remoteBranchRef); + + // Checkout the temp branch + await this._repository.checkout(tempBranchName); + + // Delete the old branch (force delete since it has un-merged commits) + await this._repository.deleteBranch(currentBranchName, true); + + // Recreate the original branch at the same commit + await this._repository.createBranch(currentBranchName, false, remoteBranch.commit); + await this._repository.setBranchUpstream(currentBranchName, remoteBranchRef); + + // Checkout the recreated branch + await this._repository.checkout(currentBranchName); + + // Delete the temp branch + await this._repository.deleteBranch(tempBranchName, true); + + Logger.appendLine(`Successfully reset branch ${currentBranchName} to remote ${remoteBranchRef}`, this.id); + } + } catch (e) { + Logger.error(`Error resetting branch to remote: ${e}`, this.id); + vscode.window.showErrorMessage(vscode.l10n.t('Failed to reset branch to remote: {0}', e.message || e)); + } + } + // If cancel, do nothing + return; + } + await this._repository.pull(); } } From 415d0bcb3459a9944413d0e6c2e3fc8bbfce3aef Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Dec 2025 10:43:40 +0000 Subject: [PATCH 4/8] Address code review feedback: improve error handling and validation Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> --- src/github/folderRepositoryManager.ts | 71 +++++++++++++++++---------- 1 file changed, 46 insertions(+), 25 deletions(-) diff --git a/src/github/folderRepositoryManager.ts b/src/github/folderRepositoryManager.ts index ad9d37bba5..0014c6550f 100644 --- a/src/github/folderRepositoryManager.ts +++ b/src/github/folderRepositoryManager.ts @@ -2505,42 +2505,63 @@ export class FolderRepositoryManager extends Disposable { ); if (result === resetToRemote) { + if (!branch.upstream || !branch.name) { + vscode.window.showErrorMessage(vscode.l10n.t('Cannot reset branch: missing upstream or branch name')); + return; + } + + let tempBranchName: string | undefined; + let originalBranchDeleted = false; try { - if (branch.upstream) { - // Fetch to ensure we have the latest remote state - await this._repository.fetch(branch.upstream.remote, branch.name); + // Fetch to ensure we have the latest remote state + await this._repository.fetch(branch.upstream.remote, branch.name); - // Get the remote branch reference - const remoteBranchRef = `refs/remotes/${branch.upstream.remote}/${branch.upstream.name}`; - const remoteBranch = await this._repository.getBranch(remoteBranchRef); - const currentBranchName = branch.name!; + // Get the remote branch reference + const remoteBranchRef = `refs/remotes/${branch.upstream.remote}/${branch.upstream.name}`; + const remoteBranch = await this._repository.getBranch(remoteBranchRef); + const currentBranchName = branch.name; - // Create a temp branch at the remote commit - const tempBranchName = `temp-pr-update-${Date.now()}`; - await this._repository.createBranch(tempBranchName, false, remoteBranch.commit); - await this._repository.setBranchUpstream(tempBranchName, remoteBranchRef); + // Create a temp branch at the remote commit with better uniqueness + tempBranchName = `temp-pr-update-${Date.now()}-${Math.random().toString(36).substring(7)}`; + await this._repository.createBranch(tempBranchName, false, remoteBranch.commit); + await this._repository.setBranchUpstream(tempBranchName, remoteBranchRef); - // Checkout the temp branch - await this._repository.checkout(tempBranchName); + // Checkout the temp branch + await this._repository.checkout(tempBranchName); - // Delete the old branch (force delete since it has un-merged commits) - await this._repository.deleteBranch(currentBranchName, true); + // Delete the old branch (force delete since it has un-merged commits) + await this._repository.deleteBranch(currentBranchName, true); + originalBranchDeleted = true; - // Recreate the original branch at the same commit - await this._repository.createBranch(currentBranchName, false, remoteBranch.commit); - await this._repository.setBranchUpstream(currentBranchName, remoteBranchRef); + // Recreate the original branch at the same commit + await this._repository.createBranch(currentBranchName, false, remoteBranch.commit); + await this._repository.setBranchUpstream(currentBranchName, remoteBranchRef); - // Checkout the recreated branch - await this._repository.checkout(currentBranchName); + // Checkout the recreated branch + await this._repository.checkout(currentBranchName); - // Delete the temp branch - await this._repository.deleteBranch(tempBranchName, true); + // Delete the temp branch + await this._repository.deleteBranch(tempBranchName, true); + tempBranchName = undefined; - Logger.appendLine(`Successfully reset branch ${currentBranchName} to remote ${remoteBranchRef}`, this.id); - } + Logger.appendLine(`Successfully reset branch ${currentBranchName} to remote ${remoteBranchRef}`, this.id); } catch (e) { Logger.error(`Error resetting branch to remote: ${e}`, this.id); - vscode.window.showErrorMessage(vscode.l10n.t('Failed to reset branch to remote: {0}', e.message || e)); + + // Attempt cleanup of any created resources + if (tempBranchName) { + try { + // If we're still on the temp branch, try to get back to a safe state + if (!originalBranchDeleted) { + await this._repository.checkout(branch.name); + } + await this._repository.deleteBranch(tempBranchName, true); + } catch (cleanupError) { + Logger.error(`Error during cleanup: ${cleanupError}`, this.id); + } + } + + vscode.window.showErrorMessage(vscode.l10n.t('Failed to reset branch to remote: {0}', e?.message || String(e))); } } // If cancel, do nothing From b441dd2cd11bcf0b1d46521c754a314ba76e50f0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Dec 2025 10:46:39 +0000 Subject: [PATCH 5/8] Fix localization message and use formatError utility Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> --- src/github/folderRepositoryManager.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/github/folderRepositoryManager.ts b/src/github/folderRepositoryManager.ts index 0014c6550f..48a01b9329 100644 --- a/src/github/folderRepositoryManager.ts +++ b/src/github/folderRepositoryManager.ts @@ -2498,7 +2498,7 @@ export class FolderRepositoryManager extends Disposable { const resetToRemote = vscode.l10n.t('Reset to Remote'); const cancel = vscode.l10n.t('Cancel'); const result = await vscode.window.showWarningMessage( - vscode.l10n.t('The pull request branch has diverged from the remote (you have {0} local commit(s), remote has {1} new commit(s)).\n\nThis usually happens when the remote branch has been force-pushed or rebased. You can reset your local branch to match the remote (this will discard your {0} local commit(s)), or cancel and resolve manually.', branch.ahead, branch.behind), + vscode.l10n.t('The pull request branch has diverged from the remote (you have {0} local commit(s), remote has {1} new commit(s)).\n\nThis usually happens when the remote branch has been force-pushed or rebased. You can reset your local branch to match the remote (this will discard your local changes), or cancel and resolve manually.', branch.ahead, branch.behind), { modal: true }, resetToRemote, cancel @@ -2561,7 +2561,7 @@ export class FolderRepositoryManager extends Disposable { } } - vscode.window.showErrorMessage(vscode.l10n.t('Failed to reset branch to remote: {0}', e?.message || String(e))); + vscode.window.showErrorMessage(vscode.l10n.t('Failed to reset branch to remote: {0}', formatError(e))); } } // If cancel, do nothing From 9911a0a78393add2ed20702adee1cc3f181b4827 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Dec 2025 10:49:40 +0000 Subject: [PATCH 6/8] Improve branch name uniqueness and fix cleanup logic Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> --- src/github/folderRepositoryManager.ts | 43 ++++++++++++++++++++++----- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/src/github/folderRepositoryManager.ts b/src/github/folderRepositoryManager.ts index 48a01b9329..618b5879f6 100644 --- a/src/github/folderRepositoryManager.ts +++ b/src/github/folderRepositoryManager.ts @@ -2513,16 +2513,33 @@ export class FolderRepositoryManager extends Disposable { let tempBranchName: string | undefined; let originalBranchDeleted = false; try { - // Fetch to ensure we have the latest remote state - await this._repository.fetch(branch.upstream.remote, branch.name); + // Fetch to ensure we have the latest remote state (using upstream name for correct refspec) + await this._repository.fetch(branch.upstream.remote, branch.upstream.name); // Get the remote branch reference const remoteBranchRef = `refs/remotes/${branch.upstream.remote}/${branch.upstream.name}`; const remoteBranch = await this._repository.getBranch(remoteBranchRef); const currentBranchName = branch.name; - // Create a temp branch at the remote commit with better uniqueness - tempBranchName = `temp-pr-update-${Date.now()}-${Math.random().toString(36).substring(7)}`; + // Create a temp branch at the remote commit with uniqueness guarantee + let tempCounter = 0; + do { + tempBranchName = `temp-pr-update-${Date.now()}-${Math.random().toString(36).substring(7)}${tempCounter > 0 ? `-${tempCounter}` : ''}`; + tempCounter++; + try { + await this._repository.getBranch(tempBranchName); + // Branch exists, try again with different name + tempBranchName = undefined; + } catch { + // Branch doesn't exist, we can use this name + break; + } + } while (tempCounter < 10); // Safety limit + + if (!tempBranchName) { + throw new Error('Could not generate unique temporary branch name'); + } + await this._repository.createBranch(tempBranchName, false, remoteBranch.commit); await this._repository.setBranchUpstream(tempBranchName, remoteBranchRef); @@ -2551,11 +2568,23 @@ export class FolderRepositoryManager extends Disposable { // Attempt cleanup of any created resources if (tempBranchName) { try { - // If we're still on the temp branch, try to get back to a safe state - if (!originalBranchDeleted) { + // Check current HEAD to see where we are + const currentHead = this._repository.state.HEAD; + + // If original branch wasn't deleted yet, we can safely checkout and cleanup + if (!originalBranchDeleted && currentHead?.name !== branch.name) { await this._repository.checkout(branch.name); } - await this._repository.deleteBranch(tempBranchName, true); + // If original was deleted and we're on temp branch, we need to notify user + // The temp branch is now the only reference to their work + if (originalBranchDeleted) { + vscode.window.showWarningMessage( + vscode.l10n.t('Branch reset partially completed. You are on temporary branch "{0}". Your original branch has been deleted but not recreated. Please manually resolve this state.', tempBranchName) + ); + } else { + // Clean up temp branch if we successfully returned to original + await this._repository.deleteBranch(tempBranchName, true); + } } catch (cleanupError) { Logger.error(`Error during cleanup: ${cleanupError}`, this.id); } From 97ccc62d60ffcb0c1b36f555c02a9b1f7de5675b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 11 Dec 2025 15:34:03 +0000 Subject: [PATCH 7/8] Use merge-base analysis to detect force-push vs normal divergence Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> --- src/github/folderRepositoryManager.ts | 31 +++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/src/github/folderRepositoryManager.ts b/src/github/folderRepositoryManager.ts index 618b5879f6..084ab4c0b4 100644 --- a/src/github/folderRepositoryManager.ts +++ b/src/github/folderRepositoryManager.ts @@ -2493,12 +2493,39 @@ export class FolderRepositoryManager extends Disposable { private async pullBranch(branch: Branch) { if (this._repository.state.HEAD?.name === branch.name) { // Check if the branch has diverged (ahead > 0 && behind > 0) - // This typically happens when the remote has been force-pushed or rebased if (branch.ahead !== undefined && branch.ahead > 0 && branch.behind !== undefined && branch.behind > 0) { + // Use merge-base analysis to distinguish between force-push and normal divergence + // If remote was force-pushed, the merge-base will be at or near the current HEAD + // If it's normal divergence, the merge-base will be older than both HEAD and remote + let isForcePush = false; + + if (branch.upstream && branch.commit) { + try { + const remoteBranchRef = `${branch.upstream.remote}/${branch.upstream.name}`; + const mergeBase = await this._repository.getMergeBase(branch.commit, remoteBranchRef); + + // If merge-base equals the current HEAD commit, it means the remote history + // was rewritten (force-push/rebase), and local commits don't exist in remote + if (mergeBase === branch.commit) { + isForcePush = true; + } + } catch (e) { + // If we can't determine merge-base, fall back to treating it as normal divergence + Logger.debug(`Could not determine merge-base: ${e}`, this.id); + } + } + + // Only show the reset dialog for force-push scenarios + if (!isForcePush) { + // Normal divergence - let the default pull behavior handle it + await this._repository.pull(); + return; + } + const resetToRemote = vscode.l10n.t('Reset to Remote'); const cancel = vscode.l10n.t('Cancel'); const result = await vscode.window.showWarningMessage( - vscode.l10n.t('The pull request branch has diverged from the remote (you have {0} local commit(s), remote has {1} new commit(s)).\n\nThis usually happens when the remote branch has been force-pushed or rebased. You can reset your local branch to match the remote (this will discard your local changes), or cancel and resolve manually.', branch.ahead, branch.behind), + vscode.l10n.t('The remote branch has been force-pushed or rebased. Your local branch has {0} commit(s) that no longer exist in the remote history.\n\nYou can reset your local branch to match the remote (this will discard your local commits), or cancel and resolve manually.', branch.ahead), { modal: true }, resetToRemote, cancel From 6bba64e31700a736c898cfa36f06abdd8dbccfb6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 11 Dec 2025 15:36:53 +0000 Subject: [PATCH 8/8] Address code review: fix null check, comment accuracy, and remove unnecessary upstream setting Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> --- src/github/folderRepositoryManager.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/github/folderRepositoryManager.ts b/src/github/folderRepositoryManager.ts index 084ab4c0b4..583939a9cd 100644 --- a/src/github/folderRepositoryManager.ts +++ b/src/github/folderRepositoryManager.ts @@ -2495,7 +2495,7 @@ export class FolderRepositoryManager extends Disposable { // Check if the branch has diverged (ahead > 0 && behind > 0) if (branch.ahead !== undefined && branch.ahead > 0 && branch.behind !== undefined && branch.behind > 0) { // Use merge-base analysis to distinguish between force-push and normal divergence - // If remote was force-pushed, the merge-base will be at or near the current HEAD + // If remote was force-pushed, the merge-base will equal the current HEAD // If it's normal divergence, the merge-base will be older than both HEAD and remote let isForcePush = false; @@ -2506,7 +2506,7 @@ export class FolderRepositoryManager extends Disposable { // If merge-base equals the current HEAD commit, it means the remote history // was rewritten (force-push/rebase), and local commits don't exist in remote - if (mergeBase === branch.commit) { + if (mergeBase && mergeBase === branch.commit) { isForcePush = true; } } catch (e) { @@ -2549,6 +2549,7 @@ export class FolderRepositoryManager extends Disposable { const currentBranchName = branch.name; // Create a temp branch at the remote commit with uniqueness guarantee + const MAX_BRANCH_NAME_ATTEMPTS = 10; let tempCounter = 0; do { tempBranchName = `temp-pr-update-${Date.now()}-${Math.random().toString(36).substring(7)}${tempCounter > 0 ? `-${tempCounter}` : ''}`; @@ -2561,14 +2562,13 @@ export class FolderRepositoryManager extends Disposable { // Branch doesn't exist, we can use this name break; } - } while (tempCounter < 10); // Safety limit + } while (tempCounter < MAX_BRANCH_NAME_ATTEMPTS); if (!tempBranchName) { throw new Error('Could not generate unique temporary branch name'); } await this._repository.createBranch(tempBranchName, false, remoteBranch.commit); - await this._repository.setBranchUpstream(tempBranchName, remoteBranchRef); // Checkout the temp branch await this._repository.checkout(tempBranchName);