Onset: version control where nobody waits and nothing gets lost. The on command, its core and onset-server.
| 6 |
| 7 | use crate::tree::{self, EntryKind}; |
| 8 | use crate::{Id, Object, Result, Source, corrupt}; |
| 9 |
| 10 | /// `tree` without `paths` (files or folders), and the IDs of what was at |
| 11 | /// them into `gone`: a file's ID, or a folder and everything under it. A |
| 12 | /// tree that has none of them comes back as it is. |
| 13 | pub fn without(src: &dyn Source, tree: &Id, paths: &[String], gone: &mut BTreeSet<Id>) -> Result<(Id, Vec<Object>)> { |
| 14 | let mut present = false; |
| 15 | for p in paths { |
| 16 | let Some(e) = tree::lookup(src, tree, p)? else { continue }; |
| 17 | present = true; |
| 18 | if e.kind == EntryKind::Tree { |
| 19 | let mut under = Vec::new(); |
| 20 | tree::reachable(src, &e.id, &mut under)?; |
| 21 | gone.extend(under); |
| 22 | } else if e.kind != EntryKind::Gitlink { |
| 23 | gone.insert(e.id); |
| 24 | } |
| 25 | } |
| 26 | if !present { |
| 27 | return Ok((*tree, Vec::new())); |
| 28 | } |
| 29 | match tree::edit(src, tree, &[], paths)? { |
| 30 | Ok(done) => Ok(done), |
| 31 | Err(bad) => Err(corrupt(format!("{bad} isn't a path a project can have"))), |
| 32 | } |
| 33 | } |
| 34 |
| 35 | /// The objects [`without`] reads for `tree` that `src` doesn't have: the |
| 36 | /// folders on the way to each path, and everything under a folder at one. |
| 37 | pub fn needs(src: &dyn Source, tree: &Id, paths: &[String], need: &mut BTreeSet<Id>) -> Result<()> { |
| 38 | for p in paths { |
| 39 | let mut at = *tree; |
| 40 | let parts: Vec<&str> = p.split('/').filter(|s| !s.is_empty()).collect(); |
| 41 | for (i, part) in parts.iter().enumerate() { |
| 42 | let Some(o) = src.get(&at)? else { |
| 43 | need.insert(at); |
| 44 | break; |
| 45 | }; |
| 46 | let Some(e) = tree::decode(&o.data)?.into_iter().find(|e| e.name == *part) else { break }; |
| 47 | if e.kind != EntryKind::Tree { |
| 48 | break; |
| 49 | } |
| 50 | if i + 1 == parts.len() { |
| 51 | trees_under(src, &e.id, need)?; |
| 52 | } else { |
| 53 | at = e.id; |
| 54 | } |
| 55 | } |
| 56 | } |
| 57 | Ok(()) |
| 58 | } |
| 59 |
| 60 | fn trees_under(src: &dyn Source, root: &Id, need: &mut BTreeSet<Id>) -> Result<()> { |
| 61 | let mut stack = vec![*root]; |
| 62 | while let Some(t) = stack.pop() { |
| 63 | let Some(o) = src.get(&t)? else { |
| 64 | need.insert(t); |
| 65 | continue; |
| 66 | }; |
| 67 | for e in tree::decode(&o.data)? { |
| 68 | if e.kind == EntryKind::Tree { |
| 69 | stack.push(e.id); |
| 70 | } |
| 71 | } |
| 72 | } |
| 73 | Ok(()) |
| 74 | } |
| 75 |
| 76 | #[cfg(test)] |
| 77 | mod tests { |
| 78 | use super::*; |
| 79 | use crate::tree::{Entry, Files}; |
| 80 | use std::collections::HashMap; |
| 81 |
| 82 | struct Mem(HashMap<Id, Object>); |
| 83 |
| 84 | impl Source for Mem { |
| 85 | fn get(&self, id: &Id) -> Result<Option<Object>> { |
| 86 | Ok(self.0.get(id).cloned()) |
| 87 | } |
| 88 | } |
| 89 |
| 90 | fn build(files: &[(&str, &str)], mem: &mut Mem) -> Id { |
| 91 | let mut map = Files::new(); |
| 92 | for (path, text) in files { |
| 93 | let blob = Object::blob(text.as_bytes().to_vec()); |
| 94 | map.insert(path.to_string(), Entry { kind: EntryKind::File, id: blob.id() }); |
| 95 | mem.0.insert(blob.id(), blob); |
| 96 | } |
| 97 | let (root, objects) = tree::build(&map); |
| 98 | for o in objects { |
| 99 | mem.0.insert(o.id(), o); |
| 100 | } |
| 101 | root |
| 102 | } |
| 103 |
| 104 | #[test] |
| 105 | fn same_as_building_without_the_path() { |
| 106 | let mut mem = Mem(HashMap::new()); |
| 107 | let before = build(&[("a.txt", "a"), ("keys/secret.pem", "s"), ("src/lib.rs", "l"), ("src/deep/x.rs", "x")], &mut mem); |
| 108 | let expected = build(&[("a.txt", "a"), ("src/lib.rs", "l"), ("src/deep/x.rs", "x")], &mut mem); |
| 109 | let mut gone = BTreeSet::new(); |
| 110 | let (after, _) = without(&mem, &before, &["keys/secret.pem".into()], &mut gone).unwrap(); |
| 111 | assert_eq!(after, expected); |
| 112 | assert_eq!(gone.into_iter().collect::<Vec<_>>(), vec![Object::blob(b"s".to_vec()).id()]); |
| 113 | } |
| 114 |
| 115 | #[test] |
| 116 | fn a_folder_and_everything_under_it() { |
| 117 | let mut mem = Mem(HashMap::new()); |
| 118 | let before = build(&[("a.txt", "a"), ("src/deep/x.rs", "x"), ("src/deep/y.rs", "y"), ("src/lib.rs", "l")], &mut mem); |
| 119 | let expected = build(&[("a.txt", "a"), ("src/lib.rs", "l")], &mut mem); |
| 120 | let mut gone = BTreeSet::new(); |
| 121 | let (after, _) = without(&mem, &before, &["src/deep".into()], &mut gone).unwrap(); |
| 122 | assert_eq!(after, expected); |
| 123 | assert!(gone.contains(&Object::blob(b"x".to_vec()).id()) && gone.contains(&Object::blob(b"y".to_vec()).id())); |
| 124 | assert_eq!(gone.len(), 3); |
| 125 | } |
| 126 |
| 127 | #[test] |
| 128 | fn untouched_without_the_path() { |
| 129 | let mut mem = Mem(HashMap::new()); |
| 130 | let before = build(&[("a.txt", "a")], &mut mem); |
| 131 | let mut gone = BTreeSet::new(); |
| 132 | assert_eq!(without(&mem, &before, &["b.txt".into()], &mut gone).unwrap().0, before); |
| 133 | assert!(gone.is_empty()); |
| 134 | } |
| 135 |
| 136 | #[test] |
| 137 | fn needs_names_the_missing_folders() { |
| 138 | let mut mem = Mem(HashMap::new()); |
| 139 | let root = build(&[("src/deep/x.rs", "x")], &mut mem); |
| 140 | let src_id = tree::lookup(&mem, &root, "src").unwrap().unwrap().id; |
| 141 | mem.0.remove(&src_id); |
| 142 | let mut need = BTreeSet::new(); |
| 143 | needs(&mem, &root, &["src/deep/x.rs".into()], &mut need).unwrap(); |
| 144 | assert_eq!(need.into_iter().collect::<Vec<_>>(), vec![src_id]); |
| 145 | } |
| 146 | } |
| 147 |