git-reviewer 2.13.0 → 2.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,14 +3,14 @@
3
3
  [![CI](https://github.com/dheerajjha/reviewer/actions/workflows/ci.yml/badge.svg)](https://github.com/dheerajjha/reviewer/actions/workflows/ci.yml)
4
4
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
5
5
  [![Node](https://img.shields.io/badge/node-%3E%3D18-brightgreen.svg)](package.json)
6
- [![Tests](https://img.shields.io/badge/tests-355-brightgreen.svg)](test/)
6
+ [![Tests](https://img.shields.io/badge/tests-372-brightgreen.svg)](test/)
7
7
  [![Dependencies](https://img.shields.io/badge/dependencies-3-brightgreen.svg)](package.json)
8
8
 
9
9
  When an agent writes the code, reading it becomes the bottleneck — and the tool
10
10
  built for reading a diff, the pull request, wants a branch, a remote and a push
11
11
  you are not ready to make yet.
12
12
 
13
- `reviewer` gives you the pull-request view of your working directory instead:
13
+ reviewer gives you the pull-request view of your working directory instead:
14
14
  inline comments with threaded follow-ups, each attached to the exact line you
15
15
  mean, with no branch, no remote, no push and no account. Then it hands the
16
16
  marked-up review back to the agent to act on.
@@ -19,15 +19,19 @@ marked-up review back to the agent to act on.
19
19
  npm install -g git-reviewer
20
20
 
21
21
  cd ~/your-project
22
- reviewer | claude -p "Apply this review."
22
+ git reviewer | claude -p "Apply this review."
23
23
  ```
24
24
 
25
- That is the whole loop. `reviewer` opens the review in your browser; when you
25
+ Installed as `git-reviewer`, it runs as `git reviewer` — git treats any
26
+ `git-<name>` command on your PATH as a subcommand, so the name you install is
27
+ the name you type. `reviewer` is the same program under a shorter name.
28
+
29
+ That is the whole loop. `git reviewer` opens the review in your browser; when you
26
30
  press **Submit Review**, the review is written to stdout and the command
27
31
  exits — so whatever is on the other end of the pipe receives it and starts
28
32
  work. Nothing to copy, no second command, no file to go and find.
29
33
 
30
- Without a pipe, `reviewer` is just a review tool: it tells you where the
34
+ Without a pipe, `git reviewer` is just a review tool: it tells you where the
31
35
  review was saved and keeps serving, because you may have more to read.
32
36
  Everything it says to you goes to stderr either way, so only the review is
33
37
  ever on stdout.
@@ -46,8 +50,8 @@ When the review and the hand-off happen at different times, the two-step form
46
50
  still works and is the one to reach for:
47
51
 
48
52
  ```bash
49
- reviewer # read the diff, comment on it
50
- reviewer export . --format prompt | claude -p "Apply this review."
53
+ git reviewer # read the diff, comment on it
54
+ git reviewer export . --format prompt | claude -p "Apply this review."
51
55
  ```
52
56
 
53
57
  Comments persist between sessions and survive the code moving underneath them —
@@ -64,8 +68,8 @@ comment they answer, and everything is saved as you type.
64
68
  ## Usage
65
69
 
66
70
  ```
67
- reviewer [repository] [options]
68
- reviewer export [repository] [--format json|prompt]
71
+ git reviewer [repository] [options]
72
+ git reviewer export [repository] [--format json|prompt]
69
73
 
70
74
  repository A git repository, or any directory inside one
71
75
  (default: the current directory)
@@ -73,20 +77,30 @@ reviewer export [repository] [--format json|prompt]
73
77
  -p, --port <n> Port to listen on (default 4500; falls back to a free
74
78
  port if that one is taken)
75
79
  --no-open Print the URL instead of opening a browser
80
+ --staged Review only staged changes (HEAD vs index)
76
81
  -f, --format <f> Export format: json (default) or prompt
77
82
  -h, --help Show help
78
83
  -v, --version Show the version
79
84
  ```
80
85
 
81
86
  ```bash
82
- reviewer # review the repository you are standing in
83
- reviewer ~/work/api # review another one
84
- reviewer . --no-open # print the URL, open it yourself
85
- reviewer . --port 8080 # somewhere other than 4500
87
+ git reviewer # review the repository you are standing in
88
+ git reviewer ~/work/api # review another one
89
+ git reviewer . --no-open # print the URL, open it yourself
90
+ git reviewer . --port 8080 # somewhere other than 4500
86
91
  ```
87
92
 
88
93
  Two at once is fine — the second one finds its own port.
89
94
 
95
+ Use `git reviewer --staged` to review what the next commit would contain.
96
+ The file list, diff and full context come from the index, so edits made after
97
+ `git add` stay out of the view. An empty index comparison stays empty; it does
98
+ not fall back to the last commit. The scope bar and exported review identify
99
+ this as a staged review. The default command still shows all working-tree
100
+ changes. As with the default view, reload after changing what is staged. Saved comments
101
+ are shared between views and may refer to code outside the current comparison;
102
+ check their anchors before applying them.
103
+
90
104
  ### When you are not standing in a repository
91
105
 
92
106
  The page opens on a picker rather than on an empty box:
@@ -110,19 +124,25 @@ To install it as a command rather than running it through `npx`:
110
124
 
111
125
  ```bash
112
126
  npm install -g git-reviewer
113
- reviewer .
127
+ git reviewer .
114
128
  ```
115
129
 
116
- The package is `git-reviewer` because `reviewer` and `code-reviewer` were both
117
- already taken on npm. It installs the command under both names, so whichever
118
- one you reach for is the one that is there:
130
+ The package is called `git-reviewer` because `reviewer` was already taken on
131
+ npm — and it turns out to be the right name. Git runs any `git-<name>` on your
132
+ PATH as a subcommand, so the name you install is the name you type. It also
133
+ installs `reviewer`, the same program under a shorter name, for anyone who
134
+ prefers it:
119
135
 
120
136
  ```bash
121
- reviewer # the command
122
- git-reviewer # the package name, if that is what you typed
123
- git reviewer # git dispatches to it like any other subcommand
137
+ git reviewer # the name it is installed as
138
+ reviewer # the same program, shorter
124
139
  ```
125
140
 
141
+ Whichever you use is the name it uses back: help and hints are printed in
142
+ terms of the command you actually ran, so they can be pasted as they are.
143
+ Under git, `git reviewer --help` opens the manual page — git reads `--help` on
144
+ any subcommand that way — and `git reviewer -h` prints the usage.
145
+
126
146
  From a clone, if you would rather not install anything:
127
147
 
128
148
  ```bash
@@ -132,7 +152,7 @@ cd reviewer && npm install && npm link
132
152
 
133
153
  ## What it shows you
134
154
 
135
- `reviewer` picks what to review based on the state of the repository, so there
155
+ reviewer picks what to review based on the state of the repository, so there
136
156
  is nothing to configure:
137
157
 
138
158
  | Repository state | What you review |
@@ -290,8 +310,8 @@ export` prints the review you saved — no server, no submit step — either as
290
310
  JSON or as instructions ready to pipe:
291
311
 
292
312
  ```bash
293
- reviewer export . --format prompt | claude -p "Apply this review to the repo."
294
- reviewer export . | jq '.comments[].file'
313
+ git reviewer export . --format prompt | claude -p "Apply this review to the repo."
314
+ git reviewer export . | jq '.comments[].file'
295
315
  ```
296
316
 
297
317
  Every comment carries an `anchor`: the exact text of the line it was left on.
@@ -362,7 +382,7 @@ rather than a public issue.
362
382
 
363
383
  ```bash
364
384
  npm install # 3 dependencies, no build step, ~6MB
365
- npm test # 355 tests
385
+ npm test # 372 tests
366
386
  npm run test:watch
367
387
  npm run test:coverage
368
388
  ```
@@ -389,7 +409,7 @@ temporary directory. The HTTP tests do not stub git: each one builds a real
389
409
  repository in a temp directory and runs real `git` against it, because diff
390
410
  parsing is exactly where a stub would be wrong in the same way the code is.
391
411
 
392
- Tests run on Linux and macOS across Node 18, 20, and 22. See
412
+ Tests run on Linux, macOS and Windows across Node 18, 20, and 22. See
393
413
  [CONTRIBUTING.md](CONTRIBUTING.md).
394
414
 
395
415
  ## Tech
@@ -413,11 +433,18 @@ The ones most likely to change how the tool feels:
413
433
  - [#2](https://github.com/dheerajjha/reviewer/issues/2) — give comments a state, so a review can be worked through and marked off
414
434
  - [#3](https://github.com/dheerajjha/reviewer/issues/3) — report whether each anchor still matches the file
415
435
  - [#4](https://github.com/dheerajjha/reviewer/issues/4) — expose the review over MCP, so an agent works through it interactively
416
- - [#6](https://github.com/dheerajjha/reviewer/issues/6) — review a commit range, not just the working tree
436
+ - [#6](https://github.com/dheerajjha/reviewer/issues/6) — open straight into a comparison from the command line, `git reviewer --range main...HEAD`
417
437
 
418
438
  [CONTRIBUTING.md](CONTRIBUTING.md) covers the setup and what a good change looks
419
439
  like here.
420
440
 
441
+ ## Related
442
+
443
+ [margins](https://github.com/dheerajjha/margins) is its sibling for notes rather
444
+ than diffs: `margins` in a folder of markdown opens it in the browser, with a
445
+ file tree, `[[wikilinks]]` and backlinks, search, and editing with a live
446
+ preview. `npm install -g margins`.
447
+
421
448
  ## License
422
449
 
423
450
  [MIT](LICENSE) © Dheeraj Jha
package/bin/reviewer.js CHANGED
@@ -4,7 +4,7 @@
4
4
  const fs = require('fs').promises;
5
5
  const path = require('path');
6
6
 
7
- const { parseArgs, buildUrl, UsageError, USAGE } = require('../lib/cli');
7
+ const { parseArgs, buildUrl, UsageError, usage, invokedAs } = require('../lib/cli');
8
8
  const { homeRelative } = require('../lib/paths');
9
9
  const { openInBrowser } = require('../lib/browser');
10
10
  const { formatPrompt } = require('../lib/agent');
@@ -19,6 +19,10 @@ const {
19
19
  const { startServer, DEFAULT_PORT, DEFAULT_HOST } = require('../server');
20
20
  const { version } = require('../package.json');
21
21
 
22
+ // What to call this command when it talks about itself: whatever the person
23
+ // typed, so a hint can be pasted back as-is. See invokedAs in lib/cli.js.
24
+ const NAME = invokedAs(process.env, process.argv[1]);
25
+
22
26
  /**
23
27
  * The `reviewer` command: start the local server and open it in a browser.
24
28
  */
@@ -138,7 +142,7 @@ async function main() {
138
142
  }
139
143
 
140
144
  if (options.help) {
141
- process.stdout.write(USAGE);
145
+ process.stdout.write(usage(NAME));
142
146
  return;
143
147
  }
144
148
 
@@ -212,7 +216,7 @@ async function main() {
212
216
  note(`\n Review saved ${homeRelative(reviewPath)}`);
213
217
  note(` ${count}\n`);
214
218
  note(' Hand it to an agent with:');
215
- note(` reviewer export ${repoPath ? homeRelative(repoPath) : '.'} --format prompt | claude -p "Apply this review."\n`);
219
+ note(` ${NAME} export ${repoPath ? homeRelative(repoPath) : '.'} --format prompt | claude -p "Apply this review."\n`);
216
220
  return;
217
221
  }
218
222
 
@@ -246,7 +250,7 @@ async function main() {
246
250
  options.port ?? (Number(process.env.PORT) || DEFAULT_PORT),
247
251
  { onReviewSubmitted, handoff: handingOff, onIdle }
248
252
  );
249
- const url = buildUrl(`http://${DEFAULT_HOST}:${server.address().port}`, repoPath);
253
+ const url = buildUrl(`http://${DEFAULT_HOST}:${server.address().port}`, repoPath, options.staged);
250
254
 
251
255
  note(`\n Code Reviewer ${url}`);
252
256
  if (repoPath) note(` reviewing ${repoPath}`);
@@ -265,7 +269,7 @@ async function main() {
265
269
  main().catch(error => {
266
270
  if (error instanceof UsageError) {
267
271
  console.error(`error: ${error.message}\n`);
268
- process.stderr.write(USAGE);
272
+ process.stderr.write(usage(NAME));
269
273
  process.exit(2);
270
274
  }
271
275
 
package/lib/agent.js CHANGED
@@ -22,7 +22,7 @@ const SCHEMA = 'code-review/v1';
22
22
  * @property {string} schema
23
23
  * @property {string} generatedAt ISO 8601
24
24
  * @property {{path: string, name: string, head: string|null, branch: string|null}} repository
25
- * @property {'working'|'lastCommit'|null} mode
25
+ * @property {'working'|'staged'|'lastCommit'|'range'|null} mode
26
26
  * @property {{comments: number, files: number}} summary
27
27
  * @property {AgentComment[]} comments
28
28
  */
@@ -205,6 +205,10 @@ function describeScope({ repository, mode, range }) {
205
205
  return `Reviewed: uncommitted changes in the working tree, against ${repository.branch ?? 'HEAD'}${at}.`;
206
206
  }
207
207
 
208
+ if (mode === 'staged') {
209
+ return 'Comparison: staged changes in the index, against HEAD. Saved comments are shared between review views and may refer to changes outside this comparison; check their anchors before applying them.';
210
+ }
211
+
208
212
  if (mode === 'range' && range?.kind === 'commits') {
209
213
  // Commits themselves, picked from a list. Both ends are *in* the review,
210
214
  // which is the difference from a comparison and worth saying: an agent
package/lib/changes.js CHANGED
@@ -268,7 +268,24 @@ function collectCommitChanges(diffSummary, statuses) {
268
268
  });
269
269
  }
270
270
 
271
+ /**
272
+ * Files in the next commit, including their index status rather than any
273
+ * later working-tree edits. Omitting HEAD also works before the first commit.
274
+ * @param {import('simple-git').SimpleGit} git
275
+ */
276
+ async function collectStagedChanges(git) {
277
+ if (await git.raw(['ls-files', '--unmerged', '-z'])) {
278
+ throw new Error('Resolve merge conflicts before reviewing staged changes.');
279
+ }
280
+ const [summary, raw] = await Promise.all([
281
+ git.diffSummary(['--cached']),
282
+ git.raw(['diff', '--cached', '--name-status', '-z', '-M'])
283
+ ]);
284
+ return collectCommitChanges(summary, parseNameStatus(raw));
285
+ }
286
+
271
287
  module.exports = {
288
+ collectStagedChanges,
272
289
  collectWorkingChanges,
273
290
  collectCommitChanges,
274
291
  classifyCommitFile,
package/lib/cli.js CHANGED
@@ -9,19 +9,22 @@ const path = require('path');
9
9
  * without starting a server or opening a browser.
10
10
  */
11
11
 
12
- const USAGE = `reviewer — review local git changes in your browser
12
+ function usage(name = 'git reviewer') {
13
+ return `${name} — review local git changes in your browser
13
14
 
14
15
  Usage:
15
- reviewer [repository] [options]
16
- reviewer export [repository] [--format json|prompt] [--file <path>]
16
+ ${name} [repository] [options]
17
+ ${name} export [repository] [--format json|prompt] [--file <path>]
17
18
 
18
- Installed under both names: reviewer and git-reviewer. The second name also
19
- makes it reachable as a git subcommand: git reviewer
19
+ One program, two names. git reviewer is the name the package is installed as
20
+ -- git runs any git-<name> on your PATH as a subcommand -- and reviewer is the
21
+ short form. Under git, use -h for this text: git reviewer --help opens the
22
+ manual page instead.
20
23
 
21
24
  Handing a review to an agent:
22
- reviewer | claude -p "Apply this review."
25
+ ${name} | claude -p "Apply this review."
23
26
 
24
- With something reading its output, reviewer writes the finished review there
27
+ With something reading its output, it writes the finished review there
25
28
  the moment you submit it in the browser, and exits. On a terminal there is
26
29
  nothing waiting, so it says where the review was saved and keeps serving.
27
30
  Everything it tells you goes to stderr either way, so only the review is
@@ -40,21 +43,29 @@ Options:
40
43
  -p, --port <n> Port to listen on (default 4500; falls back to a free
41
44
  port if that one is taken)
42
45
  --no-open Print the URL instead of opening a browser
46
+ --staged Review only staged changes (HEAD vs index)
43
47
  -f, --format <f> Export format: json (default) or prompt
44
48
  --file <path> Export comments for one repo-relative file
45
49
  -h, --help Show this help
46
50
  -v, --version Show the version
47
51
 
48
52
  Examples:
49
- reviewer review the repository you are standing in
50
- reviewer | claude -p "Apply this review."
51
- review it, then hand it straight to an agent
52
- reviewer ~/work/api review another repository
53
- reviewer . --no-open print the URL, open it yourself
54
- reviewer export . print the review as JSON
55
- reviewer export . --file src/auth.js
56
- reviewer export . --format prompt | claude -p "apply this review"
53
+ ${name}
54
+ review the repository you are standing in
55
+ ${name} | claude -p "Apply this review."
56
+ review it, then hand it straight to an agent
57
+ ${name} ~/work/api
58
+ review another repository
59
+ ${name} . --no-open
60
+ print the URL, open it yourself
61
+ ${name} export .
62
+ print the review as JSON
63
+ ${name} export . --file src/auth.js
64
+ only the comments on one file
65
+ ${name} export . --format prompt | claude -p "apply this review"
66
+ hand over a review written earlier
57
67
  `;
68
+ }
58
69
 
59
70
  /** Formats `reviewer export` can print. */
60
71
  const FORMATS = ['json', 'prompt'];
@@ -73,6 +84,7 @@ class UsageError extends Error {
73
84
  * @property {string|null} repoPath absolute path, or null when not given
74
85
  * @property {number|null} port explicit port, or null to use the default
75
86
  * @property {boolean} open whether to launch a browser
87
+ * @property {boolean} staged whether to review only the index
76
88
  * @property {'json'|'prompt'} format for `export`
77
89
  * @property {string|null} file repo-relative file to export, or all files
78
90
  * @property {boolean} help
@@ -94,6 +106,7 @@ function parseArgs(argv, cwd = process.cwd()) {
94
106
  repoPath: null,
95
107
  port: null,
96
108
  open: true,
109
+ staged: false,
97
110
  format: 'json',
98
111
  file: null,
99
112
  help: false,
@@ -128,6 +141,10 @@ function parseArgs(argv, cwd = process.cwd()) {
128
141
  options.open = false;
129
142
  break;
130
143
 
144
+ case '--staged':
145
+ options.staged = true;
146
+ break;
147
+
131
148
  case '-p':
132
149
  case '--port': {
133
150
  const value = rest[++i];
@@ -191,6 +208,10 @@ function parseArgs(argv, cwd = process.cwd()) {
191
208
  throw new UsageError('--file is only valid with reviewer export');
192
209
  }
193
210
 
211
+ if (options.staged && options.command !== 'serve') {
212
+ throw new UsageError('--staged is only valid when serving a review');
213
+ }
214
+
194
215
  return options;
195
216
  }
196
217
 
@@ -239,10 +260,11 @@ function parseFile(value, flag) {
239
260
  *
240
261
  * @param {string} origin e.g. http://127.0.0.1:4500
241
262
  * @param {string|null} repoPath
263
+ * @param {boolean} [staged]
242
264
  * @returns {string}
243
265
  */
244
- function buildUrl(origin, repoPath) {
245
- if (!repoPath) return origin;
266
+ function buildUrl(origin, repoPath, staged = false) {
267
+ if (!repoPath) return staged ? `${origin}/?staged=1` : origin;
246
268
 
247
269
  // Slashes are left as slashes. They are legal in a query value -- RFC 3986
248
270
  // has `query = *( pchar / "/" / "?" )` -- and encoding them turns the most
@@ -253,7 +275,29 @@ function buildUrl(origin, repoPath) {
253
275
  //
254
276
  // Everything else stays encoded, including the characters that would
255
277
  // otherwise end the query and take the rest of the path with them.
256
- return `${origin}/?repo=${encodeURIComponent(repoPath).replace(/%2F/g, '/')}`;
278
+ return `${origin}/?repo=${encodeURIComponent(repoPath).replace(/%2F/g, '/')}${staged ? '&staged=1' : ''}`;
279
+ }
280
+
281
+ /**
282
+ * The name to show in help and hints: the one the person actually typed.
283
+ *
284
+ * The package is git-reviewer and installs two commands, reviewer and
285
+ * git-reviewer. Showing one while someone typed the other was the confusing
286
+ * part, so this answers with theirs. Git sets GIT_EXEC_PATH when it runs a
287
+ * subcommand, which is how `git reviewer` is told apart from `git-reviewer`
288
+ * typed directly; npx sets npm_command=exec, and for an npx run neither bare
289
+ * name is on the PATH, so the hint has to say npx too.
290
+ *
291
+ * @param {NodeJS.ProcessEnv} env
292
+ * @param {string|undefined} argv1 process.argv[1]
293
+ */
294
+ function invokedAs(env, argv1) {
295
+ if (env.npm_command === 'exec') return 'npx git-reviewer';
296
+ const bin = String(argv1 || '').split(/[\\/]/).pop().replace(/\.js$/, '');
297
+ if (bin === 'git-reviewer') return env.GIT_EXEC_PATH ? 'git reviewer' : 'git-reviewer';
298
+ return 'reviewer';
257
299
  }
258
300
 
259
- module.exports = { parseArgs, buildUrl, UsageError, USAGE, FORMATS };
301
+ const USAGE = usage();
302
+
303
+ module.exports = { parseArgs, buildUrl, UsageError, USAGE, FORMATS, usage, invokedAs };
package/lib/sessions.js CHANGED
@@ -11,10 +11,11 @@ const crypto = require('crypto');
11
11
  */
12
12
 
13
13
  /**
14
- * @typedef {'working'|'lastCommit'|'range'} ReviewMode
14
+ * @typedef {'working'|'staged'|'lastCommit'|'range'} ReviewMode
15
15
  * `working` diffs the working tree against HEAD; `lastCommit` diffs HEAD
16
16
  * against its parent, which is what a clean tree falls back to; `range`
17
17
  * diffs two refs the reviewer chose.
18
+ * `staged` diffs the index against HEAD without including working-tree edits.
18
19
  */
19
20
 
20
21
  /**
@@ -0,0 +1,84 @@
1
+ .\" Manual page for git-reviewer. git opens this for `git reviewer --help`,
2
+ .\" because it reads --help on any subcommand as a request for its man page.
3
+ .TH GIT-REVIEWER 1 "2026-09-25" "git-reviewer" "User Commands"
4
+ .SH NAME
5
+ git-reviewer, reviewer \- review local git changes in your browser, and hand the review to a coding agent
6
+ .SH SYNOPSIS
7
+ .B git reviewer
8
+ .RI [ repository ]
9
+ .RB [ \-p
10
+ .IR port ]
11
+ .RB [ \-\-no\-open ]
12
+ .br
13
+ .B git reviewer export
14
+ .RI [ repository ]
15
+ .RB [ \-\-format
16
+ .IR json | prompt ]
17
+ .RB [ \-\-file
18
+ .IR path ]
19
+ .SH DESCRIPTION
20
+ Opens the uncommitted changes of a git repository, or its last commit on a
21
+ clean tree, as a pull-request style review in your browser: inline comments
22
+ on any line, threaded follow-ups, and comparisons between branches, tags and
23
+ commits \(em reviewed together, one at a time, or as a run.
24
+ .PP
25
+ With something reading its output, it writes the finished review there the
26
+ moment you submit it, and exits:
27
+ .PP
28
+ .RS
29
+ .B git reviewer | claude \-p "Apply this review."
30
+ .RE
31
+ .PP
32
+ On a terminal it says where the review was saved and keeps serving until
33
+ the tab is closed.
34
+ .PP
35
+ The package is installed as
36
+ .BR git\-reviewer ,
37
+ which git runs as
38
+ .BR "git reviewer" .
39
+ .B reviewer
40
+ is the same program under a shorter name.
41
+ .SH COMMANDS
42
+ .TP
43
+ .B export
44
+ Print the saved review of a repository to stdout, as JSON or as a prompt for
45
+ a coding agent.
46
+ .SH OPTIONS
47
+ .TP
48
+ .BR \-p ", " \-\-port " \fIn\fR"
49
+ Port to listen on. The default is 4500; if it is taken, a free one is used.
50
+ .TP
51
+ .B \-\-no\-open
52
+ Print the address instead of opening a browser.
53
+ .TP
54
+ .B \-\-staged
55
+ Review only staged changes, comparing HEAD with the index. Unstaged edits are
56
+ excluded, and an empty index comparison does not fall back to the last commit.
57
+ .TP
58
+ .BR \-f ", " \-\-format " \fIjson\fR|\fIprompt\fR"
59
+ Export format. The default is json.
60
+ .TP
61
+ .BI \-\-file " path"
62
+ Export only the comments on one repository-relative file.
63
+ .TP
64
+ .BR \-h ", " \-\-help
65
+ Print usage. Under git, use
66
+ .BR \-h :
67
+ git takes
68
+ .B \-\-help
69
+ to mean this page.
70
+ .TP
71
+ .BR \-v ", " \-\-version
72
+ Print the version.
73
+ .SH EXAMPLES
74
+ .TP
75
+ .B git reviewer
76
+ Review the repository you are standing in.
77
+ .TP
78
+ .B git reviewer | claude \-p """Apply this review."""
79
+ Review it, then hand it straight to an agent.
80
+ .TP
81
+ .B git reviewer export . \-\-format prompt | claude \-p """Apply this review."""
82
+ Hand over a review written earlier.
83
+ .SH SEE ALSO
84
+ https://github.com/dheerajjha/reviewer
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-reviewer",
3
- "version": "2.13.0",
3
+ "version": "2.14.0",
4
4
  "description": "Review your coding agent's changes like a pull request — locally, with inline comments and threaded follow-ups, then hand the marked-up review back to the agent",
5
5
  "author": "Dheeraj Jha",
6
6
  "license": "MIT",
@@ -38,7 +38,8 @@
38
38
  "bin/",
39
39
  "lib/",
40
40
  "public/",
41
- "server.js"
41
+ "server.js",
42
+ "man/"
42
43
  ],
43
44
  "scripts": {
44
45
  "start": "node bin/reviewer.js",
@@ -51,5 +52,8 @@
51
52
  "cors": "^2.8.5",
52
53
  "express": "^4.18.2",
53
54
  "simple-git": "^3.20.0"
54
- }
55
+ },
56
+ "man": [
57
+ "./man/git-reviewer.1"
58
+ ]
55
59
  }
package/public/app.js CHANGED
@@ -2,6 +2,7 @@
2
2
  // In web mode, this will be http://localhost:4500
3
3
  // In Electron, this will be the random port Electron assigned
4
4
  const API_BASE = `${window.location.origin}/api`;
5
+ const stagedByDefault = new URLSearchParams(window.location.search).get('staged') === '1';
5
6
 
6
7
  let currentRepoId = null;
7
8
  let currentFiles = [];
@@ -492,10 +493,10 @@ function renderScope(data) {
492
493
  document.getElementById('nextCommitBtn').disabled = true;
493
494
  }
494
495
  } else {
495
- label.append('Uncommitted changes');
496
+ label.append(data.mode === 'staged' ? 'Staged changes' : 'Uncommitted changes');
496
497
  if (branch) label.append(' on ', strong(branch));
497
498
  meta.textContent = plural(data.files.length, 'file') + ' changed';
498
- setNote(null);
499
+ setNote(data.mode === 'staged' ? 'The diff excludes unstaged changes. Saved comments are shared between views and may refer to other code; check their anchors.' : null);
499
500
  }
500
501
 
501
502
  row.classList.add('hidden');
@@ -781,7 +782,7 @@ async function loadRepo(scope = {}) {
781
782
  body: JSON.stringify(
782
783
  scope.commits ? { repoPath, commits: scope.commits }
783
784
  : scope.base ? { repoPath, base: scope.base, ...(scope.head ? { head: scope.head } : {}) }
784
- : { repoPath }
785
+ : { repoPath, staged: stagedByDefault }
785
786
  )
786
787
  });
787
788
 
@@ -817,13 +818,13 @@ async function loadRepo(scope = {}) {
817
818
  // `message`, and the scope bar above is still showing how many commits
818
819
  // that covers -- so this is information, not an error, and it must not
819
820
  // be dressed as one.
820
- if (data.mode === 'range') {
821
+ if (data.mode === 'range' || data.mode === 'staged') {
821
822
  // The scope bar is already explaining this; a status line repeating
822
823
  // it underneath is two bars saying one thing.
823
824
  clearStatus();
824
825
  hidePicker();
825
826
  displayFiles([]);
826
- showEmptyFileList(data.range?.kind !== 'commits'
827
+ showEmptyFileList(data.mode === 'staged' ? 'No staged changes.' : data.range?.kind !== 'commits'
827
828
  ? 'No files differ between these two.'
828
829
  : data.range.first === data.range.last
829
830
  ? 'This commit changes no files.'
package/server.js CHANGED
@@ -12,7 +12,7 @@ const {
12
12
  resolveRepoFile,
13
13
  PathEscapeError
14
14
  } = require('./lib/paths');
15
- const { collectWorkingChanges, collectCommitChanges, parseNameStatus } = require('./lib/changes');
15
+ const { collectWorkingChanges, collectStagedChanges, collectCommitChanges, parseNameStatus } = require('./lib/changes');
16
16
  const { SessionStore } = require('./lib/sessions');
17
17
  const { normalizeComments } = require('./lib/comments');
18
18
  const { formatReview, reviewFilename, commentsFilename } = require('./lib/review');
@@ -137,6 +137,18 @@ function createApp(options = {}) {
137
137
  const git = gitFactory(session.repoPath);
138
138
  const absolutePath = resolveRepoFile(session.repoPath, filePath);
139
139
 
140
+ if (session.mode === 'staged') {
141
+ try {
142
+ // `:0:` names the stage outright. A bare `:` reads a file called
143
+ // `0:foo` as stage 0 of `foo`, and shows the wrong file's text.
144
+ return await git.show([`:0:${filePath}`]);
145
+ } catch {
146
+ // A staged deletion has no index entry. Its old content comes from
147
+ // HEAD, even if a replacement now exists in the working tree.
148
+ return git.show([`HEAD:${filePath}`]).catch(() => { throw new FileNotFoundError(filePath); });
149
+ }
150
+ }
151
+
140
152
  if (session.range) {
141
153
  // A range is two commits, and the working tree is neither of them. It
142
154
  // used to be the fallback here, which was merely odd while the head of
@@ -222,6 +234,9 @@ function createApp(options = {}) {
222
234
  }
223
235
 
224
236
  async function diffForFile(git, session, filePath) {
237
+ if (session.mode === 'staged') {
238
+ return git.diff(['--cached', '--', filePath]);
239
+ }
225
240
  if (session.range) {
226
241
  return git.diff([session.range.from, session.range.head, '--', filePath]);
227
242
  }
@@ -768,7 +783,10 @@ function createApp(options = {}) {
768
783
  // Resolve to the root of the working tree before anything is keyed on
769
784
  // this path. A subdirectory passes `checkIsRepo()` quite happily and
770
785
  // then produces a file list whose diffs are all empty; see lib/repo.js.
771
- const { base, head, commits: run } = req.body ?? {};
786
+ const { base, head, commits: run, staged = false } = req.body ?? {};
787
+ if (typeof staged !== 'boolean' || (staged && (base != null || head != null || run != null))) {
788
+ return res.status(400).json({ error: 'Choose staged changes or a commit comparison, not both.' });
789
+ }
772
790
  const root = await repoRoot(repoPath, gitFactory);
773
791
  if (!root) {
774
792
  return res.status(400).json({ error: 'Not a valid git repository' });
@@ -781,7 +799,15 @@ function createApp(options = {}) {
781
799
  let range = null;
782
800
  let message;
783
801
 
784
- if (run && typeof run === 'object') {
802
+ if (staged) {
803
+ try {
804
+ files = await collectStagedChanges(git);
805
+ } catch (error) {
806
+ return res.status(400).json({ error: error.message });
807
+ }
808
+ mode = 'staged';
809
+ message = files.length ? `Found ${files.length} staged file(s)` : 'No staged changes';
810
+ } else if (run && typeof run === 'object') {
785
811
  // Commits themselves, one or a run: see resolveCommitRun.
786
812
  try {
787
813
  range = await resolveCommitRun(git, `${run.from ?? ''}`.trim(), `${run.to ?? run.from ?? ''}`.trim());