git-reviewer 2.11.0 → 2.13.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 +62 -23
- package/lib/agent.js +33 -2
- package/lib/changes.js +73 -4
- package/package.json +1 -1
- package/public/app.js +698 -88
- package/public/index.html +59 -22
- package/public/style.css +299 -47
- package/server.js +325 -30
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
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
|
|
@@ -141,34 +141,69 @@ is nothing to configure:
|
|
|
141
141
|
| Working directory clean | The last commit against its parent |
|
|
142
142
|
| No commits yet | Every tracked and untracked file, as wholly new |
|
|
143
143
|
|
|
144
|
-
###
|
|
144
|
+
### Comparing branches and commits
|
|
145
145
|
|
|
146
|
-
Those defaults cover the common case
|
|
147
|
-
|
|
148
|
-
|
|
146
|
+
Those defaults cover the common case with nothing to set up, but a branch you
|
|
147
|
+
have worked on for a day is several commits deep, and the default scope only
|
|
148
|
+
ever shows the most recent one.
|
|
149
149
|
|
|
150
|
-
**
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
150
|
+
**Compare branches** in the header opens the comparison your pull request would
|
|
151
|
+
show. If the repository has an obvious base — `origin/main`, then `main`, then
|
|
152
|
+
`master` or `develop` — it compares your current branch against it straight
|
|
153
|
+
away; otherwise it asks. Remote branches come first because a local `main` that
|
|
154
|
+
has not been pulled in a week is a quietly wrong base.
|
|
155
155
|
|
|
156
|
-
|
|
157
|
-
|
|
156
|
+
Both ends are pickers: **Base** and **Compare**, the same two words a pull
|
|
157
|
+
request uses. Each lists branches, remote branches, tags and recent commits, so
|
|
158
|
+
you do not have to go and find a SHA. **Other…** at the bottom of either takes
|
|
159
|
+
anything else git understands — `HEAD~5`, a tag, a SHA from somewhere else. The
|
|
160
|
+
⇄ button swaps them, which answers the other question: what has `main` got that
|
|
161
|
+
my branch has not?
|
|
158
162
|
|
|
159
|
-
|
|
160
|
-
|
|
163
|
+
**It shows what the branch changes, not everything that differs.** If `main`
|
|
164
|
+
has moved on since your branch was cut, a plain two-ended diff would include
|
|
165
|
+
`main`'s newer work *reversed*, as though your branch had undone it. The
|
|
166
|
+
comparison starts from where the two diverged instead, exactly as a pull
|
|
167
|
+
request does, and says what it left out:
|
|
161
168
|
|
|
162
169
|
```
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
170
|
+
main → feature/auth 3 commits · 2 files changed
|
|
171
|
+
|
|
172
|
+
main has 1 newer commit that is not on feature/auth. It is left out: this shows
|
|
173
|
+
only what feature/auth changes since the two diverged, as a pull request would.
|
|
166
174
|
```
|
|
167
175
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
review"
|
|
176
|
+
An empty comparison explains itself rather than showing a blank list. Two
|
|
177
|
+
commits that add a file and then delete it produce no diff between the ends —
|
|
178
|
+
correctly — and that reads as *"2 commits undo each other"*, not as *"nothing
|
|
179
|
+
to review"*.
|
|
180
|
+
|
|
181
|
+
#### Reviewing commit by commit
|
|
182
|
+
|
|
183
|
+
Whenever the review is made of commits, the sidebar lists them above the files:
|
|
184
|
+
the commits a comparison is made of, oldest first, or the branch's recent
|
|
185
|
+
history when you are looking at its last commit.
|
|
186
|
+
|
|
187
|
+
- **Click** a commit to review it on its own, with its full message above the
|
|
188
|
+
diff — often the only place the author says *why*.
|
|
189
|
+
- **Shift-click** a second to review the run between them, both ends included.
|
|
190
|
+
From the last commit, shift-clicking three rows down is "the last three
|
|
191
|
+
commits"; inside a comparison, it skips the ones you have already read.
|
|
192
|
+
- **`[`** and **`]`** step one commit older or newer, and **All commits** goes
|
|
193
|
+
back to the whole comparison.
|
|
194
|
+
|
|
195
|
+
That is the same gesture a pull request's commit picker uses, for the same
|
|
196
|
+
reasons: a large change reads better in the order it was written, and one
|
|
197
|
+
commit of renames is easier to skip than to read around.
|
|
198
|
+
|
|
199
|
+
A run has to be one unbroken line of history. To diff two points that are not
|
|
200
|
+
— a commit on one branch against another branch — use **Compare**; its pickers
|
|
201
|
+
list recent commits of whichever branch you are comparing, right after the
|
|
202
|
+
local branches.
|
|
203
|
+
|
|
204
|
+
Comments are kept when you switch between comparisons. One on a file that is
|
|
205
|
+
not in the current view stays in the comments list, marked **Not in this
|
|
206
|
+
view**, and is back in place whenever you open a view that includes the file.
|
|
172
207
|
|
|
173
208
|
Modified, added, deleted, renamed, and binary files are all listed, each marked
|
|
174
209
|
with its git status letter. Deleted lines are commentable too — the most useful
|
|
@@ -293,7 +328,11 @@ The UI is a client of this; nothing is hidden from you.
|
|
|
293
328
|
| Endpoint | Purpose |
|
|
294
329
|
|---|---|
|
|
295
330
|
| `GET /api/health` | Liveness, plus the number of open sessions |
|
|
296
|
-
| `POST /api/load-repo` | Open a repository; returns a `repoId` and the changed files |
|
|
331
|
+
| `POST /api/load-repo` | Open a repository; returns a `repoId` and the changed files. Pass `base` (and optionally `head`) to compare two refs from their merge base, or `commits: { from, to }` to review one commit or an unbroken run of them, both ends included |
|
|
332
|
+
| `GET /api/refs/:repoId` | Branches, remote branches, tags and recent commits, for choosing what to compare. `?head=` makes the commits those of another branch |
|
|
333
|
+
| `GET /api/log/:repoId` | Commits with their messages: `?base=&head=` for a comparison's commits, oldest first, or `?head=` alone for recent history, newest first |
|
|
334
|
+
| `GET /api/commits/:repoId` | Recent commits only; kept for callers of 2.11 |
|
|
335
|
+
| `GET /api/alive` | Held open by the page; when the last one closes, a terminal run stops |
|
|
297
336
|
| `GET /api/file/:repoId/:path` | The file's diff, as structured lines |
|
|
298
337
|
| `GET /api/file-full/:repoId/:path` | Every line of the file, for context |
|
|
299
338
|
| `POST /api/save-comments` | Persist the current comments |
|
|
@@ -323,7 +362,7 @@ rather than a public issue.
|
|
|
323
362
|
|
|
324
363
|
```bash
|
|
325
364
|
npm install # 3 dependencies, no build step, ~6MB
|
|
326
|
-
npm test #
|
|
365
|
+
npm test # 355 tests
|
|
327
366
|
npm run test:watch
|
|
328
367
|
npm run test:coverage
|
|
329
368
|
```
|
package/lib/agent.js
CHANGED
|
@@ -205,13 +205,44 @@ 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 === 'range' && range?.kind === 'commits') {
|
|
209
|
+
// Commits themselves, picked from a list. Both ends are *in* the review,
|
|
210
|
+
// which is the difference from a comparison and worth saying: an agent
|
|
211
|
+
// told "between A and B" would reasonably assume A's own changes were
|
|
212
|
+
// outside it.
|
|
213
|
+
if (range.first === range.last) {
|
|
214
|
+
return `Reviewed: commit ${range.last}${range.subject ? ` ("${range.subject}")` : ''} on its own, against its parent.`;
|
|
215
|
+
}
|
|
216
|
+
return `Reviewed: ${range.commits} commits, ${range.first} through ${range.last} inclusive, as a single combined diff.`;
|
|
217
|
+
}
|
|
218
|
+
|
|
208
219
|
if (mode === 'range' && range) {
|
|
209
220
|
// Both ends, and the count, because the count is the only thing that
|
|
210
221
|
// distinguishes "one commit of work" from "nine" once the diff has been
|
|
211
222
|
// flattened -- and an agent reading a flattened diff of nine commits
|
|
212
223
|
// should know that is what it is holding.
|
|
213
|
-
const
|
|
214
|
-
|
|
224
|
+
const plural = (n, word) => `${n} ${word}${n === 1 ? '' : 's'}`;
|
|
225
|
+
const commits = plural(range.commits, 'commit');
|
|
226
|
+
|
|
227
|
+
// Ranges saved before 2.12 carry neither names nor a merge base. Say what
|
|
228
|
+
// they can support and no more.
|
|
229
|
+
if (!range.from || !range.baseName || !range.headName) {
|
|
230
|
+
return `Reviewed: everything between ${range.base} and ${range.head} (${commits}), as a single combined diff.`;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const ends = `${range.baseName} \u2192 ${range.headName}`;
|
|
234
|
+
if (range.from === range.base) {
|
|
235
|
+
return `Reviewed: ${ends} (${range.base}..${range.head}), ${commits} as a single combined diff.`;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Diverged. The agent needs the left-out half as much as the reviewed
|
|
239
|
+
// half: code on the base that differs from the head is not something the
|
|
240
|
+
// reviewer asked anyone to change, and an agent that "fixes" it has
|
|
241
|
+
// reverted someone else's work.
|
|
242
|
+
const behind = range.behind > 0
|
|
243
|
+
? ` ${range.baseName} has ${plural(range.behind, 'newer commit')} that ${range.behind === 1 ? 'is' : 'are'} deliberately not part of this review; leave that code alone.`
|
|
244
|
+
: '';
|
|
245
|
+
return `Reviewed: what ${range.headName} changes since it diverged from ${range.baseName} (merge base ${range.from}, head ${range.head}), ${commits} as a single combined diff.${behind}`;
|
|
215
246
|
}
|
|
216
247
|
|
|
217
248
|
if (mode === 'lastCommit') {
|
package/lib/changes.js
CHANGED
|
@@ -191,12 +191,80 @@ function classifyCommitFile(file) {
|
|
|
191
191
|
* @param {import('simple-git').DiffResult} diffSummary
|
|
192
192
|
* @returns {ChangedFile[]}
|
|
193
193
|
*/
|
|
194
|
-
|
|
194
|
+
/**
|
|
195
|
+
* Read `git diff --name-status -z` into what git itself says happened to
|
|
196
|
+
* each path.
|
|
197
|
+
*
|
|
198
|
+
* This exists because `classifyCommitFile` above has to guess from line
|
|
199
|
+
* counts, and the guess is wrong for a large share of ordinary edits: a
|
|
200
|
+
* change that only adds lines to an existing file reads as `A`, and one that
|
|
201
|
+
* only removes lines reads as `D` -- a reviewer shown "deleted" on a file
|
|
202
|
+
* that lost one line has been told something false by the badge meant to
|
|
203
|
+
* orient them. Git knows the answer; ask it.
|
|
204
|
+
*
|
|
205
|
+
* `-z` rather than the default output because the default quotes and
|
|
206
|
+
* octal-escapes non-ASCII paths the same way `--stat` does, and those were
|
|
207
|
+
* #20's bug the first time. With `-z` every field is NUL-terminated and the
|
|
208
|
+
* path is raw bytes, so there is nothing to unquote.
|
|
209
|
+
*
|
|
210
|
+
* Renames and copies (`R100`, `C75`) carry two paths; the second is the one
|
|
211
|
+
* the file now lives at, which is the one the rest of the app keys on.
|
|
212
|
+
*
|
|
213
|
+
* @param {string} raw output of `git diff --name-status -z -M <from> <to>`
|
|
214
|
+
* @returns {Map<string, 'A'|'M'|'D'|'R'>} path -> status
|
|
215
|
+
*/
|
|
216
|
+
function parseNameStatus(raw) {
|
|
217
|
+
const statuses = new Map();
|
|
218
|
+
const fields = String(raw ?? '').split('\0');
|
|
219
|
+
|
|
220
|
+
for (let i = 0; i < fields.length; ) {
|
|
221
|
+
const code = fields[i];
|
|
222
|
+
if (!code) { i += 1; continue; }
|
|
223
|
+
|
|
224
|
+
const letter = code[0];
|
|
225
|
+
if (letter === 'R' || letter === 'C') {
|
|
226
|
+
const to = fields[i + 2];
|
|
227
|
+
// A copy is a new file that happens to resemble an old one; from the
|
|
228
|
+
// reviewer's side it is an addition.
|
|
229
|
+
if (to) statuses.set(to, letter === 'R' ? 'R' : 'A');
|
|
230
|
+
i += 3;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const path = fields[i + 1];
|
|
235
|
+
if (path) {
|
|
236
|
+
// T is a type change (a file became a symlink, or the reverse). The
|
|
237
|
+
// path still exists on both sides, so for review purposes it is a
|
|
238
|
+
// modification.
|
|
239
|
+
statuses.set(path, letter === 'A' || letter === 'D' ? letter : 'M');
|
|
240
|
+
}
|
|
241
|
+
i += 2;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return statuses;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* The flat file list for a commit or range review.
|
|
249
|
+
*
|
|
250
|
+
* Paths still come from `diffSummary`, whose rename and quoting handling was
|
|
251
|
+
* earned the hard way (#20, #25, #64). Only the status letter comes from
|
|
252
|
+
* `statuses` when it is given -- which every caller in the server does. The
|
|
253
|
+
* count-based guess remains as the fallback for a caller that has nothing
|
|
254
|
+
* better, not as the answer.
|
|
255
|
+
*
|
|
256
|
+
* @param {import('simple-git').DiffResult} diffSummary
|
|
257
|
+
* @param {Map<string, string>} [statuses] from `parseNameStatus`
|
|
258
|
+
*/
|
|
259
|
+
function collectCommitChanges(diffSummary, statuses) {
|
|
195
260
|
const files = diffSummary?.files ?? [];
|
|
196
261
|
return files.map(file => {
|
|
197
262
|
const { path, isRename } = parseCommitFilePath(file?.file);
|
|
198
|
-
|
|
199
|
-
return { path, status };
|
|
263
|
+
if (isRename) return { path, status: 'R' };
|
|
264
|
+
if (file?.binary) return { path, status: 'B' };
|
|
265
|
+
|
|
266
|
+
const known = statuses?.get(path);
|
|
267
|
+
return { path, status: known ?? classifyCommitFile(file) };
|
|
200
268
|
});
|
|
201
269
|
}
|
|
202
270
|
|
|
@@ -205,5 +273,6 @@ module.exports = {
|
|
|
205
273
|
collectCommitChanges,
|
|
206
274
|
classifyCommitFile,
|
|
207
275
|
unquoteGitPath,
|
|
208
|
-
parseCommitFilePath
|
|
276
|
+
parseCommitFilePath,
|
|
277
|
+
parseNameStatus
|
|
209
278
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "git-reviewer",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.13.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",
|