diff options
| author | diogo464 <[email protected]> | 2023-04-07 20:49:26 +0100 |
|---|---|---|
| committer | diogo464 <[email protected]> | 2023-04-07 20:51:32 +0100 |
| commit | e7588a7f175ca9b9604ce35d72086489da7a66e3 (patch) | |
| tree | 03379454044e505c65e32ccf96cdfc34441bf914 /src | |
| parent | fa3fecca00442acea029248c6cf1fa0ba81e96b9 (diff) | |
version bump: 0.2.00.2.0
Diffstat (limited to 'src')
| -rw-r--r-- | src/dotup/action_tree.rs | 341 | ||||
| -rw-r--r-- | src/dotup/mod.rs | 697 | ||||
| -rw-r--r-- | src/main.rs | 57 |
3 files changed, 508 insertions, 587 deletions
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 @@ | |||
| 1 | use std::{collections::HashSet, ffi::OsString, ops::Index, path::PathBuf}; | ||
| 2 | |||
| 3 | use slotmap::SlotMap; | ||
| 4 | |||
| 5 | use super::{AbsPath, AbsPathBuf}; | ||
| 6 | |||
| 7 | slotmap::new_key_type! { | ||
| 8 | pub struct NodeID; | ||
| 9 | pub struct ActionID; | ||
| 10 | } | ||
| 11 | |||
| 12 | #[derive(Debug)] | ||
| 13 | pub enum Action { | ||
| 14 | Link { source: PathBuf }, | ||
| 15 | Copy { source: PathBuf }, | ||
| 16 | } | ||
| 17 | |||
| 18 | #[derive(Debug)] | ||
| 19 | pub struct TreeAction { | ||
| 20 | path: AbsPathBuf, | ||
| 21 | action: Action, | ||
| 22 | } | ||
| 23 | |||
| 24 | #[derive(Debug)] | ||
| 25 | enum TreeNodeKind { | ||
| 26 | Action(ActionID), | ||
| 27 | SubTree(HashSet<NodeID>), | ||
| 28 | } | ||
| 29 | |||
| 30 | #[derive(Debug)] | ||
| 31 | struct TreeNode { | ||
| 32 | path: AbsPathBuf, | ||
| 33 | component: OsString, | ||
| 34 | kind: TreeNodeKind, | ||
| 35 | } | ||
| 36 | |||
| 37 | #[derive(Debug)] | ||
| 38 | pub struct ActionTree { | ||
| 39 | root_id: NodeID, | ||
| 40 | nodes: SlotMap<NodeID, TreeNode>, | ||
| 41 | actions: SlotMap<ActionID, TreeAction>, | ||
| 42 | } | ||
| 43 | |||
| 44 | // -------------------- TreeAction -------------------- // | ||
| 45 | |||
| 46 | impl TreeAction { | ||
| 47 | pub fn target(&self) -> &AbsPath { | ||
| 48 | &self.path | ||
| 49 | } | ||
| 50 | |||
| 51 | pub fn action(&self) -> &Action { | ||
| 52 | &self.action | ||
| 53 | } | ||
| 54 | } | ||
| 55 | |||
| 56 | // -------------------- TreeNodeKind -------------------- // | ||
| 57 | |||
| 58 | #[allow(unused)] | ||
| 59 | impl TreeNodeKind { | ||
| 60 | fn as_action(&self) -> ActionID { | ||
| 61 | match self { | ||
| 62 | Self::Action(id) => *id, | ||
| 63 | _ => unreachable!(), | ||
| 64 | } | ||
| 65 | } | ||
| 66 | |||
| 67 | fn as_action_mut(&mut self) -> &mut ActionID { | ||
| 68 | match self { | ||
| 69 | Self::Action(id) => id, | ||
| 70 | _ => unreachable!(), | ||
| 71 | } | ||
| 72 | } | ||
| 73 | |||
| 74 | fn as_subtree(&self) -> &HashSet<NodeID> { | ||
| 75 | match self { | ||
| 76 | Self::SubTree(ids) => ids, | ||
| 77 | _ => unreachable!(), | ||
| 78 | } | ||
| 79 | } | ||
| 80 | |||
| 81 | fn as_subtree_mut(&mut self) -> &mut HashSet<NodeID> { | ||
| 82 | match self { | ||
| 83 | Self::SubTree(ids) => ids, | ||
| 84 | _ => unreachable!(), | ||
| 85 | } | ||
| 86 | } | ||
| 87 | } | ||
| 88 | |||
| 89 | // -------------------- ActionTree -------------------- // | ||
| 90 | |||
| 91 | impl Index<ActionID> for ActionTree { | ||
| 92 | type Output = TreeAction; | ||
| 93 | |||
| 94 | fn index(&self, index: ActionID) -> &Self::Output { | ||
| 95 | self.action(index).unwrap() | ||
| 96 | } | ||
| 97 | } | ||
| 98 | |||
| 99 | impl ActionTree { | ||
| 100 | pub fn new() -> Self { | ||
| 101 | let mut nodes = SlotMap::with_key(); | ||
| 102 | let root_id = nodes.insert(TreeNode { | ||
| 103 | path: AbsPathBuf::default(), | ||
| 104 | component: OsString::new(), | ||
| 105 | kind: TreeNodeKind::SubTree(Default::default()), | ||
| 106 | }); | ||
| 107 | |||
| 108 | Self { | ||
| 109 | root_id, | ||
| 110 | nodes, | ||
| 111 | actions: Default::default(), | ||
| 112 | } | ||
| 113 | } | ||
| 114 | |||
| 115 | pub fn insert(&mut self, target: &AbsPath, action: Action) -> ActionID { | ||
| 116 | let action_id = self.actions.insert(TreeAction { | ||
| 117 | path: target.to_owned(), | ||
| 118 | action, | ||
| 119 | }); | ||
| 120 | self.force_insert_at(target, TreeNodeKind::Action(action_id)); | ||
| 121 | action_id | ||
| 122 | } | ||
| 123 | |||
| 124 | pub fn install(&self) -> std::io::Result<()> { | ||
| 125 | for action_id in self.action_ids() { | ||
| 126 | self.install_action(action_id)?; | ||
| 127 | } | ||
| 128 | Ok(()) | ||
| 129 | } | ||
| 130 | |||
| 131 | pub fn is_installed(&self, action_id: ActionID) -> bool { | ||
| 132 | let action = &self.actions[action_id]; | ||
| 133 | let target = action.target(); | ||
| 134 | match action.action() { | ||
| 135 | Action::Link { source } => { | ||
| 136 | let link = match std::fs::read_link(target) { | ||
| 137 | Ok(link) => link, | ||
| 138 | Err(_) => return false, | ||
| 139 | }; | ||
| 140 | link.canonicalize().unwrap() == source.canonicalize().unwrap() | ||
| 141 | } | ||
| 142 | Action::Copy { .. } => target.as_ref().exists(), | ||
| 143 | } | ||
| 144 | } | ||
| 145 | |||
| 146 | pub fn uninstall(&self) -> std::io::Result<()> { | ||
| 147 | for action_id in self.action_ids() { | ||
| 148 | self.uninstall_action(action_id)?; | ||
| 149 | } | ||
| 150 | Ok(()) | ||
| 151 | } | ||
| 152 | |||
| 153 | pub fn install_action(&self, action_id: ActionID) -> std::io::Result<()> { | ||
| 154 | let action = &self[action_id]; | ||
| 155 | match &action.action { | ||
| 156 | Action::Link { source } => { | ||
| 157 | let target = action.target(); | ||
| 158 | log::info!("Linking {:?} -> {:?}", source, target); | ||
| 159 | if target.as_ref().is_symlink() { | ||
| 160 | log::trace!("{:?} is a symlink, removing it", target); | ||
| 161 | std::fs::remove_file(target)?; | ||
| 162 | } | ||
| 163 | if let Some(parent) = target.parent() { | ||
| 164 | log::trace!("creating all directories up to {:?}", parent); | ||
| 165 | std::fs::create_dir_all(parent.as_ref())?; | ||
| 166 | } | ||
| 167 | log::trace!("creating symlink {:?} -> {:?}", source, target); | ||
| 168 | std::os::unix::fs::symlink(source, target)?; | ||
| 169 | } | ||
| 170 | Action::Copy { source: _ } => todo!(), | ||
| 171 | } | ||
| 172 | Ok(()) | ||
| 173 | } | ||
| 174 | |||
| 175 | pub fn uninstall_action(&self, action_id: ActionID) -> std::io::Result<()> { | ||
| 176 | let action = &self[action_id]; | ||
| 177 | if let Action::Link { ref source } = action.action { | ||
| 178 | let target = action.target(); | ||
| 179 | if target.as_ref().is_symlink() { | ||
| 180 | log::trace!("{:?} is a symlink", target); | ||
| 181 | let symlink_target = std::fs::read_link(target.as_ref())?; | ||
| 182 | if symlink_target == *source { | ||
| 183 | log::info!("symlink target is {:?}, removing it", source); | ||
| 184 | std::fs::remove_file(target)?; | ||
| 185 | } else { | ||
| 186 | log::trace!( | ||
| 187 | "symlink target is {:?}, not {:?}, not removing it", | ||
| 188 | symlink_target, | ||
| 189 | source | ||
| 190 | ); | ||
| 191 | } | ||
| 192 | } | ||
| 193 | } | ||
| 194 | Ok(()) | ||
| 195 | } | ||
| 196 | |||
| 197 | pub fn action_ids(&self) -> impl Iterator<Item = ActionID> + '_ { | ||
| 198 | self.actions.keys() | ||
| 199 | } | ||
| 200 | |||
| 201 | pub fn action(&self, action_id: ActionID) -> Option<&TreeAction> { | ||
| 202 | self.actions.get(action_id) | ||
| 203 | } | ||
| 204 | |||
| 205 | /// Creates all nodes up to the given path. | ||
| 206 | /// If one of the nodes is an action node, it will be replaced with a subtree node. | ||
| 207 | fn force_insert_at(&mut self, target: &AbsPath, kind: TreeNodeKind) -> NodeID { | ||
| 208 | let mut curr = self.root_id; | ||
| 209 | for comp in target.components() { | ||
| 210 | { | ||
| 211 | // Try to find node if it exists | ||
| 212 | let curr_node = &mut self.nodes[curr]; | ||
| 213 | match curr_node.kind { | ||
| 214 | TreeNodeKind::Action(action) => { | ||
| 215 | self.actions.remove(action); | ||
| 216 | curr_node.kind = TreeNodeKind::SubTree(Default::default()); | ||
| 217 | match curr_node.kind { | ||
| 218 | TreeNodeKind::SubTree(ref mut children) => children, | ||
| 219 | _ => unreachable!(), | ||
| 220 | } | ||
| 221 | } | ||
| 222 | TreeNodeKind::SubTree(ref mut children) => children, | ||
| 223 | }; | ||
| 224 | |||
| 225 | let children = self.nodes[curr].kind.as_subtree(); | ||
| 226 | for &child_id in children.iter() { | ||
| 227 | let child_node = &self.nodes[child_id]; | ||
| 228 | if child_node.component == comp { | ||
| 229 | curr = child_id; | ||
| 230 | break; | ||
| 231 | } | ||
| 232 | } | ||
| 233 | } | ||
| 234 | { | ||
| 235 | // Create new node | ||
| 236 | let new_node = TreeNode { | ||
| 237 | path: self.nodes[curr].path.join(comp), | ||
| 238 | component: comp.to_owned(), | ||
| 239 | kind: TreeNodeKind::SubTree(Default::default()), | ||
| 240 | }; | ||
| 241 | let new_id = self.nodes.insert(new_node); | ||
| 242 | match &mut self.nodes[curr].kind { | ||
| 243 | TreeNodeKind::SubTree(children) => children.insert(new_id), | ||
| 244 | _ => unreachable!(), | ||
| 245 | }; | ||
| 246 | curr = new_id; | ||
| 247 | } | ||
| 248 | } | ||
| 249 | let prev_kind = std::mem::replace(&mut self.nodes[curr].kind, kind); | ||
| 250 | if let TreeNodeKind::SubTree(children) = prev_kind { | ||
| 251 | for &child in children.iter() { | ||
| 252 | self.remove_node(child); | ||
| 253 | } | ||
| 254 | } | ||
| 255 | curr | ||
| 256 | } | ||
| 257 | |||
| 258 | /// Removes the given node. | ||
| 259 | /// Does not remove it from the parent's children node. | ||
| 260 | fn remove_node(&mut self, node_id: NodeID) { | ||
| 261 | let node = self | ||
| 262 | .nodes | ||
| 263 | .remove(node_id) | ||
| 264 | .expect("Node being removed does not exist"); | ||
| 265 | match node.kind { | ||
| 266 | TreeNodeKind::Action(action) => { | ||
| 267 | self.actions.remove(action); | ||
| 268 | } | ||
| 269 | TreeNodeKind::SubTree(children) => { | ||
| 270 | for child in children { | ||
| 271 | self.remove_node(child); | ||
| 272 | } | ||
| 273 | } | ||
| 274 | }; | ||
| 275 | } | ||
| 276 | } | ||
| 277 | |||
| 278 | #[cfg(test)] | ||
| 279 | mod tests { | ||
| 280 | use std::{convert::TryFrom, path::Path}; | ||
| 281 | |||
| 282 | use super::*; | ||
| 283 | |||
| 284 | #[test] | ||
| 285 | fn empty_tree() { | ||
| 286 | let _ = ActionTree::new(); | ||
| 287 | } | ||
| 288 | |||
| 289 | #[test] | ||
| 290 | fn single_action() { | ||
| 291 | let mut tree = ActionTree::new(); | ||
| 292 | |||
| 293 | let action_id = tree.insert( | ||
| 294 | TryFrom::try_from("/home/user/.config/nvim").unwrap(), | ||
| 295 | Action::Link { | ||
| 296 | source: PathBuf::from("nvim"), | ||
| 297 | }, | ||
| 298 | ); | ||
| 299 | |||
| 300 | let action = &tree[action_id]; | ||
| 301 | assert_eq!( | ||
| 302 | action.path.as_path(), | ||
| 303 | AbsPath::new(Path::new("/home/user/.config/nvim")) | ||
| 304 | ); | ||
| 305 | } | ||
| 306 | |||
| 307 | #[test] | ||
| 308 | fn subtree_replacement() { | ||
| 309 | let mut tree = ActionTree::new(); | ||
| 310 | |||
| 311 | let action_id = tree.insert( | ||
| 312 | TryFrom::try_from("/home/user/.config/nvim").unwrap(), | ||
| 313 | Action::Link { | ||
| 314 | source: PathBuf::from("nvim"), | ||
| 315 | }, | ||
| 316 | ); | ||
| 317 | let action_id_original = action_id; | ||
| 318 | |||
| 319 | let action = &tree[action_id]; | ||
| 320 | assert_eq!( | ||
| 321 | action.path.as_path(), | ||
| 322 | AbsPath::new(Path::new("/home/user/.config/nvim")) | ||
| 323 | ); | ||
| 324 | |||
| 325 | let action_id = tree.insert( | ||
| 326 | TryFrom::try_from("/home/user/.config/nvim/init.vim").unwrap(), | ||
| 327 | Action::Link { | ||
| 328 | source: PathBuf::from("nvim/init.vim"), | ||
| 329 | }, | ||
| 330 | ); | ||
| 331 | |||
| 332 | let action = &tree[action_id]; | ||
| 333 | assert_eq!( | ||
| 334 | action.path.as_path(), | ||
| 335 | AbsPath::new(Path::new("/home/user/.config/nvim/init.vim")) | ||
| 336 | ); | ||
| 337 | |||
| 338 | eprintln!("{:#?}", tree); | ||
| 339 | assert!(tree.action(action_id_original).is_none()); | ||
| 340 | } | ||
| 341 | } | ||
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 @@ | |||
| 1 | mod action_tree; | ||
| 2 | mod cfg; | 1 | mod cfg; |
| 3 | mod paths; | 2 | mod paths; |
| 4 | 3 | ||
| 5 | use std::collections::HashSet; | ||
| 6 | use std::{ | 4 | use std::{ |
| 7 | collections::HashMap, | 5 | collections::{HashMap, HashSet, VecDeque}, |
| 8 | path::{Path, PathBuf}, | 6 | path::{Path, PathBuf}, |
| 9 | }; | 7 | }; |
| 10 | 8 | ||
| 9 | use colored::Colorize; | ||
| 11 | use slotmap::SlotMap; | 10 | use slotmap::SlotMap; |
| 12 | use thiserror::Error; | 11 | use thiserror::Error; |
| 13 | 12 | ||
| @@ -20,37 +19,59 @@ slotmap::new_key_type! { pub struct GroupID; } | |||
| 20 | #[derive(Debug, Error)] | 19 | #[derive(Debug, Error)] |
| 21 | pub enum Error { | 20 | pub enum Error { |
| 22 | #[error(transparent)] | 21 | #[error(transparent)] |
| 23 | ParseError(#[from] cfg::ParseError), | 22 | InvalidConfig(#[from] cfg::ParseError), |
| 24 | #[error("error: {0}")] | 23 | #[error("error: {0}")] |
| 25 | Custom(String), | 24 | Custom(String), |
| 26 | #[error(transparent)] | 25 | #[error(transparent)] |
| 27 | IOError(#[from] std::io::Error), | 26 | IOError(#[from] std::io::Error), |
| 28 | } | 27 | } |
| 29 | 28 | ||
| 30 | #[derive(Debug, Default)] | 29 | impl Error { |
| 31 | pub struct Group { | 30 | fn custom(e: impl std::fmt::Display) -> Self { |
| 32 | name: String, | 31 | Self::Custom(e.to_string()) |
| 33 | parent: GroupID, | 32 | } |
| 34 | children: HashMap<String, GroupID>, | 33 | } |
| 35 | actions: Vec<Action>, | 34 | |
| 35 | #[derive(Debug, Clone)] | ||
| 36 | pub struct Context { | ||
| 37 | working_directory: AbsPathBuf, | ||
| 38 | destination_directory: AbsPathBuf, | ||
| 39 | } | ||
| 40 | |||
| 41 | impl Context { | ||
| 42 | pub fn new( | ||
| 43 | working_directory: impl Into<PathBuf>, | ||
| 44 | destination_directory: impl Into<PathBuf>, | ||
| 45 | ) -> std::io::Result<Self> { | ||
| 46 | let working_directory = working_directory.into().canonicalize()?; | ||
| 47 | let destination_directory = destination_directory.into().canonicalize()?; | ||
| 48 | let working_directory = | ||
| 49 | AbsPathBuf::try_from(working_directory).map_err(std::io::Error::other)?; | ||
| 50 | let destination_directory = | ||
| 51 | AbsPathBuf::try_from(destination_directory).map_err(std::io::Error::other)?; | ||
| 52 | Ok(Self { | ||
| 53 | working_directory, | ||
| 54 | destination_directory, | ||
| 55 | }) | ||
| 56 | } | ||
| 36 | } | 57 | } |
| 37 | 58 | ||
| 38 | #[derive(Debug)] | 59 | #[derive(Debug)] |
| 39 | pub struct Dotup { | 60 | pub struct InstallParams { |
| 40 | root_id: GroupID, | 61 | pub force: bool, |
| 41 | groups: SlotMap<GroupID, Group>, | ||
| 42 | } | 62 | } |
| 43 | 63 | ||
| 44 | #[derive(Debug, Clone, Copy)] | 64 | impl Default for InstallParams { |
| 45 | pub struct InstallParams<'p> { | 65 | fn default() -> Self { |
| 46 | pub cwd: &'p Path, | 66 | Self { force: false } |
| 47 | pub home: &'p Path, | 67 | } |
| 48 | } | 68 | } |
| 49 | 69 | ||
| 50 | #[derive(Debug, Clone, Copy)] | 70 | #[derive(Debug)] |
| 51 | pub struct UninstallParams<'p> { | 71 | pub struct Dotup { |
| 52 | pub cwd: &'p Path, | 72 | context: Context, |
| 53 | pub home: &'p Path, | 73 | root_id: GroupID, |
| 74 | groups: SlotMap<GroupID, Group>, | ||
| 54 | } | 75 | } |
| 55 | 76 | ||
| 56 | #[derive(Debug)] | 77 | #[derive(Debug)] |
| @@ -59,21 +80,31 @@ struct KeyValueParser { | |||
| 59 | keyvalues: Vec<cfg::KeyValue>, | 80 | keyvalues: Vec<cfg::KeyValue>, |
| 60 | } | 81 | } |
| 61 | 82 | ||
| 83 | #[derive(Debug, Default)] | ||
| 84 | struct Group { | ||
| 85 | name: String, | ||
| 86 | parent: GroupID, | ||
| 87 | children: HashMap<String, GroupID>, | ||
| 88 | actions: Vec<Action>, | ||
| 89 | } | ||
| 90 | |||
| 62 | #[derive(Debug, Clone)] | 91 | #[derive(Debug, Clone)] |
| 63 | struct IncludeAction { | 92 | struct IncludeAction { |
| 64 | group: String, | 93 | group: String, |
| 94 | group_id: GroupID, | ||
| 65 | } | 95 | } |
| 66 | 96 | ||
| 67 | #[derive(Debug, Clone)] | 97 | #[derive(Debug, Clone)] |
| 68 | struct LinkAction { | 98 | struct LinkAction { |
| 69 | source: PathBuf, | 99 | source: AbsPathBuf, |
| 70 | target: PathBuf, | 100 | target: AbsPathBuf, |
| 71 | } | 101 | } |
| 72 | 102 | ||
| 103 | #[allow(dead_code)] | ||
| 73 | #[derive(Debug, Clone)] | 104 | #[derive(Debug, Clone)] |
| 74 | struct CopyAction { | 105 | struct CopyAction { |
| 75 | source: PathBuf, | 106 | source: AbsPathBuf, |
| 76 | target: PathBuf, | 107 | target: AbsPathBuf, |
| 77 | } | 108 | } |
| 78 | 109 | ||
| 79 | #[derive(Debug, Clone)] | 110 | #[derive(Debug, Clone)] |
| @@ -83,14 +114,20 @@ enum Action { | |||
| 83 | Copy(CopyAction), | 114 | Copy(CopyAction), |
| 84 | } | 115 | } |
| 85 | 116 | ||
| 86 | pub fn load(content: &str) -> Result<Dotup> { | 117 | #[derive(Debug, Clone)] |
| 118 | enum ExecutableAction { | ||
| 119 | Link(LinkAction), | ||
| 120 | Copy(CopyAction), | ||
| 121 | } | ||
| 122 | |||
| 123 | pub fn load(context: Context, content: &str) -> Result<Dotup> { | ||
| 87 | let config = cfg::parse(content)?; | 124 | let config = cfg::parse(content)?; |
| 88 | Dotup::from_config(config) | 125 | new(context, config) |
| 89 | } | 126 | } |
| 90 | 127 | ||
| 91 | pub fn load_file(path: impl AsRef<Path>) -> Result<Dotup> { | 128 | pub fn load_file(context: Context, path: impl AsRef<Path>) -> Result<Dotup> { |
| 92 | let content = std::fs::read_to_string(path)?; | 129 | let content = std::fs::read_to_string(path)?; |
| 93 | load(&content) | 130 | load(context, &content) |
| 94 | } | 131 | } |
| 95 | 132 | ||
| 96 | pub fn format(content: &str) -> Result<String> { | 133 | pub fn format(content: &str) -> Result<String> { |
| @@ -109,190 +146,339 @@ pub fn format_file_inplace(path: &Path) -> Result<()> { | |||
| 109 | Ok(()) | 146 | Ok(()) |
| 110 | } | 147 | } |
| 111 | 148 | ||
| 112 | // -------------------- Dotup -------------------- // | 149 | pub fn install(dotup: &Dotup, params: &InstallParams, group: &str) -> Result<()> { |
| 150 | fn prompt_overwrite(params: &InstallParams, target: &AbsPath) -> Result<bool> { | ||
| 151 | if params.force { | ||
| 152 | return Ok(true); | ||
| 153 | } | ||
| 113 | 154 | ||
| 114 | impl Dotup { | 155 | let result = inquire::Confirm::new(&format!( |
| 115 | pub fn find_group_by_name(&self, name: &str) -> Option<GroupID> { | 156 | "overwrite existing file/directory '{}'?", |
| 116 | self.find_group_by_name_rooted(self.root_id, name) | 157 | target.display() |
| 158 | )) | ||
| 159 | .with_default(false) | ||
| 160 | .with_help_message("Delete the existing file/directory") | ||
| 161 | .prompt(); | ||
| 162 | |||
| 163 | match result { | ||
| 164 | Ok(overwrite) => Ok(overwrite), | ||
| 165 | Err(e) => match e { | ||
| 166 | inquire::InquireError::NotTTY => Ok(false), | ||
| 167 | _ => Err(Error::custom(e)), | ||
| 168 | }, | ||
| 169 | } | ||
| 117 | } | 170 | } |
| 118 | 171 | ||
| 119 | pub fn install(&self, params: InstallParams, group_id: GroupID) -> Result<()> { | 172 | let group_id = get_group_by_name(dotup, group)?; |
| 120 | let action_tree = self.build_action_tree(params.cwd, params.home, group_id)?; | 173 | let executable = collect_group_executable_actions(dotup, group_id)?; |
| 121 | action_tree.install()?; | 174 | |
| 122 | Ok(()) | 175 | for action in executable { |
| 176 | match action { | ||
| 177 | ExecutableAction::Link(LinkAction { source, target }) => { | ||
| 178 | log::debug!("linking '{}' to '{}'", source.display(), target.display()); | ||
| 179 | if fs_exists(&target)? { | ||
| 180 | let metadata = fs_symlink_metadata(&target)?; | ||
| 181 | |||
| 182 | // Early return if the symlink already points to the correct source | ||
| 183 | if metadata.is_symlink() && fs_symlink_points_to(&target, &source)? { | ||
| 184 | return Ok(()); | ||
| 185 | } | ||
| 186 | |||
| 187 | if !prompt_overwrite(params, &target)? { | ||
| 188 | return Ok(()); | ||
| 189 | } | ||
| 190 | |||
| 191 | fs_remove(&target)?; | ||
| 192 | } | ||
| 193 | |||
| 194 | fs_create_dir_all_upto(&target)?; | ||
| 195 | fs_create_symlink(&source, &target)?; | ||
| 196 | } | ||
| 197 | ExecutableAction::Copy(_) => todo!(), | ||
| 198 | } | ||
| 123 | } | 199 | } |
| 124 | 200 | ||
| 125 | pub fn uninstall(&self, params: UninstallParams, group_id: GroupID) -> Result<()> { | 201 | Ok(()) |
| 126 | let action_tree = self.build_action_tree(params.cwd, params.home, group_id)?; | 202 | } |
| 127 | action_tree.uninstall()?; | 203 | |
| 128 | Ok(()) | 204 | pub fn uninstall(dotup: &Dotup, group: &str) -> Result<()> { |
| 205 | let group_id = get_group_by_name(dotup, group)?; | ||
| 206 | let executable = collect_group_executable_actions(dotup, group_id)?; | ||
| 207 | |||
| 208 | for action in executable { | ||
| 209 | match action { | ||
| 210 | ExecutableAction::Link(LinkAction { source, target }) => { | ||
| 211 | if !fs_exists(&target)? { | ||
| 212 | return Ok(()); | ||
| 213 | } | ||
| 214 | |||
| 215 | if fs_symlink_points_to(&target, &source)? { | ||
| 216 | fs_remove(&target)?; | ||
| 217 | } | ||
| 218 | } | ||
| 219 | ExecutableAction::Copy(_) => todo!(), | ||
| 220 | } | ||
| 129 | } | 221 | } |
| 130 | 222 | ||
| 131 | pub fn status(&self, params: InstallParams, group_id: GroupID) -> Result<()> { | 223 | Ok(()) |
| 132 | let action_tree = self.build_action_tree(params.cwd, params.home, group_id)?; | 224 | } |
| 133 | for action_id in action_tree.action_ids() { | 225 | |
| 134 | let prefix = if action_tree.is_installed(action_id) { | 226 | pub fn status(dotup: &Dotup, group: &str) -> Result<()> { |
| 135 | "INSTALLED" | 227 | fn display_status(dotup: &Dotup, group_id: GroupID, depth: u32) -> Result<()> { |
| 136 | } else { | 228 | let group = &dotup.groups[group_id]; |
| 137 | "NOT INSTALLED" | 229 | |
| 138 | }; | 230 | println!("{}{}", " ".repeat(depth as usize), group.name.blue()); |
| 139 | let action = action_tree.action(action_id).unwrap(); | 231 | log::trace!("displaying status for group '{}'", group.name); |
| 140 | let source = match action.action() { | 232 | |
| 141 | action_tree::Action::Link { ref source } => source, | 233 | for action in group.actions.iter() { |
| 142 | action_tree::Action::Copy { ref source } => source, | 234 | match action { |
| 143 | }; | 235 | Action::Include(include) => { |
| 144 | let target = action.target(); | 236 | log::trace!("displaying status for included group '{}'", include.group); |
| 145 | println!("{}: {} -> {}", prefix, source.display(), target.display()); | 237 | display_status(dotup, include.group_id, depth + 1)?; |
| 238 | } | ||
| 239 | Action::Link(link) => { | ||
| 240 | log::trace!("displaying status for link '{}'", link.target.display()); | ||
| 241 | |||
| 242 | let target = link.target.display(); | ||
| 243 | let source = link.source.display(); | ||
| 244 | let installed = is_link_installed(&link)?; | ||
| 245 | let output = format!( | ||
| 246 | "{}{} -> {}", | ||
| 247 | " ".repeat(depth as usize + 1), | ||
| 248 | target, | ||
| 249 | source | ||
| 250 | ); | ||
| 251 | println!( | ||
| 252 | "{}", | ||
| 253 | if installed { | ||
| 254 | output.green() | ||
| 255 | } else { | ||
| 256 | output.red() | ||
| 257 | } | ||
| 258 | ); | ||
| 259 | } | ||
| 260 | Action::Copy(_) => todo!(), | ||
| 261 | } | ||
| 146 | } | 262 | } |
| 147 | Ok(()) | 263 | Ok(()) |
| 148 | } | 264 | } |
| 265 | |||
| 266 | let group_id = get_group_by_name(dotup, group)?; | ||
| 267 | display_status(dotup, group_id, 0) | ||
| 268 | } | ||
| 269 | |||
| 270 | fn new(context: Context, config: cfg::Config) -> Result<Dotup> { | ||
| 271 | let mut groups = SlotMap::default(); | ||
| 272 | let root_id = groups.insert(Default::default()); | ||
| 273 | let mut dotup = Dotup { | ||
| 274 | context, | ||
| 275 | root_id, | ||
| 276 | groups, | ||
| 277 | }; | ||
| 278 | |||
| 279 | for group_cfg in config.groups { | ||
| 280 | insert_config_group(&mut dotup, root_id, group_cfg)?; | ||
| 281 | } | ||
| 282 | |||
| 283 | resolve_includes(&mut dotup)?; | ||
| 284 | |||
| 285 | Ok(dotup) | ||
| 149 | } | 286 | } |
| 150 | 287 | ||
| 151 | impl Dotup { | 288 | fn insert_config_group( |
| 152 | fn from_config(config: cfg::Config) -> Result<Self> { | 289 | dotup: &mut Dotup, |
| 153 | let mut groups = SlotMap::default(); | 290 | parent_id: GroupID, |
| 154 | let root_id = groups.insert(Default::default()); | 291 | mut group_cfg: cfg::Group, |
| 155 | let mut dotup = Self { root_id, groups }; | 292 | ) -> Result<()> { |
| 293 | let parent = &mut dotup.groups[parent_id]; | ||
| 294 | if parent.children.contains_key(&group_cfg.name) { | ||
| 295 | return Err(Error::Custom(format!( | ||
| 296 | "group '{}' at {} already exists", | ||
| 297 | group_cfg.name, group_cfg.location, | ||
| 298 | ))); | ||
| 299 | } | ||
| 156 | 300 | ||
| 157 | for group in config.groups { | 301 | let mut group = Group { |
| 158 | dotup.insert_group(root_id, group)?; | 302 | name: group_cfg.name.clone(), |
| 303 | parent: parent_id, | ||
| 304 | children: Default::default(), | ||
| 305 | actions: Default::default(), | ||
| 306 | }; | ||
| 307 | |||
| 308 | for item in group_cfg | ||
| 309 | .items | ||
| 310 | .drain_filter(|item| std::matches!(item, cfg::GroupItem::Action(_))) | ||
| 311 | { | ||
| 312 | if let cfg::GroupItem::Action(action) = item { | ||
| 313 | let action = convert_config_action(&dotup.context, action)?; | ||
| 314 | group.actions.push(action); | ||
| 159 | } | 315 | } |
| 316 | } | ||
| 317 | |||
| 318 | let group_id = dotup.groups.insert(group); | ||
| 319 | let parent = &mut dotup.groups[parent_id]; | ||
| 320 | parent.children.insert(group_cfg.name, group_id); | ||
| 321 | |||
| 322 | for item in group_cfg.items { | ||
| 323 | if let cfg::GroupItem::Group(group) = item { | ||
| 324 | insert_config_group(dotup, group_id, group)?; | ||
| 325 | } | ||
| 326 | } | ||
| 327 | |||
| 328 | Ok(()) | ||
| 329 | } | ||
| 160 | 330 | ||
| 161 | Ok(dotup) | 331 | fn resolve_includes(dotup: &mut Dotup) -> Result<()> { |
| 332 | struct Patch { | ||
| 333 | group_id: GroupID, | ||
| 334 | action_idx: usize, | ||
| 335 | target_id: GroupID, | ||
| 162 | } | 336 | } |
| 163 | 337 | ||
| 164 | fn find_group_by_name_rooted(&self, root: GroupID, name: &str) -> Option<GroupID> { | 338 | let mut patches = Vec::new(); |
| 165 | let trimmed = name.trim_start_matches('.'); | 339 | for group_id in dotup.groups.keys() { |
| 166 | let rel_levels = name.len() - trimmed.len(); | 340 | for idx in 0..dotup.groups[group_id].actions.len() { |
| 167 | let mut current = self.root_id; | 341 | let action = &dotup.groups[group_id].actions[idx]; |
| 168 | 342 | let target = match action { | |
| 169 | if rel_levels != 0 { | 343 | Action::Include(include) => include.group.as_str(), |
| 170 | current = root; | 344 | _ => continue, |
| 171 | for _ in 0..rel_levels - 1 { | 345 | }; |
| 172 | current = self.groups[current].parent; | 346 | |
| 173 | if current == self.root_id { | 347 | let target_id = match find_group_by_name_rooted(dotup, group_id, target) { |
| 174 | break; | 348 | Some(target_id) => target_id, |
| 349 | None => { | ||
| 350 | return Err(Error::Custom(format!("group '{}' not found", target))); | ||
| 175 | } | 351 | } |
| 176 | } | 352 | }; |
| 177 | } | ||
| 178 | 353 | ||
| 179 | for comp in trimmed.split('.') { | 354 | patches.push(Patch { |
| 180 | let group = &self.groups[current]; | 355 | group_id, |
| 181 | let child_id = group.children.get(comp)?; | 356 | action_idx: idx, |
| 182 | current = *child_id; | 357 | target_id, |
| 358 | }); | ||
| 183 | } | 359 | } |
| 184 | Some(current) | ||
| 185 | } | 360 | } |
| 186 | 361 | ||
| 187 | fn insert_group(&mut self, parent_id: GroupID, mut group_cfg: cfg::Group) -> Result<()> { | 362 | for patch in patches { |
| 188 | let parent = &mut self.groups[parent_id]; | 363 | let group = &mut dotup.groups[patch.group_id]; |
| 189 | if parent.children.contains_key(&group_cfg.name) { | 364 | let action = &mut group.actions[patch.action_idx]; |
| 190 | return Err(Error::Custom(format!( | 365 | if let Action::Include(include) = action { |
| 191 | "group '{}' at {} already exists", | 366 | include.group_id = patch.target_id; |
| 192 | group_cfg.name, group_cfg.location, | ||
| 193 | ))); | ||
| 194 | } | 367 | } |
| 368 | } | ||
| 195 | 369 | ||
| 196 | let mut group = Group { | 370 | Ok(()) |
| 197 | name: group_cfg.name.clone(), | 371 | } |
| 198 | parent: parent_id, | 372 | |
| 199 | children: Default::default(), | 373 | fn convert_config_action(context: &Context, cfg_action: cfg::Action) -> Result<Action> { |
| 200 | actions: Default::default(), | 374 | let mut parser = KeyValueParser::new(cfg_action.location, cfg_action.keyvalues); |
| 201 | }; | 375 | match cfg_action.kind.as_str() { |
| 202 | 376 | "include" => { | |
| 203 | for item in group_cfg | 377 | let group = parser.expect("group")?; |
| 204 | .items | 378 | parser.finalize()?; |
| 205 | .drain_filter(|item| std::matches!(item, cfg::GroupItem::Action(_))) | 379 | Ok(Action::Include(IncludeAction { |
| 206 | { | 380 | group, |
| 207 | if let cfg::GroupItem::Action(action) = item { | 381 | group_id: Default::default(), |
| 208 | let action = cfg_action_to_action(action)?; | 382 | })) |
| 209 | group.actions.push(action); | 383 | } |
| 210 | } | 384 | "link" => { |
| 385 | let source = PathBuf::from(parser.expect("source")?); | ||
| 386 | let target = PathBuf::from(parser.expect("target")?); | ||
| 387 | parser.finalize()?; | ||
| 388 | Ok(Action::Link(LinkAction { | ||
| 389 | source: make_path_absolute(&context.working_directory, &source), | ||
| 390 | target: make_path_absolute(&context.destination_directory, &target), | ||
| 391 | })) | ||
| 211 | } | 392 | } |
| 393 | "copy" => { | ||
| 394 | let source = PathBuf::from(parser.expect("source")?); | ||
| 395 | let target = PathBuf::from(parser.expect("target")?); | ||
| 396 | parser.finalize()?; | ||
| 397 | Ok(Action::Copy(CopyAction { | ||
| 398 | source: make_path_absolute(&context.working_directory, &source), | ||
| 399 | target: make_path_absolute(&context.destination_directory, &target), | ||
| 400 | })) | ||
| 401 | } | ||
| 402 | _ => Err(Error::Custom(format!( | ||
| 403 | "unknown action '{}' at {}", | ||
| 404 | cfg_action.kind, cfg_action.location | ||
| 405 | ))), | ||
| 406 | } | ||
| 407 | } | ||
| 212 | 408 | ||
| 213 | let group_id = self.groups.insert(group); | 409 | fn get_group_by_name(dotup: &Dotup, name: &str) -> Result<GroupID> { |
| 214 | let parent = &mut self.groups[parent_id]; | 410 | find_group_by_name(dotup, name) |
| 215 | parent.children.insert(group_cfg.name, group_id); | 411 | .ok_or_else(|| Error::Custom(format!("group '{}' not found", name,))) |
| 412 | } | ||
| 216 | 413 | ||
| 217 | for item in group_cfg.items { | 414 | fn find_group_by_name(dotup: &Dotup, name: &str) -> Option<GroupID> { |
| 218 | if let cfg::GroupItem::Group(group) = item { | 415 | find_group_by_name_rooted(dotup, dotup.root_id, name) |
| 219 | self.insert_group(group_id, group)?; | 416 | } |
| 417 | |||
| 418 | fn find_group_by_name_rooted(dotup: &Dotup, root: GroupID, name: &str) -> Option<GroupID> { | ||
| 419 | let trimmed = name.trim_start_matches('.'); | ||
| 420 | let rel_levels = name.len() - trimmed.len(); | ||
| 421 | let mut current = dotup.root_id; | ||
| 422 | |||
| 423 | if rel_levels != 0 { | ||
| 424 | current = root; | ||
| 425 | for _ in 0..rel_levels - 1 { | ||
| 426 | current = dotup.groups[current].parent; | ||
| 427 | if current == dotup.root_id { | ||
| 428 | break; | ||
| 220 | } | 429 | } |
| 221 | } | 430 | } |
| 431 | } | ||
| 222 | 432 | ||
| 223 | Ok(()) | 433 | for comp in trimmed.split('.') { |
| 434 | let group = &dotup.groups[current]; | ||
| 435 | let child_id = group.children.get(comp)?; | ||
| 436 | current = *child_id; | ||
| 224 | } | 437 | } |
| 438 | Some(current) | ||
| 439 | } | ||
| 225 | 440 | ||
| 226 | fn build_action_tree( | 441 | fn collect_group_executable_actions( |
| 227 | &self, | 442 | dotup: &Dotup, |
| 228 | cwd: &Path, | 443 | group_id: GroupID, |
| 229 | home: &Path, | 444 | ) -> Result<Vec<ExecutableAction>> { |
| 230 | group_id: GroupID, | 445 | let mut executable = Vec::new(); |
| 231 | ) -> Result<action_tree::ActionTree> { | 446 | let mut visited = HashSet::new(); |
| 232 | fn inner_helper( | 447 | let mut queue = VecDeque::from_iter(std::iter::once(group_id)); |
| 233 | dotup: &Dotup, | 448 | |
| 234 | cwd: &AbsPath, | 449 | while let Some(group_id) = queue.pop_front() { |
| 235 | home: &AbsPath, | 450 | if !visited.insert(group_id) { |
| 236 | group_id: GroupID, | 451 | continue; |
| 237 | tree: &mut action_tree::ActionTree, | 452 | } |
| 238 | visited: &mut HashSet<GroupID>, | 453 | |
| 239 | ) -> Result<()> { | 454 | let group = &dotup.groups[group_id]; |
| 240 | if visited.contains(&group_id) { | 455 | for action in &group.actions { |
| 241 | return Ok(()); | 456 | match action { |
| 242 | } | 457 | Action::Include(include) => { |
| 243 | visited.insert(group_id); | 458 | queue.push_back(include.group_id); |
| 244 | |||
| 245 | let group = &dotup.groups[group_id]; | ||
| 246 | for action in group.actions.iter() { | ||
| 247 | match action { | ||
| 248 | Action::Include(action) => { | ||
| 249 | let include_id = dotup | ||
| 250 | .find_group_by_name_rooted(group_id, &action.group) | ||
| 251 | .ok_or_else(|| { | ||
| 252 | Error::Custom(format!( | ||
| 253 | "group '{}' not found in include from group '{}'", | ||
| 254 | action.group, dotup.groups[group_id].name, | ||
| 255 | )) | ||
| 256 | })?; | ||
| 257 | inner_helper(dotup, cwd, home, include_id, tree, visited)?; | ||
| 258 | } | ||
| 259 | Action::Link(action) => { | ||
| 260 | let source = make_absolute_path(cwd, &action.source).into(); | ||
| 261 | let target = make_absolute_path(home, &action.target); | ||
| 262 | tree.insert(&target, action_tree::Action::Link { source }); | ||
| 263 | } | ||
| 264 | Action::Copy(action) => { | ||
| 265 | let source = make_absolute_path(cwd, &action.source).into(); | ||
| 266 | let target = make_absolute_path(home, &action.target); | ||
| 267 | tree.insert(&target, action_tree::Action::Copy { source }); | ||
| 268 | } | ||
| 269 | } | 459 | } |
| 460 | Action::Link(action) => executable.push(ExecutableAction::Link(action.clone())), | ||
| 461 | Action::Copy(action) => executable.push(ExecutableAction::Copy(action.clone())), | ||
| 270 | } | 462 | } |
| 271 | |||
| 272 | Ok(()) | ||
| 273 | } | 463 | } |
| 464 | } | ||
| 465 | |||
| 466 | Ok(executable) | ||
| 467 | } | ||
| 468 | |||
| 469 | fn is_link_installed(link: &LinkAction) -> Result<bool> { | ||
| 470 | if !fs_exists(&link.target)? { | ||
| 471 | Ok(false) | ||
| 472 | } else { | ||
| 473 | fs_symlink_points_to(&link.target, &link.source) | ||
| 474 | } | ||
| 475 | } | ||
| 274 | 476 | ||
| 275 | let cwd = AbsPathBuf::try_from( | 477 | fn make_path_absolute(root: &AbsPath, path: &Path) -> AbsPathBuf { |
| 276 | cwd.canonicalize() | 478 | if path.is_absolute() { |
| 277 | .expect("failed to canonicalize current working directory path"), | 479 | AbsPathBuf::try_from(path.to_owned()).unwrap() |
| 278 | ) | 480 | } else { |
| 279 | .unwrap(); | 481 | root.join(path) |
| 280 | let home = AbsPathBuf::try_from( | ||
| 281 | home.canonicalize() | ||
| 282 | .expect("failed to canonicalize home directory path"), | ||
| 283 | ) | ||
| 284 | .unwrap(); | ||
| 285 | |||
| 286 | let mut tree = action_tree::ActionTree::new(); | ||
| 287 | inner_helper( | ||
| 288 | self, | ||
| 289 | &cwd, | ||
| 290 | &home, | ||
| 291 | group_id, | ||
| 292 | &mut tree, | ||
| 293 | &mut Default::default(), | ||
| 294 | )?; | ||
| 295 | Ok(tree) | ||
| 296 | } | 482 | } |
| 297 | } | 483 | } |
| 298 | 484 | ||
| @@ -328,47 +514,128 @@ impl KeyValueParser { | |||
| 328 | } | 514 | } |
| 329 | } | 515 | } |
| 330 | 516 | ||
| 331 | // -------------------- Misc -------------------- // | 517 | // -------------------- Filesystem -------------------- // |
| 332 | 518 | ||
| 333 | fn cfg_action_to_action(cfg_action: cfg::Action) -> Result<Action> { | 519 | fn fs_exists(path: impl AsRef<Path>) -> Result<bool> { |
| 334 | let mut parser = KeyValueParser::new(cfg_action.location, cfg_action.keyvalues); | 520 | path.as_ref().try_exists().map_err(|err| { |
| 335 | match cfg_action.kind.as_str() { | 521 | Error::Custom(format!( |
| 336 | "include" => { | 522 | "failed to check existence of target '{}': {}", |
| 337 | let group = parser.expect("group")?; | 523 | path.as_ref().display(), |
| 338 | parser.finalize()?; | 524 | err |
| 339 | Ok(Action::Include(IncludeAction { group })) | 525 | )) |
| 340 | } | 526 | }) |
| 341 | "link" => { | ||
| 342 | let source = parser.expect("source")?; | ||
| 343 | let target = parser.expect("target")?; | ||
| 344 | parser.finalize()?; | ||
| 345 | Ok(Action::Link(LinkAction { | ||
| 346 | source: PathBuf::from(source), | ||
| 347 | target: PathBuf::from(target), | ||
| 348 | })) | ||
| 349 | } | ||
| 350 | "copy" => { | ||
| 351 | let source = parser.expect("source")?; | ||
| 352 | let target = parser.expect("target")?; | ||
| 353 | parser.finalize()?; | ||
| 354 | Ok(Action::Copy(CopyAction { | ||
| 355 | source: PathBuf::from(source), | ||
| 356 | target: PathBuf::from(target), | ||
| 357 | })) | ||
| 358 | } | ||
| 359 | _ => Err(Error::Custom(format!( | ||
| 360 | "unknown action '{}' at {}", | ||
| 361 | cfg_action.kind, cfg_action.location | ||
| 362 | ))), | ||
| 363 | } | ||
| 364 | } | 527 | } |
| 365 | 528 | ||
| 366 | /// Returns `path` if it is already absolute. | 529 | #[allow(unused)] |
| 367 | /// Otherwise makes it absolute by prepending `self.root`. | 530 | fn fs_metadata(path: impl AsRef<Path>) -> Result<std::fs::Metadata> { |
| 368 | fn make_absolute_path(root: &AbsPath, path: &Path) -> AbsPathBuf { | 531 | let path = path.as_ref(); |
| 369 | if path.is_absolute() { | 532 | std::fs::metadata(path).map_err(|err| { |
| 370 | AbsPathBuf::try_from(path).unwrap() | 533 | Error::Custom(format!( |
| 534 | "failed to get metadata of target '{}': {}", | ||
| 535 | path.display(), | ||
| 536 | err | ||
| 537 | )) | ||
| 538 | }) | ||
| 539 | } | ||
| 540 | |||
| 541 | fn fs_symlink_metadata(path: impl AsRef<Path>) -> Result<std::fs::Metadata> { | ||
| 542 | let path = path.as_ref(); | ||
| 543 | std::fs::symlink_metadata(path).map_err(|err| { | ||
| 544 | Error::Custom(format!( | ||
| 545 | "failed to get metadata of target '{}': {}", | ||
| 546 | path.display(), | ||
| 547 | err | ||
| 548 | )) | ||
| 549 | }) | ||
| 550 | } | ||
| 551 | |||
| 552 | fn fs_read_symlink(path: impl AsRef<Path>) -> Result<PathBuf> { | ||
| 553 | let path = path.as_ref(); | ||
| 554 | std::fs::read_link(path).map_err(|err| { | ||
| 555 | Error::Custom(format!( | ||
| 556 | "failed to read symlink '{}': {}", | ||
| 557 | path.display(), | ||
| 558 | err | ||
| 559 | )) | ||
| 560 | }) | ||
| 561 | } | ||
| 562 | |||
| 563 | fn fs_canonicalize(path: impl AsRef<Path>) -> Result<PathBuf> { | ||
| 564 | let path = path.as_ref(); | ||
| 565 | path.canonicalize().map_err(|err| { | ||
| 566 | Error::Custom(format!( | ||
| 567 | "failed to canonicalize path '{}': {}", | ||
| 568 | path.display(), | ||
| 569 | err | ||
| 570 | )) | ||
| 571 | }) | ||
| 572 | } | ||
| 573 | |||
| 574 | fn fs_symlink_points_to(path: impl AsRef<Path>, target: impl AsRef<Path>) -> Result<bool> { | ||
| 575 | let path = path.as_ref(); | ||
| 576 | let target = target.as_ref(); | ||
| 577 | let link_target = fs_read_symlink(path)?; | ||
| 578 | let target_canonical = fs_canonicalize(target)?; | ||
| 579 | let link_target_canonical = fs_canonicalize(link_target)?; | ||
| 580 | Ok(target_canonical == link_target_canonical) | ||
| 581 | } | ||
| 582 | |||
| 583 | fn fs_remove(path: impl AsRef<Path>) -> Result<()> { | ||
| 584 | let path = path.as_ref(); | ||
| 585 | log::debug!("removing target '{}'", path.display()); | ||
| 586 | |||
| 587 | if !fs_exists(path)? { | ||
| 588 | return Ok(()); | ||
| 589 | } | ||
| 590 | |||
| 591 | let metadata = fs_symlink_metadata(path)?; | ||
| 592 | if metadata.is_dir() { | ||
| 593 | std::fs::remove_dir_all(path).map_err(|err| { | ||
| 594 | Error::Custom(format!( | ||
| 595 | "failed to remove target '{}': {}", | ||
| 596 | path.display(), | ||
| 597 | err | ||
| 598 | )) | ||
| 599 | }) | ||
| 371 | } else { | 600 | } else { |
| 372 | AbsPathBuf::from_rel(root, TryFrom::try_from(path).unwrap()) | 601 | std::fs::remove_file(path).map_err(|err| { |
| 602 | Error::Custom(format!( | ||
| 603 | "failed to remove target '{}': {}", | ||
| 604 | path.display(), | ||
| 605 | err | ||
| 606 | )) | ||
| 607 | }) | ||
| 373 | } | 608 | } |
| 374 | } | 609 | } |
| 610 | |||
| 611 | fn fs_create_symlink(source: impl AsRef<Path>, target: impl AsRef<Path>) -> Result<()> { | ||
| 612 | let source = source.as_ref(); | ||
| 613 | let target = target.as_ref(); | ||
| 614 | log::debug!( | ||
| 615 | "creating symlink '{}' -> '{}'", | ||
| 616 | target.display(), | ||
| 617 | source.display() | ||
| 618 | ); | ||
| 619 | std::os::unix::fs::symlink(source, target).map_err(|err| { | ||
| 620 | Error::Custom(format!( | ||
| 621 | "failed to create symlink '{}' -> '{}': {}", | ||
| 622 | target.display(), | ||
| 623 | source.display(), | ||
| 624 | err | ||
| 625 | )) | ||
| 626 | }) | ||
| 627 | } | ||
| 628 | |||
| 629 | fn fs_create_dir_all_upto(path: impl AsRef<Path>) -> Result<()> { | ||
| 630 | let path = path.as_ref(); | ||
| 631 | let parent = path.parent().ok_or_else(|| { | ||
| 632 | Error::Custom(format!("failed to get parent of path '{}'", path.display())) | ||
| 633 | })?; | ||
| 634 | std::fs::create_dir_all(parent).map_err(|err| { | ||
| 635 | Error::Custom(format!( | ||
| 636 | "failed to create directory '{}': {}", | ||
| 637 | parent.display(), | ||
| 638 | err | ||
| 639 | )) | ||
| 640 | }) | ||
| 641 | } | ||
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 @@ | |||
| 1 | #![feature(drain_filter)] | 1 | #![feature(drain_filter)] |
| 2 | #![feature(io_error_other)] | ||
| 2 | 3 | ||
| 3 | //pub mod config; | ||
| 4 | pub mod dotup; | 4 | pub mod dotup; |
| 5 | 5 | ||
| 6 | use std::path::PathBuf; | 6 | use std::path::PathBuf; |
| 7 | 7 | ||
| 8 | use anyhow::Context; | 8 | use anyhow::Context; |
| 9 | use clap::{Parser, Subcommand}; | 9 | use clap::{Parser, Subcommand}; |
| 10 | use dotup::InstallParams; | ||
| 10 | 11 | ||
| 11 | #[derive(Parser, Debug)] | 12 | #[derive(Parser, Debug)] |
| 12 | struct GlobalFlags { | 13 | struct GlobalFlags { |
| @@ -27,6 +28,9 @@ enum SubCommand { | |||
| 27 | 28 | ||
| 28 | #[derive(Parser, Debug)] | 29 | #[derive(Parser, Debug)] |
| 29 | struct InstallArgs { | 30 | struct InstallArgs { |
| 31 | #[clap(short, long)] | ||
| 32 | force: bool, | ||
| 33 | |||
| 30 | groups: Vec<String>, | 34 | groups: Vec<String>, |
| 31 | } | 35 | } |
| 32 | 36 | ||
| @@ -47,6 +51,7 @@ struct FormatArgs {} | |||
| 47 | struct Args { | 51 | struct Args { |
| 48 | #[clap(flatten)] | 52 | #[clap(flatten)] |
| 49 | globals: GlobalFlags, | 53 | globals: GlobalFlags, |
| 54 | |||
| 50 | #[clap(subcommand)] | 55 | #[clap(subcommand)] |
| 51 | command: SubCommand, | 56 | command: SubCommand, |
| 52 | } | 57 | } |
| @@ -64,6 +69,10 @@ fn main() -> anyhow::Result<()> { | |||
| 64 | } | 69 | } |
| 65 | 70 | ||
| 66 | impl GlobalFlags { | 71 | impl GlobalFlags { |
| 72 | fn get_working_dir(&self) -> PathBuf { | ||
| 73 | self.config.parent().unwrap().to_path_buf() | ||
| 74 | } | ||
| 75 | |||
| 67 | fn base_path_or_default(&self) -> PathBuf { | 76 | fn base_path_or_default(&self) -> PathBuf { |
| 68 | self.base.clone().unwrap_or_else(|| { | 77 | self.base.clone().unwrap_or_else(|| { |
| 69 | PathBuf::from(std::env::var("HOME").expect("failed to get HOME directory")) | 78 | PathBuf::from(std::env::var("HOME").expect("failed to get HOME directory")) |
| @@ -72,49 +81,29 @@ impl GlobalFlags { | |||
| 72 | } | 81 | } |
| 73 | 82 | ||
| 74 | fn command_install(globals: GlobalFlags, args: InstallArgs) -> anyhow::Result<()> { | 83 | fn command_install(globals: GlobalFlags, args: InstallArgs) -> anyhow::Result<()> { |
| 75 | let dotup = dotup::load_file(&globals.config).context("failed to parse config")?; | 84 | let context = helper_new_context(&globals)?; |
| 76 | let cwd = std::env::current_dir().context("failed to get current directory")?; | 85 | let dotup = dotup::load_file(context, &globals.config).context("failed to parse config")?; |
| 77 | let install_params = dotup::InstallParams { | 86 | let params = InstallParams { force: args.force }; |
| 78 | cwd: &cwd, | ||
| 79 | home: &globals.base_path_or_default(), | ||
| 80 | }; | ||
| 81 | for group in args.groups { | 87 | for group in args.groups { |
| 82 | match dotup.find_group_by_name(&group) { | 88 | dotup::install(&dotup, ¶ms, &group)?; |
| 83 | Some(group_id) => dotup.install(install_params, group_id)?, | ||
| 84 | None => log::error!("group not found: {}", group), | ||
| 85 | }; | ||
| 86 | } | 89 | } |
| 87 | Ok(()) | 90 | Ok(()) |
| 88 | } | 91 | } |
| 89 | 92 | ||
| 90 | fn command_uninstall(globals: GlobalFlags, args: UninstallArgs) -> anyhow::Result<()> { | 93 | fn command_uninstall(globals: GlobalFlags, args: UninstallArgs) -> anyhow::Result<()> { |
| 91 | let dotup = dotup::load_file(&globals.config).context("failed to parse config")?; | 94 | let context = helper_new_context(&globals)?; |
| 92 | let cwd = std::env::current_dir().context("failed to get current directory")?; | 95 | let dotup = dotup::load_file(context, &globals.config).context("failed to parse config")?; |
| 93 | let uninstall_params = dotup::UninstallParams { | ||
| 94 | cwd: &cwd, | ||
| 95 | home: &globals.base_path_or_default(), | ||
| 96 | }; | ||
| 97 | for group in args.groups { | 96 | for group in args.groups { |
| 98 | match dotup.find_group_by_name(&group) { | 97 | dotup::uninstall(&dotup, &group)?; |
| 99 | Some(group_id) => dotup.uninstall(uninstall_params, group_id)?, | ||
| 100 | None => log::error!("group not found: {}", group), | ||
| 101 | }; | ||
| 102 | } | 98 | } |
| 103 | Ok(()) | 99 | Ok(()) |
| 104 | } | 100 | } |
| 105 | 101 | ||
| 106 | fn command_status(globals: GlobalFlags, args: StatusArgs) -> anyhow::Result<()> { | 102 | fn command_status(globals: GlobalFlags, args: StatusArgs) -> anyhow::Result<()> { |
| 107 | let dotup = dotup::load_file(&globals.config).context("failed to parse config")?; | 103 | let context = helper_new_context(&globals)?; |
| 108 | let cwd = std::env::current_dir().context("failed to get current directory")?; | 104 | let dotup = dotup::load_file(context, &globals.config).context("failed to parse config")?; |
| 109 | let install_params = dotup::InstallParams { | ||
| 110 | cwd: &cwd, | ||
| 111 | home: &globals.base_path_or_default(), | ||
| 112 | }; | ||
| 113 | for group in args.groups { | 105 | for group in args.groups { |
| 114 | match dotup.find_group_by_name(&group) { | 106 | dotup::status(&dotup, &group)?; |
| 115 | Some(group_id) => dotup.status(install_params, group_id)?, | ||
| 116 | None => log::error!("group not found: {}", group), | ||
| 117 | }; | ||
| 118 | } | 107 | } |
| 119 | Ok(()) | 108 | Ok(()) |
| 120 | } | 109 | } |
| @@ -123,3 +112,9 @@ fn command_format(globals: GlobalFlags, _args: FormatArgs) -> anyhow::Result<()> | |||
| 123 | dotup::format_file_inplace(&globals.config).context("failed to format config")?; | 112 | dotup::format_file_inplace(&globals.config).context("failed to format config")?; |
| 124 | Ok(()) | 113 | Ok(()) |
| 125 | } | 114 | } |
| 115 | |||
| 116 | fn helper_new_context(globals: &GlobalFlags) -> anyhow::Result<dotup::Context> { | ||
| 117 | let cwd = globals.get_working_dir(); | ||
| 118 | let home = globals.base_path_or_default(); | ||
| 119 | Ok(dotup::Context::new(cwd, home)?) | ||
| 120 | } | ||
