Skip to content

Commit d2df272

Browse files
committed
feat(import): anchor imported checkpoints to default branch head
Adds resolveImportLinkCommitSHA, which resolves the commit imported checkpoints stamp as commit_sha: origin's tip of the default branch (the commit the server already knows) preferred over the local branch tip, then HEAD, then empty when nothing resolves. Wires it into `entire import <agent>` via agentimport.Options.LinkCommitSHA. Entire-Checkpoint: 01KY34D8R1WQXZCK1TV9T2RXHX
1 parent 59be645 commit d2df272

4 files changed

Lines changed: 166 additions & 0 deletions

File tree

cmd/entire/cli/import_cmd.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ fails even with --dry-run.`, imp.AgentType()),
6969
res, err := agentimport.Run(ctx, repo, imp, agentimport.Options{
7070
RepoRoot: repoRoot, OverridePath: pathFlag, SessionFilter: sessions,
7171
Now: time.Now(), DryRun: dryRun,
72+
LinkCommitSHA: resolveImportLinkCommitSHA(repo),
7273
})
7374
if err != nil {
7475
return fmt.Errorf("import %s: %w", imp.Name(), err)

cmd/entire/cli/import_link.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package cli
2+
3+
import (
4+
"github.com/go-git/go-git/v6"
5+
"github.com/go-git/go-git/v6/plumbing"
6+
7+
"github.com/entireio/cli/cmd/entire/cli/strategy"
8+
)
9+
10+
// resolveImportLinkCommitSHA returns the commit SHA imported checkpoints are
11+
// anchored to: the default branch's head at import time. Preference order:
12+
// origin's tip of the default branch (the commit the server already knows),
13+
// then the local branch tip, then HEAD. Best-effort — returns "" when nothing
14+
// resolves (e.g. an empty repo); import proceeds without a link.
15+
func resolveImportLinkCommitSHA(repo *git.Repository) string {
16+
if name := strategy.GetDefaultBranchName(repo); name != "" {
17+
if ref, err := repo.Reference(plumbing.NewRemoteReferenceName("origin", name), true); err == nil {
18+
return ref.Hash().String()
19+
}
20+
if ref, err := repo.Reference(plumbing.NewBranchReferenceName(name), true); err == nil {
21+
return ref.Hash().String()
22+
}
23+
}
24+
if head, err := repo.Head(); err == nil {
25+
return head.Hash().String()
26+
}
27+
return ""
28+
}

cmd/entire/cli/import_link_test.go

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package cli
2+
3+
import (
4+
"testing"
5+
6+
"github.com/entireio/cli/cmd/entire/cli/testutil"
7+
"github.com/go-git/go-git/v6"
8+
"github.com/go-git/go-git/v6/plumbing"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
// TestResolveImportLinkCommitSHA_LocalDefaultBranchNoOrigin proves that when
13+
// there is no origin remote, the resolver resolves via the local default
14+
// branch arm (testutil.InitRepo checks out master, so GetDefaultBranchName
15+
// returns "master" and the local-branch lookup succeeds). The true HEAD
16+
// fallback is covered by TestResolveImportLinkCommitSHA_HEADWhenNoDefaultBranch.
17+
func TestResolveImportLinkCommitSHA_LocalDefaultBranchNoOrigin(t *testing.T) {
18+
t.Parallel()
19+
20+
repoDir := t.TempDir()
21+
testutil.InitRepo(t, repoDir)
22+
testutil.WriteFile(t, repoDir, "f.txt", "init")
23+
testutil.GitAdd(t, repoDir, "f.txt")
24+
testutil.GitCommit(t, repoDir, "init")
25+
26+
repo, err := git.PlainOpen(repoDir)
27+
require.NoError(t, err)
28+
t.Cleanup(func() { _ = repo.Close() })
29+
30+
head, err := repo.Head()
31+
require.NoError(t, err)
32+
33+
got := resolveImportLinkCommitSHA(repo)
34+
require.Equal(t, head.Hash().String(), got)
35+
}
36+
37+
// TestResolveImportLinkCommitSHA_PrefersOriginDefaultBranch proves that when
38+
// origin's default branch tip differs from the local branch tip, the
39+
// resolver prefers origin's tip — that's the commit the server already
40+
// knows about.
41+
func TestResolveImportLinkCommitSHA_PrefersOriginDefaultBranch(t *testing.T) {
42+
t.Parallel()
43+
44+
repoDir := t.TempDir()
45+
testutil.InitRepo(t, repoDir)
46+
testutil.WriteFile(t, repoDir, "f.txt", "one")
47+
testutil.GitAdd(t, repoDir, "f.txt")
48+
testutil.GitCommit(t, repoDir, "first")
49+
firstSHA := testutil.GetHeadHash(t, repoDir)
50+
51+
testutil.WriteFile(t, repoDir, "f.txt", "two")
52+
testutil.GitAdd(t, repoDir, "f.txt")
53+
testutil.GitCommit(t, repoDir, "second")
54+
secondSHA := testutil.GetHeadHash(t, repoDir)
55+
56+
repo, err := git.PlainOpen(repoDir)
57+
require.NoError(t, err)
58+
t.Cleanup(func() { _ = repo.Close() })
59+
60+
// Manually create refs/remotes/origin/main -> first commit, and
61+
// refs/remotes/origin/HEAD as a symbolic ref pointing at it.
62+
firstHash := plumbing.NewHash(firstSHA)
63+
originMainRef := plumbing.NewHashReference(plumbing.NewRemoteReferenceName("origin", "main"), firstHash)
64+
require.NoError(t, repo.Storer.SetReference(originMainRef))
65+
originHeadRef := plumbing.NewSymbolicReference(
66+
plumbing.NewRemoteReferenceName("origin", "HEAD"),
67+
plumbing.NewRemoteReferenceName("origin", "main"),
68+
)
69+
require.NoError(t, repo.Storer.SetReference(originHeadRef))
70+
71+
// Also create a local main -> second commit, so origin/main and local
72+
// main genuinely diverge. testutil.InitRepo defaults to `master`, so
73+
// without this the resolver's local-branch arm is never exercised and
74+
// the assertion below can't pin the origin-over-local preference order.
75+
secondHash := plumbing.NewHash(secondSHA)
76+
localMainRef := plumbing.NewHashReference(plumbing.NewBranchReferenceName("main"), secondHash)
77+
require.NoError(t, repo.Storer.SetReference(localMainRef))
78+
79+
got := resolveImportLinkCommitSHA(repo)
80+
require.Equal(t, firstSHA, got)
81+
}
82+
83+
// TestResolveImportLinkCommitSHA_HEADWhenNoDefaultBranch proves the HEAD
84+
// fallback: when the default branch name cannot be resolved at all (no
85+
// remotes, and the checked-out branch is neither main nor master), the
86+
// resolver still returns HEAD's commit instead of "".
87+
func TestResolveImportLinkCommitSHA_HEADWhenNoDefaultBranch(t *testing.T) {
88+
t.Parallel()
89+
90+
repoDir := t.TempDir()
91+
testutil.InitRepo(t, repoDir)
92+
testutil.WriteFile(t, repoDir, "f.txt", "init")
93+
testutil.GitAdd(t, repoDir, "f.txt")
94+
testutil.GitCommit(t, repoDir, "init")
95+
sha := testutil.GetHeadHash(t, repoDir)
96+
97+
repo, err := git.PlainOpen(repoDir)
98+
require.NoError(t, err)
99+
t.Cleanup(func() { _ = repo.Close() })
100+
101+
// Rename the branch away from main/master so GetDefaultBranchName
102+
// returns "" (no origin, and no local main/master to fall back to).
103+
hash := plumbing.NewHash(sha)
104+
trunkRef := plumbing.NewHashReference(plumbing.NewBranchReferenceName("trunk"), hash)
105+
require.NoError(t, repo.Storer.SetReference(trunkRef))
106+
require.NoError(t, repo.Storer.SetReference(
107+
plumbing.NewSymbolicReference(plumbing.HEAD, plumbing.NewBranchReferenceName("trunk")),
108+
))
109+
require.NoError(t, repo.Storer.RemoveReference(plumbing.NewBranchReferenceName("master")))
110+
111+
got := resolveImportLinkCommitSHA(repo)
112+
require.Equal(t, sha, got)
113+
}
114+
115+
// TestResolveImportLinkCommitSHA_EmptyRepo proves the resolver returns "" and
116+
// does not panic on a repo with no commits.
117+
func TestResolveImportLinkCommitSHA_EmptyRepo(t *testing.T) {
118+
t.Parallel()
119+
120+
repoDir := t.TempDir()
121+
// git.PlainInit deliberately (not testutil.InitRepo): the repo must stay
122+
// commit-free, so the helper's user/GPG config is irrelevant here.
123+
repo, err := git.PlainInit(repoDir, false)
124+
require.NoError(t, err)
125+
t.Cleanup(func() { _ = repo.Close() })
126+
127+
got := resolveImportLinkCommitSHA(repo)
128+
require.Empty(t, got)
129+
}

docs/architecture/sessions-and-checkpoints.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,14 @@ When condensing multiple concurrent sessions:
305305
- `sessions` array in `CheckpointSummary` maps each session to its file paths
306306
- `files_touched` is merged from all sessions
307307

308+
Checkpoints written by `entire import <agent>` additionally carry a `commit_sha`
309+
(omitempty) on both the session `Metadata` and the root `CheckpointSummary`,
310+
set to the default branch's head at import time — origin's tip is preferred
311+
(the commit the server already knows about), falling back to the local branch
312+
tip, then HEAD, then empty when nothing resolves. It is a best-effort anchor
313+
for UI display only, not an attribution signal, and pre-existing imported
314+
checkpoints are not backfilled with it.
315+
308316
### Checkpoint Policy
309317

310318
Repo-wide checkpoint policy lives at `refs/entire/policies/checkpoint`. The ref

0 commit comments

Comments
 (0)