git-ents.gitmain
⌘K
foforge
object.rs177 lines · 6.5 KB · rusthistorycomment on this file
1//! Minimal commit reading over `gix_object::Find` — the only object
2//! access the gate performs (`arch.no-object-store-trait`: gitoxide's
3//! traits are the object seam; no private store trait).
4
5use gix_hash::ObjectId;
6use gix_object::{CommitRef, Find, Kind, TreeRef};
7
8use crate::error::{Error, Result};
9
10/// The decoded pieces of one commit the gate judges or walks.
11#[derive(Debug, Clone)]
12pub(crate) struct CommitData {
13 /// The raw commit bytes as stored — what a signature covers (minus
14 /// the `gpgsig` header itself).
15 pub raw: Vec<u8>,
16 /// The tree the commit records.
17 pub tree: ObjectId,
18 /// Parents, in order.
19 pub parents: Vec<ObjectId>,
20}
21
22/// Read `oid` and decode it as a commit; `Ok(None)` when the object
23/// exists but is not a commit (the caller turns that into a refusal, not
24/// an error).
25pub(crate) fn read_commit(objects: &dyn Find, oid: ObjectId) -> Result<Option<CommitData>> {
26 let mut buf = Vec::new();
27 let data = objects
28 .try_find(&oid, &mut buf)
29 .map_err(|source| Error::Object { oid, source })?
30 .ok_or(Error::Missing { oid })?;
31 if data.kind != Kind::Commit {
32 return Ok(None);
33 }
34 let raw = data.data.to_vec();
35 let (tree, parents) = decode_commit(&raw, oid)?;
36 Ok(Some(CommitData { tree, parents, raw }))
37}
38
39fn decode_commit(raw: &[u8], oid: ObjectId) -> Result<(ObjectId, Vec<ObjectId>)> {
40 let commit = CommitRef::from_bytes(raw, oid.kind()).map_err(|e| Error::Decode {
41 oid,
42 detail: e.to_string(),
43 })?;
44 Ok((commit.tree(), commit.parents().collect()))
45}
46
47/// Like [`read_commit`], but a non-commit is an [`Error::Decode`] —
48/// for walks where every node must be a commit.
49pub(crate) fn expect_commit(objects: &dyn Find, oid: ObjectId) -> Result<CommitData> {
50 read_commit(objects, oid)?.ok_or(Error::Decode {
51 oid,
52 detail: "expected a commit".into(),
53 })
54}
55
56/// The raw bytes of the top-level tree entry named `entry`, or `None`
57/// when `tree` has no such entry (`gate.identity-binding`: the gate reads
58/// a binding field by tree-entry name, generically — it never decodes a
59/// non-kernel entity type to recompute a refname, so it can bind a
60/// review's `target` or a toolchain's `name` without depending on the
61/// crate that owns that struct).
62///
63/// A named struct field serializes to a tree entry keyed by the field
64/// name (`facet-git-tree`'s struct-to-tree mapping); a scalar field's
65/// blob is its textual form, and a raw `[u8; 20]` oid field's blob is the
66/// 20 raw bytes. This reads exactly that blob.
67pub(crate) fn read_tree_entry(
68 objects: &dyn Find,
69 tree: ObjectId,
70 entry: &str,
71) -> Result<Option<Vec<u8>>> {
72 let mut buf = Vec::new();
73 let data = objects
74 .try_find(&tree, &mut buf)
75 .map_err(|source| Error::Object { oid: tree, source })?
76 .ok_or(Error::Missing { oid: tree })?;
77 if data.kind != Kind::Tree {
78 return Ok(None);
79 }
80 let tree = TreeRef::from_bytes(data.data, tree.kind()).map_err(|e| Error::Decode {
81 oid: tree,
82 detail: e.to_string(),
83 })?;
84 let child = tree
85 .entries
86 .iter()
87 .find(|e| e.filename == entry.as_bytes())
88 .map(|e| e.oid.to_owned());
89 let Some(child) = child else {
90 return Ok(None);
91 };
92 let mut blob_buf = Vec::new();
93 let blob = objects
94 .try_find(&child, &mut blob_buf)
95 .map_err(|source| Error::Object { oid: child, source })?
96 .ok_or(Error::Missing { oid: child })?;
97 Ok(Some(blob.data.to_vec()))
98}
99
100/// The names of every top-level entry in `tree`, for the strict-decode
101/// disjointness check (`gate.identity-binding`: a genesis tree with an
102/// entry that is not one of its entity type's fields refuses).
103pub(crate) fn tree_entry_names(objects: &dyn Find, tree: ObjectId) -> Result<Vec<String>> {
104 let mut buf = Vec::new();
105 let data = objects
106 .try_find(&tree, &mut buf)
107 .map_err(|source| Error::Object { oid: tree, source })?
108 .ok_or(Error::Missing { oid: tree })?;
109 if data.kind != Kind::Tree {
110 return Ok(Vec::new());
111 }
112 let parsed = TreeRef::from_bytes(data.data, tree.kind()).map_err(|e| Error::Decode {
113 oid: tree,
114 detail: e.to_string(),
115 })?;
116 Ok(parsed
117 .entries
118 .iter()
119 .map(|e| String::from_utf8_lossy(e.filename).into_owned())
120 .collect())
121}
122
123/// Every parentless commit reachable from `tip` by parent edges — the
124/// genesis roots of a hash-identified entity's history
125/// (`meta-ref.identity-binding`'s all-roots rule, `gate.identity-binding`).
126///
127/// Replaying a signed mutation commit as a fresh genesis is refused
128/// because the walk reaches the original genesis (that mutation's own
129/// parentless ancestor), not the replayed commit — a creation-time-only
130/// check could not tell them apart. The walk descends through *every*
131/// parent, so it holds across the merge commits divergence resolution and
132/// adoption create (`gate.same-actor-divergence`, `gate.adoption-merge`).
133/// A missing or non-commit object simply ends its path, exactly as
134/// [`descends_from`] treats one: at pre-flight, history below the last
135/// fetch may be shallow, and this is an advisory prediction there.
136pub(crate) fn all_roots(objects: &dyn Find, tip: ObjectId) -> Result<Vec<ObjectId>> {
137 let mut queue = vec![tip];
138 let mut seen = std::collections::HashSet::new();
139 let mut roots = Vec::new();
140 while let Some(oid) = queue.pop() {
141 if !seen.insert(oid) {
142 continue;
143 }
144 match read_commit(objects, oid)? {
145 Some(commit) if commit.parents.is_empty() => roots.push(oid),
146 Some(commit) => queue.extend(commit.parents),
147 None => {}
148 }
149 }
150 Ok(roots)
151}
152
153/// Whether `ancestor` is reachable from `descendant` by parent edges
154/// (inclusive: a commit descends from itself) — the DAG sense of
155/// `gate.fast-forward`.
156pub(crate) fn descends_from(
157 objects: &dyn Find,
158 descendant: ObjectId,
159 ancestor: ObjectId,
160) -> Result<bool> {
161 let mut queue = vec![descendant];
162 let mut seen = std::collections::HashSet::new();
163 while let Some(oid) = queue.pop() {
164 if oid == ancestor {
165 return Ok(true);
166 }
167 if !seen.insert(oid) {
168 continue;
169 }
170 // A missing or non-commit ancestor object simply ends this path:
171 // at pre-flight, history below the last fetch may be shallow.
172 if let Some(commit) = read_commit(objects, oid).ok().flatten() {
173 queue.extend(commit.parents);
174 }
175 }
176 Ok(false)
177}