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}