Skip to main content

mit_commit_message_lints/external/
commit_message_path.rs

1//! Resolving the commit-message file path
2//!
3//! Git passes the path to the commit message file as the first positional
4//! argument to the `commit-msg` and `prepare-commit-msg` hooks. Hook
5//! managers such as [lefthook](https://github.com/evilmartians/lefthook)
6//! intercept these hooks but do not always forward that positional argument
7//! to the commands they run. When the argument is missing we fall back to
8//! the canonical location git writes the draft message to:
9//! `<gitdir>/COMMIT_EDITMSG`.
10
11use std::path::{Path, PathBuf};
12
13use git2::Repository;
14use miette::{IntoDiagnostic, Result};
15
16/// Resolve the path to the file containing the commit log message.
17///
18/// If `provided` is `Some` it is returned unchanged — this is the normal
19/// path when git invokes the hook directly.
20///
21/// If `provided` is `None` the git repository is discovered starting from
22/// `current_dir` and the path to `COMMIT_EDITMSG` inside the git directory
23/// is returned instead.
24///
25/// # Errors
26///
27/// If no path was provided and no git repository can be discovered from
28/// `current_dir`.
29pub fn resolve_commit_message_path(
30    provided: Option<PathBuf>,
31    current_dir: &Path,
32) -> Result<PathBuf> {
33    if let Some(path) = provided {
34        Ok(path)
35    } else {
36        let repo = Repository::discover(current_dir).into_diagnostic()?;
37        Ok(repo.path().join("COMMIT_EDITMSG"))
38    }
39}
40
41#[cfg(test)]
42mod tests {
43    use std::fs;
44    use std::path::{Path, PathBuf};
45
46    use super::resolve_commit_message_path;
47    use git2::Repository;
48    use tempfile::TempDir;
49
50    #[test]
51    fn returns_provided_path_unchanged() {
52        let provided = PathBuf::from("/some/arbitrary/path");
53        let result = resolve_commit_message_path(Some(provided.clone()), Path::new("/tmp"));
54        assert_eq!(
55            result.unwrap(),
56            provided,
57            "Expected the provided path to be returned unchanged"
58        );
59    }
60
61    #[test]
62    fn defaults_to_commit_editmsg_when_not_provided() {
63        let temp = TempDir::new().unwrap();
64        let repo = Repository::init(temp.path()).unwrap();
65        let expected = repo.path().join("COMMIT_EDITMSG");
66
67        let result = resolve_commit_message_path(None, temp.path());
68
69        assert_eq!(
70            result.unwrap(),
71            expected,
72            "Expected the default COMMIT_EDITMSG path when no path is provided"
73        );
74    }
75
76    #[test]
77    fn discovers_repo_from_subdirectory() {
78        let temp = TempDir::new().unwrap();
79        let repo = Repository::init(temp.path()).unwrap();
80        let expected = repo.path().join("COMMIT_EDITMSG");
81
82        let subdir = temp.path().join("nested/deep");
83        fs::create_dir_all(&subdir).unwrap();
84
85        let result = resolve_commit_message_path(None, &subdir);
86
87        assert_eq!(
88            result.unwrap(),
89            expected,
90            "Expected the COMMIT_EDITMSG path to be discovered from a subdirectory"
91        );
92    }
93
94    #[test]
95    fn errors_when_not_in_git_repo_and_not_provided() {
96        let temp = TempDir::new().unwrap();
97        let result = resolve_commit_message_path(None, temp.path());
98        assert!(
99            result.is_err(),
100            "Expected an error when not in a git repo and no path is provided"
101        );
102    }
103}