Skip to content
1 change: 1 addition & 0 deletions src/@types/vscode.proposed.chatParticipantAdditions.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ declare module 'vscode' {
isComplete?: boolean;
toolSpecificData?: ChatTerminalToolInvocationData;
fromSubAgent?: boolean;
presentation?: 'hidden' | 'hiddenAfterComplete' | undefined;

constructor(toolName: string, toolCallId: string, isError?: boolean);
}
Expand Down
105 changes: 105 additions & 0 deletions src/github/folderRepositoryManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2492,6 +2492,111 @@ 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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Is this really the way to detect a force-push or rebase? This seems like it would also happen if there are just local and remote commits.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're absolutely right - this condition would trigger for any diverged branch, not just force-pushes. This includes the legitimate case where a user makes local commits while someone else pushes to the remote.

After reconsidering, I think the better approach is to check if we can fast-forward before attempting a pull. If ahead > 0 && behind > 0, we should:

  1. Try to determine if this is a force-push by checking the merge-base
  2. If it's a force-push scenario (merge-base doesn't include recent local commits), offer to reset
  3. Otherwise, let the normal pull/merge flow handle it

However, looking at the original issue more closely - the user reported that git sync (with rebase) worked where git pull (with merge) failed. This suggests another approach: when branches have diverged on a PR branch (where users typically don't make their own commits), we could offer a rebase-based pull instead of a merge-based pull.

Would you prefer:

  • A) More sophisticated force-push detection using merge-base analysis
  • B) Offering rebase as an option when branches diverge
  • C) Removing this detection entirely and relying on the existing checkout flow logic

Let me know your preference and I'll update accordingly.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Option A

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented merge-base analysis in commit 97ccc62.

The logic now:

  1. Calculates merge-base between local HEAD and remote branch
  2. If merge-base equals local HEAD → force-push detected (show reset dialog)
  3. If merge-base is older → normal divergence (proceed with standard pull)

This correctly distinguishes between force-push scenarios (where local commits don't exist in remote history) and normal divergence (where both sides have legitimate new commits).

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),
{ modal: true },
resetToRemote,
cancel
);

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 {
// 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 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);

// 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);
originalBranchDeleted = 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);
tempBranchName = undefined;

Logger.appendLine(`Successfully reset branch ${currentBranchName} to remote ${remoteBranchRef}`, this.id);
} catch (e) {
Logger.error(`Error resetting branch to remote: ${e}`, this.id);

// Attempt cleanup of any created resources
if (tempBranchName) {
try {
// 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);
}
// 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);
}
}

vscode.window.showErrorMessage(vscode.l10n.t('Failed to reset branch to remote: {0}', formatError(e)));
}
}
// If cancel, do nothing
return;
}

await this._repository.pull();
}
}
Expand Down