@webpieces/repo-workflow-core 0.4.975 → 0.4.977
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 +5 -2
- package/src/branch-archiver.d.ts +7 -4
- package/src/branch-archiver.js +40 -24
- package/src/branch-archiver.js.map +1 -1
- package/src/branch-reaper.d.ts +5 -3
- package/src/branch-reaper.js +33 -20
- package/src/branch-reaper.js.map +1 -1
- package/src/command-timeouts.d.ts +27 -7
- package/src/command-timeouts.js +64 -9
- package/src/command-timeouts.js.map +1 -1
- package/src/main-sync-status.d.ts +6 -9
- package/src/main-sync-status.js +66 -70
- package/src/main-sync-status.js.map +1 -1
- package/src/merged-branches.d.ts +5 -4
- package/src/merged-branches.js +55 -42
- package/src/merged-branches.js.map +1 -1
- package/src/worktree-lock-verdicts.d.ts +1 -1
- package/src/worktree-lock-verdicts.js +2 -2
- package/src/worktree-lock-verdicts.js.map +1 -1
- package/src/worktree-reaper.d.ts +4 -2
- package/src/worktree-reaper.js +35 -22
- package/src/worktree-reaper.js.map +1 -1
- package/src/worktrees.d.ts +7 -2
- package/src/worktrees.js +18 -15
- package/src/worktrees.js.map +1 -1
package/src/command-timeouts.js
CHANGED
|
@@ -4,34 +4,89 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
4
4
|
exports.CommandTimeouts = void 0;
|
|
5
5
|
const tslib_1 = require("tslib");
|
|
6
6
|
const inversify_1 = require("inversify");
|
|
7
|
+
const tooling_common_1 = require("@webpieces/tooling-common");
|
|
7
8
|
/**
|
|
8
|
-
* The ceiling on every external command the detached main-sync refresher runs (#1342)
|
|
9
|
+
* The ceiling on every external command the detached main-sync refresher runs (#1342), and the ONE place
|
|
10
|
+
* those commands are bounded.
|
|
9
11
|
*
|
|
10
12
|
* WHY EVERY ONE NEEDS A CEILING. The refresher is single-flight behind `main-sync.lock.json`, and the
|
|
11
13
|
* lock is reclaimed once its holder is older than `hangTimeoutMinutes` (default 5). Reclaiming used to
|
|
12
|
-
* be the whole of hang recovery, and it was not enough: no
|
|
14
|
+
* be the whole of hang recovery, and it was not enough: no command here carried a deadline, so a
|
|
13
15
|
* `git fetch` stuck on an ssh handshake, or a `gh pr list` stuck on the network, lived forever — and
|
|
14
16
|
* after every reclaim the next refresher could stack another one beside it. A bounded command makes a
|
|
15
17
|
* hung run END on its own; the reclaimer's kill (MainSyncLockService.tryAcquireMainSyncLock) is the
|
|
16
|
-
* backstop for a process that is stuck somewhere no
|
|
18
|
+
* backstop for a process that is stuck somewhere no deadline reaches.
|
|
17
19
|
*
|
|
18
20
|
* Sized so ONE network command plus a handful of local ones stays well inside the default
|
|
19
|
-
* 5-minute hang window.
|
|
20
|
-
*
|
|
21
|
+
* 5-minute hang window. The kill is SIGTERM (node's default), which lets git clean up its own `.lock`
|
|
22
|
+
* files rather than leaving them behind as SIGKILL would.
|
|
23
|
+
*
|
|
24
|
+
* HOW (#1381): the command goes through the process edges, and its deadline is a task scheduled on the
|
|
25
|
+
* clock edge (`TimerApi`) that aborts the run. A feature test therefore SEES each requested deadline
|
|
26
|
+
* (`MockTimerApi.getScheduledTasks()`), and fires it itself to prove the timeout path — no real time
|
|
27
|
+
* passes. The one synchronous form ({@link localGitNow}) exists for the PreToolUse guard's blocking path,
|
|
28
|
+
* whose `check()` contract is synchronous; its kill-after is passed straight to the edge.
|
|
21
29
|
*/
|
|
22
30
|
let CommandTimeouts = class CommandTimeouts {
|
|
23
31
|
static { CommandTimeouts_1 = this; }
|
|
32
|
+
git;
|
|
33
|
+
processes;
|
|
34
|
+
timer;
|
|
24
35
|
/** `git fetch` and `gh` — the two calls that cross the network. */
|
|
25
36
|
static NETWORK_MS = 90 * 1000;
|
|
26
37
|
/** Local git (merge-base, rev-list, worktree list, status, …). Generous: a cold, large repo is slow, never hung. */
|
|
27
38
|
static LOCAL_GIT_MS = 30 * 1000;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
39
|
+
// Defaulted so the non-DI call sites (the guard, the detached refresher) can `new` this, while
|
|
40
|
+
// inversify injects the container's edges — which is what a feature test rebinds.
|
|
41
|
+
constructor(git = new tooling_common_1.GitProcessClient(), processes = new tooling_common_1.NativeProcessClient(), timer = new tooling_common_1.TimerClient()) {
|
|
42
|
+
this.git = git;
|
|
43
|
+
this.processes = processes;
|
|
44
|
+
this.timer = timer;
|
|
45
|
+
}
|
|
46
|
+
/** Local git, killed after {@link LOCAL_GIT_MS}. */
|
|
47
|
+
localGit(args, cwd) {
|
|
48
|
+
return this.boundedGit(args, cwd, CommandTimeouts_1.LOCAL_GIT_MS);
|
|
49
|
+
}
|
|
50
|
+
/** Network git (`fetch`), killed after {@link NETWORK_MS}. */
|
|
51
|
+
networkGit(args, cwd) {
|
|
52
|
+
return this.boundedGit(args, cwd, CommandTimeouts_1.NETWORK_MS);
|
|
53
|
+
}
|
|
54
|
+
/** `gh`, killed after {@link NETWORK_MS}. */
|
|
55
|
+
async gh(args, cwd) {
|
|
56
|
+
const abort = new AbortController();
|
|
57
|
+
const deadline = this.deadline(CommandTimeouts_1.NETWORK_MS, `gh ${args.join(' ')}`, abort);
|
|
58
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
59
|
+
try {
|
|
60
|
+
return await this.processes.run('gh', args, cwd, abort.signal);
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
deadline.cancel();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** Local git, SYNCHRONOUSLY — the guard's blocking path only. Killed after {@link LOCAL_GIT_MS}. */
|
|
67
|
+
localGitNow(args, cwd) {
|
|
68
|
+
return this.git.runSync(args, cwd, CommandTimeouts_1.LOCAL_GIT_MS);
|
|
69
|
+
}
|
|
70
|
+
async boundedGit(args, cwd, timeoutMs) {
|
|
71
|
+
const abort = new AbortController();
|
|
72
|
+
const deadline = this.deadline(timeoutMs, `git ${args.join(' ')}`, abort);
|
|
73
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
74
|
+
try {
|
|
75
|
+
return await this.git.run(args, cwd, abort.signal);
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
deadline.cancel();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
deadline(timeoutMs, command, abort) {
|
|
82
|
+
return this.timer.schedule(timeoutMs, new tooling_common_1.TaskDto(`deadline for \`${command}\``, () => abort.abort()));
|
|
31
83
|
}
|
|
32
84
|
};
|
|
33
85
|
exports.CommandTimeouts = CommandTimeouts;
|
|
34
86
|
exports.CommandTimeouts = CommandTimeouts = CommandTimeouts_1 = tslib_1.__decorate([
|
|
35
|
-
(0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton)
|
|
87
|
+
(0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton),
|
|
88
|
+
tslib_1.__metadata("design:paramtypes", [tooling_common_1.GitProcessApi,
|
|
89
|
+
tooling_common_1.NativeProcessApi,
|
|
90
|
+
tooling_common_1.TimerApi])
|
|
36
91
|
], CommandTimeouts);
|
|
37
92
|
//# sourceMappingURL=command-timeouts.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"command-timeouts.js","sourceRoot":"","sources":["../../../../../packages/tooling/repo-workflow-core/src/command-timeouts.ts"],"names":[],"mappings":";;;;;AAAA,yCAA2D;
|
|
1
|
+
{"version":3,"file":"command-timeouts.js","sourceRoot":"","sources":["../../../../../packages/tooling/repo-workflow-core/src/command-timeouts.ts"],"names":[],"mappings":";;;;;AAAA,yCAA2D;AAC3D,8DAWmC;AAEnC;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEI,IAAM,eAAe,GAArB,MAAM,eAAe;;IASH;IACA;IACA;IAVrB,mEAAmE;IACnE,MAAM,CAAU,UAAU,GAAG,EAAE,GAAG,IAAI,CAAC;IACvC,oHAAoH;IACpH,MAAM,CAAU,YAAY,GAAG,EAAE,GAAG,IAAI,CAAC;IAEzC,+FAA+F;IAC/F,kFAAkF;IAClF,YACqB,MAAqB,IAAI,iCAAgB,EAAE,EAC3C,YAA8B,IAAI,oCAAmB,EAAE,EACvD,QAAkB,IAAI,4BAAW,EAAE;QAFnC,QAAG,GAAH,GAAG,CAAwC;QAC3C,cAAS,GAAT,SAAS,CAA8C;QACvD,UAAK,GAAL,KAAK,CAA8B;IACrD,CAAC;IAEJ,oDAAoD;IACpD,QAAQ,CAAC,IAAc,EAAE,GAAW;QAChC,OAAO,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,GAAG,EAAE,iBAAe,CAAC,YAAY,CAAC,CAAC;IACpE,CAAC;IAED,8DAA8D;IAC9D,UAAU,CAAC,IAAc,EAAE,GAAW;QAClC,OAAO,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,GAAG,EAAE,iBAAe,CAAC,UAAU,CAAC,CAAC;IAClE,CAAC;IAED,6CAA6C;IAC7C,KAAK,CAAC,EAAE,CAAC,IAAc,EAAE,GAAW;QAChC,MAAM,KAAK,GAAG,IAAI,eAAe,EAAE,CAAC;QACpC,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,iBAAe,CAAC,UAAU,EAAE,MAAM,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;QAC1F,8DAA8D;QAC9D,IAAI,CAAC;YACD,OAAO,MAAM,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;QACnE,CAAC;gBAAS,CAAC;YACP,QAAQ,CAAC,MAAM,EAAE,CAAC;QACtB,CAAC;IACL,CAAC;IAED,oGAAoG;IACpG,WAAW,CAAC,IAAc,EAAE,GAAW;QACnC,OAAO,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,EAAE,iBAAe,CAAC,YAAY,CAAC,CAAC;IACrE,CAAC;IAEO,KAAK,CAAC,UAAU,CAAC,IAAc,EAAE,GAAW,EAAE,SAAiB;QACnE,MAAM,KAAK,GAAG,IAAI,eAAe,EAAE,CAAC;QACpC,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,SAAS,EAAE,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;QAC1E,8DAA8D;QAC9D,IAAI,CAAC;YACD,OAAO,MAAM,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;QACvD,CAAC;gBAAS,CAAC;YACP,QAAQ,CAAC,MAAM,EAAE,CAAC;QACtB,CAAC;IACL,CAAC;IAEO,QAAQ,CAAC,SAAiB,EAAE,OAAe,EAAE,KAAsB;QACvE,OAAO,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,SAAS,EAAE,IAAI,wBAAO,CAAC,kBAAkB,OAAO,IAAI,EAAE,GAAS,EAAE,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;IACjH,CAAC;;AAtDQ,0CAAe;0BAAf,eAAe;IAD3B,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;6CAUX,8BAAa;QACP,iCAAgB;QACpB,yBAAQ;GAX3B,eAAe,CAuD3B","sourcesContent":["import { injectable, bindingScopeValues } from 'inversify';\nimport {\n GitProcessApi,\n GitProcessClient,\n GitResultDto,\n NativeProcessApi,\n NativeProcessClient,\n ProcessResultDto,\n ScheduledTaskDto,\n TaskDto,\n TimerApi,\n TimerClient,\n} from '@webpieces/tooling-common';\n\n/**\n * The ceiling on every external command the detached main-sync refresher runs (#1342), and the ONE place\n * those commands are bounded.\n *\n * WHY EVERY ONE NEEDS A CEILING. The refresher is single-flight behind `main-sync.lock.json`, and the\n * lock is reclaimed once its holder is older than `hangTimeoutMinutes` (default 5). Reclaiming used to\n * be the whole of hang recovery, and it was not enough: no command here carried a deadline, so a\n * `git fetch` stuck on an ssh handshake, or a `gh pr list` stuck on the network, lived forever — and\n * after every reclaim the next refresher could stack another one beside it. A bounded command makes a\n * hung run END on its own; the reclaimer's kill (MainSyncLockService.tryAcquireMainSyncLock) is the\n * backstop for a process that is stuck somewhere no deadline reaches.\n *\n * Sized so ONE network command plus a handful of local ones stays well inside the default\n * 5-minute hang window. The kill is SIGTERM (node's default), which lets git clean up its own `.lock`\n * files rather than leaving them behind as SIGKILL would.\n *\n * HOW (#1381): the command goes through the process edges, and its deadline is a task scheduled on the\n * clock edge (`TimerApi`) that aborts the run. A feature test therefore SEES each requested deadline\n * (`MockTimerApi.getScheduledTasks()`), and fires it itself to prove the timeout path — no real time\n * passes. The one synchronous form ({@link localGitNow}) exists for the PreToolUse guard's blocking path,\n * whose `check()` contract is synchronous; its kill-after is passed straight to the edge.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class CommandTimeouts {\n /** `git fetch` and `gh` — the two calls that cross the network. */\n static readonly NETWORK_MS = 90 * 1000;\n /** Local git (merge-base, rev-list, worktree list, status, …). Generous: a cold, large repo is slow, never hung. */\n static readonly LOCAL_GIT_MS = 30 * 1000;\n\n // Defaulted so the non-DI call sites (the guard, the detached refresher) can `new` this, while\n // inversify injects the container's edges — which is what a feature test rebinds.\n constructor(\n private readonly git: GitProcessApi = new GitProcessClient(),\n private readonly processes: NativeProcessApi = new NativeProcessClient(),\n private readonly timer: TimerApi = new TimerClient(),\n ) {}\n\n /** Local git, killed after {@link LOCAL_GIT_MS}. */\n localGit(args: string[], cwd: string): Promise<GitResultDto> {\n return this.boundedGit(args, cwd, CommandTimeouts.LOCAL_GIT_MS);\n }\n\n /** Network git (`fetch`), killed after {@link NETWORK_MS}. */\n networkGit(args: string[], cwd: string): Promise<GitResultDto> {\n return this.boundedGit(args, cwd, CommandTimeouts.NETWORK_MS);\n }\n\n /** `gh`, killed after {@link NETWORK_MS}. */\n async gh(args: string[], cwd: string): Promise<ProcessResultDto> {\n const abort = new AbortController();\n const deadline = this.deadline(CommandTimeouts.NETWORK_MS, `gh ${args.join(' ')}`, abort);\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return await this.processes.run('gh', args, cwd, abort.signal);\n } finally {\n deadline.cancel();\n }\n }\n\n /** Local git, SYNCHRONOUSLY — the guard's blocking path only. Killed after {@link LOCAL_GIT_MS}. */\n localGitNow(args: string[], cwd: string): GitResultDto {\n return this.git.runSync(args, cwd, CommandTimeouts.LOCAL_GIT_MS);\n }\n\n private async boundedGit(args: string[], cwd: string, timeoutMs: number): Promise<GitResultDto> {\n const abort = new AbortController();\n const deadline = this.deadline(timeoutMs, `git ${args.join(' ')}`, abort);\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return await this.git.run(args, cwd, abort.signal);\n } finally {\n deadline.cancel();\n }\n }\n\n private deadline(timeoutMs: number, command: string, abort: AbortController): ScheduledTaskDto {\n return this.timer.schedule(timeoutMs, new TaskDto(`deadline for \\`${command}\\``, (): void => abort.abort()));\n }\n}\n"]}
|
|
@@ -14,8 +14,8 @@ export declare class MainSyncStatusService {
|
|
|
14
14
|
private readonly atomicFile;
|
|
15
15
|
private readonly store;
|
|
16
16
|
private readonly worktrees;
|
|
17
|
-
private readonly
|
|
18
|
-
constructor(dotDir?: DotWebpieces, atomicFile?: AtomicFile, store?: MainSyncFileStore, worktrees?: WorktreeService,
|
|
17
|
+
private readonly commands;
|
|
18
|
+
constructor(dotDir?: DotWebpieces, atomicFile?: AtomicFile, store?: MainSyncFileStore, worktrees?: WorktreeService, commands?: CommandTimeouts);
|
|
19
19
|
/**
|
|
20
20
|
* SHARED scope: `<primary>/.webpieces/main-sync-status.json`, one per repo.
|
|
21
21
|
*
|
|
@@ -71,7 +71,7 @@ export declare class MainSyncStatusService {
|
|
|
71
71
|
* own `gh` query, so it is the right entry point for a single-worktree caller and for tests.
|
|
72
72
|
* The refresher uses computeAllMainSyncStatuses instead — see there for why.
|
|
73
73
|
*/
|
|
74
|
-
computeMainSyncStatus(repoRoot: string): MainSyncStatus
|
|
74
|
+
computeMainSyncStatus(repoRoot: string): Promise<MainSyncStatus>;
|
|
75
75
|
/**
|
|
76
76
|
* The SLOW path for EVERY branch this repo has checked out — what the detached refresher runs.
|
|
77
77
|
*
|
|
@@ -87,7 +87,7 @@ export declare class MainSyncStatusService {
|
|
|
87
87
|
* The map is rebuilt from the LIVE worktree set every refresh, so a deleted branch or removed
|
|
88
88
|
* worktree simply stops appearing. No pruning pass, no unbounded growth.
|
|
89
89
|
*/
|
|
90
|
-
computeAllMainSyncStatuses(repoRoot: string): MainSyncStatusFile
|
|
90
|
+
computeAllMainSyncStatuses(repoRoot: string): Promise<MainSyncStatusFile>;
|
|
91
91
|
private hasBranch;
|
|
92
92
|
/**
|
|
93
93
|
* One branch's status, computed in `worktreePath` (the tree that has it checked out — the
|
|
@@ -105,7 +105,7 @@ export declare class MainSyncStatusService {
|
|
|
105
105
|
* goes through writeMainSyncStatus, which MERGES its entry into the existing map; writing a whole
|
|
106
106
|
* document from here would wipe every other worktree's entry.
|
|
107
107
|
*/
|
|
108
|
-
stampCleanMainSyncStatus(repoRoot: string): void
|
|
108
|
+
stampCleanMainSyncStatus(repoRoot: string): Promise<void>;
|
|
109
109
|
/**
|
|
110
110
|
* Refresh `origin/main` WITHOUT writing `.git/FETCH_HEAD`.
|
|
111
111
|
*
|
|
@@ -123,7 +123,7 @@ export declare class MainSyncStatusService {
|
|
|
123
123
|
*/
|
|
124
124
|
private fetchOriginMain;
|
|
125
125
|
private isUnknownGitOption;
|
|
126
|
-
private
|
|
126
|
+
private git;
|
|
127
127
|
private gitBranch;
|
|
128
128
|
private localMainHash;
|
|
129
129
|
private commitDistance;
|
|
@@ -149,7 +149,4 @@ export declare function readMainSyncStatus(repoRoot: string, branch: string): Ma
|
|
|
149
149
|
export declare function readMainSyncStatusFile(repoRoot: string): MainSyncStatusFile | null;
|
|
150
150
|
export declare function writeMainSyncStatus(repoRoot: string, status: MainSyncStatus): void;
|
|
151
151
|
export declare function writeMainSyncStatusFile(repoRoot: string, file: MainSyncStatusFile): void;
|
|
152
|
-
export declare function computeMainSyncStatus(repoRoot: string): MainSyncStatus;
|
|
153
|
-
export declare function computeAllMainSyncStatuses(repoRoot: string): MainSyncStatusFile;
|
|
154
152
|
export declare function squashRecoverySteps(currentBranch: string): string[];
|
|
155
|
-
export declare function stampCleanMainSyncStatus(repoRoot: string): void;
|
package/src/main-sync-status.js
CHANGED
|
@@ -6,12 +6,8 @@ exports.readMainSyncStatus = readMainSyncStatus;
|
|
|
6
6
|
exports.readMainSyncStatusFile = readMainSyncStatusFile;
|
|
7
7
|
exports.writeMainSyncStatus = writeMainSyncStatus;
|
|
8
8
|
exports.writeMainSyncStatusFile = writeMainSyncStatusFile;
|
|
9
|
-
exports.computeMainSyncStatus = computeMainSyncStatus;
|
|
10
|
-
exports.computeAllMainSyncStatuses = computeAllMainSyncStatuses;
|
|
11
9
|
exports.squashRecoverySteps = squashRecoverySteps;
|
|
12
|
-
exports.stampCleanMainSyncStatus = stampCleanMainSyncStatus;
|
|
13
10
|
const tslib_1 = require("tslib");
|
|
14
|
-
const child_process_1 = require("child_process");
|
|
15
11
|
const inversify_1 = require("inversify");
|
|
16
12
|
const tooling_common_1 = require("@webpieces/tooling-common");
|
|
17
13
|
const main_sync_file_1 = require("./main-sync-file");
|
|
@@ -32,6 +28,15 @@ const command_timeouts_1 = require("./command-timeouts");
|
|
|
32
28
|
// and reclaims the lock (see MainSyncLockService).
|
|
33
29
|
exports.DEFAULT_HANG_TIMEOUT_MINUTES = 5;
|
|
34
30
|
const MAIN_SYNC_STATUS_FILE = 'main-sync-status.json';
|
|
31
|
+
// Internal: one bounded command's verdict and trimmed stdout. Data only.
|
|
32
|
+
class CapturedOutput {
|
|
33
|
+
ok;
|
|
34
|
+
out;
|
|
35
|
+
constructor(ok, out) {
|
|
36
|
+
this.ok = ok;
|
|
37
|
+
this.out = out;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
35
40
|
/**
|
|
36
41
|
* Reads/writes the main-sync cache and computes the slow "is my branch healthy vs origin/main?"
|
|
37
42
|
* status. `@injectable(bindingScopeValues.Singleton)` so it's injectable and drawn in consumers’ DI designs.
|
|
@@ -41,13 +46,13 @@ let MainSyncStatusService = class MainSyncStatusService {
|
|
|
41
46
|
atomicFile;
|
|
42
47
|
store;
|
|
43
48
|
worktrees;
|
|
44
|
-
|
|
45
|
-
constructor(dotDir = tooling_common_2.dotWebpieces, atomicFile = new tooling_common_1.AtomicFile(), store = new main_sync_file_1.MainSyncFileStore(atomicFile), worktrees = new worktrees_1.WorktreeService(),
|
|
49
|
+
commands;
|
|
50
|
+
constructor(dotDir = tooling_common_2.dotWebpieces, atomicFile = new tooling_common_1.AtomicFile(), store = new main_sync_file_1.MainSyncFileStore(atomicFile), worktrees = new worktrees_1.WorktreeService(), commands = new command_timeouts_1.CommandTimeouts()) {
|
|
46
51
|
this.dotDir = dotDir;
|
|
47
52
|
this.atomicFile = atomicFile;
|
|
48
53
|
this.store = store;
|
|
49
54
|
this.worktrees = worktrees;
|
|
50
|
-
this.
|
|
55
|
+
this.commands = commands;
|
|
51
56
|
}
|
|
52
57
|
/**
|
|
53
58
|
* SHARED scope: `<primary>/.webpieces/main-sync-status.json`, one per repo.
|
|
@@ -117,10 +122,10 @@ let MainSyncStatusService = class MainSyncStatusService {
|
|
|
117
122
|
* own `gh` query, so it is the right entry point for a single-worktree caller and for tests.
|
|
118
123
|
* The refresher uses computeAllMainSyncStatuses instead — see there for why.
|
|
119
124
|
*/
|
|
120
|
-
computeMainSyncStatus(repoRoot) {
|
|
125
|
+
async computeMainSyncStatus(repoRoot) {
|
|
121
126
|
// Best-effort network refresh; offline just means we evaluate against the last-fetched ref.
|
|
122
|
-
this.fetchOriginMain(repoRoot);
|
|
123
|
-
return this.computeBranchStatus(repoRoot, this.gitBranch(repoRoot), this.loadPullRequestIndex(repoRoot));
|
|
127
|
+
await this.fetchOriginMain(repoRoot);
|
|
128
|
+
return this.computeBranchStatus(repoRoot, this.gitBranch(repoRoot), await this.loadPullRequestIndex(repoRoot));
|
|
124
129
|
}
|
|
125
130
|
/**
|
|
126
131
|
* The SLOW path for EVERY branch this repo has checked out — what the detached refresher runs.
|
|
@@ -137,21 +142,21 @@ let MainSyncStatusService = class MainSyncStatusService {
|
|
|
137
142
|
* The map is rebuilt from the LIVE worktree set every refresh, so a deleted branch or removed
|
|
138
143
|
* worktree simply stops appearing. No pruning pass, no unbounded growth.
|
|
139
144
|
*/
|
|
140
|
-
computeAllMainSyncStatuses(repoRoot) {
|
|
141
|
-
this.fetchOriginMain(repoRoot);
|
|
142
|
-
const index = this.loadPullRequestIndex(repoRoot);
|
|
145
|
+
async computeAllMainSyncStatuses(repoRoot) {
|
|
146
|
+
await this.fetchOriginMain(repoRoot);
|
|
147
|
+
const index = await this.loadPullRequestIndex(repoRoot);
|
|
143
148
|
const branches = {};
|
|
144
149
|
for (const tree of this.worktrees.listWorktrees(repoRoot)) {
|
|
145
150
|
// Detached HEAD (and a bare worktree) has no branch to key an entry on. Skipping it must
|
|
146
151
|
// not stop the others being recorded, which is why this is a `continue`, not a bail-out.
|
|
147
152
|
if (tree.branch === '' || this.hasBranch(branches, tree.branch))
|
|
148
153
|
continue;
|
|
149
|
-
branches[tree.branch] = this.computeBranchStatus(tree.path, tree.branch, index);
|
|
154
|
+
branches[tree.branch] = await this.computeBranchStatus(tree.path, tree.branch, index);
|
|
150
155
|
}
|
|
151
156
|
// `main` always present, even with no worktree on it: read-stale-guard's state A looks itself
|
|
152
157
|
// up under 'main', and an absent entry there would silently disarm the stale-main block.
|
|
153
158
|
if (!this.hasBranch(branches, 'main'))
|
|
154
|
-
branches['main'] = this.mainOnlyStatus(repoRoot);
|
|
159
|
+
branches['main'] = await this.mainOnlyStatus(repoRoot);
|
|
155
160
|
return new main_sync_file_1.MainSyncStatusFile(main_sync_file_1.MAIN_SYNC_STATUS_VERSION, new Date().toISOString(), branches);
|
|
156
161
|
}
|
|
157
162
|
hasBranch(branches, branch) {
|
|
@@ -163,8 +168,8 @@ let MainSyncStatusService = class MainSyncStatusService {
|
|
|
163
168
|
* `gh`: both are done once per refresh by the caller.
|
|
164
169
|
*/
|
|
165
170
|
// webpieces-disable max-lines-new-methods -- one cohesive slow-path computation
|
|
166
|
-
computeBranchStatus(repoRoot, branch, index) {
|
|
167
|
-
return this.stampForge(this.computeBranchStatusInner(repoRoot, branch, index), index);
|
|
171
|
+
async computeBranchStatus(repoRoot, branch, index) {
|
|
172
|
+
return this.stampForge(await this.computeBranchStatusInner(repoRoot, branch, index), index);
|
|
168
173
|
}
|
|
169
174
|
// Every return path of the computation below has to carry the forge flag, and threading it through
|
|
170
175
|
// four constructions is how one of them would end up missing it. One wrapper, one assignment.
|
|
@@ -173,12 +178,12 @@ let MainSyncStatusService = class MainSyncStatusService {
|
|
|
173
178
|
return status;
|
|
174
179
|
}
|
|
175
180
|
// webpieces-disable max-lines-new-methods -- one cohesive slow-path computation
|
|
176
|
-
computeBranchStatusInner(repoRoot, branch, index) {
|
|
181
|
+
async computeBranchStatusInner(repoRoot, branch, index) {
|
|
177
182
|
const mergedPr = index.mergedFor(branch);
|
|
178
183
|
const openPr = index.openFor(branch);
|
|
179
|
-
const head = this.
|
|
180
|
-
const originMain = this.
|
|
181
|
-
const localMain = this.localMainHash(repoRoot);
|
|
184
|
+
const head = await this.git(repoRoot, ['rev-parse', 'HEAD']);
|
|
185
|
+
const originMain = await this.git(repoRoot, ['rev-parse', 'origin/main']);
|
|
186
|
+
const localMain = await this.localMainHash(repoRoot);
|
|
182
187
|
const featureHead = head.ok ? head.out : '';
|
|
183
188
|
if (!head.ok || !originMain.ok) {
|
|
184
189
|
const status = this.benignStatus(branch, featureHead);
|
|
@@ -186,24 +191,24 @@ let MainSyncStatusService = class MainSyncStatusService {
|
|
|
186
191
|
status.mergedPr = mergedPr;
|
|
187
192
|
status.openPr = openPr;
|
|
188
193
|
status.localMain = localMain;
|
|
189
|
-
status.commitsBehind = this.commitDistance(repoRoot, localMain, originMain.ok ? originMain.out : '');
|
|
194
|
+
status.commitsBehind = await this.commitDistance(repoRoot, localMain, originMain.ok ? originMain.out : '');
|
|
190
195
|
return status;
|
|
191
196
|
}
|
|
192
|
-
const forkPoint = this.
|
|
197
|
+
const forkPoint = await this.git(repoRoot, ['merge-base', 'origin/main', 'HEAD']);
|
|
193
198
|
if (!forkPoint.ok || forkPoint.out === '') {
|
|
194
199
|
const noFork = new main_sync_file_1.MainSyncStatus(branch, mergedPr !== '', mergedPr, false, null, originMain.out, featureHead, false, [], new Date().toISOString());
|
|
195
200
|
noFork.openPr = openPr;
|
|
196
201
|
noFork.localMain = localMain;
|
|
197
|
-
noFork.commitsBehind = this.commitDistance(repoRoot, localMain, originMain.out);
|
|
202
|
+
noFork.commitsBehind = await this.commitDistance(repoRoot, localMain, originMain.out);
|
|
198
203
|
return noFork;
|
|
199
204
|
}
|
|
200
|
-
const featureFiles = new Set(this.featureChangedFiles(repoRoot, forkPoint.out));
|
|
201
|
-
const mainFiles = this.changedFiles(repoRoot, forkPoint.out, 'origin/main');
|
|
205
|
+
const featureFiles = new Set(await this.featureChangedFiles(repoRoot, forkPoint.out));
|
|
206
|
+
const mainFiles = await this.changedFiles(repoRoot, forkPoint.out, 'origin/main');
|
|
202
207
|
const conflictFiles = mainFiles.filter((file) => featureFiles.has(file));
|
|
203
208
|
const status = new main_sync_file_1.MainSyncStatus(branch, mergedPr !== '', mergedPr, true, forkPoint.out, originMain.out, featureHead, conflictFiles.length > 0, conflictFiles, new Date().toISOString());
|
|
204
209
|
status.openPr = openPr;
|
|
205
210
|
status.localMain = localMain;
|
|
206
|
-
status.commitsBehind = this.commitDistance(repoRoot, localMain, originMain.out);
|
|
211
|
+
status.commitsBehind = await this.commitDistance(repoRoot, localMain, originMain.out);
|
|
207
212
|
return status;
|
|
208
213
|
}
|
|
209
214
|
// The recovery steps when there is no fork point with origin/main (a bad merge of main into branch).
|
|
@@ -229,16 +234,16 @@ let MainSyncStatusService = class MainSyncStatusService {
|
|
|
229
234
|
* goes through writeMainSyncStatus, which MERGES its entry into the existing map; writing a whole
|
|
230
235
|
* document from here would wipe every other worktree's entry.
|
|
231
236
|
*/
|
|
232
|
-
stampCleanMainSyncStatus(repoRoot) {
|
|
237
|
+
async stampCleanMainSyncStatus(repoRoot) {
|
|
233
238
|
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
234
239
|
try {
|
|
235
240
|
const branch = this.gitBranch(repoRoot);
|
|
236
|
-
const originMain = this.
|
|
237
|
-
const featureHead = this.
|
|
241
|
+
const originMain = await this.git(repoRoot, ['rev-parse', 'origin/main']);
|
|
242
|
+
const featureHead = await this.git(repoRoot, ['rev-parse', 'HEAD']);
|
|
238
243
|
if (!originMain.ok || !featureHead.ok)
|
|
239
244
|
return;
|
|
240
245
|
const status = new main_sync_file_1.MainSyncStatus(branch, false, '', true, originMain.out, originMain.out, featureHead.out, false, [], new Date().toISOString());
|
|
241
|
-
status.localMain = this.localMainHash(repoRoot);
|
|
246
|
+
status.localMain = await this.localMainHash(repoRoot);
|
|
242
247
|
status.commitsBehind = 0;
|
|
243
248
|
this.writeMainSyncStatus(repoRoot, status);
|
|
244
249
|
}
|
|
@@ -262,27 +267,23 @@ let MainSyncStatusService = class MainSyncStatusService {
|
|
|
262
267
|
* Older git rejects the flag; only then do we retry the plain form, accepting the old behaviour
|
|
263
268
|
* rather than losing the refresh entirely on a pre-2020 git.
|
|
264
269
|
*/
|
|
265
|
-
fetchOriginMain(repoRoot) {
|
|
266
|
-
const safe =
|
|
267
|
-
|
|
268
|
-
});
|
|
269
|
-
if (safe.status === 0)
|
|
270
|
+
async fetchOriginMain(repoRoot) {
|
|
271
|
+
const safe = await this.commands.networkGit(['fetch', '--no-write-fetch-head', 'origin', 'main'], repoRoot);
|
|
272
|
+
if (safe.ok)
|
|
270
273
|
return;
|
|
271
274
|
if (!this.isUnknownGitOption(safe.stderr))
|
|
272
275
|
return; // a real failure (offline, auth) — not our business
|
|
273
|
-
|
|
276
|
+
await this.commands.networkGit(['fetch', 'origin', 'main'], repoRoot);
|
|
274
277
|
}
|
|
275
278
|
// Did git reject the flag itself (too old), as opposed to failing the network refresh?
|
|
276
279
|
isUnknownGitOption(stderr) {
|
|
277
|
-
const text =
|
|
280
|
+
const text = stderr.toLowerCase();
|
|
278
281
|
return text.includes('unknown option') || text.includes('unknown switch') || text.includes('unrecognized option');
|
|
279
282
|
}
|
|
280
|
-
//
|
|
281
|
-
|
|
282
|
-
const result =
|
|
283
|
-
|
|
284
|
-
return { ok: false, out: '' };
|
|
285
|
-
return { ok: true, out: result.stdout.trim() };
|
|
283
|
+
// Local git, bounded (CommandTimeouts), stdout trimmed; ok=false on a non-zero exit, a kill or a failed start.
|
|
284
|
+
async git(repoRoot, args) {
|
|
285
|
+
const result = await this.commands.localGit(args, repoRoot);
|
|
286
|
+
return new CapturedOutput(result.ok, result.ok ? result.stdout.trim() : '');
|
|
286
287
|
}
|
|
287
288
|
// The actual checked-out branch in repoRoot — cwd-correct so the cache's `branch` label matches.
|
|
288
289
|
gitBranch(repoRoot) {
|
|
@@ -290,31 +291,31 @@ let MainSyncStatusService = class MainSyncStatusService {
|
|
|
290
291
|
}
|
|
291
292
|
// The LOCAL main hash. `refs/heads/main` (not the bare name) so it can never resolve to a remote
|
|
292
293
|
// ref or a tag. '' when main does not exist locally — which the guard treats as fail-open.
|
|
293
|
-
localMainHash(repoRoot) {
|
|
294
|
-
const result = this.
|
|
294
|
+
async localMainHash(repoRoot) {
|
|
295
|
+
const result = await this.git(repoRoot, ['rev-parse', 'refs/heads/main']);
|
|
295
296
|
return result.ok ? result.out : '';
|
|
296
297
|
}
|
|
297
298
|
// Slow-path only: the detached refresher calls this after fetching. Guards never count commits.
|
|
298
|
-
commitDistance(repoRoot, localMain, originMain) {
|
|
299
|
+
async commitDistance(repoRoot, localMain, originMain) {
|
|
299
300
|
if (localMain === '' || originMain === '')
|
|
300
301
|
return null;
|
|
301
|
-
const result = this.
|
|
302
|
+
const result = await this.git(repoRoot, ['rev-list', '--count', `${localMain}..${originMain}`]);
|
|
302
303
|
if (!result.ok || !/^\d+$/.test(result.out))
|
|
303
304
|
return null;
|
|
304
305
|
return Number(result.out);
|
|
305
306
|
}
|
|
306
|
-
changedFiles(repoRoot, base, head) {
|
|
307
|
-
const result = this.
|
|
307
|
+
async changedFiles(repoRoot, base, head) {
|
|
308
|
+
const result = await this.git(repoRoot, ['diff', '--name-only', base, head]);
|
|
308
309
|
if (!result.ok || result.out === '')
|
|
309
310
|
return [];
|
|
310
311
|
return result.out.split('\n').map((line) => line.trim()).filter((line) => line.length > 0);
|
|
311
312
|
}
|
|
312
313
|
// Every file this feature branch has touched since the fork point — committed AND still in the
|
|
313
314
|
// working tree (staged / unstaged / untracked), so a conflict is visible WHILE editing.
|
|
314
|
-
featureChangedFiles(repoRoot, forkPoint) {
|
|
315
|
+
async featureChangedFiles(repoRoot, forkPoint) {
|
|
315
316
|
const out = new Set();
|
|
316
|
-
const add = (args) => {
|
|
317
|
-
const r = this.
|
|
317
|
+
const add = async (args) => {
|
|
318
|
+
const r = await this.git(repoRoot, args);
|
|
318
319
|
if (!r.ok || r.out === '')
|
|
319
320
|
return;
|
|
320
321
|
for (const line of r.out.split('\n')) {
|
|
@@ -323,10 +324,10 @@ let MainSyncStatusService = class MainSyncStatusService {
|
|
|
323
324
|
out.add(f);
|
|
324
325
|
}
|
|
325
326
|
};
|
|
326
|
-
add(['diff', '--name-only', forkPoint, 'HEAD']); // committed since the fork point
|
|
327
|
-
add(['diff', '--name-only', 'HEAD']); // unstaged working-tree edits
|
|
328
|
-
add(['diff', '--name-only', '--cached', 'HEAD']); // staged edits
|
|
329
|
-
add(['ls-files', '--others', '--exclude-standard']); // untracked new files (respects .gitignore)
|
|
327
|
+
await add(['diff', '--name-only', forkPoint, 'HEAD']); // committed since the fork point
|
|
328
|
+
await add(['diff', '--name-only', 'HEAD']); // unstaged working-tree edits
|
|
329
|
+
await add(['diff', '--name-only', '--cached', 'HEAD']); // staged edits
|
|
330
|
+
await add(['ls-files', '--others', '--exclude-standard']); // untracked new files (respects .gitignore)
|
|
330
331
|
return [...out];
|
|
331
332
|
}
|
|
332
333
|
/**
|
|
@@ -335,10 +336,11 @@ let MainSyncStatusService = class MainSyncStatusService {
|
|
|
335
336
|
* falls out of the index and reads as "no PR", which is the fail-OPEN direction (no merged PR =
|
|
336
337
|
* no block), the same way a missing `gh` or an offline machine already degraded.
|
|
337
338
|
*/
|
|
338
|
-
loadPullRequestIndex(repoRoot) {
|
|
339
|
-
const
|
|
339
|
+
async loadPullRequestIndex(repoRoot) {
|
|
340
|
+
const gh = await this.commands.gh([
|
|
340
341
|
'pr', 'list', '--state', 'all', '--json', 'number,headRefName,state', '--limit', '200',
|
|
341
|
-
]);
|
|
342
|
+
], repoRoot);
|
|
343
|
+
const result = new CapturedOutput(gh.ok, gh.ok ? gh.stdout.trim() : '');
|
|
342
344
|
// `gh` could not be run, or exited non-zero (missing, unauthenticated, rate-limited, offline).
|
|
343
345
|
// An EMPTY index is the same fail-open answer as before — but it is now labelled UNREACHABLE, so
|
|
344
346
|
// the guards can report an abstention rather than an approval. Empty STDOUT on a zero exit is a
|
|
@@ -354,12 +356,12 @@ let MainSyncStatusService = class MainSyncStatusService {
|
|
|
354
356
|
* read-stale-guard / stale-main-bash-guard actually compare (originMain vs the live tree's
|
|
355
357
|
* ancestry) — never merged, never conflicting, so it can only ever produce the stale-main verdict.
|
|
356
358
|
*/
|
|
357
|
-
mainOnlyStatus(repoRoot) {
|
|
358
|
-
const originMain = this.
|
|
359
|
-
const localMain = this.localMainHash(repoRoot);
|
|
359
|
+
async mainOnlyStatus(repoRoot) {
|
|
360
|
+
const originMain = await this.git(repoRoot, ['rev-parse', 'origin/main']);
|
|
361
|
+
const localMain = await this.localMainHash(repoRoot);
|
|
360
362
|
const status = new main_sync_file_1.MainSyncStatus('main', false, '', true, localMain === '' ? null : localMain, originMain.ok ? originMain.out : '', localMain, false, [], new Date().toISOString());
|
|
361
363
|
status.localMain = localMain;
|
|
362
|
-
status.commitsBehind = this.commitDistance(repoRoot, localMain, originMain.ok ? originMain.out : '');
|
|
364
|
+
status.commitsBehind = await this.commitDistance(repoRoot, localMain, originMain.ok ? originMain.out : '');
|
|
363
365
|
return status;
|
|
364
366
|
}
|
|
365
367
|
// A benign status that never blocks — used when origin/main can't be resolved.
|
|
@@ -389,11 +391,5 @@ function writeMainSyncStatus(repoRoot, status) { mainSyncSvc.writeMainSyncStatus
|
|
|
389
391
|
// webpieces-disable no-function-outside-class -- temporary back-compat delegator to MainSyncStatusService; removed once consumers inject it
|
|
390
392
|
function writeMainSyncStatusFile(repoRoot, file) { mainSyncSvc.writeMainSyncStatusFile(repoRoot, file); }
|
|
391
393
|
// webpieces-disable no-function-outside-class -- temporary back-compat delegator to MainSyncStatusService; removed once consumers inject it
|
|
392
|
-
function computeMainSyncStatus(repoRoot) { return mainSyncSvc.computeMainSyncStatus(repoRoot); }
|
|
393
|
-
// webpieces-disable no-function-outside-class -- temporary back-compat delegator to MainSyncStatusService; removed once consumers inject it
|
|
394
|
-
function computeAllMainSyncStatuses(repoRoot) { return mainSyncSvc.computeAllMainSyncStatuses(repoRoot); }
|
|
395
|
-
// webpieces-disable no-function-outside-class -- temporary back-compat delegator to MainSyncStatusService; removed once consumers inject it
|
|
396
394
|
function squashRecoverySteps(currentBranch) { return mainSyncSvc.squashRecoverySteps(currentBranch); }
|
|
397
|
-
// webpieces-disable no-function-outside-class -- temporary back-compat delegator to MainSyncStatusService; removed once consumers inject it
|
|
398
|
-
function stampCleanMainSyncStatus(repoRoot) { mainSyncSvc.stampCleanMainSyncStatus(repoRoot); }
|
|
399
395
|
//# sourceMappingURL=main-sync-status.js.map
|