standard_githooks/run.rs
1/// The execution mode for a hook.
2///
3/// Determines how commands without an explicit prefix behave when they
4/// exit with a non-zero status code.
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum HookMode {
7 /// Run all commands and collect results. Report a summary at the end.
8 Collect,
9 /// Abort on the first command that fails (non-advisory).
10 FailFast,
11}
12
13/// Return the default execution mode for a given hook name.
14///
15/// Per the spec:
16/// - `pre-commit` defaults to `Collect` (show all issues at once).
17/// - `pre-push` defaults to `FailFast` (don't push broken code).
18/// - `commit-msg` defaults to `FailFast` (reject bad messages immediately).
19/// - All other hooks default to `Collect` (safe default).
20///
21/// # Example
22///
23/// ```
24/// use standard_githooks::{HookMode, default_mode};
25///
26/// assert_eq!(default_mode("pre-commit"), HookMode::Collect);
27/// assert_eq!(default_mode("pre-push"), HookMode::FailFast);
28/// assert_eq!(default_mode("commit-msg"), HookMode::FailFast);
29/// assert_eq!(default_mode("post-merge"), HookMode::Collect);
30/// ```
31pub fn default_mode(hook_name: &str) -> HookMode {
32 match hook_name {
33 "pre-push" | "commit-msg" => HookMode::FailFast,
34 _ => HookMode::Collect,
35 }
36}
37
38/// Replace `{msg}` tokens in a command string with the given file path.
39///
40/// This enables hooks like `commit-msg` to pass the commit message file
41/// path into commands. If the command does not contain `{msg}`, the
42/// original string is returned unchanged.
43///
44/// # Example
45///
46/// ```
47/// use standard_githooks::substitute_msg;
48///
49/// let result = substitute_msg("git std check --file {msg}", ".git/COMMIT_EDITMSG");
50/// assert_eq!(result, "git std check --file .git/COMMIT_EDITMSG");
51///
52/// let unchanged = substitute_msg("cargo test", ".git/COMMIT_EDITMSG");
53/// assert_eq!(unchanged, "cargo test");
54/// ```
55pub fn substitute_msg(command: &str, msg_path: &str) -> String {
56 command.replace("{msg}", msg_path)
57}
58
59#[cfg(test)]
60mod tests {
61 use super::*;
62
63 #[test]
64 fn pre_commit_defaults_to_collect() {
65 assert_eq!(default_mode("pre-commit"), HookMode::Collect);
66 }
67
68 #[test]
69 fn pre_push_defaults_to_fail_fast() {
70 assert_eq!(default_mode("pre-push"), HookMode::FailFast);
71 }
72
73 #[test]
74 fn commit_msg_defaults_to_fail_fast() {
75 assert_eq!(default_mode("commit-msg"), HookMode::FailFast);
76 }
77
78 #[test]
79 fn unknown_hook_defaults_to_collect() {
80 assert_eq!(default_mode("post-merge"), HookMode::Collect);
81 assert_eq!(default_mode("pre-rebase"), HookMode::Collect);
82 assert_eq!(default_mode("post-checkout"), HookMode::Collect);
83 }
84
85 #[test]
86 fn substitute_msg_replaces_token() {
87 let result = substitute_msg("git std check --file {msg}", ".git/COMMIT_EDITMSG");
88 assert_eq!(result, "git std check --file .git/COMMIT_EDITMSG");
89 }
90
91 #[test]
92 fn substitute_msg_no_token() {
93 let result = substitute_msg("cargo test --workspace", ".git/COMMIT_EDITMSG");
94 assert_eq!(result, "cargo test --workspace");
95 }
96
97 #[test]
98 fn substitute_msg_multiple_tokens() {
99 let result = substitute_msg("echo {msg} && cat {msg}", "/tmp/msg");
100 assert_eq!(result, "echo /tmp/msg && cat /tmp/msg");
101 }
102
103 #[test]
104 fn substitute_msg_empty_path() {
105 let result = substitute_msg("check --file {msg}", "");
106 assert_eq!(result, "check --file ");
107 }
108}