From e7588a7f175ca9b9604ce35d72086489da7a66e3 Mon Sep 17 00:00:00 2001 From: diogo464 Date: Fri, 7 Apr 2023 20:49:26 +0100 Subject: version bump: 0.2.0 --- src/dotup/action_tree.rs | 341 ----------------------- src/dotup/mod.rs | 697 ++++++++++++++++++++++++++++++++--------------- src/main.rs | 57 ++-- 3 files changed, 508 insertions(+), 587 deletions(-) delete mode 100644 src/dotup/action_tree.rs (limited to 'src') diff --git a/src/dotup/action_tree.rs b/src/dotup/action_tree.rs deleted file mode 100644 index 44d787e..0000000 --- a/src/dotup/action_tree.rs +++ /dev/null @@ -1,341 +0,0 @@ -use std::{collections::HashSet, ffi::OsString, ops::Index, path::PathBuf}; - -use slotmap::SlotMap; - -use super::{AbsPath, AbsPathBuf}; - -slotmap::new_key_type! { - pub struct NodeID; - pub struct ActionID; -} - -#[derive(Debug)] -pub enum Action { - Link { source: PathBuf }, - Copy { source: PathBuf }, -} - -#[derive(Debug)] -pub struct TreeAction { - path: AbsPathBuf, - action: Action, -} - -#[derive(Debug)] -enum TreeNodeKind { - Action(ActionID), - SubTree(HashSet), -} - -#[derive(Debug)] -struct TreeNode { - path: AbsPathBuf, - component: OsString, - kind: TreeNodeKind, -} - -#[derive(Debug)] -pub struct ActionTree { - root_id: NodeID, - nodes: SlotMap, - actions: SlotMap, -} - -// -------------------- TreeAction -------------------- // - -impl TreeAction { - pub fn target(&self) -> &AbsPath { - &self.path - } - - pub fn action(&self) -> &Action { - &self.action - } -} - -// -------------------- TreeNodeKind -------------------- // - -#[allow(unused)] -impl TreeNodeKind { - fn as_action(&self) -> ActionID { - match self { - Self::Action(id) => *id, - _ => unreachable!(), - } - } - - fn as_action_mut(&mut self) -> &mut ActionID { - match self { - Self::Action(id) => id, - _ => unreachable!(), - } - } - - fn as_subtree(&self) -> &HashSet { - match self { - Self::SubTree(ids) => ids, - _ => unreachable!(), - } - } - - fn as_subtree_mut(&mut self) -> &mut HashSet { - match self { - Self::SubTree(ids) => ids, - _ => unreachable!(), - } - } -} - -// -------------------- ActionTree -------------------- // - -impl Index for ActionTree { - type Output = TreeAction; - - fn index(&self, index: ActionID) -> &Self::Output { - self.action(index).unwrap() - } -} - -impl ActionTree { - pub fn new() -> Self { - let mut nodes = SlotMap::with_key(); - let root_id = nodes.insert(TreeNode { - path: AbsPathBuf::default(), - component: OsString::new(), - kind: TreeNodeKind::SubTree(Default::default()), - }); - - Self { - root_id, - nodes, - actions: Default::default(), - } - } - - pub fn insert(&mut self, target: &AbsPath, action: Action) -> ActionID { - let action_id = self.actions.insert(TreeAction { - path: target.to_owned(), - action, - }); - self.force_insert_at(target, TreeNodeKind::Action(action_id)); - action_id - } - - pub fn install(&self) -> std::io::Result<()> { - for action_id in self.action_ids() { - self.install_action(action_id)?; - } - Ok(()) - } - - pub fn is_installed(&self, action_id: ActionID) -> bool { - let action = &self.actions[action_id]; - let target = action.target(); - match action.action() { - Action::Link { source } => { - let link = match std::fs::read_link(target) { - Ok(link) => link, - Err(_) => return false, - }; - link.canonicalize().unwrap() == source.canonicalize().unwrap() - } - Action::Copy { .. } => target.as_ref().exists(), - } - } - - pub fn uninstall(&self) -> std::io::Result<()> { - for action_id in self.action_ids() { - self.uninstall_action(action_id)?; - } - Ok(()) - } - - pub fn install_action(&self, action_id: ActionID) -> std::io::Result<()> { - let action = &self[action_id]; - match &action.action { - Action::Link { source } => { - let target = action.target(); - log::info!("Linking {:?} -> {:?}", source, target); - if target.as_ref().is_symlink() { - log::trace!("{:?} is a symlink, removing it", target); - std::fs::remove_file(target)?; - } - if let Some(parent) = target.parent() { - log::trace!("creating all directories up to {:?}", parent); - std::fs::create_dir_all(parent.as_ref())?; - } - log::trace!("creating symlink {:?} -> {:?}", source, target); - std::os::unix::fs::symlink(source, target)?; - } - Action::Copy { source: _ } => todo!(), - } - Ok(()) - } - - pub fn uninstall_action(&self, action_id: ActionID) -> std::io::Result<()> { - let action = &self[action_id]; - if let Action::Link { ref source } = action.action { - let target = action.target(); - if target.as_ref().is_symlink() { - log::trace!("{:?} is a symlink", target); - let symlink_target = std::fs::read_link(target.as_ref())?; - if symlink_target == *source { - log::info!("symlink target is {:?}, removing it", source); - std::fs::remove_file(target)?; - } else { - log::trace!( - "symlink target is {:?}, not {:?}, not removing it", - symlink_target, - source - ); - } - } - } - Ok(()) - } - - pub fn action_ids(&self) -> impl Iterator + '_ { - self.actions.keys() - } - - pub fn action(&self, action_id: ActionID) -> Option<&TreeAction> { - self.actions.get(action_id) - } - - /// Creates all nodes up to the given path. - /// If one of the nodes is an action node, it will be replaced with a subtree node. - fn force_insert_at(&mut self, target: &AbsPath, kind: TreeNodeKind) -> NodeID { - let mut curr = self.root_id; - for comp in target.components() { - { - // Try to find node if it exists - let curr_node = &mut self.nodes[curr]; - match curr_node.kind { - TreeNodeKind::Action(action) => { - self.actions.remove(action); - curr_node.kind = TreeNodeKind::SubTree(Default::default()); - match curr_node.kind { - TreeNodeKind::SubTree(ref mut children) => children, - _ => unreachable!(), - } - } - TreeNodeKind::SubTree(ref mut children) => children, - }; - - let children = self.nodes[curr].kind.as_subtree(); - for &child_id in children.iter() { - let child_node = &self.nodes[child_id]; - if child_node.component == comp { - curr = child_id; - break; - } - } - } - { - // Create new node - let new_node = TreeNode { - path: self.nodes[curr].path.join(comp), - component: comp.to_owned(), - kind: TreeNodeKind::SubTree(Default::default()), - }; - let new_id = self.nodes.insert(new_node); - match &mut self.nodes[curr].kind { - TreeNodeKind::SubTree(children) => children.insert(new_id), - _ => unreachable!(), - }; - curr = new_id; - } - } - let prev_kind = std::mem::replace(&mut self.nodes[curr].kind, kind); - if let TreeNodeKind::SubTree(children) = prev_kind { - for &child in children.iter() { - self.remove_node(child); - } - } - curr - } - - /// Removes the given node. - /// Does not remove it from the parent's children node. - fn remove_node(&mut self, node_id: NodeID) { - let node = self - .nodes - .remove(node_id) - .expect("Node being removed does not exist"); - match node.kind { - TreeNodeKind::Action(action) => { - self.actions.remove(action); - } - TreeNodeKind::SubTree(children) => { - for child in children { - self.remove_node(child); - } - } - }; - } -} - -#[cfg(test)] -mod tests { - use std::{convert::TryFrom, path::Path}; - - use super::*; - - #[test] - fn empty_tree() { - let _ = ActionTree::new(); - } - - #[test] - fn single_action() { - let mut tree = ActionTree::new(); - - let action_id = tree.insert( - TryFrom::try_from("/home/user/.config/nvim").unwrap(), - Action::Link { - source: PathBuf::from("nvim"), - }, - ); - - let action = &tree[action_id]; - assert_eq!( - action.path.as_path(), - AbsPath::new(Path::new("/home/user/.config/nvim")) - ); - } - - #[test] - fn subtree_replacement() { - let mut tree = ActionTree::new(); - - let action_id = tree.insert( - TryFrom::try_from("/home/user/.config/nvim").unwrap(), - Action::Link { - source: PathBuf::from("nvim"), - }, - ); - let action_id_original = action_id; - - let action = &tree[action_id]; - assert_eq!( - action.path.as_path(), - AbsPath::new(Path::new("/home/user/.config/nvim")) - ); - - let action_id = tree.insert( - TryFrom::try_from("/home/user/.config/nvim/init.vim").unwrap(), - Action::Link { - source: PathBuf::from("nvim/init.vim"), - }, - ); - - let action = &tree[action_id]; - assert_eq!( - action.path.as_path(), - AbsPath::new(Path::new("/home/user/.config/nvim/init.vim")) - ); - - eprintln!("{:#?}", tree); - assert!(tree.action(action_id_original).is_none()); - } -} diff --git a/src/dotup/mod.rs b/src/dotup/mod.rs index a70cde5..cb163db 100644 --- a/src/dotup/mod.rs +++ b/src/dotup/mod.rs @@ -1,13 +1,12 @@ -mod action_tree; mod cfg; mod paths; -use std::collections::HashSet; use std::{ - collections::HashMap, + collections::{HashMap, HashSet, VecDeque}, path::{Path, PathBuf}, }; +use colored::Colorize; use slotmap::SlotMap; use thiserror::Error; @@ -20,37 +19,59 @@ slotmap::new_key_type! { pub struct GroupID; } #[derive(Debug, Error)] pub enum Error { #[error(transparent)] - ParseError(#[from] cfg::ParseError), + InvalidConfig(#[from] cfg::ParseError), #[error("error: {0}")] Custom(String), #[error(transparent)] IOError(#[from] std::io::Error), } -#[derive(Debug, Default)] -pub struct Group { - name: String, - parent: GroupID, - children: HashMap, - actions: Vec, +impl Error { + fn custom(e: impl std::fmt::Display) -> Self { + Self::Custom(e.to_string()) + } +} + +#[derive(Debug, Clone)] +pub struct Context { + working_directory: AbsPathBuf, + destination_directory: AbsPathBuf, +} + +impl Context { + pub fn new( + working_directory: impl Into, + destination_directory: impl Into, + ) -> std::io::Result { + let working_directory = working_directory.into().canonicalize()?; + let destination_directory = destination_directory.into().canonicalize()?; + let working_directory = + AbsPathBuf::try_from(working_directory).map_err(std::io::Error::other)?; + let destination_directory = + AbsPathBuf::try_from(destination_directory).map_err(std::io::Error::other)?; + Ok(Self { + working_directory, + destination_directory, + }) + } } #[derive(Debug)] -pub struct Dotup { - root_id: GroupID, - groups: SlotMap, +pub struct InstallParams { + pub force: bool, } -#[derive(Debug, Clone, Copy)] -pub struct InstallParams<'p> { - pub cwd: &'p Path, - pub home: &'p Path, +impl Default for InstallParams { + fn default() -> Self { + Self { force: false } + } } -#[derive(Debug, Clone, Copy)] -pub struct UninstallParams<'p> { - pub cwd: &'p Path, - pub home: &'p Path, +#[derive(Debug)] +pub struct Dotup { + context: Context, + root_id: GroupID, + groups: SlotMap, } #[derive(Debug)] @@ -59,21 +80,31 @@ struct KeyValueParser { keyvalues: Vec, } +#[derive(Debug, Default)] +struct Group { + name: String, + parent: GroupID, + children: HashMap, + actions: Vec, +} + #[derive(Debug, Clone)] struct IncludeAction { group: String, + group_id: GroupID, } #[derive(Debug, Clone)] struct LinkAction { - source: PathBuf, - target: PathBuf, + source: AbsPathBuf, + target: AbsPathBuf, } +#[allow(dead_code)] #[derive(Debug, Clone)] struct CopyAction { - source: PathBuf, - target: PathBuf, + source: AbsPathBuf, + target: AbsPathBuf, } #[derive(Debug, Clone)] @@ -83,14 +114,20 @@ enum Action { Copy(CopyAction), } -pub fn load(content: &str) -> Result { +#[derive(Debug, Clone)] +enum ExecutableAction { + Link(LinkAction), + Copy(CopyAction), +} + +pub fn load(context: Context, content: &str) -> Result { let config = cfg::parse(content)?; - Dotup::from_config(config) + new(context, config) } -pub fn load_file(path: impl AsRef) -> Result { +pub fn load_file(context: Context, path: impl AsRef) -> Result { let content = std::fs::read_to_string(path)?; - load(&content) + load(context, &content) } pub fn format(content: &str) -> Result { @@ -109,190 +146,339 @@ pub fn format_file_inplace(path: &Path) -> Result<()> { Ok(()) } -// -------------------- Dotup -------------------- // +pub fn install(dotup: &Dotup, params: &InstallParams, group: &str) -> Result<()> { + fn prompt_overwrite(params: &InstallParams, target: &AbsPath) -> Result { + if params.force { + return Ok(true); + } -impl Dotup { - pub fn find_group_by_name(&self, name: &str) -> Option { - self.find_group_by_name_rooted(self.root_id, name) + let result = inquire::Confirm::new(&format!( + "overwrite existing file/directory '{}'?", + target.display() + )) + .with_default(false) + .with_help_message("Delete the existing file/directory") + .prompt(); + + match result { + Ok(overwrite) => Ok(overwrite), + Err(e) => match e { + inquire::InquireError::NotTTY => Ok(false), + _ => Err(Error::custom(e)), + }, + } } - pub fn install(&self, params: InstallParams, group_id: GroupID) -> Result<()> { - let action_tree = self.build_action_tree(params.cwd, params.home, group_id)?; - action_tree.install()?; - Ok(()) + let group_id = get_group_by_name(dotup, group)?; + let executable = collect_group_executable_actions(dotup, group_id)?; + + for action in executable { + match action { + ExecutableAction::Link(LinkAction { source, target }) => { + log::debug!("linking '{}' to '{}'", source.display(), target.display()); + if fs_exists(&target)? { + let metadata = fs_symlink_metadata(&target)?; + + // Early return if the symlink already points to the correct source + if metadata.is_symlink() && fs_symlink_points_to(&target, &source)? { + return Ok(()); + } + + if !prompt_overwrite(params, &target)? { + return Ok(()); + } + + fs_remove(&target)?; + } + + fs_create_dir_all_upto(&target)?; + fs_create_symlink(&source, &target)?; + } + ExecutableAction::Copy(_) => todo!(), + } } - pub fn uninstall(&self, params: UninstallParams, group_id: GroupID) -> Result<()> { - let action_tree = self.build_action_tree(params.cwd, params.home, group_id)?; - action_tree.uninstall()?; - Ok(()) + Ok(()) +} + +pub fn uninstall(dotup: &Dotup, group: &str) -> Result<()> { + let group_id = get_group_by_name(dotup, group)?; + let executable = collect_group_executable_actions(dotup, group_id)?; + + for action in executable { + match action { + ExecutableAction::Link(LinkAction { source, target }) => { + if !fs_exists(&target)? { + return Ok(()); + } + + if fs_symlink_points_to(&target, &source)? { + fs_remove(&target)?; + } + } + ExecutableAction::Copy(_) => todo!(), + } } - pub fn status(&self, params: InstallParams, group_id: GroupID) -> Result<()> { - let action_tree = self.build_action_tree(params.cwd, params.home, group_id)?; - for action_id in action_tree.action_ids() { - let prefix = if action_tree.is_installed(action_id) { - "INSTALLED" - } else { - "NOT INSTALLED" - }; - let action = action_tree.action(action_id).unwrap(); - let source = match action.action() { - action_tree::Action::Link { ref source } => source, - action_tree::Action::Copy { ref source } => source, - }; - let target = action.target(); - println!("{}: {} -> {}", prefix, source.display(), target.display()); + Ok(()) +} + +pub fn status(dotup: &Dotup, group: &str) -> Result<()> { + fn display_status(dotup: &Dotup, group_id: GroupID, depth: u32) -> Result<()> { + let group = &dotup.groups[group_id]; + + println!("{}{}", " ".repeat(depth as usize), group.name.blue()); + log::trace!("displaying status for group '{}'", group.name); + + for action in group.actions.iter() { + match action { + Action::Include(include) => { + log::trace!("displaying status for included group '{}'", include.group); + display_status(dotup, include.group_id, depth + 1)?; + } + Action::Link(link) => { + log::trace!("displaying status for link '{}'", link.target.display()); + + let target = link.target.display(); + let source = link.source.display(); + let installed = is_link_installed(&link)?; + let output = format!( + "{}{} -> {}", + " ".repeat(depth as usize + 1), + target, + source + ); + println!( + "{}", + if installed { + output.green() + } else { + output.red() + } + ); + } + Action::Copy(_) => todo!(), + } } Ok(()) } + + let group_id = get_group_by_name(dotup, group)?; + display_status(dotup, group_id, 0) +} + +fn new(context: Context, config: cfg::Config) -> Result { + let mut groups = SlotMap::default(); + let root_id = groups.insert(Default::default()); + let mut dotup = Dotup { + context, + root_id, + groups, + }; + + for group_cfg in config.groups { + insert_config_group(&mut dotup, root_id, group_cfg)?; + } + + resolve_includes(&mut dotup)?; + + Ok(dotup) } -impl Dotup { - fn from_config(config: cfg::Config) -> Result { - let mut groups = SlotMap::default(); - let root_id = groups.insert(Default::default()); - let mut dotup = Self { root_id, groups }; +fn insert_config_group( + dotup: &mut Dotup, + parent_id: GroupID, + mut group_cfg: cfg::Group, +) -> Result<()> { + let parent = &mut dotup.groups[parent_id]; + if parent.children.contains_key(&group_cfg.name) { + return Err(Error::Custom(format!( + "group '{}' at {} already exists", + group_cfg.name, group_cfg.location, + ))); + } - for group in config.groups { - dotup.insert_group(root_id, group)?; + let mut group = Group { + name: group_cfg.name.clone(), + parent: parent_id, + children: Default::default(), + actions: Default::default(), + }; + + for item in group_cfg + .items + .drain_filter(|item| std::matches!(item, cfg::GroupItem::Action(_))) + { + if let cfg::GroupItem::Action(action) = item { + let action = convert_config_action(&dotup.context, action)?; + group.actions.push(action); } + } + + let group_id = dotup.groups.insert(group); + let parent = &mut dotup.groups[parent_id]; + parent.children.insert(group_cfg.name, group_id); + + for item in group_cfg.items { + if let cfg::GroupItem::Group(group) = item { + insert_config_group(dotup, group_id, group)?; + } + } + + Ok(()) +} - Ok(dotup) +fn resolve_includes(dotup: &mut Dotup) -> Result<()> { + struct Patch { + group_id: GroupID, + action_idx: usize, + target_id: GroupID, } - fn find_group_by_name_rooted(&self, root: GroupID, name: &str) -> Option { - let trimmed = name.trim_start_matches('.'); - let rel_levels = name.len() - trimmed.len(); - let mut current = self.root_id; - - if rel_levels != 0 { - current = root; - for _ in 0..rel_levels - 1 { - current = self.groups[current].parent; - if current == self.root_id { - break; + let mut patches = Vec::new(); + for group_id in dotup.groups.keys() { + for idx in 0..dotup.groups[group_id].actions.len() { + let action = &dotup.groups[group_id].actions[idx]; + let target = match action { + Action::Include(include) => include.group.as_str(), + _ => continue, + }; + + let target_id = match find_group_by_name_rooted(dotup, group_id, target) { + Some(target_id) => target_id, + None => { + return Err(Error::Custom(format!("group '{}' not found", target))); } - } - } + }; - for comp in trimmed.split('.') { - let group = &self.groups[current]; - let child_id = group.children.get(comp)?; - current = *child_id; + patches.push(Patch { + group_id, + action_idx: idx, + target_id, + }); } - Some(current) } - fn insert_group(&mut self, parent_id: GroupID, mut group_cfg: cfg::Group) -> Result<()> { - let parent = &mut self.groups[parent_id]; - if parent.children.contains_key(&group_cfg.name) { - return Err(Error::Custom(format!( - "group '{}' at {} already exists", - group_cfg.name, group_cfg.location, - ))); + for patch in patches { + let group = &mut dotup.groups[patch.group_id]; + let action = &mut group.actions[patch.action_idx]; + if let Action::Include(include) = action { + include.group_id = patch.target_id; } + } - let mut group = Group { - name: group_cfg.name.clone(), - parent: parent_id, - children: Default::default(), - actions: Default::default(), - }; - - for item in group_cfg - .items - .drain_filter(|item| std::matches!(item, cfg::GroupItem::Action(_))) - { - if let cfg::GroupItem::Action(action) = item { - let action = cfg_action_to_action(action)?; - group.actions.push(action); - } + Ok(()) +} + +fn convert_config_action(context: &Context, cfg_action: cfg::Action) -> Result { + let mut parser = KeyValueParser::new(cfg_action.location, cfg_action.keyvalues); + match cfg_action.kind.as_str() { + "include" => { + let group = parser.expect("group")?; + parser.finalize()?; + Ok(Action::Include(IncludeAction { + group, + group_id: Default::default(), + })) + } + "link" => { + let source = PathBuf::from(parser.expect("source")?); + let target = PathBuf::from(parser.expect("target")?); + parser.finalize()?; + Ok(Action::Link(LinkAction { + source: make_path_absolute(&context.working_directory, &source), + target: make_path_absolute(&context.destination_directory, &target), + })) } + "copy" => { + let source = PathBuf::from(parser.expect("source")?); + let target = PathBuf::from(parser.expect("target")?); + parser.finalize()?; + Ok(Action::Copy(CopyAction { + source: make_path_absolute(&context.working_directory, &source), + target: make_path_absolute(&context.destination_directory, &target), + })) + } + _ => Err(Error::Custom(format!( + "unknown action '{}' at {}", + cfg_action.kind, cfg_action.location + ))), + } +} - let group_id = self.groups.insert(group); - let parent = &mut self.groups[parent_id]; - parent.children.insert(group_cfg.name, group_id); +fn get_group_by_name(dotup: &Dotup, name: &str) -> Result { + find_group_by_name(dotup, name) + .ok_or_else(|| Error::Custom(format!("group '{}' not found", name,))) +} - for item in group_cfg.items { - if let cfg::GroupItem::Group(group) = item { - self.insert_group(group_id, group)?; +fn find_group_by_name(dotup: &Dotup, name: &str) -> Option { + find_group_by_name_rooted(dotup, dotup.root_id, name) +} + +fn find_group_by_name_rooted(dotup: &Dotup, root: GroupID, name: &str) -> Option { + let trimmed = name.trim_start_matches('.'); + let rel_levels = name.len() - trimmed.len(); + let mut current = dotup.root_id; + + if rel_levels != 0 { + current = root; + for _ in 0..rel_levels - 1 { + current = dotup.groups[current].parent; + if current == dotup.root_id { + break; } } + } - Ok(()) + for comp in trimmed.split('.') { + let group = &dotup.groups[current]; + let child_id = group.children.get(comp)?; + current = *child_id; } + Some(current) +} - fn build_action_tree( - &self, - cwd: &Path, - home: &Path, - group_id: GroupID, - ) -> Result { - fn inner_helper( - dotup: &Dotup, - cwd: &AbsPath, - home: &AbsPath, - group_id: GroupID, - tree: &mut action_tree::ActionTree, - visited: &mut HashSet, - ) -> Result<()> { - if visited.contains(&group_id) { - return Ok(()); - } - visited.insert(group_id); - - let group = &dotup.groups[group_id]; - for action in group.actions.iter() { - match action { - Action::Include(action) => { - let include_id = dotup - .find_group_by_name_rooted(group_id, &action.group) - .ok_or_else(|| { - Error::Custom(format!( - "group '{}' not found in include from group '{}'", - action.group, dotup.groups[group_id].name, - )) - })?; - inner_helper(dotup, cwd, home, include_id, tree, visited)?; - } - Action::Link(action) => { - let source = make_absolute_path(cwd, &action.source).into(); - let target = make_absolute_path(home, &action.target); - tree.insert(&target, action_tree::Action::Link { source }); - } - Action::Copy(action) => { - let source = make_absolute_path(cwd, &action.source).into(); - let target = make_absolute_path(home, &action.target); - tree.insert(&target, action_tree::Action::Copy { source }); - } +fn collect_group_executable_actions( + dotup: &Dotup, + group_id: GroupID, +) -> Result> { + let mut executable = Vec::new(); + let mut visited = HashSet::new(); + let mut queue = VecDeque::from_iter(std::iter::once(group_id)); + + while let Some(group_id) = queue.pop_front() { + if !visited.insert(group_id) { + continue; + } + + let group = &dotup.groups[group_id]; + for action in &group.actions { + match action { + Action::Include(include) => { + queue.push_back(include.group_id); } + Action::Link(action) => executable.push(ExecutableAction::Link(action.clone())), + Action::Copy(action) => executable.push(ExecutableAction::Copy(action.clone())), } - - Ok(()) } + } + + Ok(executable) +} + +fn is_link_installed(link: &LinkAction) -> Result { + if !fs_exists(&link.target)? { + Ok(false) + } else { + fs_symlink_points_to(&link.target, &link.source) + } +} - let cwd = AbsPathBuf::try_from( - cwd.canonicalize() - .expect("failed to canonicalize current working directory path"), - ) - .unwrap(); - let home = AbsPathBuf::try_from( - home.canonicalize() - .expect("failed to canonicalize home directory path"), - ) - .unwrap(); - - let mut tree = action_tree::ActionTree::new(); - inner_helper( - self, - &cwd, - &home, - group_id, - &mut tree, - &mut Default::default(), - )?; - Ok(tree) +fn make_path_absolute(root: &AbsPath, path: &Path) -> AbsPathBuf { + if path.is_absolute() { + AbsPathBuf::try_from(path.to_owned()).unwrap() + } else { + root.join(path) } } @@ -328,47 +514,128 @@ impl KeyValueParser { } } -// -------------------- Misc -------------------- // +// -------------------- Filesystem -------------------- // -fn cfg_action_to_action(cfg_action: cfg::Action) -> Result { - let mut parser = KeyValueParser::new(cfg_action.location, cfg_action.keyvalues); - match cfg_action.kind.as_str() { - "include" => { - let group = parser.expect("group")?; - parser.finalize()?; - Ok(Action::Include(IncludeAction { group })) - } - "link" => { - let source = parser.expect("source")?; - let target = parser.expect("target")?; - parser.finalize()?; - Ok(Action::Link(LinkAction { - source: PathBuf::from(source), - target: PathBuf::from(target), - })) - } - "copy" => { - let source = parser.expect("source")?; - let target = parser.expect("target")?; - parser.finalize()?; - Ok(Action::Copy(CopyAction { - source: PathBuf::from(source), - target: PathBuf::from(target), - })) - } - _ => Err(Error::Custom(format!( - "unknown action '{}' at {}", - cfg_action.kind, cfg_action.location - ))), - } +fn fs_exists(path: impl AsRef) -> Result { + path.as_ref().try_exists().map_err(|err| { + Error::Custom(format!( + "failed to check existence of target '{}': {}", + path.as_ref().display(), + err + )) + }) } -/// Returns `path` if it is already absolute. -/// Otherwise makes it absolute by prepending `self.root`. -fn make_absolute_path(root: &AbsPath, path: &Path) -> AbsPathBuf { - if path.is_absolute() { - AbsPathBuf::try_from(path).unwrap() +#[allow(unused)] +fn fs_metadata(path: impl AsRef) -> Result { + let path = path.as_ref(); + std::fs::metadata(path).map_err(|err| { + Error::Custom(format!( + "failed to get metadata of target '{}': {}", + path.display(), + err + )) + }) +} + +fn fs_symlink_metadata(path: impl AsRef) -> Result { + let path = path.as_ref(); + std::fs::symlink_metadata(path).map_err(|err| { + Error::Custom(format!( + "failed to get metadata of target '{}': {}", + path.display(), + err + )) + }) +} + +fn fs_read_symlink(path: impl AsRef) -> Result { + let path = path.as_ref(); + std::fs::read_link(path).map_err(|err| { + Error::Custom(format!( + "failed to read symlink '{}': {}", + path.display(), + err + )) + }) +} + +fn fs_canonicalize(path: impl AsRef) -> Result { + let path = path.as_ref(); + path.canonicalize().map_err(|err| { + Error::Custom(format!( + "failed to canonicalize path '{}': {}", + path.display(), + err + )) + }) +} + +fn fs_symlink_points_to(path: impl AsRef, target: impl AsRef) -> Result { + let path = path.as_ref(); + let target = target.as_ref(); + let link_target = fs_read_symlink(path)?; + let target_canonical = fs_canonicalize(target)?; + let link_target_canonical = fs_canonicalize(link_target)?; + Ok(target_canonical == link_target_canonical) +} + +fn fs_remove(path: impl AsRef) -> Result<()> { + let path = path.as_ref(); + log::debug!("removing target '{}'", path.display()); + + if !fs_exists(path)? { + return Ok(()); + } + + let metadata = fs_symlink_metadata(path)?; + if metadata.is_dir() { + std::fs::remove_dir_all(path).map_err(|err| { + Error::Custom(format!( + "failed to remove target '{}': {}", + path.display(), + err + )) + }) } else { - AbsPathBuf::from_rel(root, TryFrom::try_from(path).unwrap()) + std::fs::remove_file(path).map_err(|err| { + Error::Custom(format!( + "failed to remove target '{}': {}", + path.display(), + err + )) + }) } } + +fn fs_create_symlink(source: impl AsRef, target: impl AsRef) -> Result<()> { + let source = source.as_ref(); + let target = target.as_ref(); + log::debug!( + "creating symlink '{}' -> '{}'", + target.display(), + source.display() + ); + std::os::unix::fs::symlink(source, target).map_err(|err| { + Error::Custom(format!( + "failed to create symlink '{}' -> '{}': {}", + target.display(), + source.display(), + err + )) + }) +} + +fn fs_create_dir_all_upto(path: impl AsRef) -> Result<()> { + let path = path.as_ref(); + let parent = path.parent().ok_or_else(|| { + Error::Custom(format!("failed to get parent of path '{}'", path.display())) + })?; + std::fs::create_dir_all(parent).map_err(|err| { + Error::Custom(format!( + "failed to create directory '{}': {}", + parent.display(), + err + )) + }) +} diff --git a/src/main.rs b/src/main.rs index 80a03b9..f035a45 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,13 @@ #![feature(drain_filter)] +#![feature(io_error_other)] -//pub mod config; pub mod dotup; use std::path::PathBuf; use anyhow::Context; use clap::{Parser, Subcommand}; +use dotup::InstallParams; #[derive(Parser, Debug)] struct GlobalFlags { @@ -27,6 +28,9 @@ enum SubCommand { #[derive(Parser, Debug)] struct InstallArgs { + #[clap(short, long)] + force: bool, + groups: Vec, } @@ -47,6 +51,7 @@ struct FormatArgs {} struct Args { #[clap(flatten)] globals: GlobalFlags, + #[clap(subcommand)] command: SubCommand, } @@ -64,6 +69,10 @@ fn main() -> anyhow::Result<()> { } impl GlobalFlags { + fn get_working_dir(&self) -> PathBuf { + self.config.parent().unwrap().to_path_buf() + } + fn base_path_or_default(&self) -> PathBuf { self.base.clone().unwrap_or_else(|| { PathBuf::from(std::env::var("HOME").expect("failed to get HOME directory")) @@ -72,49 +81,29 @@ impl GlobalFlags { } fn command_install(globals: GlobalFlags, args: InstallArgs) -> anyhow::Result<()> { - let dotup = dotup::load_file(&globals.config).context("failed to parse config")?; - let cwd = std::env::current_dir().context("failed to get current directory")?; - let install_params = dotup::InstallParams { - cwd: &cwd, - home: &globals.base_path_or_default(), - }; + let context = helper_new_context(&globals)?; + let dotup = dotup::load_file(context, &globals.config).context("failed to parse config")?; + let params = InstallParams { force: args.force }; for group in args.groups { - match dotup.find_group_by_name(&group) { - Some(group_id) => dotup.install(install_params, group_id)?, - None => log::error!("group not found: {}", group), - }; + dotup::install(&dotup, ¶ms, &group)?; } Ok(()) } fn command_uninstall(globals: GlobalFlags, args: UninstallArgs) -> anyhow::Result<()> { - let dotup = dotup::load_file(&globals.config).context("failed to parse config")?; - let cwd = std::env::current_dir().context("failed to get current directory")?; - let uninstall_params = dotup::UninstallParams { - cwd: &cwd, - home: &globals.base_path_or_default(), - }; + let context = helper_new_context(&globals)?; + let dotup = dotup::load_file(context, &globals.config).context("failed to parse config")?; for group in args.groups { - match dotup.find_group_by_name(&group) { - Some(group_id) => dotup.uninstall(uninstall_params, group_id)?, - None => log::error!("group not found: {}", group), - }; + dotup::uninstall(&dotup, &group)?; } Ok(()) } fn command_status(globals: GlobalFlags, args: StatusArgs) -> anyhow::Result<()> { - let dotup = dotup::load_file(&globals.config).context("failed to parse config")?; - let cwd = std::env::current_dir().context("failed to get current directory")?; - let install_params = dotup::InstallParams { - cwd: &cwd, - home: &globals.base_path_or_default(), - }; + let context = helper_new_context(&globals)?; + let dotup = dotup::load_file(context, &globals.config).context("failed to parse config")?; for group in args.groups { - match dotup.find_group_by_name(&group) { - Some(group_id) => dotup.status(install_params, group_id)?, - None => log::error!("group not found: {}", group), - }; + dotup::status(&dotup, &group)?; } Ok(()) } @@ -123,3 +112,9 @@ fn command_format(globals: GlobalFlags, _args: FormatArgs) -> anyhow::Result<()> dotup::format_file_inplace(&globals.config).context("failed to format config")?; Ok(()) } + +fn helper_new_context(globals: &GlobalFlags) -> anyhow::Result { + let cwd = globals.get_working_dir(); + let home = globals.base_path_or_default(); + Ok(dotup::Context::new(cwd, home)?) +} -- cgit