fix: compare ahead/behind counts against base branch instead of remote tracking branch (#544)

This commit is contained in:
Carlos Florêncio
2026-04-06 09:51:58 +01:00
committed by GitHub
parent e3f83173cf
commit f9ca582036
4 changed files with 85 additions and 23 deletions
@@ -126,24 +126,21 @@ func (wt *WorkspaceTracker) getGitBranchInfo(ctx context.Context, update *types.
return nil
}
// getAheadBehindCounts populates the Ahead/Behind fields relative to the tracking or
// origin branch. Falls back to origin/main then origin/master when no upstream is set.
// getAheadBehindCounts populates the Ahead/Behind fields relative to the base branch
// (origin/main or origin/master). Always compares against the base branch rather than
// the remote tracking branch, because after a rebase the tracking branch has stale
// commit SHAs that produce inflated counts.
func (wt *WorkspaceTracker) getAheadBehindCounts(ctx context.Context, update *types.GitStatusUpdate) {
// Use remote branch if available, otherwise fall back to origin/main or origin/master
// This handles worktree branches that don't have an upstream tracking branch set
compareRef := update.RemoteBranch
if compareRef == "" {
// Try origin/main first, then origin/master
checkCmd := exec.CommandContext(ctx, "git", "rev-parse", "--verify", "origin/main")
// Always compare against the base branch (origin/main or origin/master).
// Using the remote tracking branch (origin/<feature-branch>) gives wrong counts
// after rebase because rebased commits have new SHAs.
var compareRef string
for _, candidate := range []string{"origin/main", "origin/master"} {
checkCmd := exec.CommandContext(ctx, "git", "rev-parse", "--verify", candidate)
checkCmd.Dir = wt.workDir
if err := checkCmd.Run(); err == nil {
compareRef = "origin/main"
} else {
checkCmd2 := exec.CommandContext(ctx, "git", "rev-parse", "--verify", "origin/master")
checkCmd2.Dir = wt.workDir
if err := checkCmd2.Run(); err == nil {
compareRef = "origin/master"
}
compareRef = candidate
break
}
}
if compareRef == "" {
@@ -272,6 +272,69 @@ func TestGetGitStatus_AheadBehindWithoutUpstream(t *testing.T) {
}
}
// TestGetGitStatus_AheadBehindAfterRebase verifies that ahead/behind counts
// reflect divergence from the base branch (origin/main), not the remote tracking
// branch. After a rebase, the remote tracking branch has stale SHAs, so comparing
// against it produces inflated counts.
func TestGetGitStatus_AheadBehindAfterRebase(t *testing.T) {
repoDir, cleanup := setupTestRepo(t)
defer cleanup()
log := newTestLogger(t)
wt := NewWorkspaceTracker(repoDir, log)
ctx := context.Background()
// Create a feature branch with upstream tracking
runGit(t, repoDir, "checkout", "-b", "feature-rebase")
runGit(t, repoDir, "push", "-u", "origin", "feature-rebase")
// Make 2 commits on the feature branch
writeFile(t, repoDir, "feature1.txt", "feature 1")
runGit(t, repoDir, "add", ".")
runGit(t, repoDir, "commit", "-m", "Feature commit 1")
writeFile(t, repoDir, "feature2.txt", "feature 2")
runGit(t, repoDir, "add", ".")
runGit(t, repoDir, "commit", "-m", "Feature commit 2")
// Push feature commits so remote tracking branch has them
runGit(t, repoDir, "push")
// Switch to main, add commits, push
runGit(t, repoDir, "checkout", "main")
for i := 0; i < 5; i++ {
writeFile(t, repoDir, "main"+string(rune('0'+i))+".txt", "main content")
runGit(t, repoDir, "add", ".")
runGit(t, repoDir, "commit", "-m", "Main commit")
}
runGit(t, repoDir, "push")
// Switch back to feature and rebase onto main
runGit(t, repoDir, "checkout", "feature-rebase")
runGit(t, repoDir, "fetch", "origin")
runGit(t, repoDir, "rebase", "origin/main")
// Now: local feature-rebase has 2 feature commits on top of main (rebased)
// But origin/feature-rebase still points to old pre-rebase commits
// git rev-list --left-right --count feature-rebase...origin/feature-rebase
// would show ~7 ahead (2 rebased + 5 main commits), not 2
status, err := wt.getGitStatus(ctx)
if err != nil {
t.Fatalf("failed to get git status: %v", err)
}
// Should be 2 ahead of origin/main (the 2 feature commits)
if status.Ahead != 2 {
t.Errorf("expected ahead=2, got %d", status.Ahead)
}
// Should be 0 behind origin/main (just rebased)
if status.Behind != 0 {
t.Errorf("expected behind=0, got %d", status.Behind)
}
}
// TestFilterLocalCommits_PullAndResetScenario tests the exact scenario where:
// 1. Session starts at commit X
// 2. Upstream (origin/main) gets new commits
@@ -25,10 +25,10 @@ type GitStatusUpdate struct {
// Renamed contains paths of renamed files.
Renamed []string `json:"renamed"`
// Ahead is the number of commits ahead of the remote branch.
// Ahead is the number of commits ahead of the base branch (origin/main).
Ahead int `json:"ahead"`
// Behind is the number of commits behind the remote branch.
// Behind is the number of commits behind the base branch (origin/main).
Behind int `json:"behind"`
// Branch is the current local branch name.
+8 -6
View File
@@ -251,13 +251,15 @@ function TopBarLeft({
/** Ahead/Behind commit status badges */
function GitAheadBehindBadges({
gitStatus,
baseBranch,
}: {
gitStatus: { ahead: number; behind: number; remote_branch?: string | null };
gitStatus: { ahead: number; behind: number };
baseBranch?: string;
}) {
const ahead = gitStatus?.ahead ?? 0;
const behind = gitStatus?.behind ?? 0;
if (ahead === 0 && behind === 0) return null;
const remoteBranch = gitStatus?.remote_branch || "remote";
const compareRef = baseBranch || "main";
return (
<div className="flex items-center gap-1">
{ahead > 0 && (
@@ -268,7 +270,7 @@ function GitAheadBehindBadges({
</span>
</TooltipTrigger>
<TooltipContent>
{ahead} commit{ahead !== 1 ? "s" : ""} ahead of {remoteBranch}
{ahead} commit{ahead !== 1 ? "s" : ""} ahead of {compareRef}
</TooltipContent>
</Tooltip>
)}
@@ -280,7 +282,7 @@ function GitAheadBehindBadges({
</span>
</TooltipTrigger>
<TooltipContent>
{behind} commit{behind !== 1 ? "s" : ""} behind {remoteBranch}
{behind} commit{behind !== 1 ? "s" : ""} behind {compareRef}
</TooltipContent>
</Tooltip>
)}
@@ -302,7 +304,7 @@ function TopBarRight({
}: {
activeSessionId?: string | null;
baseBranch?: string;
gitStatus: { ahead: number; behind: number; remote_branch?: string | null };
gitStatus: { ahead: number; behind: number };
showDebugOverlay?: boolean;
onToggleDebugOverlay?: () => void;
isArchived?: boolean;
@@ -329,7 +331,7 @@ function TopBarRight({
</TooltipContent>
</Tooltip>
)}
<GitAheadBehindBadges gitStatus={gitStatus} />
<GitAheadBehindBadges gitStatus={gitStatus} baseBranch={baseBranch} />
<DocumentControls activeSessionId={activeSessionId ?? null} />
{!isArchived && (
<>