Skip to main content

ytil_git/
repo.rs

1use std::path::Path;
2use std::path::PathBuf;
3
4use git2::Repository;
5use rootcause::option_ext::OptionExt;
6use rootcause::prelude::ResultExt;
7
8/// Discover the Git repository containing `path` by walking
9/// parent directories upward until a repo root is found.
10///
11/// # Errors
12/// - If the path is not inside a Git repository.
13pub fn discover(path: &Path) -> rootcause::Result<Repository> {
14    Ok(Repository::discover(path)
15        .context("error discovering repo")
16        .attach_with(|| format!("path={}", path.display()))?)
17}
18
19/// Absolute working tree root path for the repository (or worktree).
20///
21/// Uses [`Repository::workdir`] which returns the correct root for both regular
22/// repositories and linked worktrees. Falls back to [`Repository::commondir`]
23/// (with `.git` stripped) for bare repositories.
24pub fn get_root(repo: &Repository) -> PathBuf {
25    if let Some(workdir) = repo.workdir() {
26        return workdir.to_path_buf();
27    }
28    // Bare repository: derive root from commondir.
29    repo.commondir()
30        .components()
31        .filter(|c| c.as_os_str() != ".git")
32        .collect()
33}
34
35/// Computes the relative path from the repository root to the given absolute path.
36///
37/// # Errors
38/// - If the repository does not have a working directory (bare repository).
39/// - If the provided path is not within the repository's working directory.
40pub fn get_relative_path_to_repo(path: &Path, repo: &Repository) -> rootcause::Result<PathBuf> {
41    let repo_workdir = repo
42        .workdir()
43        .context("error getting repository working directory")
44        .attach_with(|| format!("repo={:?}", repo.path().display()))?;
45    Ok(Path::new("/").join(path.strip_prefix(repo_workdir)?))
46}
47
48#[cfg(test)]
49mod tests {
50    use test_that::prelude::*;
51
52    use super::*;
53
54    #[test]
55    fn test_discover_when_path_is_inside_repo_returns_repo() {
56        let (_temp_dir, repo) = crate::tests::init_test_repo(None);
57        let workdir = repo.workdir().unwrap();
58        assert_that!(discover(workdir).map(|_| ()), ok(eq(())));
59    }
60
61    #[test]
62    fn test_discover_when_path_is_not_a_repo_returns_error() {
63        let temp_dir = tempfile::TempDir::new().unwrap();
64        assert_that!(
65            discover(temp_dir.path()).map_err(|err| err.to_string()).map(|_| ()),
66            err(contains_substring("error discovering repo"))
67        );
68    }
69
70    #[test]
71    fn test_get_root_returns_workdir() {
72        let (_temp_dir, repo) = crate::tests::init_test_repo(None);
73        let root = get_root(&repo);
74        assert_that!(root, eq(repo.workdir().unwrap()));
75    }
76
77    #[test]
78    fn test_get_root_in_worktree_returns_worktree_path() {
79        let (temp_dir, repo) = crate::tests::init_test_repo(None);
80
81        let wt_dir = temp_dir.path().join("my_worktree");
82        repo.worktree("my_worktree", &wt_dir, None).unwrap();
83
84        let wt_repo = Repository::open(&wt_dir).unwrap();
85        let root = get_root(&wt_repo);
86        assert_that!(root, eq(wt_dir.canonicalize().unwrap()));
87    }
88
89    #[test]
90    fn test_get_relative_path_to_repo_when_path_inside_repo_returns_rooted_relative() {
91        let (_temp_dir, repo) = crate::tests::init_test_repo(None);
92        let workdir = repo.workdir().unwrap();
93        let file_path = workdir.join("src").join("main.rs");
94        assert_that!(
95            get_relative_path_to_repo(&file_path, &repo),
96            ok(eq(PathBuf::from("/src/main.rs")))
97        );
98    }
99
100    #[test]
101    fn test_get_relative_path_to_repo_when_path_outside_repo_returns_error() {
102        let (_temp_dir, repo) = crate::tests::init_test_repo(None);
103        let outside_path = Path::new("/completely/different/path");
104        assert_that!(get_relative_path_to_repo(outside_path, &repo), err(anything()));
105    }
106}