diff --git a/internal/cli/activity_test.go b/internal/cli/activity_test.go index 80b4806..f2ffc28 100644 --- a/internal/cli/activity_test.go +++ b/internal/cli/activity_test.go @@ -3,6 +3,7 @@ package cli import ( "context" "os" + "os/exec" "path/filepath" "testing" @@ -39,6 +40,14 @@ func TestReviewRunningReportsLiveLockNotPersistedAttempt(t *testing.T) { check(dir, false) other := filepath.Join(t.TempDir(), "checkout") git(t, dir, "worktree", "add", "-b", "other", other, "HEAD") + pause := func(cwd string) { + t.Helper() + cmd := exec.Command("git", "rebase", "--force-rebase", "--exec", "false", "HEAD~1") + cmd.Dir = cwd + output, err := cmd.CombinedOutput() + require.Error(t, err, string(output)) + require.Empty(t, git(t, cwd, "branch", "--show-current")) + } called := false reviewer := review.Reviewer{ Service: review.Service{Repository: review.Repository{Dir: dir}}, @@ -46,14 +55,33 @@ func TestReviewRunningReportsLiveLockNotPersistedAttempt(t *testing.T) { called = true check(dir, true) check(other, false) + // A paused replay must still identify its branch, including in a + // linked worktree, without treating another branch's lock as its own. + pause(dir) + pause(other) + check(dir, true) + check(other, false) + git(t, dir, "rebase", "--abort") + git(t, other, "rebase", "--abort") return "cancel", nil }, } _, err = reviewer.Review(t.Context(), review.ReviewOptions{}) require.NoError(t, err) require.True(t, called) + pause(dir) + pause(other) check(dir, false) check(other, false) + for _, cwd := range []string{dir, other} { + git(t, cwd, "rebase", "--abort") + } + // A detached checkout without rebase metadata has no branch to check. + git(t, dir, "checkout", "--detach") + out, terminal, err := executeBinary(t, binary, dir, "running", "--json") + require.Error(t, err) + require.Empty(t, terminal) + require.Contains(t, out, `"status":"error"`) } func TestReviewRunningOutsideRepository(t *testing.T) { diff --git a/internal/review/git.go b/internal/review/git.go index ba88435..1cb443d 100644 --- a/internal/review/git.go +++ b/internal/review/git.go @@ -4,6 +4,7 @@ package review import ( "context" "crypto/sha256" + "errors" "fmt" "os" "os/exec" @@ -148,6 +149,40 @@ func (r Repository) Snapshot(ctx context.Context) (Snapshot, error) { return view, nil } +// reviewBranch identifies the branch whose review lock applies during a replay. +// Other review commands still require an attached HEAD through branchView. +func (r Repository) reviewBranch(ctx context.Context) (string, error) { + branch, err := r.git(ctx, "symbolic-ref", "--quiet", "HEAD") + if err == nil { + return branch, nil + } + if exit, ok := errors.AsType[*exec.ExitError](err); !ok || exit.ExitCode() != 1 { + return "", err + } + for _, backend := range []string{"rebase-merge", "rebase-apply"} { + path, pathErr := r.git(ctx, "rev-parse", "--path-format=absolute", "--git-path", backend+"/head-name") + if pathErr != nil { + return "", pathErr + } + data, readErr := os.ReadFile(path) + if errors.Is(readErr, os.ErrNotExist) { + continue + } + if readErr != nil { + return "", readErr + } + branch = strings.TrimSpace(string(data)) + if !strings.HasPrefix(branch, "refs/heads/") { + return "", fmt.Errorf("rebase does not identify a local branch") + } + if _, err := r.git(ctx, "check-ref-format", branch); err != nil { + return "", err + } + return branch, nil + } + return "", err +} + func (r Repository) branchView(ctx context.Context) (Snapshot, string, error) { var view Snapshot repo, err := r.git(ctx, "rev-parse", "--show-toplevel") diff --git a/internal/review/service.go b/internal/review/service.go index 44832e6..db2af6b 100644 --- a/internal/review/service.go +++ b/internal/review/service.go @@ -124,14 +124,14 @@ func (s Service) Feedback(ctx context.Context) (Result, error) { return result, nil } -// ReviewRunning reports whether the current branch owns the live review lock. +// ReviewRunning reports whether the current or rebasing branch owns the live review lock. // It does not create the lock file or infer activity from persisted attempts. func (s Service) ReviewRunning(ctx context.Context) (bool, error) { path, err := WorktreePath(s.Repository.Dir) if err != nil { return false, err } - branch, err := s.Repository.git(ctx, "symbolic-ref", "--quiet", "HEAD") + branch, err := s.Repository.reviewBranch(ctx) if err != nil { return false, err }