@vernikr/size-report 2.3.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/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: 'коммит слияние, а строки слияний скрыты настройкой «rows.merges: false»',
19
- report: 'тронут только сам отчёт то, что перечислено в «skip»)',
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
- /* Коммит по названию. Имя ревизии разрешает git, и только если имени нет ищем
25
- * начало sha по списку коммитов: так у неоднозначного префикса остаётся
26
- * человеческий отказ со списком подходящих, а у имени правила git, а не наши. */
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('коммит вне истории', '«' + target + '» это коммит ' + resolved.slice(0, 7)
37
- + ', но его нет в истории отчёта: строки строятся по коммитам текущей ветки'
38
- + '\n починка: посмотрите историю отчёта: git log --oneline'
39
- + ' (всю историю репозитория показывает git log --all)');
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('нет такого коммита', '«' + target + '» не имя ревизии и не начало sha'
43
- + '\n починка: посмотрите историю: git log --oneline');
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('коммит назван неточно', 'префикс «' + target + '» неоднозначен: подходят '
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
- * одно и живёт в настройках (`pathRoles`), здесь только раскладка его ответа. */
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: 'включите строки слияний: "rows": { "merges": true }',
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
- /* Починка по причине. У «мимо колонок» она одна на два ответа (`outsideFix`) и называет
74
- * пути. У коммита без файлов (`--allow-empty`) называть нечегозначит, починки нет
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
- * «нет строки» одинаково успешные ответы, поэтому код выхода 0 у обоих. */
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 = ' коммит ' + rep.sha.slice(0, 7) + ' «' + rep.subject.slice(0, 60) + '»';
114
+ const where = ' commit ' + rep.sha.slice(0, 7) + ' "' + rep.subject.slice(0, 60) + '"';
116
115
  if (rep.row !== null) {
117
- lines.push('✓ строка есть: ' + rep.row + ' из ' + rep.rows + ' — объём изменился');
116
+ lines.push('✓ the row is there: ' + rep.row + ' of ' + rep.rows + ' — the volume changed');
118
117
  } else {
119
- lines.push('— строка не нужна: ' + REASON_TEXT[rep.reason]);
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(' тронуты колонки: ' + rep.touched.columns.join(', '));
123
- if (rep.touched.excluded.length > 0) lines.push(' исключено настройками: ' + rep.touched.excluded.join(', '));
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(' мимо колонок и исключений: ' + rep.touched.untracked.join(', ')
126
- + ' — за это отвечает проверка полноты: ' + cliCommand('check'));
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(' починка: ' + rep.fix);
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
- /* Единственная граница вызова git: закрепления настроек, блобы пачкой, история и
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'; // разделитель полей в формате git log
10
-
11
- /* Всё, что движок читает у git, читается с явно заданными настройками: их
12
- * значения по умолчанию берутся из настроек машины и меняют то, что попадает в
13
- * разбор. Без `core.quotePath=false` не-английские пути приходят закавыченными и
14
- * экранированными (`"docs/\320\267..."`): колонка с таким путём не находит файла,
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
- * стоят на машине. Цена сообщения самого git в неожиданных отказах идут
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
- * отказ (`git diff --quiet` отвечает 1 на расхождение). Исключение здесь означало бы
49
- * отказ инструмента там, где задан простой вопрос. `env` досыпается к окружению
50
- * границыим хук собирает коммит отчёта в отдельном индексе, не трогая
51
- * настоящий (см. `src/hook.js`). */
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
+ * environmentthe 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
- /* Чтение блобов пачкой. `git cat-file --batch-check` отвечает про список пар
61
- * `ревизия:путь` (sha объекта и размер), `--batch` отдаёт содержимое. Один-два
62
- * процесса на всю историю вместо спавна `git show` на каждый файл на тысячах
63
- * коммитов это разница между минутами и секундой. Побочно размер объекта
64
- * оказывается дешевле его чтения: метрике `raw` содержимое не нужно вовсе.
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
- * по порядку `ревизия:путь` git в ответе не повторяет. */
68
- const BLOB_CHUNK = 1000; // спек на пачку: ограничивает и stdin, и память
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; // «<спека> missing»
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
- /* Блобы для списка пар «ревизия:путь». `needText` читать ли содержимое: метрике
97
- * `raw` хватает размера объекта, и тогда `--batch` не вызывается вовсе.
98
- * Одинаковые спеки и одинаковые блобы запрашиваются один раз (кэш по sha). */
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
- /* Дерево HEAD: sha блобов всех файлов коммита. Это правда о содержимом HEAD,
124
- * добытая не тем же способом, что состояние движка (то читает блобы пачкой),
125
- * поэтому расхождение с ней и означает потерянную при переносе правку. Один вызов
126
- * на прогон; разбор идёт по NUL (`-z`), иначе пути с пробелами пришлось бы
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
- /* Файлы на диске такими, какими их видит git: `hash-object` пропускает каждый
142
- * файл через те же переводы строк и фильтры, что и `git add` (`.gitattributes`,
143
- * `core.autocrlf`). Поэтому «файл на диске соответствует коммиту»это сравнение
144
- * хешей, а не размеров: размер зависит от выкладки (при `core.autocrlf=true` на
145
- * диске CRLF, в git LF). Пути приходят списком, ответы позиционные — как у
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
- /* Обратный перевод: то, что git выложил бы на диск для блоба этой ревизии и пути
157
- * (`--filters` применяет фильтры выкладки). Нужен там, где переводы строк git не
158
- * возвращает обратно: файл, в котором CRLF лежат в самом коммите, при
159
- * `core.autocrlf=true` выкладывается как есть, а «очистка» вернула бы LF,сам
160
- * git про такие файлы предупреждает, а на диск кладёт именно это. */
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
- // Содержимое файла в ревизии или null, если файла там нет.
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
- /* Имя ревизии → sha коммита: `HEAD`, ветка, тег, `HEAD~1`, короткий или полный
175
- * sha. Правила имён остаются за git, а не переписываются здесь: свои разошлись бы
176
- * с ним на первом же `main~2` или `HEAD@{1}`, а `^{commit}` отсекает имена, ведущие
177
- * не к коммиту (тег на блоб, путь в дереве). Имя, начинающееся с дефиса, к git не
178
- * идёт вовсе: в `rev-parse` оно было бы ключом, а не ревизией.
179
- * Не разрешилось `null`: «имени нет» и «имя неоднозначно» разбирает вызывающий,
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
- * `%ad` дата автора в его собственной зоне (не в зоне машины), иначе таблица
190
- * собиралась бы в CI по UTC и расходилась бы с локальной сборкой.
191
- * `--diff-merges=first-parent` иначе у merge-коммита списка путей нет вовсе
192
- * (git не показывает дифф слияния, пока не попросишь): правки разрешения
193
- * конфликта выпали бы и из строки, и из переноса состояния, а состояние на HEAD
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, 'история обрезана (shallow clone): таблица строится по всей истории коммитов.\n'
218
- + ' локально: git fetch --unshallow\n'
219
- + ' в CI: actions/checkout с fetch-depth: 0');
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
  }