From 2462119defef2d7f28cd5b15b09917c9a46e20b6 Mon Sep 17 00:00:00 2001 From: diogo464 Date: Sun, 6 Feb 2022 18:32:54 +0000 Subject: snapshot --- .gitignore | 1 + Cargo.lock | 286 +++++++++++++++++ Cargo.toml | 13 + src/main.rs | 997 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 1297 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..61c1d16 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,286 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "anyhow" +version = "1.0.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94a45b455c14666b85fc40a019e8ab9eb75e3a124e05494f5397122bc9eb06e0" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "clap" +version = "3.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63edc3f163b3c71ec8aa23f9bd6070f77edbf3d1d198b164afa90ff00e4ec62" +dependencies = [ + "atty", + "bitflags", + "clap_derive", + "indexmap", + "lazy_static", + "os_str_bytes", + "strsim", + "termcolor", + "textwrap", +] + +[[package]] +name = "clap_derive" +version = "3.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a1132dc3944b31c20dd8b906b3a9f0a5d0243e092d59171414969657ac6aa85" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dotup" +version = "0.0.0" +dependencies = [ + "anyhow", + "clap", + "serde", + "slotmap", + "toml", +] + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" + +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "indexmap" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e74d72e0f9b65b5b4ca49a346af3976df0f9c61d550727f349ecd559f251a26c" + +[[package]] +name = "memchr" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" + +[[package]] +name = "os_str_bytes" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" +dependencies = [ + "memchr", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "serde" +version = "1.0.136" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.136" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "slotmap" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1e08e261d0e8f5c43123b7adf3e4ca1690d655377ac93a03b2c9d3e98de1342" +dependencies = [ + "version_check", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "termcolor" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80" + +[[package]] +name = "toml" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +dependencies = [ + "serde", +] + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..ce22ac1 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "dotup" +version = "0.0.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1.0.53" +clap = { version = "3.0.14", features = ["derive"] } +serde = { version = "1.0.136", features = ["derive"] } +slotmap = "1.0.6" +toml = "0.5.8" diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..e2a659b --- /dev/null +++ b/src/main.rs @@ -0,0 +1,997 @@ +#![feature(try_blocks)] +pub mod depot { + use std::{ + collections::HashSet, + ffi::{OsStr, OsString}, + ops::Deref, + path::{Path, PathBuf}, + }; + + use slotmap::SlotMap; + + pub use disk::{read, write}; + + slotmap::new_key_type! {pub struct LinkID;} + slotmap::new_key_type! {struct NodeID;} + + #[derive(Debug, Clone, PartialEq, Eq)] + pub enum SearchResult { + Found(LinkID), + Ancestor(LinkID), + NotFound, + } + + #[derive(Debug)] + pub struct LinkView<'a> { + link_id: LinkID, + depot: &'a Depot, + } + + impl<'a> LinkView<'a> { + pub fn origin(&self) -> &Path { + &self.depot.links[self.link_id].origin + } + + pub fn destination(&self) -> &Path { + &self.depot.links[self.link_id].destination + } + } + + // wrapper for a path under the depot + // this path is relative and does not contain `..` or similar + // Deref(Path) + struct DepotPath(PathBuf); + impl Deref for DepotPath { + type Target = Path; + + fn deref(&self) -> &Self::Target { + self.0.deref() + } + } + + #[derive(Debug, Clone)] + struct Node { + comp: OsString, + parent: NodeID, + kind: NodeKind, + } + + #[derive(Debug, Clone)] + enum NodeKind { + Link(LinkID), + Directory(HashSet), + } + + #[derive(Debug, Clone)] + struct Link { + origin: PathBuf, + destination: PathBuf, + node_id: NodeID, + } + + #[derive(Debug, Clone)] + pub struct Depot { + links: SlotMap, + nodes: SlotMap, + root: NodeID, + } + + impl Default for Depot { + fn default() -> Self { + let mut nodes = SlotMap::default(); + let root = nodes.insert(Node { + comp: Default::default(), + parent: Default::default(), + kind: NodeKind::Directory(Default::default()), + }); + Self { + links: Default::default(), + nodes, + root, + } + } + } + + impl Depot { + pub fn create( + &mut self, + origin: impl AsRef, + destination: impl AsRef, + ) -> anyhow::Result { + let origin = origin.as_ref(); + let destination = destination.as_ref(); + verify_path(origin)?; + verify_path(destination)?; + + // example + // origin = fish/config.fish + // destination = .config/fish/config.fish + + // search + // if ancestor - return error + // if found - update destination + // if not found - create + + match self.search_unchecked(&origin) { + SearchResult::Found(link_id) => { + let link = &mut self.links[link_id]; + link.destination = destination.to_owned(); + Ok(link_id) + } + SearchResult::Ancestor(_) => Err(anyhow::anyhow!( + "An ancestor of this path is already linked" + )), + SearchResult::NotFound => { + let link_id = self.links.insert(Link { + origin: origin.to_owned(), + destination: destination.to_owned(), + node_id: Default::default(), + }); + let node_id = self.node_create_link(origin, link_id); + self.links[link_id].node_id = node_id; + Ok(link_id) + } + } + } + + pub fn move_link( + &mut self, + link_id: LinkID, + destination: impl AsRef, + ) -> anyhow::Result<()> { + let destination = destination.as_ref(); + verify_path(destination)?; + + let link_node_id = self.links[link_id].node_id; + let link_parent_node_id = self.nodes[link_node_id].parent; + let (node_id, found) = self.node_search(destination); + + // the link is already at the destination + if found && node_id == link_node_id { + return Ok(()); + } + + if found { + let node = &self.nodes[node_id]; + match &node.kind { + NodeKind::Link(node_link_id) => { + let node_parent_id = node.parent; + let node_link_id = *node_link_id; + assert_ne!(link_id, node_link_id); + self.remove(node_link_id); + self.node_child_remove(link_parent_node_id, link_node_id); + self.node_child_add(node_parent_id, link_node_id); + self.node_set_parent(link_node_id, node_parent_id); + Ok(()) + } + NodeKind::Directory(..) => Err(anyhow::anyhow!( + "Cannot move link, other links exist under the destination" + )), + } + } else { + let node = &self.nodes[node_id]; + match &node.kind { + NodeKind::Link(..) => Err(anyhow::anyhow!( + "Cannot move link, an ancestor is already linked" + )), + NodeKind::Directory(_) => { + let new_node_id = self.node_create_link(destination, link_id); + self.node_remove(link_node_id); + self.links[link_id].node_id = new_node_id; + Ok(()) + } + } + } + } + + pub fn remove(&mut self, link_id: LinkID) { + let node_id = self.links[link_id].node_id; + self.node_remove(node_id); + self.links.remove(link_id); + } + + pub fn link_view(&self, link_id: LinkID) -> LinkView { + LinkView { + link_id, + depot: self, + } + } + + /// searchs for the link at `origin`. + /// returns SearchResult::Found(..) if there is a link at `origin`. + /// returns SearchResult::Ancestor(..) if an ancestor of `origin` is linked. + /// returns SearchResult::NotFound otherwise. + pub fn search(&self, origin: impl AsRef) -> anyhow::Result { + let origin = origin.as_ref(); + verify_path(origin)?; + Ok(self.search_unchecked(&origin)) + } + + /// returns an iterator for all the links at or under the given path. + pub fn links_under( + &self, + path: impl AsRef, + ) -> anyhow::Result + '_> { + let path = path.as_ref(); + verify_path(path)?; + + let mut link_ids = Vec::new(); + if let Some(node_id) = self.node_find(path) { + let mut node_ids = vec![node_id]; + while let Some(node_id) = node_ids.pop() { + let node = &self.nodes[node_id]; + match &node.kind { + NodeKind::Link(link_id) => link_ids.push(*link_id), + NodeKind::Directory(children) => node_ids.extend(children.iter().copied()), + } + } + } + Ok(link_ids.into_iter()) + } + + /// returns true if the `path` is a link or contains an ancestor that is linked. + /// returns false otherwise. + pub fn is_linked(&self, path: impl AsRef) -> bool { + match self.search(path) { + Ok(SearchResult::Found(..)) | Ok(SearchResult::Ancestor(..)) => true, + _ => false, + } + } + + fn search_unchecked(&self, origin: &Path) -> SearchResult { + debug_assert!(verify_path(origin).is_ok()); + + let mut origin_comps = iter_path_comps(&origin); + let mut curr_node = self.root; + 'outer: loop { + let node = &self.nodes[curr_node]; + let curr_comp = origin_comps.next(); + match &node.kind { + NodeKind::Link(link_id) => match curr_comp { + Some(_) => break SearchResult::Ancestor(*link_id), + None => break SearchResult::Found(*link_id), + }, + NodeKind::Directory(children) => match curr_comp { + Some(curr_comp) => { + for &child_id in children.iter() { + let child = &self.nodes[child_id]; + if &child.comp == curr_comp { + curr_node = child_id; + continue 'outer; + } + } + break SearchResult::NotFound; + } + None => break SearchResult::NotFound, + }, + } + } + } + + /// creates a new directory node with no children. + /// the node specified by `parent` must be a directory node. + fn node_create_dir_empty(&mut self, parent: NodeID, comp: OsString) -> NodeID { + let node_id = self.nodes.insert(Node { + comp, + parent, + kind: NodeKind::Directory(Default::default()), + }); + self.node_child_add(parent, node_id); + node_id + } + + /// all the nodes up to the node to be created have to be directory nodes. + /// `path` must be a verified path. + fn node_create_link(&mut self, path: &Path, link_id: LinkID) -> NodeID { + assert!(verify_path(path).is_ok()); + let mut curr_node_id = self.root; + let mut path_comps = iter_path_comps(path).peekable(); + // unwrap: a verified path has atleast 1 component + let mut curr_path_comp = path_comps.next().unwrap(); + + while path_comps.peek().is_some() { + let next_node = match self.node_children_search(curr_node_id, curr_path_comp) { + Some(child_id) => child_id, + None => self.node_create_dir_empty(curr_node_id, curr_path_comp.to_owned()), + }; + curr_node_id = next_node; + // unwrap: we known next is Some beacause of this loop's condition + curr_path_comp = path_comps.next().unwrap(); + } + + let new_node = self.nodes.insert(Node { + comp: curr_path_comp.to_owned(), + parent: curr_node_id, + kind: NodeKind::Link(link_id), + }); + self.node_child_add(curr_node_id, new_node); + new_node + } + + /// finds the node at the given path. + /// `path` must be a verified path. + fn node_find(&self, path: &Path) -> Option { + match self.node_search(path) { + (node_id, true) => Some(node_id), + _ => None, + } + } + + /// searches for the node at `path`. if that node does not exists then it returns the + /// closest node. return (closest_node, found) + fn node_search(&self, path: &Path) -> (NodeID, bool) { + debug_assert!(verify_path(path).is_ok()); + + let mut origin_comps = iter_path_comps(&path).peekable(); + let mut curr_node = self.root; + 'outer: loop { + let node = &self.nodes[curr_node]; + match origin_comps.next() { + Some(curr_comp) => match &node.kind { + NodeKind::Link(..) => break (curr_node, false), + NodeKind::Directory(children) => { + for &child_id in children.iter() { + let child = &self.nodes[child_id]; + if &child.comp == curr_comp { + curr_node = child_id; + continue 'outer; + } + } + break (curr_node, false); + } + }, + None => break (curr_node, true), + } + } + } + + /// adds `new_child` to `node_id`'s children. + /// the node specified by `node_id` must be a directory node. + fn node_child_add(&mut self, node_id: NodeID, new_child: NodeID) { + let node = &mut self.nodes[node_id]; + match node.kind { + NodeKind::Directory(ref mut children) => { + children.insert(new_child); + } + _ => unreachable!(), + } + } + + /// searchs for a child with the given comp and returns its id. + /// the node specified by `node_id` must be a directory node. + fn node_children_search(&self, node_id: NodeID, search_comp: &OsStr) -> Option { + let child_ids = match &self.nodes[node_id].kind { + NodeKind::Directory(c) => c, + _ => unreachable!(), + }; + for &child_id in child_ids { + let child = &self.nodes[child_id]; + if child.comp == search_comp { + return Some(child_id); + } + } + None + } + + /// removes `child` from `node_id`'s children. + /// the node specified by `node_id` must be a directory node and it must contain the node + /// `child`. + fn node_child_remove(&mut self, node_id: NodeID, child: NodeID) { + let node = &mut self.nodes[node_id]; + let remove_node = match &mut node.kind { + NodeKind::Directory(children) => { + let contained = children.remove(&child); + assert!(contained); + children.is_empty() + } + _ => unreachable!(), + }; + if remove_node && node_id != self.root { + self.node_remove(node_id); + } + } + + fn node_set_parent(&mut self, node_id: NodeID, parent: NodeID) { + self.nodes[node_id].parent = parent; + } + + fn node_remove(&mut self, node_id: NodeID) { + debug_assert!(node_id != self.root); + debug_assert!(self.nodes.contains_key(node_id)); + + let node = self.nodes.remove(node_id).unwrap(); + match node.kind { + NodeKind::Link(..) => {} + NodeKind::Directory(children) => { + // Right now directory nodes are only removed from inside this function and + // we do not remove directories with children + assert!(children.is_empty()); + } + } + let parent_id = node.parent; + self.node_child_remove(parent_id, node_id); + } + } + + mod disk { + use std::path::{Path, PathBuf}; + + use anyhow::Context; + use serde::{Deserialize, Serialize}; + + use super::Depot; + + #[derive(Debug, Serialize, Deserialize)] + struct DiskLink { + origin: PathBuf, + destination: PathBuf, + } + + #[derive(Debug, Serialize, Deserialize)] + struct DiskLinks { + links: Vec, + } + + pub fn read(path: &Path) -> anyhow::Result { + let contents = std::fs::read_to_string(path).context("Failed to read depot file")?; + let disk_links = toml::from_str::(&contents) + .context("Failed to parse depot file")? + .links; + let mut depot = Depot::default(); + for disk_link in disk_links { + depot + .create(disk_link.origin, disk_link.destination) + .context("Failed to build depot from file. File is in an invalid state")?; + } + Ok(depot) + } + + pub fn write(path: &Path, depot: &Depot) -> anyhow::Result<()> { + let mut links = Vec::with_capacity(depot.links.len()); + for (_, link) in depot.links.iter() { + links.push(DiskLink { + origin: link.origin.clone(), + destination: link.destination.clone(), + }); + } + let contents = toml::to_string_pretty(&DiskLinks { links }) + .context("Failed to serialize depot")?; + std::fs::write(path, contents).context("Failed to write depot to file")?; + Ok(()) + } + } + + fn verify_path(path: &Path) -> anyhow::Result<()> { + // make sure the path is not empty + // make sure the path is relative + // make sure the path does not contain '.' or '..' + if path.components().next().is_none() { + return Err(anyhow::anyhow!("Path cannot be empty")); + } + for component in path.components() { + match component { + std::path::Component::Prefix(_) => { + return Err(anyhow::anyhow!("Path cannot have prefix")) + } + std::path::Component::RootDir => { + return Err(anyhow::anyhow!("Path must be relative")) + } + std::path::Component::CurDir | std::path::Component::ParentDir => { + return Err(anyhow::anyhow!("Path cannot contain '.' or '..'")) + } + std::path::Component::Normal(_) => {} + } + } + Ok(()) + } + + /// Iterate over the components of a path. + /// # Pre + /// The path can only have "Normal" components. + fn iter_path_comps(path: &Path) -> impl Iterator { + debug_assert!(verify_path(path).is_ok()); + path.components().map(|component| match component { + std::path::Component::Normal(comp) => comp, + _ => unreachable!(), + }) + } + + #[cfg(test)] + mod tests { + use super::*; + + #[test] + fn test_depot_create() { + let mut depot = Depot::default(); + depot.create("", "dest1.txt").unwrap_err(); + depot.create("comp1.txt", "").unwrap_err(); + depot.create("", "").unwrap_err(); + + depot.create("comp1.txt", "dest1.txt").unwrap(); + depot.create("comp1.txt", "dest1_updated.txt").unwrap(); + depot + .create("./comp1.txt", "dest1_updated.txt") + .unwrap_err(); + depot.create("/comp1.txt", "dest1.txt").unwrap_err(); + depot.create("dir1/", "destdir1/").unwrap(); + depot.create("dir1/file1.txt", "destfile1.txt").unwrap_err(); + } + + #[test] + fn test_depot_move_link() { + let mut depot = Depot::default(); + let f1 = depot.create("d1/f1", "d1/f1").unwrap(); + let _f2 = depot.create("d1/f2", "d1/f2").unwrap(); + + depot.move_link(f1, "d1/f2/f1").unwrap_err(); + depot.move_link(f1, "d1").unwrap_err(); + + depot.move_link(f1, "").unwrap(); + depot.move_link(f1, "d2/f1").unwrap(); + } + + #[test] + fn test_depot_remove() { + let mut depot = Depot::default(); + let f1 = depot.create("d1/f1", "d1/f1").unwrap(); + assert_eq!(depot.search("d1/f1").unwrap(), SearchResult::Found(f1)); + depot.remove(f1); + assert_eq!(depot.search("d1/f1").unwrap(), SearchResult::NotFound); + } + + #[test] + fn test_depot_search() { + let mut depot = Depot::default(); + let f1 = depot.create("d1/f1", "d1/f1").unwrap(); + let f2 = depot.create("d1/f2", "d1/f2").unwrap(); + let f3 = depot.create("d1/f3", "d1/f3").unwrap(); + let f4 = depot.create("d1/d2/f4", "d2/f4").unwrap(); + let d3 = depot.create("d3", "d3").unwrap(); + + assert_eq!(depot.search("d1").unwrap(), SearchResult::NotFound,); + assert_eq!(depot.search("d1/f1").unwrap(), SearchResult::Found(f1),); + assert_eq!(depot.search("d1/f2").unwrap(), SearchResult::Found(f2),); + assert_eq!(depot.search("d1/f3").unwrap(), SearchResult::Found(f3),); + assert_eq!(depot.search("d1/d2/f4").unwrap(), SearchResult::Found(f4),); + assert_eq!(depot.search("d1/d2/f5").unwrap(), SearchResult::NotFound,); + assert_eq!(depot.search("d3/f6").unwrap(), SearchResult::Ancestor(d3),); + } + + #[test] + fn test_iter_path_comps() { + let path = Path::new("comp1/comp2/./comp3/file.txt"); + let mut iter = iter_path_comps(path); + assert_eq!(iter.next(), Some(OsStr::new("comp1"))); + assert_eq!(iter.next(), Some(OsStr::new("comp2"))); + assert_eq!(iter.next(), Some(OsStr::new("comp3"))); + assert_eq!(iter.next(), Some(OsStr::new("file.txt"))); + assert_eq!(iter.next(), None); + } + } +} + +mod dotup { + use std::{ + collections::HashSet, + path::{Path, PathBuf}, + }; + + use anyhow::Context; + + use crate::depot::{self, Depot, LinkID}; + + #[derive(Debug)] + struct CanonicalPair { + link_id: LinkID, + origin: PathBuf, + destination: PathBuf, + } + + #[derive(Debug)] + pub struct Dotup { + depot: Depot, + depot_dir: PathBuf, + depot_path: PathBuf, + install_base: PathBuf, + } + + impl Dotup { + fn new(depot: Depot, depot_path: PathBuf, install_base: PathBuf) -> anyhow::Result { + assert!(depot_path.is_absolute()); + assert!(depot_path.is_file()); + assert!(install_base.is_absolute()); + assert!(install_base.is_dir()); + let depot_dir = { + let mut d = depot_path.clone(); + d.pop(); + d + }; + Ok(Self { + depot, + depot_dir, + depot_path, + install_base, + }) + } + + pub fn link(&mut self, origin: impl AsRef, destination: impl AsRef) { + let link_result: anyhow::Result<()> = try { + let origin = self.prepare_origin_path(origin.as_ref())?; + let destination = destination.as_ref(); + self.depot.create(origin, destination)?; + }; + match link_result { + Ok(_) => {} + Err(e) => println!("Failed to create link : {e}"), + } + } + + pub fn unlink(&mut self, paths: impl Iterator>) { + for origin in paths { + let unlink_result: anyhow::Result<()> = try { + let origin = self.prepare_origin_path(origin.as_ref())?; + let search_results = self.depot.search(&origin)?; + match search_results { + depot::SearchResult::Found(link_id) => { + self.depot.remove(link_id); + println!("removed link {}", origin.display()); + } + depot::SearchResult::Ancestor(_) | depot::SearchResult::NotFound => { + println!("{} is not linked", origin.display()) + } + } + }; + match unlink_result { + Ok(_) => {} + Err(e) => println!("Failed to unlink {} : {e}", origin.as_ref().display()), + } + } + } + + pub fn install(&self, paths: impl Iterator>) { + let mut already_linked: HashSet = Default::default(); + for origin in paths { + let install_result: anyhow::Result<()> = try { + let origin = self.prepare_origin_path(origin.as_ref())?; + let canonical_pairs = self.canonical_pairs_under(&origin)?; + for pair in canonical_pairs { + if already_linked.contains(&pair.link_id) { + continue; + } + self.install_symlink(&pair.origin, &pair.destination)?; + already_linked.insert(pair.link_id); + } + }; + if let Err(e) = install_result { + println!("error while installing {} : {e}", origin.as_ref().display()); + } + } + } + + pub fn uninstall(&self, paths: impl Iterator>) { + for origin in paths { + let uninstall_result: anyhow::Result<()> = try { + let origin = self.prepare_origin_path(origin.as_ref())?; + let canonical_pairs = self.canonical_pairs_under(&origin)?; + for pair in canonical_pairs { + self.uninstall_symlink(&pair.origin, &pair.destination)?; + } + }; + if let Err(e) = uninstall_result { + println!( + "error while uninstalling {} : {e}", + origin.as_ref().display() + ); + } + } + } + + pub fn mv(&mut self, from: impl Iterator>, to: impl AsRef) { + let to = to.as_ref(); + let from: Vec<_> = from.map(|p| p.as_ref().to_owned()).collect(); + match from.as_slice() { + [] => unreachable!(), + [from] => self.mv_one(from, to), + [from @ ..] => self.mv_many(from, to), + } + } + + fn mv_one(&mut self, from: &Path, to: &Path) {} + + fn mv_many(&mut self, from: &[PathBuf], to: &Path) {} + + fn prepare_origin_path(&self, origin: &Path) -> anyhow::Result { + let canonical = origin + .canonicalize() + .context("Failed to canonicalize origin path")?; + let relative = canonical + .strip_prefix(&self.depot_dir) + .context("Invalid origin path, not under depot directory")?; + Ok(relative.to_owned()) + } + + // returns the canonical pairs (origin, destination) for all links under `path`. + fn canonical_pairs_under(&self, path: &Path) -> anyhow::Result> { + let origin = self.prepare_origin_path(path)?; + let mut paths = Vec::new(); + for link_id in self.depot.links_under(origin)? { + let link_view = self.depot.link_view(link_id); + let relative_origin = link_view.origin(); + let relative_destination = link_view.destination(); + let canonical_origin = self.depot_dir.join(relative_origin); + let canonical_destination = self.install_base.join(relative_destination); + paths.push(CanonicalPair { + link_id, + origin: canonical_origin, + destination: canonical_destination, + }); + } + Ok(paths) + } + + fn install_symlink(&self, origin: &Path, destination: &Path) -> anyhow::Result<()> { + debug_assert!(origin.is_absolute()); + debug_assert!(destination.is_absolute()); + + if let Some(destination_parent) = destination.parent() { + std::fs::create_dir_all(destination_parent) + .context("Failed to create directories")?; + } + + let destination_exists = destination.exists(); + let destination_is_symlink = destination.is_symlink(); + + if destination_exists && !destination_is_symlink { + return Err(anyhow::anyhow!("destination already exists")); + } + + if destination_is_symlink { + std::fs::remove_file(&destination)?; + } + std::os::unix::fs::symlink(origin, destination).context("Failed to create symlink")?; + + Ok(()) + } + + fn uninstall_symlink(&self, origin: &Path, destination: &Path) -> anyhow::Result<()> { + debug_assert!(origin.is_absolute()); + debug_assert!(destination.is_absolute()); + + if destination.is_symlink() { + let symlink_destination = destination.read_link()?.canonicalize()?; + if symlink_destination == origin { + std::fs::remove_file(&destination)?; + } + } + + Ok(()) + } + } + + pub fn read(depot_path: PathBuf, install_base: PathBuf) -> anyhow::Result { + let depot_path = depot_path + .canonicalize() + .context("Failed to canonicalize depot path")?; + let install_base = install_base + .canonicalize() + .context("Failed to canonicalize install base")?; + if !install_base.is_dir() { + return Err(anyhow::anyhow!("Install base must be a directory")); + } + let depot = depot::read(&depot_path)?; + Dotup::new(depot, depot_path, install_base) + } + + pub fn write(dotup: &Dotup) -> anyhow::Result<()> { + depot::write(&dotup.depot_path, &dotup.depot)?; + Ok(()) + } +} + +mod utils { + use std::path::PathBuf; + + use crate::{ + depot::{self, Depot}, + dotup::{self, Dotup}, + Flags, + }; + + pub const DEFAULT_DEPOT_FILE_NAME: &str = ".depot"; + + pub fn read_dotup(flags: &Flags) -> anyhow::Result { + let depot_path = depot_path_from_flags(flags)?; + let install_base = install_base_from_flags(flags); + dotup::read(depot_path, install_base) + } + + pub fn write_dotup(dotup: &Dotup) -> anyhow::Result<()> { + dotup::write(dotup) + } + + pub fn read_depot(flags: &Flags) -> anyhow::Result { + let depot_path = depot_path_from_flags(flags)?; + let depot = depot::read(&depot_path)?; + Ok(depot) + } + + pub fn write_depot(flags: &Flags, depot: &Depot) -> anyhow::Result<()> { + let depot_path = depot_path_from_flags(flags)?; + depot::write(&depot_path, depot)?; + Ok(()) + } + + pub fn depot_path_from_flags(flags: &Flags) -> anyhow::Result { + match flags.depot { + Some(ref path) => Ok(path.clone()), + None => find_depot_path().ok_or_else(|| anyhow::anyhow!("Failed to find depot path")), + } + } + + pub fn default_depot_path() -> PathBuf { + current_working_directory().join(DEFAULT_DEPOT_FILE_NAME) + } + + pub fn find_depot_path() -> Option { + let mut cwd = current_working_directory(); + loop { + let path = cwd.join(DEFAULT_DEPOT_FILE_NAME); + if path.exists() { + break Some(path); + } + if !cwd.pop() { + break None; + } + } + } + + pub fn install_base_from_flags(flags: &Flags) -> PathBuf { + match flags.install_base { + Some(ref path) => path.clone(), + None => default_install_base(), + } + } + + pub fn default_install_base() -> PathBuf { + PathBuf::from(std::env::var("HOME").expect("Failed to obtain HOME environment variable")) + } + + fn current_working_directory() -> PathBuf { + std::env::current_dir().expect("Failed to obtain current working directory") + } +} + +use std::path::PathBuf; + +use clap::Parser; +use utils::DEFAULT_DEPOT_FILE_NAME; + +#[derive(Parser, Debug)] +pub struct Flags { + #[clap(long)] + depot: Option, + #[clap(long)] + install_base: Option, +} + +#[derive(Parser, Debug)] +#[clap(author, version, about, long_about = None)] +struct Args { + #[clap(flatten)] + flags: Flags, + #[clap(subcommand)] + command: SubCommand, +} + +#[derive(Parser, Debug)] +enum SubCommand { + Init(InitArgs), + Link(LinkArgs), + Unlink(UnlinkArgs), + Install(InstallArgs), + Uninstall(UninstallArgs), + Mv(MvArgs), +} + +fn main() -> anyhow::Result<()> { + let args = Args::parse(); + match args.command { + SubCommand::Init(cmd_args) => command_init(args.flags, cmd_args), + SubCommand::Link(cmd_args) => command_link(args.flags, cmd_args), + SubCommand::Unlink(cmd_args) => command_unlink(args.flags, cmd_args), + SubCommand::Install(cmd_args) => command_install(args.flags, cmd_args), + SubCommand::Uninstall(cmd_args) => command_uninstall(args.flags, cmd_args), + SubCommand::Mv(cmd_args) => command_mv(args.flags, cmd_args), + } +} + +#[derive(Parser, Debug)] +struct InitArgs { + path: Option, +} + +fn command_init(_global_flags: Flags, args: InitArgs) -> anyhow::Result<()> { + let depot_path = { + let mut path = args.path.unwrap_or_else(utils::default_depot_path); + if path.is_dir() { + path = path.join(DEFAULT_DEPOT_FILE_NAME); + } + path + }; + + if depot_path.exists() { + println!("Depot at {} already exists", depot_path.display()); + } else { + depot::write(&depot_path, &Default::default())?; + println!("Depot initialized at {}", depot_path.display()); + } + + Ok(()) +} + +#[derive(Parser, Debug)] +struct LinkArgs { + origin: PathBuf, + destination: PathBuf, +} + +fn command_link(global_flags: Flags, args: LinkArgs) -> anyhow::Result<()> { + let mut dotup = utils::read_dotup(&global_flags)?; + dotup.link(args.origin, args.destination); + utils::write_dotup(&dotup)?; + Ok(()) +} + +#[derive(Parser, Debug)] +struct UnlinkArgs { + paths: Vec, +} + +fn command_unlink(global_flags: Flags, args: UnlinkArgs) -> anyhow::Result<()> { + let mut dotup = utils::read_dotup(&global_flags)?; + dotup.unlink(args.paths.into_iter()); + utils::write_dotup(&dotup)?; + Ok(()) +} + +#[derive(Parser, Debug)] +struct InstallArgs { + paths: Vec, +} + +fn command_install(global_flags: Flags, args: InstallArgs) -> anyhow::Result<()> { + let dotup = utils::read_dotup(&global_flags)?; + dotup.install(args.paths.into_iter()); + utils::write_dotup(&dotup)?; + Ok(()) +} + +#[derive(Parser, Debug)] +struct UninstallArgs { + paths: Vec, +} + +fn command_uninstall(global_flags: Flags, args: UninstallArgs) -> anyhow::Result<()> { + let dotup = utils::read_dotup(&global_flags)?; + dotup.uninstall(args.paths.into_iter()); + utils::write_dotup(&dotup)?; + Ok(()) +} + +#[derive(Parser, Debug)] +struct MvArgs { + paths: Vec, +} + +fn command_mv(global_flags: Flags, args: MvArgs) -> anyhow::Result<()> { + let mut dotup = utils::read_dotup(&global_flags)?; + let mut paths = args.paths; + if paths.len() < 2 { + return Err(anyhow::anyhow!("mv requires atleast 2 arguments")); + } + let to = paths.pop().unwrap(); + let from = paths; + dotup.mv(from.iter(), &to); + utils::write_dotup(&dotup)?; + Ok(()) +} -- cgit From a2117085b26557a27e8068c6caa4037e2f9f1a7f Mon Sep 17 00:00:00 2001 From: diogo464 Date: Mon, 7 Feb 2022 09:09:57 +0000 Subject: snapshot --- Cargo.lock | 10 ++ Cargo.toml | 1 + src/main.rs | 368 +++++++++++++++++++++++++++++++++++++++++++++++++++--------- 3 files changed, 327 insertions(+), 52 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 61c1d16..835a197 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + [[package]] name = "anyhow" version = "1.0.53" @@ -65,6 +74,7 @@ dependencies = [ name = "dotup" version = "0.0.0" dependencies = [ + "ansi_term", "anyhow", "clap", "serde", diff --git a/Cargo.toml b/Cargo.toml index ce22ac1..ea24a57 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +ansi_term = "0.12.1" anyhow = "1.0.53" clap = { version = "3.0.14", features = ["derive"] } serde = { version = "1.0.136", features = ["derive"] } diff --git a/src/main.rs b/src/main.rs index e2a659b..1559ede 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,7 @@ #![feature(try_blocks)] + +// TODO: rewrite all errors so they start with lower case + pub mod depot { use std::{ collections::HashSet, @@ -7,7 +10,7 @@ pub mod depot { path::{Path, PathBuf}, }; - use slotmap::SlotMap; + use slotmap::{Key, SlotMap}; pub use disk::{read, write}; @@ -134,6 +137,10 @@ pub mod depot { } } + /// moves the link specified by `link_id` to the path at `destination`. + /// if the link is already at the destination nothing is done. + /// if the destination is another link that that link is removed. + /// if the destination is under another link then an error is returned. pub fn move_link( &mut self, link_id: LinkID, @@ -158,10 +165,12 @@ pub mod depot { let node_parent_id = node.parent; let node_link_id = *node_link_id; assert_ne!(link_id, node_link_id); - self.remove(node_link_id); self.node_child_remove(link_parent_node_id, link_node_id); self.node_child_add(node_parent_id, link_node_id); self.node_set_parent(link_node_id, node_parent_id); + self.remove(node_link_id); + let new_origin = self.node_build_path(link_node_id); + self.links[link_id].origin = new_origin; Ok(()) } NodeKind::Directory(..) => Err(anyhow::anyhow!( @@ -176,8 +185,10 @@ pub mod depot { )), NodeKind::Directory(_) => { let new_node_id = self.node_create_link(destination, link_id); + let new_origin = self.node_build_path(new_node_id); self.node_remove(link_node_id); self.links[link_id].node_id = new_node_id; + self.links[link_id].origin = new_origin; Ok(()) } } @@ -207,6 +218,14 @@ pub mod depot { Ok(self.search_unchecked(&origin)) } + /// finds the link at origin. + pub fn find(&self, origin: impl AsRef) -> anyhow::Result> { + match self.search(origin)? { + SearchResult::Found(link_id) => Ok(Some(link_id)), + SearchResult::Ancestor(_) | SearchResult::NotFound => Ok(None), + } + } + /// returns an iterator for all the links at or under the given path. pub fn links_under( &self, @@ -391,6 +410,22 @@ pub mod depot { } } + /// build the path that references this node. + fn node_build_path(&self, node_id: NodeID) -> PathBuf { + fn recursive_helper(nodes: &SlotMap, nid: NodeID, pbuf: &mut PathBuf) { + if nid.is_null() { + return; + } + let parent_id = nodes[nid].parent; + recursive_helper(nodes, parent_id, pbuf); + pbuf.push(&nodes[nid].comp); + } + + let mut node_path = PathBuf::default(); + recursive_helper(&self.nodes, node_id, &mut node_path); + node_path + } + fn node_set_parent(&mut self, node_id: NodeID, parent: NodeID) { self.nodes[node_id].parent = parent; } @@ -461,6 +496,10 @@ pub mod depot { } } + /// a verified path is a path that: + /// + is not empty + /// + is relative + /// + does not contain Prefix/RootDir/ParentDir fn verify_path(path: &Path) -> anyhow::Result<()> { // make sure the path is not empty // make sure the path is relative @@ -523,11 +562,14 @@ pub mod depot { let f1 = depot.create("d1/f1", "d1/f1").unwrap(); let _f2 = depot.create("d1/f2", "d1/f2").unwrap(); + depot.move_link(f1, "").unwrap_err(); depot.move_link(f1, "d1/f2/f1").unwrap_err(); - depot.move_link(f1, "d1").unwrap_err(); - depot.move_link(f1, "").unwrap(); - depot.move_link(f1, "d2/f1").unwrap(); + depot.move_link(f1, "d1/f2").unwrap(); + depot.move_link(f1, "f1").unwrap(); + assert_eq!(depot.link_view(f1).origin(), Path::new("f1")); + depot.move_link(f1, "f2").unwrap(); + assert_eq!(depot.link_view(f1).origin(), Path::new("f2")); } #[test] @@ -576,9 +618,13 @@ mod dotup { path::{Path, PathBuf}, }; + use ansi_term::Color; use anyhow::Context; - use crate::depot::{self, Depot, LinkID}; + use crate::{ + depot::{self, Depot, LinkID}, + utils, + }; #[derive(Debug)] struct CanonicalPair { @@ -658,7 +704,7 @@ mod dotup { if already_linked.contains(&pair.link_id) { continue; } - self.install_symlink(&pair.origin, &pair.destination)?; + self.symlink_install(&pair.origin, &pair.destination)?; already_linked.insert(pair.link_id); } }; @@ -674,7 +720,7 @@ mod dotup { let origin = self.prepare_origin_path(origin.as_ref())?; let canonical_pairs = self.canonical_pairs_under(&origin)?; for pair in canonical_pairs { - self.uninstall_symlink(&pair.origin, &pair.destination)?; + self.symlink_uninstall(&pair.origin, &pair.destination)?; } }; if let Err(e) = uninstall_result { @@ -686,50 +732,182 @@ mod dotup { } } - pub fn mv(&mut self, from: impl Iterator>, to: impl AsRef) { - let to = to.as_ref(); - let from: Vec<_> = from.map(|p| p.as_ref().to_owned()).collect(); - match from.as_slice() { - [] => unreachable!(), - [from] => self.mv_one(from, to), - [from @ ..] => self.mv_many(from, to), + pub fn mv( + &mut self, + origins: impl Iterator>, + destination: impl AsRef, + ) { + let origins = { + let mut v = Vec::new(); + for origin in origins { + match self.prepare_origin_path(origin.as_ref()) { + Ok(origin) => v.push(origin), + Err(e) => { + println!("invalid link {} : {e}", origin.as_ref().display()); + return; + } + } + } + v + }; + let destination = destination.as_ref(); + + // if we are moving multiple links then the destination must be a directory + if origins.len() > 1 && destination.is_dir() { + println!("destination must be a directory"); + return; + } + + for origin in origins { + if let Err(e) = self.mv_one(&origin, destination) { + println!("error moving link {} : {e}", origin.display()); + } + } + } + + fn mv_one(&mut self, origin: &Path, destination: &Path) -> anyhow::Result<()> { + let link_id = match self.depot.find(origin)? { + Some(link_id) => link_id, + None => { + return Err(anyhow::anyhow!(format!( + "{} is not a link", + origin.display() + ))) + } + }; + let is_installed = self.symlink_is_installed_by_link_id(link_id)?; + let original_origin = self.depot.link_view(link_id).origin().to_owned(); + self.depot.move_link(link_id, destination)?; + // move the actual file on disk + if let Err(e) = std::fs::rename(origin, destination).context("Failed to move file") { + // unwrap: moving the link back to its origin place has to work + self.depot.move_link(link_id, original_origin).unwrap(); + return Err(e); + } + // reinstall because we just moved the origin + if is_installed { + self.symlink_install_by_link_id(link_id) + .context("failed to reinstall link while moving")?; + } + Ok(()) + } + + pub fn status(&self) { + let status_result: anyhow::Result<()> = try { + let curr_dir = utils::current_working_directory(); + let (dirs, files) = utils::collect_read_dir_split(curr_dir)?; + }; + if let Err(e) = status_result { + println!("error while displaying status : {e}"); } } - fn mv_one(&mut self, from: &Path, to: &Path) {} + pub fn status2(&self) { + let status_result: anyhow::Result<()> = try { + let curr_dir = &std::env::current_dir()?; + let (dirs, files) = utils::collect_read_dir_split(curr_dir)?; + for path in dirs.iter().chain(files.iter()) { + self.print_status_for(&path, 0)?; + } + }; + if let Err(e) = status_result { + println!("error while displaying status : {e}"); + } + } - fn mv_many(&mut self, from: &[PathBuf], to: &Path) {} + fn print_status_for(&self, path: &Path, depth: u32) -> anyhow::Result<()> { + fn print_depth(d: u32) { + for _ in 0..d { + print!(" "); + } + } + + let origin = self.prepare_origin_path(path)?; + if path.is_dir() { + print_depth(depth); + let file_name = path.file_name().unwrap().to_str().unwrap_or_default(); + if let Some(link_id) = self.depot.find(&origin)? { + let installed = self.symlink_is_installed_by_link_id(link_id)?; + let link_view = self.depot.link_view(link_id); + let destination = link_view.destination().display().to_string(); + let color = if installed { Color::Green } else { Color::Red }; + println!( + "{}/ ---> {}", + color.paint(file_name), + Color::Blue.paint(destination) + ); + } else { + println!("{}/", file_name); + let (dirs, files) = utils::collect_read_dir_split(path)?; + for path in dirs.iter().chain(files.iter()) { + self.print_status_for(&path, depth + 1)?; + } + } + } else if path.is_file() || path.is_symlink() { + print_depth(depth); + let file_name = path.file_name().unwrap().to_str().unwrap_or_default(); + if let Some(link_id) = self.depot.find(&origin)? { + let installed = self.symlink_is_installed_by_link_id(link_id)?; + let link_view = self.depot.link_view(link_id); + let destination = link_view.destination().display().to_string(); + let color = if installed { Color::Green } else { Color::Red }; + + println!( + "{} ---> {}", + color.paint(file_name), + Color::Blue.paint(destination) + ); + } else { + println!("{}", file_name); + } + } + Ok(()) + } fn prepare_origin_path(&self, origin: &Path) -> anyhow::Result { - let canonical = origin - .canonicalize() - .context("Failed to canonicalize origin path")?; + let canonical = utils::weakly_canonical(origin); let relative = canonical .strip_prefix(&self.depot_dir) .context("Invalid origin path, not under depot directory")?; Ok(relative.to_owned()) } - // returns the canonical pairs (origin, destination) for all links under `path`. + // returns the canonical pairs for all links under `path`. fn canonical_pairs_under(&self, path: &Path) -> anyhow::Result> { let origin = self.prepare_origin_path(path)?; - let mut paths = Vec::new(); + let mut canonical_pairs = Vec::new(); for link_id in self.depot.links_under(origin)? { - let link_view = self.depot.link_view(link_id); - let relative_origin = link_view.origin(); - let relative_destination = link_view.destination(); - let canonical_origin = self.depot_dir.join(relative_origin); - let canonical_destination = self.install_base.join(relative_destination); - paths.push(CanonicalPair { - link_id, - origin: canonical_origin, - destination: canonical_destination, - }); + canonical_pairs.push(self.canonical_pair_from_link_id(link_id)); } - Ok(paths) + Ok(canonical_pairs) + } + + fn symlink_is_installed_by_link_id(&self, link_id: LinkID) -> anyhow::Result { + let canonical_pair = self.canonical_pair_from_link_id(link_id); + self.symlink_is_installed(&canonical_pair.origin, &canonical_pair.destination) } - fn install_symlink(&self, origin: &Path, destination: &Path) -> anyhow::Result<()> { + fn symlink_is_installed(&self, origin: &Path, destination: &Path) -> anyhow::Result { + debug_assert!(origin.is_absolute()); + debug_assert!(destination.is_absolute()); + + if destination.is_symlink() { + let symlink_destination = destination.read_link()?; + match symlink_destination.canonicalize() { + Ok(canonicalized) => Ok(origin == canonicalized), + Err(_) => Ok(false), + } + } else { + Ok(false) + } + } + + fn symlink_install_by_link_id(&self, link_id: LinkID) -> anyhow::Result<()> { + let canonical_pair = self.canonical_pair_from_link_id(link_id); + self.symlink_install(&canonical_pair.origin, &canonical_pair.destination) + } + + fn symlink_install(&self, origin: &Path, destination: &Path) -> anyhow::Result<()> { debug_assert!(origin.is_absolute()); debug_assert!(destination.is_absolute()); @@ -753,7 +931,7 @@ mod dotup { Ok(()) } - fn uninstall_symlink(&self, origin: &Path, destination: &Path) -> anyhow::Result<()> { + fn symlink_uninstall(&self, origin: &Path, destination: &Path) -> anyhow::Result<()> { debug_assert!(origin.is_absolute()); debug_assert!(destination.is_absolute()); @@ -766,6 +944,19 @@ mod dotup { Ok(()) } + + fn canonical_pair_from_link_id(&self, link_id: LinkID) -> CanonicalPair { + let link_view = self.depot.link_view(link_id); + let relative_origin = link_view.origin(); + let relative_destination = link_view.destination(); + let canonical_origin = self.depot_dir.join(relative_origin); + let canonical_destination = self.install_base.join(relative_destination); + CanonicalPair { + link_id, + origin: canonical_origin, + destination: canonical_destination, + } + } } pub fn read(depot_path: PathBuf, install_base: PathBuf) -> anyhow::Result { @@ -789,16 +980,26 @@ mod dotup { } mod utils { - use std::path::PathBuf; + use std::path::{Component, Path, PathBuf}; use crate::{ - depot::{self, Depot}, dotup::{self, Dotup}, Flags, }; pub const DEFAULT_DEPOT_FILE_NAME: &str = ".depot"; + /// collects the result of std::fs::read_dir into two vecs, the first one contains all the + /// directories and the second one all the files. + pub fn collect_read_dir_split( + dir: impl AsRef, + ) -> anyhow::Result<(Vec, Vec)> { + Ok(std::fs::read_dir(dir)? + .filter_map(|e| e.ok()) + .map(|e| e.path()) + .partition(|p| p.is_dir())) + } + pub fn read_dotup(flags: &Flags) -> anyhow::Result { let depot_path = depot_path_from_flags(flags)?; let install_base = install_base_from_flags(flags); @@ -809,18 +1010,6 @@ mod utils { dotup::write(dotup) } - pub fn read_depot(flags: &Flags) -> anyhow::Result { - let depot_path = depot_path_from_flags(flags)?; - let depot = depot::read(&depot_path)?; - Ok(depot) - } - - pub fn write_depot(flags: &Flags, depot: &Depot) -> anyhow::Result<()> { - let depot_path = depot_path_from_flags(flags)?; - depot::write(&depot_path, depot)?; - Ok(()) - } - pub fn depot_path_from_flags(flags: &Flags) -> anyhow::Result { match flags.depot { Some(ref path) => Ok(path.clone()), @@ -855,10 +1044,76 @@ mod utils { pub fn default_install_base() -> PathBuf { PathBuf::from(std::env::var("HOME").expect("Failed to obtain HOME environment variable")) } + pub fn weakly_canonical(path: impl AsRef) -> PathBuf { + let cwd = current_working_directory(); + weakly_canonical_cwd(path, cwd) + } + + fn weakly_canonical_cwd(path: impl AsRef, cwd: PathBuf) -> PathBuf { + // Adapated from + // https://github.com/rust-lang/cargo/blob/fede83ccf973457de319ba6fa0e36ead454d2e20/src/cargo/util/paths.rs#L61 + let path = path.as_ref(); + + let mut components = path.components().peekable(); + let mut canonical = cwd; + let prefix = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() { + components.next(); + PathBuf::from(c.as_os_str()) + } else { + PathBuf::new() + }; + + for component in components { + match component { + Component::Prefix(_) => unreachable!(), + Component::RootDir => { + canonical = prefix.clone(); + canonical.push(component.as_os_str()) + } + Component::CurDir => {} + Component::ParentDir => { + canonical.pop(); + } + Component::Normal(p) => canonical.push(p), + }; + } + + canonical + } - fn current_working_directory() -> PathBuf { + pub fn current_working_directory() -> PathBuf { std::env::current_dir().expect("Failed to obtain current working directory") } + + #[cfg(test)] + mod tests { + use super::*; + + #[test] + fn weak_canonical_test() { + let cwd = PathBuf::from("/home/user"); + assert_eq!( + PathBuf::from("/home/dest"), + weakly_canonical_cwd("../dest", cwd.clone()) + ); + assert_eq!( + PathBuf::from("/home/dest/configs/init.vim"), + weakly_canonical_cwd("../dest/configs/init.vim", cwd.clone()) + ); + assert_eq!( + PathBuf::from("/dest/configs/init.vim"), + weakly_canonical_cwd("/dest/configs/init.vim", cwd.clone()) + ); + assert_eq!( + PathBuf::from("/home/user/configs/nvim/lua/setup.lua"), + weakly_canonical_cwd("./configs/nvim/lua/setup.lua", cwd.clone()) + ); + assert_eq!( + PathBuf::from("/home/user/configs/nvim/lua/setup.lua"), + weakly_canonical_cwd("configs/nvim/lua/setup.lua", cwd.clone()) + ); + } + } } use std::path::PathBuf; @@ -891,6 +1146,7 @@ enum SubCommand { Install(InstallArgs), Uninstall(UninstallArgs), Mv(MvArgs), + Status(StatusArgs), } fn main() -> anyhow::Result<()> { @@ -902,6 +1158,7 @@ fn main() -> anyhow::Result<()> { SubCommand::Install(cmd_args) => command_install(args.flags, cmd_args), SubCommand::Uninstall(cmd_args) => command_uninstall(args.flags, cmd_args), SubCommand::Mv(cmd_args) => command_mv(args.flags, cmd_args), + SubCommand::Status(cmd_args) => command_status(args.flags, cmd_args), } } @@ -962,7 +1219,6 @@ struct InstallArgs { fn command_install(global_flags: Flags, args: InstallArgs) -> anyhow::Result<()> { let dotup = utils::read_dotup(&global_flags)?; dotup.install(args.paths.into_iter()); - utils::write_dotup(&dotup)?; Ok(()) } @@ -974,7 +1230,6 @@ struct UninstallArgs { fn command_uninstall(global_flags: Flags, args: UninstallArgs) -> anyhow::Result<()> { let dotup = utils::read_dotup(&global_flags)?; dotup.uninstall(args.paths.into_iter()); - utils::write_dotup(&dotup)?; Ok(()) } @@ -995,3 +1250,12 @@ fn command_mv(global_flags: Flags, args: MvArgs) -> anyhow::Result<()> { utils::write_dotup(&dotup)?; Ok(()) } + +#[derive(Parser, Debug)] +struct StatusArgs {} + +fn command_status(global_flags: Flags, _args: StatusArgs) -> anyhow::Result<()> { + let dotup = utils::read_dotup(&global_flags)?; + dotup.status(); + Ok(()) +} -- cgit From cbb2edb0b523f2494fd543857195792a8eda1b62 Mon Sep 17 00:00:00 2001 From: diogo464 Date: Mon, 7 Feb 2022 14:35:11 +0000 Subject: snapshot --- src/main.rs | 572 ++++++++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 459 insertions(+), 113 deletions(-) diff --git a/src/main.rs b/src/main.rs index 1559ede..1cb68d4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -24,6 +24,12 @@ pub mod depot { NotFound, } + #[derive(Debug, Clone, PartialEq, Eq)] + pub enum DirNode { + Link(LinkID), + Directory(PathBuf), + } + #[derive(Debug)] pub struct LinkView<'a> { link_id: LinkID, @@ -96,15 +102,15 @@ pub mod depot { } impl Depot { - pub fn create( + pub fn link_create( &mut self, origin: impl AsRef, destination: impl AsRef, ) -> anyhow::Result { let origin = origin.as_ref(); let destination = destination.as_ref(); - verify_path(origin)?; - verify_path(destination)?; + verify_link_path(origin)?; + verify_link_path(destination)?; // example // origin = fish/config.fish @@ -141,13 +147,13 @@ pub mod depot { /// if the link is already at the destination nothing is done. /// if the destination is another link that that link is removed. /// if the destination is under another link then an error is returned. - pub fn move_link( + pub fn link_move( &mut self, link_id: LinkID, destination: impl AsRef, ) -> anyhow::Result<()> { let destination = destination.as_ref(); - verify_path(destination)?; + verify_link_path(destination)?; let link_node_id = self.links[link_id].node_id; let link_parent_node_id = self.nodes[link_node_id].parent; @@ -168,7 +174,7 @@ pub mod depot { self.node_child_remove(link_parent_node_id, link_node_id); self.node_child_add(node_parent_id, link_node_id); self.node_set_parent(link_node_id, node_parent_id); - self.remove(node_link_id); + self.link_remove(node_link_id); let new_origin = self.node_build_path(link_node_id); self.links[link_id].origin = new_origin; Ok(()) @@ -195,7 +201,7 @@ pub mod depot { } } - pub fn remove(&mut self, link_id: LinkID) { + pub fn link_remove(&mut self, link_id: LinkID) { let node_id = self.links[link_id].node_id; self.node_remove(node_id); self.links.remove(link_id); @@ -212,15 +218,18 @@ pub mod depot { /// returns SearchResult::Found(..) if there is a link at `origin`. /// returns SearchResult::Ancestor(..) if an ancestor of `origin` is linked. /// returns SearchResult::NotFound otherwise. - pub fn search(&self, origin: impl AsRef) -> anyhow::Result { + pub fn link_search(&self, origin: impl AsRef) -> anyhow::Result { let origin = origin.as_ref(); verify_path(origin)?; + if origin.components().next().is_none() { + return Ok(SearchResult::NotFound); + } Ok(self.search_unchecked(&origin)) } /// finds the link at origin. - pub fn find(&self, origin: impl AsRef) -> anyhow::Result> { - match self.search(origin)? { + pub fn link_find(&self, origin: impl AsRef) -> anyhow::Result> { + match self.link_search(origin)? { SearchResult::Found(link_id) => Ok(Some(link_id)), SearchResult::Ancestor(_) | SearchResult::NotFound => Ok(None), } @@ -248,17 +257,55 @@ pub mod depot { Ok(link_ids.into_iter()) } + pub fn has_links_under(&self, path: impl AsRef) -> anyhow::Result { + let path = path.as_ref(); + verify_path(path)?; + + match self.node_find(path) { + Some(node_id) => match &self.nodes[node_id].kind { + NodeKind::Link(_) => Ok(true), + NodeKind::Directory(children) => Ok(!children.is_empty()), + }, + None => Ok(false), + } + } + /// returns true if the `path` is a link or contains an ancestor that is linked. /// returns false otherwise. - pub fn is_linked(&self, path: impl AsRef) -> bool { - match self.search(path) { + pub fn link_exists(&self, path: impl AsRef) -> bool { + match self.link_search(path) { Ok(SearchResult::Found(..)) | Ok(SearchResult::Ancestor(..)) => true, _ => false, } } + pub fn read_dir( + &self, + path: impl AsRef, + ) -> anyhow::Result + '_> { + let path = path.as_ref(); + verify_path(path)?; + + let node_id = match self.node_find(path) { + Some(node_id) => node_id, + None => return Err(anyhow::anyhow!("Directory does not exist")), + }; + let node = &self.nodes[node_id]; + let children = match &node.kind { + NodeKind::Link(_) => return Err(anyhow::anyhow!("Path is not a directory")), + NodeKind::Directory(children) => children, + }; + Ok(children.iter().map(|id| { + let node = &self.nodes[*id]; + match &node.kind { + NodeKind::Link(link_id) => DirNode::Link(*link_id), + NodeKind::Directory(_) => DirNode::Directory(self.node_build_path(*id)), + } + })) + } + fn search_unchecked(&self, origin: &Path) -> SearchResult { - debug_assert!(verify_path(origin).is_ok()); + debug_assert!(verify_link_path(origin).is_ok()); let mut origin_comps = iter_path_comps(&origin); let mut curr_node = self.root; @@ -300,12 +347,12 @@ pub mod depot { } /// all the nodes up to the node to be created have to be directory nodes. - /// `path` must be a verified path. + /// `path` must be a verified link path. fn node_create_link(&mut self, path: &Path, link_id: LinkID) -> NodeID { - assert!(verify_path(path).is_ok()); + assert!(verify_link_path(path).is_ok()); let mut curr_node_id = self.root; let mut path_comps = iter_path_comps(path).peekable(); - // unwrap: a verified path has atleast 1 component + // unwrap: a verified link path has atleast 1 component let mut curr_path_comp = path_comps.next().unwrap(); while path_comps.peek().is_some() { @@ -343,6 +390,11 @@ pub mod depot { let mut origin_comps = iter_path_comps(&path).peekable(); let mut curr_node = self.root; + + if origin_comps.peek().is_none() { + return (self.root, true); + } + 'outer: loop { let node = &self.nodes[curr_node]; match origin_comps.next() { @@ -475,7 +527,7 @@ pub mod depot { let mut depot = Depot::default(); for disk_link in disk_links { depot - .create(disk_link.origin, disk_link.destination) + .link_create(disk_link.origin, disk_link.destination) .context("Failed to build depot from file. File is in an invalid state")?; } Ok(depot) @@ -500,13 +552,20 @@ pub mod depot { /// + is not empty /// + is relative /// + does not contain Prefix/RootDir/ParentDir - fn verify_path(path: &Path) -> anyhow::Result<()> { + fn verify_link_path(path: &Path) -> anyhow::Result<()> { // make sure the path is not empty - // make sure the path is relative - // make sure the path does not contain '.' or '..' if path.components().next().is_none() { return Err(anyhow::anyhow!("Path cannot be empty")); } + verify_path(path) + } + /// a verified path is a path that: + /// + is not empty + /// + is relative + /// + does not contain Prefix/RootDir/ParentDir + fn verify_path(path: &Path) -> anyhow::Result<()> { + // make sure the path is relative + // make sure the path does not contain '.' or '..' for component in path.components() { match component { std::path::Component::Prefix(_) => { @@ -540,63 +599,180 @@ pub mod depot { use super::*; #[test] - fn test_depot_create() { + fn test_verify_path() { + verify_path(Path::new("")).unwrap(); + verify_path(Path::new("f1")).unwrap(); + verify_path(Path::new("d1/f1")).unwrap(); + verify_path(Path::new("d1/f1.txt")).unwrap(); + verify_path(Path::new("d1/./f1.txt")).unwrap(); + + verify_path(Path::new("/")).unwrap_err(); + verify_path(Path::new("./f1")).unwrap_err(); + verify_path(Path::new("/d1/f1")).unwrap_err(); + verify_path(Path::new("d1/../f1.txt")).unwrap_err(); + verify_path(Path::new("/d1/../f1.txt")).unwrap_err(); + } + + #[test] + fn test_verify_link_path() { + verify_link_path(Path::new("f1")).unwrap(); + verify_link_path(Path::new("d1/f1")).unwrap(); + verify_link_path(Path::new("d1/f1.txt")).unwrap(); + verify_link_path(Path::new("d1/./f1.txt")).unwrap(); + + verify_link_path(Path::new("")).unwrap_err(); + verify_link_path(Path::new("/")).unwrap_err(); + verify_link_path(Path::new("./f1")).unwrap_err(); + verify_link_path(Path::new("/d1/f1")).unwrap_err(); + verify_link_path(Path::new("d1/../f1.txt")).unwrap_err(); + verify_link_path(Path::new("/d1/../f1.txt")).unwrap_err(); + } + + #[test] + fn test_depot_link_create() { let mut depot = Depot::default(); - depot.create("", "dest1.txt").unwrap_err(); - depot.create("comp1.txt", "").unwrap_err(); - depot.create("", "").unwrap_err(); + depot.link_create("", "dest1.txt").unwrap_err(); + depot.link_create("comp1.txt", "").unwrap_err(); + depot.link_create("", "").unwrap_err(); - depot.create("comp1.txt", "dest1.txt").unwrap(); - depot.create("comp1.txt", "dest1_updated.txt").unwrap(); + depot.link_create("comp1.txt", "dest1.txt").unwrap(); + depot.link_create("comp1.txt", "dest1_updated.txt").unwrap(); + depot + .link_create("./comp1.txt", "dest1_updated.txt") + .unwrap_err(); + depot.link_create("/comp1.txt", "dest1.txt").unwrap_err(); + depot.link_create("dir1/", "destdir1/").unwrap(); depot - .create("./comp1.txt", "dest1_updated.txt") + .link_create("dir1/file1.txt", "destfile1.txt") .unwrap_err(); - depot.create("/comp1.txt", "dest1.txt").unwrap_err(); - depot.create("dir1/", "destdir1/").unwrap(); - depot.create("dir1/file1.txt", "destfile1.txt").unwrap_err(); } #[test] - fn test_depot_move_link() { + fn test_depot_link_move() { let mut depot = Depot::default(); - let f1 = depot.create("d1/f1", "d1/f1").unwrap(); - let _f2 = depot.create("d1/f2", "d1/f2").unwrap(); + let f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); + let _f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); - depot.move_link(f1, "").unwrap_err(); - depot.move_link(f1, "d1/f2/f1").unwrap_err(); + depot.link_move(f1, "").unwrap_err(); + depot.link_move(f1, "d1/f2/f1").unwrap_err(); - depot.move_link(f1, "d1/f2").unwrap(); - depot.move_link(f1, "f1").unwrap(); + depot.link_move(f1, "d1/f2").unwrap(); + depot.link_move(f1, "f1").unwrap(); assert_eq!(depot.link_view(f1).origin(), Path::new("f1")); - depot.move_link(f1, "f2").unwrap(); + depot.link_move(f1, "f2").unwrap(); assert_eq!(depot.link_view(f1).origin(), Path::new("f2")); } #[test] - fn test_depot_remove() { + fn test_depot_links_under() { let mut depot = Depot::default(); - let f1 = depot.create("d1/f1", "d1/f1").unwrap(); - assert_eq!(depot.search("d1/f1").unwrap(), SearchResult::Found(f1)); - depot.remove(f1); - assert_eq!(depot.search("d1/f1").unwrap(), SearchResult::NotFound); + let f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); + let f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); + let f3 = depot.link_create("d1/f3", "d1/f3").unwrap(); + let f4 = depot.link_create("d1/d2/f4", "d2/f4").unwrap(); + let d3 = depot.link_create("d3", "d3").unwrap(); + + let under_f1 = depot.links_under("d1/f1").unwrap().collect::>(); + assert_eq!(under_f1, vec![f1]); + + let under_d1 = depot.links_under("d1").unwrap().collect::>(); + let expected_under_d1 = vec![f1, f2, f3, f4]; + assert!( + under_d1.len() == expected_under_d1.len() + && expected_under_d1.iter().all(|x| under_d1.contains(x)) + ); + + let under_d2 = depot.links_under("d2").unwrap().collect::>(); + assert_eq!(under_d2, vec![]); + + let under_d3 = depot.links_under("d3").unwrap().collect::>(); + assert_eq!(under_d3, vec![d3]); + + let under_root = depot.links_under("").unwrap().collect::>(); + let expected_under_root = vec![f1, f2, f3, f4, d3]; + assert!( + under_root.len() == expected_under_root.len() + && expected_under_root.iter().all(|x| under_root.contains(x)) + ); + } + + #[test] + fn test_depot_has_links_under() { + let mut depot = Depot::default(); + let _f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); + let _f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); + let _f3 = depot.link_create("d1/f3", "d1/f3").unwrap(); + let _f4 = depot.link_create("d1/d2/f4", "d2/f4").unwrap(); + let _d3 = depot.link_create("d3", "d3").unwrap(); + + assert!(depot.has_links_under("").unwrap()); + assert!(depot.has_links_under("d1").unwrap()); + assert!(depot.has_links_under("d3").unwrap()); + assert!(depot.has_links_under("d1/f1").unwrap()); + assert!(depot.has_links_under("d1/d2").unwrap()); + assert!(depot.has_links_under("d1/d2/f4").unwrap()); + + assert!(!depot.has_links_under("d2").unwrap()); + assert!(!depot.has_links_under("d4").unwrap()); + assert!(!depot.has_links_under("d1/d2/f4/f5").unwrap()); + } + + #[test] + fn test_depot_link_remove() { + let mut depot = Depot::default(); + let f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); + assert_eq!(depot.link_search("d1/f1").unwrap(), SearchResult::Found(f1)); + depot.link_remove(f1); + assert_eq!(depot.link_search("d1/f1").unwrap(), SearchResult::NotFound); } #[test] - fn test_depot_search() { + fn test_depot_link_search() { let mut depot = Depot::default(); - let f1 = depot.create("d1/f1", "d1/f1").unwrap(); - let f2 = depot.create("d1/f2", "d1/f2").unwrap(); - let f3 = depot.create("d1/f3", "d1/f3").unwrap(); - let f4 = depot.create("d1/d2/f4", "d2/f4").unwrap(); - let d3 = depot.create("d3", "d3").unwrap(); + let f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); + let f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); + let f3 = depot.link_create("d1/f3", "d1/f3").unwrap(); + let f4 = depot.link_create("d1/d2/f4", "d2/f4").unwrap(); + let d3 = depot.link_create("d3", "d3").unwrap(); + + assert_eq!(depot.link_search("d1").unwrap(), SearchResult::NotFound,); + assert_eq!(depot.link_search("d1/f1").unwrap(), SearchResult::Found(f1),); + assert_eq!(depot.link_search("d1/f2").unwrap(), SearchResult::Found(f2),); + assert_eq!(depot.link_search("d1/f3").unwrap(), SearchResult::Found(f3),); + assert_eq!( + depot.link_search("d1/d2/f4").unwrap(), + SearchResult::Found(f4), + ); + assert_eq!( + depot.link_search("d1/d2/f5").unwrap(), + SearchResult::NotFound, + ); + assert_eq!( + depot.link_search("d3/f6").unwrap(), + SearchResult::Ancestor(d3), + ); + } - assert_eq!(depot.search("d1").unwrap(), SearchResult::NotFound,); - assert_eq!(depot.search("d1/f1").unwrap(), SearchResult::Found(f1),); - assert_eq!(depot.search("d1/f2").unwrap(), SearchResult::Found(f2),); - assert_eq!(depot.search("d1/f3").unwrap(), SearchResult::Found(f3),); - assert_eq!(depot.search("d1/d2/f4").unwrap(), SearchResult::Found(f4),); - assert_eq!(depot.search("d1/d2/f5").unwrap(), SearchResult::NotFound,); - assert_eq!(depot.search("d3/f6").unwrap(), SearchResult::Ancestor(d3),); + #[test] + fn test_depot_read_dir() { + let mut depot = Depot::default(); + let f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); + let f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); + let f3 = depot.link_create("d1/f3", "d1/f3").unwrap(); + let _f4 = depot.link_create("d1/d2/f4", "d2/f4").unwrap(); + let _d3 = depot.link_create("d3", "d3").unwrap(); + + let read_dir = depot.read_dir("d1").unwrap().collect::>(); + let expected_read_dir = vec![ + DirNode::Link(f1), + DirNode::Link(f2), + DirNode::Link(f3), + DirNode::Directory(PathBuf::from("d1/d2")), + ]; + assert!( + read_dir.len() == expected_read_dir.len() + && expected_read_dir.iter().all(|x| read_dir.contains(x)) + ); } #[test] @@ -612,8 +788,9 @@ pub mod depot { } } -mod dotup { +pub mod dotup { use std::{ + cmp::Ordering, collections::HashSet, path::{Path, PathBuf}, }; @@ -622,7 +799,7 @@ mod dotup { use anyhow::Context; use crate::{ - depot::{self, Depot, LinkID}, + depot::{self, Depot, DirNode, LinkID}, utils, }; @@ -633,6 +810,90 @@ mod dotup { destination: PathBuf, } + #[derive(Debug, Clone)] + enum StatusItem { + Link { + origin: PathBuf, + destination: PathBuf, + is_directory: bool, + }, + Directory { + origin: PathBuf, + items: Vec, + }, + Unlinked { + origin: PathBuf, + is_directory: bool, + }, + } + + impl StatusItem { + fn display_ord_cmp(&self, other: &Self) -> Ordering { + match (self, other) { + ( + StatusItem::Link { + origin: l_origin, .. + }, + StatusItem::Link { + origin: r_origin, .. + }, + ) => l_origin.cmp(r_origin), + (StatusItem::Link { .. }, StatusItem::Directory { .. }) => Ordering::Less, + ( + StatusItem::Link { + is_directory: l_is_dir, + .. + }, + StatusItem::Unlinked { + is_directory: u_is_dir, + .. + }, + ) => { + if *u_is_dir && !*l_is_dir { + Ordering::Less + } else { + Ordering::Greater + } + } + (StatusItem::Directory { .. }, StatusItem::Link { .. }) => Ordering::Greater, + ( + StatusItem::Directory { + origin: l_origin, .. + }, + StatusItem::Directory { + origin: r_origin, .. + }, + ) => l_origin.cmp(r_origin), + (StatusItem::Directory { .. }, StatusItem::Unlinked { .. }) => Ordering::Greater, + ( + StatusItem::Unlinked { + is_directory: u_is_dir, + .. + }, + StatusItem::Link { + is_directory: l_is_dir, + .. + }, + ) => { + if *u_is_dir && !*l_is_dir { + Ordering::Greater + } else { + Ordering::Less + } + } + (StatusItem::Unlinked { .. }, StatusItem::Directory { .. }) => Ordering::Less, + ( + StatusItem::Unlinked { + origin: l_origin, .. + }, + StatusItem::Unlinked { + origin: r_origin, .. + }, + ) => l_origin.cmp(r_origin), + } + } + } + #[derive(Debug)] pub struct Dotup { depot: Depot, @@ -664,7 +925,7 @@ mod dotup { let link_result: anyhow::Result<()> = try { let origin = self.prepare_origin_path(origin.as_ref())?; let destination = destination.as_ref(); - self.depot.create(origin, destination)?; + self.depot.link_create(origin, destination)?; }; match link_result { Ok(_) => {} @@ -676,10 +937,10 @@ mod dotup { for origin in paths { let unlink_result: anyhow::Result<()> = try { let origin = self.prepare_origin_path(origin.as_ref())?; - let search_results = self.depot.search(&origin)?; + let search_results = self.depot.link_search(&origin)?; match search_results { depot::SearchResult::Found(link_id) => { - self.depot.remove(link_id); + self.depot.link_remove(link_id); println!("removed link {}", origin.display()); } depot::SearchResult::Ancestor(_) | depot::SearchResult::NotFound => { @@ -766,7 +1027,7 @@ mod dotup { } fn mv_one(&mut self, origin: &Path, destination: &Path) -> anyhow::Result<()> { - let link_id = match self.depot.find(origin)? { + let link_id = match self.depot.link_find(origin)? { Some(link_id) => link_id, None => { return Err(anyhow::anyhow!(format!( @@ -777,11 +1038,11 @@ mod dotup { }; let is_installed = self.symlink_is_installed_by_link_id(link_id)?; let original_origin = self.depot.link_view(link_id).origin().to_owned(); - self.depot.move_link(link_id, destination)?; + self.depot.link_move(link_id, destination)?; // move the actual file on disk if let Err(e) = std::fs::rename(origin, destination).context("Failed to move file") { // unwrap: moving the link back to its origin place has to work - self.depot.move_link(link_id, original_origin).unwrap(); + self.depot.link_move(link_id, original_origin).unwrap(); return Err(e); } // reinstall because we just moved the origin @@ -794,75 +1055,154 @@ mod dotup { pub fn status(&self) { let status_result: anyhow::Result<()> = try { - let curr_dir = utils::current_working_directory(); - let (dirs, files) = utils::collect_read_dir_split(curr_dir)?; + let canonical_dir = utils::current_working_directory(); + let item = self.status_path_to_item(&canonical_dir)?; + self.status_print_item(item, 0)?; }; if let Err(e) = status_result { println!("error while displaying status : {e}"); } } + fn status_path_to_item(&self, canonical_path: &Path) -> anyhow::Result { + debug_assert!(canonical_path.is_absolute()); + debug_assert!(canonical_path.exists()); + let relative_path = self.prepare_origin_path(&canonical_path)?; + + let item = if canonical_path.is_dir() { + if let Some(link_id) = self.depot.link_find(&relative_path)? { + let destination = self.depot.link_view(link_id).destination().to_owned(); + StatusItem::Link { + origin: relative_path, + destination, + is_directory: true, + } + } else if self.depot.has_links_under(&relative_path)? { + let mut items = Vec::new(); + let mut collected_rel_paths = HashSet::::new(); + let directory_paths = utils::collect_paths_in_dir(&canonical_path)?; + for canonical_item_path in directory_paths { + let item = self.status_path_to_item(&canonical_item_path)?; + match &item { + StatusItem::Link { origin, .. } + | StatusItem::Directory { origin, .. } => { + collected_rel_paths.insert(origin.to_owned()); + } + _ => {} + } + items.push(item); + } - pub fn status2(&self) { - let status_result: anyhow::Result<()> = try { - let curr_dir = &std::env::current_dir()?; - let (dirs, files) = utils::collect_read_dir_split(curr_dir)?; - for path in dirs.iter().chain(files.iter()) { - self.print_status_for(&path, 0)?; + for dir_node in self.depot.read_dir(&relative_path)? { + match dir_node { + DirNode::Link(link_id) => { + let link_view = self.depot.link_view(link_id); + let link_rel_path = link_view.origin(); + let link_rel_dest = link_view.destination(); + if !collected_rel_paths.contains(link_rel_path) { + items.push(StatusItem::Link { + origin: link_rel_path.to_owned(), + destination: link_rel_dest.to_owned(), + is_directory: false, + }); + } + } + DirNode::Directory(_) => {} + } + } + + StatusItem::Directory { + origin: relative_path, + items, + } + } else { + StatusItem::Unlinked { + origin: relative_path, + is_directory: true, + } + } + } else { + if let Some(link_id) = self.depot.link_find(&relative_path)? { + let destination = self.depot.link_view(link_id).destination().to_owned(); + StatusItem::Link { + origin: relative_path, + destination, + is_directory: false, + } + } else { + StatusItem::Unlinked { + origin: relative_path, + is_directory: false, + } } }; - if let Err(e) = status_result { - println!("error while displaying status : {e}"); - } + Ok(item) } - - fn print_status_for(&self, path: &Path, depth: u32) -> anyhow::Result<()> { + fn status_print_item(&self, item: StatusItem, depth: u32) -> anyhow::Result<()> { fn print_depth(d: u32) { - for _ in 0..d { - print!(" "); + for _ in 0..d.saturating_sub(1) { + print!(" "); } } - - let origin = self.prepare_origin_path(path)?; - if path.is_dir() { - print_depth(depth); - let file_name = path.file_name().unwrap().to_str().unwrap_or_default(); - if let Some(link_id) = self.depot.find(&origin)? { - let installed = self.symlink_is_installed_by_link_id(link_id)?; - let link_view = self.depot.link_view(link_id); - let destination = link_view.destination().display().to_string(); - let color = if installed { Color::Green } else { Color::Red }; - println!( - "{}/ ---> {}", - color.paint(file_name), - Color::Blue.paint(destination) - ); + fn origin_color(exists: bool, is_installed: bool) -> Color { + if !exists { + return Color::Red; + } else if is_installed { + Color::Green } else { - println!("{}/", file_name); - let (dirs, files) = utils::collect_read_dir_split(path)?; - for path in dirs.iter().chain(files.iter()) { - self.print_status_for(&path, depth + 1)?; - } + Color::Cyan } - } else if path.is_file() || path.is_symlink() { - print_depth(depth); - let file_name = path.file_name().unwrap().to_str().unwrap_or_default(); - if let Some(link_id) = self.depot.find(&origin)? { - let installed = self.symlink_is_installed_by_link_id(link_id)?; - let link_view = self.depot.link_view(link_id); - let destination = link_view.destination().display().to_string(); - let color = if installed { Color::Green } else { Color::Red }; + } + let destination_color = Color::Blue; + + print_depth(depth); + match item { + StatusItem::Link { + origin, + destination, + is_directory, + } => { + let canonical_origin = self.depot_dir.join(&origin); + let canonical_destination = self.install_base.join(&destination); + let file_name = Self::status_get_filename(&canonical_origin); + let is_installed = + self.symlink_is_installed(&canonical_origin, &canonical_destination)?; + let exists = canonical_origin.exists(); + let origin_color = origin_color(exists, is_installed); + let directory_extra = if is_directory { "/" } else { "" }; println!( - "{} ---> {}", - color.paint(file_name), - Color::Blue.paint(destination) + "{}{} -> {}", + origin_color.paint(file_name), + directory_extra, + destination_color.paint(destination.display().to_string()) ); - } else { - println!("{}", file_name); + } + StatusItem::Directory { origin, mut items } => { + items.sort_by(|a, b| StatusItem::display_ord_cmp(a, b).reverse()); + let directory_name = Self::status_get_filename(&origin); + if depth != 0 { + println!("{}/", directory_name); + } + for item in items { + self.status_print_item(item, depth + 1)?; + } + } + StatusItem::Unlinked { + origin, + is_directory, + } => { + let file_name = Self::status_get_filename(&origin); + let directory_extra = if is_directory { "/" } else { "" }; + println!("{}{}", file_name, directory_extra); } } Ok(()) } + fn status_get_filename(path: &Path) -> &str { + path.file_name() + .and_then(|s| s.to_str()) + .unwrap_or_default() + } fn prepare_origin_path(&self, origin: &Path) -> anyhow::Result { let canonical = utils::weakly_canonical(origin); @@ -994,10 +1334,16 @@ mod utils { pub fn collect_read_dir_split( dir: impl AsRef, ) -> anyhow::Result<(Vec, Vec)> { + Ok(collect_paths_in_dir(dir)? + .into_iter() + .partition(|p| p.is_dir())) + } + + pub fn collect_paths_in_dir(dir: impl AsRef) -> anyhow::Result> { Ok(std::fs::read_dir(dir)? .filter_map(|e| e.ok()) .map(|e| e.path()) - .partition(|p| p.is_dir())) + .collect()) } pub fn read_dotup(flags: &Flags) -> anyhow::Result { -- cgit From 406a3e662b3fafd73ab72a6f7bd4e22131654dfb Mon Sep 17 00:00:00 2001 From: diogo464 Date: Mon, 7 Feb 2022 15:45:21 +0000 Subject: snapshot --- Cargo.lock | 125 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 2 + src/main.rs | 77 ++++++++++++++++++++++++++----------- 3 files changed, 181 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 835a197..c005523 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + [[package]] name = "ansi_term" version = "0.12.1" @@ -40,6 +49,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + [[package]] name = "clap" version = "3.0.14" @@ -77,11 +92,36 @@ dependencies = [ "ansi_term", "anyhow", "clap", + "flexi_logger", + "log", "serde", "slotmap", "toml", ] +[[package]] +name = "flexi_logger" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "969940c39bc718475391e53a3a59b0157e64929c80cf83ad5dde5f770ecdc423" +dependencies = [ + "ansi_term", + "atty", + "glob", + "lazy_static", + "log", + "regex", + "rustversion", + "thiserror", + "time", +] + +[[package]] +name = "glob" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" + [[package]] name = "hashbrown" version = "0.11.2" @@ -113,6 +153,12 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "itoa" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" + [[package]] name = "lazy_static" version = "1.4.0" @@ -125,12 +171,30 @@ version = "0.2.117" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e74d72e0f9b65b5b4ca49a346af3976df0f9c61d550727f349ecd559f251a26c" +[[package]] +name = "log" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if", +] + [[package]] name = "memchr" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" +[[package]] +name = "num_threads" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97ba99ba6393e2c3734791401b66902d981cb03bf190af674ca69949b6d5fb15" +dependencies = [ + "libc", +] + [[package]] name = "os_str_bytes" version = "6.0.0" @@ -182,6 +246,29 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "regex" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + +[[package]] +name = "rustversion" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2cc38e8fa666e2de3c4aba7edeb5ffc5246c1c2ed0e3d17e560aeeba736b23f" + [[package]] name = "serde" version = "1.0.136" @@ -243,6 +330,44 @@ version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80" +[[package]] +name = "thiserror" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "004cbc98f30fa233c61a38bc77e96a9106e65c88f2d3bef182ae952027e5753d" +dependencies = [ + "itoa", + "libc", + "num_threads", + "time-macros", +] + +[[package]] +name = "time-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25eb0ca3468fc0acc11828786797f6ef9aa1555e4a211a60d64cc8e4d1be47d6" + [[package]] name = "toml" version = "0.5.8" diff --git a/Cargo.toml b/Cargo.toml index ea24a57..b7a73a0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,8 @@ edition = "2021" ansi_term = "0.12.1" anyhow = "1.0.53" clap = { version = "3.0.14", features = ["derive"] } +flexi_logger = "0.22.3" +log = "0.4.14" serde = { version = "1.0.136", features = ["derive"] } slotmap = "1.0.6" toml = "0.5.8" diff --git a/src/main.rs b/src/main.rs index 1cb68d4..f623450 100644 --- a/src/main.rs +++ b/src/main.rs @@ -923,7 +923,7 @@ pub mod dotup { pub fn link(&mut self, origin: impl AsRef, destination: impl AsRef) { let link_result: anyhow::Result<()> = try { - let origin = self.prepare_origin_path(origin.as_ref())?; + let origin = self.prepare_relative_path(origin.as_ref())?; let destination = destination.as_ref(); self.depot.link_create(origin, destination)?; }; @@ -936,7 +936,7 @@ pub mod dotup { pub fn unlink(&mut self, paths: impl Iterator>) { for origin in paths { let unlink_result: anyhow::Result<()> = try { - let origin = self.prepare_origin_path(origin.as_ref())?; + let origin = self.prepare_relative_path(origin.as_ref())?; let search_results = self.depot.link_search(&origin)?; match search_results { depot::SearchResult::Found(link_id) => { @@ -959,7 +959,7 @@ pub mod dotup { let mut already_linked: HashSet = Default::default(); for origin in paths { let install_result: anyhow::Result<()> = try { - let origin = self.prepare_origin_path(origin.as_ref())?; + let origin = self.prepare_relative_path(origin.as_ref())?; let canonical_pairs = self.canonical_pairs_under(&origin)?; for pair in canonical_pairs { if already_linked.contains(&pair.link_id) { @@ -978,7 +978,7 @@ pub mod dotup { pub fn uninstall(&self, paths: impl Iterator>) { for origin in paths { let uninstall_result: anyhow::Result<()> = try { - let origin = self.prepare_origin_path(origin.as_ref())?; + let origin = self.prepare_relative_path(origin.as_ref())?; let canonical_pairs = self.canonical_pairs_under(&origin)?; for pair in canonical_pairs { self.symlink_uninstall(&pair.origin, &pair.destination)?; @@ -1001,7 +1001,7 @@ pub mod dotup { let origins = { let mut v = Vec::new(); for origin in origins { - match self.prepare_origin_path(origin.as_ref()) { + match self.prepare_relative_path(origin.as_ref()) { Ok(origin) => v.push(origin), Err(e) => { println!("invalid link {} : {e}", origin.as_ref().display()); @@ -1066,7 +1066,7 @@ pub mod dotup { fn status_path_to_item(&self, canonical_path: &Path) -> anyhow::Result { debug_assert!(canonical_path.is_absolute()); debug_assert!(canonical_path.exists()); - let relative_path = self.prepare_origin_path(&canonical_path)?; + let relative_path = self.prepare_relative_path(&canonical_path)?; let item = if canonical_path.is_dir() { if let Some(link_id) = self.depot.link_find(&relative_path)? { @@ -1204,7 +1204,7 @@ pub mod dotup { .unwrap_or_default() } - fn prepare_origin_path(&self, origin: &Path) -> anyhow::Result { + fn prepare_relative_path(&self, origin: &Path) -> anyhow::Result { let canonical = utils::weakly_canonical(origin); let relative = canonical .strip_prefix(&self.depot_dir) @@ -1214,7 +1214,7 @@ pub mod dotup { // returns the canonical pairs for all links under `path`. fn canonical_pairs_under(&self, path: &Path) -> anyhow::Result> { - let origin = self.prepare_origin_path(path)?; + let origin = self.prepare_relative_path(path)?; let mut canonical_pairs = Vec::new(); for link_id in self.depot.links_under(origin)? { canonical_pairs.push(self.canonical_pair_from_link_id(link_id)); @@ -1250,11 +1250,19 @@ pub mod dotup { fn symlink_install(&self, origin: &Path, destination: &Path) -> anyhow::Result<()> { debug_assert!(origin.is_absolute()); debug_assert!(destination.is_absolute()); + log::debug!( + "symlink_install : {} -> {}", + origin.display(), + destination.display() + ); - if let Some(destination_parent) = destination.parent() { - std::fs::create_dir_all(destination_parent) - .context("Failed to create directories")?; - } + let destination_parent = destination + .parent() + .ok_or_else(|| anyhow::anyhow!("destination has no parent component"))?; + std::fs::create_dir_all(destination_parent).context("Failed to create directories")?; + // need to do this beacause if the destination path ends in '/' because the symlink + // functions will treat it as a directory but we want a file with that name. + let destination = destination.with_file_name(destination.file_name().unwrap()); let destination_exists = destination.exists(); let destination_is_symlink = destination.is_symlink(); @@ -1264,9 +1272,16 @@ pub mod dotup { } if destination_is_symlink { + log::debug!("symlink already exists, removing before recreating"); std::fs::remove_file(&destination)?; } - std::os::unix::fs::symlink(origin, destination).context("Failed to create symlink")?; + + log::debug!( + "creating filesystem symlink {} -> {}", + origin.display(), + destination.display() + ); + std::os::unix::fs::symlink(origin, destination).context("failed to create symlink")?; Ok(()) } @@ -1274,6 +1289,7 @@ pub mod dotup { fn symlink_uninstall(&self, origin: &Path, destination: &Path) -> anyhow::Result<()> { debug_assert!(origin.is_absolute()); debug_assert!(destination.is_absolute()); + let destination = destination.with_file_name(destination.file_name().unwrap()); if destination.is_symlink() { let symlink_destination = destination.read_link()?.canonicalize()?; @@ -1329,16 +1345,6 @@ mod utils { pub const DEFAULT_DEPOT_FILE_NAME: &str = ".depot"; - /// collects the result of std::fs::read_dir into two vecs, the first one contains all the - /// directories and the second one all the files. - pub fn collect_read_dir_split( - dir: impl AsRef, - ) -> anyhow::Result<(Vec, Vec)> { - Ok(collect_paths_in_dir(dir)? - .into_iter() - .partition(|p| p.is_dir())) - } - pub fn collect_paths_in_dir(dir: impl AsRef) -> anyhow::Result> { Ok(std::fs::read_dir(dir)? .filter_map(|e| e.ok()) @@ -1465,6 +1471,7 @@ mod utils { use std::path::PathBuf; use clap::Parser; +use flexi_logger::Logger; use utils::DEFAULT_DEPOT_FILE_NAME; #[derive(Parser, Debug)] @@ -1478,8 +1485,19 @@ pub struct Flags { #[derive(Parser, Debug)] #[clap(author, version, about, long_about = None)] struct Args { + /// A level of verbosity, and can be used multiple times + /// + /// Level 1 - Info + /// + /// Level 2 - Debug + /// + /// Level 3 - Trace + #[clap(short, long, parse(from_occurrences))] + verbose: i32, + #[clap(flatten)] flags: Flags, + #[clap(subcommand)] command: SubCommand, } @@ -1497,6 +1515,19 @@ enum SubCommand { fn main() -> anyhow::Result<()> { let args = Args::parse(); + + let log_level = match args.verbose { + 0 => "warn", + 1 => "info", + 2 => "debug", + _ => "trace", + }; + + Logger::try_with_env_or_str(log_level)? + .format(flexi_logger::colored_default_format) + .set_palette("196;208;32;198;15".to_string()) + .start()?; + match args.command { SubCommand::Init(cmd_args) => command_init(args.flags, cmd_args), SubCommand::Link(cmd_args) => command_link(args.flags, cmd_args), -- cgit From 37d767fbc1b0ec5f9afc41e741c56bf2407e837d Mon Sep 17 00:00:00 2001 From: diogo464 Date: Tue, 8 Feb 2022 08:47:26 +0000 Subject: snapshot --- Cargo.lock | 1 + Cargo.toml | 1 + src/main.rs | 914 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 902 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c005523..f88aa27 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -96,6 +96,7 @@ dependencies = [ "log", "serde", "slotmap", + "thiserror", "toml", ] diff --git a/Cargo.toml b/Cargo.toml index b7a73a0..973d432 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,4 +13,5 @@ flexi_logger = "0.22.3" log = "0.4.14" serde = { version = "1.0.136", features = ["derive"] } slotmap = "1.0.6" +thiserror = "1.0.30" toml = "0.5.8" diff --git a/src/main.rs b/src/main.rs index f623450..9b17787 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,847 @@ // TODO: rewrite all errors so they start with lower case -pub mod depot { +mod depot { + use std::{ + collections::HashSet, + ffi::{OsStr, OsString}, + ops::Index, + path::{Path, PathBuf}, + }; + use thiserror::Error; + + use slotmap::{Key, SlotMap}; + + //pub type Result = std::result::Result; + pub use anyhow::Result; + pub use disk::{read, write}; + + slotmap::new_key_type! {pub struct LinkID;} + slotmap::new_key_type! {struct NodeID;} + + #[derive(Debug, Error)] + enum DepotError { + #[error("path must be relative")] + InvalidPath, + #[error("path must be relative and not empty")] + InvalidLinkPath, + #[error("{0}")] + LinkCreateError(#[from] LinkCreateError), + } + + #[derive(Debug, Error)] + enum LinkCreateError { + #[error("an ancestor of this path is already linked")] + AncestorAlreadyLinked, + } + + #[derive(Debug, Clone)] + struct Node { + comp: OsString, + parent: NodeID, + kind: NodeKind, + } + + #[derive(Debug, Clone)] + enum NodeKind { + Link(LinkID), + Directory(HashSet), + } + + #[derive(Debug, Clone, PartialEq, Eq)] + pub enum NodeSearchResult { + Found(NodeID), + /// the closest NodeID up the the search point. + NotFound(NodeID), + } + + #[derive(Debug, Clone, PartialEq, Eq)] + pub enum DirNode { + Link(LinkID), + Directory(PathBuf), + } + + #[derive(Debug, Clone, PartialEq, Eq)] + pub enum SearchResult { + Found(LinkID), + Ancestor(LinkID), + NotFound, + } + + #[derive(Debug, Clone)] + struct Link { + origin: PathBuf, + destination: PathBuf, + origin_id: NodeID, + } + + #[derive(Debug)] + pub struct LinkView<'a> { + link_id: LinkID, + depot: &'a Depot, + } + + impl<'a> LinkView<'a> { + pub fn origin(&self) -> &Path { + &self.depot.links[self.link_id].origin + } + + pub fn destination(&self) -> &Path { + &self.depot.links[self.link_id].destination + } + } + + #[derive(Debug, Clone)] + struct DepotTree { + root: NodeID, + nodes: SlotMap, + } + + impl Default for DepotTree { + fn default() -> Self { + let mut nodes = SlotMap::::default(); + let root = nodes.insert(Node { + comp: Default::default(), + parent: Default::default(), + kind: NodeKind::Directory(Default::default()), + }); + Self { root, nodes } + } + } + + impl Index for DepotTree { + type Output = Node; + + fn index(&self, index: NodeID) -> &Self::Output { + self.nodes.index(index) + } + } + + impl DepotTree { + /// create a node of kind [`NodeKind::Link`]. + pub fn link_create(&mut self, path: &Path, link_id: LinkID) -> Result { + debug_assert!(path_verify_link(path).is_ok()); + + let path_search_result = self.search(path); + + // handle the error cases + match path_search_result { + NodeSearchResult::Found(node_id) => { + let node = &self.nodes[node_id]; + match &node.kind { + NodeKind::Link(_) => Err(anyhow::anyhow!("link already exists")), + NodeKind::Directory(_) => { + Err(anyhow::anyhow!("path already has links under it")) + } + } + } + NodeSearchResult::NotFound(ancestor_node_id) => { + let ancestor_node = &self.nodes[ancestor_node_id]; + match &ancestor_node.kind { + NodeKind::Link(_) => Err(anyhow::anyhow!( + "an ancestor of this path is already linked" + )), + NodeKind::Directory(_) => Ok(()), + } + } + }?; + + // create the node + // unwrap: this is a verfied link path, it must have atleast one component + let filename = path.file_name().unwrap(); + let parent_path = path_parent_or_empty(path); + let node_id = self.nodes.insert(Node { + comp: filename.to_owned(), + parent: Default::default(), + kind: NodeKind::Link(link_id), + }); + let parent_id = self.directory_get_or_create(parent_path, node_id); + self.nodes[node_id].parent = parent_id; + Ok(node_id) + } + + pub fn link_update_id(&mut self, node_id: NodeID, link_id: LinkID) { + let node = &mut self.nodes[node_id]; + match &mut node.kind { + NodeKind::Link(lid) => *lid = link_id, + NodeKind::Directory(_) => unreachable!(), + } + } + + /// attempts to moves a node of kind [`NodeKind::Link`] to `destination`. + pub fn link_move(&mut self, node_id: NodeID, destination: &Path) -> Result<()> { + let parent_id = self.nodes[node_id].parent; + let parent = &mut self.nodes[parent_id]; + + // remove the node from its parent temporarily so that the search never returns this + // link and that way any link will find means an error. + // if an error does happen then we re-add this node to its parent to keep the data + // consistent. + match &mut parent.kind { + NodeKind::Link(_) => unreachable!(), + NodeKind::Directory(children) => children.remove(&node_id), + }; + + let search_result = self.search(destination); + // handle the error cases + match search_result { + NodeSearchResult::Found(found_id) => { + assert!(found_id != node_id); + self.directory_add_child(parent_id, node_id); + return Err(anyhow::anyhow!("link already exists at that path")); + } + NodeSearchResult::NotFound(ancestor_id) => { + let ancestor = &self.nodes[ancestor_id]; + match &ancestor.kind { + NodeKind::Link(_) => { + self.directory_add_child(parent_id, node_id); + return Err(anyhow::anyhow!("ancestor path is already linked")); + } + NodeKind::Directory(_) => {} + } + } + }; + + let destination_parent = path_parent_or_empty(destination); + let new_parent_id = self.directory_get_or_create(destination_parent, node_id); + if new_parent_id != parent_id { + self.nodes[node_id].parent = new_parent_id; + + // we have to re-add and call the remove function because it could lead to the removal + // of several directories if they become empty after this remove. + self.directory_add_child(parent_id, node_id); + self.directory_remove_child(parent_id, node_id); + } + + // unwrap: destination is a verified link path so it has atleast 1 component + let comp = destination.file_name().unwrap(); + let node = &mut self.nodes[node_id]; + if node.comp != comp { + node.comp = comp.to_owned(); + } + + Ok(()) + } + + pub fn link_search(&self, path: &Path) -> SearchResult { + match self.search(path) { + NodeSearchResult::Found(node_id) => match &self.nodes[node_id].kind { + NodeKind::Link(link_id) => SearchResult::Found(*link_id), + NodeKind::Directory(_) => SearchResult::NotFound, + }, + NodeSearchResult::NotFound(node_id) => match &self.nodes[node_id].kind { + NodeKind::Link(link_id) => SearchResult::Ancestor(*link_id), + NodeKind::Directory(_) => SearchResult::NotFound, + }, + } + } + + /// remove a node of kind [`NodeKind::Link`]. + pub fn link_remove(&mut self, node_id: NodeID) { + let node = &self.nodes[node_id]; + assert!(std::matches!(node.kind, NodeKind::Link(_))); + let parent_id = node.parent; + self.nodes.remove(node_id); + self.directory_remove_child(parent_id, node_id); + } + + pub fn links_under(&self, path: &Path) -> impl Iterator + '_ { + let links = match self.search(path) { + NodeSearchResult::Found(node_id) => { + let node = &self.nodes[node_id]; + match &node.kind { + NodeKind::Link(link_id) => vec![*link_id], + NodeKind::Directory(children) => { + let mut links = Vec::new(); + let mut node_ids = Vec::from_iter(children.iter().copied()); + while let Some(child_id) = node_ids.pop() { + let child = &self.nodes[child_id]; + match &child.kind { + NodeKind::Link(link_id) => links.push(*link_id), + NodeKind::Directory(extra_children) => { + node_ids.extend(extra_children.iter().copied()) + } + } + } + links + } + } + } + NodeSearchResult::NotFound(_) => vec![], + }; + links.into_iter() + } + + pub fn has_links_under(&self, path: &Path) -> bool { + // it does not matter what type of node is found. if a directory exists then there + // must be atleast one link under it. + match self.search(path) { + NodeSearchResult::Found(_) => true, + NodeSearchResult::NotFound(_) => false, + } + } + + pub fn read_dir(&self, path: &Path) -> Result + '_> { + match self.search(path) { + NodeSearchResult::Found(node_id) => match &self.nodes[node_id].kind { + NodeKind::Link(_) => Err(anyhow::anyhow!("read dir called on a link")), + NodeKind::Directory(children) => Ok(children.iter().map(|child_id| { + let child = &self.nodes[*child_id]; + match &child.kind { + NodeKind::Link(link_id) => DirNode::Link(*link_id), + NodeKind::Directory(_) => { + DirNode::Directory(self.build_path(*child_id)) + } + } + })), + }, + NodeSearchResult::NotFound(_) => Err(anyhow::anyhow!("directory not found")), + } + } + + pub fn build_path(&self, node_id: NodeID) -> PathBuf { + fn recursive_helper(nodes: &SlotMap, nid: NodeID, pbuf: &mut PathBuf) { + if nid.is_null() { + return; + } + let parent_id = nodes[nid].parent; + recursive_helper(nodes, parent_id, pbuf); + pbuf.push(&nodes[nid].comp); + } + + let mut node_path = PathBuf::default(); + recursive_helper(&self.nodes, node_id, &mut node_path); + node_path + } + + fn search(&self, path: &Path) -> NodeSearchResult { + debug_assert!(path_verify(path).is_ok()); + + let mut curr_node_id = self.root; + let mut comp_iter = path_iter_comps(path).peekable(); + while let Some(comp) = comp_iter.next() { + if let Some(child_id) = self.directory_search_children(curr_node_id, comp) { + let child = &self.nodes[child_id]; + match &child.kind { + NodeKind::Link(_) => { + if comp_iter.peek().is_some() { + return NodeSearchResult::NotFound(child_id); + } else { + return NodeSearchResult::Found(child_id); + } + } + NodeKind::Directory(_) => curr_node_id = child_id, + } + } else { + return NodeSearchResult::NotFound(curr_node_id); + } + } + NodeSearchResult::Found(curr_node_id) + } + + // creates directories all the way up to and including path. + // there cannot be any links up to `path`. + fn directory_get_or_create(&mut self, path: &Path, initial_child: NodeID) -> NodeID { + // TODO: this could be replaced if the search function also returned the depth of the + // node and we skip those components and just start creating directories up to the + // path. + let mut curr_node_id = self.root; + for comp in path_iter_comps(path) { + if let Some(child_id) = self.directory_search_children(curr_node_id, comp) { + debug_assert!(std::matches!( + self.nodes[child_id].kind, + NodeKind::Directory(_) + )); + curr_node_id = child_id; + } else { + let new_node_id = self.nodes.insert(Node { + comp: comp.to_owned(), + parent: curr_node_id, + kind: NodeKind::Directory(Default::default()), + }); + self.directory_add_child(curr_node_id, new_node_id); + curr_node_id = new_node_id; + } + } + self.directory_add_child(curr_node_id, initial_child); + curr_node_id + } + + fn directory_search_children(&self, node_id: NodeID, comp: &OsStr) -> Option { + let node = &self.nodes[node_id]; + match &node.kind { + NodeKind::Link(_) => unreachable!(), + NodeKind::Directory(children) => { + for &child_id in children { + let child = &self.nodes[child_id]; + if child.comp == comp { + return Some(child_id); + } + } + } + } + None + } + + fn directory_add_child(&mut self, node_id: NodeID, child_id: NodeID) { + let node = &mut self.nodes[node_id]; + match &mut node.kind { + NodeKind::Link(_) => unreachable!(), + NodeKind::Directory(children) => children.insert(child_id), + }; + } + + fn directory_remove_child(&mut self, node_id: NodeID, child_id: NodeID) { + let node = &mut self.nodes[node_id]; + match &mut node.kind { + NodeKind::Link(_) => unreachable!(), + NodeKind::Directory(children) => { + children.remove(&child_id); + if children.is_empty() && !node.parent.is_null() { + let parent_id = node.parent; + self.directory_remove_child(parent_id, node_id); + } + } + } + } + } + + #[derive(Debug, Default, Clone)] + pub struct Depot { + links: SlotMap, + origin: DepotTree, + } + + impl Depot { + pub fn link_create( + &mut self, + origin: impl AsRef, + destination: impl AsRef, + ) -> Result { + let origin = origin.as_ref(); + let destination = destination.as_ref(); + path_verify_link(origin)?; + path_verify_link(destination)?; + self.link_create_unchecked(origin, destination) + } + + pub fn link_remove(&mut self, link_id: LinkID) { + let node_id = self.links[link_id].origin_id; + self.links.remove(link_id); + self.origin.link_remove(node_id); + } + + /// moves the link specified by `link_id` to the path at `destination`. + /// if the link is already at the destination nothing is done. + /// if the destination is another link that that link is removed. + /// if the destination is under another link then an error is returned. + /// `destination` will be the link's new origin. + pub fn link_move(&mut self, link_id: LinkID, destination: impl AsRef) -> Result<()> { + let destination = destination.as_ref(); + path_verify_link(destination)?; + self.link_move_unchecked(link_id, destination) + } + + pub fn link_search(&self, path: impl AsRef) -> Result { + let path = path.as_ref(); + path_verify(path)?; + Ok(self.link_search_unchecked(path)) + } + + pub fn link_find(&self, path: impl AsRef) -> Result> { + let path = path.as_ref(); + path_verify(path)?; + Ok(self.link_find_unchecked(path)) + } + + pub fn links_under( + &self, + path: impl AsRef, + ) -> Result + '_> { + let path = path.as_ref(); + path_verify(path)?; + Ok(self.links_under_unchecked(path)) + } + + pub fn has_links_under(&self, path: impl AsRef) -> Result { + let path = path.as_ref(); + path_verify(path)?; + Ok(self.has_links_under_unchecked(path)) + } + + pub fn link_view(&self, link_id: LinkID) -> LinkView { + LinkView { + link_id, + depot: self, + } + } + + pub fn read_dir( + &self, + path: impl AsRef, + ) -> Result + '_> { + let path = path.as_ref(); + path_verify(path)?; + self.read_dir_unchecked(path) + } + + fn link_create_unchecked(&mut self, origin: &Path, destination: &Path) -> Result { + let node_id = self.origin.link_create(origin, Default::default())?; + let link_id = self.links.insert(Link { + origin: origin.to_owned(), + destination: destination.to_owned(), + origin_id: node_id, + }); + self.origin.link_update_id(node_id, link_id); + Ok(link_id) + } + + fn link_move_unchecked(&mut self, link_id: LinkID, destination: &Path) -> Result<()> { + let link = &self.links[link_id]; + if link.origin == destination { + return Ok(()); + } + let node_id = link.origin_id; + self.origin.link_move(node_id, destination)?; + self.links[link_id].origin = destination.to_owned(); + Ok(()) + } + + fn link_search_unchecked(&self, path: &Path) -> SearchResult { + self.origin.link_search(path) + } + + fn link_find_unchecked(&self, path: &Path) -> Option { + match self.link_search_unchecked(path) { + SearchResult::Found(link_id) => Some(link_id), + _ => None, + } + } + + fn links_under_unchecked(&self, path: &Path) -> impl Iterator + '_ { + self.origin.links_under(path) + } + + fn has_links_under_unchecked(&self, path: &Path) -> bool { + self.origin.has_links_under(path) + } + + fn read_dir_unchecked(&self, path: &Path) -> Result + '_> { + self.origin.read_dir(path) + } + } + + /// a verified link path is a path that: + /// + is not empty + /// + is relative + /// + does not contain Prefix/RootDir/ParentDir + fn path_verify_link(path: &Path) -> Result<()> { + // make sure the path is not empty + if path.components().next().is_none() { + return Err(DepotError::InvalidLinkPath.into()); + } + path_verify(path).map_err(|_| DepotError::InvalidLinkPath.into()) + } + + /// a verified path is a path that: + /// + is not empty + /// + is relative + /// + does not contain Prefix/RootDir/ParentDir + fn path_verify(path: &Path) -> Result<()> { + // make sure the path is relative + // make sure the path does not contain '.' or '..' + for component in path.components() { + match component { + std::path::Component::Prefix(_) + | std::path::Component::RootDir + | std::path::Component::CurDir + | std::path::Component::ParentDir => return Err(DepotError::InvalidPath.into()), + std::path::Component::Normal(_) => {} + } + } + Ok(()) + } + + fn path_parent_or_empty(path: &Path) -> &Path { + path.parent().unwrap_or(Path::new("")) + } + + /// Iterate over the components of a path. + /// # Pre + /// The path can only have "Normal" components. + fn path_iter_comps(path: &Path) -> impl Iterator { + debug_assert!(path_verify(path).is_ok()); + path.components().map(|component| match component { + std::path::Component::Normal(comp) => comp, + _ => unreachable!(), + }) + } + + mod disk { + use std::path::{Path, PathBuf}; + + use anyhow::Context; + use serde::{Deserialize, Serialize}; + + use super::Depot; + + #[derive(Debug, Serialize, Deserialize)] + struct DiskLink { + origin: PathBuf, + destination: PathBuf, + } + + #[derive(Debug, Serialize, Deserialize)] + struct DiskLinks { + links: Vec, + } + + pub fn read(path: &Path) -> anyhow::Result { + let contents = std::fs::read_to_string(path).context("Failed to read depot file")?; + let disk_links = toml::from_str::(&contents) + .context("Failed to parse depot file")? + .links; + let mut depot = Depot::default(); + for disk_link in disk_links { + depot + .link_create(disk_link.origin, disk_link.destination) + .context("Failed to build depot from file. File is in an invalid state")?; + } + Ok(depot) + } + + pub fn write(path: &Path, depot: &Depot) -> anyhow::Result<()> { + let mut links = Vec::with_capacity(depot.links.len()); + for (_, link) in depot.links.iter() { + links.push(DiskLink { + origin: link.origin.clone(), + destination: link.destination.clone(), + }); + } + let contents = toml::to_string_pretty(&DiskLinks { links }) + .context("Failed to serialize depot")?; + std::fs::write(path, contents).context("Failed to write depot to file")?; + Ok(()) + } + } + + #[cfg(test)] + mod tests { + use super::*; + + #[test] + fn test_depot_link_create() { + let mut depot = Depot::default(); + let f1 = depot.link_create("f1", "f1").unwrap(); + let f2 = depot.link_create("f2", "f2").unwrap(); + let f3 = depot.link_create("d1/f3", "d1/f3").unwrap(); + let f4 = depot.link_create("d1/d2/f4", "d1/d2/d4").unwrap(); + + assert_eq!(depot.link_find("f1").unwrap(), Some(f1)); + assert_eq!(depot.link_find("f2").unwrap(), Some(f2)); + assert_eq!(depot.link_find("d1/f3").unwrap(), Some(f3)); + assert_eq!(depot.link_find("d1/d2/f4").unwrap(), Some(f4)); + + depot.link_create("f2", "").unwrap_err(); + depot.link_create("", "d4").unwrap_err(); + depot.link_create("f1/f3", "f3").unwrap_err(); + } + + #[test] + fn test_depot_link_remove() { + let mut depot = Depot::default(); + let f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); + let f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); + let _f3 = depot.link_create("d1/f3", "d1/f3").unwrap(); + let f4 = depot.link_create("d1/d2/f4", "d2/f4").unwrap(); + let d3 = depot.link_create("d3", "d3").unwrap(); + + depot.link_remove(f2); + assert_eq!(depot.link_find("d1/f1").unwrap(), Some(f1)); + assert_eq!(depot.link_find("d1/f2").unwrap(), None); + depot.link_remove(f4); + assert_eq!(depot.link_find("d1/d2/f4").unwrap(), None); + depot.link_remove(d3); + assert_eq!(depot.link_find("d3").unwrap(), None); + } + + #[test] + fn test_depot_link_move() { + let mut depot = Depot::default(); + let f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); + let _f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); + + depot.link_move(f1, "").unwrap_err(); + depot.link_move(f1, "d1/f2/f1").unwrap_err(); + depot.link_move(f1, "d1/f2").unwrap_err(); + + depot.link_move(f1, "f1").unwrap(); + assert_eq!(depot.link_view(f1).origin(), Path::new("f1")); + depot.link_move(f1, "f2").unwrap(); + assert_eq!(depot.link_view(f1).origin(), Path::new("f2")); + assert_eq!(depot.link_find("f2").unwrap(), Some(f1)); + } + + #[test] + fn test_depot_link_search() { + let mut depot = Depot::default(); + let f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); + let _f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); + let _f3 = depot.link_create("d1/f3", "d1/f3").unwrap(); + let _f4 = depot.link_create("d1/d2/f4", "d2/f4").unwrap(); + let _d3 = depot.link_create("d3", "d3").unwrap(); + + assert_eq!(depot.link_search("d1/f1").unwrap(), SearchResult::Found(f1)); + assert_eq!( + depot.link_search("d1/f1/f5").unwrap(), + SearchResult::Ancestor(f1) + ); + assert_eq!(depot.link_search("d1").unwrap(), SearchResult::NotFound); + assert_eq!( + depot.link_search("d1/d2/f5").unwrap(), + SearchResult::NotFound + ); + } + + #[test] + fn test_depot_link_find() { + let mut depot = Depot::default(); + let f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); + let _f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); + let f3 = depot.link_create("d1/f3", "d1/f3").unwrap(); + let f4 = depot.link_create("d1/d2/f4", "d2/f4").unwrap(); + let d3 = depot.link_create("d3", "d3").unwrap(); + + assert_eq!(depot.link_find("d1/f1").unwrap(), Some(f1)); + assert_eq!(depot.link_find("d1/f3").unwrap(), Some(f3)); + assert_eq!(depot.link_find("d1/d2/f4").unwrap(), Some(f4)); + assert_eq!(depot.link_find("d3").unwrap(), Some(d3)); + + assert_eq!(depot.link_find("d5").unwrap(), None); + assert_eq!(depot.link_find("d3/d5").unwrap(), None); + assert_eq!(depot.link_find("d1/d2/f5").unwrap(), None); + } + + #[test] + fn test_depot_links_under() { + let mut depot = Depot::default(); + let f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); + let f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); + let f3 = depot.link_create("d1/f3", "d1/f3").unwrap(); + let f4 = depot.link_create("d1/d2/f4", "d2/f4").unwrap(); + let d3 = depot.link_create("d3", "d3").unwrap(); + + let under_f1 = depot.links_under("d1/f1").unwrap().collect::>(); + assert_eq!(under_f1, vec![f1]); + + let under_d1 = depot.links_under("d1").unwrap().collect::>(); + let expected_under_d1 = vec![f1, f2, f3, f4]; + assert!( + under_d1.len() == expected_under_d1.len() + && expected_under_d1.iter().all(|x| under_d1.contains(x)) + ); + + let under_d2 = depot.links_under("d2").unwrap().collect::>(); + assert_eq!(under_d2, vec![]); + + let under_d3 = depot.links_under("d3").unwrap().collect::>(); + assert_eq!(under_d3, vec![d3]); + + let under_root = depot.links_under("").unwrap().collect::>(); + let expected_under_root = vec![f1, f2, f3, f4, d3]; + assert!( + under_root.len() == expected_under_root.len() + && expected_under_root.iter().all(|x| under_root.contains(x)) + ); + } + + #[test] + fn test_depot_has_links_under() { + let mut depot = Depot::default(); + let _f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); + let _f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); + let _f3 = depot.link_create("d1/f3", "d1/f3").unwrap(); + let _f4 = depot.link_create("d1/d2/f4", "d2/f4").unwrap(); + let _d3 = depot.link_create("d3", "d3").unwrap(); + + assert!(depot.has_links_under("").unwrap()); + assert!(depot.has_links_under("d1").unwrap()); + assert!(depot.has_links_under("d3").unwrap()); + assert!(depot.has_links_under("d1/f1").unwrap()); + assert!(depot.has_links_under("d1/d2").unwrap()); + assert!(depot.has_links_under("d1/d2/f4").unwrap()); + + assert!(!depot.has_links_under("d2").unwrap()); + assert!(!depot.has_links_under("d4").unwrap()); + assert!(!depot.has_links_under("d1/d2/f4/f5").unwrap()); + } + + #[test] + fn test_depot_read_dir() { + let mut depot = Depot::default(); + let f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); + let f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); + let f3 = depot.link_create("d1/f3", "d1/f3").unwrap(); + let _f4 = depot.link_create("d1/d2/f4", "d2/f4").unwrap(); + let _d3 = depot.link_create("d3", "d3").unwrap(); + + let read_dir = depot.read_dir("d1").unwrap().collect::>(); + let expected_read_dir = vec![ + DirNode::Link(f1), + DirNode::Link(f2), + DirNode::Link(f3), + DirNode::Directory(PathBuf::from("d1/d2")), + ]; + assert!( + read_dir.len() == expected_read_dir.len() + && expected_read_dir.iter().all(|x| read_dir.contains(x)) + ); + } + + #[test] + fn test_path_verify() { + path_verify(Path::new("")).unwrap(); + path_verify(Path::new("f1")).unwrap(); + path_verify(Path::new("d1/f1")).unwrap(); + path_verify(Path::new("d1/f1.txt")).unwrap(); + path_verify(Path::new("d1/./f1.txt")).unwrap(); + + path_verify(Path::new("/")).unwrap_err(); + path_verify(Path::new("./f1")).unwrap_err(); + path_verify(Path::new("/d1/f1")).unwrap_err(); + path_verify(Path::new("d1/../f1.txt")).unwrap_err(); + path_verify(Path::new("/d1/../f1.txt")).unwrap_err(); + } + + #[test] + fn test_path_verify_link() { + path_verify_link(Path::new("f1")).unwrap(); + path_verify_link(Path::new("d1/f1")).unwrap(); + path_verify_link(Path::new("d1/f1.txt")).unwrap(); + path_verify_link(Path::new("d1/./f1.txt")).unwrap(); + + path_verify_link(Path::new("")).unwrap_err(); + path_verify_link(Path::new("/")).unwrap_err(); + path_verify_link(Path::new("./f1")).unwrap_err(); + path_verify_link(Path::new("/d1/f1")).unwrap_err(); + path_verify_link(Path::new("d1/../f1.txt")).unwrap_err(); + path_verify_link(Path::new("/d1/../f1.txt")).unwrap_err(); + } + + #[test] + fn test_path_iter_comps() { + let path = Path::new("comp1/comp2/./comp3/file.txt"); + let mut iter = path_iter_comps(path); + assert_eq!(iter.next(), Some(OsStr::new("comp1"))); + assert_eq!(iter.next(), Some(OsStr::new("comp2"))); + assert_eq!(iter.next(), Some(OsStr::new("comp3"))); + assert_eq!(iter.next(), Some(OsStr::new("file.txt"))); + assert_eq!(iter.next(), None); + } + } +} + +pub mod depot2 { use std::{ collections::HashSet, ffi::{OsStr, OsString}, @@ -933,19 +1773,16 @@ pub mod dotup { } } - pub fn unlink(&mut self, paths: impl Iterator>) { + pub fn unlink(&mut self, paths: impl Iterator>, uninstall: bool) { for origin in paths { let unlink_result: anyhow::Result<()> = try { let origin = self.prepare_relative_path(origin.as_ref())?; - let search_results = self.depot.link_search(&origin)?; - match search_results { - depot::SearchResult::Found(link_id) => { - self.depot.link_remove(link_id); - println!("removed link {}", origin.display()); - } - depot::SearchResult::Ancestor(_) | depot::SearchResult::NotFound => { - println!("{} is not linked", origin.display()) + let links_under: Vec<_> = self.depot.links_under(&origin)?.collect(); + for link_id in links_under { + if uninstall && self.symlink_is_installed_by_link_id(link_id)? { + self.symlink_uninstall_by_link_id(link_id)?; } + self.depot.link_remove(link_id); } }; match unlink_result { @@ -1149,7 +1986,7 @@ pub mod dotup { } else if is_installed { Color::Green } else { - Color::Cyan + Color::RGB(255, 127, 0) } } @@ -1301,6 +2138,11 @@ pub mod dotup { Ok(()) } + fn symlink_uninstall_by_link_id(&self, link_id: LinkID) -> anyhow::Result<()> { + let canonical_pair = self.canonical_pair_from_link_id(link_id); + self.symlink_uninstall(&canonical_pair.origin, &canonical_pair.destination) + } + fn canonical_pair_from_link_id(&self, link_id: LinkID) -> CanonicalPair { let link_view = self.depot.link_view(link_id); let relative_origin = link_view.origin(); @@ -1336,7 +2178,10 @@ pub mod dotup { } mod utils { - use std::path::{Component, Path, PathBuf}; + use std::{ + collections::VecDeque, + path::{Component, Path, PathBuf}, + }; use crate::{ dotup::{self, Dotup}, @@ -1345,6 +2190,29 @@ mod utils { pub const DEFAULT_DEPOT_FILE_NAME: &str = ".depot"; + /// Returns a list of canonical paths to all the files in `dir`. This includes files in + /// subdirectories. + /// Fails if dir isnt a directory or if there is some other io error. + pub fn collect_files_in_dir_recursive(dir: impl Into) -> anyhow::Result> { + let mut paths = Vec::new(); + let mut dirs = VecDeque::new(); + dirs.push_back(dir.into()); + + while let Some(dir) = dirs.pop_front() { + for entry in std::fs::read_dir(dir)? { + let entry = entry?; + let filetype = entry.file_type()?; + if filetype.is_dir() { + dirs.push_back(entry.path()); + } else { + paths.push(entry.path()); + } + } + } + + Ok(paths) + } + pub fn collect_paths_in_dir(dir: impl AsRef) -> anyhow::Result> { Ok(std::fs::read_dir(dir)? .filter_map(|e| e.ok()) @@ -1522,6 +2390,7 @@ fn main() -> anyhow::Result<()> { 2 => "debug", _ => "trace", }; + let log_level = "trace"; Logger::try_with_env_or_str(log_level)? .format(flexi_logger::colored_default_format) @@ -1565,31 +2434,48 @@ fn command_init(_global_flags: Flags, args: InitArgs) -> anyhow::Result<()> { #[derive(Parser, Debug)] struct LinkArgs { + #[clap(long)] + directory: bool, + origin: PathBuf, + destination: PathBuf, } fn command_link(global_flags: Flags, args: LinkArgs) -> anyhow::Result<()> { let mut dotup = utils::read_dotup(&global_flags)?; - dotup.link(args.origin, args.destination); + let origins = if args.directory { + vec![args.origin] + } else { + utils::collect_files_in_dir_recursive(args.origin)? + }; + for origin in origins { + dotup.link(origin, &args.destination); + } utils::write_dotup(&dotup)?; Ok(()) } #[derive(Parser, Debug)] struct UnlinkArgs { + #[clap(long)] + uninstall: bool, + paths: Vec, } fn command_unlink(global_flags: Flags, args: UnlinkArgs) -> anyhow::Result<()> { let mut dotup = utils::read_dotup(&global_flags)?; - dotup.unlink(args.paths.into_iter()); + dotup.unlink(args.paths.into_iter(), args.uninstall); utils::write_dotup(&dotup)?; Ok(()) } #[derive(Parser, Debug)] struct InstallArgs { + #[clap(long)] + directory: bool, + paths: Vec, } -- cgit From 3d98462d7e026cf27eda3775337f5bd15147bef9 Mon Sep 17 00:00:00 2001 From: diogo464 Date: Tue, 8 Feb 2022 09:08:15 +0000 Subject: snapshot --- src/main.rs | 856 ++++-------------------------------------------------------- 1 file changed, 46 insertions(+), 810 deletions(-) diff --git a/src/main.rs b/src/main.rs index 9b17787..b4f951d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ // TODO: rewrite all errors so they start with lower case mod depot { + use anyhow::Context; use std::{ collections::HashSet, ffi::{OsStr, OsString}, @@ -26,14 +27,6 @@ mod depot { InvalidPath, #[error("path must be relative and not empty")] InvalidLinkPath, - #[error("{0}")] - LinkCreateError(#[from] LinkCreateError), - } - - #[derive(Debug, Error)] - enum LinkCreateError { - #[error("an ancestor of this path is already linked")] - AncestorAlreadyLinked, } #[derive(Debug, Clone)] @@ -470,6 +463,17 @@ mod depot { Ok(self.has_links_under_unchecked(path)) } + pub fn links_verify_install(&self, link_ids: impl Iterator) -> Result<()> { + let mut destination = DepotTree::default(); + for link_id in link_ids { + let link = &self.links[link_id]; + destination + .link_create(&link.destination, link_id) + .context("link destinations overlap")?; + } + Ok(()) + } + pub fn link_view(&self, link_id: LinkID) -> LinkView { LinkView { link_id, @@ -777,6 +781,22 @@ mod depot { assert!(!depot.has_links_under("d1/d2/f4/f5").unwrap()); } + #[test] + fn test_depot_links_verify_install() { + let mut depot = Depot::default(); + let f1 = depot.link_create("nvim", ".config/nvim").unwrap(); + let f2 = depot.link_create("alacritty", ".config/alacritty").unwrap(); + let f3 = depot.link_create("bash/.bashrc", ".bashrc").unwrap(); + let f4 = depot.link_create("bash_laptop/.bashrc", ".bashrc").unwrap(); + + depot + .links_verify_install(vec![f1, f2, f3].into_iter()) + .unwrap(); + depot + .links_verify_install(vec![f1, f2, f3, f4].into_iter()) + .unwrap_err(); + } + #[test] fn test_depot_read_dir() { let mut depot = Depot::default(); @@ -842,792 +862,6 @@ mod depot { } } -pub mod depot2 { - use std::{ - collections::HashSet, - ffi::{OsStr, OsString}, - ops::Deref, - path::{Path, PathBuf}, - }; - - use slotmap::{Key, SlotMap}; - - pub use disk::{read, write}; - - slotmap::new_key_type! {pub struct LinkID;} - slotmap::new_key_type! {struct NodeID;} - - #[derive(Debug, Clone, PartialEq, Eq)] - pub enum SearchResult { - Found(LinkID), - Ancestor(LinkID), - NotFound, - } - - #[derive(Debug, Clone, PartialEq, Eq)] - pub enum DirNode { - Link(LinkID), - Directory(PathBuf), - } - - #[derive(Debug)] - pub struct LinkView<'a> { - link_id: LinkID, - depot: &'a Depot, - } - - impl<'a> LinkView<'a> { - pub fn origin(&self) -> &Path { - &self.depot.links[self.link_id].origin - } - - pub fn destination(&self) -> &Path { - &self.depot.links[self.link_id].destination - } - } - - // wrapper for a path under the depot - // this path is relative and does not contain `..` or similar - // Deref(Path) - struct DepotPath(PathBuf); - impl Deref for DepotPath { - type Target = Path; - - fn deref(&self) -> &Self::Target { - self.0.deref() - } - } - - #[derive(Debug, Clone)] - struct Node { - comp: OsString, - parent: NodeID, - kind: NodeKind, - } - - #[derive(Debug, Clone)] - enum NodeKind { - Link(LinkID), - Directory(HashSet), - } - - #[derive(Debug, Clone)] - struct Link { - origin: PathBuf, - destination: PathBuf, - node_id: NodeID, - } - - #[derive(Debug, Clone)] - pub struct Depot { - links: SlotMap, - nodes: SlotMap, - root: NodeID, - } - - impl Default for Depot { - fn default() -> Self { - let mut nodes = SlotMap::default(); - let root = nodes.insert(Node { - comp: Default::default(), - parent: Default::default(), - kind: NodeKind::Directory(Default::default()), - }); - Self { - links: Default::default(), - nodes, - root, - } - } - } - - impl Depot { - pub fn link_create( - &mut self, - origin: impl AsRef, - destination: impl AsRef, - ) -> anyhow::Result { - let origin = origin.as_ref(); - let destination = destination.as_ref(); - verify_link_path(origin)?; - verify_link_path(destination)?; - - // example - // origin = fish/config.fish - // destination = .config/fish/config.fish - - // search - // if ancestor - return error - // if found - update destination - // if not found - create - - match self.search_unchecked(&origin) { - SearchResult::Found(link_id) => { - let link = &mut self.links[link_id]; - link.destination = destination.to_owned(); - Ok(link_id) - } - SearchResult::Ancestor(_) => Err(anyhow::anyhow!( - "An ancestor of this path is already linked" - )), - SearchResult::NotFound => { - let link_id = self.links.insert(Link { - origin: origin.to_owned(), - destination: destination.to_owned(), - node_id: Default::default(), - }); - let node_id = self.node_create_link(origin, link_id); - self.links[link_id].node_id = node_id; - Ok(link_id) - } - } - } - - /// moves the link specified by `link_id` to the path at `destination`. - /// if the link is already at the destination nothing is done. - /// if the destination is another link that that link is removed. - /// if the destination is under another link then an error is returned. - pub fn link_move( - &mut self, - link_id: LinkID, - destination: impl AsRef, - ) -> anyhow::Result<()> { - let destination = destination.as_ref(); - verify_link_path(destination)?; - - let link_node_id = self.links[link_id].node_id; - let link_parent_node_id = self.nodes[link_node_id].parent; - let (node_id, found) = self.node_search(destination); - - // the link is already at the destination - if found && node_id == link_node_id { - return Ok(()); - } - - if found { - let node = &self.nodes[node_id]; - match &node.kind { - NodeKind::Link(node_link_id) => { - let node_parent_id = node.parent; - let node_link_id = *node_link_id; - assert_ne!(link_id, node_link_id); - self.node_child_remove(link_parent_node_id, link_node_id); - self.node_child_add(node_parent_id, link_node_id); - self.node_set_parent(link_node_id, node_parent_id); - self.link_remove(node_link_id); - let new_origin = self.node_build_path(link_node_id); - self.links[link_id].origin = new_origin; - Ok(()) - } - NodeKind::Directory(..) => Err(anyhow::anyhow!( - "Cannot move link, other links exist under the destination" - )), - } - } else { - let node = &self.nodes[node_id]; - match &node.kind { - NodeKind::Link(..) => Err(anyhow::anyhow!( - "Cannot move link, an ancestor is already linked" - )), - NodeKind::Directory(_) => { - let new_node_id = self.node_create_link(destination, link_id); - let new_origin = self.node_build_path(new_node_id); - self.node_remove(link_node_id); - self.links[link_id].node_id = new_node_id; - self.links[link_id].origin = new_origin; - Ok(()) - } - } - } - } - - pub fn link_remove(&mut self, link_id: LinkID) { - let node_id = self.links[link_id].node_id; - self.node_remove(node_id); - self.links.remove(link_id); - } - - pub fn link_view(&self, link_id: LinkID) -> LinkView { - LinkView { - link_id, - depot: self, - } - } - - /// searchs for the link at `origin`. - /// returns SearchResult::Found(..) if there is a link at `origin`. - /// returns SearchResult::Ancestor(..) if an ancestor of `origin` is linked. - /// returns SearchResult::NotFound otherwise. - pub fn link_search(&self, origin: impl AsRef) -> anyhow::Result { - let origin = origin.as_ref(); - verify_path(origin)?; - if origin.components().next().is_none() { - return Ok(SearchResult::NotFound); - } - Ok(self.search_unchecked(&origin)) - } - - /// finds the link at origin. - pub fn link_find(&self, origin: impl AsRef) -> anyhow::Result> { - match self.link_search(origin)? { - SearchResult::Found(link_id) => Ok(Some(link_id)), - SearchResult::Ancestor(_) | SearchResult::NotFound => Ok(None), - } - } - - /// returns an iterator for all the links at or under the given path. - pub fn links_under( - &self, - path: impl AsRef, - ) -> anyhow::Result + '_> { - let path = path.as_ref(); - verify_path(path)?; - - let mut link_ids = Vec::new(); - if let Some(node_id) = self.node_find(path) { - let mut node_ids = vec![node_id]; - while let Some(node_id) = node_ids.pop() { - let node = &self.nodes[node_id]; - match &node.kind { - NodeKind::Link(link_id) => link_ids.push(*link_id), - NodeKind::Directory(children) => node_ids.extend(children.iter().copied()), - } - } - } - Ok(link_ids.into_iter()) - } - - pub fn has_links_under(&self, path: impl AsRef) -> anyhow::Result { - let path = path.as_ref(); - verify_path(path)?; - - match self.node_find(path) { - Some(node_id) => match &self.nodes[node_id].kind { - NodeKind::Link(_) => Ok(true), - NodeKind::Directory(children) => Ok(!children.is_empty()), - }, - None => Ok(false), - } - } - - /// returns true if the `path` is a link or contains an ancestor that is linked. - /// returns false otherwise. - pub fn link_exists(&self, path: impl AsRef) -> bool { - match self.link_search(path) { - Ok(SearchResult::Found(..)) | Ok(SearchResult::Ancestor(..)) => true, - _ => false, - } - } - - pub fn read_dir( - &self, - path: impl AsRef, - ) -> anyhow::Result + '_> { - let path = path.as_ref(); - verify_path(path)?; - - let node_id = match self.node_find(path) { - Some(node_id) => node_id, - None => return Err(anyhow::anyhow!("Directory does not exist")), - }; - let node = &self.nodes[node_id]; - let children = match &node.kind { - NodeKind::Link(_) => return Err(anyhow::anyhow!("Path is not a directory")), - NodeKind::Directory(children) => children, - }; - Ok(children.iter().map(|id| { - let node = &self.nodes[*id]; - match &node.kind { - NodeKind::Link(link_id) => DirNode::Link(*link_id), - NodeKind::Directory(_) => DirNode::Directory(self.node_build_path(*id)), - } - })) - } - - fn search_unchecked(&self, origin: &Path) -> SearchResult { - debug_assert!(verify_link_path(origin).is_ok()); - - let mut origin_comps = iter_path_comps(&origin); - let mut curr_node = self.root; - 'outer: loop { - let node = &self.nodes[curr_node]; - let curr_comp = origin_comps.next(); - match &node.kind { - NodeKind::Link(link_id) => match curr_comp { - Some(_) => break SearchResult::Ancestor(*link_id), - None => break SearchResult::Found(*link_id), - }, - NodeKind::Directory(children) => match curr_comp { - Some(curr_comp) => { - for &child_id in children.iter() { - let child = &self.nodes[child_id]; - if &child.comp == curr_comp { - curr_node = child_id; - continue 'outer; - } - } - break SearchResult::NotFound; - } - None => break SearchResult::NotFound, - }, - } - } - } - - /// creates a new directory node with no children. - /// the node specified by `parent` must be a directory node. - fn node_create_dir_empty(&mut self, parent: NodeID, comp: OsString) -> NodeID { - let node_id = self.nodes.insert(Node { - comp, - parent, - kind: NodeKind::Directory(Default::default()), - }); - self.node_child_add(parent, node_id); - node_id - } - - /// all the nodes up to the node to be created have to be directory nodes. - /// `path` must be a verified link path. - fn node_create_link(&mut self, path: &Path, link_id: LinkID) -> NodeID { - assert!(verify_link_path(path).is_ok()); - let mut curr_node_id = self.root; - let mut path_comps = iter_path_comps(path).peekable(); - // unwrap: a verified link path has atleast 1 component - let mut curr_path_comp = path_comps.next().unwrap(); - - while path_comps.peek().is_some() { - let next_node = match self.node_children_search(curr_node_id, curr_path_comp) { - Some(child_id) => child_id, - None => self.node_create_dir_empty(curr_node_id, curr_path_comp.to_owned()), - }; - curr_node_id = next_node; - // unwrap: we known next is Some beacause of this loop's condition - curr_path_comp = path_comps.next().unwrap(); - } - - let new_node = self.nodes.insert(Node { - comp: curr_path_comp.to_owned(), - parent: curr_node_id, - kind: NodeKind::Link(link_id), - }); - self.node_child_add(curr_node_id, new_node); - new_node - } - - /// finds the node at the given path. - /// `path` must be a verified path. - fn node_find(&self, path: &Path) -> Option { - match self.node_search(path) { - (node_id, true) => Some(node_id), - _ => None, - } - } - - /// searches for the node at `path`. if that node does not exists then it returns the - /// closest node. return (closest_node, found) - fn node_search(&self, path: &Path) -> (NodeID, bool) { - debug_assert!(verify_path(path).is_ok()); - - let mut origin_comps = iter_path_comps(&path).peekable(); - let mut curr_node = self.root; - - if origin_comps.peek().is_none() { - return (self.root, true); - } - - 'outer: loop { - let node = &self.nodes[curr_node]; - match origin_comps.next() { - Some(curr_comp) => match &node.kind { - NodeKind::Link(..) => break (curr_node, false), - NodeKind::Directory(children) => { - for &child_id in children.iter() { - let child = &self.nodes[child_id]; - if &child.comp == curr_comp { - curr_node = child_id; - continue 'outer; - } - } - break (curr_node, false); - } - }, - None => break (curr_node, true), - } - } - } - - /// adds `new_child` to `node_id`'s children. - /// the node specified by `node_id` must be a directory node. - fn node_child_add(&mut self, node_id: NodeID, new_child: NodeID) { - let node = &mut self.nodes[node_id]; - match node.kind { - NodeKind::Directory(ref mut children) => { - children.insert(new_child); - } - _ => unreachable!(), - } - } - - /// searchs for a child with the given comp and returns its id. - /// the node specified by `node_id` must be a directory node. - fn node_children_search(&self, node_id: NodeID, search_comp: &OsStr) -> Option { - let child_ids = match &self.nodes[node_id].kind { - NodeKind::Directory(c) => c, - _ => unreachable!(), - }; - for &child_id in child_ids { - let child = &self.nodes[child_id]; - if child.comp == search_comp { - return Some(child_id); - } - } - None - } - - /// removes `child` from `node_id`'s children. - /// the node specified by `node_id` must be a directory node and it must contain the node - /// `child`. - fn node_child_remove(&mut self, node_id: NodeID, child: NodeID) { - let node = &mut self.nodes[node_id]; - let remove_node = match &mut node.kind { - NodeKind::Directory(children) => { - let contained = children.remove(&child); - assert!(contained); - children.is_empty() - } - _ => unreachable!(), - }; - if remove_node && node_id != self.root { - self.node_remove(node_id); - } - } - - /// build the path that references this node. - fn node_build_path(&self, node_id: NodeID) -> PathBuf { - fn recursive_helper(nodes: &SlotMap, nid: NodeID, pbuf: &mut PathBuf) { - if nid.is_null() { - return; - } - let parent_id = nodes[nid].parent; - recursive_helper(nodes, parent_id, pbuf); - pbuf.push(&nodes[nid].comp); - } - - let mut node_path = PathBuf::default(); - recursive_helper(&self.nodes, node_id, &mut node_path); - node_path - } - - fn node_set_parent(&mut self, node_id: NodeID, parent: NodeID) { - self.nodes[node_id].parent = parent; - } - - fn node_remove(&mut self, node_id: NodeID) { - debug_assert!(node_id != self.root); - debug_assert!(self.nodes.contains_key(node_id)); - - let node = self.nodes.remove(node_id).unwrap(); - match node.kind { - NodeKind::Link(..) => {} - NodeKind::Directory(children) => { - // Right now directory nodes are only removed from inside this function and - // we do not remove directories with children - assert!(children.is_empty()); - } - } - let parent_id = node.parent; - self.node_child_remove(parent_id, node_id); - } - } - - mod disk { - use std::path::{Path, PathBuf}; - - use anyhow::Context; - use serde::{Deserialize, Serialize}; - - use super::Depot; - - #[derive(Debug, Serialize, Deserialize)] - struct DiskLink { - origin: PathBuf, - destination: PathBuf, - } - - #[derive(Debug, Serialize, Deserialize)] - struct DiskLinks { - links: Vec, - } - - pub fn read(path: &Path) -> anyhow::Result { - let contents = std::fs::read_to_string(path).context("Failed to read depot file")?; - let disk_links = toml::from_str::(&contents) - .context("Failed to parse depot file")? - .links; - let mut depot = Depot::default(); - for disk_link in disk_links { - depot - .link_create(disk_link.origin, disk_link.destination) - .context("Failed to build depot from file. File is in an invalid state")?; - } - Ok(depot) - } - - pub fn write(path: &Path, depot: &Depot) -> anyhow::Result<()> { - let mut links = Vec::with_capacity(depot.links.len()); - for (_, link) in depot.links.iter() { - links.push(DiskLink { - origin: link.origin.clone(), - destination: link.destination.clone(), - }); - } - let contents = toml::to_string_pretty(&DiskLinks { links }) - .context("Failed to serialize depot")?; - std::fs::write(path, contents).context("Failed to write depot to file")?; - Ok(()) - } - } - - /// a verified path is a path that: - /// + is not empty - /// + is relative - /// + does not contain Prefix/RootDir/ParentDir - fn verify_link_path(path: &Path) -> anyhow::Result<()> { - // make sure the path is not empty - if path.components().next().is_none() { - return Err(anyhow::anyhow!("Path cannot be empty")); - } - verify_path(path) - } - /// a verified path is a path that: - /// + is not empty - /// + is relative - /// + does not contain Prefix/RootDir/ParentDir - fn verify_path(path: &Path) -> anyhow::Result<()> { - // make sure the path is relative - // make sure the path does not contain '.' or '..' - for component in path.components() { - match component { - std::path::Component::Prefix(_) => { - return Err(anyhow::anyhow!("Path cannot have prefix")) - } - std::path::Component::RootDir => { - return Err(anyhow::anyhow!("Path must be relative")) - } - std::path::Component::CurDir | std::path::Component::ParentDir => { - return Err(anyhow::anyhow!("Path cannot contain '.' or '..'")) - } - std::path::Component::Normal(_) => {} - } - } - Ok(()) - } - - /// Iterate over the components of a path. - /// # Pre - /// The path can only have "Normal" components. - fn iter_path_comps(path: &Path) -> impl Iterator { - debug_assert!(verify_path(path).is_ok()); - path.components().map(|component| match component { - std::path::Component::Normal(comp) => comp, - _ => unreachable!(), - }) - } - - #[cfg(test)] - mod tests { - use super::*; - - #[test] - fn test_verify_path() { - verify_path(Path::new("")).unwrap(); - verify_path(Path::new("f1")).unwrap(); - verify_path(Path::new("d1/f1")).unwrap(); - verify_path(Path::new("d1/f1.txt")).unwrap(); - verify_path(Path::new("d1/./f1.txt")).unwrap(); - - verify_path(Path::new("/")).unwrap_err(); - verify_path(Path::new("./f1")).unwrap_err(); - verify_path(Path::new("/d1/f1")).unwrap_err(); - verify_path(Path::new("d1/../f1.txt")).unwrap_err(); - verify_path(Path::new("/d1/../f1.txt")).unwrap_err(); - } - - #[test] - fn test_verify_link_path() { - verify_link_path(Path::new("f1")).unwrap(); - verify_link_path(Path::new("d1/f1")).unwrap(); - verify_link_path(Path::new("d1/f1.txt")).unwrap(); - verify_link_path(Path::new("d1/./f1.txt")).unwrap(); - - verify_link_path(Path::new("")).unwrap_err(); - verify_link_path(Path::new("/")).unwrap_err(); - verify_link_path(Path::new("./f1")).unwrap_err(); - verify_link_path(Path::new("/d1/f1")).unwrap_err(); - verify_link_path(Path::new("d1/../f1.txt")).unwrap_err(); - verify_link_path(Path::new("/d1/../f1.txt")).unwrap_err(); - } - - #[test] - fn test_depot_link_create() { - let mut depot = Depot::default(); - depot.link_create("", "dest1.txt").unwrap_err(); - depot.link_create("comp1.txt", "").unwrap_err(); - depot.link_create("", "").unwrap_err(); - - depot.link_create("comp1.txt", "dest1.txt").unwrap(); - depot.link_create("comp1.txt", "dest1_updated.txt").unwrap(); - depot - .link_create("./comp1.txt", "dest1_updated.txt") - .unwrap_err(); - depot.link_create("/comp1.txt", "dest1.txt").unwrap_err(); - depot.link_create("dir1/", "destdir1/").unwrap(); - depot - .link_create("dir1/file1.txt", "destfile1.txt") - .unwrap_err(); - } - - #[test] - fn test_depot_link_move() { - let mut depot = Depot::default(); - let f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); - let _f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); - - depot.link_move(f1, "").unwrap_err(); - depot.link_move(f1, "d1/f2/f1").unwrap_err(); - - depot.link_move(f1, "d1/f2").unwrap(); - depot.link_move(f1, "f1").unwrap(); - assert_eq!(depot.link_view(f1).origin(), Path::new("f1")); - depot.link_move(f1, "f2").unwrap(); - assert_eq!(depot.link_view(f1).origin(), Path::new("f2")); - } - - #[test] - fn test_depot_links_under() { - let mut depot = Depot::default(); - let f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); - let f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); - let f3 = depot.link_create("d1/f3", "d1/f3").unwrap(); - let f4 = depot.link_create("d1/d2/f4", "d2/f4").unwrap(); - let d3 = depot.link_create("d3", "d3").unwrap(); - - let under_f1 = depot.links_under("d1/f1").unwrap().collect::>(); - assert_eq!(under_f1, vec![f1]); - - let under_d1 = depot.links_under("d1").unwrap().collect::>(); - let expected_under_d1 = vec![f1, f2, f3, f4]; - assert!( - under_d1.len() == expected_under_d1.len() - && expected_under_d1.iter().all(|x| under_d1.contains(x)) - ); - - let under_d2 = depot.links_under("d2").unwrap().collect::>(); - assert_eq!(under_d2, vec![]); - - let under_d3 = depot.links_under("d3").unwrap().collect::>(); - assert_eq!(under_d3, vec![d3]); - - let under_root = depot.links_under("").unwrap().collect::>(); - let expected_under_root = vec![f1, f2, f3, f4, d3]; - assert!( - under_root.len() == expected_under_root.len() - && expected_under_root.iter().all(|x| under_root.contains(x)) - ); - } - - #[test] - fn test_depot_has_links_under() { - let mut depot = Depot::default(); - let _f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); - let _f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); - let _f3 = depot.link_create("d1/f3", "d1/f3").unwrap(); - let _f4 = depot.link_create("d1/d2/f4", "d2/f4").unwrap(); - let _d3 = depot.link_create("d3", "d3").unwrap(); - - assert!(depot.has_links_under("").unwrap()); - assert!(depot.has_links_under("d1").unwrap()); - assert!(depot.has_links_under("d3").unwrap()); - assert!(depot.has_links_under("d1/f1").unwrap()); - assert!(depot.has_links_under("d1/d2").unwrap()); - assert!(depot.has_links_under("d1/d2/f4").unwrap()); - - assert!(!depot.has_links_under("d2").unwrap()); - assert!(!depot.has_links_under("d4").unwrap()); - assert!(!depot.has_links_under("d1/d2/f4/f5").unwrap()); - } - - #[test] - fn test_depot_link_remove() { - let mut depot = Depot::default(); - let f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); - assert_eq!(depot.link_search("d1/f1").unwrap(), SearchResult::Found(f1)); - depot.link_remove(f1); - assert_eq!(depot.link_search("d1/f1").unwrap(), SearchResult::NotFound); - } - - #[test] - fn test_depot_link_search() { - let mut depot = Depot::default(); - let f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); - let f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); - let f3 = depot.link_create("d1/f3", "d1/f3").unwrap(); - let f4 = depot.link_create("d1/d2/f4", "d2/f4").unwrap(); - let d3 = depot.link_create("d3", "d3").unwrap(); - - assert_eq!(depot.link_search("d1").unwrap(), SearchResult::NotFound,); - assert_eq!(depot.link_search("d1/f1").unwrap(), SearchResult::Found(f1),); - assert_eq!(depot.link_search("d1/f2").unwrap(), SearchResult::Found(f2),); - assert_eq!(depot.link_search("d1/f3").unwrap(), SearchResult::Found(f3),); - assert_eq!( - depot.link_search("d1/d2/f4").unwrap(), - SearchResult::Found(f4), - ); - assert_eq!( - depot.link_search("d1/d2/f5").unwrap(), - SearchResult::NotFound, - ); - assert_eq!( - depot.link_search("d3/f6").unwrap(), - SearchResult::Ancestor(d3), - ); - } - - #[test] - fn test_depot_read_dir() { - let mut depot = Depot::default(); - let f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); - let f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); - let f3 = depot.link_create("d1/f3", "d1/f3").unwrap(); - let _f4 = depot.link_create("d1/d2/f4", "d2/f4").unwrap(); - let _d3 = depot.link_create("d3", "d3").unwrap(); - - let read_dir = depot.read_dir("d1").unwrap().collect::>(); - let expected_read_dir = vec![ - DirNode::Link(f1), - DirNode::Link(f2), - DirNode::Link(f3), - DirNode::Directory(PathBuf::from("d1/d2")), - ]; - assert!( - read_dir.len() == expected_read_dir.len() - && expected_read_dir.iter().all(|x| read_dir.contains(x)) - ); - } - - #[test] - fn test_iter_path_comps() { - let path = Path::new("comp1/comp2/./comp3/file.txt"); - let mut iter = iter_path_comps(path); - assert_eq!(iter.next(), Some(OsStr::new("comp1"))); - assert_eq!(iter.next(), Some(OsStr::new("comp2"))); - assert_eq!(iter.next(), Some(OsStr::new("comp3"))); - assert_eq!(iter.next(), Some(OsStr::new("file.txt"))); - assert_eq!(iter.next(), None); - } - } -} - pub mod dotup { use std::{ cmp::Ordering, @@ -1793,22 +1027,20 @@ pub mod dotup { } pub fn install(&self, paths: impl Iterator>) { - let mut already_linked: HashSet = Default::default(); - for origin in paths { - let install_result: anyhow::Result<()> = try { - let origin = self.prepare_relative_path(origin.as_ref())?; - let canonical_pairs = self.canonical_pairs_under(&origin)?; - for pair in canonical_pairs { - if already_linked.contains(&pair.link_id) { - continue; - } - self.symlink_install(&pair.origin, &pair.destination)?; - already_linked.insert(pair.link_id); - } - }; - if let Err(e) = install_result { - println!("error while installing {} : {e}", origin.as_ref().display()); + let install_result: anyhow::Result<()> = try { + let mut link_ids = HashSet::::default(); + for path in paths { + let path = self.prepare_relative_path(path.as_ref())?; + link_ids.extend(self.depot.links_under(&path)?); + } + self.depot.links_verify_install(link_ids.iter().copied())?; + + for link_id in link_ids { + self.symlink_install_by_link_id(link_id)?; } + }; + if let Err(e) = install_result { + println!("error while installing : {e}"); } } @@ -2447,7 +1679,11 @@ fn command_link(global_flags: Flags, args: LinkArgs) -> anyhow::Result<()> { let origins = if args.directory { vec![args.origin] } else { - utils::collect_files_in_dir_recursive(args.origin)? + if args.origin.is_dir() { + utils::collect_files_in_dir_recursive(args.origin)? + } else { + vec![args.origin] + } }; for origin in origins { dotup.link(origin, &args.destination); -- cgit From 3e5d7e936980575b47acbaa7668082af35bf042a Mon Sep 17 00:00:00 2001 From: diogo464 Date: Tue, 8 Feb 2022 09:16:44 +0000 Subject: snapshot --- src/main.rs | 86 ++++++++++++++++++++++++++----------------------------------- 1 file changed, 37 insertions(+), 49 deletions(-) diff --git a/src/main.rs b/src/main.rs index b4f951d..7b38357 100644 --- a/src/main.rs +++ b/src/main.rs @@ -43,7 +43,7 @@ mod depot { } #[derive(Debug, Clone, PartialEq, Eq)] - pub enum NodeSearchResult { + enum NodeSearchResult { Found(NodeID), /// the closest NodeID up the the search point. NotFound(NodeID), @@ -436,6 +436,7 @@ mod depot { self.link_move_unchecked(link_id, destination) } + #[allow(unused)] pub fn link_search(&self, path: impl AsRef) -> Result { let path = path.as_ref(); path_verify(path)?; @@ -568,7 +569,7 @@ mod depot { } fn path_parent_or_empty(path: &Path) -> &Path { - path.parent().unwrap_or(Path::new("")) + path.parent().unwrap_or_else(|| Path::new("")) } /// Iterate over the components of a path. @@ -879,7 +880,6 @@ pub mod dotup { #[derive(Debug)] struct CanonicalPair { - link_id: LinkID, origin: PathBuf, destination: PathBuf, } @@ -1028,11 +1028,7 @@ pub mod dotup { pub fn install(&self, paths: impl Iterator>) { let install_result: anyhow::Result<()> = try { - let mut link_ids = HashSet::::default(); - for path in paths { - let path = self.prepare_relative_path(path.as_ref())?; - link_ids.extend(self.depot.links_under(&path)?); - } + let link_ids = self.link_ids_from_paths_iter(paths)?; self.depot.links_verify_install(link_ids.iter().copied())?; for link_id in link_ids { @@ -1045,20 +1041,16 @@ pub mod dotup { } pub fn uninstall(&self, paths: impl Iterator>) { - for origin in paths { - let uninstall_result: anyhow::Result<()> = try { - let origin = self.prepare_relative_path(origin.as_ref())?; - let canonical_pairs = self.canonical_pairs_under(&origin)?; - for pair in canonical_pairs { - self.symlink_uninstall(&pair.origin, &pair.destination)?; + let uninstall_result: anyhow::Result<()> = try { + let link_ids = self.link_ids_from_paths_iter(paths)?; + for link_id in link_ids { + if self.symlink_is_installed_by_link_id(link_id)? { + self.symlink_uninstall_by_link_id(link_id)?; } - }; - if let Err(e) = uninstall_result { - println!( - "error while uninstalling {} : {e}", - origin.as_ref().display() - ); } + }; + if let Err(e) = uninstall_result { + println!("error while uninstalling {e}",); } } @@ -1135,7 +1127,7 @@ pub mod dotup { fn status_path_to_item(&self, canonical_path: &Path) -> anyhow::Result { debug_assert!(canonical_path.is_absolute()); debug_assert!(canonical_path.exists()); - let relative_path = self.prepare_relative_path(&canonical_path)?; + let relative_path = self.prepare_relative_path(canonical_path)?; let item = if canonical_path.is_dir() { if let Some(link_id) = self.depot.link_find(&relative_path)? { @@ -1189,19 +1181,17 @@ pub mod dotup { is_directory: true, } } + } else if let Some(link_id) = self.depot.link_find(&relative_path)? { + let destination = self.depot.link_view(link_id).destination().to_owned(); + StatusItem::Link { + origin: relative_path, + destination, + is_directory: false, + } } else { - if let Some(link_id) = self.depot.link_find(&relative_path)? { - let destination = self.depot.link_view(link_id).destination().to_owned(); - StatusItem::Link { - origin: relative_path, - destination, - is_directory: false, - } - } else { - StatusItem::Unlinked { - origin: relative_path, - is_directory: false, - } + StatusItem::Unlinked { + origin: relative_path, + is_directory: false, } }; Ok(item) @@ -1214,7 +1204,7 @@ pub mod dotup { } fn origin_color(exists: bool, is_installed: bool) -> Color { if !exists { - return Color::Red; + Color::Red } else if is_installed { Color::Green } else { @@ -1281,14 +1271,16 @@ pub mod dotup { Ok(relative.to_owned()) } - // returns the canonical pairs for all links under `path`. - fn canonical_pairs_under(&self, path: &Path) -> anyhow::Result> { - let origin = self.prepare_relative_path(path)?; - let mut canonical_pairs = Vec::new(); - for link_id in self.depot.links_under(origin)? { - canonical_pairs.push(self.canonical_pair_from_link_id(link_id)); + fn link_ids_from_paths_iter( + &self, + paths: impl Iterator>, + ) -> anyhow::Result> { + let mut link_ids = HashSet::::default(); + for path in paths { + let path = self.prepare_relative_path(path.as_ref())?; + link_ids.extend(self.depot.links_under(&path)?); } - Ok(canonical_pairs) + Ok(Vec::from_iter(link_ids.into_iter())) } fn symlink_is_installed_by_link_id(&self, link_id: LinkID) -> anyhow::Result { @@ -1382,7 +1374,6 @@ pub mod dotup { let canonical_origin = self.depot_dir.join(relative_origin); let canonical_destination = self.install_base.join(relative_destination); CanonicalPair { - link_id, origin: canonical_origin, destination: canonical_destination, } @@ -1562,7 +1553,7 @@ mod utils { ); assert_eq!( PathBuf::from("/home/user/configs/nvim/lua/setup.lua"), - weakly_canonical_cwd("configs/nvim/lua/setup.lua", cwd.clone()) + weakly_canonical_cwd("configs/nvim/lua/setup.lua", cwd) ); } } @@ -1622,7 +1613,6 @@ fn main() -> anyhow::Result<()> { 2 => "debug", _ => "trace", }; - let log_level = "trace"; Logger::try_with_env_or_str(log_level)? .format(flexi_logger::colored_default_format) @@ -1678,12 +1668,10 @@ fn command_link(global_flags: Flags, args: LinkArgs) -> anyhow::Result<()> { let mut dotup = utils::read_dotup(&global_flags)?; let origins = if args.directory { vec![args.origin] + } else if args.origin.is_dir() { + utils::collect_files_in_dir_recursive(args.origin)? } else { - if args.origin.is_dir() { - utils::collect_files_in_dir_recursive(args.origin)? - } else { - vec![args.origin] - } + vec![args.origin] }; for origin in origins { dotup.link(origin, &args.destination); -- cgit