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 as _;
6use rootcause::prelude::ResultExt as _;
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 super::*;
51
52    #[test]
53    fn discover_when_path_is_inside_repo_returns_repo() {
54        let (_temp_dir, repo) = crate::tests::init_test_repo(None);
55        let workdir = repo.workdir().unwrap();
56        assert2::assert!(let Ok(_repo) = discover(workdir));
57    }
58
59    #[test]
60    fn discover_when_path_is_not_a_repo_returns_error() {
61        let temp_dir = tempfile::TempDir::new().unwrap();
62        assert2::assert!(let Err(err) = discover(temp_dir.path()));
63        assert!(err.to_string().contains("error discovering repo"));
64    }
65
66    #[test]
67    fn get_root_returns_workdir() {
68        let (_temp_dir, repo) = crate::tests::init_test_repo(None);
69        let root = get_root(&repo);
70        pretty_assertions::assert_eq!(root, repo.workdir().unwrap());
71    }
72
73    #[test]
74    fn get_root_in_worktree_returns_worktree_path() {
75        let (temp_dir, repo) = crate::tests::init_test_repo(None);
76
77        let wt_dir = temp_dir.path().join("my_worktree");
78        repo.worktree("my_worktree", &wt_dir, None).unwrap();
79
80        let wt_repo = Repository::open(&wt_dir).unwrap();
81        let root = get_root(&wt_repo);
82        pretty_assertions::assert_eq!(root, wt_dir.canonicalize().unwrap());
83    }
84
85    #[test]
86    fn get_relative_path_to_repo_when_path_inside_repo_returns_rooted_relative() {
87        let (_temp_dir, repo) = crate::tests::init_test_repo(None);
88        let workdir = repo.workdir().unwrap();
89        let file_path = workdir.join("src").join("main.rs");
90        assert2::assert!(let Ok(rel) = get_relative_path_to_repo(&file_path, &repo));
91        pretty_assertions::assert_eq!(rel, PathBuf::from("/src/main.rs"));
92    }
93
94    #[test]
95    fn get_relative_path_to_repo_when_path_outside_repo_returns_error() {
96        let (_temp_dir, repo) = crate::tests::init_test_repo(None);
97        let outside_path = Path::new("/completely/different/path");
98        assert2::assert!(let Err(_err) = get_relative_path_to_repo(outside_path, &repo));
99    }
100}