mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-06 22:49:16 +00:00
process: the rebase merges what two tasks never disagree on (#698)
14 of 48 returns in #600–#691 were rebase or merge conflicts on shared files where the two edits do not contradict each other. - .gitattributes: docs/CHANGELOG.md and docs/CHANGELOG.ru.md use the built-in merge=union driver — both tasks' lines in ## Unreleased survive a rebase, a merge and git merge-tree (#696's clean-merge test) without a stop. - rebase-generated.mjs: UPSTREAM_WINS — on a conflict in scripts/monolith-baseline.json the rebase takes dev's side; the band test on the candidate's Validate judges the merged tree (#699). Any other conflicting path aborts exactly as before, with the full list. - merge-candidate.mjs: the candidate's patch-id excludes the changelogs and the monolith baseline next to docs/reviews, so a neighbour's line next to the task entry does not re-send a green task to review. - screenshots.json needs nothing: after #697 task branches do not commit it. Canon: PROCESS.md, the rebase paragraph of the review index (#643). Issue: #698 User-Visible: no Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
This commit is contained in:
@@ -6,7 +6,8 @@ import { tmpdir } from 'node:os';
|
||||
import { dirname, join, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { REVIEWS_INDEX_PATH, planStop, rebaseRegenerating } from '../scripts/rebase-generated.mjs';
|
||||
import { REVIEWS_INDEX_PATH, UPSTREAM_WINS, planStop, rebaseRegenerating } from '../scripts/rebase-generated.mjs';
|
||||
import { PATCH_ID_EXCLUDES } from '../scripts/merge-candidate.mjs';
|
||||
import { buildIndex } from '../scripts/reviews-index.mjs';
|
||||
|
||||
// #643: doc-коммит ветки задачи конфликтует с dev только в генерируемом
|
||||
@@ -106,7 +107,7 @@ function scenario({ shared = false, tools = false } = {}) {
|
||||
|
||||
test('#643 planStop: разрешается только набор, где ВСЕ конфликты — индекс или объявленные вызывающим', () => {
|
||||
assert.deepEqual(planStop([REVIEWS_INDEX_PATH, '', ` ${REVIEWS_INDEX_PATH}`]),
|
||||
{ action: 'resolve', index: true, extra: [], conflicts: [REVIEWS_INDEX_PATH] });
|
||||
{ action: 'resolve', index: true, upstream: [], extra: [], conflicts: [REVIEWS_INDEX_PATH] });
|
||||
const mixed = planStop([REVIEWS_INDEX_PATH, 'src/x.ts']);
|
||||
assert.equal(mixed.action, 'abort');
|
||||
assert.equal(mixed.reason, 'manual');
|
||||
@@ -116,7 +117,7 @@ test('#643 planStop: разрешается только набор, где ВС
|
||||
assert.equal(planStop(['docs/reviews/sub/INDEX.md']).action, 'abort', 'другой INDEX.md — не индекс ревью');
|
||||
assert.deepEqual(planStop([]), { action: 'abort', reason: 'no-conflicts', manual: [], conflicts: [] });
|
||||
const bundle = planStop(['dist/a.js', REVIEWS_INDEX_PATH], { extra: (p) => p.startsWith('dist/') });
|
||||
assert.deepEqual(bundle, { action: 'resolve', index: true, extra: ['dist/a.js'], conflicts: ['dist/a.js', REVIEWS_INDEX_PATH] });
|
||||
assert.deepEqual(bundle, { action: 'resolve', index: true, upstream: [], extra: ['dist/a.js'], conflicts: ['dist/a.js', REVIEWS_INDEX_PATH] });
|
||||
});
|
||||
|
||||
test('#643 AC1: конфликт только в INDEX.md — ребейз проходит, индекс равен пересборке каталога', () => {
|
||||
@@ -293,3 +294,73 @@ test('#643 process.yml на настоящем bash: сбой помощника
|
||||
assert.match(r.stdout, /::error::помощник ребейза упал/);
|
||||
} finally { rmSync(root, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
// ---------- #698: ченджлог объединяется, база метрик берётся из dev ----------
|
||||
|
||||
function sharedFilesScenario({ baselineConflict = false, alsoCode = false } = {}) {
|
||||
const root = mkdtempSync(join(tmpdir(), 'hp-rebase-698-'));
|
||||
const work = join(root, 'work');
|
||||
mkdirSync(join(work, 'docs'), { recursive: true });
|
||||
mkdirSync(join(work, 'scripts'), { recursive: true });
|
||||
git(root, 'init', '-q', '-b', 'dev', work);
|
||||
copyFileSync(fileURLToPath(new URL('../.gitattributes', import.meta.url)), join(work, '.gitattributes'));
|
||||
const log = (lines) => writeFileSync(join(work, 'docs', 'CHANGELOG.md'), `# Changelog\n\n## Unreleased\n\n${lines.join('\n')}\n\n## 1.0.0\n\n- first\n`);
|
||||
const baseline = (hostRefs) => writeFileSync(join(work, 'scripts', 'monolith-baseline.json'), `${JSON.stringify({ delegates: 1, hostRefs }, null, 2)}\n`);
|
||||
log([]); baseline(100);
|
||||
writeFileSync(join(work, 'a.mjs'), 'export const a = 1;\n');
|
||||
commitAll(work, 'base');
|
||||
git(work, 'checkout', '-q', '-b', 'issue/9-x');
|
||||
log(['- task nine']);
|
||||
if (baselineConflict) baseline(95);
|
||||
if (alsoCode) writeFileSync(join(work, 'a.mjs'), 'export const a = 9;\n');
|
||||
commitAll(work, 'task');
|
||||
git(work, 'checkout', '-q', 'dev');
|
||||
log(['- task eight']);
|
||||
if (baselineConflict) baseline(103);
|
||||
if (alsoCode) writeFileSync(join(work, 'a.mjs'), 'export const a = 8;\n');
|
||||
commitAll(work, 'neighbour');
|
||||
git(work, 'checkout', '-q', 'issue/9-x');
|
||||
return { root, work };
|
||||
}
|
||||
|
||||
test('#698: записи ченджлога двух задач объединяются при ребейзе, конфликта нет', (t) => {
|
||||
const { root, work } = sharedFilesScenario();
|
||||
t.after(() => rmSync(root, { recursive: true, force: true }));
|
||||
const result = rebaseRegenerating({ onto: 'dev', cwd: work, env: ENV });
|
||||
assert.equal(result.ok, true, JSON.stringify(result));
|
||||
assert.equal(result.stops, 0, 'union — это не остановка ребейза');
|
||||
const text = readFileSync(join(work, 'docs', 'CHANGELOG.md'), 'utf8');
|
||||
assert.match(text, /- task eight\n- task nine\n/, 'обе записи в Unreleased, сторона dev первой');
|
||||
assert.doesNotMatch(text, /^(<<<<<<<|=======|>>>>>>>)/m);
|
||||
});
|
||||
|
||||
test('#698: конфликт в базе метрик монолита решается в пользу dev', (t) => {
|
||||
const { root, work } = sharedFilesScenario({ baselineConflict: true });
|
||||
t.after(() => rmSync(root, { recursive: true, force: true }));
|
||||
const result = rebaseRegenerating({ onto: 'dev', cwd: work, env: ENV });
|
||||
assert.equal(result.ok, true, JSON.stringify(result));
|
||||
assert.deepEqual(result.resolved, ['scripts/monolith-baseline.json ← dev']);
|
||||
assert.equal(JSON.parse(readFileSync(join(work, 'scripts', 'monolith-baseline.json'), 'utf8')).hostRefs, 103);
|
||||
assert.match(readFileSync(join(work, 'docs', 'CHANGELOG.md'), 'utf8'), /- task nine/);
|
||||
assert.deepEqual(UPSTREAM_WINS, ['scripts/monolith-baseline.json']);
|
||||
});
|
||||
|
||||
test('#698: база метрик вместе с конфликтом в коде — прежний отказ с перечнем', (t) => {
|
||||
const { root, work } = sharedFilesScenario({ baselineConflict: true, alsoCode: true });
|
||||
t.after(() => rmSync(root, { recursive: true, force: true }));
|
||||
const before = git(work, 'rev-parse', 'HEAD');
|
||||
const result = rebaseRegenerating({ onto: 'dev', cwd: work, env: ENV });
|
||||
assert.equal(result.ok, false);
|
||||
assert.deepEqual(result.manual, ['a.mjs']);
|
||||
assert.deepEqual(result.conflicts, ['a.mjs', 'scripts/monolith-baseline.json']);
|
||||
assert.equal(git(work, 'rev-parse', 'HEAD'), before, 'ребейз отменён, HEAD как был');
|
||||
});
|
||||
|
||||
test('#698: patch-id кандидата не видит того, что ребейз сливает сам', () => {
|
||||
for (const path of ['docs/reviews', 'docs/CHANGELOG.md', 'docs/CHANGELOG.ru.md', 'scripts/monolith-baseline.json']) {
|
||||
assert.ok(PATCH_ID_EXCLUDES.includes(`:!${path}`), path);
|
||||
}
|
||||
const attrs = readFileSync(fileURLToPath(new URL('../.gitattributes', import.meta.url)), 'utf8');
|
||||
assert.match(attrs, /^docs\/CHANGELOG\.md merge=union$/m);
|
||||
assert.match(attrs, /^docs\/CHANGELOG\.ru\.md merge=union$/m);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user