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
142
143
//! Hooks dispatch from the COMMON `.git/hooks` — shared across every linked
//! worktree — but a git operation's markers (`MERGE_HEAD`, ...) and this
//! tool's own held-changes store live in the WORKTREE-PRIVATE gitdir under
//! `.git/worktrees/<name>`. Code that assumed `hooks_dir`'s parent IS
//! `$GIT_DIR` silently broke both the "never touch a tree mid-operation"
//! guard and the restore half of index fidelity for every linked worktree —
//! discovered when it happened to this very repository's own commit while
//! this file was being written: an unrelated file's unstaged fix was parked
//! in the common directory, `restore()` looked in the worktree-private one,
//! found nothing, and gave up silently.
mod common;
use common::Repo;
use std::process::Command;
/// A linked worktree of `r`, on a fresh branch. `git worktree add` never
/// installs hooks there — every worktree dispatches through the COMMON
/// directory's `.git/hooks`, which is the whole point of this file.
fn add_worktree(r: &Repo, branch: &str) -> std::path::PathBuf {
let path = r.dir.parent().expect("parent").join(format!(
"{}-{branch}",
r.dir.file_name().expect("name").to_string_lossy()
));
let _ = std::fs::remove_dir_all(&path);
let out = r.git(&[
"worktree",
"add",
"-q",
"-b",
branch,
path.to_str().expect("utf8"),
]);
assert!(
out.status.success(),
"git worktree add failed: {}",
String::from_utf8_lossy(&out.stderr)
);
path
}
/// The worktree's OWN gitdir — `.git` there is a FILE, not a directory, so a
/// fixture that wrote through `wt.join(".git/...")` would be writing into a
/// path that does not exist as a directory at all. Asked from git directly,
/// the same way `restore()` and `restore_command()` already do.
fn worktree_git_dir(wt: &std::path::Path) -> std::path::PathBuf {
let mut cmd = Command::new("git");
cmd.arg("-C")
.arg(wt)
.args(["rev-parse", "--path-format=absolute", "--git-dir"]);
Repo::strip_git_env_impl(&mut cmd);
let out = cmd.output().expect("git rev-parse --git-dir");
std::path::PathBuf::from(String::from_utf8_lossy(&out.stdout).trim())
}
/// `git -C <dir> add <path>`, with the same GIT_* stripping every other git
/// call in this file needs (see `common::Repo::strip_git_env_impl`).
fn git_add(dir: &std::path::Path, path: &str) -> bool {
let mut cmd = Command::new("git");
cmd.arg("-C").arg(dir).args(["add", path]);
Repo::strip_git_env_impl(&mut cmd);
cmd.output().expect("git add").status.success()
}
/// Run `amont pre-commit` exactly the way the real shim would from a
/// linked worktree: `--hooks-dir` is the COMMON directory's hooks folder
/// (where the dispatching shim script actually lives), while the process
/// itself runs with its CWD in the WORKTREE, exactly as git invokes it there.
fn hook_from_worktree(
main_hooks_dir: &std::path::Path,
worktree: &std::path::Path,
) -> (i32, String) {
let mut cmd = Command::new(env!("CARGO_BIN_EXE_amont"));
cmd.arg("--hooks-dir")
.arg(main_hooks_dir)
.arg("pre-commit")
.current_dir(worktree);
Repo::strip_git_env_impl(&mut cmd);
let out = cmd.output().expect("run amont pre-commit");
(
out.status.code().unwrap_or(-1),
format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
),
)
}
/// The guard `docs`/`staged_only.rs` call "never mid-operation": a merge
/// (or cherry-pick, rebase, revert) already holds work that is not the
/// author's, so content checks must pause rather than judge it. Must hold in
/// a linked worktree exactly as it does in the main one.
#[test]
fn a_merge_in_progress_in_a_worktree_still_pauses_content_checks() {
let r = Repo::new();
r.stage("x.json", "{\n \"a\": 1\n}\n");
r.commit("chore: seed");
let wt = add_worktree(&r, "feature");
std::fs::write(wt.join("x.json"), "{ BROKEN\n").expect("write");
assert!(git_add(&wt, "x.json"));
std::fs::write(worktree_git_dir(&wt).join("MERGE_HEAD"), "deadbeef\n").expect("write");
let (code, out) = hook_from_worktree(&r.path(".git/hooks"), &wt);
assert_eq!(code, 0, "should have paused, not judged the merge:\n{out}");
assert!(out.contains("paused during a merge"), "{out}");
let _ = std::fs::remove_dir_all(&wt);
}
/// The exact incident: unstaged work in a linked worktree must survive being
/// parked aside for the duration of the checks and put back afterward, the
/// same guarantee `index_fidelity.rs` proves for the main worktree.
#[test]
fn unstaged_work_in_a_worktree_survives_the_stash_and_restore() {
let r = Repo::new();
r.stage("x.json", "{\n \"a\": 1\n}\n");
r.commit("chore: seed");
let wt = add_worktree(&r, "feature");
std::fs::write(wt.join("x.json"), "{\n \"a\": 2\n}\n").expect("write");
assert!(git_add(&wt, "x.json"));
// Now the working tree disagrees with the index — real unstaged content.
std::fs::write(wt.join("x.json"), "{ THIS IS NOT JSON\n").expect("write");
let (code, out) = hook_from_worktree(&r.path(".git/hooks"), &wt);
assert_eq!(code, 0, "judged the working tree, not the commit:\n{out}");
assert_eq!(
std::fs::read_to_string(wt.join("x.json")).expect("read"),
"{ THIS IS NOT JSON\n",
"unstaged work in the worktree was not restored"
);
assert!(
!worktree_git_dir(&wt).join("amont-held").exists(),
"held files left behind in the worktree-private gitdir"
);
assert!(
!r.path(".git/amont-held").exists(),
"held files stranded in the COMMON gitdir — the exact incident this guards against"
);
let _ = std::fs::remove_dir_all(&wt);
}