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