Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
27be52a
Refactor: Format upstream imports and struct fields
nnutter Oct 19, 2026
fbec85c
Refactor: Relocate and order upstream declarations
nnutter Oct 19, 2026
c6d0ec8
Refactor: Extract the saved-progress type
nnutter Oct 19, 2026
3493133
Refactor: Extract upstream decision predicates
nnutter Oct 19, 2026
011486e
Refactor: Move orphan-stack counts into test cases
nnutter Oct 19, 2026
6ded496
Refactor: Extract stack-table rendering
nnutter Oct 19, 2026
3e414b1
Refactor: Extract status-verbosity assertions
nnutter Oct 19, 2026
b7e576c
Refactor: Extract review-state count assertions
nnutter Oct 19, 2026
f7ef4f1
Preserve the real-tuicr test opt-in during Git isolation
nnutter Oct 19, 2026
568000f
Consolidate live-review check into status --is-reviewing flag.
nnutter Oct 19, 2026
18713b7
Rename run command to commits to distinguish stack and files reviews
nnutter Oct 19, 2026
d58a4f1
Add files command for working-tree reviews
nnutter Oct 19, 2026
ae69c03
Refactor: Move prompt helpers into their own file
nnutter Oct 19, 2026
5141512
Refactor: Move rendering and JSON helpers to display package
nnutter Oct 19, 2026
3f7a647
Refactor: Extract output selection helpers for reuse
nnutter Oct 19, 2026
8fb3dcc
Refactor: Move commits command into its own package
nnutter Oct 19, 2026
1567cfb
Refactor: Extract files command to its own package
nnutter Oct 19, 2026
7fceb0f
Refactor: Extract status command into its own package
nnutter Oct 19, 2026
eaf6767
Refactor: Extract feedback command into its own package
nnutter Oct 19, 2026
3bcd670
Refactor: Extract approve command into its own package
nnutter Oct 19, 2026
eb720d6
Refactor: Extract log command into its own package
nnutter Oct 19, 2026
1ccfc93
Refactor: Extract root command assembly into cli/root package
nnutter Oct 19, 2026
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
71 changes: 57 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,15 +151,17 @@ Current and historical feedback remain available in the result.
### Check live review activity

```sh
git-review running --json
git-review status --is-reviewing --json
```

Successful output is `{"version":1,"running":true}` or `{"version":1,"running":false}`.
The command reports whether the current branch owns the live review worktree lock without changing review state or creating files.
Successful output is `{"version":1,"command":"status","is_reviewing":true}` or `{"version":1,"command":"status","is_reviewing":false}`.
Without `--json`, the command prints `Is reviewing: true` or `Is reviewing: false`.
The flag reports whether the current branch owns the live review worktree lock without changing review state or creating files.
A running automatic review holds this lock across all selected stacks.
A persisted `reviewing` attempt alone does not indicate a running process.
A review on another branch returns `running: false`, including when that branch is in a linked worktree.
A review on another branch returns `is_reviewing: false`, including when that branch is in a linked worktree.
The lock remains repository-wide to prevent concurrent reviews from using the shared review checkout.
The flag cannot be combined with `--needs-review` or `--verbose`.
Command failures exit with status 1 and emit the standard versioned JSON error object.

### Collect saved feedback
Expand Down Expand Up @@ -195,11 +197,13 @@ Active attempts remain visible in `active`, but collecting feedback does not res
### Review a stack

```sh
git-review run
git-review run HEAD
git-review run abc123..def456
git-review commits
git-review commits HEAD
git-review commits abc123..def456
```

`run` remains as a deprecated alias for `commits`.

Without an argument, git-review reviews each eligible, non-deferred stack in oldest-first order, once per invocation.
Approval or feedback does not stop this sequence.
A commit reference selects only its containing stack.
Expand Down Expand Up @@ -233,9 +237,9 @@ An untouched review leaves no attempt in git-review's history and does not requi
### Review a range of stacks

```sh
git-review run --start abc123
git-review run --start abc123 --end def456 --json
git-review run --end def456
git-review commits --start abc123
git-review commits --start abc123 --end def456 --json
git-review commits --end def456
```

Each invocation reviews eligible stacks within the inclusive bounds in oldest-first order.
Expand Down Expand Up @@ -264,9 +268,9 @@ In an interactive terminal, git-review prompts to recover, replace, or cancel.
With JSON output or redirected streams, it returns `decision_required` without a prompt.

```sh
git-review run --recover
git-review run --replace
git-review run --recover --session <session-id>
git-review commits --recover
git-review commits --replace
git-review commits --recover --session <session-id>
```

Recovery reopens tuicr at the previous attempt's exact captured range and preserves its attempt ID.
Expand All @@ -277,7 +281,44 @@ Neither operation proceeds while the review worktree is held by a live git-revie

`--session` accepts an exact saved session ID, index slug, or indexed `sessions/<id>.json` path.
It resolves ambiguous saved sessions and is not an arbitrary filesystem path.
If an interrupted original disappeared and no lineage identifies its replacement, use `git-review run <replacement-commit> --replace`.
If an interrupted original disappeared and no lineage identifies its replacement, use `git-review commits <replacement-commit> --replace`.

### Review working-tree files

```sh
git-review files
git-review files <path>
git-review files --untracked
git-review files --untracked --json
```

Without a path, git-review runs `tuicr tui --all-files` to review every tracked file.
With a path, it runs `tuicr tui --file <path>` for that file or directory.
With `--untracked` and no path, it runs `tuicr tui --file .` to include untracked files in the current directory.
`--untracked` cannot be combined with an explicit path.
Use `--session` to select an exact saved session when several file reviews match the same repository.

File reviews run in the feature checkout, not the detached review worktree, and do not require a configured upstream.
They hold the same repository-wide worktree lock as `commits` reviews, so `status --is-reviewing` reports true during either review.
They do not create git-review attempts and do not affect `status` counts or `feedback` records.
`approve` applies only to commit stacks, not file reviews.

Successful `files` output uses `"command":"files"` to distinguish it from `"command":"commits"`.
It contains the reviewed repository, requested path, `allFiles` and `untracked` flags, and the saved file review with comments and session notes.
Saved sessions are matched to tuicr's canonical file or directory scope, not just the Git repository root.
File comment paths are relative to the Git repository root, including reviews started from a subdirectory.
Statuses mirror stack reviews without recovery decisions:

| Saved result | Status |
| --- | --- |
| Every file reviewed, without comments or session notes | `approved` |
| Any comments or session notes, including drafts | `feedback` |
| No reviewed files, comments, or session notes, including no saved session | `cancelled` (no feedback saved) |
| Some reviewed files, but incomplete and without notes | `incomplete` |

Address `files` feedback as one or more new commits, logically organized and sized.
Do not use `fixup!`, `amend!`, or `reword!` commits for file feedback.
Address `commits` feedback with native Git fixup commits inserted immediately after their target stacks instead.

### Record explicit user approval

Expand All @@ -301,6 +342,8 @@ All commands accept `--json`, before or after the subcommand.
Each command produces one JSON value followed by a newline on stdout.
Successful `status` output is an ordered array of state counts.
Other successful commands produce a versioned JSON object.
`commits` results use `"command":"commits"` with stack entries and attempts.
`files` results use `"command":"files"` with the file scope and saved file review.
JSON and terminal output consume the same typed result.
JSON retains full commit IDs, attempts, saved comments, session notes, and optional previous-version lineage.
Tuicr renders its interactive view through the controlling terminal, with diagnostics on stderr, not the JSON stream.
Expand Down
32 changes: 27 additions & 5 deletions internal/cli/activity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ func TestReviewRunningReportsLiveLockNotPersistedAttempt(t *testing.T) {
require.NoError(t, err)
check := func(cwd string, running bool) {
t.Helper()
out, terminal, err := executeBinary(t, binary, cwd, "running", "--json")
out, terminal, err := executeBinary(t, binary, cwd, "status", "--is-reviewing", "--json")
require.NoError(t, err)
require.Empty(t, terminal)
want := `{"version":1,"running":false}`
want := `{"version":1,"command":"status","is_reviewing":false}`
if running {
want = `{"version":1,"running":true}`
want = `{"version":1,"command":"status","is_reviewing":true}`
}
require.JSONEq(t, want, out)
}
Expand Down Expand Up @@ -78,15 +78,37 @@ func TestReviewRunningReportsLiveLockNotPersistedAttempt(t *testing.T) {
}
// 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")
out, terminal, err := executeBinary(t, binary, dir, "status", "--is-reviewing", "--json")
require.Error(t, err)
require.Empty(t, terminal)
require.Contains(t, out, `"status":"error"`)
}

func TestReviewRunningOutsideRepository(t *testing.T) {
out, terminal, err := execute(t, t.TempDir(), "running", "--json")
out, terminal, err := execute(t, t.TempDir(), "status", "--is-reviewing", "--json")
require.Error(t, err)
require.Empty(t, terminal)
require.Contains(t, out, `"status":"error"`)
}

func TestStatusIsReviewingTerminalAndConflicts(t *testing.T) {
binary := buildGitReview(t)
dir := featureRepo(t)
out, terminal, err := executeBinary(t, binary, dir, "status", "--is-reviewing")
require.NoError(t, err)
require.Empty(t, terminal)
require.Contains(t, out, "Is reviewing: false")
out, terminal, err = executeBinary(t, binary, dir, "status", "--is-reviewing", "--json")
require.NoError(t, err)
require.Empty(t, terminal)
require.Contains(t, out, `"is_reviewing"`)
for _, args := range [][]string{
{"status", "--is-reviewing", "--needs-review", "--json"},
{"status", "--is-reviewing", "--verbose", "--json"},
} {
out, terminal, err := executeBinary(t, binary, dir, args...)
require.Error(t, err)
require.Empty(t, terminal)
require.Contains(t, out, `"status":"error"`)
}
}
49 changes: 49 additions & 0 deletions internal/cli/approve/approve.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Package approve records explicit user approval of complete stacks.
package approve

import (
"io"

"github.com/spf13/cobra"

"github.com/nnutter/git-review/internal/display"
"github.com/nnutter/git-review/internal/review"
)

// Command houses positional arguments for the approval review.
type Command struct {
Service review.Service
Output io.Writer
First string
Last string
}

// New builds the approve sub-command. Callers install it onto the parent
// with Install.
func New(svc review.Service, out io.Writer) *cobra.Command {
c := &Command{Service: svc, Output: out}
cmd := &cobra.Command{Use: "approve <start_commit> [<end_commit>]", Aliases: []string{"confirm"}, Short: "Record explicit user approval of complete stacks", Long: "Record user approval without launching tuicr. Endpoints are inclusive and\nmust include complete stacks, including all attached feedback commits.", Args: cobra.RangeArgs(1, 2), RunE: c.RunE}
return cmd
}

// Install adds the approve sub-command to parent.
func Install(parent *cobra.Command, svc review.Service, out io.Writer) {
parent.AddCommand(New(svc, out))
}

// RunE runs the approval workflow.
func (c *Command) RunE(cmd *cobra.Command, args []string) error {
asJSON := false
if flag, err := cmd.Root().PersistentFlags().GetBool("json"); err == nil {
asJSON = flag
}
c.First = args[0]
if len(args) == 2 {
c.Last = args[1]
}
result, err := c.Service.Approve(cmd.Context(), c.First, c.Last)
if err != nil {
return err
}
return display.Write(c.Output, asJSON, result, false)
}
Loading
Loading