@webpieces/rules-config 0.4.543 → 0.4.545
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 +1 -1
- package/src/index.d.ts +3 -2
- package/src/index.js +13 -4
- package/src/index.js.map +1 -1
- package/src/main-sync-file.d.ts +94 -0
- package/src/main-sync-file.js +243 -0
- package/src/main-sync-file.js.map +1 -0
- package/src/main-sync-status.d.ts +87 -34
- package/src/main-sync-status.js +147 -91
- package/src/main-sync-status.js.map +1 -1
- package/src/review-provenance.d.ts +8 -0
- package/src/review-provenance.js +9 -0
- package/src/review-provenance.js.map +1 -1
- package/src/subagent-provenance.d.ts +55 -5
- package/src/subagent-provenance.js +77 -12
- package/src/subagent-provenance.js.map +1 -1
|
@@ -1,21 +1,9 @@
|
|
|
1
1
|
import { AtomicFile } from './atomic-file';
|
|
2
|
+
import { MAIN_SYNC_STATUS_VERSION, MainSyncFileStore, MainSyncStatus, MainSyncStatusFile, PullRequestIndex } from './main-sync-file';
|
|
2
3
|
import { DotWebpieces } from './state-dir';
|
|
4
|
+
import { WorktreeService } from './worktrees';
|
|
5
|
+
export { MainSyncStatus, MainSyncStatusFile, PullRequestIndex, MAIN_SYNC_STATUS_VERSION };
|
|
3
6
|
export declare const DEFAULT_HANG_TIMEOUT_MINUTES = 5;
|
|
4
|
-
export declare class MainSyncStatus {
|
|
5
|
-
branch: string;
|
|
6
|
-
branchAlreadyMerged: boolean;
|
|
7
|
-
mergedPr: string;
|
|
8
|
-
hasForkPoint: boolean;
|
|
9
|
-
forkPoint: string | null;
|
|
10
|
-
originMain: string;
|
|
11
|
-
featureHead: string;
|
|
12
|
-
conflict: boolean;
|
|
13
|
-
conflictFiles: string[];
|
|
14
|
-
timestamp: string;
|
|
15
|
-
openPr: string;
|
|
16
|
-
localMain: string;
|
|
17
|
-
constructor(branch: string, branchAlreadyMerged: boolean, mergedPr: string, hasForkPoint: boolean, forkPoint: string | null, originMain: string, featureHead: string, conflict: boolean, conflictFiles: string[], timestamp: string);
|
|
18
|
-
}
|
|
19
7
|
export declare class MainSyncLock {
|
|
20
8
|
state: string;
|
|
21
9
|
started: number;
|
|
@@ -29,7 +17,9 @@ export declare class MainSyncLock {
|
|
|
29
17
|
export declare class MainSyncStatusService {
|
|
30
18
|
private readonly dotDir;
|
|
31
19
|
private readonly atomicFile;
|
|
32
|
-
|
|
20
|
+
private readonly store;
|
|
21
|
+
private readonly worktrees;
|
|
22
|
+
constructor(dotDir?: DotWebpieces, atomicFile?: AtomicFile, store?: MainSyncFileStore, worktrees?: WorktreeService);
|
|
33
23
|
/**
|
|
34
24
|
* SHARED scope: `<primary>/.webpieces/main-sync-status.json`, one per repo.
|
|
35
25
|
*
|
|
@@ -37,14 +27,13 @@ export declare class MainSyncStatusService {
|
|
|
37
27
|
* one `.git`, one `origin/main`, one fetch, one answer. A per-worktree copy under single-flight
|
|
38
28
|
* would only ever be written for whichever worktree won the lock anyway.
|
|
39
29
|
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* atomically) is the obvious follow-up.
|
|
30
|
+
* FORMERLY a known defect, now fixed: the file's CONTENT used to be keyed to ONE branch, and every
|
|
31
|
+
* reader fails OPEN when the cached branch is not the branch being judged (`stale-cross-branch-cache
|
|
32
|
+
* (fail-open)`). With several worktrees on several branches, only the winner of the lock was ever
|
|
33
|
+
* described, so every other worktree's guards abstained — and which one was armed thrashed as the
|
|
34
|
+
* lock changed hands. The file is now a MAP of branch -> status (see MainSyncStatusFile): the single
|
|
35
|
+
* winning refresher enumerates every worktree and records all of their branches in one atomic write,
|
|
36
|
+
* so one fetch arms everyone. Single-flight is unchanged; only what the winner writes changed.
|
|
48
37
|
*/
|
|
49
38
|
mainSyncStatusPath(repoRoot: string): string;
|
|
50
39
|
/**
|
|
@@ -62,14 +51,33 @@ export declare class MainSyncStatusService {
|
|
|
62
51
|
* `isRefreshInProgress` proves the holder is finished, hung past hangTimeoutMinutes, or dead.
|
|
63
52
|
*/
|
|
64
53
|
mainSyncLockPath(repoRoot: string): string;
|
|
65
|
-
readMainSyncStatus(repoRoot: string): MainSyncStatus | null;
|
|
66
54
|
/**
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
55
|
+
* The cached status FOR ONE BRANCH — the only read the four main-sync guards perform.
|
|
56
|
+
*
|
|
57
|
+
* `branch` is mandatory and is the branch being judged. A branch the last refresh never saw
|
|
58
|
+
* returns null, which every guard already treats as `no-sync-cache (fail-open)`, so an unknown
|
|
59
|
+
* branch degrades exactly as a missing file did. Any error (missing, truncated, malformed JSON)
|
|
60
|
+
* also returns null — this must never throw on the blocking path.
|
|
61
|
+
*/
|
|
62
|
+
readMainSyncStatus(repoRoot: string, branch: string): MainSyncStatus | null;
|
|
63
|
+
readMainSyncStatusFile(repoRoot: string): MainSyncStatusFile | null;
|
|
64
|
+
/**
|
|
65
|
+
* Merge ONE branch's entry into the existing map, atomically, leaving siblings untouched.
|
|
66
|
+
*
|
|
67
|
+
* Read-modify-write rather than replace: this is the writer used by everyone who is NOT the
|
|
68
|
+
* single-flight refresher (notably pr-gate's post-merge stamp, which holds no lock and knows about
|
|
69
|
+
* exactly one branch). Replacing the document from there would erase every other worktree's entry
|
|
70
|
+
* and silently disarm their guards until the next refresh.
|
|
71
71
|
*/
|
|
72
72
|
writeMainSyncStatus(repoRoot: string, status: MainSyncStatus): void;
|
|
73
|
+
/**
|
|
74
|
+
* Replace the WHOLE document — only the winning refresher may do this.
|
|
75
|
+
*
|
|
76
|
+
* ATOMIC (temp file + `rename()` in the same directory). The refresher writes this file while
|
|
77
|
+
* guards on the blocking path read it, possibly from another worktree; a plain `writeFileSync`
|
|
78
|
+
* truncates first, so a reader landing in that window gets a torn document. See AtomicFile.
|
|
79
|
+
*/
|
|
80
|
+
writeMainSyncStatusFile(repoRoot: string, file: MainSyncStatusFile): void;
|
|
73
81
|
readMainSyncLock(repoRoot: string): MainSyncLock | null;
|
|
74
82
|
writeMainSyncLock(repoRoot: string, lock: MainSyncLock): void;
|
|
75
83
|
isLockStale(lock: MainSyncLock, hangTimeoutMinutes: number, now?: number): boolean;
|
|
@@ -91,11 +99,42 @@ export declare class MainSyncStatusService {
|
|
|
91
99
|
inProcessLock(now?: number, pid?: number): MainSyncLock;
|
|
92
100
|
finishedLock(started: number): MainSyncLock;
|
|
93
101
|
/**
|
|
94
|
-
* The SLOW path
|
|
95
|
-
*
|
|
102
|
+
* The SLOW path for ONE branch: the branch checked out in `repoRoot`. Runs its own fetch and its
|
|
103
|
+
* own `gh` query, so it is the right entry point for a single-worktree caller and for tests.
|
|
104
|
+
* The refresher uses computeAllMainSyncStatuses instead — see there for why.
|
|
96
105
|
*/
|
|
97
106
|
computeMainSyncStatus(repoRoot: string): MainSyncStatus;
|
|
107
|
+
/**
|
|
108
|
+
* The SLOW path for EVERY branch this repo has checked out — what the detached refresher runs.
|
|
109
|
+
*
|
|
110
|
+
* The cache is shared by all worktrees but was written for one branch, so only one worktree's
|
|
111
|
+
* guards were ever armed. Enumerating the worktrees here and writing all of their branches in one
|
|
112
|
+
* atomic map arms every worktree off a single refresh.
|
|
113
|
+
*
|
|
114
|
+
* NETWORK COST DOES NOT SCALE WITH BRANCH COUNT, which is the whole reason this is a separate
|
|
115
|
+
* method: ONE `git fetch` and ONE repo-wide `gh pr list` for all N branches (the old code asked
|
|
116
|
+
* `gh` twice per branch, so looping it would have cost 2N round trips). Everything after that is
|
|
117
|
+
* local git in each worktree — merge-base, rev-parse, diff, ls-files.
|
|
118
|
+
*
|
|
119
|
+
* The map is rebuilt from the LIVE worktree set every refresh, so a deleted branch or removed
|
|
120
|
+
* worktree simply stops appearing. No pruning pass, no unbounded growth.
|
|
121
|
+
*/
|
|
122
|
+
computeAllMainSyncStatuses(repoRoot: string): MainSyncStatusFile;
|
|
123
|
+
private hasBranch;
|
|
124
|
+
/**
|
|
125
|
+
* One branch's status, computed in `worktreePath` (the tree that has it checked out — the
|
|
126
|
+
* working-tree overlap signals are only correct from there). Does NOT fetch and does NOT call
|
|
127
|
+
* `gh`: both are done once per refresh by the caller.
|
|
128
|
+
*/
|
|
129
|
+
private computeBranchStatus;
|
|
98
130
|
squashRecoverySteps(currentBranch: string): string[];
|
|
131
|
+
/**
|
|
132
|
+
* Synchronously stamp a clean "up to date with main" status — call right after a successful merge.
|
|
133
|
+
*
|
|
134
|
+
* This is the one writer that takes NO lock, and it knows about exactly one branch. It therefore
|
|
135
|
+
* goes through writeMainSyncStatus, which MERGES its entry into the existing map; writing a whole
|
|
136
|
+
* document from here would wipe every other worktree's entry.
|
|
137
|
+
*/
|
|
99
138
|
stampCleanMainSyncStatus(repoRoot: string): void;
|
|
100
139
|
/**
|
|
101
140
|
* Refresh `origin/main` WITHOUT writing `.git/FETCH_HEAD`.
|
|
@@ -121,14 +160,27 @@ export declare class MainSyncStatusService {
|
|
|
121
160
|
private localMainHash;
|
|
122
161
|
private changedFiles;
|
|
123
162
|
private featureChangedFiles;
|
|
124
|
-
|
|
125
|
-
|
|
163
|
+
/**
|
|
164
|
+
* ONE repo-wide `gh pr list` giving every branch's merged/open PR — replaces the two per-branch
|
|
165
|
+
* `--head <branch>` queries. `--limit 200` bounds it; a branch whose only PR is older than that
|
|
166
|
+
* falls out of the index and reads as "no PR", which is the fail-OPEN direction (no merged PR =
|
|
167
|
+
* no block), the same way a missing `gh` or an offline machine already degraded.
|
|
168
|
+
*/
|
|
169
|
+
private loadPullRequestIndex;
|
|
170
|
+
/**
|
|
171
|
+
* A `main` entry synthesised WITHOUT a worktree standing on main. Carries only the two hashes
|
|
172
|
+
* read-stale-guard / stale-main-bash-guard actually compare (originMain vs the live tree's
|
|
173
|
+
* ancestry) — never merged, never conflicting, so it can only ever produce the stale-main verdict.
|
|
174
|
+
*/
|
|
175
|
+
private mainOnlyStatus;
|
|
126
176
|
private benignStatus;
|
|
127
177
|
}
|
|
128
178
|
export declare function mainSyncStatusPath(repoRoot: string): string;
|
|
129
179
|
export declare function mainSyncLockPath(repoRoot: string): string;
|
|
130
|
-
export declare function readMainSyncStatus(repoRoot: string): MainSyncStatus | null;
|
|
180
|
+
export declare function readMainSyncStatus(repoRoot: string, branch: string): MainSyncStatus | null;
|
|
181
|
+
export declare function readMainSyncStatusFile(repoRoot: string): MainSyncStatusFile | null;
|
|
131
182
|
export declare function writeMainSyncStatus(repoRoot: string, status: MainSyncStatus): void;
|
|
183
|
+
export declare function writeMainSyncStatusFile(repoRoot: string, file: MainSyncStatusFile): void;
|
|
132
184
|
export declare function readMainSyncLock(repoRoot: string): MainSyncLock | null;
|
|
133
185
|
export declare function writeMainSyncLock(repoRoot: string, lock: MainSyncLock): void;
|
|
134
186
|
export declare function isLockStale(lock: MainSyncLock, hangTimeoutMinutes: number, now?: number): boolean;
|
|
@@ -137,5 +189,6 @@ export declare function tryAcquireMainSyncLock(repoRoot: string, hangTimeoutMinu
|
|
|
137
189
|
export declare function inProcessLock(now?: number, pid?: number): MainSyncLock;
|
|
138
190
|
export declare function finishedLock(started: number): MainSyncLock;
|
|
139
191
|
export declare function computeMainSyncStatus(repoRoot: string): MainSyncStatus;
|
|
192
|
+
export declare function computeAllMainSyncStatuses(repoRoot: string): MainSyncStatusFile;
|
|
140
193
|
export declare function squashRecoverySteps(currentBranch: string): string[];
|
|
141
194
|
export declare function stampCleanMainSyncStatus(repoRoot: string): void;
|
package/src/main-sync-status.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.MainSyncStatusService = exports.MainSyncLock = exports.
|
|
3
|
+
exports.MainSyncStatusService = exports.MainSyncLock = exports.DEFAULT_HANG_TIMEOUT_MINUTES = exports.MAIN_SYNC_STATUS_VERSION = exports.PullRequestIndex = exports.MainSyncStatusFile = exports.MainSyncStatus = void 0;
|
|
4
4
|
exports.mainSyncStatusPath = mainSyncStatusPath;
|
|
5
5
|
exports.mainSyncLockPath = mainSyncLockPath;
|
|
6
6
|
exports.readMainSyncStatus = readMainSyncStatus;
|
|
7
|
+
exports.readMainSyncStatusFile = readMainSyncStatusFile;
|
|
7
8
|
exports.writeMainSyncStatus = writeMainSyncStatus;
|
|
9
|
+
exports.writeMainSyncStatusFile = writeMainSyncStatusFile;
|
|
8
10
|
exports.readMainSyncLock = readMainSyncLock;
|
|
9
11
|
exports.writeMainSyncLock = writeMainSyncLock;
|
|
10
12
|
exports.isLockStale = isLockStale;
|
|
@@ -13,6 +15,7 @@ exports.tryAcquireMainSyncLock = tryAcquireMainSyncLock;
|
|
|
13
15
|
exports.inProcessLock = inProcessLock;
|
|
14
16
|
exports.finishedLock = finishedLock;
|
|
15
17
|
exports.computeMainSyncStatus = computeMainSyncStatus;
|
|
18
|
+
exports.computeAllMainSyncStatuses = computeAllMainSyncStatuses;
|
|
16
19
|
exports.squashRecoverySteps = squashRecoverySteps;
|
|
17
20
|
exports.stampCleanMainSyncStatus = stampCleanMainSyncStatus;
|
|
18
21
|
const tslib_1 = require("tslib");
|
|
@@ -21,8 +24,14 @@ const fs = tslib_1.__importStar(require("fs"));
|
|
|
21
24
|
const path = tslib_1.__importStar(require("path"));
|
|
22
25
|
const inversify_1 = require("inversify");
|
|
23
26
|
const atomic_file_1 = require("./atomic-file");
|
|
27
|
+
const main_sync_file_1 = require("./main-sync-file");
|
|
28
|
+
Object.defineProperty(exports, "MAIN_SYNC_STATUS_VERSION", { enumerable: true, get: function () { return main_sync_file_1.MAIN_SYNC_STATUS_VERSION; } });
|
|
29
|
+
Object.defineProperty(exports, "MainSyncStatus", { enumerable: true, get: function () { return main_sync_file_1.MainSyncStatus; } });
|
|
30
|
+
Object.defineProperty(exports, "MainSyncStatusFile", { enumerable: true, get: function () { return main_sync_file_1.MainSyncStatusFile; } });
|
|
31
|
+
Object.defineProperty(exports, "PullRequestIndex", { enumerable: true, get: function () { return main_sync_file_1.PullRequestIndex; } });
|
|
24
32
|
const state_dir_1 = require("./state-dir");
|
|
25
33
|
const to_error_1 = require("./to-error");
|
|
34
|
+
const worktrees_1 = require("./worktrees");
|
|
26
35
|
// Shared "is my feature branch healthy relative to origin/main?" state. The SLOW signals (git fetch +
|
|
27
36
|
// merge-base + same-file-overlap + a merged-PR lookup) are computed by the ai-hook-rules refresher in a
|
|
28
37
|
// DETACHED background process; the feature-branch-guard only READS this cached file. pr-gate's merge
|
|
@@ -33,39 +42,6 @@ const MAIN_SYNC_STATUS_FILE = 'main-sync-status.json';
|
|
|
33
42
|
const MAIN_SYNC_LOCK_FILE = 'main-sync.lock.json';
|
|
34
43
|
const LOCK_STATE_INPROCESS = 'inprocess';
|
|
35
44
|
const LOCK_STATE_FINISHED = 'finished';
|
|
36
|
-
// Data-only (per CLAUDE.md, classes for data).
|
|
37
|
-
class MainSyncStatus {
|
|
38
|
-
branch;
|
|
39
|
-
branchAlreadyMerged;
|
|
40
|
-
mergedPr;
|
|
41
|
-
hasForkPoint;
|
|
42
|
-
forkPoint;
|
|
43
|
-
originMain;
|
|
44
|
-
featureHead;
|
|
45
|
-
conflict;
|
|
46
|
-
conflictFiles;
|
|
47
|
-
timestamp;
|
|
48
|
-
// An OPEN (not merged) PR tracking this branch, if any — '' = none or not-yet-known. Advisory.
|
|
49
|
-
// Kept OUT of the positional constructor (a defaulted field) so existing call sites don't churn.
|
|
50
|
-
openPr = '';
|
|
51
|
-
// The LOCAL refs/heads/main hash — '' = main does not exist locally (fresh clone / worktree) or
|
|
52
|
-
// could not be read. Paired with `originMain`, this is what tells the read-stale-guard whether a
|
|
53
|
-
// checked-out `main` is behind its remote. Defaulted field for the same reason as `openPr`.
|
|
54
|
-
localMain = '';
|
|
55
|
-
constructor(branch, branchAlreadyMerged, mergedPr, hasForkPoint, forkPoint, originMain, featureHead, conflict, conflictFiles, timestamp) {
|
|
56
|
-
this.branch = branch;
|
|
57
|
-
this.branchAlreadyMerged = branchAlreadyMerged;
|
|
58
|
-
this.mergedPr = mergedPr;
|
|
59
|
-
this.hasForkPoint = hasForkPoint;
|
|
60
|
-
this.forkPoint = forkPoint;
|
|
61
|
-
this.originMain = originMain;
|
|
62
|
-
this.featureHead = featureHead;
|
|
63
|
-
this.conflict = conflict;
|
|
64
|
-
this.conflictFiles = conflictFiles;
|
|
65
|
-
this.timestamp = timestamp;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
exports.MainSyncStatus = MainSyncStatus;
|
|
69
45
|
// Concurrency state machine for the detached refresher. `started` is epoch ms. `pid` is the refresher
|
|
70
46
|
// process's pid (0 = unknown) — used so a KILLED refresher doesn't wedge `inprocess` for the timeout.
|
|
71
47
|
class MainSyncLock {
|
|
@@ -86,9 +62,13 @@ exports.MainSyncLock = MainSyncLock;
|
|
|
86
62
|
let MainSyncStatusService = class MainSyncStatusService {
|
|
87
63
|
dotDir;
|
|
88
64
|
atomicFile;
|
|
89
|
-
|
|
65
|
+
store;
|
|
66
|
+
worktrees;
|
|
67
|
+
constructor(dotDir = state_dir_1.dotWebpieces, atomicFile = new atomic_file_1.AtomicFile(), store = new main_sync_file_1.MainSyncFileStore(atomicFile), worktrees = new worktrees_1.WorktreeService()) {
|
|
90
68
|
this.dotDir = dotDir;
|
|
91
69
|
this.atomicFile = atomicFile;
|
|
70
|
+
this.store = store;
|
|
71
|
+
this.worktrees = worktrees;
|
|
92
72
|
}
|
|
93
73
|
/**
|
|
94
74
|
* SHARED scope: `<primary>/.webpieces/main-sync-status.json`, one per repo.
|
|
@@ -97,14 +77,13 @@ let MainSyncStatusService = class MainSyncStatusService {
|
|
|
97
77
|
* one `.git`, one `origin/main`, one fetch, one answer. A per-worktree copy under single-flight
|
|
98
78
|
* would only ever be written for whichever worktree won the lock anyway.
|
|
99
79
|
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
* atomically) is the obvious follow-up.
|
|
80
|
+
* FORMERLY a known defect, now fixed: the file's CONTENT used to be keyed to ONE branch, and every
|
|
81
|
+
* reader fails OPEN when the cached branch is not the branch being judged (`stale-cross-branch-cache
|
|
82
|
+
* (fail-open)`). With several worktrees on several branches, only the winner of the lock was ever
|
|
83
|
+
* described, so every other worktree's guards abstained — and which one was armed thrashed as the
|
|
84
|
+
* lock changed hands. The file is now a MAP of branch -> status (see MainSyncStatusFile): the single
|
|
85
|
+
* winning refresher enumerates every worktree and records all of their branches in one atomic write,
|
|
86
|
+
* so one fetch arms everyone. Single-flight is unchanged; only what the winner writes changed.
|
|
108
87
|
*/
|
|
109
88
|
mainSyncStatusPath(repoRoot) {
|
|
110
89
|
return this.dotDir.sharedFile(repoRoot, MAIN_SYNC_STATUS_FILE);
|
|
@@ -126,33 +105,41 @@ let MainSyncStatusService = class MainSyncStatusService {
|
|
|
126
105
|
mainSyncLockPath(repoRoot) {
|
|
127
106
|
return this.dotDir.sharedFile(repoRoot, MAIN_SYNC_LOCK_FILE);
|
|
128
107
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const error = (0, to_error_1.toError)(err);
|
|
144
|
-
void error;
|
|
145
|
-
return null;
|
|
146
|
-
}
|
|
108
|
+
/**
|
|
109
|
+
* The cached status FOR ONE BRANCH — the only read the four main-sync guards perform.
|
|
110
|
+
*
|
|
111
|
+
* `branch` is mandatory and is the branch being judged. A branch the last refresh never saw
|
|
112
|
+
* returns null, which every guard already treats as `no-sync-cache (fail-open)`, so an unknown
|
|
113
|
+
* branch degrades exactly as a missing file did. Any error (missing, truncated, malformed JSON)
|
|
114
|
+
* also returns null — this must never throw on the blocking path.
|
|
115
|
+
*/
|
|
116
|
+
readMainSyncStatus(repoRoot, branch) {
|
|
117
|
+
return this.store.branchStatus(this.readMainSyncStatusFile(repoRoot), branch);
|
|
118
|
+
}
|
|
119
|
+
// The whole branch-keyed document, or null. Callers that need more than one branch use this.
|
|
120
|
+
readMainSyncStatusFile(repoRoot) {
|
|
121
|
+
return this.store.readFile(this.mainSyncStatusPath(repoRoot));
|
|
147
122
|
}
|
|
148
123
|
/**
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
124
|
+
* Merge ONE branch's entry into the existing map, atomically, leaving siblings untouched.
|
|
125
|
+
*
|
|
126
|
+
* Read-modify-write rather than replace: this is the writer used by everyone who is NOT the
|
|
127
|
+
* single-flight refresher (notably pr-gate's post-merge stamp, which holds no lock and knows about
|
|
128
|
+
* exactly one branch). Replacing the document from there would erase every other worktree's entry
|
|
129
|
+
* and silently disarm their guards until the next refresh.
|
|
153
130
|
*/
|
|
154
131
|
writeMainSyncStatus(repoRoot, status) {
|
|
155
|
-
this.
|
|
132
|
+
this.store.mergeBranch(this.mainSyncStatusPath(repoRoot), status);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Replace the WHOLE document — only the winning refresher may do this.
|
|
136
|
+
*
|
|
137
|
+
* ATOMIC (temp file + `rename()` in the same directory). The refresher writes this file while
|
|
138
|
+
* guards on the blocking path read it, possibly from another worktree; a plain `writeFileSync`
|
|
139
|
+
* truncates first, so a reader landing in that window gets a torn document. See AtomicFile.
|
|
140
|
+
*/
|
|
141
|
+
writeMainSyncStatusFile(repoRoot, file) {
|
|
142
|
+
this.store.writeFile(this.mainSyncStatusPath(repoRoot), file);
|
|
156
143
|
}
|
|
157
144
|
readMainSyncLock(repoRoot) {
|
|
158
145
|
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
@@ -249,16 +236,59 @@ let MainSyncStatusService = class MainSyncStatusService {
|
|
|
249
236
|
return new MainSyncLock(LOCK_STATE_FINISHED, started, 0);
|
|
250
237
|
}
|
|
251
238
|
/**
|
|
252
|
-
* The SLOW path
|
|
253
|
-
*
|
|
239
|
+
* The SLOW path for ONE branch: the branch checked out in `repoRoot`. Runs its own fetch and its
|
|
240
|
+
* own `gh` query, so it is the right entry point for a single-worktree caller and for tests.
|
|
241
|
+
* The refresher uses computeAllMainSyncStatuses instead — see there for why.
|
|
254
242
|
*/
|
|
255
|
-
// webpieces-disable max-lines-new-methods -- one cohesive slow-path computation
|
|
256
243
|
computeMainSyncStatus(repoRoot) {
|
|
257
|
-
const branch = this.gitBranch(repoRoot);
|
|
258
|
-
const mergedPr = this.detectMergedPr(repoRoot, branch);
|
|
259
|
-
const openPr = this.detectOpenPr(repoRoot, branch);
|
|
260
244
|
// Best-effort network refresh; offline just means we evaluate against the last-fetched ref.
|
|
261
245
|
this.fetchOriginMain(repoRoot);
|
|
246
|
+
return this.computeBranchStatus(repoRoot, this.gitBranch(repoRoot), this.loadPullRequestIndex(repoRoot));
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* The SLOW path for EVERY branch this repo has checked out — what the detached refresher runs.
|
|
250
|
+
*
|
|
251
|
+
* The cache is shared by all worktrees but was written for one branch, so only one worktree's
|
|
252
|
+
* guards were ever armed. Enumerating the worktrees here and writing all of their branches in one
|
|
253
|
+
* atomic map arms every worktree off a single refresh.
|
|
254
|
+
*
|
|
255
|
+
* NETWORK COST DOES NOT SCALE WITH BRANCH COUNT, which is the whole reason this is a separate
|
|
256
|
+
* method: ONE `git fetch` and ONE repo-wide `gh pr list` for all N branches (the old code asked
|
|
257
|
+
* `gh` twice per branch, so looping it would have cost 2N round trips). Everything after that is
|
|
258
|
+
* local git in each worktree — merge-base, rev-parse, diff, ls-files.
|
|
259
|
+
*
|
|
260
|
+
* The map is rebuilt from the LIVE worktree set every refresh, so a deleted branch or removed
|
|
261
|
+
* worktree simply stops appearing. No pruning pass, no unbounded growth.
|
|
262
|
+
*/
|
|
263
|
+
computeAllMainSyncStatuses(repoRoot) {
|
|
264
|
+
this.fetchOriginMain(repoRoot);
|
|
265
|
+
const index = this.loadPullRequestIndex(repoRoot);
|
|
266
|
+
const branches = {};
|
|
267
|
+
for (const tree of this.worktrees.listWorktrees(repoRoot)) {
|
|
268
|
+
// Detached HEAD (and a bare worktree) has no branch to key an entry on. Skipping it must
|
|
269
|
+
// not stop the others being recorded, which is why this is a `continue`, not a bail-out.
|
|
270
|
+
if (tree.branch === '' || this.hasBranch(branches, tree.branch))
|
|
271
|
+
continue;
|
|
272
|
+
branches[tree.branch] = this.computeBranchStatus(tree.path, tree.branch, index);
|
|
273
|
+
}
|
|
274
|
+
// `main` always present, even with no worktree on it: read-stale-guard's state A looks itself
|
|
275
|
+
// up under 'main', and an absent entry there would silently disarm the stale-main block.
|
|
276
|
+
if (!this.hasBranch(branches, 'main'))
|
|
277
|
+
branches['main'] = this.mainOnlyStatus(repoRoot);
|
|
278
|
+
return new main_sync_file_1.MainSyncStatusFile(main_sync_file_1.MAIN_SYNC_STATUS_VERSION, new Date().toISOString(), branches);
|
|
279
|
+
}
|
|
280
|
+
hasBranch(branches, branch) {
|
|
281
|
+
return Object.prototype.hasOwnProperty.call(branches, branch);
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* One branch's status, computed in `worktreePath` (the tree that has it checked out — the
|
|
285
|
+
* working-tree overlap signals are only correct from there). Does NOT fetch and does NOT call
|
|
286
|
+
* `gh`: both are done once per refresh by the caller.
|
|
287
|
+
*/
|
|
288
|
+
// webpieces-disable max-lines-new-methods -- one cohesive slow-path computation
|
|
289
|
+
computeBranchStatus(repoRoot, branch, index) {
|
|
290
|
+
const mergedPr = index.mergedFor(branch);
|
|
291
|
+
const openPr = index.openFor(branch);
|
|
262
292
|
const head = this.capture(repoRoot, 'git', ['rev-parse', 'HEAD']);
|
|
263
293
|
const originMain = this.capture(repoRoot, 'git', ['rev-parse', 'origin/main']);
|
|
264
294
|
const localMain = this.localMainHash(repoRoot);
|
|
@@ -273,7 +303,7 @@ let MainSyncStatusService = class MainSyncStatusService {
|
|
|
273
303
|
}
|
|
274
304
|
const forkPoint = this.capture(repoRoot, 'git', ['merge-base', 'origin/main', 'HEAD']);
|
|
275
305
|
if (!forkPoint.ok || forkPoint.out === '') {
|
|
276
|
-
const noFork = new MainSyncStatus(branch, mergedPr !== '', mergedPr, false, null, originMain.out, featureHead, false, [], new Date().toISOString());
|
|
306
|
+
const noFork = new main_sync_file_1.MainSyncStatus(branch, mergedPr !== '', mergedPr, false, null, originMain.out, featureHead, false, [], new Date().toISOString());
|
|
277
307
|
noFork.openPr = openPr;
|
|
278
308
|
noFork.localMain = localMain;
|
|
279
309
|
return noFork;
|
|
@@ -281,7 +311,7 @@ let MainSyncStatusService = class MainSyncStatusService {
|
|
|
281
311
|
const featureFiles = new Set(this.featureChangedFiles(repoRoot, forkPoint.out));
|
|
282
312
|
const mainFiles = this.changedFiles(repoRoot, forkPoint.out, 'origin/main');
|
|
283
313
|
const conflictFiles = mainFiles.filter((file) => featureFiles.has(file));
|
|
284
|
-
const status = new MainSyncStatus(branch, mergedPr !== '', mergedPr, true, forkPoint.out, originMain.out, featureHead, conflictFiles.length > 0, conflictFiles, new Date().toISOString());
|
|
314
|
+
const status = new main_sync_file_1.MainSyncStatus(branch, mergedPr !== '', mergedPr, true, forkPoint.out, originMain.out, featureHead, conflictFiles.length > 0, conflictFiles, new Date().toISOString());
|
|
285
315
|
status.openPr = openPr;
|
|
286
316
|
status.localMain = localMain;
|
|
287
317
|
return status;
|
|
@@ -302,7 +332,13 @@ let MainSyncStatusService = class MainSyncStatusService {
|
|
|
302
332
|
'5. If a PR exists: open a NEW PR for the -v2 branch and close the old one.',
|
|
303
333
|
];
|
|
304
334
|
}
|
|
305
|
-
|
|
335
|
+
/**
|
|
336
|
+
* Synchronously stamp a clean "up to date with main" status — call right after a successful merge.
|
|
337
|
+
*
|
|
338
|
+
* This is the one writer that takes NO lock, and it knows about exactly one branch. It therefore
|
|
339
|
+
* goes through writeMainSyncStatus, which MERGES its entry into the existing map; writing a whole
|
|
340
|
+
* document from here would wipe every other worktree's entry.
|
|
341
|
+
*/
|
|
306
342
|
stampCleanMainSyncStatus(repoRoot) {
|
|
307
343
|
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
308
344
|
try {
|
|
@@ -311,7 +347,7 @@ let MainSyncStatusService = class MainSyncStatusService {
|
|
|
311
347
|
const featureHead = this.capture(repoRoot, 'git', ['rev-parse', 'HEAD']);
|
|
312
348
|
if (!originMain.ok || !featureHead.ok)
|
|
313
349
|
return;
|
|
314
|
-
const status = new MainSyncStatus(branch, false, '', true, originMain.out, originMain.out, featureHead.out, false, [], new Date().toISOString());
|
|
350
|
+
const status = new main_sync_file_1.MainSyncStatus(branch, false, '', true, originMain.out, originMain.out, featureHead.out, false, [], new Date().toISOString());
|
|
315
351
|
status.localMain = this.localMainHash(repoRoot);
|
|
316
352
|
this.writeMainSyncStatus(repoRoot, status);
|
|
317
353
|
}
|
|
@@ -411,30 +447,44 @@ let MainSyncStatusService = class MainSyncStatusService {
|
|
|
411
447
|
add(['ls-files', '--others', '--exclude-standard']); // untracked new files (respects .gitignore)
|
|
412
448
|
return [...out];
|
|
413
449
|
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
450
|
+
/**
|
|
451
|
+
* ONE repo-wide `gh pr list` giving every branch's merged/open PR — replaces the two per-branch
|
|
452
|
+
* `--head <branch>` queries. `--limit 200` bounds it; a branch whose only PR is older than that
|
|
453
|
+
* falls out of the index and reads as "no PR", which is the fail-OPEN direction (no merged PR =
|
|
454
|
+
* no block), the same way a missing `gh` or an offline machine already degraded.
|
|
455
|
+
*/
|
|
456
|
+
loadPullRequestIndex(repoRoot) {
|
|
457
|
+
const result = this.capture(repoRoot, 'gh', [
|
|
458
|
+
'pr', 'list', '--state', 'all', '--json', 'number,headRefName,state', '--limit', '200',
|
|
459
|
+
]);
|
|
460
|
+
if (!result.ok || result.out === '')
|
|
461
|
+
return new main_sync_file_1.PullRequestIndex({}, {});
|
|
462
|
+
return this.store.indexPullRequests(result.out);
|
|
420
463
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
464
|
+
/**
|
|
465
|
+
* A `main` entry synthesised WITHOUT a worktree standing on main. Carries only the two hashes
|
|
466
|
+
* read-stale-guard / stale-main-bash-guard actually compare (originMain vs the live tree's
|
|
467
|
+
* ancestry) — never merged, never conflicting, so it can only ever produce the stale-main verdict.
|
|
468
|
+
*/
|
|
469
|
+
mainOnlyStatus(repoRoot) {
|
|
470
|
+
const originMain = this.capture(repoRoot, 'git', ['rev-parse', 'origin/main']);
|
|
471
|
+
const localMain = this.localMainHash(repoRoot);
|
|
472
|
+
const status = new main_sync_file_1.MainSyncStatus('main', false, '', true, localMain === '' ? null : localMain, originMain.ok ? originMain.out : '', localMain, false, [], new Date().toISOString());
|
|
473
|
+
status.localMain = localMain;
|
|
474
|
+
return status;
|
|
427
475
|
}
|
|
428
476
|
// A benign status that never blocks — used when origin/main can't be resolved.
|
|
429
477
|
benignStatus(branch, featureHead) {
|
|
430
|
-
return new MainSyncStatus(branch, false, '', true, null, '', featureHead, false, [], new Date().toISOString());
|
|
478
|
+
return new main_sync_file_1.MainSyncStatus(branch, false, '', true, null, '', featureHead, false, [], new Date().toISOString());
|
|
431
479
|
}
|
|
432
480
|
};
|
|
433
481
|
exports.MainSyncStatusService = MainSyncStatusService;
|
|
434
482
|
exports.MainSyncStatusService = MainSyncStatusService = tslib_1.__decorate([
|
|
435
483
|
(0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton),
|
|
436
484
|
tslib_1.__metadata("design:paramtypes", [state_dir_1.DotWebpieces,
|
|
437
|
-
atomic_file_1.AtomicFile
|
|
485
|
+
atomic_file_1.AtomicFile,
|
|
486
|
+
main_sync_file_1.MainSyncFileStore,
|
|
487
|
+
worktrees_1.WorktreeService])
|
|
438
488
|
], MainSyncStatusService);
|
|
439
489
|
// Temporary migration delegators to MainSyncStatusService — removed once consumers inject it.
|
|
440
490
|
const mainSyncSvc = new MainSyncStatusService();
|
|
@@ -443,10 +493,14 @@ function mainSyncStatusPath(repoRoot) { return mainSyncSvc.mainSyncStatusPath(re
|
|
|
443
493
|
// webpieces-disable no-function-outside-class -- temporary back-compat delegator to MainSyncStatusService; removed once consumers inject it
|
|
444
494
|
function mainSyncLockPath(repoRoot) { return mainSyncSvc.mainSyncLockPath(repoRoot); }
|
|
445
495
|
// webpieces-disable no-function-outside-class -- temporary back-compat delegator to MainSyncStatusService; removed once consumers inject it
|
|
446
|
-
function readMainSyncStatus(repoRoot) { return mainSyncSvc.readMainSyncStatus(repoRoot); }
|
|
496
|
+
function readMainSyncStatus(repoRoot, branch) { return mainSyncSvc.readMainSyncStatus(repoRoot, branch); }
|
|
497
|
+
// webpieces-disable no-function-outside-class -- temporary back-compat delegator to MainSyncStatusService; removed once consumers inject it
|
|
498
|
+
function readMainSyncStatusFile(repoRoot) { return mainSyncSvc.readMainSyncStatusFile(repoRoot); }
|
|
447
499
|
// webpieces-disable no-function-outside-class -- temporary back-compat delegator to MainSyncStatusService; removed once consumers inject it
|
|
448
500
|
function writeMainSyncStatus(repoRoot, status) { mainSyncSvc.writeMainSyncStatus(repoRoot, status); }
|
|
449
501
|
// webpieces-disable no-function-outside-class -- temporary back-compat delegator to MainSyncStatusService; removed once consumers inject it
|
|
502
|
+
function writeMainSyncStatusFile(repoRoot, file) { mainSyncSvc.writeMainSyncStatusFile(repoRoot, file); }
|
|
503
|
+
// webpieces-disable no-function-outside-class -- temporary back-compat delegator to MainSyncStatusService; removed once consumers inject it
|
|
450
504
|
function readMainSyncLock(repoRoot) { return mainSyncSvc.readMainSyncLock(repoRoot); }
|
|
451
505
|
// webpieces-disable no-function-outside-class -- temporary back-compat delegator to MainSyncStatusService; removed once consumers inject it
|
|
452
506
|
function writeMainSyncLock(repoRoot, lock) { mainSyncSvc.writeMainSyncLock(repoRoot, lock); }
|
|
@@ -463,6 +517,8 @@ function finishedLock(started) { return mainSyncSvc.finishedLock(started); }
|
|
|
463
517
|
// webpieces-disable no-function-outside-class -- temporary back-compat delegator to MainSyncStatusService; removed once consumers inject it
|
|
464
518
|
function computeMainSyncStatus(repoRoot) { return mainSyncSvc.computeMainSyncStatus(repoRoot); }
|
|
465
519
|
// webpieces-disable no-function-outside-class -- temporary back-compat delegator to MainSyncStatusService; removed once consumers inject it
|
|
520
|
+
function computeAllMainSyncStatuses(repoRoot) { return mainSyncSvc.computeAllMainSyncStatuses(repoRoot); }
|
|
521
|
+
// webpieces-disable no-function-outside-class -- temporary back-compat delegator to MainSyncStatusService; removed once consumers inject it
|
|
466
522
|
function squashRecoverySteps(currentBranch) { return mainSyncSvc.squashRecoverySteps(currentBranch); }
|
|
467
523
|
// webpieces-disable no-function-outside-class -- temporary back-compat delegator to MainSyncStatusService; removed once consumers inject it
|
|
468
524
|
function stampCleanMainSyncStatus(repoRoot) { mainSyncSvc.stampCleanMainSyncStatus(repoRoot); }
|