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}