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