1use std::path::{Path, PathBuf};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
5pub enum VcsBackend {
6 Git,
8 Jj,
10 Colocated,
12}
13
14impl VcsBackend {
15 pub fn is_jj(self) -> bool {
17 matches!(self, Self::Jj | Self::Colocated)
18 }
19
20 pub fn has_git(self) -> bool {
22 matches!(self, Self::Git | Self::Colocated)
23 }
24}
25
26pub fn detect_vcs(path: &Path) -> anyhow::Result<(VcsBackend, PathBuf)> {
31 for dir in path.ancestors() {
32 let has_jj = dir.join(".jj").is_dir();
33 let has_git = dir.join(".git").exists();
34
35 match (has_jj, has_git) {
36 (true, true) => return Ok((VcsBackend::Colocated, dir.to_path_buf())),
37 (true, false) => return Ok((VcsBackend::Jj, dir.to_path_buf())),
38 (false, true) => return Ok((VcsBackend::Git, dir.to_path_buf())),
39 (false, false) => continue,
40 }
41 }
42 anyhow::bail!("not a git or jj repository")
43}
44
45#[cfg(test)]
46mod tests {
47 use super::*;
48 use std::fs;
49
50 #[test]
51 fn detect_empty_dir() {
52 let tmp = tempfile::tempdir().expect("tempdir");
53 assert!(detect_vcs(tmp.path()).is_err());
54 }
55
56 #[test]
57 fn detect_jj_only() {
58 let tmp = tempfile::tempdir().expect("tempdir");
59 fs::create_dir(tmp.path().join(".jj")).expect("mkdir .jj");
60 let (backend, root) = detect_vcs(tmp.path()).expect("should detect");
61 assert_eq!(backend, VcsBackend::Jj);
62 assert_eq!(root, tmp.path());
63 assert!(backend.is_jj());
64 assert!(!backend.has_git());
65 }
66
67 #[test]
68 fn detect_git_only() {
69 let tmp = tempfile::tempdir().expect("tempdir");
70 fs::create_dir(tmp.path().join(".git")).expect("mkdir .git");
71 let (backend, root) = detect_vcs(tmp.path()).expect("should detect");
72 assert_eq!(backend, VcsBackend::Git);
73 assert_eq!(root, tmp.path());
74 assert!(!backend.is_jj());
75 assert!(backend.has_git());
76 }
77
78 #[test]
79 fn detect_colocated() {
80 let tmp = tempfile::tempdir().expect("tempdir");
81 fs::create_dir(tmp.path().join(".jj")).expect("mkdir .jj");
82 fs::create_dir(tmp.path().join(".git")).expect("mkdir .git");
83 let (backend, _) = detect_vcs(tmp.path()).expect("should detect");
84 assert_eq!(backend, VcsBackend::Colocated);
85 assert!(backend.is_jj());
86 assert!(backend.has_git());
87 }
88
89 #[test]
90 fn detect_ancestor() {
91 let tmp = tempfile::tempdir().expect("tempdir");
92 fs::create_dir(tmp.path().join(".jj")).expect("mkdir .jj");
93 let child = tmp.path().join("subdir");
94 fs::create_dir(&child).expect("mkdir subdir");
95 let (backend, root) = detect_vcs(&child).expect("should detect");
96 assert_eq!(backend, VcsBackend::Jj);
97 assert_eq!(root, tmp.path());
98 }
99
100 #[test]
101 fn detect_git_worktree_file() {
102 let tmp = tempfile::tempdir().expect("tempdir");
103 fs::write(tmp.path().join(".git"), "gitdir: /other/.git/worktrees/wt")
104 .expect("write .git file");
105 let (backend, _) = detect_vcs(tmp.path()).expect("should detect");
106 assert_eq!(backend, VcsBackend::Git);
107 }
108}