Skip to main content

vcs_runner/
detect.rs

1use std::path::{Path, PathBuf};
2
3/// Which version control system is managing a directory.
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
5pub enum VcsBackend {
6    /// Pure git repository (.git/ only)
7    Git,
8    /// Pure jj repository (.jj/ only)
9    Jj,
10    /// Colocated: both .jj/ and .git/ exist
11    Colocated,
12}
13
14impl VcsBackend {
15    /// Whether this backend uses jj (true for both `Jj` and `Colocated`).
16    pub fn is_jj(self) -> bool {
17        matches!(self, Self::Jj | Self::Colocated)
18    }
19
20    /// Whether this backend has a git repository (true for both `Git` and `Colocated`).
21    pub fn has_git(self) -> bool {
22        matches!(self, Self::Git | Self::Colocated)
23    }
24}
25
26/// Detect the VCS backend for a path, without running any subprocesses.
27///
28/// Checks the path itself, then walks up ancestors. Returns the backend
29/// type and the repo root path.
30pub 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}