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/// The worktree root of the repository containing `path`. A `.jj` with no
27/// `.git` reports [`RepoError::JjNotColocated`].
28pub fn discover(path: &Path) -> Result<PathBuf, RepoError> {
29    let repo = match git2::Repository::discover(path) {
30        Ok(repo) => repo,
31        Err(err) if err.code() == git2::ErrorCode::NotFound => {
32            return Err(find_uncolocated_jj(path).map_or_else(
33                || RepoError::NotFound(path.to_path_buf()),
34                RepoError::JjNotColocated,
35            ));
36        }
37        Err(err) => return Err(RepoError::Git(err)),
38    };
39    repo.workdir()
40        .map(Path::to_path_buf)
41        .ok_or_else(|| RepoError::Bare(path.to_path_buf()))
42}
43
44fn find_uncolocated_jj(path: &Path) -> Option<PathBuf> {
45    let mut dir = if path.is_dir() {
46        Some(path)
47    } else {
48        path.parent()
49    };
50    while let Some(candidate) = dir {
51        if candidate.join(".jj").is_dir() {
52            return Some(candidate.to_path_buf());
53        }
54        dir = candidate.parent();
55    }
56    None
57}
58
59/// [`JjVcs`] when a `.jj` directory sits beside `.git`, else [`GitVcs`].
60pub fn open(root: &Path) -> Result<Box<dyn Vcs>, VcsError> {
61    open_with_settings(root, &DiffSettings::default())
62}
63
64pub fn open_with_settings(root: &Path, settings: &DiffSettings) -> Result<Box<dyn Vcs>, VcsError> {
65    if root.join(".jj").is_dir() {
66        Ok(Box::new(JjVcs::open_with_settings(root, settings)?))
67    } else {
68        Ok(Box::new(GitVcs::open_with_settings(root, settings)?))
69    }
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75
76    #[test]
77    fn discovers_this_repository() {
78        let here = std::env::current_dir().expect("cwd");
79        let root = discover(&here).expect("repo root");
80        assert!(root.join(".git").exists());
81    }
82
83    #[test]
84    fn fails_outside_a_repository() {
85        // discover() walks every ancestor, so we rely on the tempdir having
86        // no repo above it
87        let dir = tempfile::tempdir().expect("tempdir");
88        assert!(matches!(discover(dir.path()), Err(RepoError::NotFound(_))));
89    }
90
91    #[test]
92    fn bare_repository_is_reported() {
93        let dir = tempfile::tempdir().expect("tempdir");
94        git2::Repository::init_bare(dir.path()).expect("init bare");
95        assert!(matches!(discover(dir.path()), Err(RepoError::Bare(_))));
96    }
97}