fix: compare ahead/behind counts against base branch instead of remote tracking branch (#544)
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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 && (
|
||||
<>
|
||||
|
||||
Reference in New Issue
Block a user