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    /// Hex object id of the tree at `HEAD`.
61    ///
62    /// # Errors
63    /// Returns [`GitError::Git`] if `HEAD` cannot be resolved to a tree.
64    pub fn head_tree_id(&self) -> Result<String, GitError> {
65        let tree = self.inner.head_tree().map_err(ge)?;
66        Ok(tree.id().to_hex().to_string())
67    }
68
69    /// Every blob reachable from the `HEAD` tree, with full paths.
70    ///
71    /// # Errors
72    /// Returns [`GitError`] if the tree cannot be traversed or a path is not
73    /// valid UTF-8.
74    pub fn walk_blobs(&self) -> Result<Vec<BlobRef>, GitError> {
75        let tree = self.inner.head_tree().map_err(ge)?;
76        let mut recorder = gix::traverse::tree::Recorder::default();
77        tree.traverse().breadthfirst(&mut recorder).map_err(ge)?;
78
79        let mut out = Vec::new();
80        for entry in recorder.records {
81            if !entry.mode.is_blob() {
82                continue;
83            }
84            let path = String::from_utf8(entry.filepath.into())
85                .map_err(|e| GitError::NonUtf8Path(e.into_bytes()))?;
86            out.push(BlobRef {
87                path,
88                oid: entry.oid.to_hex().to_string(),
89            });
90        }
91        Ok(out)
92    }
93
94    /// Read the bytes of the blob with hex object id `oid`.
95    ///
96    /// # Errors
97    /// Returns [`GitError::Git`] if the id is malformed or the object is absent.
98    pub fn read_blob(&self, oid: &str) -> Result<Vec<u8>, GitError> {
99        let id = gix::ObjectId::from_hex(oid.as_bytes()).map_err(ge)?;
100        // `detach()` moves the owned data out without cloning; `Object` itself
101        // implements `Drop`, so the bare field cannot be moved out directly.
102        Ok(self.inner.find_object(id).map_err(ge)?.detach().data)
103    }
104}