@feigi/fleet-ctl 3.18.11 → 3.18.12
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
CHANGED
|
@@ -18,6 +18,7 @@ import { tmpdir } from "node:os";
|
|
|
18
18
|
import { join } from "node:path";
|
|
19
19
|
import { fileURLToPath } from "node:url";
|
|
20
20
|
import { writeExecStub } from "./exec-stub.mjs";
|
|
21
|
+
import { worktreeNames } from "./worktree-porcelain.mjs";
|
|
21
22
|
|
|
22
23
|
const CLAIM = fileURLToPath(new URL("./claim-ticket.sh", import.meta.url));
|
|
23
24
|
const REAP = fileURLToPath(new URL("./reap.sh", import.meta.url));
|
|
@@ -111,7 +112,7 @@ test("a freshly claimed branch is not [gone], and reap.sh leaves it alone", (t)
|
|
|
111
112
|
assert.deepEqual(JSON.parse(reaped.stdout).reaped, []);
|
|
112
113
|
// Positive control on the same run: the branch and its worktree survived.
|
|
113
114
|
assert.equal(f.git(f.w, "rev-parse", "--verify", "refs/heads/fix/42-slug").length, 40);
|
|
114
|
-
assert.
|
|
115
|
+
assert.ok(worktreeNames(f.git(f.w, "worktree", "list", "--porcelain")).includes("42-slug"));
|
|
115
116
|
});
|
|
116
117
|
|
|
117
118
|
test("after push -u, merge and prune, reap.sh does see the branch as [gone]", (t) => {
|
package/scripts/reap.test.mjs
CHANGED
|
@@ -17,6 +17,7 @@ import { join } from "node:path";
|
|
|
17
17
|
import { fileURLToPath } from "node:url";
|
|
18
18
|
import { slowTransport, SSH_URL } from "./slow-transport.mjs";
|
|
19
19
|
import { writeExecStub } from "./exec-stub.mjs";
|
|
20
|
+
import { hasAttribute, worktreeNames } from "./worktree-porcelain.mjs";
|
|
20
21
|
|
|
21
22
|
const SCRIPT = fileURLToPath(new URL("./reap.sh", import.meta.url));
|
|
22
23
|
|
|
@@ -43,6 +44,9 @@ const ENV = {
|
|
|
43
44
|
const git = (cwd, ...args) =>
|
|
44
45
|
execFileSync("git", args, { cwd, env: ENV, encoding: "utf8" }).trim();
|
|
45
46
|
|
|
47
|
+
/** worktreeNames() of `w`'s listing: never its paths, which carry TMPDIR (#2531). */
|
|
48
|
+
const listedNames = (w) => worktreeNames(git(w, "worktree", "list", "--porcelain"));
|
|
49
|
+
|
|
46
50
|
// Absolute path to the real git, for any test that shadows `git` on PATH.
|
|
47
51
|
const REAL_GIT = execFileSync("sh", ["-c", "command -v git"], { encoding: "utf8" }).trim();
|
|
48
52
|
|
|
@@ -746,14 +750,14 @@ test("a successful --apply run still reaps, still prunes, and exits 0 unchanged"
|
|
|
746
750
|
const wtDir = join(w, "..", "stale-wt");
|
|
747
751
|
git(w, "worktree", "add", "-q", "-b", "scratch/stale", wtDir, "main");
|
|
748
752
|
rmSync(wtDir, { recursive: true, force: true });
|
|
749
|
-
assert.
|
|
753
|
+
assert.ok(listedNames(w).includes("stale-wt"), "fixture must start with a prunable registration");
|
|
750
754
|
|
|
751
755
|
const { code, json } = runReap(w, ["--apply"]);
|
|
752
756
|
|
|
753
757
|
assert.equal(code, 0);
|
|
754
758
|
assert.deepEqual(json.reaped, ["feature/merged"]);
|
|
755
759
|
assert.deepEqual(json.kept, []);
|
|
756
|
-
assert.
|
|
760
|
+
assert.ok(!listedNames(w).includes("stale-wt"), "the prune must still run and clear the stale registration");
|
|
757
761
|
});
|
|
758
762
|
|
|
759
763
|
// The other half of the same guard, and the half no test had: the default
|
|
@@ -1307,7 +1311,7 @@ test("a [gone] branch whose worktree directory was deleted by hand is reaped, ne
|
|
|
1307
1311
|
const w = repo(t);
|
|
1308
1312
|
const wt = mergedGoneBranchWithWorktree(w, "feature/merged", "merged work");
|
|
1309
1313
|
rmSync(wt, { recursive: true, force: true });
|
|
1310
|
-
assert.
|
|
1314
|
+
assert.ok(listedNames(w).includes("feature/merged"), "fixture: the stale registration must still be listed");
|
|
1311
1315
|
|
|
1312
1316
|
const { code, json } = runReap(w, ["--apply"]);
|
|
1313
1317
|
|
|
@@ -1315,7 +1319,7 @@ test("a [gone] branch whose worktree directory was deleted by hand is reaped, ne
|
|
|
1315
1319
|
assert.deepEqual(json.reaped, ["feature/merged"], "a directory that is not there holds no work to protect");
|
|
1316
1320
|
assert.deepEqual(json.kept, []);
|
|
1317
1321
|
assert.equal(branchExists(w, "feature/merged"), false);
|
|
1318
|
-
assert.
|
|
1322
|
+
assert.ok(!listedNames(w).includes("feature/merged"), "the stale registration must not survive the run");
|
|
1319
1323
|
});
|
|
1320
1324
|
|
|
1321
1325
|
test("a dry run and --apply agree about a deleted worktree directory (#128)", (t) => {
|
|
@@ -2010,7 +2014,7 @@ test("a refusal that LEFT the registration in place is reported as such, distinc
|
|
|
2010
2014
|
assert.doesNotMatch(json.kept[0].reason, /\n/, "git's message here really is two lines — it must arrive flattened");
|
|
2011
2015
|
assert.match(stderr, /KEEP feature\/merged/);
|
|
2012
2016
|
|
|
2013
|
-
assert.
|
|
2017
|
+
assert.ok(listedNames(w).includes("feature/merged"), "the registration really did survive");
|
|
2014
2018
|
assert.equal(existsSync(wt), true);
|
|
2015
2019
|
assert.equal(branchExists(w, "feature/merged"), true);
|
|
2016
2020
|
});
|
|
@@ -2543,9 +2547,9 @@ test("the dry run removes nothing, and still cannot predict a refusal (#391)", (
|
|
|
2543
2547
|
// Nothing was touched: directories, registrations and branches all intact.
|
|
2544
2548
|
assert.equal(existsSync(healthy), true, "a dry run must not remove the worktree it COULD have removed");
|
|
2545
2549
|
assert.equal(existsSync(locked), true);
|
|
2546
|
-
const reg =
|
|
2547
|
-
assert.
|
|
2548
|
-
assert.
|
|
2550
|
+
const reg = listedNames(w);
|
|
2551
|
+
assert.ok(reg.includes("feature/a-healthy"));
|
|
2552
|
+
assert.ok(reg.includes("feature/b-locked"));
|
|
2549
2553
|
assert.equal(branchExists(w, "feature/a-healthy"), true);
|
|
2550
2554
|
assert.equal(branchExists(w, "feature/b-locked"), true);
|
|
2551
2555
|
});
|
|
@@ -2663,7 +2667,8 @@ test("a detached worktree whose HEAD is merged is removed, not walked past (#381
|
|
|
2663
2667
|
const w = repo(t);
|
|
2664
2668
|
const wt = detachedMergedWorktree(w, "docs/79-brief", "work that landed");
|
|
2665
2669
|
assert.ok(existsSync(wt), "fixture");
|
|
2666
|
-
assert.
|
|
2670
|
+
assert.ok(hasAttribute(git(w, "worktree", "list", "--porcelain"), "detached"), "fixture: the worktree must really be detached");
|
|
2671
|
+
assert.ok(listedNames(w).includes("79-brief"), "fixture: the worktree must start out listed");
|
|
2667
2672
|
|
|
2668
2673
|
const { code, json, stderr } = runReap(w, ["--apply"]);
|
|
2669
2674
|
|
|
@@ -2674,7 +2679,7 @@ test("a detached worktree whose HEAD is merged is removed, not walked past (#381
|
|
|
2674
2679
|
assert.ok(stderr.includes(` REMOVED worktree ${wt}`), `the removal must be reported: ${stderr}`);
|
|
2675
2680
|
// The registration goes with it, or the in-flight probe still reads the
|
|
2676
2681
|
// ticket as taken — the whole cost this ticket exists to stop.
|
|
2677
|
-
assert.
|
|
2682
|
+
assert.ok(!listedNames(w).includes("79-brief"));
|
|
2678
2683
|
});
|
|
2679
2684
|
|
|
2680
2685
|
test("a branchless sweep whose awk cannot read the listing declines, never sweeps silently (#993)", (t) => {
|
|
@@ -2798,7 +2803,7 @@ test("a detached worktree whose HEAD git cannot resolve is kept, never swept (#3
|
|
|
2798
2803
|
writeFileSync(join(adminEntry(w, wt), "HEAD"), "not an object id\n");
|
|
2799
2804
|
assert.match(
|
|
2800
2805
|
git(w, "worktree", "list", "--porcelain"),
|
|
2801
|
-
|
|
2806
|
+
/^HEAD 0{40}$/m,
|
|
2802
2807
|
"fixture: git must report the null object id for this worktree",
|
|
2803
2808
|
);
|
|
2804
2809
|
|
|
@@ -2958,14 +2963,15 @@ test("a detached worktree whose directory was deleted by hand still has its regi
|
|
|
2958
2963
|
const w = repo(t);
|
|
2959
2964
|
const wt = detachedMergedWorktree(w, "docs/79-brief", "work that landed");
|
|
2960
2965
|
rmSync(wt, { recursive: true, force: true });
|
|
2961
|
-
assert.
|
|
2966
|
+
assert.ok(hasAttribute(git(w, "worktree", "list", "--porcelain"), "prunable"), "fixture: the stale registration must still be listed");
|
|
2967
|
+
assert.ok(listedNames(w).includes("79-brief"), "fixture: the stale registration must be listed by name before the run");
|
|
2962
2968
|
|
|
2963
2969
|
const { code, json } = runReap(w, ["--apply"]);
|
|
2964
2970
|
|
|
2965
2971
|
assert.equal(code, 0);
|
|
2966
2972
|
assert.deepEqual(json.worktreesRemoved, [wt]);
|
|
2967
2973
|
assert.deepEqual(json.kept, []);
|
|
2968
|
-
assert.
|
|
2974
|
+
assert.ok(!listedNames(w).includes("79-brief"));
|
|
2969
2975
|
});
|
|
2970
2976
|
|
|
2971
2977
|
test("a detached worktree behind an unreadable parent is kept, never removed as absent (#381, #83)", (t) => {
|
|
@@ -22,6 +22,7 @@ import { dirname, join } from "node:path";
|
|
|
22
22
|
import { fileURLToPath } from "node:url";
|
|
23
23
|
import { slowTransport, SSH_URL } from "./slow-transport.mjs";
|
|
24
24
|
import { writeExecStub } from "./exec-stub.mjs";
|
|
25
|
+
import { hasAttribute } from "./worktree-porcelain.mjs";
|
|
25
26
|
|
|
26
27
|
const SCRIPT = fileURLToPath(new URL("./release-ticket.sh", import.meta.url));
|
|
27
28
|
const INFLIGHT = fileURLToPath(new URL("./inflight.sh", import.meta.url));
|
|
@@ -994,6 +995,7 @@ test("the locked-stray unlock hint survives a literal ' in the worktree path (#1
|
|
|
994
995
|
git(c.wt, "checkout", "-q", "--detach", "HEAD");
|
|
995
996
|
git(r.w, "worktree", "lock", c.wt, "--reason", "held by a review");
|
|
996
997
|
rmSync(c.wt, { recursive: true, force: true });
|
|
998
|
+
assert.ok(hasAttribute(git(r.w, "worktree", "list", "--porcelain"), "locked"), "fixture: the stray must really be locked before the remedy runs");
|
|
997
999
|
|
|
998
1000
|
const { code, json, stderr } = release(r, c);
|
|
999
1001
|
assert.equal(code, 1, stderr);
|
|
@@ -1005,7 +1007,7 @@ test("the locked-stray unlock hint survives a literal ' in the worktree path (#1
|
|
|
1005
1007
|
execFileSync("sh", ["-c", remedy], { cwd: r.w, encoding: "utf8", env: ENV });
|
|
1006
1008
|
|
|
1007
1009
|
assert.ok(
|
|
1008
|
-
!git(r.w, "worktree", "list", "--porcelain")
|
|
1010
|
+
!hasAttribute(git(r.w, "worktree", "list", "--porcelain"), "locked"),
|
|
1009
1011
|
`the pasted command actually unlocked the real entry, not a truncated one: ${remedy}`,
|
|
1010
1012
|
);
|
|
1011
1013
|
});
|
|
@@ -1034,6 +1036,7 @@ test("the locked-stray unlock hint survives a plain space in the worktree path (
|
|
|
1034
1036
|
git(c.wt, "checkout", "-q", "--detach", "HEAD");
|
|
1035
1037
|
git(r.w, "worktree", "lock", c.wt, "--reason", "held by a review");
|
|
1036
1038
|
rmSync(c.wt, { recursive: true, force: true });
|
|
1039
|
+
assert.ok(hasAttribute(git(r.w, "worktree", "list", "--porcelain"), "locked"), "fixture: the stray must really be locked before the remedy runs");
|
|
1037
1040
|
|
|
1038
1041
|
const { code, json, stderr } = release(r, c);
|
|
1039
1042
|
assert.equal(code, 1, stderr);
|
|
@@ -1045,7 +1048,7 @@ test("the locked-stray unlock hint survives a plain space in the worktree path (
|
|
|
1045
1048
|
execFileSync("sh", ["-c", remedy], { cwd: r.w, encoding: "utf8", env: ENV });
|
|
1046
1049
|
|
|
1047
1050
|
assert.ok(
|
|
1048
|
-
!git(r.w, "worktree", "list", "--porcelain")
|
|
1051
|
+
!hasAttribute(git(r.w, "worktree", "list", "--porcelain"), "locked"),
|
|
1049
1052
|
`the pasted command actually unlocked the real entry, not a truncated one: ${remedy}`,
|
|
1050
1053
|
);
|
|
1051
1054
|
});
|
|
@@ -2732,7 +2735,7 @@ test("an unrelated prunable worktree does not block release of a branch it never
|
|
|
2732
2735
|
const stale = join(r.w, "..", "stale-wt");
|
|
2733
2736
|
git(r.w, "worktree", "add", "-q", "--detach", stale, "origin/main");
|
|
2734
2737
|
rmSync(stale, { recursive: true, force: true });
|
|
2735
|
-
assert.
|
|
2738
|
+
assert.ok(hasAttribute(git(r.w, "worktree", "list", "--porcelain"), "prunable"), "fixture: the stale registration must still be listed");
|
|
2736
2739
|
|
|
2737
2740
|
const { code, json, stderr } = release(r, c);
|
|
2738
2741
|
assert.equal(code, 0, stderr);
|
|
@@ -2748,7 +2751,7 @@ test("a prunable sibling that genuinely holds the claim via a stopped rebase sti
|
|
|
2748
2751
|
// no longer exists to be entered.
|
|
2749
2752
|
const { r, c, sib } = heldSibling(t, stopAtEditRebase);
|
|
2750
2753
|
rmSync(sib, { recursive: true, force: true });
|
|
2751
|
-
assert.
|
|
2754
|
+
assert.ok(hasAttribute(git(r.w, "worktree", "list", "--porcelain"), "prunable"), "fixture: the sibling's directory must be gone, its registration still there");
|
|
2752
2755
|
|
|
2753
2756
|
const { code, json, stderr } = release(r, c);
|
|
2754
2757
|
assert.equal(code, 2, stderr);
|
|
@@ -3605,9 +3608,8 @@ test("a worktree with no surviving ancestor below / still releases, never a perm
|
|
|
3605
3608
|
// died BEFORE it, so a fix that merely downgraded that `die` to a warning
|
|
3606
3609
|
// would exit 0 here too, with the stale registration still standing and the
|
|
3607
3610
|
// claim reported released — the same false success from the other side.
|
|
3608
|
-
assert.
|
|
3609
|
-
git(r.w, "worktree", "list", "--porcelain"),
|
|
3610
|
-
/nonexistent-top-level-178/,
|
|
3611
|
+
assert.ok(
|
|
3612
|
+
!git(r.w, "worktree", "list", "--porcelain").split("\n").includes(`worktree ${dest}`),
|
|
3611
3613
|
"the registration must actually be gone, not merely un-refused",
|
|
3612
3614
|
);
|
|
3613
3615
|
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// What a test guard may ask of a `git worktree list --porcelain` listing.
|
|
2
|
+
//
|
|
3
|
+
// Every `worktree` line carries the full path, and a fixture's path starts
|
|
4
|
+
// with the operator's TMPDIR, so a guard that greps the whole listing for a
|
|
5
|
+
// fixture's name or for an attribute answers for TMPDIR as well: one holding
|
|
6
|
+
// `locked`, `stale-wt` or `79-brief` failed the guard that looks for its
|
|
7
|
+
// absence (#2531). These read only the parts a fixture controls.
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The names the listing gives its worktrees: each `worktree` line's last path
|
|
11
|
+
* component, and each `branch refs/heads/` line's branch. The directories
|
|
12
|
+
* above a worktree are never a name.
|
|
13
|
+
*/
|
|
14
|
+
export const worktreeNames = (porcelain) =>
|
|
15
|
+
porcelain.split("\n").flatMap((l) =>
|
|
16
|
+
l.startsWith("worktree ") ? [l.slice("worktree ".length).replace(/.*\//, "")]
|
|
17
|
+
: l.startsWith("branch refs/heads/") ? [l.slice("branch refs/heads/".length)]
|
|
18
|
+
: []);
|
|
19
|
+
|
|
20
|
+
const ATTRIBUTES = new Set(["locked", "prunable", "detached"]);
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Whether any worktree in the listing carries the attribute `attr`
|
|
24
|
+
* (`locked`, `prunable`, `detached`). git prints an attribute on a line of its
|
|
25
|
+
* own, the bare word or the word, a space and a reason, so the line is matched
|
|
26
|
+
* whole and a path containing the word never is. Any other `attr` throws: a
|
|
27
|
+
* misspelling would answer false for ever, and a guard that asserts the
|
|
28
|
+
* attribute's absence would pass on it.
|
|
29
|
+
*/
|
|
30
|
+
export const hasAttribute = (porcelain, attr) => {
|
|
31
|
+
if (!ATTRIBUTES.has(attr)) {
|
|
32
|
+
throw new Error(`hasAttribute: unknown attribute ${JSON.stringify(attr)}; one of ${[...ATTRIBUTES].join(", ")}`);
|
|
33
|
+
}
|
|
34
|
+
return porcelain.split("\n").some((l) => l === attr || l.startsWith(`${attr} `));
|
|
35
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// worktree-porcelain.mjs against real `git worktree list --porcelain` output.
|
|
2
|
+
// Zero deps: `node --test plugin/scripts/worktree-porcelain.test.mjs`.
|
|
3
|
+
|
|
4
|
+
import { test } from "node:test";
|
|
5
|
+
import assert from "node:assert/strict";
|
|
6
|
+
import { execFileSync } from "node:child_process";
|
|
7
|
+
import { mkdirSync, mkdtempSync, realpathSync, rmSync } from "node:fs";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { hasAttribute, worktreeNames } from "./worktree-porcelain.mjs";
|
|
11
|
+
|
|
12
|
+
const ENV = {
|
|
13
|
+
...process.env,
|
|
14
|
+
GIT_DIR: undefined,
|
|
15
|
+
GIT_WORK_TREE: undefined,
|
|
16
|
+
GIT_AUTHOR_NAME: "t",
|
|
17
|
+
GIT_AUTHOR_EMAIL: "t@example.com",
|
|
18
|
+
GIT_COMMITTER_NAME: "t",
|
|
19
|
+
GIT_COMMITTER_EMAIL: "t@example.com",
|
|
20
|
+
GIT_CONFIG_GLOBAL: "/dev/null",
|
|
21
|
+
GIT_CONFIG_SYSTEM: "/dev/null",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const git = (cwd, ...args) =>
|
|
25
|
+
execFileSync("git", args, { cwd, env: ENV, encoding: "utf8" }).trim();
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A checkout on `main` whose parent directories spell the attributes the guards
|
|
29
|
+
* look for (`locked`, `prunable`, `detached`) and some of the names they look
|
|
30
|
+
* for (`feature/merged`, `79-brief`), the way an operator's TMPDIR can.
|
|
31
|
+
*/
|
|
32
|
+
function repoUnder(t) {
|
|
33
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), "worktree-porcelain-")));
|
|
34
|
+
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
35
|
+
const w = join(root, "locked", "prunable", "detached", "feature", "merged", "79-brief", "w");
|
|
36
|
+
mkdirSync(w, { recursive: true });
|
|
37
|
+
execFileSync("git", ["-c", "init.defaultBranch=main", "init", "-q", w], { env: ENV });
|
|
38
|
+
git(w, "commit", "-q", "--allow-empty", "-m", "root");
|
|
39
|
+
return w;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const listing = (w) => git(w, "worktree", "list", "--porcelain");
|
|
43
|
+
|
|
44
|
+
test("a name or attribute that is only in a worktree's parent directories is not reported", (t) => {
|
|
45
|
+
const w = repoUnder(t);
|
|
46
|
+
git(w, "worktree", "add", "-q", "-b", "fix/9-x", join(w, ".worktrees", "9-x"), "main");
|
|
47
|
+
const l = listing(w);
|
|
48
|
+
for (const word of ["locked", "prunable", "detached", "feature/merged", "79-brief"]) {
|
|
49
|
+
assert.ok(l.includes(word), `fixture: the listing must carry ${word} in a path: ${l}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
assert.deepEqual(worktreeNames(l), ["w", "main", "9-x", "fix/9-x"]);
|
|
53
|
+
for (const attr of ["locked", "prunable", "detached"]) {
|
|
54
|
+
assert.equal(hasAttribute(l, attr), false, `${attr} is only a parent directory here: ${l}`);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("a worktree's own basename, branch and attributes are reported", (t) => {
|
|
59
|
+
const w = repoUnder(t);
|
|
60
|
+
const brief = join(w, ".worktrees", "79-brief");
|
|
61
|
+
git(w, "worktree", "add", "-q", "-b", "feature/merged", brief, "main");
|
|
62
|
+
const lone = join(w, ".worktrees", "lone");
|
|
63
|
+
git(w, "worktree", "add", "-q", "--detach", lone, "main");
|
|
64
|
+
|
|
65
|
+
assert.deepEqual(worktreeNames(listing(w)), ["w", "main", "79-brief", "feature/merged", "lone"]);
|
|
66
|
+
assert.equal(hasAttribute(listing(w), "detached"), true, "the bare-word form");
|
|
67
|
+
|
|
68
|
+
git(w, "worktree", "lock", brief, "--reason", "held by a review");
|
|
69
|
+
assert.match(listing(w), /^locked held by a review$/m, "fixture: a reason follows the word on its line");
|
|
70
|
+
assert.equal(hasAttribute(listing(w), "locked"), true, "the word-and-reason form");
|
|
71
|
+
git(w, "worktree", "unlock", brief);
|
|
72
|
+
git(w, "worktree", "lock", brief);
|
|
73
|
+
assert.equal(hasAttribute(listing(w), "locked"), true);
|
|
74
|
+
git(w, "worktree", "unlock", brief);
|
|
75
|
+
assert.equal(hasAttribute(listing(w), "locked"), false);
|
|
76
|
+
|
|
77
|
+
rmSync(lone, { recursive: true, force: true });
|
|
78
|
+
assert.equal(hasAttribute(listing(w), "prunable"), true);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("a lock reason that spells a guard's prefix is not a name", (t) => {
|
|
82
|
+
const w = repoUnder(t);
|
|
83
|
+
const a = join(w, ".worktrees", "a");
|
|
84
|
+
git(w, "worktree", "add", "-q", "-b", "fix/a", a, "main");
|
|
85
|
+
git(w, "worktree", "lock", a, "--reason", "worktree on usb");
|
|
86
|
+
const b = join(w, ".worktrees", "b");
|
|
87
|
+
git(w, "worktree", "add", "-q", "-b", "fix/b", b, "main");
|
|
88
|
+
git(w, "worktree", "lock", b, "--reason", "branch refs/heads/held");
|
|
89
|
+
const c = join(w, ".worktrees", "c");
|
|
90
|
+
git(w, "worktree", "add", "-q", "-b", "fix/c", c, "main");
|
|
91
|
+
git(w, "worktree", "lock", c, "--reason", "see refs/heads/held");
|
|
92
|
+
const l = listing(w);
|
|
93
|
+
for (const reason of ["worktree on usb", "branch refs/heads/held", "see refs/heads/held"]) {
|
|
94
|
+
assert.match(l, new RegExp(`^locked ${reason}$`, "m"), `fixture: the reason is on its own line: ${l}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
assert.deepEqual(worktreeNames(l), ["w", "main", "a", "fix/a", "b", "fix/b", "c", "fix/c"]);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("an attribute git never prints is refused, not answered false", () => {
|
|
101
|
+
assert.throws(() => hasAttribute("worktree /x\nlocked\n", "lock"), /unknown attribute "lock"/);
|
|
102
|
+
});
|