Skip to main content

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}