git-reviewer 2.10.0 → 2.11.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 +31 -2
- package/package.json +1 -1
- package/public/app.js +63 -1
- package/public/index.html +8 -2
- package/public/style.css +29 -0
- package/server.js +58 -0
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,6 +141,35 @@ 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
|
|
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.
|
|
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.
|
|
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`.
|
|
158
|
+
|
|
159
|
+
The bar keeps saying how many commits the range covers, and it keeps saying it
|
|
160
|
+
when there are no files:
|
|
161
|
+
|
|
162
|
+
```
|
|
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.
|
|
166
|
+
```
|
|
167
|
+
|
|
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".
|
|
172
|
+
|
|
144
173
|
Modified, added, deleted, renamed, and binary files are all listed, each marked
|
|
145
174
|
with its git status letter. Deleted lines are commentable too — the most useful
|
|
146
175
|
review note is often about the code someone removed. A file deleted outright is
|
|
@@ -294,7 +323,7 @@ rather than a public issue.
|
|
|
294
323
|
|
|
295
324
|
```bash
|
|
296
325
|
npm install # 3 dependencies, no build step, ~6MB
|
|
297
|
-
npm test #
|
|
326
|
+
npm test # 314 tests
|
|
298
327
|
npm run test:watch
|
|
299
328
|
npm run test:coverage
|
|
300
329
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "git-reviewer",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.11.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",
|
package/public/app.js
CHANGED
|
@@ -156,19 +156,77 @@ function renderScope(data) {
|
|
|
156
156
|
bar.classList.remove('hidden');
|
|
157
157
|
reset.classList.toggle('hidden', data.mode !== 'range');
|
|
158
158
|
|
|
159
|
+
const select = document.getElementById('compareSelect');
|
|
160
|
+
|
|
159
161
|
if (data.mode === 'range' && data.range) {
|
|
160
162
|
const { base, head, commits } = data.range;
|
|
161
163
|
label.textContent =
|
|
162
164
|
`Comparing ${shortSha(base)} \u2192 ${shortSha(head)} \u2014 ` +
|
|
163
165
|
`${commits} commit${commits === 1 ? '' : 's'}, shown as one combined diff`;
|
|
164
|
-
|
|
166
|
+
// Only if the base is one of the offered commits. A range opened by
|
|
167
|
+
// typing `HEAD~5` or a branch name has no matching option, and leaving
|
|
168
|
+
// the picker on its placeholder is honest -- the scope bar above already
|
|
169
|
+
// says exactly what is being compared.
|
|
170
|
+
select.value = [...select.options].some(option => option.value === base) ? base : '';
|
|
165
171
|
return;
|
|
166
172
|
}
|
|
167
173
|
|
|
168
174
|
label.textContent = data.mode === 'lastCommit'
|
|
169
175
|
? 'Reviewing the last commit'
|
|
170
176
|
: 'Reviewing uncommitted changes';
|
|
177
|
+
select.value = '';
|
|
178
|
+
document.getElementById('compareBase').value = '';
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Fill the commit picker for the open repository.
|
|
183
|
+
*
|
|
184
|
+
* The newest commit is deliberately not offered. "Changes since HEAD" is
|
|
185
|
+
* either nothing or the working tree depending on how you squint, and both
|
|
186
|
+
* of those are already what "Back to latest" gives you -- so listing it
|
|
187
|
+
* would be offering a choice that cannot do anything useful.
|
|
188
|
+
*
|
|
189
|
+
* A failure here is silent on purpose: the text box next to it still works,
|
|
190
|
+
* and a repository whose history will not list is not a reason to refuse to
|
|
191
|
+
* review it.
|
|
192
|
+
*/
|
|
193
|
+
async function loadCommitOptions(repoId) {
|
|
194
|
+
const select = document.getElementById('compareSelect');
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
const response = await fetch(`${API_BASE}/commits/${repoId}`);
|
|
198
|
+
if (!response.ok) return;
|
|
199
|
+
|
|
200
|
+
const { commits } = await response.json();
|
|
201
|
+
const options = commits.slice(1);
|
|
202
|
+
|
|
203
|
+
select.innerHTML = '';
|
|
204
|
+
if (options.length === 0) {
|
|
205
|
+
select.appendChild(new Option('No earlier commits', ''));
|
|
206
|
+
select.disabled = true;
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
select.disabled = false;
|
|
211
|
+
select.appendChild(new Option('Pick a commit…', ''));
|
|
212
|
+
for (const commit of options) {
|
|
213
|
+
// The subject is what a person recognises; the sha is there to
|
|
214
|
+
// disambiguate two commits with the same message, which is common
|
|
215
|
+
// enough in a branch that has been rebased.
|
|
216
|
+
select.appendChild(new Option(`${commit.short} · ${commit.subject} · ${commit.when}`, commit.sha));
|
|
217
|
+
}
|
|
218
|
+
} catch {
|
|
219
|
+
/* the text box beside it is the fallback */
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Compare from whatever was chosen in the picker. */
|
|
224
|
+
function compareFromSelection() {
|
|
225
|
+
const base = document.getElementById('compareSelect').value;
|
|
226
|
+
if (!base) return;
|
|
227
|
+
|
|
171
228
|
document.getElementById('compareBase').value = '';
|
|
229
|
+
loadRepo({ base });
|
|
172
230
|
}
|
|
173
231
|
|
|
174
232
|
/** Reload the repository as a range starting at whatever was typed. */
|
|
@@ -186,6 +244,7 @@ function compareFrom() {
|
|
|
186
244
|
/** Back to the default scope: uncommitted changes, or the last commit. */
|
|
187
245
|
function resetScope() {
|
|
188
246
|
document.getElementById('compareBase').value = '';
|
|
247
|
+
document.getElementById('compareSelect').value = '';
|
|
189
248
|
loadRepo();
|
|
190
249
|
}
|
|
191
250
|
|
|
@@ -230,6 +289,9 @@ async function loadRepo(scope = {}) {
|
|
|
230
289
|
setRepoButton(data.repoPath);
|
|
231
290
|
}
|
|
232
291
|
|
|
292
|
+
// Before renderScope, which needs the options present to be able to
|
|
293
|
+
// select the current base among them.
|
|
294
|
+
await loadCommitOptions(data.repoId);
|
|
233
295
|
renderScope(data);
|
|
234
296
|
|
|
235
297
|
if (data.files.length === 0) {
|
package/public/index.html
CHANGED
|
@@ -58,11 +58,17 @@
|
|
|
58
58
|
<div id="scopeBar" class="scope-bar hidden">
|
|
59
59
|
<span id="scopeLabel" class="scope-label"></span>
|
|
60
60
|
<div class="scope-compare">
|
|
61
|
-
|
|
61
|
+
<!-- "since", not "from": `base..HEAD` does not include base's own
|
|
62
|
+
changes, and a picker that says "compare from x1" invites the
|
|
63
|
+
reading that x1 is in the diff. -->
|
|
64
|
+
<label for="compareSelect">Changes since</label>
|
|
65
|
+
<select id="compareSelect" onchange="compareFromSelection()" aria-label="Review everything after this commit">
|
|
66
|
+
<option value="">Pick a commit…</option>
|
|
67
|
+
</select>
|
|
62
68
|
<input
|
|
63
69
|
type="text"
|
|
64
70
|
id="compareBase"
|
|
65
|
-
placeholder="
|
|
71
|
+
placeholder="or a branch, tag or ref"
|
|
66
72
|
spellcheck="false"
|
|
67
73
|
autocomplete="off"
|
|
68
74
|
value="">
|
package/public/style.css
CHANGED
|
@@ -1935,3 +1935,32 @@ button:disabled {
|
|
|
1935
1935
|
width: 120px;
|
|
1936
1936
|
}
|
|
1937
1937
|
}
|
|
1938
|
+
|
|
1939
|
+
/* The picker carries the common case; the text box beside it carries
|
|
1940
|
+
* everything else — a branch, a tag, `HEAD~5`, a sha from somewhere else.
|
|
1941
|
+
* Both stay visible rather than one hiding behind a mode, because a control
|
|
1942
|
+
* you cannot see is a feature people believe is missing. */
|
|
1943
|
+
.scope-compare select {
|
|
1944
|
+
max-width: 320px;
|
|
1945
|
+
font-size: 12px;
|
|
1946
|
+
padding: 4px 6px;
|
|
1947
|
+
border: 1px solid #d1d5da;
|
|
1948
|
+
border-radius: 6px;
|
|
1949
|
+
background: #fff;
|
|
1950
|
+
color: #24292e;
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
.scope-compare select:disabled {
|
|
1954
|
+
color: #6a737d;
|
|
1955
|
+
background: #f3f4f6;
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
@media (max-width: 720px) {
|
|
1959
|
+
.scope-compare {
|
|
1960
|
+
flex-wrap: wrap;
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
.scope-compare select {
|
|
1964
|
+
max-width: 100%;
|
|
1965
|
+
}
|
|
1966
|
+
}
|
package/server.js
CHANGED
|
@@ -62,6 +62,12 @@ class FileNotFoundError extends Error {
|
|
|
62
62
|
* leaves before deciding it is not coming back.
|
|
63
63
|
* @returns {import('express').Express}
|
|
64
64
|
*/
|
|
65
|
+
// How far back the range picker offers to go. Long enough to cover the work
|
|
66
|
+
// someone is plausibly still reviewing, short enough that the list is a list
|
|
67
|
+
// rather than the whole history -- anything older is still reachable by
|
|
68
|
+
// typing a ref, which is what the text box is for.
|
|
69
|
+
const COMMIT_LIST_LIMIT = 30;
|
|
70
|
+
|
|
65
71
|
function createApp(options = {}) {
|
|
66
72
|
const {
|
|
67
73
|
reviewsDir = resolveReviewsDir(),
|
|
@@ -293,6 +299,58 @@ function createApp(options = {}) {
|
|
|
293
299
|
return `${commits}, ${fileCount} changed file${fileCount === 1 ? '' : 's'} between the two ends`;
|
|
294
300
|
}
|
|
295
301
|
|
|
302
|
+
/**
|
|
303
|
+
* Recent commits, so a range can be picked instead of typed.
|
|
304
|
+
*
|
|
305
|
+
* Nobody knows the SHA of the commit they want to review back to, and
|
|
306
|
+
* asking for one is asking the reviewer to go and run `git log` in another
|
|
307
|
+
* window to use a tool whose whole point is not having to.
|
|
308
|
+
*
|
|
309
|
+
* Each entry is what a person recognises a commit by -- the subject line
|
|
310
|
+
* and when it landed -- with the SHA carried alongside rather than being
|
|
311
|
+
* the thing they have to read.
|
|
312
|
+
*
|
|
313
|
+
* `%x00` separates the fields because a commit subject can contain
|
|
314
|
+
* anything a person can type, including whatever delimiter looked safe.
|
|
315
|
+
*
|
|
316
|
+
* @param {import('simple-git').SimpleGit} git
|
|
317
|
+
* @param {number} limit
|
|
318
|
+
*/
|
|
319
|
+
async function recentCommits(git, limit) {
|
|
320
|
+
const raw = await git.raw([
|
|
321
|
+
'log', `--max-count=${limit}`, '--format=%H%x00%h%x00%s%x00%cr'
|
|
322
|
+
]);
|
|
323
|
+
|
|
324
|
+
return raw
|
|
325
|
+
.split('\n')
|
|
326
|
+
.filter(line => line.trim() !== '')
|
|
327
|
+
.map(line => {
|
|
328
|
+
const [sha, short, subject, when] = line.split('\u0000');
|
|
329
|
+
return { sha, short, subject, when };
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* The commits of an open repository, newest first.
|
|
335
|
+
*
|
|
336
|
+
* Behind a session id like the file endpoints rather than taking a path:
|
|
337
|
+
* this reads history out of a repository, and the session is what says
|
|
338
|
+
* which repository the caller has already been granted.
|
|
339
|
+
*/
|
|
340
|
+
app.get('/api/commits/:repoId', async (req, res) => {
|
|
341
|
+
try {
|
|
342
|
+
const session = requireSession(res, req.params.repoId);
|
|
343
|
+
if (!session) return;
|
|
344
|
+
|
|
345
|
+
const commits = await recentCommits(gitFactory(session.repoPath), COMMIT_LIST_LIMIT);
|
|
346
|
+
res.json({ commits });
|
|
347
|
+
} catch (error) {
|
|
348
|
+
// A repository with no commits is not an error, it is an empty list.
|
|
349
|
+
console.error('Commit list error:', error.message);
|
|
350
|
+
res.json({ commits: [] });
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
|
|
296
354
|
/** Liveness probe, and a count of how many repositories are open. */
|
|
297
355
|
app.get('/api/health', (req, res) => {
|
|
298
356
|
res.json({ status: 'ok', sessions: sessions.size });
|