ci: ребейз виден автору до пуша, конфликт называет файлы, уход dev отмечается

Конвейер приводит ветку к dev сам (#257) и при конфликте возвращает задачу, не
тратя цикл ревью. Оставались три щели, и все три про то, что человек узнаёт
поздно и без подробностей.

Первое. Отставание теперь видно в scripts/pre-push-gate.mjs до пуша, с числом
коммитов и готовой командой. Это предупреждение, а не гейт: гейтом остаётся
конвейер, который забыть не может. Смысл в цене — после любого ребейза разбор
становится полным, а не по дельте (§7.2), а конфликт всё равно чинится на машине
автора. Отключается --no-rebase-check.

Второе. Конфликт называет файлы. Список снимается ДО `git rebase --abort`: abort
снимает состояние конфликта вместе с ним, и раньше автору доставалось «не
ребейзится» без единого имени. Логика проверена на настоящем конфликте в
одноразовом репозитории — два файла названы.

Третье. Если dev ушёл вперёд, пока шло ревью, это записывается в summary
прогона, а при зелёном вердикте ещё и комментарием: вердикт вынесен по дереву,
которое уже не совпадает с вершиной линии, и слияние приведёт ветку к dev.
Комментарий только при зелёном — шуметь на каждом прогоне ни к чему, а вот
молчать перед слиянием нельзя.

Чего задача не делает: не заставляет dev стоять на месте, пока идёт ревью. Если
возвраты частые именно из-за темпа, лечится очередью слияний, а это решение о
процессе, не о скрипте.

Два мутанта проверены руками — «советовать ребейз всегда» и «никогда не сообщать
про уход dev», — каждый убит.

Issue: #364
User-Visible: no
This commit is contained in:
Claude
2026-08-29 10:06:41 +03:00
parent d3d3312adb
commit 4d73774779
5 changed files with 172 additions and 4 deletions
+21
View File
@@ -31,6 +31,7 @@ import { spawnSync } from 'node:child_process';
import { existsSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { rebaseAdvice } from './branch-state.mjs';
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
const argv = process.argv.slice(2);
@@ -63,6 +64,26 @@ const capture = (command, args) => spawnSync(command, args, { cwd: ROOT, encodin
const steps = [];
const skipped = [];
// ---- приведена ли ветка к dev ---------------------------------------------
// Конвейер ребейзит сам (#257), но после ребейза разбор становится полным, а не
// по дельте (§7.2), и конфликт всплывает в комментарии через сорок минут вместо
// машины автора. Поэтому предупреждение, а не гейт: гейтом остаётся конвейер.
if (!flag('no-rebase-check')) {
const fetched = capture('git', ['fetch', '-q', 'origin', base.replace(/^origin\//, '')]);
if (fetched.status !== 0) {
skipped.push(`проверка отставания от ${base} — git fetch не удался`);
} else {
const ancestor = capture('git', ['merge-base', '--is-ancestor', base, head]);
if (ancestor.status === 0) {
console.log(`\n── Ветка\n содержит весь ${base}, ребейз не нужен`);
} else {
const counted = capture('git', ['rev-list', '--count', `${head}..${base}`]);
const advice = rebaseAdvice({ behind: Number(counted.stdout.trim()), base });
if (advice) skipped.push(advice);
}
}
}
// ---- что тронуто ----------------------------------------------------------
const diff = capture('git', ['diff', '--name-only', `${base}..${head}`]);
if (diff.status !== 0) {