1use git_filter_tree::FilterTree as _;
2use git2::{Error, ErrorCode, Oid, Repository};
3
4#[derive(Debug, Clone)]
6pub struct MetadataOptions {
7 pub shard_level: u8,
10 pub force: bool,
12}
13
14impl Default for MetadataOptions {
15 fn default() -> Self {
16 Self {
17 shard_level: 1,
18 force: false,
19 }
20 }
21}
22
23#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct MetadataEntry {
26 pub path: String,
28 pub content: Option<Vec<u8>>,
30 pub oid: Oid,
32 pub is_tree: bool,
34}
35
36pub trait MetadataIndex {
42 fn metadata_list(&self, ref_name: &str) -> Result<Vec<(Oid, Oid)>, Error>;
45
46 fn metadata_get(&self, ref_name: &str, target: &Oid) -> Result<Option<Oid>, Error>;
49
50 fn metadata(
55 &self,
56 ref_name: &str,
57 target: &Oid,
58 tree: &Oid,
59 opts: &MetadataOptions,
60 ) -> Result<Oid, Error>;
61
62 fn metadata_commit(&self, ref_name: &str, root: Oid, message: &str) -> Result<Oid, Error>;
66
67 #[deprecated(since = "0.1.0", note = "use `metadata` + `metadata_commit` instead")]
74 fn metadata_set(
75 &self,
76 ref_name: &str,
77 target: &Oid,
78 tree: &Oid,
79 opts: &MetadataOptions,
80 ) -> Result<Oid, Error> {
81 #[allow(deprecated)]
82 let new_root = self.metadata(ref_name, target, tree, opts)?;
83 let msg = format!("metadata: set {} -> {}", target, tree);
84 self.metadata_commit(ref_name, new_root, &msg)?;
85 Ok(new_root)
86 }
87
88 fn metadata_show(&self, ref_name: &str, target: &Oid) -> Result<Vec<MetadataEntry>, Error>;
91
92 fn metadata_add(
99 &self,
100 ref_name: &str,
101 target: &Oid,
102 path: &str,
103 content: Option<&[u8]>,
104 opts: &MetadataOptions,
105 ) -> Result<Oid, Error>;
106
107 fn metadata_remove_paths(
113 &self,
114 ref_name: &str,
115 target: &Oid,
116 patterns: &[&str],
117 keep: bool,
118 ) -> Result<bool, Error>;
119
120 fn metadata_remove(&self, ref_name: &str, target: &Oid) -> Result<bool, Error>;
123
124 fn metadata_copy(
128 &self,
129 ref_name: &str,
130 from: &Oid,
131 to: &Oid,
132 opts: &MetadataOptions,
133 ) -> Result<Oid, Error>;
134
135 fn metadata_prune(&self, ref_name: &str, dry_run: bool) -> Result<Vec<Oid>, Error>;
138
139 fn metadata_get_ref(&self, ref_name: &str) -> String;
141
142 fn link(
147 &self,
148 ref_name: &str,
149 a: &str,
150 b: &str,
151 forward: &str,
152 reverse: &str,
153 meta: Option<&[u8]>,
154 ) -> Result<Oid, Error>;
155
156 fn unlink(
160 &self,
161 ref_name: &str,
162 a: &str,
163 b: &str,
164 forward: &str,
165 reverse: &str,
166 ) -> Result<Oid, Error>;
167
168 fn linked(
172 &self,
173 ref_name: &str,
174 key: &str,
175 relation: Option<&str>,
176 ) -> Result<Vec<(String, String)>, Error>;
177
178 fn is_linked(&self, ref_name: &str, a: &str, b: &str, forward: &str) -> Result<bool, Error>;
180}
181
182const MAX_SHARD_LEVEL: u8 = 19;
189
190fn shard_oid(oid: &Oid, shard_level: u8) -> Result<(Vec<String>, String), Error> {
194 if shard_level > MAX_SHARD_LEVEL {
195 return Err(Error::from_str(&format!(
196 "shard_level {} exceeds maximum of {}",
197 shard_level, MAX_SHARD_LEVEL
198 )));
199 }
200 let hex = oid.to_string();
201 let mut segments = Vec::with_capacity(shard_level as usize);
202 let mut pos = 0;
203 for _ in 0..shard_level {
204 segments.push(hex[pos..pos + 2].to_string());
205 pos += 2;
206 }
207 let leaf = hex[pos..].to_string();
208 Ok((segments, leaf))
209}
210
211fn resolve_root_tree<'r>(
213 repo: &'r Repository,
214 ref_name: &str,
215) -> Result<Option<git2::Tree<'r>>, Error> {
216 match repo.find_reference(ref_name) {
217 Ok(reference) => {
218 let commit = reference.peel_to_commit()?;
219 let tree = commit.tree()?;
220 Ok(Some(tree))
221 }
222 Err(e) if e.code() == ErrorCode::NotFound => Ok(None),
223 Err(e) => Err(e),
224 }
225}
226
227fn walk_tree<'a>(
229 repo: &'a Repository,
230 root: &git2::Tree<'a>,
231 segments: &[String],
232) -> Result<Option<git2::Tree<'a>>, Error> {
233 let mut current = root.clone();
234 for seg in segments {
235 let id = match current.get_name(seg) {
236 Some(entry) => entry.id(),
237 None => return Ok(None),
238 };
239 current = repo.find_tree(id)?;
240 }
241 Ok(Some(current))
242}
243
244fn is_fanout_segment(name: &str) -> bool {
246 name.len() == 2 && name.bytes().all(|b| b.is_ascii_hexdigit())
247}
248
249fn collect_entries(
251 repo: &Repository,
252 tree: &git2::Tree<'_>,
253 prefix: &str,
254) -> Result<Vec<(Oid, Oid)>, Error> {
255 let mut results = Vec::new();
256 for entry in tree.iter() {
257 let name = entry.name().unwrap_or("");
258 if entry.kind() != Some(git2::ObjectType::Tree) {
259 continue;
260 }
261 let full = format!("{prefix}{name}");
262 if is_fanout_segment(name) {
263 let subtree = repo.find_tree(entry.id())?;
264 results.extend(collect_entries(repo, &subtree, &full)?);
265 } else if let Ok(oid) = Oid::from_str(&full)
266 && oid.to_string() == full
267 {
268 results.push((oid, entry.id()));
269 }
270 }
271 Ok(results)
272}
273
274fn detect_fanout(
276 repo: &Repository,
277 root: &git2::Tree<'_>,
278 target: &Oid,
279) -> Result<Option<(Vec<String>, String, Oid)>, Error> {
280 let hex = target.to_string();
281 let max_depth = hex.len() / 2;
282 for depth in 0..max_depth {
283 let prefix_len = depth * 2;
284 let segments: Vec<String> = (0..depth)
285 .map(|i| hex[i * 2..i * 2 + 2].to_string())
286 .collect();
287 let leaf = &hex[prefix_len..];
288
289 if let Some(subtree) = walk_tree(repo, root, &segments)?
290 && let Some(entry) = subtree.get_name(leaf)
291 && entry.kind() == Some(git2::ObjectType::Tree)
292 {
293 return Ok(Some((segments, leaf.to_string(), entry.id())));
294 }
295 }
296 Ok(None)
297}
298
299fn build_fanout(
301 repo: &Repository,
302 existing_root: Option<&git2::Tree<'_>>,
303 segments: &[String],
304 leaf: &str,
305 value_tree_oid: &Oid,
306) -> Result<Oid, Error> {
307 let mut existing_subtrees: Vec<Option<git2::Tree<'_>>> = Vec::new();
308 if let Some(root) = existing_root {
309 let mut current = Some(root.clone());
310 existing_subtrees.push(current.clone());
311 for seg in segments {
312 current = match ¤t {
313 Some(t) => match t.get_name(seg) {
314 Some(e) => Some(repo.find_tree(e.id())?),
315 None => None,
316 },
317 None => None,
318 };
319 existing_subtrees.push(current.clone());
320 }
321 } else {
322 for _ in 0..=segments.len() {
323 existing_subtrees.push(None);
324 }
325 }
326
327 let deepest_existing = existing_subtrees.last().and_then(|o| o.as_ref());
328 let mut builder = repo.treebuilder(deepest_existing)?;
329 builder.insert(leaf, *value_tree_oid, 0o040000)?;
330 let mut child_oid = builder.write()?;
331
332 for (i, seg) in segments.iter().enumerate().rev() {
333 let parent_existing = existing_subtrees[i].as_ref();
334 let mut builder = repo.treebuilder(parent_existing)?;
335 builder.insert(seg, child_oid, 0o040000)?;
336 child_oid = builder.write()?;
337 }
338
339 Ok(child_oid)
340}
341
342enum RemoveResult {
344 NotFound,
345 Empty,
346 Removed(Oid),
347}
348
349fn build_fanout_remove(
351 repo: &Repository,
352 root: &git2::Tree<'_>,
353 segments: &[String],
354 leaf: &str,
355) -> Result<RemoveResult, Error> {
356 let mut chain_oids: Vec<Oid> = vec![root.id()];
357 {
358 let mut current = root.clone();
359 for seg in segments {
360 let id = match current.get_name(seg) {
361 Some(e) => e.id(),
362 None => return Ok(RemoveResult::NotFound),
363 };
364 chain_oids.push(id);
365 current = repo.find_tree(id)?;
366 }
367 }
368
369 let deepest = repo.find_tree(*chain_oids.last().unwrap())?;
370 let mut builder = repo.treebuilder(Some(&deepest))?;
371 if builder.get(leaf)?.is_none() {
372 return Ok(RemoveResult::NotFound);
373 }
374 builder.remove(leaf)?;
375
376 let mut child_oid = if builder.is_empty() {
377 None
378 } else {
379 Some(builder.write()?)
380 };
381
382 for (i, seg) in segments.iter().enumerate().rev() {
383 let parent = repo.find_tree(chain_oids[i])?;
384 let mut builder = repo.treebuilder(Some(&parent))?;
385 match child_oid {
386 Some(oid) => {
387 builder.insert(seg, oid, 0o040000)?;
388 }
389 None => {
390 builder.remove(seg)?;
391 }
392 }
393 child_oid = if builder.is_empty() {
394 None
395 } else {
396 Some(builder.write()?)
397 };
398 }
399
400 match child_oid {
401 Some(oid) => Ok(RemoveResult::Removed(oid)),
402 None => Ok(RemoveResult::Empty),
403 }
404}
405
406fn commit_index(
408 repo: &Repository,
409 ref_name: &str,
410 tree_oid: Oid,
411 message: &str,
412) -> Result<Oid, Error> {
413 let tree = repo.find_tree(tree_oid)?;
414 let sig = repo.signature()?;
415
416 let parent = match repo.find_reference(ref_name) {
417 Ok(r) => Some(r.peel_to_commit()?),
418 Err(e) if e.code() == ErrorCode::NotFound => None,
419 Err(e) => return Err(e),
420 };
421
422 let parents: Vec<&git2::Commit<'_>> = parent.iter().collect();
423 let commit_oid = repo.commit(Some(ref_name), &sig, &sig, message, &tree, &parents)?;
424 Ok(commit_oid)
425}
426
427fn collect_tree_entries(
429 repo: &Repository,
430 tree: &git2::Tree<'_>,
431 prefix: &str,
432) -> Result<Vec<MetadataEntry>, Error> {
433 let mut results = Vec::new();
434 for entry in tree.iter() {
435 let name = entry.name().unwrap_or("");
436 let path = if prefix.is_empty() {
437 name.to_string()
438 } else {
439 format!("{prefix}/{name}")
440 };
441 match entry.kind() {
442 Some(git2::ObjectType::Tree) => {
443 let subtree = repo.find_tree(entry.id())?;
444 results.extend(collect_tree_entries(repo, &subtree, &path)?);
445 }
446 Some(git2::ObjectType::Blob) => {
447 let blob = repo.find_blob(entry.id())?;
448 results.push(MetadataEntry {
449 path,
450 content: Some(blob.content().to_vec()),
451 oid: entry.id(),
452 is_tree: false,
453 });
454 }
455 _ => {}
456 }
457 }
458 Ok(results)
459}
460
461fn insert_path_into_tree(
464 repo: &Repository,
465 existing: Option<&git2::Tree<'_>>,
466 path: &str,
467 blob_oid: Oid,
468) -> Result<Oid, Error> {
469 let components: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
470 if components.is_empty() {
471 return Err(Error::from_str("empty path"));
472 }
473 insert_path_recursive(repo, existing, &components, blob_oid)
474}
475
476fn insert_path_recursive(
477 repo: &Repository,
478 existing: Option<&git2::Tree<'_>>,
479 components: &[&str],
480 blob_oid: Oid,
481) -> Result<Oid, Error> {
482 assert!(!components.is_empty());
483
484 let name = components[0];
485
486 if components.len() == 1 {
487 let mut builder = repo.treebuilder(existing)?;
489 builder.insert(name, blob_oid, 0o100644)?;
490 return builder.write();
491 }
492
493 let sub_existing = match existing {
495 Some(tree) => match tree.get_name(name) {
496 Some(entry) if entry.kind() == Some(git2::ObjectType::Tree) => {
497 Some(repo.find_tree(entry.id())?)
498 }
499 _ => None,
500 },
501 None => None,
502 };
503
504 let child_oid = insert_path_recursive(repo, sub_existing.as_ref(), &components[1..], blob_oid)?;
505
506 let mut builder = repo.treebuilder(existing)?;
507 builder.insert(name, child_oid, 0o040000)?;
508 builder.write()
509}
510
511fn remove_path_from_tree(
514 repo: &Repository,
515 tree: &git2::Tree<'_>,
516 path: &str,
517) -> Result<Option<Oid>, Error> {
518 let components: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
519 if components.is_empty() {
520 return Err(Error::from_str("empty path"));
521 }
522 remove_path_recursive(repo, tree, &components)
523}
524
525fn remove_path_recursive(
526 repo: &Repository,
527 tree: &git2::Tree<'_>,
528 components: &[&str],
529) -> Result<Option<Oid>, Error> {
530 assert!(!components.is_empty());
531 let name = components[0];
532
533 if components.len() == 1 {
534 let mut builder = repo.treebuilder(Some(tree))?;
536 if builder.get(name)?.is_none() {
537 return Err(Error::from_str("path not found"));
538 }
539 builder.remove(name)?;
540 if builder.is_empty() {
541 Ok(None)
542 } else {
543 Ok(Some(builder.write()?))
544 }
545 } else {
546 let entry = tree
548 .get_name(name)
549 .ok_or_else(|| Error::from_str("path not found"))?;
550 let subtree = repo.find_tree(entry.id())?;
551 let child_oid = remove_path_recursive(repo, &subtree, &components[1..])?;
552
553 let mut builder = repo.treebuilder(Some(tree))?;
554 match child_oid {
555 Some(oid) => {
556 builder.insert(name, oid, 0o040000)?;
557 }
558 None => {
559 builder.remove(name)?;
560 }
561 }
562 if builder.is_empty() {
563 Ok(None)
564 } else {
565 Ok(Some(builder.write()?))
566 }
567 }
568}
569
570fn path_exists_in_tree(repo: &Repository, tree: &git2::Tree<'_>, path: &str) -> bool {
572 let components: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
573 if components.is_empty() {
574 return false;
575 }
576 path_exists_recursive(repo, tree, &components)
577}
578
579fn path_exists_recursive(repo: &Repository, tree: &git2::Tree<'_>, components: &[&str]) -> bool {
580 if components.is_empty() {
581 return false;
582 }
583 match tree.get_name(components[0]) {
584 None => false,
585 Some(entry) => {
586 if components.len() == 1 {
587 true
588 } else if entry.kind() == Some(git2::ObjectType::Tree) {
589 match repo.find_tree(entry.id()) {
590 Ok(subtree) => path_exists_recursive(repo, &subtree, &components[1..]),
591 Err(_) => false,
592 }
593 } else {
594 false
595 }
596 }
597 }
598}
599
600fn glob_matches(pattern: &str, path: &str) -> bool {
604 let pat_parts: Vec<&str> = pattern.split('/').filter(|s| !s.is_empty()).collect();
605 let path_parts: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
606
607 if pattern == path {
609 return true;
610 }
611
612 if !pat_parts.is_empty()
614 && !pat_parts.iter().any(|p| *p == "*" || *p == "**")
615 && path_parts.starts_with(&pat_parts)
616 {
617 return true;
618 }
619
620 glob_match_recursive(&pat_parts, &path_parts)
621}
622
623fn glob_match_recursive(pattern: &[&str], path: &[&str]) -> bool {
624 if pattern.is_empty() {
625 return path.is_empty();
626 }
627
628 if pattern[0] == "**" {
629 let rest_pat = &pattern[1..];
631 for i in 0..=path.len() {
632 if glob_match_recursive(rest_pat, &path[i..]) {
633 return true;
634 }
635 }
636 return false;
637 }
638
639 if path.is_empty() {
640 return false;
641 }
642
643 let matches_component = pattern[0] == "*" || pattern[0] == path[0];
644 if matches_component {
645 glob_match_recursive(&pattern[1..], &path[1..])
646 } else {
647 false
648 }
649}
650
651fn collect_leaf_paths(
654 repo: &Repository,
655 tree: &git2::Tree<'_>,
656 prefix: &str,
657 cb: &mut dyn FnMut(String),
658) -> Result<(), Error> {
659 for entry in tree.iter() {
660 let name = match entry.name() {
661 Some(n) => n,
662 None => continue,
663 };
664 let full = if prefix.is_empty() {
665 name.to_string()
666 } else {
667 format!("{}/{}", prefix, name)
668 };
669 if entry.kind() == Some(git2::ObjectType::Tree) {
670 let subtree = repo.find_tree(entry.id())?;
671 collect_leaf_paths(repo, &subtree, &full, cb)?;
672 } else {
673 cb(full);
674 }
675 }
676 Ok(())
677}
678
679impl MetadataIndex for Repository {
684 fn metadata_list(&self, ref_name: &str) -> Result<Vec<(Oid, Oid)>, Error> {
685 let root = match resolve_root_tree(self, ref_name)? {
686 Some(t) => t,
687 None => return Ok(Vec::new()),
688 };
689 collect_entries(self, &root, "")
690 }
691
692 fn metadata_get(&self, ref_name: &str, target: &Oid) -> Result<Option<Oid>, Error> {
693 let root = match resolve_root_tree(self, ref_name)? {
694 Some(t) => t,
695 None => return Ok(None),
696 };
697 Ok(detect_fanout(self, &root, target)?.map(|(_, _, oid)| oid))
698 }
699
700 fn metadata(
701 &self,
702 ref_name: &str,
703 target: &Oid,
704 tree: &Oid,
705 opts: &MetadataOptions,
706 ) -> Result<Oid, Error> {
707 self.find_tree(*tree)?;
708
709 let (segments, leaf) = shard_oid(target, opts.shard_level)?;
710 let existing_root = resolve_root_tree(self, ref_name)?;
711
712 if !opts.force
713 && let Some(ref root) = existing_root
714 && detect_fanout(self, root, target)?.is_some()
715 {
716 return Err(Error::from_str(
717 "metadata entry already exists (use force to overwrite)",
718 ));
719 }
720
721 build_fanout(self, existing_root.as_ref(), &segments, &leaf, tree)
722 }
723
724 fn metadata_commit(&self, ref_name: &str, root: Oid, message: &str) -> Result<Oid, Error> {
725 commit_index(self, ref_name, root, message)
726 }
727
728 fn metadata_show(&self, ref_name: &str, target: &Oid) -> Result<Vec<MetadataEntry>, Error> {
729 let root = match resolve_root_tree(self, ref_name)? {
730 Some(t) => t,
731 None => return Ok(Vec::new()),
732 };
733
734 let tree_oid = match detect_fanout(self, &root, target)? {
735 Some((_, _, oid)) => oid,
736 None => return Ok(Vec::new()),
737 };
738
739 let tree = self.find_tree(tree_oid)?;
740 collect_tree_entries(self, &tree, "")
741 }
742
743 fn metadata_add(
744 &self,
745 ref_name: &str,
746 target: &Oid,
747 path: &str,
748 content: Option<&[u8]>,
749 opts: &MetadataOptions,
750 ) -> Result<Oid, Error> {
751 let blob_oid = self.blob(content.unwrap_or(b""))?;
752
753 let existing_root = resolve_root_tree(self, ref_name)?;
754
755 let existing_meta_tree = match &existing_root {
757 Some(root) => match detect_fanout(self, root, target)? {
758 Some((_, _, oid)) => Some(self.find_tree(oid)?),
759 None => None,
760 },
761 None => None,
762 };
763
764 if !opts.force
766 && let Some(ref meta_tree) = existing_meta_tree
767 && path_exists_in_tree(self, meta_tree, path)
768 {
769 return Err(Error::from_str(
770 "path already exists in metadata (use --force to overwrite)",
771 ));
772 }
773
774 let new_meta_tree_oid =
776 insert_path_into_tree(self, existing_meta_tree.as_ref(), path, blob_oid)?;
777
778 let (segments, leaf) = if existing_meta_tree.is_some() {
780 match &existing_root {
782 Some(root) => match detect_fanout(self, root, target)? {
783 Some((s, l, _)) => (s, l),
784 None => shard_oid(target, opts.shard_level)?,
785 },
786 None => shard_oid(target, opts.shard_level)?,
787 }
788 } else {
789 shard_oid(target, opts.shard_level)?
790 };
791
792 let new_root = build_fanout(
793 self,
794 existing_root.as_ref(),
795 &segments,
796 &leaf,
797 &new_meta_tree_oid,
798 )?;
799
800 let msg = format!("metadata: add {} to {}", path, target);
801 commit_index(self, ref_name, new_root, &msg)?;
802
803 Ok(new_meta_tree_oid)
804 }
805
806 fn metadata_remove_paths(
807 &self,
808 ref_name: &str,
809 target: &Oid,
810 patterns: &[&str],
811 keep: bool,
812 ) -> Result<bool, Error> {
813 let root = match resolve_root_tree(self, ref_name)? {
814 Some(t) => t,
815 None => return Ok(false),
816 };
817
818 let (segments, leaf, meta_oid) = match detect_fanout(self, &root, target)? {
819 Some(t) => t,
820 None => return Ok(false),
821 };
822
823 let meta_tree = self.find_tree(meta_oid)?;
824 let patterns_owned: Vec<String> = patterns.iter().map(|s| s.to_string()).collect();
825 let new_meta_tree = self.filter_by_predicate(&meta_tree, |_repo, path| {
826 let path_str = path.to_str().unwrap_or("");
827 let matched = patterns_owned.iter().any(|p| glob_matches(p, path_str));
828 if keep { matched } else { !matched }
829 })?;
830
831 if new_meta_tree.is_empty() {
832 match build_fanout_remove(self, &root, &segments, &leaf)? {
834 RemoveResult::NotFound => Ok(false),
835 RemoveResult::Empty => {
836 let mut reference = self.find_reference(ref_name)?;
837 reference.delete()?;
838 Ok(true)
839 }
840 RemoveResult::Removed(new_root) => {
841 let msg = format!("metadata: remove paths from {}", target);
842 commit_index(self, ref_name, new_root, &msg)?;
843 Ok(true)
844 }
845 }
846 } else if new_meta_tree.id() == meta_oid {
847 Ok(false)
848 } else {
849 let new_root = build_fanout(self, Some(&root), &segments, &leaf, &new_meta_tree.id())?;
850 let msg = format!("metadata: remove paths from {}", target);
851 commit_index(self, ref_name, new_root, &msg)?;
852 Ok(true)
853 }
854 }
855
856 fn metadata_remove(&self, ref_name: &str, target: &Oid) -> Result<bool, Error> {
857 let root = match resolve_root_tree(self, ref_name)? {
858 Some(t) => t,
859 None => return Ok(false),
860 };
861
862 let (segments, leaf) = match detect_fanout(self, &root, target)? {
863 Some((segments, leaf, _)) => (segments, leaf),
864 None => return Ok(false),
865 };
866
867 match build_fanout_remove(self, &root, &segments, &leaf)? {
868 RemoveResult::NotFound => Ok(false),
869 RemoveResult::Empty => {
870 let mut reference = self.find_reference(ref_name)?;
871 reference.delete()?;
872 Ok(true)
873 }
874 RemoveResult::Removed(new_root) => {
875 let msg = format!("metadata: remove {}", target);
876 commit_index(self, ref_name, new_root, &msg)?;
877 Ok(true)
878 }
879 }
880 }
881
882 fn metadata_copy(
883 &self,
884 ref_name: &str,
885 from: &Oid,
886 to: &Oid,
887 opts: &MetadataOptions,
888 ) -> Result<Oid, Error> {
889 let root = match resolve_root_tree(self, ref_name)? {
890 Some(t) => t,
891 None => {
892 return Err(Error::from_str(&format!(
893 "no metadata entry for source {}",
894 from
895 )));
896 }
897 };
898
899 let source_tree_oid = match detect_fanout(self, &root, from)? {
900 Some((_, _, oid)) => oid,
901 None => {
902 return Err(Error::from_str(&format!(
903 "no metadata entry for source {}",
904 from
905 )));
906 }
907 };
908
909 if !opts.force && detect_fanout(self, &root, to)?.is_some() {
910 return Err(Error::from_str(
911 "metadata entry already exists for target (use --force to overwrite)",
912 ));
913 }
914
915 let (segments, leaf) = shard_oid(to, opts.shard_level)?;
916 let new_root = build_fanout(self, Some(&root), &segments, &leaf, &source_tree_oid)?;
917
918 let msg = format!("metadata: copy {} -> {}", from, to);
919 commit_index(self, ref_name, new_root, &msg)?;
920
921 Ok(source_tree_oid)
922 }
923
924 fn metadata_prune(&self, ref_name: &str, dry_run: bool) -> Result<Vec<Oid>, Error> {
925 let entries = self.metadata_list(ref_name)?;
926 let mut pruned = Vec::new();
927 let odb = self.odb()?;
928
929 for (target, _) in &entries {
930 if !odb.exists(*target) {
931 pruned.push(*target);
932 }
933 }
934
935 if !dry_run && !pruned.is_empty() {
936 let mut root = match resolve_root_tree(self, ref_name)? {
937 Some(t) => t,
938 None => return Ok(pruned),
939 };
940
941 for target in &pruned {
942 let (segments, leaf) = match detect_fanout(self, &root, target)? {
943 Some((segments, leaf, _)) => (segments, leaf),
944 None => continue,
945 };
946
947 match build_fanout_remove(self, &root, &segments, &leaf)? {
948 RemoveResult::NotFound => {}
949 RemoveResult::Empty => {
950 let mut reference = self.find_reference(ref_name)?;
951 reference.delete()?;
952 return Ok(pruned);
953 }
954 RemoveResult::Removed(new_root) => {
955 root = self.find_tree(new_root)?;
956 }
957 }
958 }
959
960 let msg = format!("metadata: prune {} entries", pruned.len());
962 commit_index(self, ref_name, root.id(), &msg)?;
963 }
964
965 Ok(pruned)
966 }
967
968 fn metadata_get_ref(&self, ref_name: &str) -> String {
969 ref_name.to_string()
970 }
971
972 fn link(
973 &self,
974 ref_name: &str,
975 a: &str,
976 b: &str,
977 forward: &str,
978 reverse: &str,
979 meta: Option<&[u8]>,
980 ) -> Result<Oid, Error> {
981 let blob_oid = self.blob(meta.unwrap_or(b""))?;
982 let existing_root = resolve_root_tree(self, ref_name)?;
983
984 let forward_path = format!("{}/{}/{}", a, forward, b);
986 let tree1 = insert_path_into_tree(self, existing_root.as_ref(), &forward_path, blob_oid)?;
987
988 let reverse_path = format!("{}/{}/{}", b, reverse, a);
990 let tree1_obj = self.find_tree(tree1)?;
991 let tree2 = insert_path_into_tree(self, Some(&tree1_obj), &reverse_path, blob_oid)?;
992
993 let msg = format!("link: {} -[{}]-> {}", a, forward, b);
994 commit_index(self, ref_name, tree2, &msg)?;
995 Ok(tree2)
996 }
997
998 fn unlink(
999 &self,
1000 ref_name: &str,
1001 a: &str,
1002 b: &str,
1003 forward: &str,
1004 reverse: &str,
1005 ) -> Result<Oid, Error> {
1006 let root =
1007 resolve_root_tree(self, ref_name)?.ok_or_else(|| Error::from_str("ref not found"))?;
1008
1009 let forward_path = format!("{}/{}/{}", a, forward, b);
1011 let tree1 = remove_path_from_tree(self, &root, &forward_path)?
1012 .ok_or_else(|| Error::from_str("tree became empty after unlink"))?;
1013
1014 let tree1_obj = self.find_tree(tree1)?;
1016 let reverse_path = format!("{}/{}/{}", b, reverse, a);
1017 let tree2_opt = remove_path_from_tree(self, &tree1_obj, &reverse_path)?;
1018
1019 match tree2_opt {
1020 Some(tree2) => {
1021 let msg = format!("unlink: {} -[{}]-> {}", a, forward, b);
1022 commit_index(self, ref_name, tree2, &msg)?;
1023 Ok(tree2)
1024 }
1025 None => {
1026 let mut reference = self.find_reference(ref_name)?;
1028 reference.delete()?;
1029 let empty = self.treebuilder(None)?.write()?;
1030 Ok(empty)
1031 }
1032 }
1033 }
1034
1035 fn linked(
1036 &self,
1037 ref_name: &str,
1038 key: &str,
1039 relation: Option<&str>,
1040 ) -> Result<Vec<(String, String)>, Error> {
1041 let root = match resolve_root_tree(self, ref_name)? {
1042 Some(t) => t,
1043 None => return Ok(Vec::new()),
1044 };
1045
1046 let key_tree = if key.contains('/') {
1048 let components: Vec<&str> = key.split('/').filter(|s| !s.is_empty()).collect();
1049 let mut current = root.clone();
1050 for component in &components {
1051 let next_id = match current.get_name(component) {
1052 Some(e) if e.kind() == Some(git2::ObjectType::Tree) => e.id(),
1053 _ => return Ok(Vec::new()),
1054 };
1055 current = self.find_tree(next_id)?;
1056 }
1057 current
1058 } else {
1059 let key_entry = match root.get_name(key) {
1060 Some(e) => e,
1061 None => return Ok(Vec::new()),
1062 };
1063 self.find_tree(key_entry.id())?
1064 };
1065
1066 let mut results = Vec::new();
1067
1068 if let Some(rel) = relation {
1069 if let Some(rel_entry) = key_tree.get_name(rel)
1071 && rel_entry.kind() == Some(git2::ObjectType::Tree)
1072 {
1073 let rel_tree = self.find_tree(rel_entry.id())?;
1074 collect_leaf_paths(self, &rel_tree, "", &mut |path| {
1075 results.push((rel.to_string(), path));
1076 })?;
1077 }
1078 } else {
1079 for rel_entry in key_tree.iter() {
1081 if rel_entry.kind() == Some(git2::ObjectType::Tree) {
1082 let rel_name = rel_entry.name().unwrap_or("").to_string();
1083 let rel_tree = self.find_tree(rel_entry.id())?;
1084 collect_leaf_paths(self, &rel_tree, "", &mut |path| {
1085 results.push((rel_name.clone(), path));
1086 })?;
1087 }
1088 }
1089 }
1090
1091 Ok(results)
1092 }
1093
1094 fn is_linked(&self, ref_name: &str, a: &str, b: &str, forward: &str) -> Result<bool, Error> {
1095 let root = match resolve_root_tree(self, ref_name)? {
1096 Some(t) => t,
1097 None => return Ok(false),
1098 };
1099 let path = format!("{}/{}/{}", a, forward, b);
1100 Ok(path_exists_in_tree(self, &root, &path))
1101 }
1102}
1103
1104#[cfg(test)]
1105mod tests;