@webpieces/rules-config 0.3.358 → 0.3.360
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 +3 -3
- package/src/branch-mutation-log.d.ts +13 -5
- package/src/branch-mutation-log.js +86 -76
- package/src/branch-mutation-log.js.map +1 -1
- package/src/index.d.ts +3 -3
- package/src/index.js +4 -1
- package/src/index.js.map +1 -1
- package/src/main-sync-status.d.ts +32 -7
- package/src/main-sync-status.js +240 -238
- package/src/main-sync-status.js.map +1 -1
- package/src/review-json.d.ts +13 -5
- package/src/review-json.js +106 -87
- package/src/review-json.js.map +1 -1
- package/src/rules-config-design.d.ts +7 -1
- package/src/rules-config-design.js +14 -2
- package/src/rules-config-design.js.map +1 -1
package/src/main-sync-status.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.MainSyncLock = exports.MainSyncStatus = exports.DEFAULT_HANG_TIMEOUT_MINUTES = void 0;
|
|
3
|
+
exports.MainSyncStatusService = exports.MainSyncLock = exports.MainSyncStatus = exports.DEFAULT_HANG_TIMEOUT_MINUTES = void 0;
|
|
4
4
|
exports.mainSyncStatusPath = mainSyncStatusPath;
|
|
5
5
|
exports.mainSyncLockPath = mainSyncLockPath;
|
|
6
6
|
exports.readMainSyncStatus = readMainSyncStatus;
|
|
@@ -18,24 +18,21 @@ const tslib_1 = require("tslib");
|
|
|
18
18
|
const child_process_1 = require("child_process");
|
|
19
19
|
const fs = tslib_1.__importStar(require("fs"));
|
|
20
20
|
const path = tslib_1.__importStar(require("path"));
|
|
21
|
+
const core_context_1 = require("@webpieces/core-context");
|
|
22
|
+
const inversify_1 = require("inversify");
|
|
21
23
|
const constants_1 = require("./constants");
|
|
22
24
|
const to_error_1 = require("./to-error");
|
|
23
|
-
// Shared "is my feature branch healthy relative to origin/main?" state. The SLOW signals (git fetch
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
// dep of both packages) so neither depends on the other.
|
|
29
|
-
// How long an `inprocess` refresher lock may sit before a new refresher assumes the prior run hung
|
|
30
|
-
// and proceeds anyway. Overridable per-rule via FeatureBranchGuardConfig.hangTimeoutMinutes.
|
|
25
|
+
// Shared "is my feature branch healthy relative to origin/main?" state. The SLOW signals (git fetch +
|
|
26
|
+
// merge-base + same-file-overlap + a merged-PR lookup) are computed by the ai-hook-rules refresher in a
|
|
27
|
+
// DETACHED background process; the feature-branch-guard only READS this cached file. pr-gate's merge
|
|
28
|
+
// flow also writes it synchronously after a merge. Lives here (the shared dep of both).
|
|
29
|
+
// How long an `inprocess` refresher lock may sit before a new refresher assumes the prior run hung.
|
|
31
30
|
exports.DEFAULT_HANG_TIMEOUT_MINUTES = 5;
|
|
32
31
|
const MAIN_SYNC_STATUS_FILE = 'main-sync-status.json';
|
|
33
32
|
const MAIN_SYNC_LOCK_FILE = 'main-sync.lock.json';
|
|
34
33
|
const LOCK_STATE_INPROCESS = 'inprocess';
|
|
35
34
|
const LOCK_STATE_FINISHED = 'finished';
|
|
36
|
-
// Data-only (per CLAUDE.md, classes for data).
|
|
37
|
-
// false (no merge-base with origin/main). `branchAlreadyMerged` flags that this feature branch was
|
|
38
|
-
// already merged into main (a merged PR exists) — the "you're working on a finished branch" case.
|
|
35
|
+
// Data-only (per CLAUDE.md, classes for data).
|
|
39
36
|
class MainSyncStatus {
|
|
40
37
|
branch;
|
|
41
38
|
branchAlreadyMerged;
|
|
@@ -47,10 +44,7 @@ class MainSyncStatus {
|
|
|
47
44
|
conflict;
|
|
48
45
|
conflictFiles;
|
|
49
46
|
timestamp;
|
|
50
|
-
// An OPEN (not merged) PR tracking this branch, if any — '' = none or not-yet-known.
|
|
51
|
-
// refresher (best-effort) and read by the feature-branch-guard so a mid-work conflict block can
|
|
52
|
-
// steer straight to the PR flow instead of the update-only flow (which would just fail-fast when a
|
|
53
|
-
// PR exists). Advisory only — the authoritative gate is wp-start-update's own fail-fast check.
|
|
47
|
+
// An OPEN (not merged) PR tracking this branch, if any — '' = none or not-yet-known. Advisory.
|
|
54
48
|
// Kept OUT of the positional constructor (a defaulted field) so existing call sites don't churn.
|
|
55
49
|
openPr = '';
|
|
56
50
|
constructor(branch, branchAlreadyMerged, mergedPr, hasForkPoint, forkPoint, originMain, featureHead, conflict, conflictFiles, timestamp) {
|
|
@@ -67,10 +61,8 @@ class MainSyncStatus {
|
|
|
67
61
|
}
|
|
68
62
|
}
|
|
69
63
|
exports.MainSyncStatus = MainSyncStatus;
|
|
70
|
-
// Concurrency state machine for the detached refresher. `started` is epoch
|
|
71
|
-
//
|
|
72
|
-
// refresher that was KILLED before writing its finished lock (SIGKILL skips the finally) doesn't
|
|
73
|
-
// wedge `inprocess` for the whole hangTimeout: if its pid is gone, the lock is reclaimable now.
|
|
64
|
+
// Concurrency state machine for the detached refresher. `started` is epoch ms. `pid` is the refresher
|
|
65
|
+
// process's pid (0 = unknown) — used so a KILLED refresher doesn't wedge `inprocess` for the timeout.
|
|
74
66
|
class MainSyncLock {
|
|
75
67
|
state;
|
|
76
68
|
started;
|
|
@@ -82,237 +74,247 @@ class MainSyncLock {
|
|
|
82
74
|
}
|
|
83
75
|
}
|
|
84
76
|
exports.MainSyncLock = MainSyncLock;
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
77
|
+
/**
|
|
78
|
+
* Reads/writes the main-sync cache + lock and computes the slow "is my branch healthy vs origin/main?"
|
|
79
|
+
* status. `@provideSingleton` so it's injectable and drawn in the rules-config DI design.
|
|
80
|
+
*/
|
|
81
|
+
let MainSyncStatusService = class MainSyncStatusService {
|
|
82
|
+
mainSyncStatusPath(repoRoot) {
|
|
83
|
+
return path.join(repoRoot, constants_1.WEBPIECES_TMP_DIR, MAIN_SYNC_STATUS_FILE);
|
|
84
|
+
}
|
|
85
|
+
mainSyncLockPath(repoRoot) {
|
|
86
|
+
return path.join(repoRoot, constants_1.WEBPIECES_TMP_DIR, MAIN_SYNC_LOCK_FILE);
|
|
87
|
+
}
|
|
88
|
+
// Pure read — any error (missing file, malformed JSON) returns null so the guard fails OPEN.
|
|
89
|
+
readMainSyncStatus(repoRoot) {
|
|
90
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
91
|
+
try {
|
|
92
|
+
const statusPath = this.mainSyncStatusPath(repoRoot);
|
|
93
|
+
if (!fs.existsSync(statusPath))
|
|
94
|
+
return null;
|
|
95
|
+
const raw = JSON.parse(fs.readFileSync(statusPath, 'utf8'));
|
|
96
|
+
const status = new MainSyncStatus(raw.branch ?? '', raw.branchAlreadyMerged ?? false, raw.mergedPr ?? '', raw.hasForkPoint ?? true, raw.forkPoint ?? null, raw.originMain ?? '', raw.featureHead ?? '', raw.conflict ?? false, raw.conflictFiles ?? [], raw.timestamp ?? '');
|
|
97
|
+
status.openPr = raw.openPr ?? '';
|
|
98
|
+
return status;
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
const error = (0, to_error_1.toError)(err);
|
|
102
|
+
void error;
|
|
101
103
|
return null;
|
|
102
|
-
|
|
103
|
-
const status = new MainSyncStatus(raw.branch ?? '', raw.branchAlreadyMerged ?? false, raw.mergedPr ?? '', raw.hasForkPoint ?? true, raw.forkPoint ?? null, raw.originMain ?? '', raw.featureHead ?? '', raw.conflict ?? false, raw.conflictFiles ?? [], raw.timestamp ?? '');
|
|
104
|
-
status.openPr = raw.openPr ?? '';
|
|
105
|
-
return status;
|
|
104
|
+
}
|
|
106
105
|
}
|
|
107
|
-
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
106
|
+
writeMainSyncStatus(repoRoot, status) {
|
|
107
|
+
const statusPath = this.mainSyncStatusPath(repoRoot);
|
|
108
|
+
this.ensureDir(statusPath);
|
|
109
|
+
fs.writeFileSync(statusPath, JSON.stringify(status, null, 2) + '\n');
|
|
111
110
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
111
|
+
readMainSyncLock(repoRoot) {
|
|
112
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
113
|
+
try {
|
|
114
|
+
const lockPath = this.mainSyncLockPath(repoRoot);
|
|
115
|
+
if (!fs.existsSync(lockPath))
|
|
116
|
+
return null;
|
|
117
|
+
const raw = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
|
|
118
|
+
return new MainSyncLock(raw.state ?? LOCK_STATE_FINISHED, raw.started ?? 0, raw.pid ?? 0);
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
const error = (0, to_error_1.toError)(err);
|
|
122
|
+
void error;
|
|
123
123
|
return null;
|
|
124
|
-
|
|
125
|
-
return new MainSyncLock(raw.state ?? LOCK_STATE_FINISHED, raw.started ?? 0, raw.pid ?? 0);
|
|
124
|
+
}
|
|
126
125
|
}
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
126
|
+
writeMainSyncLock(repoRoot, lock) {
|
|
127
|
+
const lockPath = this.mainSyncLockPath(repoRoot);
|
|
128
|
+
this.ensureDir(lockPath);
|
|
129
|
+
fs.writeFileSync(lockPath, JSON.stringify(lock, null, 2) + '\n');
|
|
131
130
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
ensureDir(lockPath);
|
|
136
|
-
fs.writeFileSync(lockPath, JSON.stringify(lock, null, 2) + '\n');
|
|
137
|
-
}
|
|
138
|
-
// A lock is stale (the prior refresher is assumed hung) once it has been `inprocess` longer than
|
|
139
|
-
// hangTimeoutMinutes. `now` is injectable for tests; defaults to Date.now().
|
|
140
|
-
function isLockStale(lock, hangTimeoutMinutes, now = Date.now()) {
|
|
141
|
-
return now - lock.started > hangTimeoutMinutes * 60 * 1000;
|
|
142
|
-
}
|
|
143
|
-
// Liveness probe: is `pid` still a running process? `process.kill(pid, 0)` sends no signal, it only
|
|
144
|
-
// tests existence — ESRCH means the process is gone, EPERM means it exists but isn't ours (alive).
|
|
145
|
-
// pid <= 0 means "unknown" (an old lock with no pid) → assume alive and fall back to staleness only.
|
|
146
|
-
function isProcessAlive(pid) {
|
|
147
|
-
if (pid <= 0)
|
|
148
|
-
return true;
|
|
149
|
-
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
150
|
-
try {
|
|
151
|
-
process.kill(pid, 0);
|
|
152
|
-
return true;
|
|
131
|
+
// A lock is stale (prior refresher assumed hung) once `inprocess` longer than hangTimeoutMinutes.
|
|
132
|
+
isLockStale(lock, hangTimeoutMinutes, now = Date.now()) {
|
|
133
|
+
return now - lock.started > hangTimeoutMinutes * 60 * 1000;
|
|
153
134
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
135
|
+
// True when another refresher is actively running and we should NOT start a second one.
|
|
136
|
+
isRefreshInProgress(repoRoot, hangTimeoutMinutes, now = Date.now()) {
|
|
137
|
+
const lock = this.readMainSyncLock(repoRoot);
|
|
138
|
+
if (!lock)
|
|
139
|
+
return false;
|
|
140
|
+
if (lock.state !== LOCK_STATE_INPROCESS)
|
|
141
|
+
return false;
|
|
142
|
+
if (this.isLockStale(lock, hangTimeoutMinutes, now))
|
|
143
|
+
return false;
|
|
144
|
+
return this.isProcessAlive(lock.pid);
|
|
158
145
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
return { ok: true, out: result.stdout.trim() };
|
|
186
|
-
}
|
|
187
|
-
// The actual checked-out branch in repoRoot — cwd-correct so the cache's `branch` label always
|
|
188
|
-
// matches what the feature-branch-guard compares against (its own `git rev-parse` in the workspace).
|
|
189
|
-
function gitBranch(repoRoot) {
|
|
190
|
-
const result = capture(repoRoot, 'git', ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
191
|
-
return result.ok ? result.out : '';
|
|
192
|
-
}
|
|
193
|
-
function changedFiles(repoRoot, base, head) {
|
|
194
|
-
const result = capture(repoRoot, 'git', ['diff', '--name-only', base, head]);
|
|
195
|
-
if (!result.ok || result.out === '')
|
|
196
|
-
return [];
|
|
197
|
-
return result.out.split('\n').map((line) => line.trim()).filter((line) => line.length > 0);
|
|
198
|
-
}
|
|
199
|
-
// Every file this feature branch has touched since the fork point — committed AND still in the
|
|
200
|
-
// working tree (staged / unstaged / untracked). The committed-only `git diff forkPoint..HEAD` was
|
|
201
|
-
// BLIND to uncommitted edits: for most of an editing session the files you are actively changing are
|
|
202
|
-
// not yet in HEAD, so the overlap with main's changes was empty and conflict=false even when
|
|
203
|
-
// origin/main had already moved onto those same files. Unioning in the working-tree changes makes the
|
|
204
|
-
// conflict visible WHILE editing, so the guard can force an early merge instead of a painful late one.
|
|
205
|
-
function featureChangedFiles(repoRoot, forkPoint) {
|
|
206
|
-
const out = new Set();
|
|
207
|
-
const add = (args) => {
|
|
208
|
-
const r = capture(repoRoot, 'git', args);
|
|
209
|
-
if (!r.ok || r.out === '')
|
|
210
|
-
return;
|
|
211
|
-
for (const line of r.out.split('\n')) {
|
|
212
|
-
const f = line.trim();
|
|
213
|
-
if (f.length > 0)
|
|
214
|
-
out.add(f);
|
|
146
|
+
inProcessLock(now = Date.now(), pid = process.pid) {
|
|
147
|
+
return new MainSyncLock(LOCK_STATE_INPROCESS, now, pid);
|
|
148
|
+
}
|
|
149
|
+
finishedLock(started) {
|
|
150
|
+
return new MainSyncLock(LOCK_STATE_FINISHED, started, 0);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* The SLOW path, run only inside the detached refresher. Computes every cached signal the
|
|
154
|
+
* feature-branch-guard needs. Never run on the hook's blocking path.
|
|
155
|
+
*/
|
|
156
|
+
// webpieces-disable max-lines-new-methods -- one cohesive slow-path computation
|
|
157
|
+
computeMainSyncStatus(repoRoot) {
|
|
158
|
+
const branch = this.gitBranch(repoRoot);
|
|
159
|
+
const mergedPr = this.detectMergedPr(repoRoot, branch);
|
|
160
|
+
const openPr = this.detectOpenPr(repoRoot, branch);
|
|
161
|
+
// Best-effort network refresh; offline just means we evaluate against the last-fetched ref.
|
|
162
|
+
(0, child_process_1.spawnSync)('git', ['fetch', 'origin', 'main'], { cwd: repoRoot, stdio: 'ignore' });
|
|
163
|
+
const head = this.capture(repoRoot, 'git', ['rev-parse', 'HEAD']);
|
|
164
|
+
const originMain = this.capture(repoRoot, 'git', ['rev-parse', 'origin/main']);
|
|
165
|
+
const featureHead = head.ok ? head.out : '';
|
|
166
|
+
if (!head.ok || !originMain.ok) {
|
|
167
|
+
const status = this.benignStatus(branch, featureHead);
|
|
168
|
+
status.branchAlreadyMerged = mergedPr !== '';
|
|
169
|
+
status.mergedPr = mergedPr;
|
|
170
|
+
status.openPr = openPr;
|
|
171
|
+
return status;
|
|
215
172
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
if (!branch || branch === 'main')
|
|
227
|
-
return '';
|
|
228
|
-
const result = capture(repoRoot, 'gh', ['pr', 'list', '--head', branch, '--state', 'merged', '--json', 'number', '--jq', '.[0].number']);
|
|
229
|
-
return result.ok ? result.out : '';
|
|
230
|
-
}
|
|
231
|
-
// An OPEN PR tracking this branch, if any. Best-effort — this is ADVISORY (it only lets the guard's
|
|
232
|
-
// conflict block steer to the PR flow early), so an unreachable gh degrades to '' here. The HARD gate
|
|
233
|
-
// that must never guess is wp-start-update's own openPrForBranch, which fails fast instead.
|
|
234
|
-
function detectOpenPr(repoRoot, branch) {
|
|
235
|
-
if (!branch || branch === 'main')
|
|
236
|
-
return '';
|
|
237
|
-
const result = capture(repoRoot, 'gh', ['pr', 'list', '--head', branch, '--state', 'open', '--json', 'number', '--jq', '.[0].number']);
|
|
238
|
-
return result.ok ? result.out : '';
|
|
239
|
-
}
|
|
240
|
-
// A benign status that never blocks — used when origin/main can't be resolved (no remote yet,
|
|
241
|
-
// offline before first fetch). hasForkPoint=true + conflict=false so the guard allows the edit.
|
|
242
|
-
function benignStatus(branch, featureHead) {
|
|
243
|
-
return new MainSyncStatus(branch, false, '', true, null, '', featureHead, false, [], new Date().toISOString());
|
|
244
|
-
}
|
|
245
|
-
/**
|
|
246
|
-
* The SLOW path, run only inside the detached refresher. Computes every cached signal the
|
|
247
|
-
* feature-branch-guard needs: whether the branch is already merged (merged PR), whether a fork point
|
|
248
|
-
* with origin/main still exists, and whether origin/main and this branch touched the SAME file since
|
|
249
|
-
* the fork point (the deliberately-simple conflict heuristic — it over-blocks rather than miss a real
|
|
250
|
-
* conflict). Never run on the hook's blocking path.
|
|
251
|
-
*/
|
|
252
|
-
function computeMainSyncStatus(repoRoot) {
|
|
253
|
-
const branch = gitBranch(repoRoot);
|
|
254
|
-
const mergedPr = detectMergedPr(repoRoot, branch);
|
|
255
|
-
// Advisory: lets the guard's conflict block steer to the PR flow early when a PR is already open.
|
|
256
|
-
const openPr = detectOpenPr(repoRoot, branch);
|
|
257
|
-
// Best-effort network refresh; offline just means we evaluate against the last-fetched ref.
|
|
258
|
-
(0, child_process_1.spawnSync)('git', ['fetch', 'origin', 'main'], { cwd: repoRoot, stdio: 'ignore' });
|
|
259
|
-
const head = capture(repoRoot, 'git', ['rev-parse', 'HEAD']);
|
|
260
|
-
const originMain = capture(repoRoot, 'git', ['rev-parse', 'origin/main']);
|
|
261
|
-
const featureHead = head.ok ? head.out : '';
|
|
262
|
-
if (!head.ok || !originMain.ok) {
|
|
263
|
-
const status = benignStatus(branch, featureHead);
|
|
264
|
-
status.branchAlreadyMerged = mergedPr !== '';
|
|
265
|
-
status.mergedPr = mergedPr;
|
|
173
|
+
const forkPoint = this.capture(repoRoot, 'git', ['merge-base', 'origin/main', 'HEAD']);
|
|
174
|
+
if (!forkPoint.ok || forkPoint.out === '') {
|
|
175
|
+
const noFork = new MainSyncStatus(branch, mergedPr !== '', mergedPr, false, null, originMain.out, featureHead, false, [], new Date().toISOString());
|
|
176
|
+
noFork.openPr = openPr;
|
|
177
|
+
return noFork;
|
|
178
|
+
}
|
|
179
|
+
const featureFiles = new Set(this.featureChangedFiles(repoRoot, forkPoint.out));
|
|
180
|
+
const mainFiles = this.changedFiles(repoRoot, forkPoint.out, 'origin/main');
|
|
181
|
+
const conflictFiles = mainFiles.filter((file) => featureFiles.has(file));
|
|
182
|
+
const status = new MainSyncStatus(branch, mergedPr !== '', mergedPr, true, forkPoint.out, originMain.out, featureHead, conflictFiles.length > 0, conflictFiles, new Date().toISOString());
|
|
266
183
|
status.openPr = openPr;
|
|
267
184
|
return status;
|
|
268
185
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
186
|
+
// The recovery steps when there is no fork point with origin/main (a bad merge of main into branch).
|
|
187
|
+
squashRecoverySteps(currentBranch) {
|
|
188
|
+
return [
|
|
189
|
+
'1. Fetch latest main: git fetch origin main',
|
|
190
|
+
`2. New branch off origin/main: git checkout -b ${currentBranch}-v2 origin/main`,
|
|
191
|
+
`3. Squash-merge old branch: git merge --squash ${currentBranch}`,
|
|
192
|
+
`4. Commit the squash: git add -A && git commit -m "Squashed from ${currentBranch}"`,
|
|
193
|
+
'5. If a PR exists: open a NEW PR for the -v2 branch and close the old one.',
|
|
194
|
+
];
|
|
275
195
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
'1. Fetch latest main: git fetch origin main',
|
|
293
|
-
`2. New branch off origin/main: git checkout -b ${currentBranch}-v2 origin/main`,
|
|
294
|
-
`3. Squash-merge old branch: git merge --squash ${currentBranch}`,
|
|
295
|
-
`4. Commit the squash: git add -A && git commit -m "Squashed from ${currentBranch}"`,
|
|
296
|
-
'5. If a PR exists: open a NEW PR for the -v2 branch and close the old one.',
|
|
297
|
-
];
|
|
298
|
-
}
|
|
299
|
-
// Synchronously stamp a clean "up to date with main" status — call right after a successful merge
|
|
300
|
-
// (the branch now contains origin/main). Unblocks the next edit immediately without waiting for the
|
|
301
|
-
// async refresher. Best-effort: a git failure is swallowed (the refresher will recompute later).
|
|
302
|
-
function stampCleanMainSyncStatus(repoRoot) {
|
|
303
|
-
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
304
|
-
try {
|
|
305
|
-
const branch = gitBranch(repoRoot);
|
|
306
|
-
const originMain = capture(repoRoot, 'git', ['rev-parse', 'origin/main']);
|
|
307
|
-
const featureHead = capture(repoRoot, 'git', ['rev-parse', 'HEAD']);
|
|
308
|
-
if (!originMain.ok || !featureHead.ok)
|
|
309
|
-
return;
|
|
310
|
-
const status = new MainSyncStatus(branch, false, '', true, originMain.out, originMain.out, featureHead.out, false, [], new Date().toISOString());
|
|
311
|
-
writeMainSyncStatus(repoRoot, status);
|
|
196
|
+
// Synchronously stamp a clean "up to date with main" status — call right after a successful merge.
|
|
197
|
+
stampCleanMainSyncStatus(repoRoot) {
|
|
198
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
199
|
+
try {
|
|
200
|
+
const branch = this.gitBranch(repoRoot);
|
|
201
|
+
const originMain = this.capture(repoRoot, 'git', ['rev-parse', 'origin/main']);
|
|
202
|
+
const featureHead = this.capture(repoRoot, 'git', ['rev-parse', 'HEAD']);
|
|
203
|
+
if (!originMain.ok || !featureHead.ok)
|
|
204
|
+
return;
|
|
205
|
+
const status = new MainSyncStatus(branch, false, '', true, originMain.out, originMain.out, featureHead.out, false, [], new Date().toISOString());
|
|
206
|
+
this.writeMainSyncStatus(repoRoot, status);
|
|
207
|
+
}
|
|
208
|
+
catch (err) {
|
|
209
|
+
const error = (0, to_error_1.toError)(err);
|
|
210
|
+
void error;
|
|
211
|
+
}
|
|
312
212
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
void error;
|
|
213
|
+
ensureDir(filePath) {
|
|
214
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
316
215
|
}
|
|
317
|
-
|
|
216
|
+
// Liveness probe: is `pid` still a running process? pid <= 0 (unknown) → assume alive.
|
|
217
|
+
isProcessAlive(pid) {
|
|
218
|
+
if (pid <= 0)
|
|
219
|
+
return true;
|
|
220
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
221
|
+
try {
|
|
222
|
+
process.kill(pid, 0);
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
catch (err) {
|
|
226
|
+
const error = (0, to_error_1.toError)(err);
|
|
227
|
+
return !error.message.includes('ESRCH');
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
// Run a command capturing trimmed stdout; ok=false on spawn failure or non-zero exit.
|
|
231
|
+
capture(repoRoot, cmd, args) {
|
|
232
|
+
const result = (0, child_process_1.spawnSync)(cmd, args, { cwd: repoRoot, encoding: 'utf8' });
|
|
233
|
+
if (result.status !== 0 || typeof result.stdout !== 'string')
|
|
234
|
+
return { ok: false, out: '' };
|
|
235
|
+
return { ok: true, out: result.stdout.trim() };
|
|
236
|
+
}
|
|
237
|
+
// The actual checked-out branch in repoRoot — cwd-correct so the cache's `branch` label matches.
|
|
238
|
+
gitBranch(repoRoot) {
|
|
239
|
+
const result = this.capture(repoRoot, 'git', ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
240
|
+
return result.ok ? result.out : '';
|
|
241
|
+
}
|
|
242
|
+
changedFiles(repoRoot, base, head) {
|
|
243
|
+
const result = this.capture(repoRoot, 'git', ['diff', '--name-only', base, head]);
|
|
244
|
+
if (!result.ok || result.out === '')
|
|
245
|
+
return [];
|
|
246
|
+
return result.out.split('\n').map((line) => line.trim()).filter((line) => line.length > 0);
|
|
247
|
+
}
|
|
248
|
+
// Every file this feature branch has touched since the fork point — committed AND still in the
|
|
249
|
+
// working tree (staged / unstaged / untracked), so a conflict is visible WHILE editing.
|
|
250
|
+
featureChangedFiles(repoRoot, forkPoint) {
|
|
251
|
+
const out = new Set();
|
|
252
|
+
const add = (args) => {
|
|
253
|
+
const r = this.capture(repoRoot, 'git', args);
|
|
254
|
+
if (!r.ok || r.out === '')
|
|
255
|
+
return;
|
|
256
|
+
for (const line of r.out.split('\n')) {
|
|
257
|
+
const f = line.trim();
|
|
258
|
+
if (f.length > 0)
|
|
259
|
+
out.add(f);
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
add(['diff', '--name-only', forkPoint, 'HEAD']); // committed since the fork point
|
|
263
|
+
add(['diff', '--name-only', 'HEAD']); // unstaged working-tree edits
|
|
264
|
+
add(['diff', '--name-only', '--cached', 'HEAD']); // staged edits
|
|
265
|
+
add(['ls-files', '--others', '--exclude-standard']); // untracked new files (respects .gitignore)
|
|
266
|
+
return [...out];
|
|
267
|
+
}
|
|
268
|
+
// Has this feature branch already been merged into main? Reliable signal: a MERGED PR exists.
|
|
269
|
+
detectMergedPr(repoRoot, branch) {
|
|
270
|
+
if (!branch || branch === 'main')
|
|
271
|
+
return '';
|
|
272
|
+
const result = this.capture(repoRoot, 'gh', ['pr', 'list', '--head', branch, '--state', 'merged', '--json', 'number', '--jq', '.[0].number']);
|
|
273
|
+
return result.ok ? result.out : '';
|
|
274
|
+
}
|
|
275
|
+
// An OPEN PR tracking this branch, if any. Best-effort/advisory.
|
|
276
|
+
detectOpenPr(repoRoot, branch) {
|
|
277
|
+
if (!branch || branch === 'main')
|
|
278
|
+
return '';
|
|
279
|
+
const result = this.capture(repoRoot, 'gh', ['pr', 'list', '--head', branch, '--state', 'open', '--json', 'number', '--jq', '.[0].number']);
|
|
280
|
+
return result.ok ? result.out : '';
|
|
281
|
+
}
|
|
282
|
+
// A benign status that never blocks — used when origin/main can't be resolved.
|
|
283
|
+
benignStatus(branch, featureHead) {
|
|
284
|
+
return new MainSyncStatus(branch, false, '', true, null, '', featureHead, false, [], new Date().toISOString());
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
exports.MainSyncStatusService = MainSyncStatusService;
|
|
288
|
+
exports.MainSyncStatusService = MainSyncStatusService = tslib_1.__decorate([
|
|
289
|
+
(0, core_context_1.provideSingleton)(),
|
|
290
|
+
(0, inversify_1.injectable)()
|
|
291
|
+
], MainSyncStatusService);
|
|
292
|
+
// Temporary migration delegators to MainSyncStatusService — removed once consumers inject it.
|
|
293
|
+
const mainSyncSvc = new MainSyncStatusService();
|
|
294
|
+
// webpieces-disable no-function-outside-class -- temporary back-compat delegator to MainSyncStatusService; removed once consumers inject it
|
|
295
|
+
function mainSyncStatusPath(repoRoot) { return mainSyncSvc.mainSyncStatusPath(repoRoot); }
|
|
296
|
+
// webpieces-disable no-function-outside-class -- temporary back-compat delegator to MainSyncStatusService; removed once consumers inject it
|
|
297
|
+
function mainSyncLockPath(repoRoot) { return mainSyncSvc.mainSyncLockPath(repoRoot); }
|
|
298
|
+
// webpieces-disable no-function-outside-class -- temporary back-compat delegator to MainSyncStatusService; removed once consumers inject it
|
|
299
|
+
function readMainSyncStatus(repoRoot) { return mainSyncSvc.readMainSyncStatus(repoRoot); }
|
|
300
|
+
// webpieces-disable no-function-outside-class -- temporary back-compat delegator to MainSyncStatusService; removed once consumers inject it
|
|
301
|
+
function writeMainSyncStatus(repoRoot, status) { mainSyncSvc.writeMainSyncStatus(repoRoot, status); }
|
|
302
|
+
// webpieces-disable no-function-outside-class -- temporary back-compat delegator to MainSyncStatusService; removed once consumers inject it
|
|
303
|
+
function readMainSyncLock(repoRoot) { return mainSyncSvc.readMainSyncLock(repoRoot); }
|
|
304
|
+
// webpieces-disable no-function-outside-class -- temporary back-compat delegator to MainSyncStatusService; removed once consumers inject it
|
|
305
|
+
function writeMainSyncLock(repoRoot, lock) { mainSyncSvc.writeMainSyncLock(repoRoot, lock); }
|
|
306
|
+
// webpieces-disable no-function-outside-class -- temporary back-compat delegator to MainSyncStatusService; removed once consumers inject it
|
|
307
|
+
function isLockStale(lock, hangTimeoutMinutes, now = Date.now()) { return mainSyncSvc.isLockStale(lock, hangTimeoutMinutes, now); }
|
|
308
|
+
// webpieces-disable no-function-outside-class -- temporary back-compat delegator to MainSyncStatusService; removed once consumers inject it
|
|
309
|
+
function isRefreshInProgress(repoRoot, hangTimeoutMinutes, now = Date.now()) { return mainSyncSvc.isRefreshInProgress(repoRoot, hangTimeoutMinutes, now); }
|
|
310
|
+
// webpieces-disable no-function-outside-class -- temporary back-compat delegator to MainSyncStatusService; removed once consumers inject it
|
|
311
|
+
function inProcessLock(now = Date.now(), pid = process.pid) { return mainSyncSvc.inProcessLock(now, pid); }
|
|
312
|
+
// webpieces-disable no-function-outside-class -- temporary back-compat delegator to MainSyncStatusService; removed once consumers inject it
|
|
313
|
+
function finishedLock(started) { return mainSyncSvc.finishedLock(started); }
|
|
314
|
+
// webpieces-disable no-function-outside-class -- temporary back-compat delegator to MainSyncStatusService; removed once consumers inject it
|
|
315
|
+
function computeMainSyncStatus(repoRoot) { return mainSyncSvc.computeMainSyncStatus(repoRoot); }
|
|
316
|
+
// webpieces-disable no-function-outside-class -- temporary back-compat delegator to MainSyncStatusService; removed once consumers inject it
|
|
317
|
+
function squashRecoverySteps(currentBranch) { return mainSyncSvc.squashRecoverySteps(currentBranch); }
|
|
318
|
+
// webpieces-disable no-function-outside-class -- temporary back-compat delegator to MainSyncStatusService; removed once consumers inject it
|
|
319
|
+
function stampCleanMainSyncStatus(repoRoot) { mainSyncSvc.stampCleanMainSyncStatus(repoRoot); }
|
|
318
320
|
//# sourceMappingURL=main-sync-status.js.map
|