@webpieces/pr-gate 0.4.495 → 0.4.496
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/package.json +2 -2
- package/src/scripts/commands/cleanup-command.d.ts +28 -6
- package/src/scripts/commands/cleanup-command.js +121 -19
- package/src/scripts/commands/cleanup-command.js.map +1 -1
- package/src/scripts/commands/land-pr-command.d.ts +19 -2
- package/src/scripts/commands/land-pr-command.js +51 -3
- package/src/scripts/commands/land-pr-command.js.map +1 -1
- package/src/scripts/workflow/cleanTmp.js +4 -1
- package/src/scripts/workflow/cleanTmp.js.map +1 -1
- package/src/scripts/workflow/merge-info-index.d.ts +60 -0
- package/src/scripts/workflow/merge-info-index.js +164 -0
- package/src/scripts/workflow/merge-info-index.js.map +1 -0
- package/src/scripts/workflow/merge-start.d.ts +1 -0
- package/src/scripts/workflow/merge-start.js +24 -6
- package/src/scripts/workflow/merge-start.js.map +1 -1
- package/src/scripts/workflow/merge-state.d.ts +78 -2
- package/src/scripts/workflow/merge-state.js +144 -17
- package/src/scripts/workflow/merge-state.js.map +1 -1
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MergeInfoIndex = exports.MergeInfoIndexFile = exports.MergedBranchEntry = exports.ArchiveRecord = exports.MergeRecord = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const fs = tslib_1.__importStar(require("fs"));
|
|
6
|
+
const path = tslib_1.__importStar(require("path"));
|
|
7
|
+
const rules_config_1 = require("@webpieces/rules-config");
|
|
8
|
+
const inversify_1 = require("inversify");
|
|
9
|
+
const merge_state_1 = require("./merge-state");
|
|
10
|
+
/**
|
|
11
|
+
* `.webpieces/merge-info/index.json` — the one answer to "which merges, across ALL branches, were
|
|
12
|
+
* 3-point?", which is the actual review question.
|
|
13
|
+
*
|
|
14
|
+
* WHY AN INDEX IS REQUIRED and a directory name will not do: a single branch can alternate
|
|
15
|
+
* clean → 3-point → clean → 3-point across its syncs. So the clean/3-point axis belongs to the MERGE,
|
|
16
|
+
* not the branch, and cannot be encoded in `merged/<branch>/`. With the index, one jq lists every
|
|
17
|
+
* 3-point merge in the repo and stays correct for alternating branches:
|
|
18
|
+
*
|
|
19
|
+
* jq -r '.merged | to_entries[] | .key as $b | .value.merges[] |
|
|
20
|
+
* select(.threeWay) | "\($b) merge-\(.n)"' .webpieces/merge-info/index.json
|
|
21
|
+
*
|
|
22
|
+
* WHY IT IS DERIVED, NOT INCREMENTALLY WRITTEN: every fact in it already exists on disk — `threeWay`
|
|
23
|
+
* is the presence of `conflicts.md` in the run dir, `conflicts` is that file's list, and the archive
|
|
24
|
+
* fields come from `archive.json`. Rebuilding by scanning `merged/` makes the index a pure projection
|
|
25
|
+
* that cannot drift from the directories, and makes a lost or corrupt index self-healing.
|
|
26
|
+
*/
|
|
27
|
+
// One merge slot within a branch. Data-only (per CLAUDE.md, classes for data).
|
|
28
|
+
class MergeRecord {
|
|
29
|
+
n;
|
|
30
|
+
threeWay;
|
|
31
|
+
// Only non-empty when threeWay — the files the AI actually had to resolve.
|
|
32
|
+
conflicts;
|
|
33
|
+
constructor(n, threeWay, conflicts) {
|
|
34
|
+
this.n = n;
|
|
35
|
+
this.threeWay = threeWay;
|
|
36
|
+
this.conflicts = conflicts;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
exports.MergeRecord = MergeRecord;
|
|
40
|
+
// `{ archiveTag, tipSha, baseSha, pr, mergedAt }` — the landed branch's archive record, written into
|
|
41
|
+
// `merged/<feature>/archive.json` and mirrored into the index so one file answers everything.
|
|
42
|
+
class ArchiveRecord {
|
|
43
|
+
archiveTag;
|
|
44
|
+
tipSha;
|
|
45
|
+
baseSha;
|
|
46
|
+
pr;
|
|
47
|
+
mergedAt;
|
|
48
|
+
// eslint-disable-next-line @typescript-eslint/max-params
|
|
49
|
+
constructor(archiveTag, tipSha, baseSha, pr, mergedAt) {
|
|
50
|
+
this.archiveTag = archiveTag;
|
|
51
|
+
this.tipSha = tipSha;
|
|
52
|
+
this.baseSha = baseSha;
|
|
53
|
+
this.pr = pr;
|
|
54
|
+
this.mergedAt = mergedAt;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
exports.ArchiveRecord = ArchiveRecord;
|
|
58
|
+
// One landed branch's whole story: which PR, where its history is archived, and every merge it took.
|
|
59
|
+
class MergedBranchEntry {
|
|
60
|
+
pr;
|
|
61
|
+
archiveTag;
|
|
62
|
+
merges;
|
|
63
|
+
constructor(pr, archiveTag, merges) {
|
|
64
|
+
this.pr = pr;
|
|
65
|
+
this.archiveTag = archiveTag;
|
|
66
|
+
this.merges = merges;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
exports.MergedBranchEntry = MergedBranchEntry;
|
|
70
|
+
// The whole file. `merged` is keyed by feature slug, matching the `merged/<feature>/` dir names.
|
|
71
|
+
class MergeInfoIndexFile {
|
|
72
|
+
merged;
|
|
73
|
+
constructor(merged) {
|
|
74
|
+
this.merged = merged;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
exports.MergeInfoIndexFile = MergeInfoIndexFile;
|
|
78
|
+
let MergeInfoIndex = class MergeInfoIndex {
|
|
79
|
+
mergeState;
|
|
80
|
+
constructor(mergeState) {
|
|
81
|
+
this.mergeState = mergeState;
|
|
82
|
+
}
|
|
83
|
+
indexPath(repoRoot) {
|
|
84
|
+
return path.join(this.mergeState.mergeInfoRoot(repoRoot), merge_state_1.MERGE_INDEX_FILE);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* MOVE a landed feature's whole `staged/<feature>/` tree to `merged/<feature>/`, drop its
|
|
88
|
+
* `archive.json` in, and rebuild the index. This is what makes `staged/` self-cleaning: it holds
|
|
89
|
+
* only branches still in flight, instead of growing for the life of the repo.
|
|
90
|
+
*
|
|
91
|
+
* Returns false when there was nothing staged to promote (a branch that landed without ever syncing
|
|
92
|
+
* from main), which is a normal outcome and not an error.
|
|
93
|
+
*/
|
|
94
|
+
promoteToMerged(repoRoot, featureName, archive) {
|
|
95
|
+
const staged = this.mergeState.stagedDirFor(repoRoot, featureName);
|
|
96
|
+
const merged = this.mergeState.mergedDirFor(repoRoot, featureName);
|
|
97
|
+
if (!fs.existsSync(staged)) {
|
|
98
|
+
this.rebuildIndex(repoRoot);
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
fs.mkdirSync(path.dirname(merged), { recursive: true });
|
|
102
|
+
// An existing merged/<feature> means the same branch name landed before. Keep BOTH — the older
|
|
103
|
+
// record moves aside rather than being overwritten, since it is the audit trail of a real PR.
|
|
104
|
+
if (fs.existsSync(merged))
|
|
105
|
+
fs.renameSync(merged, `${merged}-prev-${String(Date.now())}`);
|
|
106
|
+
fs.renameSync(staged, merged);
|
|
107
|
+
fs.writeFileSync(path.join(merged, merge_state_1.ARCHIVE_RECORD_FILE), JSON.stringify(archive, null, 2) + '\n');
|
|
108
|
+
this.rebuildIndex(repoRoot);
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
/** Rebuild `index.json` by scanning `merged/` from scratch. Pure projection of what is on disk. */
|
|
112
|
+
rebuildIndex(repoRoot) {
|
|
113
|
+
const mergedRoot = path.join(this.mergeState.mergeInfoRoot(repoRoot), merge_state_1.MERGED_DIR);
|
|
114
|
+
const entries = {};
|
|
115
|
+
if (fs.existsSync(mergedRoot)) {
|
|
116
|
+
for (const feature of fs.readdirSync(mergedRoot)) {
|
|
117
|
+
const home = path.join(mergedRoot, feature);
|
|
118
|
+
if (!fs.statSync(home).isDirectory())
|
|
119
|
+
continue;
|
|
120
|
+
entries[feature] = this.entryFor(home);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const index = new MergeInfoIndexFile(entries);
|
|
124
|
+
const target = this.indexPath(repoRoot);
|
|
125
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
126
|
+
fs.writeFileSync(target, JSON.stringify(index, null, 2) + '\n');
|
|
127
|
+
return index;
|
|
128
|
+
}
|
|
129
|
+
// One landed branch, derived entirely from its own directory: the archive record it was promoted
|
|
130
|
+
// with, plus one MergeRecord per `merge-<n>/` whose threeWay is the presence of conflicts.md.
|
|
131
|
+
entryFor(home) {
|
|
132
|
+
const archive = this.readArchive(home);
|
|
133
|
+
const merges = [];
|
|
134
|
+
for (const n of this.mergeState.listMergeRunDirs(home)) {
|
|
135
|
+
const runDir = path.join(home, `merge-${String(n)}`);
|
|
136
|
+
const threeWay = this.mergeState.wasThreeWay(runDir);
|
|
137
|
+
merges.push(new MergeRecord(n, threeWay, threeWay ? this.mergeState.readConflictedFiles(runDir) : []));
|
|
138
|
+
}
|
|
139
|
+
return new MergedBranchEntry(archive.pr, archive.archiveTag, merges);
|
|
140
|
+
}
|
|
141
|
+
// Missing/corrupt archive.json degrades to empty fields rather than failing the whole index — the
|
|
142
|
+
// merge records are still worth publishing, and a lost archive tag is not worth losing them over.
|
|
143
|
+
readArchive(home) {
|
|
144
|
+
const filePath = path.join(home, merge_state_1.ARCHIVE_RECORD_FILE);
|
|
145
|
+
if (!fs.existsSync(filePath))
|
|
146
|
+
return new ArchiveRecord('', '', '', 0, '');
|
|
147
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
148
|
+
try {
|
|
149
|
+
const raw = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
150
|
+
return new ArchiveRecord(raw.archiveTag ?? '', raw.tipSha ?? '', raw.baseSha ?? '', raw.pr ?? 0, raw.mergedAt ?? '');
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
const error = (0, rules_config_1.toError)(err);
|
|
154
|
+
void error;
|
|
155
|
+
return new ArchiveRecord('', '', '', 0, '');
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
exports.MergeInfoIndex = MergeInfoIndex;
|
|
160
|
+
exports.MergeInfoIndex = MergeInfoIndex = tslib_1.__decorate([
|
|
161
|
+
(0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton),
|
|
162
|
+
tslib_1.__metadata("design:paramtypes", [merge_state_1.MergeState])
|
|
163
|
+
], MergeInfoIndex);
|
|
164
|
+
//# sourceMappingURL=merge-info-index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"merge-info-index.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/merge-info-index.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport { toError } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nimport { ARCHIVE_RECORD_FILE, MERGED_DIR, MERGE_INDEX_FILE, MergeState } from './merge-state';\n\n/**\n * `.webpieces/merge-info/index.json` — the one answer to \"which merges, across ALL branches, were\n * 3-point?\", which is the actual review question.\n *\n * WHY AN INDEX IS REQUIRED and a directory name will not do: a single branch can alternate\n * clean → 3-point → clean → 3-point across its syncs. So the clean/3-point axis belongs to the MERGE,\n * not the branch, and cannot be encoded in `merged/<branch>/`. With the index, one jq lists every\n * 3-point merge in the repo and stays correct for alternating branches:\n *\n * jq -r '.merged | to_entries[] | .key as $b | .value.merges[] |\n * select(.threeWay) | \"\\($b) merge-\\(.n)\"' .webpieces/merge-info/index.json\n *\n * WHY IT IS DERIVED, NOT INCREMENTALLY WRITTEN: every fact in it already exists on disk — `threeWay`\n * is the presence of `conflicts.md` in the run dir, `conflicts` is that file's list, and the archive\n * fields come from `archive.json`. Rebuilding by scanning `merged/` makes the index a pure projection\n * that cannot drift from the directories, and makes a lost or corrupt index self-healing.\n */\n\n// One merge slot within a branch. Data-only (per CLAUDE.md, classes for data).\nexport class MergeRecord {\n n: number;\n threeWay: boolean;\n // Only non-empty when threeWay — the files the AI actually had to resolve.\n conflicts: string[];\n\n constructor(n: number, threeWay: boolean, conflicts: string[]) {\n this.n = n;\n this.threeWay = threeWay;\n this.conflicts = conflicts;\n }\n}\n\n// `{ archiveTag, tipSha, baseSha, pr, mergedAt }` — the landed branch's archive record, written into\n// `merged/<feature>/archive.json` and mirrored into the index so one file answers everything.\nexport class ArchiveRecord {\n archiveTag: string;\n tipSha: string;\n baseSha: string;\n pr: number;\n mergedAt: string;\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(archiveTag: string, tipSha: string, baseSha: string, pr: number, mergedAt: string) {\n this.archiveTag = archiveTag;\n this.tipSha = tipSha;\n this.baseSha = baseSha;\n this.pr = pr;\n this.mergedAt = mergedAt;\n }\n}\n\n// One landed branch's whole story: which PR, where its history is archived, and every merge it took.\nexport class MergedBranchEntry {\n pr: number;\n archiveTag: string;\n merges: MergeRecord[];\n\n constructor(pr: number, archiveTag: string, merges: MergeRecord[]) {\n this.pr = pr;\n this.archiveTag = archiveTag;\n this.merges = merges;\n }\n}\n\n// The whole file. `merged` is keyed by feature slug, matching the `merged/<feature>/` dir names.\nexport class MergeInfoIndexFile {\n merged: Record<string, MergedBranchEntry>;\n\n constructor(merged: Record<string, MergedBranchEntry>) {\n this.merged = merged;\n }\n}\n\n// Raw JSON shape for the cast at the parse boundary.\ninterface RawArchiveRecord {\n archiveTag?: string;\n tipSha?: string;\n baseSha?: string;\n pr?: number;\n mergedAt?: string;\n}\n\n@injectable(bindingScopeValues.Singleton)\nexport class MergeInfoIndex {\n constructor(private readonly mergeState: MergeState) {}\n\n indexPath(repoRoot: string): string {\n return path.join(this.mergeState.mergeInfoRoot(repoRoot), MERGE_INDEX_FILE);\n }\n\n /**\n * MOVE a landed feature's whole `staged/<feature>/` tree to `merged/<feature>/`, drop its\n * `archive.json` in, and rebuild the index. This is what makes `staged/` self-cleaning: it holds\n * only branches still in flight, instead of growing for the life of the repo.\n *\n * Returns false when there was nothing staged to promote (a branch that landed without ever syncing\n * from main), which is a normal outcome and not an error.\n */\n promoteToMerged(repoRoot: string, featureName: string, archive: ArchiveRecord): boolean {\n const staged = this.mergeState.stagedDirFor(repoRoot, featureName);\n const merged = this.mergeState.mergedDirFor(repoRoot, featureName);\n if (!fs.existsSync(staged)) {\n this.rebuildIndex(repoRoot);\n return false;\n }\n fs.mkdirSync(path.dirname(merged), { recursive: true });\n // An existing merged/<feature> means the same branch name landed before. Keep BOTH — the older\n // record moves aside rather than being overwritten, since it is the audit trail of a real PR.\n if (fs.existsSync(merged)) fs.renameSync(merged, `${merged}-prev-${String(Date.now())}`);\n fs.renameSync(staged, merged);\n fs.writeFileSync(path.join(merged, ARCHIVE_RECORD_FILE), JSON.stringify(archive, null, 2) + '\\n');\n this.rebuildIndex(repoRoot);\n return true;\n }\n\n /** Rebuild `index.json` by scanning `merged/` from scratch. Pure projection of what is on disk. */\n rebuildIndex(repoRoot: string): MergeInfoIndexFile {\n const mergedRoot = path.join(this.mergeState.mergeInfoRoot(repoRoot), MERGED_DIR);\n const entries: Record<string, MergedBranchEntry> = {};\n if (fs.existsSync(mergedRoot)) {\n for (const feature of fs.readdirSync(mergedRoot)) {\n const home = path.join(mergedRoot, feature);\n if (!fs.statSync(home).isDirectory()) continue;\n entries[feature] = this.entryFor(home);\n }\n }\n const index = new MergeInfoIndexFile(entries);\n const target = this.indexPath(repoRoot);\n fs.mkdirSync(path.dirname(target), { recursive: true });\n fs.writeFileSync(target, JSON.stringify(index, null, 2) + '\\n');\n return index;\n }\n\n // One landed branch, derived entirely from its own directory: the archive record it was promoted\n // with, plus one MergeRecord per `merge-<n>/` whose threeWay is the presence of conflicts.md.\n private entryFor(home: string): MergedBranchEntry {\n const archive = this.readArchive(home);\n const merges: MergeRecord[] = [];\n for (const n of this.mergeState.listMergeRunDirs(home)) {\n const runDir = path.join(home, `merge-${String(n)}`);\n const threeWay = this.mergeState.wasThreeWay(runDir);\n merges.push(new MergeRecord(n, threeWay, threeWay ? this.mergeState.readConflictedFiles(runDir) : []));\n }\n return new MergedBranchEntry(archive.pr, archive.archiveTag, merges);\n }\n\n // Missing/corrupt archive.json degrades to empty fields rather than failing the whole index — the\n // merge records are still worth publishing, and a lost archive tag is not worth losing them over.\n private readArchive(home: string): ArchiveRecord {\n const filePath = path.join(home, ARCHIVE_RECORD_FILE);\n if (!fs.existsSync(filePath)) return new ArchiveRecord('', '', '', 0, '');\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const raw = JSON.parse(fs.readFileSync(filePath, 'utf8')) as RawArchiveRecord;\n return new ArchiveRecord(\n raw.archiveTag ?? '', raw.tipSha ?? '', raw.baseSha ?? '', raw.pr ?? 0, raw.mergedAt ?? '');\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return new ArchiveRecord('', '', '', 0, '');\n }\n }\n}\n"]}
|
|
@@ -39,14 +39,16 @@ class MergeStartResult {
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
exports.MergeStartResult = MergeStartResult;
|
|
42
|
-
// The one number `n` for a sync and the
|
|
43
|
-
//
|
|
42
|
+
// The one number `n` for a sync and the things it names: the pre-merge backup branch, its paired
|
|
43
|
+
// conflict-context run dir, and (via `n`) the `preMerge<n>.hash` record of the tip it snapshotted.
|
|
44
44
|
class SyncSlot {
|
|
45
45
|
backupBranch;
|
|
46
46
|
runDir;
|
|
47
|
-
|
|
47
|
+
n;
|
|
48
|
+
constructor(backupBranch, runDir, n) {
|
|
48
49
|
this.backupBranch = backupBranch;
|
|
49
50
|
this.runDir = runDir;
|
|
51
|
+
this.n = n;
|
|
50
52
|
}
|
|
51
53
|
}
|
|
52
54
|
// Single source of truth for the merge process (written at conflict time, parameterized with the live
|
|
@@ -169,6 +171,11 @@ let MergeStart = class MergeStart {
|
|
|
169
171
|
const prNumber = this.detectPr(this.branchNaming.baseBranchName(currentBranch));
|
|
170
172
|
process.stdout.write(prNumber ? `Existing PR #${prNumber} will be updated.\n` : 'No existing PR (one can be created later).\n');
|
|
171
173
|
this.createBackup(currentBranch, backupBranch);
|
|
174
|
+
// Record THIS sync's pre-merge tip as `staged/<feature>/preMerge<n>.hash` — every intermediate
|
|
175
|
+
// state, not just the last one. It is what lets the `<feature>PreMerge<n>` snapshot BRANCH be
|
|
176
|
+
// disposable (branches count toward the branch cap; a written-down hash does not), and it is the
|
|
177
|
+
// ref the archive tag is cut from when the PR lands.
|
|
178
|
+
this.mergeState.writePreMergeHash(home, slot.n, this.fullSha(currentBranch));
|
|
172
179
|
const backupEvent = new rules_config_1.BranchMutationEvent(verb, 'BACKUP');
|
|
173
180
|
backupEvent.fromBranch = currentBranch;
|
|
174
181
|
backupEvent.toBranch = backupBranch;
|
|
@@ -202,7 +209,8 @@ let MergeStart = class MergeStart {
|
|
|
202
209
|
// reaches main's history: finish-upsert-pr squash-merges the PR with an explicit
|
|
203
210
|
// `gh pr merge --subject <PR title> --body-file <commit summary>`, so main carries the PR title.
|
|
204
211
|
this.gitExec.runGitChecked(['commit', '-m', `Squash merge of ${currentBranch}`], 'Failed to commit squash merge');
|
|
205
|
-
|
|
212
|
+
// Clean merge ⇒ hashes only, no conflicts.md. Its ABSENCE is what marks this merge clean.
|
|
213
|
+
this.mergeState.recordCleanMerge(mergeDir, hashes.hashForkPoint, hashes.hashFeatureHead, hashes.hashMainHead);
|
|
206
214
|
}
|
|
207
215
|
return new MergeStartResult('clean', new MergeContext(currentBranch, squashBranch, backupBranch, prNumber), mergeDir);
|
|
208
216
|
}
|
|
@@ -219,7 +227,7 @@ let MergeStart = class MergeStart {
|
|
|
219
227
|
let n = this.mergeState.nextMergeSlotNumber(home);
|
|
220
228
|
while (branchExists(this.branchNaming.preMergeBackupName(currentBranch, n)))
|
|
221
229
|
n += 1;
|
|
222
|
-
return new SyncSlot(this.branchNaming.preMergeBackupName(currentBranch, n), this.mergeState.mergeRunDirFor(home, n));
|
|
230
|
+
return new SyncSlot(this.branchNaming.preMergeBackupName(currentBranch, n), this.mergeState.mergeRunDirFor(home, n), n);
|
|
223
231
|
}
|
|
224
232
|
// Snapshot the pre-merge state onto the caller-chosen `backupBranch`, never overwriting.
|
|
225
233
|
createBackup(currentBranch, backupBranch) {
|
|
@@ -285,7 +293,10 @@ let MergeStart = class MergeStart {
|
|
|
285
293
|
const raw = (0, child_process_1.execSync)('git diff --name-only --diff-filter=U', { encoding: 'utf8' }).trim();
|
|
286
294
|
const conflictedFiles = raw.split('\n').filter((f) => f.trim() !== '');
|
|
287
295
|
fs.mkdirSync(mergeDir, { recursive: true });
|
|
288
|
-
|
|
296
|
+
// `conflicts.md` replaces the old `updatemain-conflicted-files.txt` (which nothing read) AND is
|
|
297
|
+
// the 3-point signal itself: this file exists in a run dir if and only if that merge conflicted,
|
|
298
|
+
// which is what lets the index classify each merge without a per-branch directory-name scheme.
|
|
299
|
+
this.mergeState.writeConflicts(mergeDir, conflictedFiles);
|
|
289
300
|
fs.writeFileSync(path.join(mergeDir, 'updatemain-hashes.json'), JSON.stringify(hashes, null, 2) + '\n');
|
|
290
301
|
this.saveConflictContext(conflictedFiles, mergeDir, hashes.hashForkPoint, hashes.hashFeatureHead, hashes.hashMainHead);
|
|
291
302
|
const marker = new merge_state_1.MergeMarker(currentBranch, squashBranch, backupBranch, prNumber, conflictedFiles, hashes.hashForkPoint, hashes.hashFeatureHead, hashes.hashMainHead, false);
|
|
@@ -298,6 +309,13 @@ let MergeStart = class MergeStart {
|
|
|
298
309
|
const result = (0, child_process_1.spawnSync)('git', ['rev-parse', '--short', ref], { encoding: 'utf8' });
|
|
299
310
|
return result.status === 0 ? (result.stdout ?? '').trim() : '';
|
|
300
311
|
}
|
|
312
|
+
// FULL sha of a ref (best-effort — '' if it can't resolve). Recorded for the pre-merge tips because
|
|
313
|
+
// those hashes are meant to be used later to restore state, and an abbreviated sha can go ambiguous
|
|
314
|
+
// as the repo grows.
|
|
315
|
+
fullSha(ref) {
|
|
316
|
+
const result = (0, child_process_1.spawnSync)('git', ['rev-parse', ref], { encoding: 'utf8' });
|
|
317
|
+
return result.status === 0 ? (result.stdout ?? '').trim() : '';
|
|
318
|
+
}
|
|
301
319
|
// Log the CONFLICT phase with the conflicted-file list + artifact paths a resolver needs.
|
|
302
320
|
logConflict(repoRoot, verb, mergeDir) {
|
|
303
321
|
const raw = (0, child_process_1.spawnSync)('git', ['diff', '--name-only', '--diff-filter=U'], { cwd: repoRoot, encoding: 'utf8' });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"merge-start.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/merge-start.ts"],"names":[],"mappings":"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","sourcesContent":["import { execSync, spawnSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport {\n WEBPIECES_TMP_DIR, MERGE_EXPLANATION_FILE, CliExitError,\n MutationVerb, BranchMutationEvent, logBranchMutation, SyncFlowGuidance,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { GatherInfo } from '../git-gatherInfo';\nimport { BranchNaming } from './branch-naming';\nimport { GitExec } from './git-exec';\nimport { MergeState, MergeMarker } from './merge-state';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\ninterface HashPoints {\n hashForkPoint: string;\n hashFeatureHead: string;\n hashMainHead: string;\n}\n\n// The four branch names merge-END needs to finalize a merge (swap squash→feature, push, clean up).\nexport class MergeContext {\n currentBranch: string;\n squashBranch: string;\n backupBranch: string;\n prNumber: string;\n\n constructor(currentBranch: string, squashBranch: string, backupBranch: string, prNumber: string) {\n this.currentBranch = currentBranch;\n this.squashBranch = squashBranch;\n this.backupBranch = backupBranch;\n this.prNumber = prNumber;\n }\n}\n\n// Outcome of merge-start: 'clean' carries the context for merge-END to finalize; 'conflict' means the\n// marker + context files were written and the caller should hand back to the AI (exit 2).\nexport class MergeStartResult {\n status: 'clean' | 'conflict';\n context: MergeContext | null;\n runDir: string; // this sync's numbered `merge-<n>/` dir — passed to merge-END so it reads THIS marker\n\n constructor(status: 'clean' | 'conflict', context: MergeContext | null, runDir: string) {\n this.status = status;\n this.context = context;\n this.runDir = runDir;\n }\n}\n\n// The one number `n` for a sync and the two things it names: the pre-merge backup branch and its\n// paired conflict-context run dir.\nclass SyncSlot {\n backupBranch: string;\n runDir: string;\n\n constructor(backupBranch: string, runDir: string) {\n this.backupBranch = backupBranch;\n this.runDir = runDir;\n }\n}\n\n// Single source of truth for the merge process (written at conflict time, parameterized with the live\n// MERGE_DIR + conflicted-file list so it can never drift from the actual layout).\nconst MERGE_PROCESS_TEMPLATE = `# AI-Assisted Squash-Merge Conflict Resolution (generated)\n\nThis file was generated by \\`pnpm {{START_COMMAND}}\\` when the 3-point squash-merge hit conflicts.\nIt is the single source of truth for the merge process — follow it exactly.\n\nYou are on branch \\`{{SQUASH_BRANCH}}\\` with conflict markers in the working tree.\n\\`MERGE_DIR = {{MERGE_DIR}}\\`\n\n## How the gate works\n\n- Resolve every conflicted file in the working tree.\n- Run **\\`pnpm {{FINISH_COMMAND}}\\`** — the validation + finish gate. It scans for leftover conflict\n markers, checks each conflicted file has a written merge explanation, validates, and commits the\n merge. It is the paired half of \\`{{START_COMMAND}}\\` — never finish with the other flow's command\n (\\`wp-start-update\\` pairs with \\`wp-finish-update\\`; \\`wp-start-upsert-pr\\` pairs with\n \\`wp-finish-upsert-pr\\`, which additionally runs the \\`nx affected\\` build, renders the dashboard,\n and creates/updates the PR).\n- **Do NOT run \\`git add\\` / \\`git commit\\` / \\`git push\\` / \\`gh pr\\` yourself.** They are blocked by\n the \\`merge-in-progress-guard\\` hook until the gate validates. The gate does the commit.\n\n## STEP 1 — Load the merge context\n\nPer conflicted file, \\`MERGE_DIR/updatemain-<safe_path>/\\` holds (\\`<safe_path>\\` = path with \\`/\\`→\\`__\\`):\n\n\\`\\`\\`\nA-forkpoint.txt # file at fork point (base)\nB-feature.txt # file on your feature branch\nC-main.txt # file on main\nB-A.diff # what your feature changed (B−A)\nC-A.diff # what main changed (C−A)\n\\`\\`\\`\n\n\\`updatemain-hashes.json\\` holds A/B/C commit hashes. To see why main changed:\n\\`git log <A>..<C> --oneline\\`.\n\n## STEP 2 — Resolve each conflicted file\n\nFor each file: read the working-tree file (the markers) and its \\`B-A.diff\\` / \\`C-A.diff\\` (intent),\nthen Edit to the resolved version, removing ALL conflict markers.\n\nStrategies: goals align & non-overlapping → merge both · one side removes what the other modifies\n→ prefer the removal · same lines, simple (imports/format) → merge both · same lines, complex or\nconflicting goals → ask the user · feature re-implements what main already squashed → prefer\nmain's, then re-apply only the genuinely new feature work.\n\n**Then write a merge explanation** for each conflicted file — NOT a comment in the source (that\nbreaks for JSON and deleted files). Write it next to that file's diffs, at:\n\n\\`\\`\\`\nMERGE_DIR/updatemain-<safe_path>/{{EXPLANATION_FILE}}\n\\`\\`\\`\n\n(\\`<safe_path>\\` = the conflict file path with \\`/\\` → \\`__\\`, the same dir that holds its\n\\`A-forkpoint.txt\\` / \\`B-A.diff\\` / \\`C-A.diff\\`.) In it, explain in a few sentences how you resolved\nthis file: which side you took where, what you combined from B-A.diff vs C-A.diff, and why. The\ngate fails if any conflicted file's explanation is missing or empty. Do not paste A/B/C context\nblocks into the source code.\n\n## STEP 3 — Run the gate (validates the merge AND finalizes it)\n\n\\`\\`\\`\npnpm {{FINISH_COMMAND}}\n\\`\\`\\`\n\n- Leftover conflict markers → fix those files and re-run.\n- Missing merge explanation → write it (see STEP 2) and re-run.\n- Build failure → fix the TypeScript/lint errors and re-run (the gate re-stages for you).\n- Missing review.json (PR flow only) → write it in the printed format (your PR review), then re-run.\n- On success it commits and finalizes the merge (in the PR flow it also renders the dashboard and\n creates/updates the PR).\n\n## Conflicted files\n\n{{FILE_LIST}}\n\n## If you need to bail out\n\nA numbered backup branch was created (e.g. \\`<feature>PreMerge1\\`). To abandon:\n\n\\`\\`\\`\ngit merge --abort 2>/dev/null; git checkout <feature> ; git branch -D {{SQUASH_BRANCH}}\n\\`\\`\\`\n\nThen delete \\`{{MERGE_DIR}}/\\` for a clean slate.\n`;\n\n// merge-START: the first half of the 3-point squash-merge lifecycle. Brings origin/main into a fresh\n// `<branch>Squash`, and on conflict writes the 3-point context + unvalidated marker + process doc,\n// then hands control back to the AI. On a clean merge it commits the squash and returns the branch\n// context so the caller (RunUpdate) can run merge-END to finalize. NEVER finalizes or posts a PR.\n@injectable(bindingScopeValues.Singleton)\nexport class MergeStart {\n constructor(\n private readonly gatherInfo: GatherInfo,\n private readonly branchNaming: BranchNaming,\n private readonly gitExec: GitExec,\n private readonly mergeState: MergeState,\n ) {}\n\n async mergeStart(repoRoot: string, verb: MutationVerb, home: string, finishCommand: string): Promise<MergeStartResult> {\n const currentBranch = execSync('git branch --show-current', { encoding: 'utf8' }).trim();\n if (currentBranch.endsWith('Squash')) {\n throw new CliExitError(1, `❌ On a leftover ${currentBranch} branch with no merge marker. Clean up: git branch -D ${currentBranch}`);\n }\n\n // One number `n` for this sync drives BOTH the backup branch and its `merge-<n>/` context dir.\n const slot = this.chooseSyncSlot(home, currentBranch);\n const backupBranch = slot.backupBranch;\n const mergeDir = slot.runDir;\n\n process.stdout.write('\\n' + SEP + '🔄 Squash-Merge Update from Main\\n' + SEP + '\\n');\n const info = await this.gatherInfo.gatherInfo();\n const hashes = info.hashes;\n if (info.alreadyUpToDate) {\n process.stdout.write('ℹ️ Branch already even with main; nothing to merge, continuing to push/build.\\n');\n }\n\n const prNumber = this.detectPr(this.branchNaming.baseBranchName(currentBranch));\n process.stdout.write(prNumber ? `Existing PR #${prNumber} will be updated.\\n` : 'No existing PR (one can be created later).\\n');\n\n this.createBackup(currentBranch, backupBranch);\n const backupEvent = new BranchMutationEvent(verb, 'BACKUP');\n backupEvent.fromBranch = currentBranch;\n backupEvent.toBranch = backupBranch;\n logBranchMutation(repoRoot, backupEvent);\n\n const squashBranch = `${currentBranch}Squash`;\n if (spawnSync('git', ['show-ref', '--verify', '--quiet', `refs/heads/${squashBranch}`]).status === 0) {\n throw new CliExitError(1, `❌ Stale ${squashBranch} from a previous run. Delete it: git branch -D ${squashBranch}`);\n }\n\n // Branch the squash off origin/main directly — worktree-native. origin/main was already fetched\n // in gatherInfo(), and A/B/C are computed purely from origin/main, so the merge base is identical\n // to the old checkout-main + pull path.\n const originMainSha = this.shortSha('origin/main');\n this.gitExec.runGitChecked(['checkout', '-b', squashBranch, 'origin/main'], 'Failed to create squash branch off origin/main');\n const baseEvent = new BranchMutationEvent(verb, 'PULL');\n baseEvent.newMain = originMainSha;\n logBranchMutation(repoRoot, baseEvent);\n\n process.stdout.write('\\n' + SEP + `🔀 Squash merging ${currentBranch}\\n` + SEP + '\\n');\n const merge = spawnSync('git', ['merge', '--squash', currentBranch], { stdio: 'inherit' });\n if (merge.status !== 0) {\n this.handleConflictsHandback(repoRoot, mergeDir, currentBranch, squashBranch, backupBranch, prNumber, hashes, finishCommand);\n this.logConflict(repoRoot, verb, mergeDir);\n return new MergeStartResult('conflict', null, mergeDir);\n }\n logBranchMutation(repoRoot, new BranchMutationEvent(verb, 'SQUASH'));\n\n const nothingStaged = spawnSync('git', ['diff-index', '--quiet', '--cached', 'HEAD', '--']).status === 0;\n if (nothingStaged) {\n process.stdout.write('ℹ️ Already up-to-date with main (nothing to merge).\\n');\n } else {\n // Internal, transient subject for the single squash commit on the feature branch. It NO LONGER\n // reaches main's history: finish-upsert-pr squash-merges the PR with an explicit\n // `gh pr merge --subject <PR title> --body-file <commit summary>`, so main carries the PR title.\n this.gitExec.runGitChecked(['commit', '-m', `Squash merge of ${currentBranch}`], 'Failed to commit squash merge');\n this.mergeState.writeCleanMergeMarker(mergeDir, hashes.hashForkPoint, hashes.hashFeatureHead, hashes.hashMainHead);\n }\n return new MergeStartResult('clean', new MergeContext(currentBranch, squashBranch, backupBranch, prNumber), mergeDir);\n }\n\n // Detect the PR by its STABLE feature branch (a leftover `…wpN` still resolves to the one name the\n // PR lives on).\n private detectPr(baseBranch: string): string {\n const result = spawnSync(\n 'gh', ['pr', 'list', '--head', baseBranch, '--json', 'number', '--jq', '.[0].number'],\n { encoding: 'utf8' },\n );\n return result.status === 0 ? (result.stdout ?? '').trim() : '';\n }\n\n // Pick this sync's slot number MONOTONICALLY from the durable audit dirs, then step past any\n // existing `<branch>PreMerge<n>` branch so the same `n` is free for the paired backup branch.\n private chooseSyncSlot(home: string, currentBranch: string): SyncSlot {\n const branchExists = (name: string): boolean =>\n spawnSync('git', ['show-ref', '--verify', '--quiet', `refs/heads/${name}`]).status === 0;\n let n = this.mergeState.nextMergeSlotNumber(home);\n while (branchExists(this.branchNaming.preMergeBackupName(currentBranch, n))) n += 1;\n return new SyncSlot(this.branchNaming.preMergeBackupName(currentBranch, n), this.mergeState.mergeRunDirFor(home, n));\n }\n\n // Snapshot the pre-merge state onto the caller-chosen `backupBranch`, never overwriting.\n private createBackup(currentBranch: string, backupBranch: string): void {\n process.stdout.write('\\n' + SEP + '💾 Creating Pre-Merge Backup\\n' + SEP + '\\n');\n this.gitExec.runGitChecked(['checkout', '-b', backupBranch], 'Failed to create backup branch');\n this.gitExec.runGitChecked(['checkout', currentBranch], 'Failed to return to feature branch');\n process.stdout.write(`✅ Backup created: ${backupBranch}\\n\\n`);\n }\n\n private saveConflictContext(\n conflictedFiles: string[], mergeDir: string, forkPoint: string, featureHead: string, mainHead: string,\n ): void {\n for (const file of conflictedFiles) {\n const fileDir = this.mergeState.perFileContextDir(mergeDir, file);\n fs.mkdirSync(fileDir, { recursive: true });\n\n const fork = spawnSync('git', ['show', `${forkPoint}:${file}`], { encoding: 'utf8' });\n fs.writeFileSync(path.join(fileDir, 'A-forkpoint.txt'), fork.status === 0 ? (fork.stdout ?? '') : '(file did not exist)\\n');\n const feature = spawnSync('git', ['show', `${featureHead}:${file}`], { encoding: 'utf8' });\n fs.writeFileSync(path.join(fileDir, 'B-feature.txt'), feature.status === 0 ? (feature.stdout ?? '') : '(file did not exist)\\n');\n const main = spawnSync('git', ['show', `${mainHead}:${file}`], { encoding: 'utf8' });\n fs.writeFileSync(path.join(fileDir, 'C-main.txt'), main.status === 0 ? (main.stdout ?? '') : '(file did not exist)\\n');\n\n const ba = spawnSync('git', ['diff', forkPoint, featureHead, '--', file], { encoding: 'utf8' });\n fs.writeFileSync(path.join(fileDir, 'B-A.diff'), ba.stdout ?? '');\n const ca = spawnSync('git', ['diff', forkPoint, mainHead, '--', file], { encoding: 'utf8' });\n fs.writeFileSync(path.join(fileDir, 'C-A.diff'), ca.stdout ?? '');\n }\n }\n\n private mergeProcessDoc(mergeDir: string, squashBranch: string, conflictedFiles: string[], finishCommand: string): string {\n const fileList = conflictedFiles.map((f: string): string => `- \\`${f}\\``).join('\\n');\n return MERGE_PROCESS_TEMPLATE\n .replace(/\\{\\{SQUASH_BRANCH\\}\\}/g, squashBranch)\n .replace(/\\{\\{MERGE_DIR\\}\\}/g, mergeDir)\n .replace(/\\{\\{EXPLANATION_FILE\\}\\}/g, MERGE_EXPLANATION_FILE)\n .replace(/\\{\\{FINISH_COMMAND\\}\\}/g, finishCommand)\n .replace(/\\{\\{START_COMMAND\\}\\}/g, new SyncFlowGuidance().pairedStart(finishCommand))\n .replace(/\\{\\{FILE_LIST\\}\\}/g, fileList);\n }\n\n // Returns the absolute path of the written doc.\n private writeMergeProcessDoc(repoRoot: string, mergeDir: string, squashBranch: string, conflictedFiles: string[], finishCommand: string): string {\n const docDir = path.join(repoRoot, WEBPIECES_TMP_DIR, 'instruct-ai');\n fs.mkdirSync(docDir, { recursive: true });\n const docPath = path.join(docDir, 'webpieces.mergeprocess.md');\n fs.writeFileSync(docPath, this.mergeProcessDoc(mergeDir, squashBranch, conflictedFiles, finishCommand));\n return docPath;\n }\n\n // The AI-facing \"what just happened / what to do next\" recap on the conflict path.\n private printConflictHandback(\n docPath: string, mergeDir: string, squashBranch: string, conflictedFiles: string[], finishCommand: string,\n ): void {\n process.stdout.write('\\n' + SEP + `⚠️ Conflicts in ${conflictedFiles.length} file(s) — handing control back to you\\n` + SEP + '\\n');\n process.stdout.write('Here is exactly what I did and what you need to do:\\n\\n');\n process.stdout.write('What I did:\\n');\n process.stdout.write(' 1. snapshotted your pre-merge state to a PreMerge branch\\n');\n process.stdout.write(' 2. pulled origin/main and squash-merged your work onto it\\n');\n process.stdout.write(` 3. hit conflicts — you are now on the transient branch ${squashBranch}\\n\\n`);\n process.stdout.write('What you need to do:\\n');\n process.stdout.write(` 1. read the merge process doc: ${docPath}\\n`);\n process.stdout.write(` 2. resolve each conflicted file below (its 3-point A/B/C context + diffs are in\\n`);\n process.stdout.write(` ${mergeDir}/updatemain-<file>/), and write that file's merge-explanation.md\\n`);\n process.stdout.write(` 3. run pnpm ${finishCommand} — it validates, commits, and finalizes (do NOT git add/commit/push yourself)\\n\\n`);\n process.stdout.write('Conflicted files:\\n');\n for (const file of conflictedFiles) process.stdout.write(` - ${file}\\n`);\n process.stdout.write('\\n' + SEP);\n }\n\n // Write the conflict context files + the unvalidated marker + the process doc. Does NOT exit.\n private handleConflictsHandback(\n repoRoot: string, mergeDir: string, currentBranch: string, squashBranch: string,\n backupBranch: string, prNumber: string, hashes: HashPoints, finishCommand: string,\n ): void {\n const raw = execSync('git diff --name-only --diff-filter=U', { encoding: 'utf8' }).trim();\n const conflictedFiles = raw.split('\\n').filter((f: string): boolean => f.trim() !== '');\n fs.mkdirSync(mergeDir, { recursive: true });\n fs.writeFileSync(path.join(mergeDir, 'updatemain-conflicted-files.txt'), raw + '\\n');\n fs.writeFileSync(path.join(mergeDir, 'updatemain-hashes.json'), JSON.stringify(hashes, null, 2) + '\\n');\n this.saveConflictContext(conflictedFiles, mergeDir, hashes.hashForkPoint, hashes.hashFeatureHead, hashes.hashMainHead);\n\n const marker = new MergeMarker(\n currentBranch, squashBranch, backupBranch, prNumber, conflictedFiles,\n hashes.hashForkPoint, hashes.hashFeatureHead, hashes.hashMainHead, false,\n );\n this.mergeState.writeMergeMarker(mergeDir, marker);\n const docPath = this.writeMergeProcessDoc(repoRoot, mergeDir, squashBranch, conflictedFiles, finishCommand);\n this.printConflictHandback(docPath, mergeDir, squashBranch, conflictedFiles, finishCommand);\n }\n\n // Short sha of a ref (best-effort — '' if it can't resolve).\n private shortSha(ref: string): string {\n const result = spawnSync('git', ['rev-parse', '--short', ref], { encoding: 'utf8' });\n return result.status === 0 ? (result.stdout ?? '').trim() : '';\n }\n\n // Log the CONFLICT phase with the conflicted-file list + artifact paths a resolver needs.\n private logConflict(repoRoot: string, verb: MutationVerb, mergeDir: string): void {\n const raw = spawnSync('git', ['diff', '--name-only', '--diff-filter=U'], { cwd: repoRoot, encoding: 'utf8' });\n const files = (raw.status === 0 ? (raw.stdout ?? '') : '').split('\\n').map((f: string): string => f.trim()).filter((f: string): boolean => f !== '');\n const event = new BranchMutationEvent(verb, 'CONFLICT');\n event.conflict = true;\n event.conflictFiles = files;\n event.artifacts = [\n path.join(mergeDir, 'updatemain-<file>'),\n path.join(repoRoot, WEBPIECES_TMP_DIR, 'instruct-ai', 'webpieces.mergeprocess.md'),\n ];\n logBranchMutation(repoRoot, event);\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"merge-start.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/merge-start.ts"],"names":[],"mappings":"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","sourcesContent":["import { execSync, spawnSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport {\n WEBPIECES_TMP_DIR, MERGE_EXPLANATION_FILE, CliExitError,\n MutationVerb, BranchMutationEvent, logBranchMutation, SyncFlowGuidance,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { GatherInfo } from '../git-gatherInfo';\nimport { BranchNaming } from './branch-naming';\nimport { GitExec } from './git-exec';\nimport { MergeState, MergeMarker } from './merge-state';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\ninterface HashPoints {\n hashForkPoint: string;\n hashFeatureHead: string;\n hashMainHead: string;\n}\n\n// The four branch names merge-END needs to finalize a merge (swap squash→feature, push, clean up).\nexport class MergeContext {\n currentBranch: string;\n squashBranch: string;\n backupBranch: string;\n prNumber: string;\n\n constructor(currentBranch: string, squashBranch: string, backupBranch: string, prNumber: string) {\n this.currentBranch = currentBranch;\n this.squashBranch = squashBranch;\n this.backupBranch = backupBranch;\n this.prNumber = prNumber;\n }\n}\n\n// Outcome of merge-start: 'clean' carries the context for merge-END to finalize; 'conflict' means the\n// marker + context files were written and the caller should hand back to the AI (exit 2).\nexport class MergeStartResult {\n status: 'clean' | 'conflict';\n context: MergeContext | null;\n runDir: string; // this sync's numbered `merge-<n>/` dir — passed to merge-END so it reads THIS marker\n\n constructor(status: 'clean' | 'conflict', context: MergeContext | null, runDir: string) {\n this.status = status;\n this.context = context;\n this.runDir = runDir;\n }\n}\n\n// The one number `n` for a sync and the things it names: the pre-merge backup branch, its paired\n// conflict-context run dir, and (via `n`) the `preMerge<n>.hash` record of the tip it snapshotted.\nclass SyncSlot {\n backupBranch: string;\n runDir: string;\n n: number;\n\n constructor(backupBranch: string, runDir: string, n: number) {\n this.backupBranch = backupBranch;\n this.runDir = runDir;\n this.n = n;\n }\n}\n\n// Single source of truth for the merge process (written at conflict time, parameterized with the live\n// MERGE_DIR + conflicted-file list so it can never drift from the actual layout).\nconst MERGE_PROCESS_TEMPLATE = `# AI-Assisted Squash-Merge Conflict Resolution (generated)\n\nThis file was generated by \\`pnpm {{START_COMMAND}}\\` when the 3-point squash-merge hit conflicts.\nIt is the single source of truth for the merge process — follow it exactly.\n\nYou are on branch \\`{{SQUASH_BRANCH}}\\` with conflict markers in the working tree.\n\\`MERGE_DIR = {{MERGE_DIR}}\\`\n\n## How the gate works\n\n- Resolve every conflicted file in the working tree.\n- Run **\\`pnpm {{FINISH_COMMAND}}\\`** — the validation + finish gate. It scans for leftover conflict\n markers, checks each conflicted file has a written merge explanation, validates, and commits the\n merge. It is the paired half of \\`{{START_COMMAND}}\\` — never finish with the other flow's command\n (\\`wp-start-update\\` pairs with \\`wp-finish-update\\`; \\`wp-start-upsert-pr\\` pairs with\n \\`wp-finish-upsert-pr\\`, which additionally runs the \\`nx affected\\` build, renders the dashboard,\n and creates/updates the PR).\n- **Do NOT run \\`git add\\` / \\`git commit\\` / \\`git push\\` / \\`gh pr\\` yourself.** They are blocked by\n the \\`merge-in-progress-guard\\` hook until the gate validates. The gate does the commit.\n\n## STEP 1 — Load the merge context\n\nPer conflicted file, \\`MERGE_DIR/updatemain-<safe_path>/\\` holds (\\`<safe_path>\\` = path with \\`/\\`→\\`__\\`):\n\n\\`\\`\\`\nA-forkpoint.txt # file at fork point (base)\nB-feature.txt # file on your feature branch\nC-main.txt # file on main\nB-A.diff # what your feature changed (B−A)\nC-A.diff # what main changed (C−A)\n\\`\\`\\`\n\n\\`updatemain-hashes.json\\` holds A/B/C commit hashes. To see why main changed:\n\\`git log <A>..<C> --oneline\\`.\n\n## STEP 2 — Resolve each conflicted file\n\nFor each file: read the working-tree file (the markers) and its \\`B-A.diff\\` / \\`C-A.diff\\` (intent),\nthen Edit to the resolved version, removing ALL conflict markers.\n\nStrategies: goals align & non-overlapping → merge both · one side removes what the other modifies\n→ prefer the removal · same lines, simple (imports/format) → merge both · same lines, complex or\nconflicting goals → ask the user · feature re-implements what main already squashed → prefer\nmain's, then re-apply only the genuinely new feature work.\n\n**Then write a merge explanation** for each conflicted file — NOT a comment in the source (that\nbreaks for JSON and deleted files). Write it next to that file's diffs, at:\n\n\\`\\`\\`\nMERGE_DIR/updatemain-<safe_path>/{{EXPLANATION_FILE}}\n\\`\\`\\`\n\n(\\`<safe_path>\\` = the conflict file path with \\`/\\` → \\`__\\`, the same dir that holds its\n\\`A-forkpoint.txt\\` / \\`B-A.diff\\` / \\`C-A.diff\\`.) In it, explain in a few sentences how you resolved\nthis file: which side you took where, what you combined from B-A.diff vs C-A.diff, and why. The\ngate fails if any conflicted file's explanation is missing or empty. Do not paste A/B/C context\nblocks into the source code.\n\n## STEP 3 — Run the gate (validates the merge AND finalizes it)\n\n\\`\\`\\`\npnpm {{FINISH_COMMAND}}\n\\`\\`\\`\n\n- Leftover conflict markers → fix those files and re-run.\n- Missing merge explanation → write it (see STEP 2) and re-run.\n- Build failure → fix the TypeScript/lint errors and re-run (the gate re-stages for you).\n- Missing review.json (PR flow only) → write it in the printed format (your PR review), then re-run.\n- On success it commits and finalizes the merge (in the PR flow it also renders the dashboard and\n creates/updates the PR).\n\n## Conflicted files\n\n{{FILE_LIST}}\n\n## If you need to bail out\n\nA numbered backup branch was created (e.g. \\`<feature>PreMerge1\\`). To abandon:\n\n\\`\\`\\`\ngit merge --abort 2>/dev/null; git checkout <feature> ; git branch -D {{SQUASH_BRANCH}}\n\\`\\`\\`\n\nThen delete \\`{{MERGE_DIR}}/\\` for a clean slate.\n`;\n\n// merge-START: the first half of the 3-point squash-merge lifecycle. Brings origin/main into a fresh\n// `<branch>Squash`, and on conflict writes the 3-point context + unvalidated marker + process doc,\n// then hands control back to the AI. On a clean merge it commits the squash and returns the branch\n// context so the caller (RunUpdate) can run merge-END to finalize. NEVER finalizes or posts a PR.\n@injectable(bindingScopeValues.Singleton)\nexport class MergeStart {\n constructor(\n private readonly gatherInfo: GatherInfo,\n private readonly branchNaming: BranchNaming,\n private readonly gitExec: GitExec,\n private readonly mergeState: MergeState,\n ) {}\n\n async mergeStart(repoRoot: string, verb: MutationVerb, home: string, finishCommand: string): Promise<MergeStartResult> {\n const currentBranch = execSync('git branch --show-current', { encoding: 'utf8' }).trim();\n if (currentBranch.endsWith('Squash')) {\n throw new CliExitError(1, `❌ On a leftover ${currentBranch} branch with no merge marker. Clean up: git branch -D ${currentBranch}`);\n }\n\n // One number `n` for this sync drives BOTH the backup branch and its `merge-<n>/` context dir.\n const slot = this.chooseSyncSlot(home, currentBranch);\n const backupBranch = slot.backupBranch;\n const mergeDir = slot.runDir;\n\n process.stdout.write('\\n' + SEP + '🔄 Squash-Merge Update from Main\\n' + SEP + '\\n');\n const info = await this.gatherInfo.gatherInfo();\n const hashes = info.hashes;\n if (info.alreadyUpToDate) {\n process.stdout.write('ℹ️ Branch already even with main; nothing to merge, continuing to push/build.\\n');\n }\n\n const prNumber = this.detectPr(this.branchNaming.baseBranchName(currentBranch));\n process.stdout.write(prNumber ? `Existing PR #${prNumber} will be updated.\\n` : 'No existing PR (one can be created later).\\n');\n\n this.createBackup(currentBranch, backupBranch);\n // Record THIS sync's pre-merge tip as `staged/<feature>/preMerge<n>.hash` — every intermediate\n // state, not just the last one. It is what lets the `<feature>PreMerge<n>` snapshot BRANCH be\n // disposable (branches count toward the branch cap; a written-down hash does not), and it is the\n // ref the archive tag is cut from when the PR lands.\n this.mergeState.writePreMergeHash(home, slot.n, this.fullSha(currentBranch));\n const backupEvent = new BranchMutationEvent(verb, 'BACKUP');\n backupEvent.fromBranch = currentBranch;\n backupEvent.toBranch = backupBranch;\n logBranchMutation(repoRoot, backupEvent);\n\n const squashBranch = `${currentBranch}Squash`;\n if (spawnSync('git', ['show-ref', '--verify', '--quiet', `refs/heads/${squashBranch}`]).status === 0) {\n throw new CliExitError(1, `❌ Stale ${squashBranch} from a previous run. Delete it: git branch -D ${squashBranch}`);\n }\n\n // Branch the squash off origin/main directly — worktree-native. origin/main was already fetched\n // in gatherInfo(), and A/B/C are computed purely from origin/main, so the merge base is identical\n // to the old checkout-main + pull path.\n const originMainSha = this.shortSha('origin/main');\n this.gitExec.runGitChecked(['checkout', '-b', squashBranch, 'origin/main'], 'Failed to create squash branch off origin/main');\n const baseEvent = new BranchMutationEvent(verb, 'PULL');\n baseEvent.newMain = originMainSha;\n logBranchMutation(repoRoot, baseEvent);\n\n process.stdout.write('\\n' + SEP + `🔀 Squash merging ${currentBranch}\\n` + SEP + '\\n');\n const merge = spawnSync('git', ['merge', '--squash', currentBranch], { stdio: 'inherit' });\n if (merge.status !== 0) {\n this.handleConflictsHandback(repoRoot, mergeDir, currentBranch, squashBranch, backupBranch, prNumber, hashes, finishCommand);\n this.logConflict(repoRoot, verb, mergeDir);\n return new MergeStartResult('conflict', null, mergeDir);\n }\n logBranchMutation(repoRoot, new BranchMutationEvent(verb, 'SQUASH'));\n\n const nothingStaged = spawnSync('git', ['diff-index', '--quiet', '--cached', 'HEAD', '--']).status === 0;\n if (nothingStaged) {\n process.stdout.write('ℹ️ Already up-to-date with main (nothing to merge).\\n');\n } else {\n // Internal, transient subject for the single squash commit on the feature branch. It NO LONGER\n // reaches main's history: finish-upsert-pr squash-merges the PR with an explicit\n // `gh pr merge --subject <PR title> --body-file <commit summary>`, so main carries the PR title.\n this.gitExec.runGitChecked(['commit', '-m', `Squash merge of ${currentBranch}`], 'Failed to commit squash merge');\n // Clean merge ⇒ hashes only, no conflicts.md. Its ABSENCE is what marks this merge clean.\n this.mergeState.recordCleanMerge(mergeDir, hashes.hashForkPoint, hashes.hashFeatureHead, hashes.hashMainHead);\n }\n return new MergeStartResult('clean', new MergeContext(currentBranch, squashBranch, backupBranch, prNumber), mergeDir);\n }\n\n // Detect the PR by its STABLE feature branch (a leftover `…wpN` still resolves to the one name the\n // PR lives on).\n private detectPr(baseBranch: string): string {\n const result = spawnSync(\n 'gh', ['pr', 'list', '--head', baseBranch, '--json', 'number', '--jq', '.[0].number'],\n { encoding: 'utf8' },\n );\n return result.status === 0 ? (result.stdout ?? '').trim() : '';\n }\n\n // Pick this sync's slot number MONOTONICALLY from the durable audit dirs, then step past any\n // existing `<branch>PreMerge<n>` branch so the same `n` is free for the paired backup branch.\n private chooseSyncSlot(home: string, currentBranch: string): SyncSlot {\n const branchExists = (name: string): boolean =>\n spawnSync('git', ['show-ref', '--verify', '--quiet', `refs/heads/${name}`]).status === 0;\n let n = this.mergeState.nextMergeSlotNumber(home);\n while (branchExists(this.branchNaming.preMergeBackupName(currentBranch, n))) n += 1;\n return new SyncSlot(this.branchNaming.preMergeBackupName(currentBranch, n), this.mergeState.mergeRunDirFor(home, n), n);\n }\n\n // Snapshot the pre-merge state onto the caller-chosen `backupBranch`, never overwriting.\n private createBackup(currentBranch: string, backupBranch: string): void {\n process.stdout.write('\\n' + SEP + '💾 Creating Pre-Merge Backup\\n' + SEP + '\\n');\n this.gitExec.runGitChecked(['checkout', '-b', backupBranch], 'Failed to create backup branch');\n this.gitExec.runGitChecked(['checkout', currentBranch], 'Failed to return to feature branch');\n process.stdout.write(`✅ Backup created: ${backupBranch}\\n\\n`);\n }\n\n private saveConflictContext(\n conflictedFiles: string[], mergeDir: string, forkPoint: string, featureHead: string, mainHead: string,\n ): void {\n for (const file of conflictedFiles) {\n const fileDir = this.mergeState.perFileContextDir(mergeDir, file);\n fs.mkdirSync(fileDir, { recursive: true });\n\n const fork = spawnSync('git', ['show', `${forkPoint}:${file}`], { encoding: 'utf8' });\n fs.writeFileSync(path.join(fileDir, 'A-forkpoint.txt'), fork.status === 0 ? (fork.stdout ?? '') : '(file did not exist)\\n');\n const feature = spawnSync('git', ['show', `${featureHead}:${file}`], { encoding: 'utf8' });\n fs.writeFileSync(path.join(fileDir, 'B-feature.txt'), feature.status === 0 ? (feature.stdout ?? '') : '(file did not exist)\\n');\n const main = spawnSync('git', ['show', `${mainHead}:${file}`], { encoding: 'utf8' });\n fs.writeFileSync(path.join(fileDir, 'C-main.txt'), main.status === 0 ? (main.stdout ?? '') : '(file did not exist)\\n');\n\n const ba = spawnSync('git', ['diff', forkPoint, featureHead, '--', file], { encoding: 'utf8' });\n fs.writeFileSync(path.join(fileDir, 'B-A.diff'), ba.stdout ?? '');\n const ca = spawnSync('git', ['diff', forkPoint, mainHead, '--', file], { encoding: 'utf8' });\n fs.writeFileSync(path.join(fileDir, 'C-A.diff'), ca.stdout ?? '');\n }\n }\n\n private mergeProcessDoc(mergeDir: string, squashBranch: string, conflictedFiles: string[], finishCommand: string): string {\n const fileList = conflictedFiles.map((f: string): string => `- \\`${f}\\``).join('\\n');\n return MERGE_PROCESS_TEMPLATE\n .replace(/\\{\\{SQUASH_BRANCH\\}\\}/g, squashBranch)\n .replace(/\\{\\{MERGE_DIR\\}\\}/g, mergeDir)\n .replace(/\\{\\{EXPLANATION_FILE\\}\\}/g, MERGE_EXPLANATION_FILE)\n .replace(/\\{\\{FINISH_COMMAND\\}\\}/g, finishCommand)\n .replace(/\\{\\{START_COMMAND\\}\\}/g, new SyncFlowGuidance().pairedStart(finishCommand))\n .replace(/\\{\\{FILE_LIST\\}\\}/g, fileList);\n }\n\n // Returns the absolute path of the written doc.\n private writeMergeProcessDoc(repoRoot: string, mergeDir: string, squashBranch: string, conflictedFiles: string[], finishCommand: string): string {\n const docDir = path.join(repoRoot, WEBPIECES_TMP_DIR, 'instruct-ai');\n fs.mkdirSync(docDir, { recursive: true });\n const docPath = path.join(docDir, 'webpieces.mergeprocess.md');\n fs.writeFileSync(docPath, this.mergeProcessDoc(mergeDir, squashBranch, conflictedFiles, finishCommand));\n return docPath;\n }\n\n // The AI-facing \"what just happened / what to do next\" recap on the conflict path.\n private printConflictHandback(\n docPath: string, mergeDir: string, squashBranch: string, conflictedFiles: string[], finishCommand: string,\n ): void {\n process.stdout.write('\\n' + SEP + `⚠️ Conflicts in ${conflictedFiles.length} file(s) — handing control back to you\\n` + SEP + '\\n');\n process.stdout.write('Here is exactly what I did and what you need to do:\\n\\n');\n process.stdout.write('What I did:\\n');\n process.stdout.write(' 1. snapshotted your pre-merge state to a PreMerge branch\\n');\n process.stdout.write(' 2. pulled origin/main and squash-merged your work onto it\\n');\n process.stdout.write(` 3. hit conflicts — you are now on the transient branch ${squashBranch}\\n\\n`);\n process.stdout.write('What you need to do:\\n');\n process.stdout.write(` 1. read the merge process doc: ${docPath}\\n`);\n process.stdout.write(` 2. resolve each conflicted file below (its 3-point A/B/C context + diffs are in\\n`);\n process.stdout.write(` ${mergeDir}/updatemain-<file>/), and write that file's merge-explanation.md\\n`);\n process.stdout.write(` 3. run pnpm ${finishCommand} — it validates, commits, and finalizes (do NOT git add/commit/push yourself)\\n\\n`);\n process.stdout.write('Conflicted files:\\n');\n for (const file of conflictedFiles) process.stdout.write(` - ${file}\\n`);\n process.stdout.write('\\n' + SEP);\n }\n\n // Write the conflict context files + the unvalidated marker + the process doc. Does NOT exit.\n private handleConflictsHandback(\n repoRoot: string, mergeDir: string, currentBranch: string, squashBranch: string,\n backupBranch: string, prNumber: string, hashes: HashPoints, finishCommand: string,\n ): void {\n const raw = execSync('git diff --name-only --diff-filter=U', { encoding: 'utf8' }).trim();\n const conflictedFiles = raw.split('\\n').filter((f: string): boolean => f.trim() !== '');\n fs.mkdirSync(mergeDir, { recursive: true });\n // `conflicts.md` replaces the old `updatemain-conflicted-files.txt` (which nothing read) AND is\n // the 3-point signal itself: this file exists in a run dir if and only if that merge conflicted,\n // which is what lets the index classify each merge without a per-branch directory-name scheme.\n this.mergeState.writeConflicts(mergeDir, conflictedFiles);\n fs.writeFileSync(path.join(mergeDir, 'updatemain-hashes.json'), JSON.stringify(hashes, null, 2) + '\\n');\n this.saveConflictContext(conflictedFiles, mergeDir, hashes.hashForkPoint, hashes.hashFeatureHead, hashes.hashMainHead);\n\n const marker = new MergeMarker(\n currentBranch, squashBranch, backupBranch, prNumber, conflictedFiles,\n hashes.hashForkPoint, hashes.hashFeatureHead, hashes.hashMainHead, false,\n );\n this.mergeState.writeMergeMarker(mergeDir, marker);\n const docPath = this.writeMergeProcessDoc(repoRoot, mergeDir, squashBranch, conflictedFiles, finishCommand);\n this.printConflictHandback(docPath, mergeDir, squashBranch, conflictedFiles, finishCommand);\n }\n\n // Short sha of a ref (best-effort — '' if it can't resolve).\n private shortSha(ref: string): string {\n const result = spawnSync('git', ['rev-parse', '--short', ref], { encoding: 'utf8' });\n return result.status === 0 ? (result.stdout ?? '').trim() : '';\n }\n\n // FULL sha of a ref (best-effort — '' if it can't resolve). Recorded for the pre-merge tips because\n // those hashes are meant to be used later to restore state, and an abbreviated sha can go ambiguous\n // as the repo grows.\n private fullSha(ref: string): string {\n const result = spawnSync('git', ['rev-parse', ref], { encoding: 'utf8' });\n return result.status === 0 ? (result.stdout ?? '').trim() : '';\n }\n\n // Log the CONFLICT phase with the conflicted-file list + artifact paths a resolver needs.\n private logConflict(repoRoot: string, verb: MutationVerb, mergeDir: string): void {\n const raw = spawnSync('git', ['diff', '--name-only', '--diff-filter=U'], { cwd: repoRoot, encoding: 'utf8' });\n const files = (raw.status === 0 ? (raw.stdout ?? '') : '').split('\\n').map((f: string): string => f.trim()).filter((f: string): boolean => f !== '');\n const event = new BranchMutationEvent(verb, 'CONFLICT');\n event.conflict = true;\n event.conflictFiles = files;\n event.artifacts = [\n path.join(mergeDir, 'updatemain-<file>'),\n path.join(repoRoot, WEBPIECES_TMP_DIR, 'instruct-ai', 'webpieces.mergeprocess.md'),\n ];\n logBranchMutation(repoRoot, event);\n }\n}\n"]}
|
|
@@ -22,13 +22,89 @@ export declare class MergeHashRecord {
|
|
|
22
22
|
timestamp: string;
|
|
23
23
|
constructor(hashForkPoint: string, hashFeatureHead: string, hashMainHead: string, timestamp: string);
|
|
24
24
|
}
|
|
25
|
-
|
|
25
|
+
/**
|
|
26
|
+
* The two halves of `.webpieces/merge-info/`.
|
|
27
|
+
*
|
|
28
|
+
* `staged/` mirrors branches that STILL EXIST — it answers "what am I working on right now", and it
|
|
29
|
+
* self-cleans when a PR lands (the whole dir is MOVED to `merged/`) instead of growing forever.
|
|
30
|
+
* `merged/` is the post-mortem record, and is where the archive-tag reference lives.
|
|
31
|
+
*
|
|
32
|
+
* Both are reserved names: a feature branch slugifies `/`→`-`, so no feature can ever be called
|
|
33
|
+
* `staged` or `merged` and collide with them.
|
|
34
|
+
*/
|
|
35
|
+
export declare const STAGED_DIR = "staged";
|
|
36
|
+
export declare const MERGED_DIR = "merged";
|
|
37
|
+
/**
|
|
38
|
+
* The 3-point conflict record. PRESENT ONLY WHEN THE MERGE ACTUALLY CONFLICTED.
|
|
39
|
+
*
|
|
40
|
+
* This replaces the old `no-3point-merge.md`, which was written into EVERY clean merge dir and whose
|
|
41
|
+
* entire content was "nothing interesting happened here" plus the same A/B/C shas that
|
|
42
|
+
* `updatemain-hashes.json` already carries, one line below, in machine-readable form. Nothing read it.
|
|
43
|
+
* So ABSENCE is now the signal: "does this directory contain conflicts.md?" is the whole question, and
|
|
44
|
+
* the shas a human might have wanted from the marker are still on disk in updatemain-hashes.json.
|
|
45
|
+
*/
|
|
46
|
+
export declare const CONFLICTS_FILE = "conflicts.md";
|
|
47
|
+
export declare const ARCHIVE_RECORD_FILE = "archive.json";
|
|
48
|
+
export declare const MERGE_INDEX_FILE = "index.json";
|
|
26
49
|
/** Filesystem layout + read/write of the per-feature merge run dirs and their conflict markers. */
|
|
27
50
|
export declare class MergeState {
|
|
28
51
|
mergeDirFor(repoRoot: string, featureName: string): string;
|
|
52
|
+
/** `.webpieces/merge-info/staged/<feature>/` — the in-flight home. */
|
|
53
|
+
stagedDirFor(repoRoot: string, featureName: string): string;
|
|
54
|
+
/** `.webpieces/merge-info/merged/<feature>/` — the landed home. */
|
|
55
|
+
mergedDirFor(repoRoot: string, featureName: string): string;
|
|
56
|
+
/** `.webpieces/merge-info/` itself — the home holding `staged/`, `merged/` and `index.json`. */
|
|
57
|
+
mergeInfoRoot(repoRoot: string): string;
|
|
58
|
+
/**
|
|
59
|
+
* MIGRATION for the pre-`staged/` layout, done IN PLACE and lazily.
|
|
60
|
+
*
|
|
61
|
+
* Old layout put the feature home directly at `merge-info/<feature>/`. That dir is real, live state
|
|
62
|
+
* (it can hold an UNVALIDATED merge marker for a merge that is in progress RIGHT NOW), so leaving
|
|
63
|
+
* legacy dirs alone was not an option: the in-flight merge would become invisible to the finish
|
|
64
|
+
* gate, which now looks under `staged/`. Tolerating both layouts on read was the other candidate and
|
|
65
|
+
* was rejected — two homes means every reader, the guard included, needs two lookups forever.
|
|
66
|
+
*
|
|
67
|
+
* So: the first time anything asks for a feature's home, an existing legacy dir is MOVED to
|
|
68
|
+
* `staged/<feature>/`. `fs.renameSync` is atomic within a filesystem and moves the whole tree with no
|
|
69
|
+
* copying, so no merge record can be half-migrated. If `staged/<feature>` already exists the legacy
|
|
70
|
+
* dir is left untouched — never merged, never deleted — because two homes for one feature is exactly
|
|
71
|
+
* the ambiguity a human must resolve. `.webpieces/` is gitignored local state, so nothing here is
|
|
72
|
+
* ever part of a commit.
|
|
73
|
+
*/
|
|
74
|
+
private migrateLegacyHome;
|
|
75
|
+
/**
|
|
76
|
+
* Record THIS sync's pre-merge tip as `staged/<feature>/preMerge<n>.hash`.
|
|
77
|
+
*
|
|
78
|
+
* Every intermediate state, not just the last: a branch updated from main five times has five
|
|
79
|
+
* genuinely different pre-merge tips, and the fifth tells you nothing about what the second looked
|
|
80
|
+
* like. The hashes are what make the `<feature>PreMerge<n>` snapshot BRANCHES disposable — once the
|
|
81
|
+
* tip is written down (and, at land time, tagged), the branch itself is pure branch-cap pressure.
|
|
82
|
+
*/
|
|
83
|
+
writePreMergeHash(home: string, n: number, sha: string): void;
|
|
84
|
+
/** Every recorded pre-merge tip for a feature, in slot order. [] when none were recorded. */
|
|
85
|
+
readPreMergeHashes(home: string): string[];
|
|
29
86
|
mergeRunDirFor(home: string, n: number): string;
|
|
30
87
|
nextMergeSlotNumber(home: string): number;
|
|
31
|
-
|
|
88
|
+
/**
|
|
89
|
+
* Record a CLEAN (no-conflict) sync: the A/B/C shas in `updatemain-hashes.json`, and NOTHING ELSE.
|
|
90
|
+
*
|
|
91
|
+
* The `no-3point-merge.md` placeholder this used to also write is gone. Its whole content was a
|
|
92
|
+
* sentence saying nothing interesting happened plus the same three shas being written on the very
|
|
93
|
+
* next line in machine-readable form — a file that appeared in every clean directory a human opens,
|
|
94
|
+
* carried no information the JSON did not, and had no consumer anywhere in the codebase. Absence is
|
|
95
|
+
* now the signal instead: a run dir with no {@link CONFLICTS_FILE} was a clean merge.
|
|
96
|
+
*/
|
|
97
|
+
recordCleanMerge(mergeDir: string, forkPoint: string, featureHead: string, mainHead: string): void;
|
|
98
|
+
/** Path of a run dir's conflict record. Its EXISTENCE is what makes the merge "3-point". */
|
|
99
|
+
conflictsPath(mergeDir: string): string;
|
|
100
|
+
/** True when this run dir records a 3-point merge — i.e. `conflicts.md` is there. */
|
|
101
|
+
wasThreeWay(mergeDir: string): boolean;
|
|
102
|
+
/** Write the 3-point conflict record. Only ever called on the conflict path. */
|
|
103
|
+
writeConflicts(mergeDir: string, files: string[]): void;
|
|
104
|
+
/** The conflicted-file list back out of `conflicts.md`. [] when the merge was clean. */
|
|
105
|
+
readConflictedFiles(mergeDir: string): string[];
|
|
106
|
+
/** Every `merge-<n>/` run dir under a feature home, in slot order. [] when the home does not exist. */
|
|
107
|
+
listMergeRunDirs(home: string): number[];
|
|
32
108
|
findActiveMergeRunDir(home: string): string | null;
|
|
33
109
|
perFileContextDir(mergeDir: string, file: string): string;
|
|
34
110
|
markerPath(mergeDir: string): string;
|