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/server.js
CHANGED
|
@@ -12,7 +12,7 @@ const {
|
|
|
12
12
|
resolveRepoFile,
|
|
13
13
|
PathEscapeError
|
|
14
14
|
} = require('./lib/paths');
|
|
15
|
-
const { collectWorkingChanges, collectCommitChanges } = require('./lib/changes');
|
|
15
|
+
const { collectWorkingChanges, 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');
|
|
@@ -68,6 +68,16 @@ class FileNotFoundError extends Error {
|
|
|
68
68
|
// typing a ref, which is what the text box is for.
|
|
69
69
|
const COMMIT_LIST_LIMIT = 30;
|
|
70
70
|
|
|
71
|
+
// Branches and tags offered per group. A repository with two hundred stale
|
|
72
|
+
// branches should still produce a list someone can scan; the ones past the
|
|
73
|
+
// cut are the least recently touched and are still reachable by typing.
|
|
74
|
+
const REF_LIST_LIMIT = 40;
|
|
75
|
+
|
|
76
|
+
// How many commits the commits list shows. A branch of more than this is a
|
|
77
|
+
// review nobody is doing commit by commit; the list says it is truncated
|
|
78
|
+
// rather than pretending the rest are not there.
|
|
79
|
+
const LOG_LIMIT = 200;
|
|
80
|
+
|
|
71
81
|
function createApp(options = {}) {
|
|
72
82
|
const {
|
|
73
83
|
reviewsDir = resolveReviewsDir(),
|
|
@@ -128,10 +138,18 @@ function createApp(options = {}) {
|
|
|
128
138
|
const absolutePath = resolveRepoFile(session.repoPath, filePath);
|
|
129
139
|
|
|
130
140
|
if (session.range) {
|
|
141
|
+
// A range is two commits, and the working tree is neither of them. It
|
|
142
|
+
// used to be the fallback here, which was merely odd while the head of
|
|
143
|
+
// every range was HEAD and is wrong now that it can be another branch:
|
|
144
|
+
// Full Context on feature/billing, opened from a checkout of
|
|
145
|
+
// feature/auth, would show auth's copy of the file under billing's name.
|
|
146
|
+
//
|
|
147
|
+
// A path missing at the head was deleted inside the range, and the text
|
|
148
|
+
// worth showing is the text that was deleted.
|
|
131
149
|
try {
|
|
132
150
|
return await git.show([`${session.range.head}:${filePath}`]);
|
|
133
151
|
} catch {
|
|
134
|
-
return
|
|
152
|
+
return git.show([`${session.range.from}:${filePath}`]).catch(() => '');
|
|
135
153
|
}
|
|
136
154
|
}
|
|
137
155
|
|
|
@@ -205,7 +223,7 @@ function createApp(options = {}) {
|
|
|
205
223
|
|
|
206
224
|
async function diffForFile(git, session, filePath) {
|
|
207
225
|
if (session.range) {
|
|
208
|
-
return git.diff([session.range.
|
|
226
|
+
return git.diff([session.range.from, session.range.head, '--', filePath]);
|
|
209
227
|
}
|
|
210
228
|
if (!(await hasCommits(git))) {
|
|
211
229
|
return '';
|
|
@@ -251,7 +269,7 @@ function createApp(options = {}) {
|
|
|
251
269
|
}
|
|
252
270
|
}
|
|
253
271
|
|
|
254
|
-
const resolve = async
|
|
272
|
+
const resolve = async ref => {
|
|
255
273
|
try {
|
|
256
274
|
return (await git.revparse([`${ref}^{commit}`])).trim();
|
|
257
275
|
} catch {
|
|
@@ -259,19 +277,151 @@ function createApp(options = {}) {
|
|
|
259
277
|
}
|
|
260
278
|
};
|
|
261
279
|
|
|
262
|
-
const baseSha = await resolve(
|
|
263
|
-
const headSha = await resolve(
|
|
264
|
-
|
|
265
|
-
//
|
|
266
|
-
//
|
|
267
|
-
//
|
|
268
|
-
//
|
|
269
|
-
//
|
|
270
|
-
|
|
271
|
-
|
|
280
|
+
const baseSha = await resolve(base);
|
|
281
|
+
const headSha = await resolve(head);
|
|
282
|
+
|
|
283
|
+
// The diff starts at the merge base, not at `base` itself -- what a pull
|
|
284
|
+
// request shows, and for the same reason. Compare feature/auth against a
|
|
285
|
+
// main that has moved on since the branch was cut, and a plain
|
|
286
|
+
// `git diff main feature/auth` includes main's newer commits *reversed*:
|
|
287
|
+
// the review then shows the feature author "undoing" a change they never
|
|
288
|
+
// touched, and a reviewer comments on it. On straight-line history the
|
|
289
|
+
// merge base of an ancestor and its descendant is the ancestor, so this
|
|
290
|
+
// changes nothing for x1..x5.
|
|
291
|
+
//
|
|
292
|
+
// Unrelated histories have no merge base; the plain two-ended diff is the
|
|
293
|
+
// only answer there, and it is the one given.
|
|
294
|
+
const from = await git.raw(['merge-base', baseSha, headSha])
|
|
295
|
+
.then(out => out.trim() || baseSha)
|
|
296
|
+
.catch(() => baseSha);
|
|
297
|
+
|
|
298
|
+
// `base..head` is what head has and base does not -- exactly the commits
|
|
299
|
+
// whose changes are in the diff. `head..base` is the other direction: what
|
|
300
|
+
// base has moved on by, which the merge-base diff deliberately leaves out
|
|
301
|
+
// and which the page has to say it left out, or someone will go looking
|
|
302
|
+
// for main's change and conclude the tool lost it.
|
|
303
|
+
const count = async range => Number(
|
|
304
|
+
(await git.raw(['rev-list', '--count', range])).trim()
|
|
272
305
|
);
|
|
306
|
+
const commits = await count(`${baseSha}..${headSha}`);
|
|
307
|
+
const behind = await count(`${headSha}..${baseSha}`);
|
|
308
|
+
|
|
309
|
+
// What the person asked for, kept alongside the SHAs. Nobody chose
|
|
310
|
+
// `3eb57959`; they chose `main`, and a scope bar that answers in SHAs has
|
|
311
|
+
// translated their question into one they did not ask.
|
|
312
|
+
const nameOf = async ref => {
|
|
313
|
+
if (ref !== 'HEAD') return ref;
|
|
314
|
+
const branch = await git.revparse(['--abbrev-ref', 'HEAD']).then(b => b.trim()).catch(() => 'HEAD');
|
|
315
|
+
return branch === 'HEAD' ? headSha.slice(0, 8) : branch;
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
base: baseSha,
|
|
320
|
+
head: headSha,
|
|
321
|
+
from,
|
|
322
|
+
commits: Number.isFinite(commits) ? commits : 0,
|
|
323
|
+
behind: Number.isFinite(behind) ? behind : 0,
|
|
324
|
+
baseName: await nameOf(base),
|
|
325
|
+
headName: await nameOf(head)
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* The range for reviewing commits themselves: one, or an unbroken run.
|
|
331
|
+
*
|
|
332
|
+
* Different from comparing two refs. `from` and `to` are both *in* the
|
|
333
|
+
* review -- clicking one commit means that commit's changes, and
|
|
334
|
+
* shift-clicking a second means everything from the first through the
|
|
335
|
+
* second -- so the diff starts at the parent of `from`, not at `from`.
|
|
336
|
+
* That is what GitHub does when you pick commits inside a pull request, and
|
|
337
|
+
* what "review commit 3 of 5" has to mean.
|
|
338
|
+
*
|
|
339
|
+
* `from` has to come before `to` on one line of history. Two commits on
|
|
340
|
+
* different branches have no run between them; that is a comparison, and
|
|
341
|
+
* the compare pickers exist for it.
|
|
342
|
+
*
|
|
343
|
+
* @throws {Error} with a message meant for the page
|
|
344
|
+
*/
|
|
345
|
+
async function resolveCommitRun(git, fromRef, toRef) {
|
|
346
|
+
for (const [label, ref] of [['first commit', fromRef], ['last commit', toRef]]) {
|
|
347
|
+
if (!looksLikeRef(ref)) throw new Error(`That ${label} does not look like a commit.`);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const resolve = async ref => {
|
|
351
|
+
try {
|
|
352
|
+
return (await git.revparse([`${ref}^{commit}`])).trim();
|
|
353
|
+
} catch {
|
|
354
|
+
throw new Error(`No commit called "${ref}" in this repository.`);
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
const first = await resolve(fromRef);
|
|
358
|
+
const last = await resolve(toRef);
|
|
359
|
+
|
|
360
|
+
// Ancestry through output, not exit code: `merge-base --is-ancestor`
|
|
361
|
+
// answers only by exiting 1, and simple-git resolves a silent non-zero
|
|
362
|
+
// exit -- so it reported every pair as ordered, including main before a
|
|
363
|
+
// branch that main was not an ancestor of. The merge base of an ancestor
|
|
364
|
+
// and its descendant is the ancestor, and that is checkable.
|
|
365
|
+
const mergeBase = (await git.raw(['merge-base', first, last]).catch(() => '')).trim();
|
|
366
|
+
if (mergeBase !== first) {
|
|
367
|
+
throw new Error(
|
|
368
|
+
`${first.slice(0, 7)} does not come before ${last.slice(0, 7)} on one line of history, ` +
|
|
369
|
+
'so there is no run of commits between them. Use Compare to diff two unrelated points.'
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// A root commit has no parent. Its "before" is the empty tree, which is
|
|
374
|
+
// also how `git show` presents a root commit: every file added.
|
|
375
|
+
let from;
|
|
376
|
+
let hasParent = true;
|
|
377
|
+
try {
|
|
378
|
+
from = (await git.revparse([`${first}^`])).trim();
|
|
379
|
+
} catch {
|
|
380
|
+
hasParent = false;
|
|
381
|
+
from = (await git.raw(['hash-object', '-t', 'tree', '/dev/null'])).trim();
|
|
382
|
+
}
|
|
273
383
|
|
|
274
|
-
|
|
384
|
+
const count = Number((await git.raw(['rev-list', '--count', hasParent ? `${from}..${last}` : last, '--'])).trim());
|
|
385
|
+
const subject = first === last
|
|
386
|
+
? (await git.raw(['log', '-1', '--format=%s', last, '--']).catch(() => '')).trim()
|
|
387
|
+
: null;
|
|
388
|
+
|
|
389
|
+
return {
|
|
390
|
+
kind: 'commits',
|
|
391
|
+
first,
|
|
392
|
+
last,
|
|
393
|
+
base: from,
|
|
394
|
+
head: last,
|
|
395
|
+
from,
|
|
396
|
+
commits: Number.isFinite(count) ? count : 1,
|
|
397
|
+
behind: 0,
|
|
398
|
+
baseName: first.slice(0, 7),
|
|
399
|
+
headName: last.slice(0, 7),
|
|
400
|
+
...(subject ? { subject } : {})
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** What to say about a commit or a run of them once loaded. */
|
|
405
|
+
function describeCommitRun(range, fileCount) {
|
|
406
|
+
const plural = (n, word) => `${n} ${word}${n === 1 ? '' : 's'}`;
|
|
407
|
+
const files = fileCount === 0 ? 'no files changed' : plural(fileCount, 'changed file');
|
|
408
|
+
return range.first === range.last
|
|
409
|
+
? `Commit ${range.headName}${range.subject ? `: ${range.subject}` : ''} — ${files}`
|
|
410
|
+
: `${plural(range.commits, 'commit')}, ${range.baseName} through ${range.headName} — ${files}`;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* The changed files of a range, with the status git gives each one.
|
|
415
|
+
*
|
|
416
|
+
* @param {import('simple-git').SimpleGit} git
|
|
417
|
+
* @param {{from: string, head: string}} range
|
|
418
|
+
*/
|
|
419
|
+
async function collectRangeChanges(git, range) {
|
|
420
|
+
const [summary, nameStatus] = await Promise.all([
|
|
421
|
+
git.diffSummary([range.from, range.head]),
|
|
422
|
+
git.raw(['diff', '--name-status', '-z', '-M', range.from, range.head])
|
|
423
|
+
]);
|
|
424
|
+
return collectCommitChanges(summary, parseNameStatus(nameStatus));
|
|
275
425
|
}
|
|
276
426
|
|
|
277
427
|
/**
|
|
@@ -288,15 +438,20 @@ function createApp(options = {}) {
|
|
|
288
438
|
* @returns {string}
|
|
289
439
|
*/
|
|
290
440
|
function describeRange(range, fileCount) {
|
|
291
|
-
const
|
|
441
|
+
const plural = (n, word) => `${n} ${word}${n === 1 ? '' : 's'}`;
|
|
442
|
+
const ends = `${range.baseName ?? range.base.slice(0, 8)} \u2192 ${range.headName ?? range.head.slice(0, 8)}`;
|
|
443
|
+
|
|
444
|
+
if (fileCount > 0) {
|
|
445
|
+
return `${ends}: ${plural(range.commits, 'commit')}, ${plural(fileCount, 'changed file')}`;
|
|
446
|
+
}
|
|
292
447
|
|
|
293
|
-
if (
|
|
294
|
-
return range.
|
|
295
|
-
?
|
|
296
|
-
: `${
|
|
448
|
+
if (range.commits === 0) {
|
|
449
|
+
return range.behind === 0
|
|
450
|
+
? `${ends}: both point at the same commit, so there is nothing between them.`
|
|
451
|
+
: `${ends}: ${range.headName ?? 'the head'} has no commits that ${range.baseName ?? 'the base'} does not already have.`;
|
|
297
452
|
}
|
|
298
453
|
|
|
299
|
-
return `${
|
|
454
|
+
return `${ends}: ${plural(range.commits, 'commit')}, and no net change \u2014 every change in them was undone again inside the range.`;
|
|
300
455
|
}
|
|
301
456
|
|
|
302
457
|
/**
|
|
@@ -316,9 +471,9 @@ function createApp(options = {}) {
|
|
|
316
471
|
* @param {import('simple-git').SimpleGit} git
|
|
317
472
|
* @param {number} limit
|
|
318
473
|
*/
|
|
319
|
-
async function recentCommits(git, limit) {
|
|
474
|
+
async function recentCommits(git, limit, head = 'HEAD') {
|
|
320
475
|
const raw = await git.raw([
|
|
321
|
-
'log', `--max-count=${limit}`, '--format=%H%x00%h%x00%s%x00%cr'
|
|
476
|
+
'log', `--max-count=${limit}`, '--format=%H%x00%h%x00%s%x00%cr', head, '--'
|
|
322
477
|
]);
|
|
323
478
|
|
|
324
479
|
return raw
|
|
@@ -331,7 +486,138 @@ function createApp(options = {}) {
|
|
|
331
486
|
}
|
|
332
487
|
|
|
333
488
|
/**
|
|
334
|
-
*
|
|
489
|
+
* Everything the compare pickers offer: branches, tags and recent commits.
|
|
490
|
+
*
|
|
491
|
+
* Branches come first in the page because comparing branches is what people
|
|
492
|
+
* reach for -- "what does my branch change against main" -- and a picker
|
|
493
|
+
* that only lists commits of the current branch makes that the one
|
|
494
|
+
* comparison you cannot pick. Remote-tracking branches are included because
|
|
495
|
+
* `origin/main` is the base most reviews actually want, and a local `main`
|
|
496
|
+
* that has not been pulled in a week is a quietly wrong one.
|
|
497
|
+
*
|
|
498
|
+
* Sorted by most recent activity, so the branch you are working on and the
|
|
499
|
+
* one you branched from are near the top rather than alphabetised into the
|
|
500
|
+
* middle of forty others.
|
|
501
|
+
*/
|
|
502
|
+
app.get('/api/refs/:repoId', async (req, res) => {
|
|
503
|
+
const session = requireSession(res, req.params.repoId);
|
|
504
|
+
if (!session) return;
|
|
505
|
+
|
|
506
|
+
const git = gitFactory(session.repoPath);
|
|
507
|
+
const refList = async (pattern, limit) => {
|
|
508
|
+
try {
|
|
509
|
+
const raw = await git.raw([
|
|
510
|
+
'for-each-ref', `--count=${limit}`, '--sort=-committerdate',
|
|
511
|
+
'--format=%(refname:short)%00%(objectname)%00%(committerdate:relative)%00%(subject)%00%(symref)',
|
|
512
|
+
pattern
|
|
513
|
+
]);
|
|
514
|
+
return raw.split('\n').filter(Boolean).map(line => {
|
|
515
|
+
const [name, sha, when, subject, symref] = line.split('\u0000');
|
|
516
|
+
return { name, sha, when, subject, symref };
|
|
517
|
+
})
|
|
518
|
+
// A symbolic ref points at another ref rather than being one --
|
|
519
|
+
// `origin/HEAD` in every clone. It cannot be filtered by name:
|
|
520
|
+
// `refname:short` abbreviates it to plain `origin`, which is how it
|
|
521
|
+
// first slipped into the list as a "branch".
|
|
522
|
+
.filter(ref => !ref.symref)
|
|
523
|
+
.map(({ symref, ...ref }) => ref);
|
|
524
|
+
} catch {
|
|
525
|
+
return [];
|
|
526
|
+
}
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
const current = await git.revparse(['--abbrev-ref', 'HEAD']).then(b => b.trim()).catch(() => null);
|
|
530
|
+
|
|
531
|
+
// The commits offered are the history of whatever is being compared, not
|
|
532
|
+
// always of the checkout: comparing main against feature/billing from a
|
|
533
|
+
// checkout of feature/auth should offer billing's commits to pick from.
|
|
534
|
+
const requested = typeof req.query.head === 'string' ? req.query.head.trim() : '';
|
|
535
|
+
const head = requested && looksLikeRef(requested) ? requested : 'HEAD';
|
|
536
|
+
|
|
537
|
+
const [branches, remotes, tags, commits] = await Promise.all([
|
|
538
|
+
refList('refs/heads', REF_LIST_LIMIT),
|
|
539
|
+
refList('refs/remotes', REF_LIST_LIMIT),
|
|
540
|
+
refList('refs/tags', REF_LIST_LIMIT),
|
|
541
|
+
recentCommits(git, COMMIT_LIST_LIMIT, head).catch(() => [])
|
|
542
|
+
]);
|
|
543
|
+
|
|
544
|
+
res.json({
|
|
545
|
+
// `HEAD` from --abbrev-ref means detached; say so rather than pretend
|
|
546
|
+
// there is a branch called HEAD.
|
|
547
|
+
current: current === 'HEAD' ? null : current,
|
|
548
|
+
branches,
|
|
549
|
+
remotes,
|
|
550
|
+
tags,
|
|
551
|
+
commits
|
|
552
|
+
});
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* The commits a review can be narrowed to, one at a time or as a run.
|
|
557
|
+
*
|
|
558
|
+
* With a base: the commits the comparison is made of -- what `base..head`
|
|
559
|
+
* has -- oldest first, because a branch reads as a story in the order it
|
|
560
|
+
* was written, and stepping through it commit by commit is the point.
|
|
561
|
+
* Without one: the recent history of `head`, newest first, the way `git
|
|
562
|
+
* log` reads, because there is no story there, only "how far back".
|
|
563
|
+
*
|
|
564
|
+
* Each commit carries its full message. The subject is what fits in a
|
|
565
|
+
* list, but the body is often where the author explains the change, and a
|
|
566
|
+
* reviewer looking at one commit should not have to leave the tool to read
|
|
567
|
+
* why it was made.
|
|
568
|
+
*
|
|
569
|
+
* Records are split on %x1e rather than on newlines, because a commit body
|
|
570
|
+
* is made of newlines.
|
|
571
|
+
*/
|
|
572
|
+
app.get('/api/log/:repoId', async (req, res) => {
|
|
573
|
+
const session = requireSession(res, req.params.repoId);
|
|
574
|
+
if (!session) return;
|
|
575
|
+
|
|
576
|
+
const git = gitFactory(session.repoPath);
|
|
577
|
+
const pick = name => (typeof req.query[name] === 'string' ? req.query[name].trim() : '');
|
|
578
|
+
const head = pick('head') || 'HEAD';
|
|
579
|
+
const base = pick('base');
|
|
580
|
+
|
|
581
|
+
for (const ref of base ? [base, head] : [head]) {
|
|
582
|
+
if (!looksLikeRef(ref)) {
|
|
583
|
+
return res.status(400).json({ error: 'That does not look like a commit or branch.' });
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
try {
|
|
588
|
+
const range = base ? `${base}..${head}` : head;
|
|
589
|
+
const total = Number((await git.raw(['rev-list', '--count', range, '--'])).trim()) || 0;
|
|
590
|
+
const raw = await git.raw([
|
|
591
|
+
'log', `--max-count=${LOG_LIMIT}`, ...(base ? ['--reverse'] : []),
|
|
592
|
+
'--format=%H%x00%h%x00%s%x00%an%x00%cr%x00%P%x00%b%x1e', range, '--'
|
|
593
|
+
]);
|
|
594
|
+
|
|
595
|
+
const commits = raw.split('\u001e').map(record => record.replace(/^\n/, '')).filter(Boolean).map(record => {
|
|
596
|
+
const [sha, short, subject, author, when, parents, body] = record.split('\u0000');
|
|
597
|
+
return {
|
|
598
|
+
sha, short, subject, author, when,
|
|
599
|
+
body: (body ?? '').trim(),
|
|
600
|
+
// A merge is shown against its first parent when reviewed alone --
|
|
601
|
+
// what it brought in -- and the list says it is one, because that
|
|
602
|
+
// diff can be surprisingly large.
|
|
603
|
+
merge: (parents ?? '').trim().split(/\s+/).filter(Boolean).length > 1
|
|
604
|
+
};
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
res.json({
|
|
608
|
+
order: base ? 'oldest-first' : 'newest-first',
|
|
609
|
+
total,
|
|
610
|
+
truncated: total > commits.length,
|
|
611
|
+
commits
|
|
612
|
+
});
|
|
613
|
+
} catch {
|
|
614
|
+
res.status(400).json({ error: `Could not read the history of "${base ? `${base}..${head}` : head}".` });
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* The commits of an open repository, newest first. Kept for 2.11 callers;
|
|
620
|
+
* the page itself uses /api/refs and /api/log.
|
|
335
621
|
*
|
|
336
622
|
* Behind a session id like the file endpoints rather than taking a path:
|
|
337
623
|
* this reads history out of a repository, and the session is what says
|
|
@@ -482,7 +768,7 @@ function createApp(options = {}) {
|
|
|
482
768
|
// Resolve to the root of the working tree before anything is keyed on
|
|
483
769
|
// this path. A subdirectory passes `checkIsRepo()` quite happily and
|
|
484
770
|
// then produces a file list whose diffs are all empty; see lib/repo.js.
|
|
485
|
-
const { base, head } = req.body ?? {};
|
|
771
|
+
const { base, head, commits: run } = req.body ?? {};
|
|
486
772
|
const root = await repoRoot(repoPath, gitFactory);
|
|
487
773
|
if (!root) {
|
|
488
774
|
return res.status(400).json({ error: 'Not a valid git repository' });
|
|
@@ -495,7 +781,18 @@ function createApp(options = {}) {
|
|
|
495
781
|
let range = null;
|
|
496
782
|
let message;
|
|
497
783
|
|
|
498
|
-
if (
|
|
784
|
+
if (run && typeof run === 'object') {
|
|
785
|
+
// Commits themselves, one or a run: see resolveCommitRun.
|
|
786
|
+
try {
|
|
787
|
+
range = await resolveCommitRun(git, `${run.from ?? ''}`.trim(), `${run.to ?? run.from ?? ''}`.trim());
|
|
788
|
+
} catch (error) {
|
|
789
|
+
return res.status(400).json({ error: error.message });
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
files = await collectRangeChanges(git, range);
|
|
793
|
+
mode = 'range';
|
|
794
|
+
message = describeCommitRun(range, files.length);
|
|
795
|
+
} else if (base !== undefined && base !== null && `${base}`.trim() !== '') {
|
|
499
796
|
// An explicit range. Nothing falls back to anything here: someone who
|
|
500
797
|
// asked for x1..x3 and is silently shown their working tree instead
|
|
501
798
|
// has been told a lie about what they are reviewing.
|
|
@@ -505,7 +802,7 @@ function createApp(options = {}) {
|
|
|
505
802
|
return res.status(400).json({ error: error.message });
|
|
506
803
|
}
|
|
507
804
|
|
|
508
|
-
files =
|
|
805
|
+
files = await collectRangeChanges(git, range);
|
|
509
806
|
mode = 'range';
|
|
510
807
|
message = describeRange(range, files.length);
|
|
511
808
|
} else {
|
|
@@ -517,9 +814,7 @@ function createApp(options = {}) {
|
|
|
517
814
|
if (files.length === 0) {
|
|
518
815
|
try {
|
|
519
816
|
const lastCommit = await resolveRange(git, 'HEAD~1', 'HEAD');
|
|
520
|
-
const commitFiles =
|
|
521
|
-
await git.diffSummary([lastCommit.base, lastCommit.head])
|
|
522
|
-
);
|
|
817
|
+
const commitFiles = await collectRangeChanges(git, lastCommit);
|
|
523
818
|
if (commitFiles.length > 0) {
|
|
524
819
|
files = commitFiles;
|
|
525
820
|
mode = 'lastCommit';
|