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!(result.unwrap(), provided);
55    }
56
57    #[test]
58    fn defaults_to_commit_editmsg_when_not_provided() {
59        let temp = TempDir::new().unwrap();
60        let repo = Repository::init(temp.path()).unwrap();
61        let expected = repo.path().join("COMMIT_EDITMSG");
62
63        let result = resolve_commit_message_path(None, temp.path());
64
65        assert_eq!(result.unwrap(), expected);
66    }
67
68    #[test]
69    fn discovers_repo_from_subdirectory() {
70        let temp = TempDir::new().unwrap();
71        let repo = Repository::init(temp.path()).unwrap();
72        let expected = repo.path().join("COMMIT_EDITMSG");
73
74        let subdir = temp.path().join("nested/deep");
75        fs::create_dir_all(&subdir).unwrap();
76
77        let result = resolve_commit_message_path(None, &subdir);
78
79        assert_eq!(result.unwrap(), expected);
80    }
81
82    #[test]
83    fn errors_when_not_in_git_repo_and_not_provided() {
84        let temp = TempDir::new().unwrap();
85        let result = resolve_commit_message_path(None, temp.path());
86        assert!(result.is_err());
87    }
88}