Skip to content

Commit 74f573e

Browse files
authored
Merge pull request #1846 from entireio/feat/import-author-signature
fix(import): stamp importer git identity on checkpoint commits
2 parents 6264846 + 4df67dc commit 74f573e

2 files changed

Lines changed: 126 additions & 2 deletions

File tree

cmd/entire/cli/agentimport/agentimport.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,12 @@ func Run(ctx context.Context, repo *git.Repository, imp Importer, opts Options)
156156
// whole import run pays for at most one history walk, not one per turn.
157157
anchorResolver := newTurnAnchorResolver(repo, opts.LinkCommitSHA, opts.Now)
158158

159+
// Resolve the importer's git identity once per run (not per turn):
160+
// checkpoint commits otherwise carry an empty author, which the
161+
// GitHub->mirror ingestion path falls back to when it has no pusher
162+
// identity for imported sessions, leaving them unattributed.
163+
authorName, authorEmail := cp.GetGitAuthorFromRepo(repo)
164+
159165
for _, sf := range files {
160166
res.SessionsScanned++
161167
full, readErr := os.ReadFile(sf.Path)
@@ -195,7 +201,7 @@ func Run(ctx context.Context, repo *git.Repository, imp Importer, opts Options)
195201
logging.Debug(ctx, "import: turn anchor fell back",
196202
"sessionID", sf.SessionID, "turnUUID", turn.UUID, "candidates", len(turn.CommitSHAs))
197203
}
198-
if err := writeTurn(ctx, stores, imp, cid, sf, red, turn, anchor); err != nil {
204+
if err := writeTurn(ctx, stores, imp, cid, sf, red, turn, anchor, authorName, authorEmail); err != nil {
199205
return res, err
200206
}
201207
existing[cid.String()] = true
@@ -286,7 +292,7 @@ func writeSessionState(ctx context.Context, imp Importer, sf SessionFile, turns
286292
return nil
287293
}
288294

289-
func writeTurn(ctx context.Context, stores *cp.Stores, imp Importer, cid id.CheckpointID, sf SessionFile, red redact.RedactedBytes, turn Turn, anchorCommitSHA string) error {
295+
func writeTurn(ctx context.Context, stores *cp.Stores, imp Importer, cid id.CheckpointID, sf SessionFile, red redact.RedactedBytes, turn Turn, anchorCommitSHA, authorName, authorEmail string) error {
290296
if err := stores.Persistent.Write(ctx, cp.Session(cp.WriteOptions{
291297
CheckpointID: cid,
292298
SessionID: sf.SessionID,
@@ -301,6 +307,8 @@ func writeTurn(ctx context.Context, stores *cp.Stores, imp Importer, cid id.Chec
301307
CheckpointTranscriptStart: turn.LineStart,
302308
TokenUsage: turn.Tokens,
303309
CommitSHA: anchorCommitSHA,
310+
AuthorName: authorName,
311+
AuthorEmail: authorEmail,
304312
})); err != nil {
305313
return fmt.Errorf("write imported checkpoint %s: %w", cid, err)
306314
}

cmd/entire/cli/agentimport/agentimport_test.go

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,122 @@ func TestRun_CursorImporterEndToEnd(t *testing.T) {
384384
}
385385
}
386386

387+
// TestRun_StampsImporterGitAuthorOnCheckpointCommit proves an imported
388+
// checkpoint's underlying git commit on entire/checkpoints/v1 carries the
389+
// importer's configured git identity (resolved once per Run via
390+
// checkpoint.GetGitAuthorFromRepo), not an empty signature.
391+
//
392+
// Motivation: on the GitHub->mirror ingestion path, the data plane has no
393+
// pusher identity for imported sessions and falls back to the checkpoint
394+
// commit's git author. An empty author meant imported sessions couldn't be
395+
// attributed to the importer.
396+
func TestRun_StampsImporterGitAuthorOnCheckpointCommit(t *testing.T) {
397+
t.Parallel()
398+
repo, repoDir := initRepoWithCommit(t)
399+
claudeDir := t.TempDir()
400+
writeFixtureSession(t, claudeDir, "sess-author.jsonl")
401+
402+
res, err := Run(context.Background(), repo, claudeImporter{}, Options{
403+
RepoRoot: repoDir, OverridePath: claudeDir,
404+
Now: time.Date(2026, 6, 25, 0, 0, 0, 0, time.UTC),
405+
})
406+
if err != nil {
407+
t.Fatal(err)
408+
}
409+
if res.TurnsImported != 2 {
410+
t.Fatalf("want 2 imported, got %+v", res)
411+
}
412+
413+
stores, err := cp.Open(context.Background(), repo, cp.OpenOptions{})
414+
if err != nil {
415+
t.Fatal(err)
416+
}
417+
ar, ok := stores.Persistent.(cp.AuthorReader)
418+
if !ok {
419+
t.Fatalf("persistent store %T does not implement AuthorReader", stores.Persistent)
420+
}
421+
cid := DeriveCheckpointID("sess-author", "u1")
422+
author, err := ar.GetCheckpointAuthor(context.Background(), cid)
423+
if err != nil {
424+
t.Fatal(err)
425+
}
426+
// initRepoWithCommit uses testutil.InitRepo, which configures this
427+
// repo-local git identity.
428+
const wantName, wantEmail = "Test User", "test@example.com"
429+
if author.Name != wantName || author.Email != wantEmail {
430+
t.Fatalf("checkpoint commit author = %+v, want Name=%q Email=%q (the repo's configured git identity)",
431+
author, wantName, wantEmail)
432+
}
433+
}
434+
435+
// TestRun_UnconfiguredGitIdentityFallsBackToDefaults proves that when the
436+
// importer's repo has no configured git user (no local or global user.name /
437+
// user.email), the imported checkpoint commit still gets a signature — the
438+
// same "Unknown"/"unknown@local" default checkpoint.GetGitAuthorFromRepo
439+
// already applies elsewhere, rather than an empty one.
440+
func TestRun_UnconfiguredGitIdentityFallsBackToDefaults(t *testing.T) {
441+
// Cannot use t.Parallel(): isolates HOME via t.Setenv so this repo's
442+
// global git config resolution can't see the developer's real ~/.gitconfig.
443+
home := t.TempDir()
444+
t.Setenv("HOME", home)
445+
t.Setenv("XDG_CONFIG_HOME", "")
446+
447+
repoDir := t.TempDir()
448+
repo, err := git.PlainInit(repoDir, false)
449+
if err != nil {
450+
t.Fatal(err)
451+
}
452+
// Seed one commit with a real timestamp (the anchor resolver's bounded
453+
// walk stops at commits older than its date cutoff; a zero-value When
454+
// would halt it immediately). The commit's own author signature is
455+
// independent of GetGitAuthorFromRepo's config-based resolution under
456+
// test here.
457+
wt, err := repo.Worktree()
458+
if err != nil {
459+
t.Fatal(err)
460+
}
461+
testutil.WriteFile(t, repoDir, "f.txt", "x")
462+
if _, err := wt.Add("f.txt"); err != nil {
463+
t.Fatal(err)
464+
}
465+
if _, err := wt.Commit("init", &git.CommitOptions{
466+
Author: &object.Signature{Name: "Seed", Email: "seed@test.com", When: time.Now()},
467+
}); err != nil {
468+
t.Fatal(err)
469+
}
470+
471+
claudeDir := t.TempDir()
472+
writeFixtureSession(t, claudeDir, "sess-noauthor.jsonl")
473+
474+
res, err := Run(context.Background(), repo, claudeImporter{}, Options{
475+
RepoRoot: repoDir, OverridePath: claudeDir,
476+
Now: time.Date(2026, 6, 25, 0, 0, 0, 0, time.UTC),
477+
})
478+
if err != nil {
479+
t.Fatal(err)
480+
}
481+
if res.TurnsImported != 2 {
482+
t.Fatalf("want 2 imported, got %+v", res)
483+
}
484+
485+
stores, err := cp.Open(context.Background(), repo, cp.OpenOptions{})
486+
if err != nil {
487+
t.Fatal(err)
488+
}
489+
ar, ok := stores.Persistent.(cp.AuthorReader)
490+
if !ok {
491+
t.Fatalf("persistent store %T does not implement AuthorReader", stores.Persistent)
492+
}
493+
cid := DeriveCheckpointID("sess-noauthor", "u1")
494+
author, err := ar.GetCheckpointAuthor(context.Background(), cid)
495+
if err != nil {
496+
t.Fatal(err)
497+
}
498+
if author.Name != "Unknown" || author.Email != "unknown@local" {
499+
t.Fatalf("checkpoint commit author = %+v, want the GetGitAuthorFromRepo defaults", author)
500+
}
501+
}
502+
387503
func TestRun_DryRunWritesNothing(t *testing.T) {
388504
t.Parallel()
389505
repo, repoDir := initRepoWithCommit(t)

0 commit comments

Comments
 (0)