Skip to main content

diffler_core/
repo.rs

1//! Repository discovery and backend selection.
2
3use std::path::{Path, PathBuf};
4
5use thiserror::Error;
6
7use crate::diffalgo::DiffSettings;
8use crate::git::GitVcs;
9use crate::jj::JjVcs;
10use crate::vcs::{Vcs, VcsError};
11
12#[derive(Debug, Error)]
13pub enum RepoError {
14    #[error("not a git repository (or any parent): {0}")]
15    NotFound(PathBuf),
16    #[error("repository has no working directory: {0}")]
17    Bare(PathBuf),
18    #[error(transparent)]
19    Git(#[from] git2::Error),
20}
21
22/// The worktree root of the repository containing `path`. A `.jj` with no
23/// `.git` above it is a jj repo, whose root we return.
24pub fn discover(path: &Path) -> Result<PathBuf, RepoError> {
25    let repo = match git2::Repository::discover(path) {
26        Ok(repo) => repo,
27        Err(err) if err.code() == git2::ErrorCode::NotFound => {
28            return find_jj_root(path).ok_or_else(|| RepoError::NotFound(path.to_path_buf()));
29        }
30        Err(err) => return Err(RepoError::Git(err)),
31    };
32    repo.workdir()
33        .map(Path::to_path_buf)
34        .ok_or_else(|| RepoError::Bare(path.to_path_buf()))
35}
36
37fn find_jj_root(path: &Path) -> Option<PathBuf> {
38    let mut dir = if path.is_dir() {
39        Some(path)
40    } else {
41        path.parent()
42    };
43    while let Some(candidate) = dir {
44        if candidate.join(".jj").is_dir() {
45            return Some(candidate.to_path_buf());
46        }
47        dir = candidate.parent();
48    }
49    None
50}
51
52/// [`JjVcs`] when the root has a `.jj` directory, else [`GitVcs`].
53pub fn open(root: &Path) -> Result<Box<dyn Vcs>, VcsError> {
54    open_with_settings(root, &DiffSettings::default())
55}
56
57pub fn open_with_settings(root: &Path, settings: &DiffSettings) -> Result<Box<dyn Vcs>, VcsError> {
58    if root.join(".jj").is_dir() {
59        Ok(Box::new(JjVcs::open_with_settings(root, settings)?))
60    } else {
61        Ok(Box::new(GitVcs::open_with_settings(root, settings)?))
62    }
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68
69    #[test]
70    fn discovers_this_repository() {
71        let here = std::env::current_dir().expect("cwd");
72        let root = discover(&here).expect("repo root");
73        assert!(root.join(".git").exists());
74    }
75
76    #[test]
77    fn fails_outside_a_repository() {
78        // discover() walks every ancestor, so we rely on the tempdir having
79        // no repo above it
80        let dir = tempfile::tempdir().expect("tempdir");
81        assert!(matches!(discover(dir.path()), Err(RepoError::NotFound(_))));
82    }
83
84    #[test]
85    fn bare_repository_is_reported() {
86        let dir = tempfile::tempdir().expect("tempdir");
87        git2::Repository::init_bare(dir.path()).expect("init bare");
88        assert!(matches!(discover(dir.path()), Err(RepoError::Bare(_))));
89    }
90}