git-reviewer 2.11.0 → 2.12.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,7 +3,7 @@
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-314-brightgreen.svg)](test/)
6
+ [![Tests](https://img.shields.io/badge/tests-335-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
@@ -141,34 +141,46 @@ 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
- ### Reviewing further back than the last commit
144
+ ### Comparing branches
145
145
 
146
- Those defaults cover the common case and nothing else needs setting up, but a
147
- branch you have been working on for a day is several commits deep, and the
148
- default scope only ever shows you the most recent one.
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
- **Changes since** in the header takes any earlier commit and reloads the review
151
- as a single combined diff up to `HEAD`. Pick one from the dropdown — recent
152
- commits by subject line and when they landed, so you do not have to go and find
153
- a SHA — or type a branch, a tag, or something like `HEAD~5` in the box beside
154
- it for anything the list does not reach.
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
- The commit the range starts from is not itself included: "changes since x1"
157
- means everything after x1, the same as `x1..HEAD`.
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
- The bar keeps saying how many commits the range covers, and it keeps saying it
160
- when there are no files:
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
- Comparing 86b28f11 → 27668114 — 2 commits, shown as one combined diff
164
- 2 commits, and no net change between the two ends — every change in them was
165
- undone again inside the range.
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
- That is a real answer, not an empty screen. A file added in one commit and
169
- deleted in a later one is correctly absent from a diff of the two ends, and
170
- "no net change across 2 commits" is a different statement from "nothing to
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
+ Comments are kept when you switch between comparisons. One on a file that is
182
+ not in the current view stays in the comments list, marked **Not in this
183
+ view**, and is back in place whenever you open a view that includes the file.
172
184
 
173
185
  Modified, added, deleted, renamed, and binary files are all listed, each marked
174
186
  with its git status letter. Deleted lines are commentable too — the most useful
@@ -293,7 +305,10 @@ The UI is a client of this; nothing is hidden from you.
293
305
  | Endpoint | Purpose |
294
306
  |---|---|
295
307
  | `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 |
308
+ | `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 |
309
+ | `GET /api/refs/:repoId` | Branches, remote branches, tags and recent commits, for choosing what to compare |
310
+ | `GET /api/commits/:repoId` | Recent commits only; kept for callers of 2.11 |
311
+ | `GET /api/alive` | Held open by the page; when the last one closes, a terminal run stops |
297
312
  | `GET /api/file/:repoId/:path` | The file's diff, as structured lines |
298
313
  | `GET /api/file-full/:repoId/:path` | Every line of the file, for context |
299
314
  | `POST /api/save-comments` | Persist the current comments |
@@ -323,7 +338,7 @@ rather than a public issue.
323
338
 
324
339
  ```bash
325
340
  npm install # 3 dependencies, no build step, ~6MB
326
- npm test # 314 tests
341
+ npm test # 335 tests
327
342
  npm run test:watch
328
343
  npm run test:coverage
329
344
  ```
package/lib/agent.js CHANGED
@@ -210,8 +210,28 @@ function describeScope({ repository, mode, range }) {
210
210
  // distinguishes "one commit of work" from "nine" once the diff has been
211
211
  // flattened -- and an agent reading a flattened diff of nine commits
212
212
  // should know that is what it is holding.
213
- const commits = `${range.commits} commit${range.commits === 1 ? '' : 's'}`;
214
- return `Reviewed: everything between ${range.base} and ${range.head} (${commits}), as a single combined diff.`;
213
+ const plural = (n, word) => `${n} ${word}${n === 1 ? '' : 's'}`;
214
+ const commits = plural(range.commits, 'commit');
215
+
216
+ // Ranges saved before 2.12 carry neither names nor a merge base. Say what
217
+ // they can support and no more.
218
+ if (!range.from || !range.baseName || !range.headName) {
219
+ return `Reviewed: everything between ${range.base} and ${range.head} (${commits}), as a single combined diff.`;
220
+ }
221
+
222
+ const ends = `${range.baseName} \u2192 ${range.headName}`;
223
+ if (range.from === range.base) {
224
+ return `Reviewed: ${ends} (${range.base}..${range.head}), ${commits} as a single combined diff.`;
225
+ }
226
+
227
+ // Diverged. The agent needs the left-out half as much as the reviewed
228
+ // half: code on the base that differs from the head is not something the
229
+ // reviewer asked anyone to change, and an agent that "fixes" it has
230
+ // reverted someone else's work.
231
+ const behind = range.behind > 0
232
+ ? ` ${range.baseName} has ${plural(range.behind, 'newer commit')} that ${range.behind === 1 ? 'is' : 'are'} deliberately not part of this review; leave that code alone.`
233
+ : '';
234
+ 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
235
  }
216
236
 
217
237
  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
- function collectCommitChanges(diffSummary) {
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
- const status = isRename ? 'R' : classifyCommitFile(file);
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.11.0",
3
+ "version": "2.12.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",