From 28b2da7c0ef6f8fe80e32193f42ba18c9a7f7ac5 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Thu, 26 Mar 2026 10:11:24 -0500 Subject: [PATCH] feat: optimize detached HEAD merge base for jj users When HEAD is detached (standard jj workflow), findMergeBase now walks remote tracking refs under refs/remotes/origin/ to find the closest ancestor of HEAD. This produces a minimal patch containing only truly unpushed local changes, instead of falling back to origin/main and including the entire branch diff. The algorithm: enumerate all remote tracking refs, check which are ancestors of HEAD, and pick the one with the fewest commits between it and HEAD. Falls back to default branch merge base if no remote ancestor is found. Closes DEP-3974 Made-with: Cursor --- pkg/cmd/ci/run.go | 62 ++++++++++++++++++++++++++++++++++++++++-- pkg/cmd/ci/run_test.go | 42 ++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/ci/run.go b/pkg/cmd/ci/run.go index b1f5b8c6..316c50fb 100644 --- a/pkg/cmd/ci/run.go +++ b/pkg/cmd/ci/run.go @@ -309,10 +309,8 @@ func findMergeBase(workflowDir string) (baseBranch string, mergeBase string, err branch := strings.TrimSpace(string(branchOut)) if branch != "" && branch != "HEAD" { // not detached remoteBranch := "origin/" + branch - // Verify the remote branch exists _, err := exec.Command("git", "-C", workflowDir, "rev-parse", "--verify", remoteBranch).Output() if err == nil { - // Use merge-base to find common ancestor shaOut, err := exec.Command("git", "-C", workflowDir, "merge-base", "HEAD", remoteBranch).Output() if err == nil { sha := strings.TrimSpace(string(shaOut)) @@ -321,6 +319,11 @@ func findMergeBase(workflowDir string) (baseBranch string, mergeBase string, err } } } + } else if branch == "HEAD" { + // Detached HEAD: find the closest remote tracking ancestor + if ref, sha, err := findClosestRemoteAncestor(workflowDir); err == nil { + return ref, sha, nil + } } } @@ -340,6 +343,61 @@ func findMergeBase(workflowDir string) (baseBranch string, mergeBase string, err return defaultBranch, strings.TrimSpace(string(mergeBaseOut)), nil } +// findClosestRemoteAncestor walks remote tracking refs to find the one +// closest to HEAD (fewest commits between it and HEAD). This produces +// smaller patches for detached-HEAD workflows like jj. +func findClosestRemoteAncestor(workflowDir string) (refName string, sha string, err error) { + refsOut, err := exec.Command("git", "-C", workflowDir, + "for-each-ref", "refs/remotes/origin/", "--format=%(objectname) %(refname:short)").Output() + if err != nil { + return "", "", err + } + + bestRef := "" + bestSHA := "" + bestDist := -1 + + for _, line := range strings.Split(strings.TrimSpace(string(refsOut)), "\n") { + parts := strings.SplitN(line, " ", 2) + if len(parts) != 2 { + continue + } + refSHA, ref := parts[0], parts[1] + + // Skip origin/HEAD (symbolic ref, not a real branch) + if ref == "origin/HEAD" { + continue + } + + // Check if this ref is an ancestor of HEAD + err := exec.Command("git", "-C", workflowDir, "merge-base", "--is-ancestor", refSHA, "HEAD").Run() + if err != nil { + continue + } + + // Count commits between this ref and HEAD + countOut, err := exec.Command("git", "-C", workflowDir, "rev-list", "--count", refSHA+"..HEAD").Output() + if err != nil { + continue + } + + dist := 0 + fmt.Sscanf(strings.TrimSpace(string(countOut)), "%d", &dist) + + if bestDist < 0 || dist < bestDist { + bestDist = dist + bestRef = ref + bestSHA = refSHA + } + } + + if bestRef == "" { + return "", "", fmt.Errorf("no remote ancestor found") + } + + return bestRef, bestSHA, nil +} + func detectPatch(workflowDir string) *patchInfo { baseBranch, mergeBase, err := findMergeBase(workflowDir) if err != nil { diff --git a/pkg/cmd/ci/run_test.go b/pkg/cmd/ci/run_test.go index 13679162..708d290f 100644 --- a/pkg/cmd/ci/run_test.go +++ b/pkg/cmd/ci/run_test.go @@ -169,3 +169,45 @@ func TestFindMergeBase_DetachedHEAD(t *testing.T) { _ = baseBranch } + +func TestFindMergeBase_DetachedHEAD_WithPushedAncestor(t *testing.T) { + bare := initBareRemote(t) + clone := cloneRepo(t, bare) + + // Create a feature branch and push it + run(t, clone, "git", "checkout", "-b", "feature/jj-test") + writeFile(t, filepath.Join(clone, "feature.txt"), "pushed work") + run(t, clone, "git", "add", ".") + run(t, clone, "git", "commit", "-m", "pushed feature commit") + run(t, clone, "git", "push", "-u", "origin", "feature/jj-test") + + pushedSHA := run(t, clone, "git", "rev-parse", "HEAD") + + // Add two local-only commits (simulating jj workflow) + writeFile(t, filepath.Join(clone, "local1.txt"), "local1") + run(t, clone, "git", "add", ".") + run(t, clone, "git", "commit", "-m", "local commit 1") + + writeFile(t, filepath.Join(clone, "local2.txt"), "local2") + run(t, clone, "git", "add", ".") + run(t, clone, "git", "commit", "-m", "local commit 2") + + // Detach HEAD (standard jj workflow) + headSHA := run(t, clone, "git", "rev-parse", "HEAD") + run(t, clone, "git", "checkout", headSHA) + + baseBranch, mergeBase, err := findMergeBase(clone) + if err != nil { + t.Fatalf("findMergeBase failed: %v", err) + } + + // Should find the pushed feature branch as the closest ancestor, + // NOT fall back to origin/main (which would produce a bigger patch) + if mergeBase != pushedSHA { + t.Errorf("expected mergeBase=%s (pushed feature commit), got %s", pushedSHA, mergeBase) + } + + if baseBranch != "origin/feature/jj-test" { + t.Errorf("expected baseBranch=origin/feature/jj-test, got %q", baseBranch) + } +}