1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
mod common;
use common::TestRepo;
/// Stand in for another git-stk process holding the operation lock. Uses this
/// test process's own (live) PID so the lock reads as genuinely held, not as a
/// stale lock left by a dead process.
fn hold_lock(repo: &TestRepo) {
std::fs::write(
repo.path().join(".git/stk-lock"),
format!("{} merge\n", std::process::id()),
)
.expect("write lock");
}
#[test]
fn mutating_command_refuses_while_locked() {
let repo = TestRepo::new();
hold_lock(&repo);
repo.stack()
.args(["new", "feature/x"])
.assert()
.failure()
.stderr(predicates::str::contains(
"another git stk operation is in progress",
))
// The holder line is surfaced so the message is actionable.
.stderr(predicates::str::contains(format!(
"{} merge",
std::process::id()
)));
}
// Auto-reclaim relies on a PID-liveness probe (kill(pid, 0)), which git-stk
// only does on Unix; on Windows a stale lock is removed by hand.
#[cfg(unix)]
#[test]
fn a_stale_lock_from_a_dead_process_is_reclaimed() {
let repo = TestRepo::new();
// A lock left by a process that no longer exists - a huge PID that the
// kernel will report as no-such-process.
std::fs::write(repo.path().join(".git/stk-lock"), "2000000000 merge\n")
.expect("write stale lock");
repo.stack()
.args(["new", "feature/x"])
.assert()
.success()
.stderr(predicates::str::contains("reclaiming a stale git-stk lock"));
// The command reclaimed the lock, did its work, and released it on the way
// out, so nothing lingers.
assert!(!repo.path().join(".git/stk-lock").exists());
// And the work actually happened.
assert_eq!(
repo.git(["config", "--get", "branch.feature/x.stkParent"]),
"main"
);
}
#[test]
fn linked_worktrees_share_one_lock() {
let repo = TestRepo::new();
// A linked worktree of the same repo: it shares the common git dir, and so
// the same `branch.*` stack metadata the lock guards.
repo.git(["branch", "wt-branch"]);
let worktree = repo.path().join("linked-wt");
repo.git(["worktree", "add", worktree.to_str().unwrap(), "wt-branch"]);
// The main worktree holds the lock (written under the shared common dir).
hold_lock(&repo);
// A mutating command from the linked worktree must see that one lock and
// refuse - a per-worktree lock would let it clobber the shared metadata.
repo.stack_in(&worktree)
.args(["new", "feature/x"])
.assert()
.failure()
.stderr(predicates::str::contains(
"another git stk operation is in progress",
));
}
#[test]
fn run_is_lock_guarded() {
// `run` rewrites nothing, but it holds the lock for the whole window it
// walks the stack (see lock_name in main.rs). Pin that: it must refuse
// while another operation holds the lock, like any mutating command.
let repo = TestRepo::new();
hold_lock(&repo);
repo.stack()
.args(["run", "--", "true"])
.assert()
.failure()
.stderr(predicates::str::contains(
"another git stk operation is in progress",
));
}
#[test]
fn read_only_command_ignores_the_lock() {
let repo = TestRepo::new();
hold_lock(&repo);
// Navigation/read-only commands are safe to run alongside anything.
repo.stack().args(["list"]).assert().success();
}
#[test]
fn mutating_command_releases_the_lock_when_done() {
let repo = TestRepo::new();
repo.stack().args(["new", "feature/x"]).assert().success();
assert!(
!repo.path().join(".git/stk-lock").exists(),
"the lock file should be gone once the command finishes"
);
}
#[test]
fn mutating_command_releases_the_lock_on_failure() {
let repo = TestRepo::new();
repo.stack().args(["new", "feature/x"]).assert().success();
// A second `new` of the same branch acquires the lock, then errors. The
// lock is RAII, so it must still be cleaned up - otherwise a single failed
// command would wedge the repo for every later one.
repo.stack().args(["new", "feature/x"]).assert().failure();
assert!(
!repo.path().join(".git/stk-lock").exists(),
"the lock must be released even when the command errors"
);
// Proof it is actually free: the next mutating command succeeds.
repo.stack().args(["new", "feature/y"]).assert().success();
}