aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authordiogo464 <[email protected]>2023-04-07 20:49:26 +0100
committerdiogo464 <[email protected]>2023-04-07 20:51:32 +0100
commite7588a7f175ca9b9604ce35d72086489da7a66e3 (patch)
tree03379454044e505c65e32ccf96cdfc34441bf914 /src
parentfa3fecca00442acea029248c6cf1fa0ba81e96b9 (diff)
version bump: 0.2.00.2.0
Diffstat (limited to 'src')
-rw-r--r--src/dotup/action_tree.rs341
-rw-r--r--src/dotup/mod.rs697
-rw-r--r--src/main.rs57
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 @@
1use std::{collections::HashSet, ffi::OsString, ops::Index, path::PathBuf};
2
3use slotmap::SlotMap;
4
5use super::{AbsPath, AbsPathBuf};
6
7slotmap::new_key_type! {
8 pub struct NodeID;
9 pub struct ActionID;
10}
11
12#[derive(Debug)]
13pub enum Action {
14 Link { source: PathBuf },
15 Copy { source: PathBuf },
16}
17
18#[derive(Debug)]
19pub struct TreeAction {
20 path: AbsPathBuf,
21 action: Action,
22}
23
24#[derive(Debug)]
25enum TreeNodeKind {
26 Action(ActionID),
27 SubTree(HashSet<NodeID>),
28}
29
30#[derive(Debug)]
31struct TreeNode {
32 path: AbsPathBuf,
33 component: OsString,
34 kind: TreeNodeKind,
35}
36
37#[derive(Debug)]
38pub struct ActionTree {
39 root_id: NodeID,
40 nodes: SlotMap<NodeID, TreeNode>,
41 actions: SlotMap<ActionID, TreeAction>,
42}
43
44// -------------------- TreeAction -------------------- //
45
46impl 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)]
59impl 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
91impl 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
99impl 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)]
279mod 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 @@
1mod action_tree;
2mod cfg; 1mod cfg;
3mod paths; 2mod paths;
4 3
5use std::collections::HashSet;
6use std::{ 4use std::{
7 collections::HashMap, 5 collections::{HashMap, HashSet, VecDeque},
8 path::{Path, PathBuf}, 6 path::{Path, PathBuf},
9}; 7};
10 8
9use colored::Colorize;
11use slotmap::SlotMap; 10use slotmap::SlotMap;
12use thiserror::Error; 11use thiserror::Error;
13 12
@@ -20,37 +19,59 @@ slotmap::new_key_type! { pub struct GroupID; }
20#[derive(Debug, Error)] 19#[derive(Debug, Error)]
21pub enum Error { 20pub 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)] 29impl Error {
31pub 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)]
36pub struct Context {
37 working_directory: AbsPathBuf,
38 destination_directory: AbsPathBuf,
39}
40
41impl 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)]
39pub struct Dotup { 60pub struct InstallParams {
40 root_id: GroupID, 61 pub force: bool,
41 groups: SlotMap<GroupID, Group>,
42} 62}
43 63
44#[derive(Debug, Clone, Copy)] 64impl Default for InstallParams {
45pub 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)]
51pub struct UninstallParams<'p> { 71pub 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)]
84struct 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)]
63struct IncludeAction { 92struct IncludeAction {
64 group: String, 93 group: String,
94 group_id: GroupID,
65} 95}
66 96
67#[derive(Debug, Clone)] 97#[derive(Debug, Clone)]
68struct LinkAction { 98struct 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)]
74struct CopyAction { 105struct 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
86pub fn load(content: &str) -> Result<Dotup> { 117#[derive(Debug, Clone)]
118enum ExecutableAction {
119 Link(LinkAction),
120 Copy(CopyAction),
121}
122
123pub 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
91pub fn load_file(path: impl AsRef<Path>) -> Result<Dotup> { 128pub 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
96pub fn format(content: &str) -> Result<String> { 133pub 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 -------------------- // 149pub 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
114impl 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(()) 204pub 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) { 226pub 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
270fn 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
151impl Dotup { 288fn 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) 331fn 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(), 373fn 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); 409fn 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 { 414fn 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
418fn 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( 441fn 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
469fn 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( 477fn 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
333fn cfg_action_to_action(cfg_action: cfg::Action) -> Result<Action> { 519fn 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`. 530fn fs_metadata(path: impl AsRef<Path>) -> Result<std::fs::Metadata> {
368fn 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
541fn 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
552fn 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
563fn 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
574fn 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
583fn 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
611fn 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
629fn 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;
4pub mod dotup; 4pub mod dotup;
5 5
6use std::path::PathBuf; 6use std::path::PathBuf;
7 7
8use anyhow::Context; 8use anyhow::Context;
9use clap::{Parser, Subcommand}; 9use clap::{Parser, Subcommand};
10use dotup::InstallParams;
10 11
11#[derive(Parser, Debug)] 12#[derive(Parser, Debug)]
12struct GlobalFlags { 13struct GlobalFlags {
@@ -27,6 +28,9 @@ enum SubCommand {
27 28
28#[derive(Parser, Debug)] 29#[derive(Parser, Debug)]
29struct InstallArgs { 30struct 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 {}
47struct Args { 51struct 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
66impl GlobalFlags { 71impl 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
74fn command_install(globals: GlobalFlags, args: InstallArgs) -> anyhow::Result<()> { 83fn 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, &params, &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
90fn command_uninstall(globals: GlobalFlags, args: UninstallArgs) -> anyhow::Result<()> { 93fn 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
106fn command_status(globals: GlobalFlags, args: StatusArgs) -> anyhow::Result<()> { 102fn 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
116fn 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}