@vernikr/size-report 2.4.0 → 2.5.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 +929 -1081
- package/bin/postinstall.js +17 -18
- package/bin/size.js +2 -2
- package/package.json +3 -4
- package/src/args.js +72 -72
- package/src/artifact.js +14 -14
- package/src/check.js +41 -42
- package/src/cli.js +26 -29
- package/src/config.js +87 -91
- package/src/css.js +14 -14
- package/src/data.js +31 -37
- package/src/derived.js +31 -35
- package/src/doctor.js +95 -99
- package/src/explain.js +46 -47
- package/src/git.js +66 -71
- package/src/history.js +74 -77
- package/src/hook.js +130 -149
- package/src/init.js +36 -37
- package/src/journal.js +17 -15
- package/src/locales.js +25 -16
- package/src/metrics.js +56 -55
- package/src/minify.js +28 -27
- package/src/modes.js +57 -60
- package/src/optional.js +13 -11
- package/src/page/app.css +75 -91
- package/src/page/app.js +30 -35
- package/src/page/build.js +40 -39
- package/src/page/dom.js +8 -9
- package/src/page/panel.js +48 -51
- package/src/page/state.js +72 -87
- package/src/page/table.js +21 -24
- package/src/parse-worker.js +10 -10
- package/src/parse.js +43 -45
- package/src/project.js +100 -104
- package/src/refusal.js +75 -76
- package/src/size-table.js +41 -76
- package/src/strip/forms.js +5 -5
- package/src/strip/guard.js +27 -28
- package/src/strip/js.js +27 -27
- package/src/strip.js +18 -21
- package/src/table.css +13 -14
- package/src/tokens.js +28 -27
- package/src/tool.js +10 -11
- package/templates/README.md +71 -77
- package/templates/ci.yml +33 -33
- package/templates/size-report.config.json +3 -3
- package/CHANGELOG.md +0 -690
package/src/explain.js
CHANGED
|
@@ -3,56 +3,55 @@ import { measureHistory } from './history.js';
|
|
|
3
3
|
import { cliCommand, refuseCause } from './refusal.js';
|
|
4
4
|
import { outsideFix, pathRoles } from './config.js';
|
|
5
5
|
|
|
6
|
-
/*
|
|
6
|
+
/* Why a commit has no row — the answer to one question about one commit.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* коммита), так что выдумать их нельзя: чего нет в истории — о том молчание. */
|
|
8
|
+
* The answer is built on the same run as the report itself: the reason comes from the engine rather than
|
|
9
|
+
* being derived here anew, or two answers about one commit would drift apart. But the engine has one reason
|
|
10
|
+
* for two cases ("no change in volume" covers both "the numbers did not move" and "not a single column
|
|
11
|
+
* file"), because the report does not need that difference; here it is the whole question, so the reason is
|
|
12
|
+
* joined by evidence — which files the commit touched and which of them are columns, what is excluded, and
|
|
13
|
+
* what is not tracked at all. The evidence comes from the same facts (the commit's list of changed paths),
|
|
14
|
+
* so it cannot be invented: what the history does not hold, the answer is silent about. */
|
|
16
15
|
|
|
17
16
|
const REASON_TEXT = {
|
|
18
|
-
merge: '
|
|
19
|
-
report: '
|
|
20
|
-
outside: '
|
|
21
|
-
flat: '
|
|
17
|
+
merge: 'the commit is a merge, and the rows of merges are hidden by the setting "rows.merges: false"',
|
|
18
|
+
report: 'only the report itself was touched (and what "skip" lists)',
|
|
19
|
+
outside: 'no file of the commit is tracked by a column',
|
|
20
|
+
flat: 'the numbers did not move: column files were touched, and the volume did not change'
|
|
22
21
|
};
|
|
23
22
|
|
|
24
|
-
/*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
23
|
+
/* A commit by name. git resolves a revision name, and only when there is no such name do we look up a sha
|
|
24
|
+
* prefix in the list of commits: that way an ambiguous prefix keeps a human refusal listing the candidates,
|
|
25
|
+
* while a name follows git's rules rather than ours. */
|
|
27
26
|
function lookup(root, commits, target) {
|
|
28
27
|
const resolved = resolveCommit(root, String(target));
|
|
29
28
|
const needle = String(target).toLowerCase();
|
|
30
29
|
const found = resolved === null
|
|
31
30
|
? commits.filter((c) => c.sha.toLowerCase().indexOf(needle) === 0)
|
|
32
31
|
: commits.filter((c) => c.sha === resolved);
|
|
33
|
-
//
|
|
34
|
-
//
|
|
32
|
+
// The name resolved and the commit is missing from the report: that is not "no such commit" — the commit
|
|
33
|
+
// exists, and exactly that has to be said, or the person goes looking for a problem in the history.
|
|
35
34
|
if (resolved !== null && found.length === 0) {
|
|
36
|
-
refuseCause('
|
|
37
|
-
+ ',
|
|
38
|
-
+ '\n
|
|
39
|
-
+ ' (
|
|
35
|
+
refuseCause('commit outside the history', '"' + target + '" is the commit ' + resolved.slice(0, 7)
|
|
36
|
+
+ ', but it is not in the history of the report: rows are built over the commits of the current branch'
|
|
37
|
+
+ '\n fix: look at the history of the report: git log --oneline'
|
|
38
|
+
+ ' (the whole history of the repository is shown by git log --all)');
|
|
40
39
|
}
|
|
41
40
|
if (found.length === 0) {
|
|
42
|
-
refuseCause('
|
|
43
|
-
+ '\n
|
|
41
|
+
refuseCause('no such commit', '"' + target + '" is not a revision name and not the start of a sha'
|
|
42
|
+
+ '\n fix: look at the history: git log --oneline');
|
|
44
43
|
}
|
|
45
44
|
if (found.length > 1) {
|
|
46
|
-
refuseCause('
|
|
47
|
-
+ found.length + ' коммитов'
|
|
45
|
+
refuseCause('ambiguous commit', 'the prefix "' + target + '" is ambiguous: ' + found.length + ' commits fit'
|
|
48
46
|
+ '\n ' + found.slice(0, 5).map((c) => c.sha.slice(0, 7) + ' ' + c.subject).join('\n ')
|
|
49
|
-
+ '\n
|
|
47
|
+
+ '\n fix: name more characters');
|
|
50
48
|
}
|
|
51
49
|
return found[0];
|
|
52
50
|
}
|
|
53
51
|
|
|
54
|
-
/*
|
|
55
|
-
*
|
|
52
|
+
/* The evidence: what the commit touched — columns, excluded, past the columns. The judgement about a path's
|
|
53
|
+
* role is one and lives with the settings (`pathRoles`); here it is only sorted into buckets by that
|
|
54
|
+
* answer. */
|
|
56
55
|
function touchedOf(cfg, files) {
|
|
57
56
|
const role = pathRoles(cfg);
|
|
58
57
|
const touched = { columns: [], excluded: [], untracked: [] };
|
|
@@ -65,14 +64,14 @@ function touchedOf(cfg, files) {
|
|
|
65
64
|
}
|
|
66
65
|
|
|
67
66
|
const FIX = {
|
|
68
|
-
merge: '
|
|
69
|
-
report: '
|
|
70
|
-
flat: '
|
|
67
|
+
merge: 'turn on the rows of merges: "rows": { "merges": true }',
|
|
68
|
+
report: 'not needed: a row about the commit cannot lie inside that very commit — update the report separately',
|
|
69
|
+
flat: 'not needed: the volume did not change — a row without a single number would read as a breakage'
|
|
71
70
|
};
|
|
72
71
|
|
|
73
|
-
/*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
72
|
+
/* The fix by reason. For "past the columns" it is one text for two answers (`outsideFix`) and names the
|
|
73
|
+
* paths. For a commit with no files (`--allow-empty`) there is nothing to name — so there is no fix at all
|
|
74
|
+
* rather than a command without names, and this branch is what holds that case. */
|
|
76
75
|
function fixFor(reason, touched) {
|
|
77
76
|
if (reason === 'outside') {
|
|
78
77
|
return touched.untracked.length === 0 ? null : outsideFix(touched.untracked);
|
|
@@ -89,8 +88,8 @@ export function explainCommit(cfg, root, target) {
|
|
|
89
88
|
const dropped = measured.dropped.find((d) => d.sha === c.sha);
|
|
90
89
|
const touched = touchedOf(cfg, c.files);
|
|
91
90
|
|
|
92
|
-
/*
|
|
93
|
-
*
|
|
91
|
+
/* The difference the report's row does not carry: "no change in volume" means something else for a commit
|
|
92
|
+
* past the columns than for one that touched a column. */
|
|
94
93
|
let reason = row >= 0 ? null : dropped.reason;
|
|
95
94
|
if (reason === 'flat' && touched.columns.length === 0) reason = 'outside';
|
|
96
95
|
const fix = fixFor(reason, touched);
|
|
@@ -108,23 +107,23 @@ export function explainCommit(cfg, root, target) {
|
|
|
108
107
|
};
|
|
109
108
|
}
|
|
110
109
|
|
|
111
|
-
/*
|
|
112
|
-
*
|
|
110
|
+
/* A refusal here reaches a person as text and an agent as a field: "there is a row" and "there is no row"
|
|
111
|
+
* are equally successful answers, hence exit code 0 for both. */
|
|
113
112
|
export function explainText(rep) {
|
|
114
113
|
const lines = [];
|
|
115
|
-
const where = '
|
|
114
|
+
const where = ' commit ' + rep.sha.slice(0, 7) + ' "' + rep.subject.slice(0, 60) + '"';
|
|
116
115
|
if (rep.row !== null) {
|
|
117
|
-
lines.push('✓
|
|
116
|
+
lines.push('✓ the row is there: ' + rep.row + ' of ' + rep.rows + ' — the volume changed');
|
|
118
117
|
} else {
|
|
119
|
-
lines.push('—
|
|
118
|
+
lines.push('— no row needed: ' + REASON_TEXT[rep.reason]);
|
|
120
119
|
}
|
|
121
120
|
lines.push(where);
|
|
122
|
-
if (rep.touched.columns.length > 0) lines.push('
|
|
123
|
-
if (rep.touched.excluded.length > 0) lines.push('
|
|
121
|
+
if (rep.touched.columns.length > 0) lines.push(' columns touched: ' + rep.touched.columns.join(', '));
|
|
122
|
+
if (rep.touched.excluded.length > 0) lines.push(' excluded by the settings: ' + rep.touched.excluded.join(', '));
|
|
124
123
|
if (rep.touched.untracked.length > 0) {
|
|
125
|
-
lines.push('
|
|
126
|
-
+ ' —
|
|
124
|
+
lines.push(' past the columns and the exceptions: ' + rep.touched.untracked.join(', ')
|
|
125
|
+
+ ' — the coverage check answers for this: ' + cliCommand('check'));
|
|
127
126
|
}
|
|
128
|
-
if (rep.fix !== null) lines.push('
|
|
127
|
+
if (rep.fix !== null) lines.push(' fix: ' + rep.fix);
|
|
129
128
|
return lines.join('\n');
|
|
130
129
|
}
|
package/src/git.js
CHANGED
|
@@ -1,26 +1,24 @@
|
|
|
1
1
|
import { execFileSync, spawnSync } from 'child_process';
|
|
2
2
|
import { EXIT, refuse } from './refusal.js';
|
|
3
3
|
|
|
4
|
-
/*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
/* The single boundary of git calls: pinned settings, blobs in batches, the history, and the
|
|
5
|
+
* comparison against the disk. Everything the tool knows about the repository's content comes
|
|
6
|
+
* from here — which is why the pins are set here rather than in every call. */
|
|
7
7
|
|
|
8
8
|
export const MAX_BUF = 256 * 1024 * 1024;
|
|
9
|
-
const FIELD = '\u0001'; //
|
|
10
|
-
|
|
11
|
-
/*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* отчёта. Закрепление задаётся здесь, а не в каждом вызове: иначе его забудет
|
|
19
|
-
* следующий вызов.
|
|
9
|
+
const FIELD = '\u0001'; // field separator of the `git log` format
|
|
10
|
+
|
|
11
|
+
/* Everything the engine reads from git is read with explicitly pinned settings: their defaults
|
|
12
|
+
* come from the machine and change what ends up in the parse. Without `core.quotePath=false`
|
|
13
|
+
* non-English paths arrive quoted and escaped (`"docs/\320\267..."`): a column holding such a
|
|
14
|
+
* path finds no file, and a commit whose only change of volume it was loses its row. The other
|
|
15
|
+
* pins close the same class of defect — colouring and a signature block would mix into the parsed
|
|
16
|
+
* stream, and signature re-encoding into the row labels of the report. A pin is set here rather
|
|
17
|
+
* than in every call, or the next call would forget it.
|
|
20
18
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
19
|
+
* The locale is pinned along with them: the parse must not depend on which translations the
|
|
20
|
+
* machine has. The price is that git's own messages in unexpected refusals come out in English,
|
|
21
|
+
* while the tool's own messages stay as they are. */
|
|
24
22
|
export const GIT_PINS = [
|
|
25
23
|
'core.quotePath=false',
|
|
26
24
|
'color.ui=never',
|
|
@@ -44,11 +42,11 @@ export function git(root, args) {
|
|
|
44
42
|
});
|
|
45
43
|
}
|
|
46
44
|
|
|
47
|
-
/*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
45
|
+
/* The same read, but with the exit code: where a non-zero code is an expected answer rather than
|
|
46
|
+
* a refusal (`git diff --quiet` answers 1 on a difference), an exception would mean the tool
|
|
47
|
+
* refusing where a simple question was asked. `env` is added on top of the boundary's
|
|
48
|
+
* environment — the hook builds the report commit in a separate index with it, leaving the real
|
|
49
|
+
* one untouched (`src/hook.js`). */
|
|
52
50
|
export function gitTry(root, args, env) {
|
|
53
51
|
const res = spawnSync('git', gitArgv(args), {
|
|
54
52
|
cwd: root, encoding: 'utf8', maxBuffer: MAX_BUF,
|
|
@@ -57,15 +55,15 @@ export function gitTry(root, args, env) {
|
|
|
57
55
|
return { status: res.status, stdout: res.stdout || '', stderr: res.stderr || '' };
|
|
58
56
|
}
|
|
59
57
|
|
|
60
|
-
/*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
58
|
+
/* Reading blobs in batches. `git cat-file --batch-check` answers about a list of `revision:path`
|
|
59
|
+
* pairs (the object sha and its size), `--batch` returns the content. One or two processes for
|
|
60
|
+
* the whole history instead of spawning `git show` per file: across thousands of commits that is
|
|
61
|
+
* the difference between minutes and a second. As a side effect the object size turns out cheaper
|
|
62
|
+
* than reading the object — the `raw` metric needs no content at all.
|
|
65
63
|
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
const BLOB_CHUNK = 1000; //
|
|
64
|
+
* The answers are positional (one line per request), so requests and answers are matched by
|
|
65
|
+
* order: git does not repeat the `revision:path` in an answer. */
|
|
66
|
+
const BLOB_CHUNK = 1000; // specs per batch: it bounds both stdin and memory
|
|
69
67
|
|
|
70
68
|
function catFileCheck(root, specs) {
|
|
71
69
|
const out = execFileSync('git', gitArgv(['cat-file', '--batch-check=%(objectname) %(objecttype) %(objectsize)']), {
|
|
@@ -85,21 +83,21 @@ function catFileBatch(root, shas) {
|
|
|
85
83
|
if (nl < 0) break;
|
|
86
84
|
const f = buf.toString('utf8', i, nl).split(' ');
|
|
87
85
|
i = nl + 1;
|
|
88
|
-
if (f.length < 3) continue; //
|
|
86
|
+
if (f.length < 3) continue; // "<spec> missing"
|
|
89
87
|
const size = Number(f[2]);
|
|
90
88
|
out.set(f[0], { size: size, text: buf.toString('utf8', i, i + size) });
|
|
91
|
-
i += size + 1; //
|
|
89
|
+
i += size + 1; // the newline that follows the content
|
|
92
90
|
}
|
|
93
91
|
return out;
|
|
94
92
|
}
|
|
95
93
|
|
|
96
|
-
/*
|
|
97
|
-
* `raw`
|
|
98
|
-
*
|
|
94
|
+
/* Blobs for a list of `revision:path` pairs. `needText` decides whether the content is read: the
|
|
95
|
+
* `raw` metric lives happily with the object size alone, and then `--batch` is not called at all.
|
|
96
|
+
* Identical specs and identical blobs are requested once (the cache is keyed by sha). */
|
|
99
97
|
export function readBlobs(root, specs, needText) {
|
|
100
98
|
const uniq = [...new Set(specs)];
|
|
101
99
|
const out = new Map();
|
|
102
|
-
const texts = new Map(); // sha
|
|
100
|
+
const texts = new Map(); // blob sha → content
|
|
103
101
|
for (let start = 0; start < uniq.length; start += BLOB_CHUNK) {
|
|
104
102
|
const part = uniq.slice(start, start + BLOB_CHUNK);
|
|
105
103
|
const lines = catFileCheck(root, part);
|
|
@@ -120,11 +118,10 @@ export function readBlobs(root, specs, needText) {
|
|
|
120
118
|
return out;
|
|
121
119
|
}
|
|
122
120
|
|
|
123
|
-
/*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
* раскодировать. */
|
|
121
|
+
/* The HEAD tree: the blob shas of every file of the commit. This is the truth about the content
|
|
122
|
+
* of HEAD obtained by another route than the engine's state (which reads blobs in batches), so a
|
|
123
|
+
* disagreement with it means an edit lost while the state was carried forward. One call per run;
|
|
124
|
+
* the output is split by NUL (`-z`), or paths with spaces would have to be unquoted. */
|
|
128
125
|
export function headTree(root) {
|
|
129
126
|
const out = new Map();
|
|
130
127
|
const tree = execFileSync('git', gitArgv(['ls-tree', '-r', '-z', 'HEAD']), {
|
|
@@ -138,12 +135,11 @@ export function headTree(root) {
|
|
|
138
135
|
return out;
|
|
139
136
|
}
|
|
140
137
|
|
|
141
|
-
/*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
* `cat-file`. */
|
|
138
|
+
/* The files on disk as git sees them: `hash-object` runs each file through the same line-ending
|
|
139
|
+
* conversions and filters as `git add` (`.gitattributes`, `core.autocrlf`). So "the file on disk
|
|
140
|
+
* matches the commit" is a comparison of hashes rather than of sizes — a size depends on the
|
|
141
|
+
* checkout (`core.autocrlf=true` gives CRLF on disk and LF inside git). Paths come as a list and
|
|
142
|
+
* the answers are positional, as with `cat-file`. */
|
|
147
143
|
export function diskHashes(root, paths) {
|
|
148
144
|
const out = new Map();
|
|
149
145
|
const lines = execFileSync('git', gitArgv(['hash-object', '--stdin-paths']), {
|
|
@@ -153,31 +149,31 @@ export function diskHashes(root, paths) {
|
|
|
153
149
|
return out;
|
|
154
150
|
}
|
|
155
151
|
|
|
156
|
-
/*
|
|
157
|
-
* (`--filters`
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
152
|
+
/* The reverse conversion: what git would write to disk for a blob of this revision and path
|
|
153
|
+
* (`--filters` applies the checkout filters). Needed where git does not undo its line endings: a
|
|
154
|
+
* file whose CRLF sits in the commit itself is written out as it is under `core.autocrlf=true`,
|
|
155
|
+
* while "cleaning" would turn it back into LF — git warns about such files and puts exactly this
|
|
156
|
+
* on disk. */
|
|
161
157
|
export function diskForm(root, rev, p) {
|
|
162
158
|
return execFileSync('git', gitArgv(['cat-file', '--filters', rev + ':' + p]), {
|
|
163
159
|
cwd: root, maxBuffer: MAX_BUF, env: gitEnv()
|
|
164
160
|
});
|
|
165
161
|
}
|
|
166
162
|
|
|
167
|
-
//
|
|
163
|
+
// The content of a file in a revision, or null when the file is not there.
|
|
168
164
|
export function blobAt(root, rev, p) {
|
|
169
165
|
const blobs = readBlobs(root, [rev + ':' + p], true);
|
|
170
166
|
const blob = blobs.get(rev + ':' + p);
|
|
171
167
|
return blob === undefined ? null : blob.text;
|
|
172
168
|
}
|
|
173
169
|
|
|
174
|
-
/*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
170
|
+
/* A revision name → the commit sha: `HEAD`, a branch, a tag, `HEAD~1`, a short or a full sha. The
|
|
171
|
+
* rules of names stay with git instead of being rewritten here: our own would part ways with it on
|
|
172
|
+
* the first `main~2` or `HEAD@{1}`, while `^{commit}` cuts off names that lead somewhere other
|
|
173
|
+
* than a commit (a tag on a blob, a path in the tree). A name starting with a dash never reaches
|
|
174
|
+
* git: in `rev-parse` it would be a flag rather than a revision.
|
|
175
|
+
* Unresolved gives `null`: "no such name" and "the name is ambiguous" are told apart by the
|
|
176
|
+
* caller, which has the commit list for that. */
|
|
181
177
|
export function resolveCommit(root, name) {
|
|
182
178
|
if (name === '' || name.charAt(0) === '-') return null;
|
|
183
179
|
const res = gitTry(root, ['rev-parse', '--verify', '--quiet', name + '^{commit}']);
|
|
@@ -185,14 +181,13 @@ export function resolveCommit(root, name) {
|
|
|
185
181
|
return res.status === 0 && /^[0-9a-f]{40}$/.test(sha) ? sha : null;
|
|
186
182
|
}
|
|
187
183
|
|
|
188
|
-
/*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
* ровно то, что оно привнесло поверх своей ветки. */
|
|
184
|
+
/* The history in one call: the commit header and the list of paths it changed. `%ad` is the
|
|
185
|
+
* author's date in the author's own zone rather than the machine's, or the table would be built in
|
|
186
|
+
* UTC in CI and disagree with a local build. `--diff-merges=first-parent` is there because a merge
|
|
187
|
+
* commit otherwise has no path list at all (git shows a merge diff only on request): edits made
|
|
188
|
+
* while resolving a conflict would drop out of both the row and the carried state, and the state
|
|
189
|
+
* at HEAD would disagree with the file content in the tree. Against the first parent a merge shows
|
|
190
|
+
* exactly what it brought on top of its branch. */
|
|
196
191
|
export function readHistory(root) {
|
|
197
192
|
const log = git(root, [
|
|
198
193
|
'log', '--reverse', '--name-only', '--diff-merges=first-parent', '--date=format:%Y-%m-%d %H:%M',
|
|
@@ -214,8 +209,8 @@ export function readHistory(root) {
|
|
|
214
209
|
|
|
215
210
|
export function assertFullHistory(root) {
|
|
216
211
|
if (git(root, ['rev-parse', '--is-shallow-repository']).trim() === 'true') {
|
|
217
|
-
refuse(EXIT.SHALLOW, '
|
|
218
|
-
+ '
|
|
219
|
-
+ '
|
|
212
|
+
refuse(EXIT.SHALLOW, 'the history is truncated (shallow clone): the table is built over the whole history of commits.\n'
|
|
213
|
+
+ ' locally: git fetch --unshallow\n'
|
|
214
|
+
+ ' in CI: actions/checkout with fetch-depth: 0');
|
|
220
215
|
}
|
|
221
216
|
}
|