Skip to content

Commit 7a7fd08

Browse files
Harden factory-facing drift and contract semantics
Co-authored-by: Andrew <andrewxhill@gmail.com>
1 parent fb0c64a commit 7a7fd08

4 files changed

Lines changed: 109 additions & 12 deletions

File tree

docs/FLOWS.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
These are the intended operator flows for `mainline` today.
44

5+
If an agent wrapper, factory daemon, or operator UI parses `--json` output,
6+
bind only to the documented `v1` machine contract in
7+
[JSON_CONTRACTS.md](/Users/devrel/Projects/recallnet/mainline/docs/JSON_CONTRACTS.md).
8+
That document is the compatibility policy. The internal Go structs are not the
9+
public contract.
10+
511
## Solo Developer
612

713
Initialize once:

docs/JSON_CONTRACTS.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ Optional top-level keys:
5151
- `has_upstream`
5252
- `is_protected_branch`
5353

54+
`ahead_count` and `behind_count` are exact commit counts relative to the
55+
upstream ref, not boolean-like drift flags.
56+
5457
`latest_submission` and entries in `active_submissions` extend the durable
5558
submission record with optional blocked-state diagnostics:
5659

internal/git/git.go

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -607,27 +607,37 @@ func (e Engine) BranchStatus(branch string, protectedBranch string) (BranchStatu
607607
return status, nil
608608
}
609609

610-
branchBehind, err := branchCommit.IsAncestor(upstreamCommit)
610+
behind, ahead, err := e.symmetricDifferenceCounts(upstreamRefName.String(), branchRef.Name().String())
611611
if err != nil {
612612
return BranchStatus{}, err
613613
}
614-
upstreamBehind, err := upstreamCommit.IsAncestor(branchCommit)
614+
status.AheadCount = ahead
615+
status.BehindCount = behind
616+
617+
return status, nil
618+
}
619+
620+
func (e Engine) symmetricDifferenceCounts(leftRef string, rightRef string) (behind int, ahead int, err error) {
621+
layout, err := DiscoverRepositoryLayout(e.RepositoryRoot)
615622
if err != nil {
616-
return BranchStatus{}, err
623+
return 0, 0, err
617624
}
618625

619-
if branchBehind {
620-
status.BehindCount = 1
626+
cmd := exec.Command("git", "rev-list", "--left-right", "--count", leftRef+"..."+rightRef)
627+
cmd.Dir = layout.WorktreeRoot
628+
output, err := cmd.CombinedOutput()
629+
if err != nil {
630+
return 0, 0, fmt.Errorf("count symmetric difference for %s...%s: %w: %s", leftRef, rightRef, err, strings.TrimSpace(string(output)))
621631
}
622-
if upstreamBehind {
623-
status.AheadCount = 1
632+
633+
text := strings.TrimSpace(string(output))
634+
if _, err := fmt.Sscanf(text, "%d %d", &behind, &ahead); err == nil {
635+
return behind, ahead, nil
624636
}
625-
if !branchBehind && !upstreamBehind {
626-
status.AheadCount = 1
627-
status.BehindCount = 1
637+
if _, err := fmt.Sscanf(text, "%d\t%d", &behind, &ahead); err == nil {
638+
return behind, ahead, nil
628639
}
629-
630-
return status, nil
640+
return 0, 0, fmt.Errorf("parse symmetric difference counts from %q", text)
631641
}
632642

633643
// InspectHealth reports doctor-level repository health.

internal/git/git_test.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package git
2+
3+
import (
4+
"bytes"
5+
"os"
6+
"os/exec"
7+
"path/filepath"
8+
"testing"
9+
)
10+
11+
func TestBranchStatusReportsExactAheadBehindCounts(t *testing.T) {
12+
remoteDir := filepath.Join(t.TempDir(), "origin.git")
13+
runGitCommand(t, t.TempDir(), "git", "init", "--bare", remoteDir)
14+
15+
repoRoot := t.TempDir()
16+
runGitCommand(t, repoRoot, "git", "init", "-b", "main")
17+
runGitCommand(t, repoRoot, "git", "config", "user.name", "Test User")
18+
runGitCommand(t, repoRoot, "git", "config", "user.email", "test@example.com")
19+
runGitCommand(t, repoRoot, "git", "config", "core.hooksPath", ".git/hooks")
20+
21+
writeFile(t, filepath.Join(repoRoot, "README.md"), "# test\n")
22+
runGitCommand(t, repoRoot, "git", "add", "README.md")
23+
runGitCommand(t, repoRoot, "git", "commit", "-m", "initial")
24+
runGitCommand(t, repoRoot, "git", "remote", "add", "origin", remoteDir)
25+
runGitCommand(t, repoRoot, "git", "push", "-u", "origin", "main")
26+
27+
peerClone := filepath.Join(t.TempDir(), "peer")
28+
runGitCommand(t, t.TempDir(), "git", "clone", remoteDir, peerClone)
29+
runGitCommand(t, peerClone, "git", "config", "user.name", "Peer User")
30+
runGitCommand(t, peerClone, "git", "config", "user.email", "peer@example.com")
31+
runGitCommand(t, peerClone, "git", "config", "core.hooksPath", ".git/hooks")
32+
writeFile(t, filepath.Join(peerClone, "peer.txt"), "peer\n")
33+
runGitCommand(t, peerClone, "git", "add", "peer.txt")
34+
runGitCommand(t, peerClone, "git", "commit", "-m", "peer change")
35+
runGitCommand(t, peerClone, "git", "push", "origin", "main")
36+
37+
writeFile(t, filepath.Join(repoRoot, "local-one.txt"), "one\n")
38+
runGitCommand(t, repoRoot, "git", "add", "local-one.txt")
39+
runGitCommand(t, repoRoot, "git", "commit", "-m", "local one")
40+
writeFile(t, filepath.Join(repoRoot, "local-two.txt"), "two\n")
41+
runGitCommand(t, repoRoot, "git", "add", "local-two.txt")
42+
runGitCommand(t, repoRoot, "git", "commit", "-m", "local two")
43+
runGitCommand(t, repoRoot, "git", "fetch", "origin")
44+
45+
status, err := NewEngine(repoRoot).BranchStatus("main", "main")
46+
if err != nil {
47+
t.Fatalf("BranchStatus: %v", err)
48+
}
49+
if status.AheadCount != 2 || status.BehindCount != 1 {
50+
t.Fatalf("expected ahead=2 behind=1, got %+v", status)
51+
}
52+
}
53+
54+
func runGitCommand(t *testing.T, dir string, name string, args ...string) string {
55+
t.Helper()
56+
57+
cmd := exec.Command(name, args...)
58+
cmd.Dir = dir
59+
60+
var stdout bytes.Buffer
61+
var stderr bytes.Buffer
62+
cmd.Stdout = &stdout
63+
cmd.Stderr = &stderr
64+
65+
if err := cmd.Run(); err != nil {
66+
t.Fatalf("%s %v failed: %v: %s", name, args, err, stderr.String())
67+
}
68+
69+
return stdout.String()
70+
}
71+
72+
func writeFile(t *testing.T, path string, contents string) {
73+
t.Helper()
74+
75+
if err := os.WriteFile(path, []byte(contents), 0o644); err != nil {
76+
t.Fatalf("WriteFile(%s): %v", path, err)
77+
}
78+
}

0 commit comments

Comments
 (0)