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
//! Checks that pause while a git operation is part-way through.
//!
//! This replaces one hard-coded `CHERRY_PICK_HEAD` test in one dispatcher —
//! the other carried a comment admitting it had no guard "because the zsh
//! pre-push had none either", which is history rather than a decision.
mod common;
use common::Repo;
fn conflicted() -> String {
format!(
"{}\nours\n{}\ntheirs\n{}\n",
"<".repeat(7),
"=".repeat(7),
">".repeat(7)
)
}
/// Content checks cannot say anything useful mid-operation: half the tree is
/// somebody else's work and you cannot fix it from in here.
#[test]
fn content_checks_pause_during_a_merge() {
let r = Repo::new();
r.stage("x.json", "{ BROKEN\n");
assert!(!r.hook("pre-commit", &[]).passed(), "baseline");
std::fs::write(r.path(".git/MERGE_HEAD"), "deadbeef\n").expect("write");
let run = r.hook("pre-commit", &[]);
assert!(run.passed(), "should have paused:\n{}", run.output());
assert!(run.says("paused during a merge"), "{}", run.output());
assert!(
run.says("pre-commit-lint-json-yaml"),
"must name what paused:\n{}",
run.output()
);
}
/// Every operation, not just the one somebody hit first.
#[test]
fn every_marker_is_recognised() {
for (marker, word) in [
("MERGE_HEAD", "a merge"),
("CHERRY_PICK_HEAD", "a cherry-pick"),
("REVERT_HEAD", "a revert"),
// `REBASE_HEAD` is NOT here — see `a_finished_rebase_is_not_a_rebase`.
("rebase-apply", "a rebase"),
] {
let r = Repo::new();
r.stage("x.json", "{ BROKEN\n");
std::fs::write(r.path(".git").join(marker), "deadbeef\n").expect("write");
let run = r.hook("pre-commit", &[]);
assert!(run.passed(), "{marker} did not pause:\n{}", run.output());
assert!(run.says(word), "{marker} misnamed:\n{}", run.output());
}
}
/// `rebase-merge` is a DIRECTORY, not a file. `Path::exists` covers both, and
/// an implementation that only tested for files would miss the common case —
/// an interactive rebase.
#[test]
fn an_interactive_rebase_directory_counts() {
let r = Repo::new();
r.stage("x.json", "{ BROKEN\n");
std::fs::create_dir_all(r.path(".git/rebase-merge")).expect("mkdir");
let run = r.hook("pre-commit", &[]);
assert!(run.passed(), "{}", run.output());
assert!(run.says("a rebase"), "{}", run.output());
}
/// A rebase that FINISHED is not a rebase in progress.
///
/// `REBASE_HEAD` used to count, and git does not remove it when
/// `rebase --continue` completes — it is a convenience ref naming the commit
/// the rebase last stopped on, and it says a rebase HAPPENED. Every other
/// marker here is cleaned up by the operation that wrote it.
///
/// The cost was silent and permanent: in a worktree that had ever hit a
/// rebase conflict, `pull-rebase` and all four push test gates paused on
/// every push from then on, announced by a line that reads as a passing
/// condition. Found in this repository, on the branch that fixed it.
#[test]
fn a_finished_rebase_is_not_a_rebase() {
let r = Repo::new();
r.stage("x.json", "{ BROKEN\n");
// What git leaves behind: the ref, and no rebase directory.
std::fs::write(r.path(".git").join("REBASE_HEAD"), "deadbeef\n").expect("write");
let run = r.hook("pre-commit", &[]);
assert!(
!run.says("a rebase"),
"a leftover REBASE_HEAD paused the checks:\n{}",
run.output()
);
assert!(
!run.passed(),
"the broken JSON must still be caught:\n{}",
run.output()
);
}
/// The behaviour change worth arguing for: the old guard skipped the WHOLE
/// pre-commit stage during a cherry-pick, which silenced the one check you most
/// want on a resolution commit. Leaving a conflict marker in the commit that
/// resolves a merge is the bug.
#[test]
fn merge_conflict_still_runs_during_a_merge() {
let r = Repo::new();
r.stage("bad.txt", &conflicted());
std::fs::write(r.path(".git/MERGE_HEAD"), "deadbeef\n").expect("write");
let run = r.hook("pre-commit", &[]);
assert!(
!run.passed(),
"a conflict marker in a resolution commit must still block:\n{}",
run.output()
);
assert!(run.says("conflict"), "{}", run.output());
}
/// Nothing in progress means nothing paused, and no noise about it.
#[test]
fn a_normal_commit_says_nothing_about_git_state() {
let r = Repo::new();
r.stage("a.txt", "hello\n");
let run = r.hook("pre-commit", &[]);
assert!(run.passed(), "{}", run.output());
assert!(!run.says("paused"), "{}", run.output());
}