@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/src/history.js CHANGED
@@ -5,28 +5,26 @@ import { METRICS, measureBlob, pointExact } from './metrics.js';
5
5
  import { touchedSection } from './journal.js';
6
6
  import { EXIT, refuse } from './refusal.js';
7
7
 
8
- /* Обход истории: измерение по коммитам, сдвиг чисел, перенос состояния между
9
- * коммитами, сверка с рабочим деревом и сборката единственная точка, из
10
- * которой состояние и строки попадают наружу. Верхний этаж чтения: ниже git и
11
- * метрики, выше — только уже собранные значения. */
8
+ /* Walking the history: measuring per commit, shifting numbers, carrying state between commits,
9
+ * comparing against the working tree, and buildingthe one point from which rows and state
10
+ * leave this layer. The top floor of reading: below it are git and the metrics, above it only
11
+ * assembled values. */
12
12
 
13
- /* Причина, по которой коммит не получил строки, ключ, а не текст: по нему и
14
- * считается сводка, и отвечает `explain`. Слова для человека в `skipLine`, и
15
- * они те же, что были строкой раньше: «без изменения объёма» накрывает и тот
16
- * случай, когда коммит не тронул ни одного файла колонок, в отчёте эта разница
17
- * не проводилась, и эталон контракта её сохраняет; отличие видно в `explain`, где
18
- * оно и нужно. */
13
+ /* Why a commit got no row is a key rather than a text: the summary counts by it and `explain`
14
+ * answers by it. The words a human reads live in `skipLine` and are the ones that used to be the
15
+ * row itself: "no change of volume" also covers a commit that touched no column at all — the
16
+ * report never made that distinction and the contract fixture preserves it, while the difference
17
+ * is visible in `explain`, where it is needed. */
19
18
  const SKIP_WORDS = { merge: 'merge', report: 'только таблица', flat: 'без изменения объёма' };
20
19
 
21
20
  export function skipLine(dropped) {
22
21
  return dropped.sha.slice(0, 7) + ' (' + SKIP_WORDS[dropped.reason] + ')';
23
22
  }
24
23
 
25
- /* Сдвинул ли коммит хотя бы одно число. Сравниваются числа, а не список файлов:
26
- * правка в пробелах или комментариях размера не меняет, и строка про неё была бы
27
- * пустой, а у слияния клетки выходят нулевыми всегда, когда разрешение конфликта
28
- * совпало с тем, что уже дала ветка. Колонка, которой коммит не касался,
29
- * остаётся тем же объектом состояния. */
24
+ /* Whether a commit moved at least one number. Numbers are compared rather than the list of files:
25
+ * an edit to whitespace or comments changes no size and its row would be empty, while a merge
26
+ * gives zero cells whenever resolving the conflict produced what the branch already had. A column
27
+ * the commit did not touch keeps the very same state object. */
30
28
  function changesVolume(state, before, columns, metrics) {
31
29
  return columns.some((_col, i) => {
32
30
  const a = state[i], b = before[i];
@@ -36,11 +34,11 @@ function changesVolume(state, before, columns, metrics) {
36
34
  });
37
35
  }
38
36
 
39
- /* План чтения: какие пары «ревизия:путь» понадобятся и всё содержимое сразу иначе
40
- * на каждый коммит приходилось бы по git-вызову на колонку. Псевдонимов колонки,
41
- * которых коммит коснулся, может быть и два: при выключенном распознавании
42
- * переименований git отдаёт в одном коммите и старое имя, и новое. Собираются все
43
- * какой из них в коммите действительно есть, решается потом, по прочитанным блобам. */
37
+ /* The read plan: which `revision:path` pairs will be needed, and all the content at once
38
+ * otherwise every commit would cost one git call per column. A column may have both of its aliases
39
+ * touched by one commit: with rename detection off, git reports the old name and the new one in
40
+ * the same commit. All of them are collected which one is really there is decided later, by the
41
+ * blobs that came back. */
44
42
  function readPlan(cfg, root, commits, needText) {
45
43
  const plan = commits.map((c) => {
46
44
  const changed = new Set(c.files);
@@ -58,10 +56,10 @@ function readPlan(cfg, root, commits, needText) {
58
56
  return { plan: plan, blobs: readBlobs(root, specs, needText) };
59
57
  }
60
58
 
61
- /* Замер блоба с памятью на проход: ревизия с тем же содержимым (откат, повторный
62
- * merge) не пересчитывается. */
59
+ /* Measuring a blob with a memory kept for the run: a revision with the same content (a revert, a
60
+ * repeated merge) is not measured twice. */
63
61
  function measurer(cfg) {
64
- const measured = new Map(); // sha блоба + метрикачисло
62
+ const measured = new Map(); // blob sha + metricnumber
65
63
  return (name, blob, file, rev) => {
66
64
  const key = blob.sha + '\u0000' + name;
67
65
  if (measured.has(key)) return measured.get(key);
@@ -71,22 +69,22 @@ function measurer(cfg) {
71
69
  };
72
70
  }
73
71
 
74
- /* Правки коммита в состояние: из псевдонимов берётся тот, который в коммите есть, а
75
- * не первый по порядку настроек,исчезнувшее имя в коммите отсутствует, и
76
- * состояние, взятое по порядку, теряло файл (а сверка с деревом отказывала). */
72
+ /* The commit's edits applied to the state: the alias taken is the one present in the commit
73
+ * rather than the first in the settings order a vanished name is absent from the commit, and a
74
+ * state taken in settings order lost the file (and the comparison against the tree refused). */
77
75
  function applyPicks(pass, c, picks) {
78
76
  picks.forEach((candidates, i) => {
79
77
  const pick = candidates.find((cand) => pass.blobs.get(cand.spec) !== undefined);
80
78
  if (pick === undefined) {
81
- // Путь в коммите есть, а файла по нему нетфайл удалён.
79
+ // The path is in the commit but no blob came back for it the file was deleted.
82
80
  if (candidates.length > 0) pass.state[i] = null;
83
81
  return;
84
82
  }
85
83
  const blob = pass.blobs.get(pick.spec);
86
84
  const cells = {};
87
- /* Приближённость числа свойство пути, а не блоба: от расширения зависит,
88
- * возьмёт ли формат минификатор. Поэтому она считается здесь, вместо с
89
- * замером, и в кэш содержимого не попадает. */
85
+ /* Approximation is a property of the path rather than of the blob: whether the format goes to
86
+ * the minifier depends on its extension. It is computed here along with the measurement, and
87
+ * so it does not enter the content cache. */
90
88
  const approx = {};
91
89
  pass.metrics.forEach((m) => {
92
90
  cells[m] = pass.measure(m, blob, pick.path, c.sha);
@@ -96,9 +94,9 @@ function applyPicks(pass, c, picks) {
96
94
  });
97
95
  }
98
96
 
99
- /* Один коммит прохода: сдвиг состояния, затем нужна ли коммиту строка. `pass`
100
- * общий на весь проход (состояние, списки, память замеров), поэтому функция только
101
- * двигает его вперёд. */
97
+ /* One commit of the run: shift the state, then decide whether the commit needs a row. `pass` is
98
+ * shared by the whole run (state, lists, measurement memory), so the function only moves it
99
+ * forward. */
102
100
  function stepCommit(pass, c, ci) {
103
101
  const plan = pass.plan[ci];
104
102
  let section = null;
@@ -134,14 +132,14 @@ function stepCommit(pass, c, ci) {
134
132
  });
135
133
  }
136
134
 
137
- /* Проход по истории. Состояние колонки переносится вперёд, а перезамер делается
138
- * только для изменившихся в коммите файлов.
135
+ /* The run over the history. A column's state is carried forward, and only the files changed by the
136
+ * commit are measured again.
139
137
  *
140
- * `known` уже прочитанная история: проходам, которым она нужна ещё и сама по
141
- * себе (полнота покрытия), незачем звать `git log` второй раз. */
138
+ * `known` is the history already read: a pass that needs it for its own sake as well (coverage)
139
+ * has no reason to call `git log` a second time. */
142
140
  export function measureHistory(cfg, root, known) {
143
141
  const commits = known === undefined ? readHistory(root) : known;
144
- // Текст журнала нужен всегда: ссылка в раздел не метрика, но тоже чтение.
142
+ // The journal text is always needed: a link to a section is not a metric, but it is a read too.
145
143
  const needText = !!cfg.journal || cfg.metrics.some((m) => METRICS[m].needsText);
146
144
  const reads = readPlan(cfg, root, commits, needText);
147
145
  const pass = {
@@ -158,16 +156,16 @@ export function measureHistory(cfg, root, known) {
158
156
  journalPrev: ''
159
157
  };
160
158
  commits.forEach((c, ci) => stepCommit(pass, c, ci));
161
- /* Какие колонки тронул последний коммит по тому же плану чтения, по которому
162
- * идёт перенос состояния: путь колонки есть в списке изменённых путей коммита.
163
- * Страница ставит эти колонки впереди остальных: отчёт пересобирается после
164
- * каждого коммита, и первый вопрос читателя — что принесла эта правка.
159
+ /* Which columns the last commit touched, read from the same plan the state is carried along:
160
+ * a column's path is in the list of paths the commit changed. The page puts those columns ahead
161
+ * of the rest the report is rebuilt after every commit, and a reader's first question is what
162
+ * this edit brought.
165
163
  *
166
- * Берётся последний коммит, задевший хотя бы одну колонку, считая от верхушки
167
- * назад. Коммиты мимо колонок (и, прежде всего, сам отчёт, который коммитит хук)
168
- * пропускаются: правка отчёта не правка проекта. Иначе знак зависел бы от
169
- * собственного коммита отчёта: тот же прогон давал бы другие байты, отчёт
170
- * перестал бы быть неподвижной точкой, а хук коммитил бы его по второму разу. */
164
+ * The commit taken is the last one that touched at least one column, counting back from the top.
165
+ * Commits that went past the columns (above all the report itself, which the hook commits) are
166
+ * skipped: an edit to the report is not an edit to the project. Otherwise the mark would depend
167
+ * on the report's own commit the same run would produce different bytes, the report would stop
168
+ * being a fixed point, and the hook would commit it a second time. */
171
169
  let last = cfg.columns.map(() => false);
172
170
  for (let i = commits.length - 1; i >= 0 && !last.some(Boolean); i--) {
173
171
  const picks = reads.plan[i].picks.map((paths) => paths.length > 0);
@@ -176,20 +174,19 @@ export function measureHistory(cfg, root, known) {
176
174
  return { rows: pass.rows, dropped: pass.dropped, mixed: pass.mixed, state: pass.state, last: last };
177
175
  }
178
176
 
179
- /* Сверка с рабочим деревом отвечает на два вопроса, и оба обязательны: состояние
180
- * движка на HEAD совпадает с деревом коммита, и файл на диске соответствует тому
181
- * же содержимому. Первый ловит правку, потерянную при переносе состояния между
182
- * коммитами (например, у merge-коммита, которого нет в списке изменённых путей): и
183
- * потерянное создание файла (в дереве он есть, а состояние о нём не знает), и
184
- * потерянное изменение (файл есть с обеих сторон, содержимое разное), и потерянное
185
- * удаление (состояние о файле знает, а в дереве его нет). Сравнение при этом идёт
186
- * с расхождением, а не с пустотой: колонка, чей файл жил в истории и был удалён до
187
- * HEAD, пуста с обеих сторон это не потеря, а её видно в отчёте. Второй вопрос
188
- * правка, которой в истории нет вовсе. Размеры для этого не годятся: на диске они
189
- * зависят от выкладки (при `core.autocrlf=true` значение по умолчанию в установке
190
- * Git для Windows CRLF против LF), и инструмент отказывался работать там, где всё
191
- * в порядке. Файлы, изменённые в дереве, из сверки с диском выпадают: их
192
- * содержимое в коммите и на диске различается законно. */
177
+ /* The comparison against the working tree answers two questions, and both are needed: the engine's
178
+ * state at HEAD matches the tree of the commit, and the file on disk matches that same content.
179
+ * The first catches an edit lost while carrying state between commits (in a merge commit missing
180
+ * from the list of changed paths, say): a lost creation (the file is in the tree while the state
181
+ * knows nothing of it), a lost edit (the file is on both sides with different content) and a lost
182
+ * deletion (the state knows the file while the tree does not). An empty state is compared against
183
+ * the aliases present in the tree rather than against nothing: a column whose file lived in the
184
+ * history and was deleted before HEAD is empty on both sides that is not a loss, and the report
185
+ * shows as much. The second question is an edit the history does not hold at all, and sizes are no
186
+ * good for it: on disk they depend on the checkout (`core.autocrlf=true`, the default in Git for
187
+ * Windows, gives CRLF against LF), and the tool used to refuse to work where everything was in
188
+ * order. Files edited in the tree drop out of the disk comparison: their content legitimately
189
+ * differs between the commit and the disk. */
193
190
  function assertMatchesDisk(state, cfg, root) {
194
191
  const dirty = new Set(git(root, ['status', '--porcelain']).split('\n')
195
192
  .map((l) => l.trim()).filter((l) => l !== '').map((l) => l.replace(/^\S+\s+/, '').replace(/^.* -> /, '')));
@@ -202,27 +199,27 @@ function assertMatchesDisk(state, cfg, root) {
202
199
  const inTree = p === undefined ? undefined : tree.get(p);
203
200
  const lost = s === null ? aliases.length > 0 : inTree !== s.sha;
204
201
  if (lost) {
205
- refuse(EXIT.VIOLATION, 'состояние «' + col.label + '» на HEAD не совпало с деревом коммита (в дереве '
206
- + (aliases.length === 0 ? 'файла нет'
202
+ refuse(EXIT.VIOLATION, 'the state of "' + col.label + '" at HEAD did not match the tree of the commit (in the tree: '
203
+ + (aliases.length === 0 ? 'the file is absent'
207
204
  : aliases.map((alias) => alias + ' ' + tree.get(alias).slice(0, 7)).join(', '))
208
- + ', в состоянии ' + (s === null ? 'файла нет' : s.path + ' ' + s.sha.slice(0, 7))
209
- + '): перенос состояния между коммитами пропустил правку'
210
- // Пересборка здесь не починка: состояние считается тем же прогоном, и
211
- // устаревшей таблицы в этом расхождении нет. Поэтому совет называет не
212
- // команду починки, а то, чем это можно показать.
213
- + '\n починка: пересборкой это не лечитсярасхождение в самом переносе состояния,'
214
- + ' а не в таблице. Разбор: git show HEAD:' + p);
205
+ + ', in the state: ' + (s === null ? 'the file is absent' : s.path + ' ' + s.sha.slice(0, 7))
206
+ + '): carrying the state between commits lost an edit'
207
+ // Rebuilding is no fix here: the state comes from this very run, so no stale table is
208
+ // involved in this disagreement. Hence the advice names not a fix command but the way to
209
+ // show the thing.
210
+ + '\n fix: a rebuild does not cure this the discrepancy is in the carrying of the state itself,'
211
+ + ' not in the table. See it with: git show HEAD:' + p);
215
212
  }
216
213
  if (p !== undefined && !dirty.has(p) && clean.indexOf(p) < 0) clean.push(p);
217
214
  });
218
215
  if (clean.length === 0) return;
219
216
  const onDisk = diskHashes(root, clean);
220
217
  clean.forEach((p) => {
221
- if (onDisk.get(p) === tree.get(p)) return; // git считает файл неизменным
222
- if (fs.readFileSync(path.join(root, p)).equals(diskForm(root, 'HEAD', p))) return; // переводы строк необратимы
223
- refuse(EXIT.VIOLATION, 'содержимое ' + p + ' на диске разошлось с HEAD (' + onDisk.get(p).slice(0, 7)
224
- + ' вместо ' + tree.get(p).slice(0, 7) + '), хотя git не считает файл изменённым: правка есть только на диске'
225
- + '\n починка: закоммитьте правку или откатите её: git checkout -- ' + p);
218
+ if (onDisk.get(p) === tree.get(p)) return; // git counts the file as unmodified
219
+ if (fs.readFileSync(path.join(root, p)).equals(diskForm(root, 'HEAD', p))) return; // line endings are not reversible
220
+ refuse(EXIT.VIOLATION, 'the content of ' + p + ' on disk diverged from HEAD (' + onDisk.get(p).slice(0, 7)
221
+ + ' instead of ' + tree.get(p).slice(0, 7) + '), though git does not count the file as modified: the edit exists on disk only'
222
+ + '\n fix: commit the edit or roll it back: git checkout -- ' + p);
226
223
  });
227
224
  }
228
225
 
@@ -231,8 +228,8 @@ export function build(cfg, root) {
231
228
  const measured = measureHistory(cfg, root);
232
229
  assertMatchesDisk(measured.state, cfg, root);
233
230
  if (measured.mixed.length > 0) {
234
- console.error('! таблицу обновляли вместе с кодом: ' + measured.mixed.join(', ')
235
- + ' — так строка коммита не может попасть в сам коммит; обновляйте таблицу отдельным коммитом.');
231
+ console.error('! the table was updated together with the code: ' + measured.mixed.join(', ')
232
+ + ' — that way the commit cannot carry a line about itself; update the table in a separate commit.');
236
233
  }
237
234
  return measured;
238
235
  }