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(
19        "jj repo at {0} is not colocated with git; run `jj git colocation enable` there to use diffler"
20    )]
21    JjNotColocated(PathBuf),
22    #[error(transparent)]
23    Git(#[from] git2::Error),
24}
25
26/// Discover the repository containing `path` and return its working directory root.
27/// A `.jj` directory with no colocated `.git` is a distinct, more actionable
28/// failure than a plain "not a repository": we found a jj repo but no git one.
29pub fn discover(path: &Path) -> Result<PathBuf, RepoError> {
30    let repo = match git2::Repository::discover(path) {
31        Ok(repo) => repo,
32        Err(err) if err.code() == git2::ErrorCode::NotFound => {
33            return Err(find_uncolocated_jj(path).map_or_else(
34                || RepoError::NotFound(path.to_path_buf()),
35                RepoError::JjNotColocated,
36            ));
37        }
38        Err(err) => return Err(RepoError::Git(err)),
39    };
40    repo.workdir()
41        .map(Path::to_path_buf)
42        .ok_or_else(|| RepoError::Bare(path.to_path_buf()))
43}
44
45/// Walk `path` and its ancestors for a `.jj` directory, for the discovery
46/// error message when git2 found no `.git` at all.
47fn find_uncolocated_jj(path: &Path) -> Option<PathBuf> {
48    let mut dir = if path.is_dir() {
49        Some(path)
50    } else {
51        path.parent()
52    };
53    while let Some(candidate) = dir {
54        if candidate.join(".jj").is_dir() {
55            return Some(candidate.to_path_buf());
56        }
57        dir = candidate.parent();
58    }
59    None
60}
61
62/// Open the right backend for `root`, as [`discover`] resolved it, at
63/// [`DiffSettings::default`]: [`JjVcs`] when a `.jj` directory sits beside
64/// `.git` (a colocated jj repo), plain [`GitVcs`] otherwise.
65pub fn open(root: &Path) -> Result<Box<dyn Vcs>, VcsError> {
66    open_with_settings(root, &DiffSettings::default())
67}
68
69/// [`open`] with the line-diff context, algorithm and indent heuristic the
70/// review diffs with.
71pub fn open_with_settings(root: &Path, settings: &DiffSettings) -> Result<Box<dyn Vcs>, VcsError> {
72    if root.join(".jj").is_dir() {
73        Ok(Box::new(JjVcs::open_with_settings(root, settings)?))
74    } else {
75        Ok(Box::new(GitVcs::open_with_settings(root, settings)?))
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82
83    #[test]
84    fn discovers_this_repository() {
85        let here = std::env::current_dir().expect("cwd");
86        let root = discover(&here).expect("repo root");
87        assert!(root.join(".git").exists());
88    }
89
90    #[test]
91    fn fails_outside_a_repository() {
92        // discover() walks all ancestors, so this test needs a dir whose
93        // ancestors are repo-free; tempdirs satisfy that on CI runners
94        let dir = tempfile::tempdir().expect("tempdir");
95        assert!(matches!(discover(dir.path()), Err(RepoError::NotFound(_))));
96    }
97
98    #[test]
99    fn bare_repository_is_reported() {
100        let dir = tempfile::tempdir().expect("tempdir");
101        git2::Repository::init_bare(dir.path()).expect("init bare");
102        assert!(matches!(discover(dir.path()), Err(RepoError::Bare(_))));
103    }
104}