mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
fix(process): rebase resolves a conflict only in docs/reviews/INDEX.md by rebuilding the index
A pipeline doc commit carries the review document and the rebuilt INDEX.md; while the task waits, dev receives other tasks' documents with their own INDEX.md, and the rebase of the branch conflicts in the index every time. 24.09 this bounced green #617, #618, #629, #642 to S6. scripts/rebase-generated.mjs: shared rebase helper. At every stop, if ALL conflicting paths are docs/reviews/INDEX.md (or paths the caller resolves itself), the index is rebuilt from the directory in the stop tree, staged, and the rebase continues; any other path aborts and returns the full list. CLI exit 3 = refusal with paths on stdout. Wired into process.yml «Привести ветку к dev» (helper taken from dev via git archive; conflict/conflicts outputs, lease, ref wait and --commit-if-stale kept), merge-candidate rebaseOnto (claude[bot] identity, --commit-if-stale kept) and rebase-on-dev.mjs (index next to GENERATED_ROOTS; bundle still dev copy + rebuild). Issue: #643 User-Visible: no
This commit is contained in:
@@ -26,6 +26,9 @@ import { fileURLToPath } from 'node:url';
|
||||
import {
|
||||
CI_PROOF_POLICIES, evaluateCiProof, githubCandidateTree, loadGithubProofContext,
|
||||
} from './ci-proof.mjs';
|
||||
import { rebaseRegenerating } from './rebase-generated.mjs';
|
||||
// Личность конвейера — одна на ребейз и на коммит индекса (#643).
|
||||
import { CONVEYOR_IDENTITY } from './reviews-index.mjs';
|
||||
|
||||
/** Скрипт индекса — по абсолютному пути: слияние работает из чужого cwd (worktree кандидата). */
|
||||
const REVIEWS_INDEX_SCRIPT = fileURLToPath(new URL('./reviews-index.mjs', import.meta.url));
|
||||
@@ -148,8 +151,15 @@ export function realOps({
|
||||
},
|
||||
rebaseOnto: (branchTip, onto) => {
|
||||
must(git('checkout', '-q', '-B', 'merge-into-dev', branchTip), 'checkout');
|
||||
const r = spawnSync('git', ['-c', 'user.name=claude[bot]', '-c', 'user.email=209825114+claude[bot]@users.noreply.github.com', 'rebase', onto], { encoding: 'utf8' });
|
||||
if (r.status !== 0) { spawnSync('git', ['rebase', '--abort']); return null; }
|
||||
// #643: doc-коммит ветки конфликтует с документами других задач в dev
|
||||
// только в генерируемом INDEX.md — это решается пересборкой индекса, а
|
||||
// не возвратом зелёной задачи в S6. Любой другой конфликт — отказ, как
|
||||
// был; ребейз при отказе уже отменён помощником.
|
||||
const r = rebaseRegenerating({ onto, gitPrefix: CONVEYOR_IDENTITY });
|
||||
if (!r.ok) {
|
||||
console.log(`ребейз на ${onto} отменён: ${r.conflicts.join(', ') || r.reason}`);
|
||||
return null;
|
||||
}
|
||||
// #635 r2: dev мог принести новые документы ревью — снимок INDEX.md
|
||||
// в кандидате их не знает. Коммит индекса — doc-коммит конвейера:
|
||||
// patch-id его не видит (`:!docs/reviews`), а тест «индекс свеж» в
|
||||
|
||||
@@ -8782,6 +8782,75 @@ const MUTANT_DEFINITIONS = [
|
||||
replace: " current = [line]; // mutant: last physical line instead of the paragraph\n }\n for (const paragraph of paragraphs) {",
|
||||
}],
|
||||
},
|
||||
// #643: ребейз разрешает конфликт в генерируемом docs/reviews/INDEX.md
|
||||
// пересборкой — и только когда ВСЕ конфликты остановки — индекс.
|
||||
{
|
||||
id: 'rebase-index-resolves-mixed-conflicts',
|
||||
guard: 'node --test --test-name-pattern="#643 (planStop|AC2)" test/rebase-generated.test.mjs',
|
||||
because: '#643 AC2: a stop where the index conflicts together with any other path is a real '
|
||||
+ 'conflict; resolving it because the index is among the paths commits conflict markers or '
|
||||
+ 'loops the rebase instead of refusing with the list of files',
|
||||
patches: [{
|
||||
file: 'scripts/rebase-generated.mjs',
|
||||
find: " if (manual.length) return { action: 'abort', reason: 'manual', manual, conflicts };",
|
||||
replace: " if (manual.length && !conflicts.includes(REVIEWS_INDEX_PATH)) return { action: 'abort', reason: 'manual', manual, conflicts }; // mutant: index anywhere unlocks the stop",
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'rebase-index-takes-a-side',
|
||||
guard: 'node --test --test-name-pattern="#643 AC1" test/rebase-generated.test.mjs',
|
||||
because: '#643 AC1: neither side of the rebase knows the other side\'s review documents; taking '
|
||||
+ '--ours (dev) drops the branch\'s own document from the index, the result must be a rebuild',
|
||||
patches: [{
|
||||
file: 'scripts/rebase-generated.mjs',
|
||||
find: ' rebuildIndex();\n',
|
||||
replace: " git(['checkout', '--ours', '--', REVIEWS_INDEX_PATH]); // mutant: take dev's index\n",
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'rebase-index-failure-leaves-half-rebase',
|
||||
guard: 'node --test --test-name-pattern="#643: сбой пересборки" test/rebase-generated.test.mjs',
|
||||
because: 'a crash in the middle of the rebase (the generator failed) must abort it; otherwise '
|
||||
+ 'the caller\'s tree is left mid-rebase and the next step works on a detached half-result',
|
||||
patches: [{
|
||||
file: 'scripts/rebase-generated.mjs',
|
||||
find: " if (rebaseInProgress(git, cwd)) git(['rebase', '--abort'], { allowFailure: true });\n",
|
||||
replace: ' // mutant: no abort on failure\n',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'rebase-index-refusal-exits-like-a-crash',
|
||||
guard: 'node --test --test-name-pattern="#643 CLI" test/rebase-generated.test.mjs',
|
||||
because: 'Node exits with 1 on its own uncaught errors; a refusal reported as 1 is '
|
||||
+ 'indistinguishable from a crashed helper, and the pipeline would call a broken tool a branch conflict',
|
||||
patches: [{
|
||||
file: 'scripts/rebase-generated.mjs',
|
||||
find: ' process.exit(EXIT_CONFLICT);',
|
||||
replace: ' process.exit(1); // mutant: refusal = generic failure code',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'merge-rebase-refuses-index-conflict',
|
||||
guard: 'node --test --test-name-pattern="#643 AC1" test/merge-candidate.test.mjs',
|
||||
because: '#643: a green task whose doc commit conflicts with dev only in the generated INDEX.md '
|
||||
+ 'must merge through Validate; a plain rebase sends it back to S6 on every move of dev',
|
||||
patches: [{
|
||||
file: 'scripts/merge-candidate.mjs',
|
||||
find: ' const r = rebaseRegenerating({ onto, gitPrefix: CONVEYOR_IDENTITY });',
|
||||
replace: " const r = spawnSync('git', [...CONVEYOR_IDENTITY, 'rebase', onto]).status === 0 ? { ok: true } : (spawnSync('git', ['rebase', '--abort']), { ok: false, conflicts: [] }); // mutant: plain rebase",
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'rebase-on-dev-stages-index-unrebuilt',
|
||||
guard: 'node --test --test-name-pattern="#643 AC3" test/rebase-on-dev.test.mjs',
|
||||
because: '#643 AC3: the author\'s rebase must rebuild INDEX.md like the pipeline does; staging '
|
||||
+ 'the conflicted file as-is commits conflict markers into a generated file',
|
||||
patches: [{
|
||||
file: 'scripts/rebase-on-dev.mjs',
|
||||
find: ' onto: upstream, cwd, git,\n',
|
||||
replace: ' onto: upstream, cwd, git, rebuildIndex: () => {}, // mutant: index staged as-is\n',
|
||||
}],
|
||||
},
|
||||
// #634: ролевые конспекты PROCESS.md, цена входа агента, генерируемый
|
||||
// Snapshot и индекс приложений TESTING.md. Каждая защита — от тихого
|
||||
// расхождения выжимки с каноном или тихого роста входа.
|
||||
|
||||
Executable
+172
@@ -0,0 +1,172 @@
|
||||
#!/usr/bin/env node
|
||||
// #643: ребейз, который сам решает конфликт в генерируемом индексе ревью.
|
||||
//
|
||||
// Шаг публикации документа ревью коммитит в ветку задачи и документ, и
|
||||
// пересобранный `docs/reviews/INDEX.md`. Пока задача ждёт слияния, в `dev`
|
||||
// приезжают документы других задач со своей версией индекса, и doc-коммит
|
||||
// ветки конфликтует на ребейзе ВСЕГДА — 24.09 так отскочили в S6 зелёные
|
||||
// #617, #618, #629, #642 и дважды до ревью #631. Решать там нечего: индекс —
|
||||
// функция каталога `docs/reviews/` (класс C), и правильная версия — не «наша»
|
||||
// и не «их», а пересборка по дереву, в котором остановился ребейз.
|
||||
//
|
||||
// Правило одно: остановку разрешает только набор конфликтов, в котором ВСЕ
|
||||
// пути — индекс (или пути, которые вызывающий объявил своими: бандл в
|
||||
// `rebase-on-dev.mjs`). Хоть один другой путь — `git rebase --abort` и
|
||||
// перечень ВСЕХ конфликтующих файлов, индекс в нём тоже: автор видит полную
|
||||
// картину, дерево и HEAD как были.
|
||||
//
|
||||
// node scripts/rebase-generated.mjs --onto=origin/dev
|
||||
//
|
||||
// Код 0 — ребейз завершён; 3 — отказ (ребейз отменён), в stdout по строке на
|
||||
// конфликтующий путь (для `$GITHUB_OUTPUT` конвейера), пояснения — в stderr;
|
||||
// любой другой — сбой. Отказ намеренно не 1: единицей Node завершается сам на
|
||||
// необработанном исключении и ненайденном модуле, и сбой помощника читался
|
||||
// бы как конфликт ветки.
|
||||
// Личность коммитера берётся из окружения (конвейер задаёт GIT_AUTHOR_* и
|
||||
// GIT_COMMITTER_*) либо из `gitPrefix` (`-c user.name=…`) при вызове из кода.
|
||||
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { isMainModule } from './spawn-portable.mjs';
|
||||
|
||||
export const REVIEWS_INDEX_PATH = 'docs/reviews/INDEX.md';
|
||||
/** Скрипт индекса — по абсолютному пути: ребейз идёт и из чужого cwd (worktree кандидата). */
|
||||
export const REVIEWS_INDEX_SCRIPT = fileURLToPath(new URL('./reviews-index.mjs', import.meta.url));
|
||||
/** Предохранитель от зацикливания: коммитов в ветке задачи единицы, не тысячи. */
|
||||
export const MAX_STOPS = 500;
|
||||
/** Код выхода CLI «ребейз отменён, вот конфликты». */
|
||||
export const EXIT_CONFLICT = 3;
|
||||
|
||||
const uniquePaths = (paths) => [...new Set(paths.map((p) => String(p).trim()).filter(Boolean))].sort();
|
||||
|
||||
/**
|
||||
* Чистое решение по одной остановке ребейза.
|
||||
*
|
||||
* @param {string[]} paths конфликтующие пути (`git diff --name-only --diff-filter=U`)
|
||||
* @param {{ extra?: (path: string) => boolean }} [opts] пути, которые вызывающий решает сам
|
||||
* @returns {{ action: 'resolve', index: boolean, extra: string[], conflicts: string[] }
|
||||
* | { action: 'abort', reason: 'no-conflicts'|'manual', manual: string[], conflicts: string[] }}
|
||||
*/
|
||||
export function planStop(paths, { extra = () => false } = {}) {
|
||||
const conflicts = uniquePaths(paths);
|
||||
if (!conflicts.length) return { action: 'abort', reason: 'no-conflicts', manual: [], conflicts };
|
||||
const manual = conflicts.filter((path) => path !== REVIEWS_INDEX_PATH && !extra(path));
|
||||
if (manual.length) return { action: 'abort', reason: 'manual', manual, conflicts };
|
||||
return {
|
||||
action: 'resolve',
|
||||
index: conflicts.includes(REVIEWS_INDEX_PATH),
|
||||
extra: conflicts.filter((path) => path !== REVIEWS_INDEX_PATH),
|
||||
conflicts,
|
||||
};
|
||||
}
|
||||
|
||||
/** git-исполнитель: `(args, { allowFailure }) => { ok, status, stdout, stderr }`. */
|
||||
export function makeGit({ cwd = process.cwd(), env = process.env, gitPrefix = [] } = {}) {
|
||||
const run = (args, { allowFailure = false } = {}) => {
|
||||
const r = spawnSync('git', [...gitPrefix, ...args], {
|
||||
cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
|
||||
// Редактор сообщения на `rebase --continue` не открывается никогда.
|
||||
env: { ...env, GIT_EDITOR: 'true' },
|
||||
});
|
||||
if (r.error) throw r.error;
|
||||
const out = { ok: r.status === 0, status: r.status, stdout: (r.stdout || '').trim(), stderr: (r.stderr || '').trim() };
|
||||
if (!out.ok && !allowFailure) throw new Error(`git ${args.join(' ')} → ${out.stderr || out.stdout}`);
|
||||
return out;
|
||||
};
|
||||
run.cwd = cwd;
|
||||
return run;
|
||||
}
|
||||
|
||||
/** Пересборка индекса по каталогу в дереве остановки — тем же генератором, что и конвейер. */
|
||||
export function rebuildIndexWith({ cwd = process.cwd(), env = process.env } = {}) {
|
||||
return () => {
|
||||
const r = spawnSync(process.execPath, [REVIEWS_INDEX_SCRIPT, '--dir=docs/reviews'], {
|
||||
cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], env,
|
||||
});
|
||||
if (r.error) throw r.error;
|
||||
if (r.status !== 0) throw new Error(`reviews-index.mjs → ${(r.stderr || r.stdout || '').trim()}`);
|
||||
};
|
||||
}
|
||||
|
||||
const conflictedPaths = (git) => git(['diff', '--name-only', '--diff-filter=U']).stdout.split('\n');
|
||||
const rebaseInProgress = (git, cwd) => ['rebase-merge', 'rebase-apply'].some((name) => {
|
||||
const probe = git(['rev-parse', '--git-path', name], { allowFailure: true });
|
||||
return probe.ok && existsSync(resolve(git.cwd || cwd, probe.stdout));
|
||||
});
|
||||
|
||||
/**
|
||||
* Ребейз на `onto` с разрешением генерируемых конфликтов.
|
||||
*
|
||||
* Возвращает `{ ok: true, stops, resolved }` либо
|
||||
* `{ ok: false, reason, conflicts, manual, output }` — во втором случае ребейз
|
||||
* уже отменён. Сбой посреди ребейза (исключение) тоже отменяет его: дерево
|
||||
* вызывающего не остаётся в полуребейзе.
|
||||
*/
|
||||
export function rebaseRegenerating({
|
||||
onto, cwd = process.cwd(), env = process.env, gitPrefix = [],
|
||||
git = makeGit({ cwd, env, gitPrefix }),
|
||||
rebuildIndex = rebuildIndexWith({ cwd, env }),
|
||||
extra = null, log = () => {}, maxStops = MAX_STOPS,
|
||||
}) {
|
||||
if (!onto) throw new Error('rebaseRegenerating: не задано, на что ребейзить (onto)');
|
||||
const resolved = [];
|
||||
let stops = 0;
|
||||
const abort = (result) => {
|
||||
git(['rebase', '--abort'], { allowFailure: true });
|
||||
return { ok: false, resolved, stops, ...result };
|
||||
};
|
||||
try {
|
||||
let step = git(['rebase', onto], { allowFailure: true });
|
||||
while (!step.ok) {
|
||||
stops += 1;
|
||||
if (stops > maxStops) return abort({ reason: 'too-many-stops', conflicts: [], manual: [], output: step.stderr || step.stdout });
|
||||
const plan = planStop(conflictedPaths(git), { extra: extra?.match });
|
||||
if (plan.action === 'abort') {
|
||||
return abort({ reason: plan.reason, conflicts: plan.conflicts, manual: plan.manual, output: step.stderr || step.stdout });
|
||||
}
|
||||
for (const path of plan.extra) resolved.push(`${path} ← ${extra.resolve(path)}`);
|
||||
if (plan.index) {
|
||||
// Не --ours и не --theirs: ни одна сторона не знает документов другой.
|
||||
rebuildIndex();
|
||||
git(['add', '--', REVIEWS_INDEX_PATH]);
|
||||
resolved.push(`${REVIEWS_INDEX_PATH} ← пересборка`);
|
||||
}
|
||||
log(`остановка ${stops}: решено ${plan.conflicts.join(', ')}`);
|
||||
// Коммит, ставший пустым после пересборки (индекс-коммит конвейера
|
||||
// поверх уже пересобранного индекса), пропускается явно: что делает
|
||||
// `--continue` с пустым результатом, зависит от версии git и бэкенда
|
||||
// ребейза, а `--skip` при индексе, равном HEAD, не теряет ничего.
|
||||
const empty = git(['diff', '--cached', '--quiet', 'HEAD'], { allowFailure: true }).ok;
|
||||
step = git(['rebase', empty ? '--skip' : '--continue'], { allowFailure: true });
|
||||
}
|
||||
return { ok: true, stops, resolved };
|
||||
} catch (error) {
|
||||
if (rebaseInProgress(git, cwd)) git(['rebase', '--abort'], { allowFailure: true });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
if (isMainModule(import.meta.url)) { // #496: переносимо для Windows
|
||||
const onto = process.argv.find((a) => a.startsWith('--onto='))?.slice('--onto='.length);
|
||||
if (!onto) {
|
||||
console.error('usage: rebase-generated.mjs --onto=<ref>');
|
||||
process.exit(2);
|
||||
}
|
||||
try {
|
||||
const result = rebaseRegenerating({ onto, log: (line) => console.error(line) });
|
||||
if (result.ok) {
|
||||
console.error(`ребейз на ${onto} завершён; остановок решено: ${result.stops}${result.resolved.length ? ` (${result.resolved.join('; ')})` : ''}`);
|
||||
process.exit(0);
|
||||
}
|
||||
if (result.conflicts.length) process.stdout.write(`${result.conflicts.join('\n')}\n`);
|
||||
console.error(result.reason === 'manual'
|
||||
? `ребейз на ${onto} отменён: конфликт вне генерируемых путей — ${result.manual.join(', ')}`
|
||||
: `ребейз на ${onto} отменён (${result.reason}):\n${result.output}`);
|
||||
process.exit(EXIT_CONFLICT);
|
||||
} catch (error) {
|
||||
console.error(`rebase-generated: ${error.message}`);
|
||||
process.exit(2);
|
||||
}
|
||||
}
|
||||
+24
-19
@@ -7,7 +7,10 @@
|
||||
// content-hashed чанки. Руками это не решается, решается пересборкой. Скрипт
|
||||
// делает ровно это: при конфликте ТОЛЬКО в сгенерированных путях берёт версию
|
||||
// dev, доводит ребейз до конца, пересобирает бандл (`npm run bundle:sync`) и,
|
||||
// если он отличается, амендит последний коммит ветки. Конфликт в любом другом
|
||||
// если он отличается, амендит последний коммит ветки. Индекс ревью
|
||||
// `docs/reviews/INDEX.md` (#643) — тоже генерируемый: при конфликте он
|
||||
// пересобирается по каталогу в дереве остановки (общий помощник
|
||||
// `rebase-generated.mjs`, тот же, что у конвейера). Конфликт в любом другом
|
||||
// пути — останов с `git rebase --abort`: содержательные конфликты решает автор.
|
||||
//
|
||||
// node scripts/rebase-on-dev.mjs # ребейз текущей ветки
|
||||
@@ -20,17 +23,21 @@ import { existsSync, rmSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { portableCommand } from './spawn-portable.mjs';
|
||||
import { REVIEWS_INDEX_PATH, rebaseRegenerating } from './rebase-generated.mjs';
|
||||
|
||||
export const GENERATED_ROOTS = ['dist/', 'custom_components/houseplan/frontend/'];
|
||||
export const isGenerated = (path) => GENERATED_ROOTS.some((root) => path.startsWith(root));
|
||||
/** Генерируемые пути, которые решаются пересборкой в момент остановки, а не версией dev (#643). */
|
||||
export const REGENERATED_PATHS = [REVIEWS_INDEX_PATH];
|
||||
export const isRegenerated = (path) => REGENERATED_PATHS.includes(path);
|
||||
|
||||
/** Разделить конфликтующие пути: сгенерированные решаем сами, остальные — нет. */
|
||||
/** Разделить конфликтующие пути: бандл и индекс решаем сами, остальные — нет. */
|
||||
export function splitConflicts(paths) {
|
||||
const generated = []; const manual = [];
|
||||
const generated = []; const regenerated = []; const manual = [];
|
||||
for (const path of paths.map((p) => p.trim()).filter(Boolean)) {
|
||||
(isGenerated(path) ? generated : manual).push(path);
|
||||
(isGenerated(path) ? generated : isRegenerated(path) ? regenerated : manual).push(path);
|
||||
}
|
||||
return { generated, manual };
|
||||
return { generated, regenerated, manual };
|
||||
}
|
||||
|
||||
export function makeGit(cwd) {
|
||||
@@ -91,25 +98,23 @@ export function rebaseOnDev({
|
||||
const both = theirs.filter((path) => ours.has(path));
|
||||
const predicted = splitConflicts(both);
|
||||
if (predicted.generated.length) log(`бандл менялся с обеих сторон: ${predicted.generated.length} файл(ов) — решится пересборкой`);
|
||||
if (predicted.regenerated.length) log(`индекс ревью менялся с обеих сторон — решится пересборкой по каталогу: ${predicted.regenerated.join(', ')}`);
|
||||
if (predicted.manual.length) log(`менялись с обеих сторон и НЕ сгенерированы (возможен ручной конфликт): ${predicted.manual.join(', ')}`);
|
||||
if (dryRun) { log('--dry-run: дерево не тронуто'); return { branch, rebased: false, resolved: [], rebuilt: false, predicted }; }
|
||||
|
||||
const resolved = [];
|
||||
let step = git(['rebase', upstream], { allowFailure: true });
|
||||
while (!step.ok) {
|
||||
const conflicts = git(['diff', '--name-only', '--diff-filter=U']).stdout.split('\n').filter(Boolean);
|
||||
if (!conflicts.length) {
|
||||
git(['rebase', '--abort'], { allowFailure: true });
|
||||
throw new Error(`rebase остановился без конфликтов:\n${step.stderr || step.stdout}`);
|
||||
// Цикл остановок — общий с конвейером (#643): индекс ревью пересобирается
|
||||
// помощником, бандл — версией dev здесь, всё прочее — отказ с abort.
|
||||
const outcome = rebaseRegenerating({
|
||||
onto: upstream, cwd, git,
|
||||
extra: { match: isGenerated, resolve: (path) => resolveGeneratedConflict(git, path) },
|
||||
});
|
||||
if (!outcome.ok) {
|
||||
if (outcome.reason === 'manual') {
|
||||
throw new Error(`конфликт вне сгенерированных путей — ребейз отменён, дерево как было:\n ${outcome.manual.join('\n ')}`);
|
||||
}
|
||||
const { generated, manual } = splitConflicts(conflicts);
|
||||
if (manual.length) {
|
||||
git(['rebase', '--abort']);
|
||||
throw new Error(`конфликт вне сгенерированных путей — ребейз отменён, дерево как было:\n ${manual.join('\n ')}`);
|
||||
}
|
||||
for (const path of generated) resolved.push(`${path} ← ${resolveGeneratedConflict(git, path)}`);
|
||||
step = git(['rebase', '--continue'], { allowFailure: true });
|
||||
throw new Error(`rebase остановился без конфликтов (${outcome.reason}) — ребейз отменён:\n${outcome.output}`);
|
||||
}
|
||||
const { resolved } = outcome;
|
||||
log(`ребейз завершён; сгенерированных конфликтов решено: ${resolved.length}`);
|
||||
|
||||
// Пересборка: версия dev в бандле — не версия этой ветки. Собираем и, если
|
||||
|
||||
Reference in New Issue
Block a user