giro/
lib.rs

1use std::{
2    io::Error,
3    path::{Path, PathBuf},
4};
5
6/// Check if a path resides within a git repository, and if so, return the
7/// path to the root of the git repository. If the path does not reside insdie
8/// a git repository, `None` will be returned.
9///
10/// The ".git" directory inside a git repository is also considered to be
11/// inside the git repository.
12pub fn git_root<P: AsRef<Path>>(path: P) -> Result<Option<PathBuf>, Error> {
13    let path: PathBuf = normalize(path)?;
14    Ok(traverse(&path))
15}
16
17fn normalize<P: AsRef<Path>>(path: P) -> Result<PathBuf, Error> {
18    let path: PathBuf = path.as_ref().canonicalize()?;
19    if path.is_dir() {
20        Ok(path)
21    } else {
22        match path.parent() {
23            Some(parent) => Ok(parent.to_path_buf()),
24            None => Ok(path),
25        }
26    }
27}
28
29fn traverse(path: &Path) -> Option<PathBuf> {
30    let git_config: PathBuf = path.join(".git").join("config");
31    if git_config.exists() {
32        Some(path.to_path_buf())
33    } else {
34        match path.parent() {
35            Some(parent) => traverse(parent),
36            None => None,
37        }
38    }
39}
40
41#[cfg(test)]
42mod tests {
43    use super::*;
44
45    #[test]
46    fn test_directory_is_not_in_git_repo() {
47        let git_root: Option<PathBuf> = git_root("/dev").unwrap();
48        assert_eq!(None, git_root);
49    }
50
51    #[test]
52    fn test_file_is_not_in_git_repo() {
53        let git_root: Option<PathBuf> = git_root("/dev/null").unwrap();
54        assert_eq!(None, git_root);
55    }
56
57    #[test]
58    fn test_project_has_git_root() {
59        let git_root: Option<PathBuf> = git_root(".").unwrap();
60        assert!(git_root.is_some());
61    }
62
63    #[test]
64    fn test_project_git_config_dir_is_inside_git_repo() {
65        let git_root: Option<PathBuf> = git_root(".git/").unwrap();
66        assert!(git_root.is_some());
67    }
68}