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