From 65c4d760a4d8279df06c87f8284725098fc42cbd Mon Sep 17 00:00:00 2001 From: Stefan Haubold Date: Mon, 26 Jan 2026 19:01:32 +0100 Subject: [PATCH 1/2] don't offer adding a trailer in a rebase or a cherry-pick or reverts Entire-Checkpoint: 05fc557f7824 --- cmd/entire/cli/strategy/hooks_test.go | 189 ++++++++++++++++++ .../cli/strategy/manual_commit_hooks.go | 49 +++++ 2 files changed, 238 insertions(+) diff --git a/cmd/entire/cli/strategy/hooks_test.go b/cmd/entire/cli/strategy/hooks_test.go index 0b5197bc8..df6e6e606 100644 --- a/cmd/entire/cli/strategy/hooks_test.go +++ b/cmd/entire/cli/strategy/hooks_test.go @@ -137,6 +137,195 @@ func TestGetGitDirInPath_NotARepo(t *testing.T) { } } +func TestIsGitSequenceOperation_NoOperation(t *testing.T) { + // Create a temp directory and initialize a real git repo + tmpDir := t.TempDir() + t.Chdir(tmpDir) + + ctx := context.Background() + cmd := exec.CommandContext(ctx, "git", "init") + cmd.Dir = tmpDir + if err := cmd.Run(); err != nil { + t.Fatalf("failed to init git repo: %v", err) + } + + // No sequence operation in progress + if isGitSequenceOperation() { + t.Error("isGitSequenceOperation() = true, want false for clean repo") + } +} + +func TestIsGitSequenceOperation_RebaseMerge(t *testing.T) { + tmpDir := t.TempDir() + t.Chdir(tmpDir) + + ctx := context.Background() + cmd := exec.CommandContext(ctx, "git", "init") + cmd.Dir = tmpDir + if err := cmd.Run(); err != nil { + t.Fatalf("failed to init git repo: %v", err) + } + + // Simulate rebase-merge state + rebaseMergeDir := filepath.Join(tmpDir, ".git", "rebase-merge") + if err := os.MkdirAll(rebaseMergeDir, 0o755); err != nil { + t.Fatalf("failed to create rebase-merge dir: %v", err) + } + + if !isGitSequenceOperation() { + t.Error("isGitSequenceOperation() = false, want true during rebase-merge") + } +} + +func TestIsGitSequenceOperation_RebaseApply(t *testing.T) { + tmpDir := t.TempDir() + t.Chdir(tmpDir) + + ctx := context.Background() + cmd := exec.CommandContext(ctx, "git", "init") + cmd.Dir = tmpDir + if err := cmd.Run(); err != nil { + t.Fatalf("failed to init git repo: %v", err) + } + + // Simulate rebase-apply state + rebaseApplyDir := filepath.Join(tmpDir, ".git", "rebase-apply") + if err := os.MkdirAll(rebaseApplyDir, 0o755); err != nil { + t.Fatalf("failed to create rebase-apply dir: %v", err) + } + + if !isGitSequenceOperation() { + t.Error("isGitSequenceOperation() = false, want true during rebase-apply") + } +} + +func TestIsGitSequenceOperation_CherryPick(t *testing.T) { + tmpDir := t.TempDir() + t.Chdir(tmpDir) + + ctx := context.Background() + cmd := exec.CommandContext(ctx, "git", "init") + cmd.Dir = tmpDir + if err := cmd.Run(); err != nil { + t.Fatalf("failed to init git repo: %v", err) + } + + // Simulate cherry-pick state + cherryPickHead := filepath.Join(tmpDir, ".git", "CHERRY_PICK_HEAD") + if err := os.WriteFile(cherryPickHead, []byte("abc123"), 0o644); err != nil { + t.Fatalf("failed to create CHERRY_PICK_HEAD: %v", err) + } + + if !isGitSequenceOperation() { + t.Error("isGitSequenceOperation() = false, want true during cherry-pick") + } +} + +func TestIsGitSequenceOperation_Revert(t *testing.T) { + tmpDir := t.TempDir() + t.Chdir(tmpDir) + + ctx := context.Background() + cmd := exec.CommandContext(ctx, "git", "init") + cmd.Dir = tmpDir + if err := cmd.Run(); err != nil { + t.Fatalf("failed to init git repo: %v", err) + } + + // Simulate revert state + revertHead := filepath.Join(tmpDir, ".git", "REVERT_HEAD") + if err := os.WriteFile(revertHead, []byte("abc123"), 0o644); err != nil { + t.Fatalf("failed to create REVERT_HEAD: %v", err) + } + + if !isGitSequenceOperation() { + t.Error("isGitSequenceOperation() = false, want true during revert") + } +} + +func TestIsGitSequenceOperation_Worktree(t *testing.T) { + // Test that detection works in a worktree (git dir is different) + tmpDir := t.TempDir() + mainRepo := filepath.Join(tmpDir, "main") + worktreeDir := filepath.Join(tmpDir, "worktree") + + if err := os.MkdirAll(mainRepo, 0o755); err != nil { + t.Fatalf("failed to create main repo dir: %v", err) + } + + ctx := context.Background() + + // Initialize main repo with a commit + cmd := exec.CommandContext(ctx, "git", "init") + cmd.Dir = mainRepo + if err := cmd.Run(); err != nil { + t.Fatalf("failed to init main repo: %v", err) + } + + cmd = exec.CommandContext(ctx, "git", "config", "user.email", "test@test.com") + cmd.Dir = mainRepo + if err := cmd.Run(); err != nil { + t.Fatalf("failed to configure git email: %v", err) + } + + cmd = exec.CommandContext(ctx, "git", "config", "user.name", "Test User") + cmd.Dir = mainRepo + if err := cmd.Run(); err != nil { + t.Fatalf("failed to configure git name: %v", err) + } + + testFile := filepath.Join(mainRepo, "test.txt") + if err := os.WriteFile(testFile, []byte("test"), 0o644); err != nil { + t.Fatalf("failed to create test file: %v", err) + } + + cmd = exec.CommandContext(ctx, "git", "add", ".") + cmd.Dir = mainRepo + if err := cmd.Run(); err != nil { + t.Fatalf("failed to git add: %v", err) + } + + cmd = exec.CommandContext(ctx, "git", "commit", "-m", "initial") + cmd.Dir = mainRepo + if err := cmd.Run(); err != nil { + t.Fatalf("failed to git commit: %v", err) + } + + // Create a worktree + cmd = exec.CommandContext(ctx, "git", "worktree", "add", worktreeDir, "-b", "feature") + cmd.Dir = mainRepo + if err := cmd.Run(); err != nil { + t.Fatalf("failed to create worktree: %v", err) + } + + // Change to worktree + t.Chdir(worktreeDir) + + // Should not detect sequence operation in clean worktree + if isGitSequenceOperation() { + t.Error("isGitSequenceOperation() = true in clean worktree, want false") + } + + // Get the worktree's git dir and simulate rebase state there + cmd = exec.CommandContext(ctx, "git", "rev-parse", "--git-dir") + cmd.Dir = worktreeDir + output, err := cmd.Output() + if err != nil { + t.Fatalf("failed to get git dir: %v", err) + } + gitDir := strings.TrimSpace(string(output)) + + rebaseMergeDir := filepath.Join(gitDir, "rebase-merge") + if err := os.MkdirAll(rebaseMergeDir, 0o755); err != nil { + t.Fatalf("failed to create rebase-merge dir in worktree: %v", err) + } + + // Now should detect sequence operation + if !isGitSequenceOperation() { + t.Error("isGitSequenceOperation() = false in worktree during rebase, want true") + } +} + func TestInstallGitHook_Idempotent(t *testing.T) { // Create a temp directory and initialize a real git repo tmpDir := t.TempDir() diff --git a/cmd/entire/cli/strategy/manual_commit_hooks.go b/cmd/entire/cli/strategy/manual_commit_hooks.go index f571f2764..889d1fd52 100644 --- a/cmd/entire/cli/strategy/manual_commit_hooks.go +++ b/cmd/entire/cli/strategy/manual_commit_hooks.go @@ -6,6 +6,8 @@ import ( "fmt" "log/slog" "os" + "os/exec" + "path/filepath" "strings" "entire.io/cli/cmd/entire/cli/agent" @@ -133,6 +135,43 @@ func stripCheckpointTrailer(message string) string { return strings.Join(result, "\n") } +// isGitSequenceOperation checks if git is currently in the middle of a rebase, +// cherry-pick, or revert operation. During these operations, commits are being +// replayed and should not be linked to agent sessions. +// +// Detects: +// - rebase: .git/rebase-merge/ or .git/rebase-apply/ directories +// - cherry-pick: .git/CHERRY_PICK_HEAD file +// - revert: .git/REVERT_HEAD file +func isGitSequenceOperation() bool { + // Get git directory (handles worktrees correctly) + ctx := context.Background() + cmd := exec.CommandContext(ctx, "git", "rev-parse", "--git-dir") + output, err := cmd.Output() + if err != nil { + return false // Can't determine, assume not in sequence operation + } + gitDir := strings.TrimSpace(string(output)) + + // Check for rebase state directories + if _, err := os.Stat(filepath.Join(gitDir, "rebase-merge")); err == nil { + return true + } + if _, err := os.Stat(filepath.Join(gitDir, "rebase-apply")); err == nil { + return true + } + + // Check for cherry-pick and revert state files + if _, err := os.Stat(filepath.Join(gitDir, "CHERRY_PICK_HEAD")); err == nil { + return true + } + if _, err := os.Stat(filepath.Join(gitDir, "REVERT_HEAD")); err == nil { + return true + } + + return false +} + // PrepareCommitMsg is called by the git prepare-commit-msg hook. // Adds an Entire-Checkpoint trailer to the commit message with a stable checkpoint ID. // Only adds a trailer if there's actually new session content to condense. @@ -149,6 +188,16 @@ func stripCheckpointTrailer(message string) string { func (s *ManualCommitStrategy) PrepareCommitMsg(commitMsgFile string, source string) error { logCtx := logging.WithComponent(context.Background(), "checkpoint") + // Skip during rebase, cherry-pick, or revert operations + // These are replaying existing commits and should not be linked to agent sessions + if isGitSequenceOperation() { + logging.Debug(logCtx, "prepare-commit-msg: skipped during git sequence operation", + slog.String("strategy", "manual-commit"), + slog.String("source", source), + ) + return nil + } + // Skip for merge, squash, and commit (amend) sources // These are auto-generated or reusing existing messages - not from Claude sessions switch source { From a35d983ec0e0ddcf4705ec79b25f509b3c313902 Mon Sep 17 00:00:00 2001 From: Stefan Haubold Date: Mon, 26 Jan 2026 21:04:35 +0100 Subject: [PATCH 2/2] use existing GetGitDir() Entire-Checkpoint: 87ae9c9a92c1 --- cmd/entire/cli/strategy/manual_commit_hooks.go | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/cmd/entire/cli/strategy/manual_commit_hooks.go b/cmd/entire/cli/strategy/manual_commit_hooks.go index 889d1fd52..dc33d737f 100644 --- a/cmd/entire/cli/strategy/manual_commit_hooks.go +++ b/cmd/entire/cli/strategy/manual_commit_hooks.go @@ -6,7 +6,6 @@ import ( "fmt" "log/slog" "os" - "os/exec" "path/filepath" "strings" @@ -144,14 +143,11 @@ func stripCheckpointTrailer(message string) string { // - cherry-pick: .git/CHERRY_PICK_HEAD file // - revert: .git/REVERT_HEAD file func isGitSequenceOperation() bool { - // Get git directory (handles worktrees correctly) - ctx := context.Background() - cmd := exec.CommandContext(ctx, "git", "rev-parse", "--git-dir") - output, err := cmd.Output() + // Get git directory (handles worktrees and relative paths correctly) + gitDir, err := GetGitDir() if err != nil { return false // Can't determine, assume not in sequence operation } - gitDir := strings.TrimSpace(string(output)) // Check for rebase state directories if _, err := os.Stat(filepath.Join(gitDir, "rebase-merge")); err == nil {