Skip to content

Commit 4df67dc

Browse files
peyton-altclaude
andcommitted
fix(import): stamp importer git identity on checkpoint commits
entire import wrote checkpoint commits with an empty git author signature. On the GitHub->mirror ingestion path, the data plane has no pusher identity for imported sessions and falls back to the checkpoint commit's git author; an empty author meant imported sessions couldn't be attributed to the importer. Resolve the author once per import run via the existing checkpoint.GetGitAuthorFromRepo (author email is HMAC'd, never stored) and thread it into every written checkpoint's commit, fixing attribution via existing, de-identifying machinery. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QUDkzmpwgC4BiZ894UfnTy
1 parent 2a88eba commit 4df67dc

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)