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 +38 -23
- package/lib/agent.js +22 -2
- package/lib/changes.js +73 -4
- package/package.json +1 -1
- package/public/app.js +341 -96
- package/public/index.html +35 -21
- package/public/style.css +156 -48
- package/server.js +149 -25
package/public/style.css
CHANGED
|
@@ -1878,89 +1878,197 @@ button:disabled {
|
|
|
1878
1878
|
text-align: center;
|
|
1879
1879
|
}
|
|
1880
1880
|
|
|
1881
|
-
/*
|
|
1881
|
+
/* ---------------------------------------------------------------------------
|
|
1882
|
+
* What the review is of, and how to change it.
|
|
1882
1883
|
*
|
|
1883
|
-
* Its own
|
|
1884
|
-
* controls: choosing a repository and choosing which of
|
|
1885
|
-
*
|
|
1886
|
-
* one long form.
|
|
1884
|
+
* Its own block under the status line rather than beside the repository
|
|
1885
|
+
* controls: choosing a repository and choosing which two points of it to look
|
|
1886
|
+
* between are different decisions, and crowding them into one row made the
|
|
1887
|
+
* header read as one long form.
|
|
1888
|
+
* ------------------------------------------------------------------------ */
|
|
1889
|
+
|
|
1887
1890
|
.scope-bar {
|
|
1891
|
+
margin-top: 8px;
|
|
1892
|
+
padding: 10px 12px;
|
|
1893
|
+
border-radius: 6px;
|
|
1894
|
+
background: #f6f8fa;
|
|
1895
|
+
border: 1px solid #e1e4e8;
|
|
1896
|
+
font-size: 13px;
|
|
1897
|
+
color: #24292e;
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
.scope-summary {
|
|
1888
1901
|
display: flex;
|
|
1889
1902
|
align-items: center;
|
|
1890
1903
|
justify-content: space-between;
|
|
1891
1904
|
flex-wrap: wrap;
|
|
1892
1905
|
gap: 8px 16px;
|
|
1893
|
-
margin-top: 8px;
|
|
1894
|
-
padding: 8px 12px;
|
|
1895
|
-
border-radius: 6px;
|
|
1896
|
-
background: #f3f4f6;
|
|
1897
|
-
border-left: 3px solid #6a737d;
|
|
1898
|
-
font-size: 13px;
|
|
1899
1906
|
}
|
|
1900
1907
|
|
|
1901
|
-
/* The scope reads as a sentence
|
|
1902
|
-
*
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1908
|
+
/* The scope reads as a sentence -- "main → feature/auth" -- rather than as a
|
|
1909
|
+
* labelled field, because the names are the information. */
|
|
1910
|
+
.scope-text {
|
|
1911
|
+
display: flex;
|
|
1912
|
+
align-items: baseline;
|
|
1913
|
+
flex-wrap: wrap;
|
|
1914
|
+
gap: 4px 12px;
|
|
1906
1915
|
min-width: 0;
|
|
1907
1916
|
}
|
|
1908
1917
|
|
|
1909
|
-
.scope-
|
|
1918
|
+
.scope-label {
|
|
1919
|
+
font-size: 14px;
|
|
1920
|
+
overflow-wrap: anywhere;
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
.scope-label strong {
|
|
1924
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
1925
|
+
font-size: 13px;
|
|
1926
|
+
font-weight: 600;
|
|
1927
|
+
padding: 1px 6px;
|
|
1928
|
+
border-radius: 4px;
|
|
1929
|
+
background: #fff;
|
|
1930
|
+
border: 1px solid #d1d5da;
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
.scope-meta {
|
|
1934
|
+
color: #586069;
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
.scope-actions {
|
|
1910
1938
|
display: flex;
|
|
1911
|
-
align-items: center;
|
|
1912
1939
|
gap: 8px;
|
|
1913
1940
|
flex-shrink: 0;
|
|
1914
1941
|
}
|
|
1915
1942
|
|
|
1916
|
-
.
|
|
1917
|
-
|
|
1918
|
-
|
|
1943
|
+
/* Secondary buttons. The :hover rule is not optional: `button:hover` sets the
|
|
1944
|
+
* primary blue and outranks a single class on specificity, so a secondary
|
|
1945
|
+
* button without its own hover rule turns into dark text on dark blue the
|
|
1946
|
+
* moment the pointer reaches it. That shipped once, on "Back to latest". */
|
|
1947
|
+
.scope-action,
|
|
1948
|
+
.compare-swap {
|
|
1949
|
+
background: #fff;
|
|
1950
|
+
color: #24292e;
|
|
1951
|
+
border: 1px solid #d1d5da;
|
|
1952
|
+
padding: 5px 12px;
|
|
1953
|
+
font-size: 13px;
|
|
1919
1954
|
}
|
|
1920
1955
|
|
|
1921
|
-
.scope-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1956
|
+
.scope-action:hover,
|
|
1957
|
+
.compare-swap:hover {
|
|
1958
|
+
background: #f3f4f6;
|
|
1959
|
+
border-color: #b8bec6;
|
|
1925
1960
|
}
|
|
1926
1961
|
|
|
1927
|
-
.scope-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1962
|
+
.scope-action:focus-visible,
|
|
1963
|
+
.compare-swap:focus-visible,
|
|
1964
|
+
.compare-end select:focus-visible,
|
|
1965
|
+
.compare-other:focus-visible {
|
|
1966
|
+
outline: 2px solid #0366d6;
|
|
1967
|
+
outline-offset: 1px;
|
|
1931
1968
|
}
|
|
1932
1969
|
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1970
|
+
.compare-row {
|
|
1971
|
+
display: flex;
|
|
1972
|
+
align-items: flex-end;
|
|
1973
|
+
flex-wrap: wrap;
|
|
1974
|
+
gap: 8px 10px;
|
|
1975
|
+
margin-top: 10px;
|
|
1976
|
+
padding-top: 10px;
|
|
1977
|
+
border-top: 1px solid #e1e4e8;
|
|
1937
1978
|
}
|
|
1938
1979
|
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1980
|
+
.compare-end {
|
|
1981
|
+
display: flex;
|
|
1982
|
+
flex-direction: column;
|
|
1983
|
+
gap: 4px;
|
|
1984
|
+
flex: 1 1 240px;
|
|
1985
|
+
min-width: 0;
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
.compare-end label {
|
|
1989
|
+
font-size: 11px;
|
|
1990
|
+
font-weight: 600;
|
|
1991
|
+
text-transform: uppercase;
|
|
1992
|
+
letter-spacing: 0.04em;
|
|
1993
|
+
color: #586069;
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
.compare-end select,
|
|
1997
|
+
.compare-other {
|
|
1998
|
+
width: 100%;
|
|
1999
|
+
min-width: 0;
|
|
2000
|
+
font-size: 13px;
|
|
2001
|
+
padding: 6px 8px;
|
|
1947
2002
|
border: 1px solid #d1d5da;
|
|
1948
2003
|
border-radius: 6px;
|
|
1949
2004
|
background: #fff;
|
|
1950
2005
|
color: #24292e;
|
|
1951
2006
|
}
|
|
1952
2007
|
|
|
1953
|
-
.
|
|
1954
|
-
|
|
1955
|
-
|
|
2008
|
+
.compare-other {
|
|
2009
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
2010
|
+
font-size: 12px;
|
|
1956
2011
|
}
|
|
1957
2012
|
|
|
2013
|
+
.compare-swap {
|
|
2014
|
+
flex: 0 0 auto;
|
|
2015
|
+
font-size: 16px;
|
|
2016
|
+
line-height: 1;
|
|
2017
|
+
padding: 7px 10px;
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
/* Why the file list looks the way it does. Its own line, in the same quiet
|
|
2021
|
+
* voice as the meta text: this is explanation, not a warning. An empty
|
|
2022
|
+
* comparison is a real answer, and it must not be dressed as an error. */
|
|
2023
|
+
.scope-note {
|
|
2024
|
+
margin: 10px 0 0;
|
|
2025
|
+
color: #586069;
|
|
2026
|
+
line-height: 1.45;
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
.files-empty {
|
|
2030
|
+
padding: 16px 12px;
|
|
2031
|
+
color: #586069;
|
|
2032
|
+
font-size: 13px;
|
|
2033
|
+
line-height: 1.45;
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
/* Narrow screens: the two ends stack, and the swap button sits between them
|
|
2037
|
+
* rather than overflowing the bar -- which the single-row version did, at
|
|
2038
|
+
* 760px, with the Compare button hanging off the right edge. */
|
|
1958
2039
|
@media (max-width: 720px) {
|
|
1959
|
-
.
|
|
1960
|
-
flex-
|
|
2040
|
+
.compare-row {
|
|
2041
|
+
flex-direction: column;
|
|
2042
|
+
align-items: stretch;
|
|
1961
2043
|
}
|
|
1962
2044
|
|
|
1963
|
-
.
|
|
1964
|
-
|
|
2045
|
+
.compare-end {
|
|
2046
|
+
flex: 1 1 auto;
|
|
1965
2047
|
}
|
|
2048
|
+
|
|
2049
|
+
.compare-swap {
|
|
2050
|
+
align-self: center;
|
|
2051
|
+
transform: rotate(90deg);
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
.scope-actions {
|
|
2055
|
+
width: 100%;
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
/* A comment on a file that is not in the current comparison. Kept, not
|
|
2060
|
+
* lost -- dimmed so it reads as "elsewhere", with a line saying so, because a
|
|
2061
|
+
* sidebar entry that looks attached but goes nowhere when clicked reads as a
|
|
2062
|
+
* bug. */
|
|
2063
|
+
.comment-item-elsewhere {
|
|
2064
|
+
opacity: 0.72;
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
.comment-item-elsewhere-note {
|
|
2068
|
+
margin-top: 6px;
|
|
2069
|
+
font-size: 11px;
|
|
2070
|
+
font-weight: 600;
|
|
2071
|
+
text-transform: uppercase;
|
|
2072
|
+
letter-spacing: 0.04em;
|
|
2073
|
+
color: #6a737d;
|
|
1966
2074
|
}
|
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,11 @@ 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
|
+
|
|
71
76
|
function createApp(options = {}) {
|
|
72
77
|
const {
|
|
73
78
|
reviewsDir = resolveReviewsDir(),
|
|
@@ -128,10 +133,18 @@ function createApp(options = {}) {
|
|
|
128
133
|
const absolutePath = resolveRepoFile(session.repoPath, filePath);
|
|
129
134
|
|
|
130
135
|
if (session.range) {
|
|
136
|
+
// A range is two commits, and the working tree is neither of them. It
|
|
137
|
+
// used to be the fallback here, which was merely odd while the head of
|
|
138
|
+
// every range was HEAD and is wrong now that it can be another branch:
|
|
139
|
+
// Full Context on feature/billing, opened from a checkout of
|
|
140
|
+
// feature/auth, would show auth's copy of the file under billing's name.
|
|
141
|
+
//
|
|
142
|
+
// A path missing at the head was deleted inside the range, and the text
|
|
143
|
+
// worth showing is the text that was deleted.
|
|
131
144
|
try {
|
|
132
145
|
return await git.show([`${session.range.head}:${filePath}`]);
|
|
133
146
|
} catch {
|
|
134
|
-
return
|
|
147
|
+
return git.show([`${session.range.from}:${filePath}`]).catch(() => '');
|
|
135
148
|
}
|
|
136
149
|
}
|
|
137
150
|
|
|
@@ -205,7 +218,7 @@ function createApp(options = {}) {
|
|
|
205
218
|
|
|
206
219
|
async function diffForFile(git, session, filePath) {
|
|
207
220
|
if (session.range) {
|
|
208
|
-
return git.diff([session.range.
|
|
221
|
+
return git.diff([session.range.from, session.range.head, '--', filePath]);
|
|
209
222
|
}
|
|
210
223
|
if (!(await hasCommits(git))) {
|
|
211
224
|
return '';
|
|
@@ -251,7 +264,7 @@ function createApp(options = {}) {
|
|
|
251
264
|
}
|
|
252
265
|
}
|
|
253
266
|
|
|
254
|
-
const resolve = async
|
|
267
|
+
const resolve = async ref => {
|
|
255
268
|
try {
|
|
256
269
|
return (await git.revparse([`${ref}^{commit}`])).trim();
|
|
257
270
|
} catch {
|
|
@@ -259,19 +272,67 @@ function createApp(options = {}) {
|
|
|
259
272
|
}
|
|
260
273
|
};
|
|
261
274
|
|
|
262
|
-
const baseSha = await resolve(
|
|
263
|
-
const headSha = await resolve(
|
|
264
|
-
|
|
265
|
-
//
|
|
266
|
-
//
|
|
267
|
-
//
|
|
268
|
-
//
|
|
269
|
-
//
|
|
270
|
-
|
|
271
|
-
|
|
275
|
+
const baseSha = await resolve(base);
|
|
276
|
+
const headSha = await resolve(head);
|
|
277
|
+
|
|
278
|
+
// The diff starts at the merge base, not at `base` itself -- what a pull
|
|
279
|
+
// request shows, and for the same reason. Compare feature/auth against a
|
|
280
|
+
// main that has moved on since the branch was cut, and a plain
|
|
281
|
+
// `git diff main feature/auth` includes main's newer commits *reversed*:
|
|
282
|
+
// the review then shows the feature author "undoing" a change they never
|
|
283
|
+
// touched, and a reviewer comments on it. On straight-line history the
|
|
284
|
+
// merge base of an ancestor and its descendant is the ancestor, so this
|
|
285
|
+
// changes nothing for x1..x5.
|
|
286
|
+
//
|
|
287
|
+
// Unrelated histories have no merge base; the plain two-ended diff is the
|
|
288
|
+
// only answer there, and it is the one given.
|
|
289
|
+
const from = await git.raw(['merge-base', baseSha, headSha])
|
|
290
|
+
.then(out => out.trim() || baseSha)
|
|
291
|
+
.catch(() => baseSha);
|
|
292
|
+
|
|
293
|
+
// `base..head` is what head has and base does not -- exactly the commits
|
|
294
|
+
// whose changes are in the diff. `head..base` is the other direction: what
|
|
295
|
+
// base has moved on by, which the merge-base diff deliberately leaves out
|
|
296
|
+
// and which the page has to say it left out, or someone will go looking
|
|
297
|
+
// for main's change and conclude the tool lost it.
|
|
298
|
+
const count = async range => Number(
|
|
299
|
+
(await git.raw(['rev-list', '--count', range])).trim()
|
|
272
300
|
);
|
|
301
|
+
const commits = await count(`${baseSha}..${headSha}`);
|
|
302
|
+
const behind = await count(`${headSha}..${baseSha}`);
|
|
303
|
+
|
|
304
|
+
// What the person asked for, kept alongside the SHAs. Nobody chose
|
|
305
|
+
// `3eb57959`; they chose `main`, and a scope bar that answers in SHAs has
|
|
306
|
+
// translated their question into one they did not ask.
|
|
307
|
+
const nameOf = async ref => {
|
|
308
|
+
if (ref !== 'HEAD') return ref;
|
|
309
|
+
const branch = await git.revparse(['--abbrev-ref', 'HEAD']).then(b => b.trim()).catch(() => 'HEAD');
|
|
310
|
+
return branch === 'HEAD' ? headSha.slice(0, 8) : branch;
|
|
311
|
+
};
|
|
273
312
|
|
|
274
|
-
return {
|
|
313
|
+
return {
|
|
314
|
+
base: baseSha,
|
|
315
|
+
head: headSha,
|
|
316
|
+
from,
|
|
317
|
+
commits: Number.isFinite(commits) ? commits : 0,
|
|
318
|
+
behind: Number.isFinite(behind) ? behind : 0,
|
|
319
|
+
baseName: await nameOf(base),
|
|
320
|
+
headName: await nameOf(head)
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* The changed files of a range, with the status git gives each one.
|
|
326
|
+
*
|
|
327
|
+
* @param {import('simple-git').SimpleGit} git
|
|
328
|
+
* @param {{from: string, head: string}} range
|
|
329
|
+
*/
|
|
330
|
+
async function collectRangeChanges(git, range) {
|
|
331
|
+
const [summary, nameStatus] = await Promise.all([
|
|
332
|
+
git.diffSummary([range.from, range.head]),
|
|
333
|
+
git.raw(['diff', '--name-status', '-z', '-M', range.from, range.head])
|
|
334
|
+
]);
|
|
335
|
+
return collectCommitChanges(summary, parseNameStatus(nameStatus));
|
|
275
336
|
}
|
|
276
337
|
|
|
277
338
|
/**
|
|
@@ -288,15 +349,20 @@ function createApp(options = {}) {
|
|
|
288
349
|
* @returns {string}
|
|
289
350
|
*/
|
|
290
351
|
function describeRange(range, fileCount) {
|
|
291
|
-
const
|
|
352
|
+
const plural = (n, word) => `${n} ${word}${n === 1 ? '' : 's'}`;
|
|
353
|
+
const ends = `${range.baseName ?? range.base.slice(0, 8)} \u2192 ${range.headName ?? range.head.slice(0, 8)}`;
|
|
292
354
|
|
|
293
|
-
if (fileCount
|
|
294
|
-
return range.commits
|
|
295
|
-
? 'Those two commits are the same, so there is nothing between them.'
|
|
296
|
-
: `${commits}, and no net change between the two ends — every change in them was undone again inside the range.`;
|
|
355
|
+
if (fileCount > 0) {
|
|
356
|
+
return `${ends}: ${plural(range.commits, 'commit')}, ${plural(fileCount, 'changed file')}`;
|
|
297
357
|
}
|
|
298
358
|
|
|
299
|
-
|
|
359
|
+
if (range.commits === 0) {
|
|
360
|
+
return range.behind === 0
|
|
361
|
+
? `${ends}: both point at the same commit, so there is nothing between them.`
|
|
362
|
+
: `${ends}: ${range.headName ?? 'the head'} has no commits that ${range.baseName ?? 'the base'} does not already have.`;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return `${ends}: ${plural(range.commits, 'commit')}, and no net change \u2014 every change in them was undone again inside the range.`;
|
|
300
366
|
}
|
|
301
367
|
|
|
302
368
|
/**
|
|
@@ -337,6 +403,66 @@ function createApp(options = {}) {
|
|
|
337
403
|
* this reads history out of a repository, and the session is what says
|
|
338
404
|
* which repository the caller has already been granted.
|
|
339
405
|
*/
|
|
406
|
+
/**
|
|
407
|
+
* Everything the compare pickers offer: branches, tags and recent commits.
|
|
408
|
+
*
|
|
409
|
+
* Branches come first in the page because comparing branches is what people
|
|
410
|
+
* reach for -- "what does my branch change against main" -- and a picker
|
|
411
|
+
* that only lists commits of the current branch makes that the one
|
|
412
|
+
* comparison you cannot pick. Remote-tracking branches are included because
|
|
413
|
+
* `origin/main` is the base most reviews actually want, and a local `main`
|
|
414
|
+
* that has not been pulled in a week is a quietly wrong one.
|
|
415
|
+
*
|
|
416
|
+
* Sorted by most recent activity, so the branch you are working on and the
|
|
417
|
+
* one you branched from are near the top rather than alphabetised into the
|
|
418
|
+
* middle of forty others.
|
|
419
|
+
*/
|
|
420
|
+
app.get('/api/refs/:repoId', async (req, res) => {
|
|
421
|
+
const session = requireSession(res, req.params.repoId);
|
|
422
|
+
if (!session) return;
|
|
423
|
+
|
|
424
|
+
const git = gitFactory(session.repoPath);
|
|
425
|
+
const refList = async (pattern, limit) => {
|
|
426
|
+
try {
|
|
427
|
+
const raw = await git.raw([
|
|
428
|
+
'for-each-ref', `--count=${limit}`, '--sort=-committerdate',
|
|
429
|
+
'--format=%(refname:short)%00%(objectname)%00%(committerdate:relative)%00%(subject)%00%(symref)',
|
|
430
|
+
pattern
|
|
431
|
+
]);
|
|
432
|
+
return raw.split('\n').filter(Boolean).map(line => {
|
|
433
|
+
const [name, sha, when, subject, symref] = line.split('\u0000');
|
|
434
|
+
return { name, sha, when, subject, symref };
|
|
435
|
+
})
|
|
436
|
+
// A symbolic ref points at another ref rather than being one --
|
|
437
|
+
// `origin/HEAD` in every clone. It cannot be filtered by name:
|
|
438
|
+
// `refname:short` abbreviates it to plain `origin`, which is how it
|
|
439
|
+
// first slipped into the list as a "branch".
|
|
440
|
+
.filter(ref => !ref.symref)
|
|
441
|
+
.map(({ symref, ...ref }) => ref);
|
|
442
|
+
} catch {
|
|
443
|
+
return [];
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
const current = await git.revparse(['--abbrev-ref', 'HEAD']).then(b => b.trim()).catch(() => null);
|
|
448
|
+
const [branches, remotes, tags, commits] = await Promise.all([
|
|
449
|
+
refList('refs/heads', REF_LIST_LIMIT),
|
|
450
|
+
refList('refs/remotes', REF_LIST_LIMIT),
|
|
451
|
+
refList('refs/tags', REF_LIST_LIMIT),
|
|
452
|
+
recentCommits(git, COMMIT_LIST_LIMIT).catch(() => [])
|
|
453
|
+
]);
|
|
454
|
+
|
|
455
|
+
res.json({
|
|
456
|
+
// `HEAD` from --abbrev-ref means detached; say so rather than pretend
|
|
457
|
+
// there is a branch called HEAD.
|
|
458
|
+
current: current === 'HEAD' ? null : current,
|
|
459
|
+
branches,
|
|
460
|
+
remotes,
|
|
461
|
+
tags,
|
|
462
|
+
commits
|
|
463
|
+
});
|
|
464
|
+
});
|
|
465
|
+
|
|
340
466
|
app.get('/api/commits/:repoId', async (req, res) => {
|
|
341
467
|
try {
|
|
342
468
|
const session = requireSession(res, req.params.repoId);
|
|
@@ -505,7 +631,7 @@ function createApp(options = {}) {
|
|
|
505
631
|
return res.status(400).json({ error: error.message });
|
|
506
632
|
}
|
|
507
633
|
|
|
508
|
-
files =
|
|
634
|
+
files = await collectRangeChanges(git, range);
|
|
509
635
|
mode = 'range';
|
|
510
636
|
message = describeRange(range, files.length);
|
|
511
637
|
} else {
|
|
@@ -517,9 +643,7 @@ function createApp(options = {}) {
|
|
|
517
643
|
if (files.length === 0) {
|
|
518
644
|
try {
|
|
519
645
|
const lastCommit = await resolveRange(git, 'HEAD~1', 'HEAD');
|
|
520
|
-
const commitFiles =
|
|
521
|
-
await git.diffSummary([lastCommit.base, lastCommit.head])
|
|
522
|
-
);
|
|
646
|
+
const commitFiles = await collectRangeChanges(git, lastCommit);
|
|
523
647
|
if (commitFiles.length > 0) {
|
|
524
648
|
files = commitFiles;
|
|
525
649
|
mode = 'lastCommit';
|