@feigi/fleet-ctl 3.82.5 → 3.82.6
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/scripts/derive-testcmd.sh +25 -11
- package/scripts/fleet-dir.mjs +21 -17
- package/scripts/git-env.mjs +29 -16
- package/scripts/ledger.mjs +9 -6
package/package.json
CHANGED
|
@@ -21,8 +21,10 @@
|
|
|
21
21
|
# directory git-env.mjs's resolveWorkspace() names for the repository: the
|
|
22
22
|
# common git dir's parent when the common dir is named `.git` or that parent's
|
|
23
23
|
# own `.git` resolves to it, otherwise the common git dir itself (a bare
|
|
24
|
-
# repository, a submodule).
|
|
25
|
-
#
|
|
24
|
+
# repository, a submodule). Only an absent `<parent>/.git` is "not this
|
|
25
|
+
# parent"; one that exists and that git refuses fails the probe, so a linked
|
|
26
|
+
# worktree never re-keys the cache to the common dir. The ledger and the
|
|
27
|
+
# heartbeat live in that same place — so a claimed worktree and a
|
|
26
28
|
# review worktree read the one cache the main checkout holds rather than a
|
|
27
29
|
# private copy each. A single JSON object:
|
|
28
30
|
#
|
|
@@ -179,9 +181,18 @@ common=$(git -C "$repo" rev-parse --path-format=absolute --git-common-dir 2>&1)
|
|
|
179
181
|
# shell rather than asked of node: the interpreter probe below must stay the
|
|
180
182
|
# first node call, since its refusals are pinned in that order. A common dir
|
|
181
183
|
# named `.git` belongs to its parent checkout. Otherwise the parent's `.git`,
|
|
182
|
-
# a symlink or a
|
|
183
|
-
#
|
|
184
|
-
#
|
|
184
|
+
# a symlink, a gitfile or a directory, that git resolves to the common dir
|
|
185
|
+
# makes the parent the workspace. Only an absent `<parent>/.git` is "not this
|
|
186
|
+
# parent": one that exists and that git refuses (a dangling or invalid gitfile,
|
|
187
|
+
# an unreadable one) fails the probe, never re-keys the cache to the common dir.
|
|
188
|
+
# Otherwise the common dir is the workspace itself (a bare repository, a
|
|
189
|
+
# submodule's git dir). git prints the realpath for a gitfile but the path as
|
|
190
|
+
# spelled for a `.git` directory, so its answer is canonicalised before the
|
|
191
|
+
# compare; `$target` stays absolute, since a relative operand is answered
|
|
192
|
+
# relative to the cwd. Only git's stdout is the answer: anything it writes to
|
|
193
|
+
# stderr on a success would otherwise land inside the path and fail the
|
|
194
|
+
# compare, re-keying the cache in silence. A refusal asks once more for the
|
|
195
|
+
# reason, its stderr alone.
|
|
185
196
|
real=$(cd -P "$common" 2>/dev/null && pwd -P) || real=$common
|
|
186
197
|
parent=${common%/*}
|
|
187
198
|
case ${real##*/} in
|
|
@@ -189,13 +200,16 @@ case ${real##*/} in
|
|
|
189
200
|
*)
|
|
190
201
|
ws=$common
|
|
191
202
|
target=${real%/*}/.git
|
|
192
|
-
if [ -
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
203
|
+
if [ -e "$target" ] || [ -L "$target" ]; then
|
|
204
|
+
named=$(git rev-parse --resolve-git-dir "$target" 2>/dev/null) || {
|
|
205
|
+
gitrc=$?
|
|
206
|
+
why=$(git rev-parse --resolve-git-dir "$target" 2>&1 >/dev/null) || :
|
|
207
|
+
case $gitrc in 128) die "cannot resolve the workspace of $repo: $target — $why" ;; esac
|
|
208
|
+
unrunnable "git did not resolve $target (exit $gitrc), so the Recipe cache was not read${why:+ — $why}"
|
|
209
|
+
}
|
|
210
|
+
named=$(cd -P "$named" 2>/dev/null && pwd -P) || named=
|
|
211
|
+
if [ "$named" = "$real" ]; then ws=$parent; fi
|
|
196
212
|
fi
|
|
197
|
-
named=$(cd -P "$target" 2>/dev/null && pwd -P) || named=
|
|
198
|
-
if [ "$named" = "$real" ]; then ws=$parent; fi
|
|
199
213
|
;;
|
|
200
214
|
esac
|
|
201
215
|
cache="$ws/.fleet/recipe.json"
|
package/scripts/fleet-dir.mjs
CHANGED
|
@@ -10,13 +10,16 @@
|
|
|
10
10
|
// every worktree of one repository shares, and git-env.mjs's resolveWorkspace()
|
|
11
11
|
// turns that answer into the workspace it names: the common git dir's parent
|
|
12
12
|
// when the common dir is named `.git` or that parent's own `.git` resolves to
|
|
13
|
-
// it, otherwise the common git dir itself.
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
13
|
+
// it, otherwise the common git dir itself. Only an absent `<parent>/.git` is
|
|
14
|
+
// "not this parent"; one that exists and that git refuses fails the probe, so
|
|
15
|
+
// a linked worktree never re-keys to the common dir. This module is the spawn
|
|
16
|
+
// around that function — the scrubbed env, the bound, the join onto `.fleet/`
|
|
17
|
+
// — so a new `.fleet/`-rooted file is one `fleetFile("name")` call rather
|
|
18
|
+
// than another hand-spelled probe. The bound is per git child, not per call:
|
|
19
|
+
// when the common git dir is not named `.git` and the parent's `.git`
|
|
20
|
+
// exists, resolveWorkspace() runs a second `rev-parse` after this module's
|
|
21
|
+
// own, with a bound of its own, so one call's worst case approaches twice
|
|
22
|
+
// `timeoutMs`.
|
|
20
23
|
//
|
|
21
24
|
// One error policy: anything that leaves no workspace THROWS
|
|
22
25
|
// FleetDirUnresolvable, never a null or an empty path. What a caller does
|
|
@@ -115,15 +118,16 @@ function workspaceOf(cwd, timeoutMs, canonicalise) {
|
|
|
115
118
|
* that spelling without changing which file it names.
|
|
116
119
|
*
|
|
117
120
|
* `timeoutMs` (default git-env.mjs's DEFAULT_GIT_TIMEOUT_MS) bounds each git
|
|
118
|
-
* child, not the call. When the common git dir is not named `.git`
|
|
119
|
-
* `.
|
|
120
|
-
*
|
|
121
|
-
* with its own full `timeoutMs`,
|
|
122
|
-
* `timeoutMs`.
|
|
121
|
+
* child, not the call. When the common git dir is not named `.git` and the
|
|
122
|
+
* `.git` in its parent exists (the `.bare` layout, or any other git dir that
|
|
123
|
+
* shares a parent with a `.git`), resolveWorkspace()
|
|
124
|
+
* runs a second probe after `--git-common-dir`, with its own full `timeoutMs`,
|
|
125
|
+
* so one call's worst case approaches twice `timeoutMs`.
|
|
123
126
|
*
|
|
124
127
|
* Throws FleetDirUnresolvable for every probe that leaves no workspace — no
|
|
125
|
-
* repository, an unreadable `.git`, git
|
|
126
|
-
* `timeoutMs` — with git's own reason in the
|
|
128
|
+
* repository, an unreadable `.git`, a parent's `.git` that git refuses, git
|
|
129
|
+
* missing, a probe killed at `timeoutMs` — with git's own reason in the
|
|
130
|
+
* message.
|
|
127
131
|
*/
|
|
128
132
|
export function fleetFile(name, { cwd = process.cwd(), timeoutMs, canonicalise = false } = {}) {
|
|
129
133
|
const { dir } = workspaceOf(cwd, timeoutMs, canonicalise);
|
|
@@ -146,9 +150,9 @@ export function fleetFile(name, { cwd = process.cwd(), timeoutMs, canonicalise =
|
|
|
146
150
|
*
|
|
147
151
|
* `home` exists for tests; it defaults to the user's home directory.
|
|
148
152
|
* `timeoutMs` bounds each git child exactly as in fleetFile(): two may run in
|
|
149
|
-
* sequence when the common git dir is not named `.git
|
|
150
|
-
* case approaches twice `timeoutMs`. Throws
|
|
151
|
-
* fleetFile() does.
|
|
153
|
+
* sequence when the common git dir is not named `.git` and its parent's `.git`
|
|
154
|
+
* exists, so one call's worst case approaches twice `timeoutMs`. Throws
|
|
155
|
+
* FleetDirUnresolvable exactly as fleetFile() does.
|
|
152
156
|
*/
|
|
153
157
|
export function scratchRoot({ cwd = process.cwd(), timeoutMs, home = homedir() } = {}) {
|
|
154
158
|
const { dir, gitDir } = workspaceOf(cwd, timeoutMs, true);
|
package/scripts/git-env.mjs
CHANGED
|
@@ -63,8 +63,10 @@ export function gitEnv(overrides = {}, base = process.env) {
|
|
|
63
63
|
// `.git` or when that parent's own `.git` resolves to it (the `.bare`
|
|
64
64
|
// layout's gitfile); otherwise it is the common dir itself — a bare
|
|
65
65
|
// repository's, a submodule's or a `--separate-git-dir` clone's git dir — so
|
|
66
|
-
// two such dirs sharing a parent do not collapse onto one workspace.
|
|
67
|
-
//
|
|
66
|
+
// two such dirs sharing a parent do not collapse onto one workspace. Only an
|
|
67
|
+
// absent `<parent>/.git` is "not this parent"; one that git refuses fails the
|
|
68
|
+
// probe, so a linked worktree never re-keys to the common dir. The rule's
|
|
69
|
+
// full statement sits beside `resolveWorkspace()` below. The main
|
|
68
70
|
// checkout's work tree is a separate question with its own answer,
|
|
69
71
|
// `mainCheckoutDirFromGitCommonDir()`: a `.git` symlinked to a git dir
|
|
70
72
|
// elsewhere moves the workspace out of the checkout, never the work tree.
|
|
@@ -79,7 +81,7 @@ export function gitEnv(overrides = {}, base = process.env) {
|
|
|
79
81
|
// composes the rest around it.
|
|
80
82
|
|
|
81
83
|
import { spawnSync } from "node:child_process";
|
|
82
|
-
import { realpathSync } from "node:fs";
|
|
84
|
+
import { lstatSync, realpathSync } from "node:fs";
|
|
83
85
|
import { basename, dirname, join, resolve } from "node:path";
|
|
84
86
|
|
|
85
87
|
// A `rev-parse` answers in milliseconds; this bound exists so a git that never
|
|
@@ -87,8 +89,10 @@ import { basename, dirname, join, resolve } from "node:path";
|
|
|
87
89
|
export const DEFAULT_GIT_TIMEOUT_MS = 10_000;
|
|
88
90
|
|
|
89
91
|
// Thrown when the one probe the rule needs beyond `--git-common-dir` could not
|
|
90
|
-
// run to an answer: git missing,
|
|
91
|
-
// that
|
|
92
|
+
// run to an answer: git missing, killed at its bound, or a `<parent>/.git`
|
|
93
|
+
// that exists and that git refuses. Only an absent `<parent>/.git` is "no";
|
|
94
|
+
// one git refuses fails the probe, so a linked worktree never re-keys to the
|
|
95
|
+
// common dir.
|
|
92
96
|
export class WorkspaceProbeFailed extends Error {
|
|
93
97
|
constructor(message) {
|
|
94
98
|
super(message);
|
|
@@ -107,25 +111,32 @@ function canonicalPath(path) {
|
|
|
107
111
|
}
|
|
108
112
|
|
|
109
113
|
// Why a probe that did not answer did not: the spawn error, the signal that
|
|
110
|
-
// killed it,
|
|
111
|
-
// probe with no exit status, so no exit code is ever left to name.
|
|
114
|
+
// killed it, its stderr, or its exit status when stderr says nothing.
|
|
112
115
|
function probeWhy(r) {
|
|
113
116
|
if (r.error && r.error.message) return r.error.message;
|
|
114
117
|
if (r.signal) return `killed by ${r.signal}`;
|
|
115
|
-
return String(r.stderr ?? "").trim() ||
|
|
118
|
+
return String(r.stderr ?? "").trim() || `git exited ${r.status}`;
|
|
116
119
|
}
|
|
117
120
|
|
|
118
121
|
// Does `<parent>/.git` name `gitDir` (a realpath)? `--resolve-git-dir` follows
|
|
119
|
-
// a gitfile, so the `.bare` layout's `.git` file answers with `.bare`.
|
|
122
|
+
// a gitfile, so the `.bare` layout's `.git` file answers with `.bare`. Only an
|
|
123
|
+
// absent `<parent>/.git` is no, without a spawn; one that exists and that git
|
|
124
|
+
// refuses (a dangling or invalid gitfile, an unreadable one) fails the probe.
|
|
125
|
+
// Git's stderr is only quoted, never read: its text depends on the locale.
|
|
120
126
|
function parentGitDirNames(parent, gitDir, cwd, timeoutMs) {
|
|
121
127
|
const candidate = join(parent, ".git");
|
|
128
|
+
try {
|
|
129
|
+
lstatSync(candidate);
|
|
130
|
+
} catch (e) {
|
|
131
|
+
if (e.code === "ENOENT") return false;
|
|
132
|
+
throw new WorkspaceProbeFailed(`could not resolve ${candidate}: ${e.message}`);
|
|
133
|
+
}
|
|
122
134
|
const r = spawnSync("git", ["rev-parse", "--resolve-git-dir", candidate], {
|
|
123
135
|
cwd, encoding: "utf8", timeout: timeoutMs ?? DEFAULT_GIT_TIMEOUT_MS, env: gitEnv(),
|
|
124
136
|
});
|
|
125
|
-
if (r.error || r.status
|
|
137
|
+
if (r.error || r.status !== 0) {
|
|
126
138
|
throw new WorkspaceProbeFailed(`could not resolve ${candidate}: ${probeWhy(r)}`);
|
|
127
139
|
}
|
|
128
|
-
if (r.status !== 0) return false;
|
|
129
140
|
return realpathOr(resolve(parent, r.stdout.trim())) === gitDir;
|
|
130
141
|
}
|
|
131
142
|
|
|
@@ -138,8 +149,9 @@ function parentGitDirNames(parent, gitDir, cwd, timeoutMs) {
|
|
|
138
149
|
* The rule, on the common dir `C` (the realpath of the answer):
|
|
139
150
|
* 1. `C` is named `.git`: the workspace is its parent. No spawn.
|
|
140
151
|
* 2. Otherwise `<parent of C>/.git` is probed with `--resolve-git-dir`. If it
|
|
141
|
-
* names `C`, the workspace is that parent (the `.bare` layout). A
|
|
142
|
-
*
|
|
152
|
+
* names `C`, the workspace is that parent (the `.bare` layout). A
|
|
153
|
+
* `.git` that is absent, or that git names as something else, is "no".
|
|
154
|
+
* A `.git` that exists and that git refuses fails the probe.
|
|
143
155
|
* 3. Otherwise the workspace is `C` itself, with `gitDir: true`.
|
|
144
156
|
*
|
|
145
157
|
* `gitDir` is what the scratch root's key reads: a git dir's `.git` suffix
|
|
@@ -160,12 +172,13 @@ function parentGitDirNames(parent, gitDir, cwd, timeoutMs) {
|
|
|
160
172
|
* uncanonicalised path.
|
|
161
173
|
*
|
|
162
174
|
* `timeoutMs` (default DEFAULT_GIT_TIMEOUT_MS) bounds this function's own
|
|
163
|
-
* step-2 probe only, the one spawn it
|
|
175
|
+
* step-2 probe only, the one spawn it can make. The caller's `--git-common-dir`
|
|
164
176
|
* probe that produced `gitCommonDir` is the caller's to bound, so a caller
|
|
165
177
|
* passing the same `timeoutMs` to both can wait close to twice that in all.
|
|
166
178
|
*
|
|
167
|
-
* Throws WorkspaceProbeFailed
|
|
168
|
-
* answer
|
|
179
|
+
* Throws WorkspaceProbeFailed when the step-2 probe could not run to an
|
|
180
|
+
* answer: git missing or killed at its bound, or a `<parent of C>/.git` that
|
|
181
|
+
* exists and that git refuses.
|
|
169
182
|
*/
|
|
170
183
|
export function resolveWorkspace(gitCommonDir, cwd = process.cwd(), { canonicalise = false, timeoutMs } = {}) {
|
|
171
184
|
const common = String(gitCommonDir ?? "").trim();
|
package/scripts/ledger.mjs
CHANGED
|
@@ -126,20 +126,23 @@ function cause(...candidates) {
|
|
|
126
126
|
// though "dominant" overstates that relationship: a normal invocation
|
|
127
127
|
// without `--file` can pay this same budget TWICE in sequence, once here and
|
|
128
128
|
// once more in repoCheck() below, before ever reaching `gh` at all — and
|
|
129
|
-
// THREE times
|
|
130
|
-
// `.bare` layout,
|
|
131
|
-
//
|
|
129
|
+
// THREE times where the common git dir is not named `.git` and its parent
|
|
130
|
+
// holds a `.git` (the `.bare` layout, or any other git dir sharing a parent
|
|
131
|
+
// with a `.git`), where fleetFile() runs a second probe with its own full budget:
|
|
132
132
|
// fleetFile()'s bound is per git child, not per call. At default settings
|
|
133
133
|
// two stalled probes TIE `gh`'s own 20 s bound and three exceed it, and even
|
|
134
134
|
// one stall can EXCEED it — up to GIT_TIMEOUT_MS + 20 s — when `gh` still
|
|
135
135
|
// runs (measured). `check`'s worst case is the sum of whichever of its
|
|
136
136
|
// children actually stall, not a single fixed term. That is three children
|
|
137
|
-
// normally; in
|
|
137
|
+
// normally; in the `.bare` layout it is three git children and `gh` is a fourth.
|
|
138
|
+
// A bare repository, a submodule or a `--separate-git-dir` clone has no `.git`
|
|
139
|
+
// beside its git dir, so fleetFile() runs no second probe there.
|
|
138
140
|
// Where the ledger's repository has no work tree (the `.bare` layout, a bare
|
|
139
141
|
// repository, a `--separate-git-dir` clone; not a submodule) repoCheck()'s
|
|
140
142
|
// `--show-toplevel` is refused and it asks the same directory for
|
|
141
|
-
// `--git-common-dir` to bind `gh` to
|
|
142
|
-
// `
|
|
143
|
+
// `--git-common-dir` to bind `gh` to: four git children and `gh` a fifth in
|
|
144
|
+
// the `.bare` layout, three and `gh` a fourth in the other two. `gh` runs in
|
|
145
|
+
// every one of those layouts. With `--file` the
|
|
143
146
|
// fleetFile() children drop out: two children normally, three where the
|
|
144
147
|
// repository has no work tree.
|
|
145
148
|
//
|