//! Deleting paths from history (`vcs purge`): each tree without them, and //! what was at them. Only the folders on the way to a path are read and //! written again, so going through every version of a long history is quick. use std::collections::BTreeSet; use crate::tree::{self, EntryKind}; use crate::{Id, Object, Result, Source, corrupt}; /// `tree` without `paths` (files or folders), and the IDs of what was at /// them into `gone`: a file's ID, or a folder and everything under it. A /// tree that has none of them comes back as it is. pub fn without(src: &dyn Source, tree: &Id, paths: &[String], gone: &mut BTreeSet) -> Result<(Id, Vec)> { let mut present = false; for p in paths { let Some(e) = tree::lookup(src, tree, p)? else { continue }; present = true; if e.kind == EntryKind::Tree { let mut under = Vec::new(); tree::reachable(src, &e.id, &mut under)?; gone.extend(under); } else if e.kind != EntryKind::Gitlink { gone.insert(e.id); } } if !present { return Ok((*tree, Vec::new())); } match tree::edit(src, tree, &[], paths)? { Ok(done) => Ok(done), Err(bad) => Err(corrupt(format!("{bad} isn't a path a project can have"))), } } /// The objects [`without`] reads for `tree` that `src` doesn't have: the /// folders on the way to each path, and everything under a folder at one. pub fn needs(src: &dyn Source, tree: &Id, paths: &[String], need: &mut BTreeSet) -> Result<()> { for p in paths { let mut at = *tree; let parts: Vec<&str> = p.split('/').filter(|s| !s.is_empty()).collect(); for (i, part) in parts.iter().enumerate() { let Some(o) = src.get(&at)? else { need.insert(at); break; }; let Some(e) = tree::decode(&o.data)?.into_iter().find(|e| e.name == *part) else { break }; if e.kind != EntryKind::Tree { break; } if i + 1 == parts.len() { trees_under(src, &e.id, need)?; } else { at = e.id; } } } Ok(()) } fn trees_under(src: &dyn Source, root: &Id, need: &mut BTreeSet) -> Result<()> { let mut stack = vec![*root]; while let Some(t) = stack.pop() { let Some(o) = src.get(&t)? else { need.insert(t); continue; }; for e in tree::decode(&o.data)? { if e.kind == EntryKind::Tree { stack.push(e.id); } } } Ok(()) } #[cfg(test)] mod tests { use super::*; use crate::tree::{Entry, Files}; use std::collections::HashMap; struct Mem(HashMap); impl Source for Mem { fn get(&self, id: &Id) -> Result> { Ok(self.0.get(id).cloned()) } } fn build(files: &[(&str, &str)], mem: &mut Mem) -> Id { let mut map = Files::new(); for (path, text) in files { let blob = Object::blob(text.as_bytes().to_vec()); map.insert(path.to_string(), Entry { kind: EntryKind::File, id: blob.id() }); mem.0.insert(blob.id(), blob); } let (root, objects) = tree::build(&map); for o in objects { mem.0.insert(o.id(), o); } root } #[test] fn same_as_building_without_the_path() { let mut mem = Mem(HashMap::new()); let before = build(&[("a.txt", "a"), ("keys/secret.pem", "s"), ("src/lib.rs", "l"), ("src/deep/x.rs", "x")], &mut mem); let expected = build(&[("a.txt", "a"), ("src/lib.rs", "l"), ("src/deep/x.rs", "x")], &mut mem); let mut gone = BTreeSet::new(); let (after, _) = without(&mem, &before, &["keys/secret.pem".into()], &mut gone).unwrap(); assert_eq!(after, expected); assert_eq!(gone.into_iter().collect::>(), vec![Object::blob(b"s".to_vec()).id()]); } #[test] fn a_folder_and_everything_under_it() { let mut mem = Mem(HashMap::new()); let before = build(&[("a.txt", "a"), ("src/deep/x.rs", "x"), ("src/deep/y.rs", "y"), ("src/lib.rs", "l")], &mut mem); let expected = build(&[("a.txt", "a"), ("src/lib.rs", "l")], &mut mem); let mut gone = BTreeSet::new(); let (after, _) = without(&mem, &before, &["src/deep".into()], &mut gone).unwrap(); assert_eq!(after, expected); assert!(gone.contains(&Object::blob(b"x".to_vec()).id()) && gone.contains(&Object::blob(b"y".to_vec()).id())); assert_eq!(gone.len(), 3); } #[test] fn untouched_without_the_path() { let mut mem = Mem(HashMap::new()); let before = build(&[("a.txt", "a")], &mut mem); let mut gone = BTreeSet::new(); assert_eq!(without(&mem, &before, &["b.txt".into()], &mut gone).unwrap().0, before); assert!(gone.is_empty()); } #[test] fn needs_names_the_missing_folders() { let mut mem = Mem(HashMap::new()); let root = build(&[("src/deep/x.rs", "x")], &mut mem); let src_id = tree::lookup(&mem, &root, "src").unwrap().unwrap().id; mem.0.remove(&src_id); let mut need = BTreeSet::new(); needs(&mem, &root, &["src/deep/x.rs".into()], &mut need).unwrap(); assert_eq!(need.into_iter().collect::>(), vec![src_id]); } }