Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions cmd/entire/cli/agentimport/agentimport.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,12 @@ func Run(ctx context.Context, repo *git.Repository, imp Importer, opts Options)
// whole import run pays for at most one history walk, not one per turn.
anchorResolver := newTurnAnchorResolver(repo, opts.LinkCommitSHA, opts.Now)

// Resolve the importer's git identity once per run (not per turn):
// checkpoint commits otherwise carry an empty author, which the
// GitHub->mirror ingestion path falls back to when it has no pusher
// identity for imported sessions, leaving them unattributed.
authorName, authorEmail := cp.GetGitAuthorFromRepo(repo)

for _, sf := range files {
res.SessionsScanned++
full, readErr := os.ReadFile(sf.Path)
Expand Down Expand Up @@ -195,7 +201,7 @@ func Run(ctx context.Context, repo *git.Repository, imp Importer, opts Options)
logging.Debug(ctx, "import: turn anchor fell back",
"sessionID", sf.SessionID, "turnUUID", turn.UUID, "candidates", len(turn.CommitSHAs))
}
if err := writeTurn(ctx, stores, imp, cid, sf, red, turn, anchor); err != nil {
if err := writeTurn(ctx, stores, imp, cid, sf, red, turn, anchor, authorName, authorEmail); err != nil {
return res, err
}
existing[cid.String()] = true
Expand Down Expand Up @@ -286,7 +292,7 @@ func writeSessionState(ctx context.Context, imp Importer, sf SessionFile, turns
return nil
}

func writeTurn(ctx context.Context, stores *cp.Stores, imp Importer, cid id.CheckpointID, sf SessionFile, red redact.RedactedBytes, turn Turn, anchorCommitSHA string) error {
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 {
if err := stores.Persistent.Write(ctx, cp.Session(cp.WriteOptions{
CheckpointID: cid,
SessionID: sf.SessionID,
Expand All @@ -301,6 +307,8 @@ func writeTurn(ctx context.Context, stores *cp.Stores, imp Importer, cid id.Chec
CheckpointTranscriptStart: turn.LineStart,
TokenUsage: turn.Tokens,
CommitSHA: anchorCommitSHA,
AuthorName: authorName,
AuthorEmail: authorEmail,
})); err != nil {
return fmt.Errorf("write imported checkpoint %s: %w", cid, err)
}
Expand Down
116 changes: 116 additions & 0 deletions cmd/entire/cli/agentimport/agentimport_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,122 @@ func TestRun_CursorImporterEndToEnd(t *testing.T) {
}
}

// TestRun_StampsImporterGitAuthorOnCheckpointCommit proves an imported
// checkpoint's underlying git commit on entire/checkpoints/v1 carries the
// importer's configured git identity (resolved once per Run via
// checkpoint.GetGitAuthorFromRepo), not an empty signature.
//
// Motivation: 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.
func TestRun_StampsImporterGitAuthorOnCheckpointCommit(t *testing.T) {
t.Parallel()
repo, repoDir := initRepoWithCommit(t)
claudeDir := t.TempDir()
writeFixtureSession(t, claudeDir, "sess-author.jsonl")

res, err := Run(context.Background(), repo, claudeImporter{}, Options{
RepoRoot: repoDir, OverridePath: claudeDir,
Now: time.Date(2026, 6, 25, 0, 0, 0, 0, time.UTC),
})
if err != nil {
t.Fatal(err)
}
if res.TurnsImported != 2 {
t.Fatalf("want 2 imported, got %+v", res)
}

stores, err := cp.Open(context.Background(), repo, cp.OpenOptions{})
if err != nil {
t.Fatal(err)
}
ar, ok := stores.Persistent.(cp.AuthorReader)
if !ok {
t.Fatalf("persistent store %T does not implement AuthorReader", stores.Persistent)
}
cid := DeriveCheckpointID("sess-author", "u1")
author, err := ar.GetCheckpointAuthor(context.Background(), cid)
if err != nil {
t.Fatal(err)
}
// initRepoWithCommit uses testutil.InitRepo, which configures this
// repo-local git identity.
const wantName, wantEmail = "Test User", "test@example.com"
if author.Name != wantName || author.Email != wantEmail {
t.Fatalf("checkpoint commit author = %+v, want Name=%q Email=%q (the repo's configured git identity)",
author, wantName, wantEmail)
}
}

// TestRun_UnconfiguredGitIdentityFallsBackToDefaults proves that when the
// importer's repo has no configured git user (no local or global user.name /
// user.email), the imported checkpoint commit still gets a signature — the
// same "Unknown"/"unknown@local" default checkpoint.GetGitAuthorFromRepo
// already applies elsewhere, rather than an empty one.
func TestRun_UnconfiguredGitIdentityFallsBackToDefaults(t *testing.T) {
// Cannot use t.Parallel(): isolates HOME via t.Setenv so this repo's
// global git config resolution can't see the developer's real ~/.gitconfig.
home := t.TempDir()
t.Setenv("HOME", home)
t.Setenv("XDG_CONFIG_HOME", "")

Comment thread
gtrrz-victor marked this conversation as resolved.
repoDir := t.TempDir()
repo, err := git.PlainInit(repoDir, false)
if err != nil {
t.Fatal(err)
}
// Seed one commit with a real timestamp (the anchor resolver's bounded
// walk stops at commits older than its date cutoff; a zero-value When
// would halt it immediately). The commit's own author signature is
// independent of GetGitAuthorFromRepo's config-based resolution under
// test here.
wt, err := repo.Worktree()
if err != nil {
t.Fatal(err)
}
testutil.WriteFile(t, repoDir, "f.txt", "x")
if _, err := wt.Add("f.txt"); err != nil {
t.Fatal(err)
}
if _, err := wt.Commit("init", &git.CommitOptions{
Author: &object.Signature{Name: "Seed", Email: "seed@test.com", When: time.Now()},
}); err != nil {
t.Fatal(err)
}

claudeDir := t.TempDir()
writeFixtureSession(t, claudeDir, "sess-noauthor.jsonl")

res, err := Run(context.Background(), repo, claudeImporter{}, Options{
RepoRoot: repoDir, OverridePath: claudeDir,
Now: time.Date(2026, 6, 25, 0, 0, 0, 0, time.UTC),
})
if err != nil {
t.Fatal(err)
}
if res.TurnsImported != 2 {
t.Fatalf("want 2 imported, got %+v", res)
}

stores, err := cp.Open(context.Background(), repo, cp.OpenOptions{})
if err != nil {
t.Fatal(err)
}
ar, ok := stores.Persistent.(cp.AuthorReader)
if !ok {
t.Fatalf("persistent store %T does not implement AuthorReader", stores.Persistent)
}
cid := DeriveCheckpointID("sess-noauthor", "u1")
author, err := ar.GetCheckpointAuthor(context.Background(), cid)
if err != nil {
t.Fatal(err)
}
if author.Name != "Unknown" || author.Email != "unknown@local" {
t.Fatalf("checkpoint commit author = %+v, want the GetGitAuthorFromRepo defaults", author)
}
}

func TestRun_DryRunWritesNothing(t *testing.T) {
t.Parallel()
repo, repoDir := initRepoWithCommit(t)
Expand Down
Loading