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 +53 -26
- package/bin/reviewer.js +9 -5
- package/lib/agent.js +5 -1
- package/lib/changes.js +17 -0
- package/lib/cli.js +63 -19
- package/lib/sessions.js +2 -1
- package/man/git-reviewer.1 +84 -0
- package/package.json +7 -3
- package/public/app.js +6 -5
- package/server.js +29 -3
package/README.md
CHANGED
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
[](https://github.com/dheerajjha/reviewer/actions/workflows/ci.yml)
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
[](package.json)
|
|
6
|
-
[](test/)
|
|
7
7
|
[](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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
83
|
-
reviewer ~/work/api
|
|
84
|
-
reviewer . --no-open
|
|
85
|
-
reviewer . --port 8080
|
|
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`
|
|
117
|
-
|
|
118
|
-
|
|
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
|
|
122
|
-
|
|
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
|
-
|
|
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 #
|
|
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
|
|
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) —
|
|
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,
|
|
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(
|
|
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(`
|
|
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(
|
|
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
|
-
|
|
12
|
+
function usage(name = 'git reviewer') {
|
|
13
|
+
return `${name} — review local git changes in your browser
|
|
13
14
|
|
|
14
15
|
Usage:
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
${name} [repository] [options]
|
|
17
|
+
${name} export [repository] [--format json|prompt] [--file <path>]
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
25
|
+
${name} | claude -p "Apply this review."
|
|
23
26
|
|
|
24
|
-
With something reading its output,
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
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.
|
|
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 (
|
|
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());
|