Skip to main content

rto_graph/
git.rs

1//! A thin `gix` wrapper exposing exactly the git facts the sync engine needs:
2//! the HEAD tree id, the blobs in that tree, and blob contents. Kept small so
3//! all `gix` coupling lives in one place.
4
5use std::path::Path;
6
7/// A blob in a tree: its repository-relative path and hex object id.
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct BlobRef {
10    /// Repository-relative path (forward-slash separated).
11    pub path: String,
12    /// Hex-encoded git blob object id.
13    pub oid: String,
14}
15
16/// Errors raised while reading from a git repository.
17#[derive(Debug, thiserror::Error)]
18pub enum GitError {
19    /// A `gix` operation failed (message preserved).
20    #[error("git error: {0}")]
21    Git(String),
22    /// A tree entry path was not valid UTF-8.
23    #[error("non-utf8 path in tree: {0:?}")]
24    NonUtf8Path(Vec<u8>),
25}
26
27fn ge<E: std::fmt::Display>(e: E) -> GitError {
28    GitError::Git(e.to_string())
29}
30
31/// A discovered git repository.
32pub struct Repo {
33    inner: gix::Repository,
34}
35
36impl Repo {
37    /// Discover the repository containing `path` (walking upwards to the `.git`).
38    ///
39    /// # Errors
40    /// Returns [`GitError::Git`] if no repository is found or it cannot be opened.
41    pub fn discover(path: &Path) -> Result<Self, GitError> {
42        Ok(Self {
43            inner: gix::discover(path).map_err(ge)?,
44        })
45    }
46
47    /// The repository's *common* git directory. The cache lives under here so it
48    /// is shared across linked worktrees (which each have their own git dir).
49    #[must_use]
50    pub fn common_dir(&self) -> &Path {
51        self.inner.common_dir()
52    }
53
54    /// This worktree's git directory (per-worktree; the graph DB lives here).
55    #[must_use]
56    pub fn git_dir(&self) -> &Path {
57        self.inner.git_dir()
58    }
59
60    /// The working directory, if this is not a bare repository. The dirty
61    /// overlay reads uncommitted file contents from here.
62    #[must_use]
63    pub fn workdir(&self) -> Option<&Path> {
64        self.inner.workdir()
65    }
66
67    /// The hex git blob object id that `bytes` would have, without writing
68    /// anything. Used to detect whether a working-copy file differs from the
69    /// committed blob (same content ⇒ same id).
70    ///
71    /// # Errors
72    /// Returns [`GitError::Git`] if hashing fails.
73    pub fn blob_oid(&self, bytes: &[u8]) -> Result<String, GitError> {
74        let id = gix::objs::compute_hash(self.inner.object_hash(), gix::objs::Kind::Blob, bytes)
75            .map_err(ge)?;
76        Ok(id.to_hex().to_string())
77    }
78
79    /// Hex object id of the tree at `HEAD`.
80    ///
81    /// # Errors
82    /// Returns [`GitError::Git`] if `HEAD` cannot be resolved to a tree.
83    pub fn head_tree_id(&self) -> Result<String, GitError> {
84        let tree = self.inner.head_tree().map_err(ge)?;
85        Ok(tree.id().to_hex().to_string())
86    }
87
88    /// Every blob reachable from the `HEAD` tree, with full paths.
89    ///
90    /// # Errors
91    /// Returns [`GitError`] if the tree cannot be traversed or a path is not
92    /// valid UTF-8.
93    pub fn walk_blobs(&self) -> Result<Vec<BlobRef>, GitError> {
94        let tree = self.inner.head_tree().map_err(ge)?;
95        let mut recorder = gix::traverse::tree::Recorder::default();
96        tree.traverse().breadthfirst(&mut recorder).map_err(ge)?;
97
98        let mut out = Vec::new();
99        for entry in recorder.records {
100            if !entry.mode.is_blob() {
101                continue;
102            }
103            let path = String::from_utf8(entry.filepath.into())
104                .map_err(|e| GitError::NonUtf8Path(e.into_bytes()))?;
105            out.push(BlobRef {
106                path,
107                oid: entry.oid.to_hex().to_string(),
108            });
109        }
110        Ok(out)
111    }
112
113    /// Read the bytes of the blob with hex object id `oid`.
114    ///
115    /// # Errors
116    /// Returns [`GitError::Git`] if the id is malformed or the object is absent.
117    pub fn read_blob(&self, oid: &str) -> Result<Vec<u8>, GitError> {
118        let id = gix::ObjectId::from_hex(oid.as_bytes()).map_err(ge)?;
119        // `detach()` moves the owned data out without cloning; `Object` itself
120        // implements `Drop`, so the bare field cannot be moved out directly.
121        Ok(self.inner.find_object(id).map_err(ge)?.detach().data)
122    }
123}