1use git2::{Error, Oid, Repository};
7
8#[derive(Debug, Clone)]
10pub struct LedgerEntry {
11 pub id: String,
13 pub ref_: String,
15 pub commit: Oid,
17 pub fields: Vec<(String, Vec<u8>)>,
19}
20
21pub enum IdStrategy<'a> {
23 Sequential,
25 ContentAddressed(&'a [u8]),
27 CallerProvided(&'a str),
29}
30
31pub enum Mutation<'a> {
33 Set(&'a str, &'a [u8]),
35 Delete(&'a str),
37}
38
39pub trait Ledger {
41 fn create(
43 &self,
44 ref_prefix: &str,
45 strategy: &IdStrategy<'_>,
46 fields: &[(&str, &[u8])],
47 message: &str,
48 ) -> Result<LedgerEntry, Error>;
49
50 fn read(&self, ref_name: &str) -> Result<LedgerEntry, Error>;
52
53 fn update(
55 &self,
56 ref_name: &str,
57 mutations: &[Mutation<'_>],
58 message: &str,
59 ) -> Result<LedgerEntry, Error>;
60
61 fn list(&self, ref_prefix: &str) -> Result<Vec<String>, Error>;
63
64 fn history(&self, ref_name: &str) -> Result<Vec<Oid>, Error>;
66}
67
68fn insert_nested(
74 repo: &Repository,
75 builder: &mut git2::TreeBuilder<'_>,
76 components: &[&str],
77 blob_oid: Oid,
78) -> Result<(), Error> {
79 match components {
80 [leaf] => {
81 builder.insert(leaf, blob_oid, 0o100644)?;
82 }
83 [head, rest @ ..] => {
84 let mut sub_builder = if let Some(existing) = builder.get(head)? {
85 let existing_tree = repo.find_tree(existing.id())?;
86 repo.treebuilder(Some(&existing_tree))?
87 } else {
88 repo.treebuilder(None)?
89 };
90 insert_nested(repo, &mut sub_builder, rest, blob_oid)?;
91 let sub_tree = sub_builder.write()?;
92 builder.insert(head, sub_tree, 0o040000)?;
93 }
94 [] => {}
95 }
96 Ok(())
97}
98
99fn remove_nested(
102 repo: &Repository,
103 builder: &mut git2::TreeBuilder<'_>,
104 components: &[&str],
105) -> Result<bool, Error> {
106 match components {
107 [leaf] => {
108 let _ = builder.remove(leaf);
109 }
110 [head, rest @ ..] => {
111 let existing_tree_id = builder
112 .get(head)?
113 .filter(|e| e.kind() == Some(git2::ObjectType::Tree))
114 .map(|e| e.id());
115 if let Some(tree_id) = existing_tree_id {
116 let et = repo.find_tree(tree_id)?;
117 let mut sub_builder = repo.treebuilder(Some(&et))?;
118 let empty = remove_nested(repo, &mut sub_builder, rest)?;
119 if empty {
120 let _ = builder.remove(head);
121 } else {
122 let sub_tree = sub_builder.write()?;
123 builder.insert(head, sub_tree, 0o040000)?;
124 }
125 }
126 }
127 [] => {}
128 }
129 Ok(builder.is_empty())
130}
131
132fn build_fields_tree(repo: &Repository, fields: &[(&str, &[u8])]) -> Result<Oid, Error> {
134 let mut builder = repo.treebuilder(None)?;
135 for (name, value) in fields {
136 let blob_oid = repo.blob(value)?;
137 let components: Vec<&str> = name.split('/').collect();
138 insert_nested(repo, &mut builder, &components, blob_oid)?;
139 }
140 builder.write()
141}
142
143fn read_fields(
145 repo: &Repository,
146 tree: &git2::Tree<'_>,
147 prefix: &str,
148) -> Result<Vec<(String, Vec<u8>)>, Error> {
149 let mut fields = Vec::new();
150 for entry in tree.iter() {
151 let name = entry.name().unwrap_or("").to_string();
152 let path = if prefix.is_empty() {
153 name.clone()
154 } else {
155 format!("{}/{}", prefix, name)
156 };
157 match entry.kind() {
158 Some(git2::ObjectType::Blob) => {
159 let blob = repo.find_blob(entry.id())?;
160 fields.push((path, blob.content().to_vec()));
161 }
162 Some(git2::ObjectType::Tree) => {
163 let subtree = repo.find_tree(entry.id())?;
164 fields.extend(read_fields(repo, &subtree, &path)?);
165 }
166 _ => {}
167 }
168 }
169 Ok(fields)
170}
171
172fn id_from_ref(ref_name: &str, ref_prefix: &str) -> String {
174 let prefix = if ref_prefix.ends_with('/') {
175 ref_prefix.to_string()
176 } else {
177 format!("{}/", ref_prefix)
178 };
179 ref_name
180 .strip_prefix(&prefix)
181 .unwrap_or(ref_name)
182 .to_string()
183}
184
185fn next_sequential_id(repo: &Repository, ref_prefix: &str) -> Result<u64, Error> {
187 let pattern = if ref_prefix.ends_with('/') {
188 format!("{}*", ref_prefix)
189 } else {
190 format!("{}/*", ref_prefix)
191 };
192 let refs = repo.references_glob(&pattern)?;
193 let mut max_id: u64 = 0;
194 for reference in refs {
195 let reference = reference?;
196 if let Some(name) = reference.name() {
197 let id_str = id_from_ref(name, ref_prefix);
198 if let Ok(n) = id_str.parse::<u64>() {
199 max_id = max_id.max(n);
200 }
201 }
202 }
203 Ok(max_id + 1)
204}
205
206impl Ledger for Repository {
211 fn create(
212 &self,
213 ref_prefix: &str,
214 strategy: &IdStrategy<'_>,
215 fields: &[(&str, &[u8])],
216 message: &str,
217 ) -> Result<LedgerEntry, Error> {
218 let id = match strategy {
219 IdStrategy::Sequential => {
220 let next = next_sequential_id(self, ref_prefix)?;
221 next.to_string()
222 }
223 IdStrategy::ContentAddressed(bytes) => {
224 let oid = self.blob(bytes)?;
225 oid.to_string()
226 }
227 IdStrategy::CallerProvided(s) => s.to_string(),
228 };
229
230 let ref_name = if ref_prefix.ends_with('/') {
231 format!("{}{}", ref_prefix, id)
232 } else {
233 format!("{}/{}", ref_prefix, id)
234 };
235
236 if self.find_reference(&ref_name).is_ok() {
238 return Err(Error::from_str(&format!(
239 "record already exists: {}",
240 ref_name
241 )));
242 }
243
244 let tree_oid = build_fields_tree(self, fields)?;
245 let tree = self.find_tree(tree_oid)?;
246 let sig = self.signature()?;
247
248 let commit_oid = self.commit(
249 Some(&ref_name),
250 &sig,
251 &sig,
252 message,
253 &tree,
254 &[], )?;
256
257 let fields = read_fields(self, &tree, "")?;
258 let id = ref_name.rsplit('/').next().unwrap_or(&ref_name).to_string();
259
260 Ok(LedgerEntry {
261 id,
262 ref_: ref_name,
263 commit: commit_oid,
264 fields,
265 })
266 }
267
268 fn read(&self, ref_name: &str) -> Result<LedgerEntry, Error> {
269 let reference = self.find_reference(ref_name)?;
270 let commit = reference.peel_to_commit()?;
271 let tree = commit.tree()?;
272 let fields = read_fields(self, &tree, "")?;
273
274 let id = ref_name.rsplit('/').next().unwrap_or(ref_name).to_string();
276
277 Ok(LedgerEntry {
278 id,
279 ref_: ref_name.to_string(),
280 commit: commit.id(),
281 fields,
282 })
283 }
284
285 fn update(
286 &self,
287 ref_name: &str,
288 mutations: &[Mutation<'_>],
289 message: &str,
290 ) -> Result<LedgerEntry, Error> {
291 let reference = self.find_reference(ref_name)?;
292 let parent_commit = reference.peel_to_commit()?;
293 let existing_tree = parent_commit.tree()?;
294
295 let mut builder = self.treebuilder(Some(&existing_tree))?;
296
297 for mutation in mutations {
298 match mutation {
299 Mutation::Set(name, value) => {
300 let blob_oid = self.blob(value)?;
301 let components: Vec<&str> = name.split('/').collect();
302 insert_nested(self, &mut builder, &components, blob_oid)?;
303 }
304 Mutation::Delete(name) => {
305 let components: Vec<&str> = name.split('/').collect();
306 remove_nested(self, &mut builder, &components)?;
307 }
308 }
309 }
310
311 let tree_oid = builder.write()?;
312 let tree = self.find_tree(tree_oid)?;
313 let sig = self.signature()?;
314
315 let commit_oid = self.commit(
316 Some(ref_name),
317 &sig,
318 &sig,
319 message,
320 &tree,
321 &[&parent_commit],
322 )?;
323
324 let fields = read_fields(self, &tree, "")?;
325 let id = ref_name.rsplit('/').next().unwrap_or(ref_name).to_string();
326
327 Ok(LedgerEntry {
328 id,
329 ref_: ref_name.to_string(),
330 commit: commit_oid,
331 fields,
332 })
333 }
334
335 fn list(&self, ref_prefix: &str) -> Result<Vec<String>, Error> {
336 let pattern = if ref_prefix.ends_with('/') {
337 format!("{}*", ref_prefix)
338 } else {
339 format!("{}/*", ref_prefix)
340 };
341 let refs = self.references_glob(&pattern)?;
342 let mut ids = Vec::new();
343 for reference in refs {
344 let reference = reference?;
345 if let Some(name) = reference.name() {
346 ids.push(id_from_ref(name, ref_prefix));
347 }
348 }
349 ids.sort();
350 Ok(ids)
351 }
352
353 fn history(&self, ref_name: &str) -> Result<Vec<Oid>, Error> {
354 let reference = self.find_reference(ref_name)?;
355 let commit = reference.peel_to_commit()?;
356
357 let mut oids = Vec::new();
358 let mut current = Some(commit);
359 while let Some(c) = current {
360 oids.push(c.id());
361 current = c.parent(0).ok();
362 }
363 Ok(oids)
364 }
365}
366
367#[cfg(test)]
368mod tests;