1use 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
22pub 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
52pub 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 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}