Skip to main content

wt/git/
discover.rs

1//! Repository discovery and identity via `gix` (spec §4 read operations):
2//! discovery from a directory upward, bare detection, and the current worktree.
3//!
4//! Resolving the *primary* worktree root is done from `git rev-parse
5//! --git-common-dir` (in `open_session`) rather than from `gix`, because `gix`'s
6//! `common_dir()` does not resolve the linked-worktree indirection reliably — a
7//! fallback the spec §4 explicitly sanctions.
8
9use std::path::{Path, PathBuf};
10
11use crate::error::{Error, Result};
12
13/// A discovered Git repository, wrapping the `gix` handle.
14pub(crate) struct Repo {
15    inner: gix::Repository,
16}
17
18impl Repo {
19    /// Discovers the repository containing `start`, searching upward. Returns
20    /// [`Error::NotInRepo`] when `start` is not inside a repository.
21    pub(crate) fn discover(start: &Path) -> Result<Repo> {
22        match gix::discover(start) {
23            Ok(inner) => Ok(Repo { inner }),
24            Err(_) => Err(Error::NotInRepo),
25        }
26    }
27
28    /// Borrows the underlying `gix` repository for other read modules.
29    pub(crate) fn gix(&self) -> &gix::Repository {
30        &self.inner
31    }
32
33    /// Whether this is a bare repository (no working tree).
34    pub(crate) fn is_bare(&self) -> bool {
35        self.inner.workdir().is_none()
36    }
37
38    /// The working directory of the worktree `wt` was invoked from, or `None`
39    /// for a bare repository.
40    pub(crate) fn current_workdir(&self) -> Option<PathBuf> {
41        self.inner.workdir().map(Path::to_path_buf)
42    }
43
44    /// The git directory for the current worktree (`.git`, or
45    /// `.git/worktrees/<name>` for a linked worktree).
46    pub(crate) fn git_dir(&self) -> PathBuf {
47        self.inner.git_dir().to_path_buf()
48    }
49}
50
51#[cfg(test)]
52mod tests {
53    use super::*;
54    use crate::testutil::TestRepo;
55
56    #[test]
57    fn discovers_from_root_and_subdir() {
58        let repo = TestRepo::init();
59        let r = Repo::discover(repo.root()).unwrap();
60        assert!(!r.is_bare());
61        assert_eq!(canon(&r.current_workdir().unwrap()), canon(repo.root()));
62
63        // From a nested subdirectory, discovery still finds the same worktree.
64        let sub = repo.root().join("a/b");
65        std::fs::create_dir_all(&sub).unwrap();
66        let r2 = Repo::discover(&sub).unwrap();
67        assert_eq!(canon(&r2.current_workdir().unwrap()), canon(repo.root()));
68    }
69
70    #[test]
71    fn current_workdir_from_linked_worktree() {
72        let repo = TestRepo::init();
73        repo.add_worktree("feature/x", "../wt-x");
74        let linked = repo.root().parent().unwrap().join("wt-x");
75        let r = Repo::discover(&linked).unwrap();
76        assert_eq!(canon(&r.current_workdir().unwrap()), canon(&linked));
77    }
78
79    #[test]
80    fn not_in_repo_errors() {
81        let dir = tempfile::tempdir().unwrap();
82        assert!(matches!(Repo::discover(dir.path()), Err(Error::NotInRepo)));
83    }
84
85    #[test]
86    fn bare_repo_is_detected() {
87        let repo = TestRepo::init_bare();
88        let r = Repo::discover(repo.root()).unwrap();
89        assert!(r.is_bare());
90        assert!(r.current_workdir().is_none());
91    }
92
93    /// Canonicalizes a path so comparisons ignore `/private` symlink prefixes on
94    /// macOS temp dirs.
95    fn canon(p: &Path) -> PathBuf {
96        std::fs::canonicalize(p).unwrap_or_else(|_| p.to_path_buf())
97    }
98}