diff options
| author | diogo464 <[email protected]> | 2022-09-23 13:45:57 +0100 |
|---|---|---|
| committer | diogo464 <[email protected]> | 2022-09-23 13:45:57 +0100 |
| commit | e5a38bab67f790803ff98484fc5835adba7bf62a (patch) | |
| tree | f93bc54b128db58c3e84c0db8908dda729222dc7 /src | |
| parent | e28be1bbe34e5430333d5bb95437e75bcfaf2edb (diff) | |
rewrite
Diffstat (limited to 'src')
| -rw-r--r-- | src/config/mod.rs | 97 | ||||
| -rw-r--r-- | src/config/parse.rs | 254 | ||||
| -rw-r--r-- | src/config/parser.rs | 20 | ||||
| -rw-r--r-- | src/depot.rs | 850 | ||||
| -rw-r--r-- | src/dotup.rs | 593 | ||||
| -rw-r--r-- | src/dotup/action_tree.rs | 347 | ||||
| -rw-r--r-- | src/dotup/cfg.rs | 352 | ||||
| -rw-r--r-- | src/dotup/mod.rs | 380 | ||||
| -rw-r--r-- | src/dotup/paths.rs | 365 | ||||
| -rw-r--r-- | src/main.rs | 265 | ||||
| -rw-r--r-- | src/utils.rs | 178 |
11 files changed, 1897 insertions, 1804 deletions
diff --git a/src/config/mod.rs b/src/config/mod.rs new file mode 100644 index 0000000..98ba9fb --- /dev/null +++ b/src/config/mod.rs | |||
| @@ -0,0 +1,97 @@ | |||
| 1 | mod parse; | ||
| 2 | |||
| 3 | use std::path::Path; | ||
| 4 | |||
| 5 | pub struct Config { | ||
| 6 | groups: Vec<Group>, | ||
| 7 | } | ||
| 8 | |||
| 9 | pub struct Group { | ||
| 10 | includes: Vec<IncludeAction>, | ||
| 11 | links: Vec<LinkAction>, | ||
| 12 | copies: Vec<CopyAction>, | ||
| 13 | } | ||
| 14 | |||
| 15 | pub struct IncludeAction { | ||
| 16 | group: String, | ||
| 17 | } | ||
| 18 | |||
| 19 | pub struct LinkAction { | ||
| 20 | source: String, | ||
| 21 | target: String, | ||
| 22 | } | ||
| 23 | |||
| 24 | pub struct CopyAction { | ||
| 25 | source: String, | ||
| 26 | target: String, | ||
| 27 | } | ||
| 28 | |||
| 29 | pub fn parse(content: &str) -> std::io::Result<Config> { | ||
| 30 | todo!() | ||
| 31 | } | ||
| 32 | |||
| 33 | pub fn parse_path(path: impl AsRef<Path>) -> std::io::Result<Config> { | ||
| 34 | todo!() | ||
| 35 | } | ||
| 36 | |||
| 37 | pub fn format(content: &str) -> std::io::Result<String> { | ||
| 38 | todo!() | ||
| 39 | } | ||
| 40 | |||
| 41 | pub fn format_path(path: impl AsRef<Path>) -> std::io::Result<String> { | ||
| 42 | todo!() | ||
| 43 | } | ||
| 44 | |||
| 45 | pub fn format_inplace(path: impl AsRef<Path>) -> std::io::Result<()> { | ||
| 46 | todo!() | ||
| 47 | } | ||
| 48 | |||
| 49 | impl Config { | ||
| 50 | pub fn groups(&self) -> impl Iterator<Item = &Group> { | ||
| 51 | std::iter::empty() | ||
| 52 | } | ||
| 53 | |||
| 54 | pub fn group(&self, name: &str) -> Option<&Group> { | ||
| 55 | todo!() | ||
| 56 | } | ||
| 57 | } | ||
| 58 | |||
| 59 | impl Group { | ||
| 60 | pub fn groups(&self) -> impl Iterator<Item = &IncludeAction> { | ||
| 61 | std::iter::empty() | ||
| 62 | } | ||
| 63 | |||
| 64 | pub fn links(&self) -> impl Iterator<Item = &LinkAction> { | ||
| 65 | std::iter::empty() | ||
| 66 | } | ||
| 67 | |||
| 68 | pub fn copies(&self) -> impl Iterator<Item = &CopyAction> { | ||
| 69 | std::iter::empty() | ||
| 70 | } | ||
| 71 | } | ||
| 72 | |||
| 73 | impl IncludeAction { | ||
| 74 | pub fn name(&self) -> &str { | ||
| 75 | todo!() | ||
| 76 | } | ||
| 77 | } | ||
| 78 | |||
| 79 | impl LinkAction { | ||
| 80 | pub fn source(&self) -> &str { | ||
| 81 | todo!() | ||
| 82 | } | ||
| 83 | |||
| 84 | pub fn dest(&self) -> &str { | ||
| 85 | todo!() | ||
| 86 | } | ||
| 87 | } | ||
| 88 | |||
| 89 | impl CopyAction { | ||
| 90 | pub fn source(&self) -> &str { | ||
| 91 | todo!() | ||
| 92 | } | ||
| 93 | |||
| 94 | pub fn dest(&self) -> &str { | ||
| 95 | todo!() | ||
| 96 | } | ||
| 97 | } | ||
diff --git a/src/config/parse.rs b/src/config/parse.rs new file mode 100644 index 0000000..f1e33b0 --- /dev/null +++ b/src/config/parse.rs | |||
| @@ -0,0 +1,254 @@ | |||
| 1 | use nom::{ | ||
| 2 | branch::alt, | ||
| 3 | bytes::complete::{tag, take_while, take_while1}, | ||
| 4 | character::complete::{alphanumeric0, alphanumeric1, multispace0, space1}, | ||
| 5 | combinator::map, | ||
| 6 | multi::{many0, separated_list0}, | ||
| 7 | sequence::{delimited, preceded}, | ||
| 8 | }; | ||
| 9 | |||
| 10 | type Span<'s> = nom_locate::LocatedSpan<&'s str>; | ||
| 11 | type IResult<'s, I, O, E = ParserError<'s>> = nom::IResult<I, O, E>; | ||
| 12 | |||
| 13 | #[derive(Debug, PartialEq, Eq)] | ||
| 14 | struct ParserError<'s> { | ||
| 15 | location: Span<'s>, | ||
| 16 | message: Option<String>, | ||
| 17 | } | ||
| 18 | |||
| 19 | #[derive(Debug)] | ||
| 20 | struct KeyValueParser<'s> { | ||
| 21 | span: Span<'s>, | ||
| 22 | kvs: Vec<KeyValue<'s>>, | ||
| 23 | } | ||
| 24 | |||
| 25 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] | ||
| 26 | struct KeyValue<'s> { | ||
| 27 | key: &'s str, | ||
| 28 | value: &'s str, | ||
| 29 | } | ||
| 30 | |||
| 31 | #[derive(Debug, Clone, PartialEq, Eq)] | ||
| 32 | struct LinkAction { | ||
| 33 | source: String, | ||
| 34 | target: String, | ||
| 35 | } | ||
| 36 | |||
| 37 | enum RichAction { | ||
| 38 | Link(LinkAction), | ||
| 39 | } | ||
| 40 | |||
| 41 | struct RichGroup { | ||
| 42 | name: String, | ||
| 43 | items: Vec<RichItem>, | ||
| 44 | } | ||
| 45 | |||
| 46 | enum RichItem { | ||
| 47 | Group(RichGroup), | ||
| 48 | } | ||
| 49 | |||
| 50 | struct RichConfig {} | ||
| 51 | |||
| 52 | impl<'s> ParserError<'s> { | ||
| 53 | fn custom(location: Span<'s>, message: impl Into<String>) -> Self { | ||
| 54 | Self { | ||
| 55 | location, | ||
| 56 | message: Some(message.into()), | ||
| 57 | } | ||
| 58 | } | ||
| 59 | |||
| 60 | fn missing_key(span: Span<'s>, key: &'s str) -> Self { | ||
| 61 | Self::custom(span, format!("missing key: {key}")) | ||
| 62 | } | ||
| 63 | } | ||
| 64 | |||
| 65 | impl<'s> From<ParserError<'s>> for nom::Err<ParserError<'s>> { | ||
| 66 | fn from(e: ParserError<'s>) -> Self { | ||
| 67 | Self::Failure(e) | ||
| 68 | } | ||
| 69 | } | ||
| 70 | |||
| 71 | impl<'s> nom::error::ParseError<Span<'s>> for ParserError<'s> { | ||
| 72 | fn from_error_kind(input: Span<'s>, kind: nom::error::ErrorKind) -> Self { | ||
| 73 | Self::custom(input, format!("error kind: {kind:?}")) | ||
| 74 | } | ||
| 75 | |||
| 76 | fn append(input: Span, kind: nom::error::ErrorKind, other: Self) -> Self { | ||
| 77 | todo!() | ||
| 78 | } | ||
| 79 | |||
| 80 | fn or(self, other: Self) -> Self { | ||
| 81 | other | ||
| 82 | } | ||
| 83 | |||
| 84 | fn from_char(input: Span<'s>, c: char) -> Self { | ||
| 85 | Self::custom(input, format!("invalid character: {c}")) | ||
| 86 | } | ||
| 87 | } | ||
| 88 | |||
| 89 | impl<'s> KeyValueParser<'s> { | ||
| 90 | fn new(span: Span<'s>, kvs: Vec<KeyValue<'s>>) -> Self { | ||
| 91 | Self { span, kvs } | ||
| 92 | } | ||
| 93 | |||
| 94 | fn get(&self, key: &'static str) -> Option<&'s str> { | ||
| 95 | self.kvs.iter().find(|kv| kv.key == key).map(|kv| kv.value) | ||
| 96 | } | ||
| 97 | |||
| 98 | fn expect(&self, key: &'static str) -> Result<&'s str, ParserError<'s>> { | ||
| 99 | self.get(key) | ||
| 100 | .ok_or(ParserError::missing_key(self.span, key)) | ||
| 101 | } | ||
| 102 | } | ||
| 103 | |||
| 104 | fn is_value_char(c: char) -> bool { | ||
| 105 | c.is_alphanumeric() || c == '_' || c == '-' || c == '.' || c == '/' | ||
| 106 | } | ||
| 107 | |||
| 108 | fn whitespace0(i: Span) -> IResult<Span, Span> { | ||
| 109 | take_while(char::is_whitespace)(i) | ||
| 110 | } | ||
| 111 | |||
| 112 | fn whitespace1(i: Span) -> IResult<Span, Span> { | ||
| 113 | take_while1(char::is_whitespace)(i) | ||
| 114 | } | ||
| 115 | |||
| 116 | fn linesep(i: Span) -> IResult<Span, Span> { | ||
| 117 | take_while(|c: char| c.is_whitespace() && c != '\n')(i)?; | ||
| 118 | take_while(|c: char| c == '\n')(i)?; | ||
| 119 | take_while(char::is_whitespace)(i) | ||
| 120 | } | ||
| 121 | |||
| 122 | fn keyvalue(i: Span) -> IResult<Span, KeyValue> { | ||
| 123 | let (i, key) = alphanumeric1(i)?; | ||
| 124 | let (i, _) = tag("=")(i)?; | ||
| 125 | let (i, val) = delimited(tag("\""), take_while(is_value_char), tag("\""))(i)?; | ||
| 126 | Ok(( | ||
| 127 | i, | ||
| 128 | KeyValue { | ||
| 129 | key: key.fragment(), | ||
| 130 | value: val.fragment(), | ||
| 131 | }, | ||
| 132 | )) | ||
| 133 | } | ||
| 134 | |||
| 135 | fn keyvalues(i: Span) -> IResult<Span, Vec<KeyValue>> { | ||
| 136 | separated_list0(space1, keyvalue)(i) | ||
| 137 | } | ||
| 138 | |||
| 139 | fn link_action(i: Span) -> IResult<Span, LinkAction> { | ||
| 140 | let (i, kvs) = preceded(tag("link"), preceded(space1, keyvalues))(i)?; | ||
| 141 | eprintln!("{kvs:#?}"); | ||
| 142 | eprintln!("{i:?}"); | ||
| 143 | let kvparser = KeyValueParser::new(i, kvs); | ||
| 144 | let src = kvparser.expect("src")?.to_string(); | ||
| 145 | let dst = kvparser.expect("dst")?.to_string(); | ||
| 146 | Ok(( | ||
| 147 | i, | ||
| 148 | LinkAction { | ||
| 149 | source: src, | ||
| 150 | target: dst, | ||
| 151 | }, | ||
| 152 | )) | ||
| 153 | } | ||
| 154 | |||
| 155 | fn rich_action(i: Span) -> IResult<Span, RichAction> { | ||
| 156 | todo!() | ||
| 157 | } | ||
| 158 | |||
| 159 | fn rich_group(i: Span) -> IResult<Span, RichGroup> { | ||
| 160 | let mut header = preceded(tag("group"), preceded(multispace0, alphanumeric1)); | ||
| 161 | let mut open_bracket = delimited(multispace0, tag("{"), multispace0); | ||
| 162 | let mut close_bracket = preceded(multispace0, tag("}")); | ||
| 163 | let mut body = separated_list0(linesep, rich_item); | ||
| 164 | |||
| 165 | let (i, name) = header(i)?; | ||
| 166 | let (i, _) = open_bracket(i)?; | ||
| 167 | let (i, items) = body(i)?; | ||
| 168 | let (i, _) = close_bracket(i)?; | ||
| 169 | |||
| 170 | Ok(( | ||
| 171 | i, | ||
| 172 | RichGroup { | ||
| 173 | name: name.to_string(), | ||
| 174 | items, | ||
| 175 | }, | ||
| 176 | )) | ||
| 177 | } | ||
| 178 | |||
| 179 | fn rich_item(i: Span) -> IResult<Span, RichItem> { | ||
| 180 | alt((map(rich_group, RichItem::Group),))(i) | ||
| 181 | } | ||
| 182 | |||
| 183 | fn config(i: Span) -> IResult<Span, RichConfig> { | ||
| 184 | let (_, groups) = many0(rich_group)(i)?; | ||
| 185 | todo!() | ||
| 186 | } | ||
| 187 | |||
| 188 | #[cfg(test)] | ||
| 189 | mod tests { | ||
| 190 | use super::*; | ||
| 191 | |||
| 192 | #[test] | ||
| 193 | fn parse_keyvalue() { | ||
| 194 | let input = Span::new(r#"key="value""#); | ||
| 195 | let (rem, kv) = keyvalue(input).unwrap(); | ||
| 196 | assert!(rem.is_empty()); | ||
| 197 | assert_eq!( | ||
| 198 | kv, | ||
| 199 | KeyValue { | ||
| 200 | key: "key", | ||
| 201 | value: "value" | ||
| 202 | } | ||
| 203 | ); | ||
| 204 | } | ||
| 205 | |||
| 206 | #[test] | ||
| 207 | fn parse_keyvalues() { | ||
| 208 | let kvs = vec![ | ||
| 209 | KeyValue { | ||
| 210 | key: "key1", | ||
| 211 | value: "value1", | ||
| 212 | }, | ||
| 213 | KeyValue { | ||
| 214 | key: "key2", | ||
| 215 | value: "value2", | ||
| 216 | }, | ||
| 217 | ]; | ||
| 218 | |||
| 219 | let input = Span::new(r#"key1="value1" key2="value2""#); | ||
| 220 | let (rem, res) = keyvalues(input).unwrap(); | ||
| 221 | assert!(rem.is_empty()); | ||
| 222 | assert_eq!(res, kvs); | ||
| 223 | |||
| 224 | let kvs = vec![ | ||
| 225 | KeyValue { | ||
| 226 | key: "src", | ||
| 227 | value: "tmux/", | ||
| 228 | }, | ||
| 229 | KeyValue { | ||
| 230 | key: "dst", | ||
| 231 | value: ".config/tmux", | ||
| 232 | }, | ||
| 233 | ]; | ||
| 234 | |||
| 235 | let input = Span::new(r#"src="tmux/" dst=".config/tmux""#); | ||
| 236 | let (rem, res) = keyvalues(input).unwrap(); | ||
| 237 | assert!(rem.is_empty()); | ||
| 238 | assert_eq!(res, kvs); | ||
| 239 | } | ||
| 240 | |||
| 241 | #[test] | ||
| 242 | fn parse_link_action() { | ||
| 243 | let input = Span::new(r#"link src="tmux/" dst=".config/tmux""#); | ||
| 244 | let (rem, res) = link_action(input).unwrap(); | ||
| 245 | assert!(rem.is_empty()); | ||
| 246 | assert_eq!( | ||
| 247 | res, | ||
| 248 | LinkAction { | ||
| 249 | source: "tmux/".to_string(), | ||
| 250 | target: ".config/tmux".to_string() | ||
| 251 | } | ||
| 252 | ); | ||
| 253 | } | ||
| 254 | } | ||
diff --git a/src/config/parser.rs b/src/config/parser.rs new file mode 100644 index 0000000..102f15a --- /dev/null +++ b/src/config/parser.rs | |||
| @@ -0,0 +1,20 @@ | |||
| 1 | #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] | ||
| 2 | struct Location { | ||
| 3 | line: u32, | ||
| 4 | column: u32, | ||
| 5 | } | ||
| 6 | |||
| 7 | #[derive(Debug, Clone)] | ||
| 8 | struct Scanner<'s> { | ||
| 9 | location: Location, | ||
| 10 | content: std::str::Chars<'s>, | ||
| 11 | } | ||
| 12 | |||
| 13 | impl<'s> Scanner<'s> { | ||
| 14 | fn new(content: &'s str) -> Self { | ||
| 15 | Self { | ||
| 16 | location: Default::default(), | ||
| 17 | content: content.chars(), | ||
| 18 | } | ||
| 19 | } | ||
| 20 | } | ||
diff --git a/src/depot.rs b/src/depot.rs deleted file mode 100644 index b2d4e3c..0000000 --- a/src/depot.rs +++ /dev/null | |||
| @@ -1,850 +0,0 @@ | |||
| 1 | use anyhow::Context; | ||
| 2 | use std::{ | ||
| 3 | collections::HashSet, | ||
| 4 | ffi::{OsStr, OsString}, | ||
| 5 | ops::Index, | ||
| 6 | path::{Path, PathBuf}, | ||
| 7 | }; | ||
| 8 | use thiserror::Error; | ||
| 9 | |||
| 10 | use slotmap::{Key, SlotMap}; | ||
| 11 | |||
| 12 | //pub type Result<T, E = DepotError> = std::result::Result<T, E>; | ||
| 13 | pub use anyhow::Result; | ||
| 14 | pub use disk::{read, write}; | ||
| 15 | |||
| 16 | slotmap::new_key_type! {pub struct LinkID;} | ||
| 17 | slotmap::new_key_type! {struct NodeID;} | ||
| 18 | |||
| 19 | #[derive(Debug, Error)] | ||
| 20 | enum DepotError { | ||
| 21 | #[error("path must be relative")] | ||
| 22 | InvalidPath, | ||
| 23 | #[error("path must be relative and not empty")] | ||
| 24 | InvalidLinkPath, | ||
| 25 | } | ||
| 26 | |||
| 27 | #[derive(Debug, Clone)] | ||
| 28 | struct Node { | ||
| 29 | comp: OsString, | ||
| 30 | parent: NodeID, | ||
| 31 | kind: NodeKind, | ||
| 32 | } | ||
| 33 | |||
| 34 | #[derive(Debug, Clone)] | ||
| 35 | enum NodeKind { | ||
| 36 | Link(LinkID), | ||
| 37 | Directory(HashSet<NodeID>), | ||
| 38 | } | ||
| 39 | |||
| 40 | #[derive(Debug, Clone, PartialEq, Eq)] | ||
| 41 | enum NodeSearchResult { | ||
| 42 | Found(NodeID), | ||
| 43 | /// the closest NodeID up the the search point. | ||
| 44 | NotFound(NodeID), | ||
| 45 | } | ||
| 46 | |||
| 47 | #[derive(Debug, Clone, PartialEq, Eq)] | ||
| 48 | pub enum DirNode { | ||
| 49 | Link(LinkID), | ||
| 50 | Directory(PathBuf), | ||
| 51 | } | ||
| 52 | |||
| 53 | #[derive(Debug, Clone, PartialEq, Eq)] | ||
| 54 | pub enum SearchResult { | ||
| 55 | Found(LinkID), | ||
| 56 | Ancestor(LinkID), | ||
| 57 | NotFound, | ||
| 58 | } | ||
| 59 | |||
| 60 | #[derive(Debug, Clone)] | ||
| 61 | struct Link { | ||
| 62 | origin: PathBuf, | ||
| 63 | destination: PathBuf, | ||
| 64 | origin_id: NodeID, | ||
| 65 | } | ||
| 66 | |||
| 67 | #[derive(Debug)] | ||
| 68 | pub struct LinkView<'a> { | ||
| 69 | link_id: LinkID, | ||
| 70 | depot: &'a Depot, | ||
| 71 | } | ||
| 72 | |||
| 73 | impl<'a> LinkView<'a> { | ||
| 74 | pub fn origin(&self) -> &Path { | ||
| 75 | &self.depot.links[self.link_id].origin | ||
| 76 | } | ||
| 77 | |||
| 78 | pub fn destination(&self) -> &Path { | ||
| 79 | &self.depot.links[self.link_id].destination | ||
| 80 | } | ||
| 81 | } | ||
| 82 | |||
| 83 | #[derive(Debug, Clone)] | ||
| 84 | struct DepotTree { | ||
| 85 | root: NodeID, | ||
| 86 | nodes: SlotMap<NodeID, Node>, | ||
| 87 | } | ||
| 88 | |||
| 89 | impl Default for DepotTree { | ||
| 90 | fn default() -> Self { | ||
| 91 | let mut nodes = SlotMap::<NodeID, Node>::default(); | ||
| 92 | let root = nodes.insert(Node { | ||
| 93 | comp: Default::default(), | ||
| 94 | parent: Default::default(), | ||
| 95 | kind: NodeKind::Directory(Default::default()), | ||
| 96 | }); | ||
| 97 | Self { root, nodes } | ||
| 98 | } | ||
| 99 | } | ||
| 100 | |||
| 101 | impl Index<NodeID> for DepotTree { | ||
| 102 | type Output = Node; | ||
| 103 | |||
| 104 | fn index(&self, index: NodeID) -> &Self::Output { | ||
| 105 | self.nodes.index(index) | ||
| 106 | } | ||
| 107 | } | ||
| 108 | |||
| 109 | impl DepotTree { | ||
| 110 | /// create a node of kind [`NodeKind::Link`]. | ||
| 111 | pub fn link_create(&mut self, path: &Path, link_id: LinkID) -> Result<NodeID> { | ||
| 112 | debug_assert!(path_verify_link(path).is_ok()); | ||
| 113 | |||
| 114 | let path_search_result = self.search(path); | ||
| 115 | |||
| 116 | // handle the error cases | ||
| 117 | match path_search_result { | ||
| 118 | NodeSearchResult::Found(node_id) => { | ||
| 119 | let node = &self.nodes[node_id]; | ||
| 120 | match &node.kind { | ||
| 121 | NodeKind::Link(_) => Err(anyhow::anyhow!("link already exists")), | ||
| 122 | NodeKind::Directory(_) => { | ||
| 123 | Err(anyhow::anyhow!("path already has links under it")) | ||
| 124 | } | ||
| 125 | } | ||
| 126 | } | ||
| 127 | NodeSearchResult::NotFound(ancestor_node_id) => { | ||
| 128 | let ancestor_node = &self.nodes[ancestor_node_id]; | ||
| 129 | match &ancestor_node.kind { | ||
| 130 | NodeKind::Link(_) => Err(anyhow::anyhow!( | ||
| 131 | "an ancestor of this path is already linked" | ||
| 132 | )), | ||
| 133 | NodeKind::Directory(_) => Ok(()), | ||
| 134 | } | ||
| 135 | } | ||
| 136 | }?; | ||
| 137 | |||
| 138 | // create the node | ||
| 139 | // unwrap: this is a verfied link path, it must have atleast one component | ||
| 140 | let filename = path.file_name().unwrap(); | ||
| 141 | let parent_path = path_parent_or_empty(path); | ||
| 142 | let node_id = self.nodes.insert(Node { | ||
| 143 | comp: filename.to_owned(), | ||
| 144 | parent: Default::default(), | ||
| 145 | kind: NodeKind::Link(link_id), | ||
| 146 | }); | ||
| 147 | let parent_id = self.directory_get_or_create(parent_path, node_id); | ||
| 148 | self.nodes[node_id].parent = parent_id; | ||
| 149 | Ok(node_id) | ||
| 150 | } | ||
| 151 | |||
| 152 | pub fn link_update_id(&mut self, node_id: NodeID, link_id: LinkID) { | ||
| 153 | let node = &mut self.nodes[node_id]; | ||
| 154 | match &mut node.kind { | ||
| 155 | NodeKind::Link(lid) => *lid = link_id, | ||
| 156 | NodeKind::Directory(_) => unreachable!(), | ||
| 157 | } | ||
| 158 | } | ||
| 159 | |||
| 160 | /// attempts to moves a node of kind [`NodeKind::Link`] to `destination`. | ||
| 161 | pub fn link_move(&mut self, node_id: NodeID, destination: &Path) -> Result<()> { | ||
| 162 | let parent_id = self.nodes[node_id].parent; | ||
| 163 | let parent = &mut self.nodes[parent_id]; | ||
| 164 | |||
| 165 | // remove the node from its parent temporarily so that the search never returns this | ||
| 166 | // link and that way any link will find means an error. | ||
| 167 | // if an error does happen then we re-add this node to its parent to keep the data | ||
| 168 | // consistent. | ||
| 169 | match &mut parent.kind { | ||
| 170 | NodeKind::Link(_) => unreachable!(), | ||
| 171 | NodeKind::Directory(children) => children.remove(&node_id), | ||
| 172 | }; | ||
| 173 | |||
| 174 | let search_result = self.search(destination); | ||
| 175 | // handle the error cases | ||
| 176 | match search_result { | ||
| 177 | NodeSearchResult::Found(found_id) => { | ||
| 178 | assert!(found_id != node_id); | ||
| 179 | self.directory_add_child(parent_id, node_id); | ||
| 180 | return Err(anyhow::anyhow!("link already exists at that path")); | ||
| 181 | } | ||
| 182 | NodeSearchResult::NotFound(ancestor_id) => { | ||
| 183 | let ancestor = &self.nodes[ancestor_id]; | ||
| 184 | match &ancestor.kind { | ||
| 185 | NodeKind::Link(_) => { | ||
| 186 | self.directory_add_child(parent_id, node_id); | ||
| 187 | return Err(anyhow::anyhow!("ancestor path is already linked")); | ||
| 188 | } | ||
| 189 | NodeKind::Directory(_) => {} | ||
| 190 | } | ||
| 191 | } | ||
| 192 | }; | ||
| 193 | |||
| 194 | let destination_parent = path_parent_or_empty(destination); | ||
| 195 | let new_parent_id = self.directory_get_or_create(destination_parent, node_id); | ||
| 196 | if new_parent_id != parent_id { | ||
| 197 | self.nodes[node_id].parent = new_parent_id; | ||
| 198 | |||
| 199 | // we have to re-add and call the remove function because it could lead to the removal | ||
| 200 | // of several directories if they become empty after this remove. | ||
| 201 | self.directory_add_child(parent_id, node_id); | ||
| 202 | self.directory_remove_child(parent_id, node_id); | ||
| 203 | } | ||
| 204 | |||
| 205 | // unwrap: destination is a verified link path so it has atleast 1 component | ||
| 206 | let comp = destination.file_name().unwrap(); | ||
| 207 | let node = &mut self.nodes[node_id]; | ||
| 208 | if node.comp != comp { | ||
| 209 | node.comp = comp.to_owned(); | ||
| 210 | } | ||
| 211 | |||
| 212 | Ok(()) | ||
| 213 | } | ||
| 214 | |||
| 215 | pub fn link_search(&self, path: &Path) -> SearchResult { | ||
| 216 | match self.search(path) { | ||
| 217 | NodeSearchResult::Found(node_id) => match &self.nodes[node_id].kind { | ||
| 218 | NodeKind::Link(link_id) => SearchResult::Found(*link_id), | ||
| 219 | NodeKind::Directory(_) => SearchResult::NotFound, | ||
| 220 | }, | ||
| 221 | NodeSearchResult::NotFound(node_id) => match &self.nodes[node_id].kind { | ||
| 222 | NodeKind::Link(link_id) => SearchResult::Ancestor(*link_id), | ||
| 223 | NodeKind::Directory(_) => SearchResult::NotFound, | ||
| 224 | }, | ||
| 225 | } | ||
| 226 | } | ||
| 227 | |||
| 228 | /// remove a node of kind [`NodeKind::Link`]. | ||
| 229 | pub fn link_remove(&mut self, node_id: NodeID) { | ||
| 230 | let node = &self.nodes[node_id]; | ||
| 231 | assert!(std::matches!(node.kind, NodeKind::Link(_))); | ||
| 232 | let parent_id = node.parent; | ||
| 233 | self.nodes.remove(node_id); | ||
| 234 | self.directory_remove_child(parent_id, node_id); | ||
| 235 | } | ||
| 236 | |||
| 237 | pub fn links_under(&self, path: &Path) -> impl Iterator<Item = LinkID> + '_ { | ||
| 238 | let links = match self.search(path) { | ||
| 239 | NodeSearchResult::Found(node_id) => { | ||
| 240 | let node = &self.nodes[node_id]; | ||
| 241 | match &node.kind { | ||
| 242 | NodeKind::Link(link_id) => vec![*link_id], | ||
| 243 | NodeKind::Directory(children) => { | ||
| 244 | let mut links = Vec::new(); | ||
| 245 | let mut node_ids = Vec::from_iter(children.iter().copied()); | ||
| 246 | while let Some(child_id) = node_ids.pop() { | ||
| 247 | let child = &self.nodes[child_id]; | ||
| 248 | match &child.kind { | ||
| 249 | NodeKind::Link(link_id) => links.push(*link_id), | ||
| 250 | NodeKind::Directory(extra_children) => { | ||
| 251 | node_ids.extend(extra_children.iter().copied()) | ||
| 252 | } | ||
| 253 | } | ||
| 254 | } | ||
| 255 | links | ||
| 256 | } | ||
| 257 | } | ||
| 258 | } | ||
| 259 | NodeSearchResult::NotFound(_) => vec![], | ||
| 260 | }; | ||
| 261 | links.into_iter() | ||
| 262 | } | ||
| 263 | |||
| 264 | pub fn has_links_under(&self, path: &Path) -> bool { | ||
| 265 | // it does not matter what type of node is found. if a directory exists then there | ||
| 266 | // must be atleast one link under it. | ||
| 267 | match self.search(path) { | ||
| 268 | NodeSearchResult::Found(_) => true, | ||
| 269 | NodeSearchResult::NotFound(_) => false, | ||
| 270 | } | ||
| 271 | } | ||
| 272 | |||
| 273 | pub fn read_dir(&self, path: &Path) -> Result<impl Iterator<Item = DirNode> + '_> { | ||
| 274 | match self.search(path) { | ||
| 275 | NodeSearchResult::Found(node_id) => match &self.nodes[node_id].kind { | ||
| 276 | NodeKind::Link(_) => Err(anyhow::anyhow!("read dir called on a link")), | ||
| 277 | NodeKind::Directory(children) => Ok(children.iter().map(|child_id| { | ||
| 278 | let child = &self.nodes[*child_id]; | ||
| 279 | match &child.kind { | ||
| 280 | NodeKind::Link(link_id) => DirNode::Link(*link_id), | ||
| 281 | NodeKind::Directory(_) => DirNode::Directory(self.build_path(*child_id)), | ||
| 282 | } | ||
| 283 | })), | ||
| 284 | }, | ||
| 285 | NodeSearchResult::NotFound(_) => Err(anyhow::anyhow!("directory not found")), | ||
| 286 | } | ||
| 287 | } | ||
| 288 | |||
| 289 | pub fn build_path(&self, node_id: NodeID) -> PathBuf { | ||
| 290 | fn recursive_helper(nodes: &SlotMap<NodeID, Node>, nid: NodeID, pbuf: &mut PathBuf) { | ||
| 291 | if nid.is_null() { | ||
| 292 | return; | ||
| 293 | } | ||
| 294 | let parent_id = nodes[nid].parent; | ||
| 295 | recursive_helper(nodes, parent_id, pbuf); | ||
| 296 | pbuf.push(&nodes[nid].comp); | ||
| 297 | } | ||
| 298 | |||
| 299 | let mut node_path = PathBuf::default(); | ||
| 300 | recursive_helper(&self.nodes, node_id, &mut node_path); | ||
| 301 | node_path | ||
| 302 | } | ||
| 303 | |||
| 304 | fn search(&self, path: &Path) -> NodeSearchResult { | ||
| 305 | debug_assert!(path_verify(path).is_ok()); | ||
| 306 | |||
| 307 | let mut curr_node_id = self.root; | ||
| 308 | let mut comp_iter = path_iter_comps(path).peekable(); | ||
| 309 | while let Some(comp) = comp_iter.next() { | ||
| 310 | if let Some(child_id) = self.directory_search_children(curr_node_id, comp) { | ||
| 311 | let child = &self.nodes[child_id]; | ||
| 312 | match &child.kind { | ||
| 313 | NodeKind::Link(_) => { | ||
| 314 | if comp_iter.peek().is_some() { | ||
| 315 | return NodeSearchResult::NotFound(child_id); | ||
| 316 | } else { | ||
| 317 | return NodeSearchResult::Found(child_id); | ||
| 318 | } | ||
| 319 | } | ||
| 320 | NodeKind::Directory(_) => curr_node_id = child_id, | ||
| 321 | } | ||
| 322 | } else { | ||
| 323 | return NodeSearchResult::NotFound(curr_node_id); | ||
| 324 | } | ||
| 325 | } | ||
| 326 | NodeSearchResult::Found(curr_node_id) | ||
| 327 | } | ||
| 328 | |||
| 329 | // creates directories all the way up to and including path. | ||
| 330 | // there cannot be any links up to `path`. | ||
| 331 | fn directory_get_or_create(&mut self, path: &Path, initial_child: NodeID) -> NodeID { | ||
| 332 | // TODO: this could be replaced if the search function also returned the depth of the | ||
| 333 | // node and we skip those components and just start creating directories up to the | ||
| 334 | // path. | ||
| 335 | let mut curr_node_id = self.root; | ||
| 336 | for comp in path_iter_comps(path) { | ||
| 337 | if let Some(child_id) = self.directory_search_children(curr_node_id, comp) { | ||
| 338 | debug_assert!(std::matches!( | ||
| 339 | self.nodes[child_id].kind, | ||
| 340 | NodeKind::Directory(_) | ||
| 341 | )); | ||
| 342 | curr_node_id = child_id; | ||
| 343 | } else { | ||
| 344 | let new_node_id = self.nodes.insert(Node { | ||
| 345 | comp: comp.to_owned(), | ||
| 346 | parent: curr_node_id, | ||
| 347 | kind: NodeKind::Directory(Default::default()), | ||
| 348 | }); | ||
| 349 | self.directory_add_child(curr_node_id, new_node_id); | ||
| 350 | curr_node_id = new_node_id; | ||
| 351 | } | ||
| 352 | } | ||
| 353 | self.directory_add_child(curr_node_id, initial_child); | ||
| 354 | curr_node_id | ||
| 355 | } | ||
| 356 | |||
| 357 | fn directory_search_children(&self, node_id: NodeID, comp: &OsStr) -> Option<NodeID> { | ||
| 358 | let node = &self.nodes[node_id]; | ||
| 359 | match &node.kind { | ||
| 360 | NodeKind::Link(_) => unreachable!(), | ||
| 361 | NodeKind::Directory(children) => { | ||
| 362 | for &child_id in children { | ||
| 363 | let child = &self.nodes[child_id]; | ||
| 364 | if child.comp == comp { | ||
| 365 | return Some(child_id); | ||
| 366 | } | ||
| 367 | } | ||
| 368 | } | ||
| 369 | } | ||
| 370 | None | ||
| 371 | } | ||
| 372 | |||
| 373 | fn directory_add_child(&mut self, node_id: NodeID, child_id: NodeID) { | ||
| 374 | let node = &mut self.nodes[node_id]; | ||
| 375 | match &mut node.kind { | ||
| 376 | NodeKind::Link(_) => unreachable!(), | ||
| 377 | NodeKind::Directory(children) => children.insert(child_id), | ||
| 378 | }; | ||
| 379 | } | ||
| 380 | |||
| 381 | fn directory_remove_child(&mut self, node_id: NodeID, child_id: NodeID) { | ||
| 382 | let node = &mut self.nodes[node_id]; | ||
| 383 | match &mut node.kind { | ||
| 384 | NodeKind::Link(_) => unreachable!(), | ||
| 385 | NodeKind::Directory(children) => { | ||
| 386 | children.remove(&child_id); | ||
| 387 | if children.is_empty() && !node.parent.is_null() { | ||
| 388 | let parent_id = node.parent; | ||
| 389 | self.directory_remove_child(parent_id, node_id); | ||
| 390 | } | ||
| 391 | } | ||
| 392 | } | ||
| 393 | } | ||
| 394 | } | ||
| 395 | |||
| 396 | #[derive(Debug, Default, Clone)] | ||
| 397 | pub struct Depot { | ||
| 398 | links: SlotMap<LinkID, Link>, | ||
| 399 | origin: DepotTree, | ||
| 400 | } | ||
| 401 | |||
| 402 | impl Depot { | ||
| 403 | pub fn link_create( | ||
| 404 | &mut self, | ||
| 405 | origin: impl AsRef<Path>, | ||
| 406 | destination: impl AsRef<Path>, | ||
| 407 | ) -> Result<LinkID> { | ||
| 408 | let origin = origin.as_ref(); | ||
| 409 | let destination = destination.as_ref(); | ||
| 410 | path_verify_link(origin)?; | ||
| 411 | path_verify_link(destination)?; | ||
| 412 | self.link_create_unchecked(origin, destination) | ||
| 413 | } | ||
| 414 | |||
| 415 | pub fn link_remove(&mut self, link_id: LinkID) { | ||
| 416 | let node_id = self.links[link_id].origin_id; | ||
| 417 | self.links.remove(link_id); | ||
| 418 | self.origin.link_remove(node_id); | ||
| 419 | } | ||
| 420 | |||
| 421 | /// moves the link specified by `link_id` to the path at `destination`. | ||
| 422 | /// if the link is already at the destination nothing is done. | ||
| 423 | /// if the destination is another link that that link is removed. | ||
| 424 | /// if the destination is under another link then an error is returned. | ||
| 425 | /// `destination` will be the link's new origin. | ||
| 426 | pub fn link_move(&mut self, link_id: LinkID, destination: impl AsRef<Path>) -> Result<()> { | ||
| 427 | let destination = destination.as_ref(); | ||
| 428 | path_verify_link(destination)?; | ||
| 429 | self.link_move_unchecked(link_id, destination) | ||
| 430 | } | ||
| 431 | |||
| 432 | #[allow(unused)] | ||
| 433 | pub fn link_search(&self, path: impl AsRef<Path>) -> Result<SearchResult> { | ||
| 434 | let path = path.as_ref(); | ||
| 435 | path_verify(path)?; | ||
| 436 | Ok(self.link_search_unchecked(path)) | ||
| 437 | } | ||
| 438 | |||
| 439 | pub fn link_find(&self, path: impl AsRef<Path>) -> Result<Option<LinkID>> { | ||
| 440 | let path = path.as_ref(); | ||
| 441 | path_verify(path)?; | ||
| 442 | Ok(self.link_find_unchecked(path)) | ||
| 443 | } | ||
| 444 | |||
| 445 | pub fn links_under(&self, path: impl AsRef<Path>) -> Result<impl Iterator<Item = LinkID> + '_> { | ||
| 446 | let path = path.as_ref(); | ||
| 447 | path_verify(path)?; | ||
| 448 | Ok(self.links_under_unchecked(path)) | ||
| 449 | } | ||
| 450 | |||
| 451 | pub fn has_links_under(&self, path: impl AsRef<Path>) -> Result<bool> { | ||
| 452 | let path = path.as_ref(); | ||
| 453 | path_verify(path)?; | ||
| 454 | Ok(self.has_links_under_unchecked(path)) | ||
| 455 | } | ||
| 456 | |||
| 457 | pub fn links_verify_install(&self, link_ids: impl Iterator<Item = LinkID>) -> Result<()> { | ||
| 458 | let mut destination = DepotTree::default(); | ||
| 459 | for link_id in link_ids { | ||
| 460 | let link = &self.links[link_id]; | ||
| 461 | destination | ||
| 462 | .link_create(&link.destination, link_id) | ||
| 463 | .context("link destinations overlap")?; | ||
| 464 | } | ||
| 465 | Ok(()) | ||
| 466 | } | ||
| 467 | |||
| 468 | pub fn link_view(&self, link_id: LinkID) -> LinkView { | ||
| 469 | LinkView { | ||
| 470 | link_id, | ||
| 471 | depot: self, | ||
| 472 | } | ||
| 473 | } | ||
| 474 | |||
| 475 | pub fn read_dir(&self, path: impl AsRef<Path>) -> Result<impl Iterator<Item = DirNode> + '_> { | ||
| 476 | let path = path.as_ref(); | ||
| 477 | path_verify(path)?; | ||
| 478 | self.read_dir_unchecked(path) | ||
| 479 | } | ||
| 480 | |||
| 481 | fn link_create_unchecked(&mut self, origin: &Path, destination: &Path) -> Result<LinkID> { | ||
| 482 | let node_id = self.origin.link_create(origin, Default::default())?; | ||
| 483 | let link_id = self.links.insert(Link { | ||
| 484 | origin: origin.to_owned(), | ||
| 485 | destination: destination.to_owned(), | ||
| 486 | origin_id: node_id, | ||
| 487 | }); | ||
| 488 | self.origin.link_update_id(node_id, link_id); | ||
| 489 | Ok(link_id) | ||
| 490 | } | ||
| 491 | |||
| 492 | fn link_move_unchecked(&mut self, link_id: LinkID, destination: &Path) -> Result<()> { | ||
| 493 | let link = &self.links[link_id]; | ||
| 494 | if link.origin == destination { | ||
| 495 | return Ok(()); | ||
| 496 | } | ||
| 497 | let node_id = link.origin_id; | ||
| 498 | self.origin.link_move(node_id, destination)?; | ||
| 499 | self.links[link_id].origin = destination.to_owned(); | ||
| 500 | Ok(()) | ||
| 501 | } | ||
| 502 | |||
| 503 | fn link_search_unchecked(&self, path: &Path) -> SearchResult { | ||
| 504 | self.origin.link_search(path) | ||
| 505 | } | ||
| 506 | |||
| 507 | fn link_find_unchecked(&self, path: &Path) -> Option<LinkID> { | ||
| 508 | match self.link_search_unchecked(path) { | ||
| 509 | SearchResult::Found(link_id) => Some(link_id), | ||
| 510 | _ => None, | ||
| 511 | } | ||
| 512 | } | ||
| 513 | |||
| 514 | fn links_under_unchecked(&self, path: &Path) -> impl Iterator<Item = LinkID> + '_ { | ||
| 515 | self.origin.links_under(path) | ||
| 516 | } | ||
| 517 | |||
| 518 | fn has_links_under_unchecked(&self, path: &Path) -> bool { | ||
| 519 | self.origin.has_links_under(path) | ||
| 520 | } | ||
| 521 | |||
| 522 | fn read_dir_unchecked(&self, path: &Path) -> Result<impl Iterator<Item = DirNode> + '_> { | ||
| 523 | self.origin.read_dir(path) | ||
| 524 | } | ||
| 525 | } | ||
| 526 | |||
| 527 | /// a verified link path is a path that: | ||
| 528 | /// + is not empty | ||
| 529 | /// + is relative | ||
| 530 | /// + does not contain Prefix/RootDir/ParentDir | ||
| 531 | fn path_verify_link(path: &Path) -> Result<()> { | ||
| 532 | // make sure the path is not empty | ||
| 533 | if path.components().next().is_none() { | ||
| 534 | return Err(DepotError::InvalidLinkPath.into()); | ||
| 535 | } | ||
| 536 | path_verify(path).map_err(|_| DepotError::InvalidLinkPath.into()) | ||
| 537 | } | ||
| 538 | |||
| 539 | /// a verified path is a path that: | ||
| 540 | /// + is not empty | ||
| 541 | /// + is relative | ||
| 542 | /// + does not contain Prefix/RootDir/ParentDir | ||
| 543 | fn path_verify(path: &Path) -> Result<()> { | ||
| 544 | // make sure the path is relative | ||
| 545 | // make sure the path does not contain '.' or '..' | ||
| 546 | for component in path.components() { | ||
| 547 | match component { | ||
| 548 | std::path::Component::Prefix(_) | ||
| 549 | | std::path::Component::RootDir | ||
| 550 | | std::path::Component::CurDir | ||
| 551 | | std::path::Component::ParentDir => return Err(DepotError::InvalidPath.into()), | ||
| 552 | std::path::Component::Normal(_) => {} | ||
| 553 | } | ||
| 554 | } | ||
| 555 | Ok(()) | ||
| 556 | } | ||
| 557 | |||
| 558 | fn path_parent_or_empty(path: &Path) -> &Path { | ||
| 559 | path.parent().unwrap_or_else(|| Path::new("")) | ||
| 560 | } | ||
| 561 | |||
| 562 | /// Iterate over the components of a path. | ||
| 563 | /// # Pre | ||
| 564 | /// The path can only have "Normal" components. | ||
| 565 | fn path_iter_comps(path: &Path) -> impl Iterator<Item = &OsStr> { | ||
| 566 | debug_assert!(path_verify(path).is_ok()); | ||
| 567 | path.components().map(|component| match component { | ||
| 568 | std::path::Component::Normal(comp) => comp, | ||
| 569 | _ => unreachable!(), | ||
| 570 | }) | ||
| 571 | } | ||
| 572 | |||
| 573 | mod disk { | ||
| 574 | use std::path::{Path, PathBuf}; | ||
| 575 | |||
| 576 | use anyhow::Context; | ||
| 577 | use serde::{Deserialize, Serialize}; | ||
| 578 | |||
| 579 | use super::Depot; | ||
| 580 | |||
| 581 | #[derive(Debug, Serialize, Deserialize)] | ||
| 582 | struct DiskLink { | ||
| 583 | origin: PathBuf, | ||
| 584 | destination: PathBuf, | ||
| 585 | } | ||
| 586 | |||
| 587 | #[derive(Debug, Serialize, Deserialize)] | ||
| 588 | struct DiskLinks { | ||
| 589 | links: Vec<DiskLink>, | ||
| 590 | } | ||
| 591 | |||
| 592 | pub fn read(path: &Path) -> anyhow::Result<Depot> { | ||
| 593 | let contents = std::fs::read_to_string(path).context("Failed to read depot file")?; | ||
| 594 | let disk_links = toml::from_str::<DiskLinks>(&contents) | ||
| 595 | .context("Failed to parse depot file")? | ||
| 596 | .links; | ||
| 597 | let mut depot = Depot::default(); | ||
| 598 | for disk_link in disk_links { | ||
| 599 | depot | ||
| 600 | .link_create(disk_link.origin, disk_link.destination) | ||
| 601 | .context("Failed to build depot from file. File is in an invalid state")?; | ||
| 602 | } | ||
| 603 | Ok(depot) | ||
| 604 | } | ||
| 605 | |||
| 606 | pub fn write(path: &Path, depot: &Depot) -> anyhow::Result<()> { | ||
| 607 | let mut links = Vec::with_capacity(depot.links.len()); | ||
| 608 | for (_, link) in depot.links.iter() { | ||
| 609 | links.push(DiskLink { | ||
| 610 | origin: link.origin.clone(), | ||
| 611 | destination: link.destination.clone(), | ||
| 612 | }); | ||
| 613 | } | ||
| 614 | let contents = | ||
| 615 | toml::to_string_pretty(&DiskLinks { links }).context("Failed to serialize depot")?; | ||
| 616 | std::fs::write(path, contents).context("Failed to write depot to file")?; | ||
| 617 | Ok(()) | ||
| 618 | } | ||
| 619 | } | ||
| 620 | |||
| 621 | #[cfg(test)] | ||
| 622 | mod tests { | ||
| 623 | use super::*; | ||
| 624 | |||
| 625 | #[test] | ||
| 626 | fn test_depot_link_create() { | ||
| 627 | let mut depot = Depot::default(); | ||
| 628 | let f1 = depot.link_create("f1", "f1").unwrap(); | ||
| 629 | let f2 = depot.link_create("f2", "f2").unwrap(); | ||
| 630 | let f3 = depot.link_create("d1/f3", "d1/f3").unwrap(); | ||
| 631 | let f4 = depot.link_create("d1/d2/f4", "d1/d2/d4").unwrap(); | ||
| 632 | |||
| 633 | assert_eq!(depot.link_find("f1").unwrap(), Some(f1)); | ||
| 634 | assert_eq!(depot.link_find("f2").unwrap(), Some(f2)); | ||
| 635 | assert_eq!(depot.link_find("d1/f3").unwrap(), Some(f3)); | ||
| 636 | assert_eq!(depot.link_find("d1/d2/f4").unwrap(), Some(f4)); | ||
| 637 | |||
| 638 | depot.link_create("f2", "").unwrap_err(); | ||
| 639 | depot.link_create("", "d4").unwrap_err(); | ||
| 640 | depot.link_create("f1/f3", "f3").unwrap_err(); | ||
| 641 | } | ||
| 642 | |||
| 643 | #[test] | ||
| 644 | fn test_depot_link_remove() { | ||
| 645 | let mut depot = Depot::default(); | ||
| 646 | let f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); | ||
| 647 | let f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); | ||
| 648 | let _f3 = depot.link_create("d1/f3", "d1/f3").unwrap(); | ||
| 649 | let f4 = depot.link_create("d1/d2/f4", "d2/f4").unwrap(); | ||
| 650 | let d3 = depot.link_create("d3", "d3").unwrap(); | ||
| 651 | |||
| 652 | depot.link_remove(f2); | ||
| 653 | assert_eq!(depot.link_find("d1/f1").unwrap(), Some(f1)); | ||
| 654 | assert_eq!(depot.link_find("d1/f2").unwrap(), None); | ||
| 655 | depot.link_remove(f4); | ||
| 656 | assert_eq!(depot.link_find("d1/d2/f4").unwrap(), None); | ||
| 657 | depot.link_remove(d3); | ||
| 658 | assert_eq!(depot.link_find("d3").unwrap(), None); | ||
| 659 | } | ||
| 660 | |||
| 661 | #[test] | ||
| 662 | fn test_depot_link_move() { | ||
| 663 | let mut depot = Depot::default(); | ||
| 664 | let f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); | ||
| 665 | let _f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); | ||
| 666 | |||
| 667 | depot.link_move(f1, "").unwrap_err(); | ||
| 668 | depot.link_move(f1, "d1/f2/f1").unwrap_err(); | ||
| 669 | depot.link_move(f1, "d1/f2").unwrap_err(); | ||
| 670 | |||
| 671 | depot.link_move(f1, "f1").unwrap(); | ||
| 672 | assert_eq!(depot.link_view(f1).origin(), Path::new("f1")); | ||
| 673 | depot.link_move(f1, "f2").unwrap(); | ||
| 674 | assert_eq!(depot.link_view(f1).origin(), Path::new("f2")); | ||
| 675 | assert_eq!(depot.link_find("f2").unwrap(), Some(f1)); | ||
| 676 | } | ||
| 677 | |||
| 678 | #[test] | ||
| 679 | fn test_depot_link_search() { | ||
| 680 | let mut depot = Depot::default(); | ||
| 681 | let f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); | ||
| 682 | let _f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); | ||
| 683 | let _f3 = depot.link_create("d1/f3", "d1/f3").unwrap(); | ||
| 684 | let _f4 = depot.link_create("d1/d2/f4", "d2/f4").unwrap(); | ||
| 685 | let _d3 = depot.link_create("d3", "d3").unwrap(); | ||
| 686 | |||
| 687 | assert_eq!(depot.link_search("d1/f1").unwrap(), SearchResult::Found(f1)); | ||
| 688 | assert_eq!( | ||
| 689 | depot.link_search("d1/f1/f5").unwrap(), | ||
| 690 | SearchResult::Ancestor(f1) | ||
| 691 | ); | ||
| 692 | assert_eq!(depot.link_search("d1").unwrap(), SearchResult::NotFound); | ||
| 693 | assert_eq!( | ||
| 694 | depot.link_search("d1/d2/f5").unwrap(), | ||
| 695 | SearchResult::NotFound | ||
| 696 | ); | ||
| 697 | } | ||
| 698 | |||
| 699 | #[test] | ||
| 700 | fn test_depot_link_find() { | ||
| 701 | let mut depot = Depot::default(); | ||
| 702 | let f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); | ||
| 703 | let _f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); | ||
| 704 | let f3 = depot.link_create("d1/f3", "d1/f3").unwrap(); | ||
| 705 | let f4 = depot.link_create("d1/d2/f4", "d2/f4").unwrap(); | ||
| 706 | let d3 = depot.link_create("d3", "d3").unwrap(); | ||
| 707 | |||
| 708 | assert_eq!(depot.link_find("d1/f1").unwrap(), Some(f1)); | ||
| 709 | assert_eq!(depot.link_find("d1/f3").unwrap(), Some(f3)); | ||
| 710 | assert_eq!(depot.link_find("d1/d2/f4").unwrap(), Some(f4)); | ||
| 711 | assert_eq!(depot.link_find("d3").unwrap(), Some(d3)); | ||
| 712 | |||
| 713 | assert_eq!(depot.link_find("d5").unwrap(), None); | ||
| 714 | assert_eq!(depot.link_find("d3/d5").unwrap(), None); | ||
| 715 | assert_eq!(depot.link_find("d1/d2/f5").unwrap(), None); | ||
| 716 | } | ||
| 717 | |||
| 718 | #[test] | ||
| 719 | fn test_depot_links_under() { | ||
| 720 | let mut depot = Depot::default(); | ||
| 721 | let f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); | ||
| 722 | let f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); | ||
| 723 | let f3 = depot.link_create("d1/f3", "d1/f3").unwrap(); | ||
| 724 | let f4 = depot.link_create("d1/d2/f4", "d2/f4").unwrap(); | ||
| 725 | let d3 = depot.link_create("d3", "d3").unwrap(); | ||
| 726 | |||
| 727 | let under_f1 = depot.links_under("d1/f1").unwrap().collect::<Vec<_>>(); | ||
| 728 | assert_eq!(under_f1, vec![f1]); | ||
| 729 | |||
| 730 | let under_d1 = depot.links_under("d1").unwrap().collect::<Vec<_>>(); | ||
| 731 | let expected_under_d1 = vec![f1, f2, f3, f4]; | ||
| 732 | assert!( | ||
| 733 | under_d1.len() == expected_under_d1.len() | ||
| 734 | && expected_under_d1.iter().all(|x| under_d1.contains(x)) | ||
| 735 | ); | ||
| 736 | |||
| 737 | let under_d2 = depot.links_under("d2").unwrap().collect::<Vec<_>>(); | ||
| 738 | assert_eq!(under_d2, vec![]); | ||
| 739 | |||
| 740 | let under_d3 = depot.links_under("d3").unwrap().collect::<Vec<_>>(); | ||
| 741 | assert_eq!(under_d3, vec![d3]); | ||
| 742 | |||
| 743 | let under_root = depot.links_under("").unwrap().collect::<Vec<_>>(); | ||
| 744 | let expected_under_root = vec![f1, f2, f3, f4, d3]; | ||
| 745 | assert!( | ||
| 746 | under_root.len() == expected_under_root.len() | ||
| 747 | && expected_under_root.iter().all(|x| under_root.contains(x)) | ||
| 748 | ); | ||
| 749 | } | ||
| 750 | |||
| 751 | #[test] | ||
| 752 | fn test_depot_has_links_under() { | ||
| 753 | let mut depot = Depot::default(); | ||
| 754 | let _f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); | ||
| 755 | let _f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); | ||
| 756 | let _f3 = depot.link_create("d1/f3", "d1/f3").unwrap(); | ||
| 757 | let _f4 = depot.link_create("d1/d2/f4", "d2/f4").unwrap(); | ||
| 758 | let _d3 = depot.link_create("d3", "d3").unwrap(); | ||
| 759 | |||
| 760 | assert!(depot.has_links_under("").unwrap()); | ||
| 761 | assert!(depot.has_links_under("d1").unwrap()); | ||
| 762 | assert!(depot.has_links_under("d3").unwrap()); | ||
| 763 | assert!(depot.has_links_under("d1/f1").unwrap()); | ||
| 764 | assert!(depot.has_links_under("d1/d2").unwrap()); | ||
| 765 | assert!(depot.has_links_under("d1/d2/f4").unwrap()); | ||
| 766 | |||
| 767 | assert!(!depot.has_links_under("d2").unwrap()); | ||
| 768 | assert!(!depot.has_links_under("d4").unwrap()); | ||
| 769 | assert!(!depot.has_links_under("d1/d2/f4/f5").unwrap()); | ||
| 770 | } | ||
| 771 | |||
| 772 | #[test] | ||
| 773 | fn test_depot_links_verify_install() { | ||
| 774 | let mut depot = Depot::default(); | ||
| 775 | let f1 = depot.link_create("nvim", ".config/nvim").unwrap(); | ||
| 776 | let f2 = depot.link_create("alacritty", ".config/alacritty").unwrap(); | ||
| 777 | let f3 = depot.link_create("bash/.bashrc", ".bashrc").unwrap(); | ||
| 778 | let f4 = depot.link_create("bash_laptop/.bashrc", ".bashrc").unwrap(); | ||
| 779 | |||
| 780 | depot | ||
| 781 | .links_verify_install(vec![f1, f2, f3].into_iter()) | ||
| 782 | .unwrap(); | ||
| 783 | depot | ||
| 784 | .links_verify_install(vec![f1, f2, f3, f4].into_iter()) | ||
| 785 | .unwrap_err(); | ||
| 786 | } | ||
| 787 | |||
| 788 | #[test] | ||
| 789 | fn test_depot_read_dir() { | ||
| 790 | let mut depot = Depot::default(); | ||
| 791 | let f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); | ||
| 792 | let f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); | ||
| 793 | let f3 = depot.link_create("d1/f3", "d1/f3").unwrap(); | ||
| 794 | let _f4 = depot.link_create("d1/d2/f4", "d2/f4").unwrap(); | ||
| 795 | let _d3 = depot.link_create("d3", "d3").unwrap(); | ||
| 796 | |||
| 797 | let read_dir = depot.read_dir("d1").unwrap().collect::<Vec<_>>(); | ||
| 798 | let expected_read_dir = vec![ | ||
| 799 | DirNode::Link(f1), | ||
| 800 | DirNode::Link(f2), | ||
| 801 | DirNode::Link(f3), | ||
| 802 | DirNode::Directory(PathBuf::from("d1/d2")), | ||
| 803 | ]; | ||
| 804 | assert!( | ||
| 805 | read_dir.len() == expected_read_dir.len() | ||
| 806 | && expected_read_dir.iter().all(|x| read_dir.contains(x)) | ||
| 807 | ); | ||
| 808 | } | ||
| 809 | |||
| 810 | #[test] | ||
| 811 | fn test_path_verify() { | ||
| 812 | path_verify(Path::new("")).unwrap(); | ||
| 813 | path_verify(Path::new("f1")).unwrap(); | ||
| 814 | path_verify(Path::new("d1/f1")).unwrap(); | ||
| 815 | path_verify(Path::new("d1/f1.txt")).unwrap(); | ||
| 816 | path_verify(Path::new("d1/./f1.txt")).unwrap(); | ||
| 817 | |||
| 818 | path_verify(Path::new("/")).unwrap_err(); | ||
| 819 | path_verify(Path::new("./f1")).unwrap_err(); | ||
| 820 | path_verify(Path::new("/d1/f1")).unwrap_err(); | ||
| 821 | path_verify(Path::new("d1/../f1.txt")).unwrap_err(); | ||
| 822 | path_verify(Path::new("/d1/../f1.txt")).unwrap_err(); | ||
| 823 | } | ||
| 824 | |||
| 825 | #[test] | ||
| 826 | fn test_path_verify_link() { | ||
| 827 | path_verify_link(Path::new("f1")).unwrap(); | ||
| 828 | path_verify_link(Path::new("d1/f1")).unwrap(); | ||
| 829 | path_verify_link(Path::new("d1/f1.txt")).unwrap(); | ||
| 830 | path_verify_link(Path::new("d1/./f1.txt")).unwrap(); | ||
| 831 | |||
| 832 | path_verify_link(Path::new("")).unwrap_err(); | ||
| 833 | path_verify_link(Path::new("/")).unwrap_err(); | ||
| 834 | path_verify_link(Path::new("./f1")).unwrap_err(); | ||
| 835 | path_verify_link(Path::new("/d1/f1")).unwrap_err(); | ||
| 836 | path_verify_link(Path::new("d1/../f1.txt")).unwrap_err(); | ||
| 837 | path_verify_link(Path::new("/d1/../f1.txt")).unwrap_err(); | ||
| 838 | } | ||
| 839 | |||
| 840 | #[test] | ||
| 841 | fn test_path_iter_comps() { | ||
| 842 | let path = Path::new("comp1/comp2/./comp3/file.txt"); | ||
| 843 | let mut iter = path_iter_comps(path); | ||
| 844 | assert_eq!(iter.next(), Some(OsStr::new("comp1"))); | ||
| 845 | assert_eq!(iter.next(), Some(OsStr::new("comp2"))); | ||
| 846 | assert_eq!(iter.next(), Some(OsStr::new("comp3"))); | ||
| 847 | assert_eq!(iter.next(), Some(OsStr::new("file.txt"))); | ||
| 848 | assert_eq!(iter.next(), None); | ||
| 849 | } | ||
| 850 | } | ||
diff --git a/src/dotup.rs b/src/dotup.rs deleted file mode 100644 index 8de7920..0000000 --- a/src/dotup.rs +++ /dev/null | |||
| @@ -1,593 +0,0 @@ | |||
| 1 | use std::{ | ||
| 2 | cmp::Ordering, | ||
| 3 | collections::HashSet, | ||
| 4 | path::{Path, PathBuf}, | ||
| 5 | }; | ||
| 6 | |||
| 7 | use ansi_term::Color; | ||
| 8 | use anyhow::Context; | ||
| 9 | |||
| 10 | use crate::{ | ||
| 11 | depot::{self, Depot, DirNode, LinkID}, | ||
| 12 | utils, | ||
| 13 | }; | ||
| 14 | |||
| 15 | #[derive(Debug)] | ||
| 16 | struct CanonicalPair { | ||
| 17 | origin: PathBuf, | ||
| 18 | destination: PathBuf, | ||
| 19 | } | ||
| 20 | |||
| 21 | #[derive(Debug, Clone)] | ||
| 22 | enum StatusItem { | ||
| 23 | Link { | ||
| 24 | origin: PathBuf, | ||
| 25 | destination: PathBuf, | ||
| 26 | is_directory: bool, | ||
| 27 | }, | ||
| 28 | Directory { | ||
| 29 | origin: PathBuf, | ||
| 30 | items: Vec<StatusItem>, | ||
| 31 | }, | ||
| 32 | Unlinked { | ||
| 33 | origin: PathBuf, | ||
| 34 | is_directory: bool, | ||
| 35 | }, | ||
| 36 | } | ||
| 37 | |||
| 38 | impl StatusItem { | ||
| 39 | fn display_ord_cmp(&self, other: &Self) -> Ordering { | ||
| 40 | match (self, other) { | ||
| 41 | ( | ||
| 42 | StatusItem::Link { | ||
| 43 | origin: l_origin, .. | ||
| 44 | }, | ||
| 45 | StatusItem::Link { | ||
| 46 | origin: r_origin, .. | ||
| 47 | }, | ||
| 48 | ) => l_origin.cmp(r_origin), | ||
| 49 | (StatusItem::Link { .. }, StatusItem::Directory { .. }) => Ordering::Less, | ||
| 50 | ( | ||
| 51 | StatusItem::Link { | ||
| 52 | is_directory: l_is_dir, | ||
| 53 | .. | ||
| 54 | }, | ||
| 55 | StatusItem::Unlinked { | ||
| 56 | is_directory: u_is_dir, | ||
| 57 | .. | ||
| 58 | }, | ||
| 59 | ) => { | ||
| 60 | if *u_is_dir && !*l_is_dir { | ||
| 61 | Ordering::Less | ||
| 62 | } else { | ||
| 63 | Ordering::Greater | ||
| 64 | } | ||
| 65 | } | ||
| 66 | (StatusItem::Directory { .. }, StatusItem::Link { .. }) => Ordering::Greater, | ||
| 67 | ( | ||
| 68 | StatusItem::Directory { | ||
| 69 | origin: l_origin, .. | ||
| 70 | }, | ||
| 71 | StatusItem::Directory { | ||
| 72 | origin: r_origin, .. | ||
| 73 | }, | ||
| 74 | ) => l_origin.cmp(r_origin), | ||
| 75 | (StatusItem::Directory { .. }, StatusItem::Unlinked { .. }) => Ordering::Greater, | ||
| 76 | ( | ||
| 77 | StatusItem::Unlinked { | ||
| 78 | is_directory: u_is_dir, | ||
| 79 | .. | ||
| 80 | }, | ||
| 81 | StatusItem::Link { | ||
| 82 | is_directory: l_is_dir, | ||
| 83 | .. | ||
| 84 | }, | ||
| 85 | ) => { | ||
| 86 | if *u_is_dir && !*l_is_dir { | ||
| 87 | Ordering::Greater | ||
| 88 | } else { | ||
| 89 | Ordering::Less | ||
| 90 | } | ||
| 91 | } | ||
| 92 | (StatusItem::Unlinked { .. }, StatusItem::Directory { .. }) => Ordering::Less, | ||
| 93 | ( | ||
| 94 | StatusItem::Unlinked { | ||
| 95 | origin: l_origin, .. | ||
| 96 | }, | ||
| 97 | StatusItem::Unlinked { | ||
| 98 | origin: r_origin, .. | ||
| 99 | }, | ||
| 100 | ) => l_origin.cmp(r_origin), | ||
| 101 | } | ||
| 102 | } | ||
| 103 | } | ||
| 104 | |||
| 105 | #[derive(Debug)] | ||
| 106 | pub struct Dotup { | ||
| 107 | depot: Depot, | ||
| 108 | depot_dir: PathBuf, | ||
| 109 | depot_path: PathBuf, | ||
| 110 | install_base: PathBuf, | ||
| 111 | } | ||
| 112 | |||
| 113 | impl Dotup { | ||
| 114 | fn new(depot: Depot, depot_path: PathBuf, install_base: PathBuf) -> anyhow::Result<Self> { | ||
| 115 | assert!(depot_path.is_absolute()); | ||
| 116 | assert!(depot_path.is_file()); | ||
| 117 | assert!(install_base.is_absolute()); | ||
| 118 | assert!(install_base.is_dir()); | ||
| 119 | let depot_dir = { | ||
| 120 | let mut d = depot_path.clone(); | ||
| 121 | d.pop(); | ||
| 122 | d | ||
| 123 | }; | ||
| 124 | Ok(Self { | ||
| 125 | depot, | ||
| 126 | depot_dir, | ||
| 127 | depot_path, | ||
| 128 | install_base, | ||
| 129 | }) | ||
| 130 | } | ||
| 131 | |||
| 132 | pub fn link(&mut self, origin: impl AsRef<Path>, destination: impl AsRef<Path>) { | ||
| 133 | let link_result: anyhow::Result<()> = try { | ||
| 134 | let origin = self.prepare_relative_origin(origin.as_ref())?; | ||
| 135 | let destination_ends_with_slash = utils::path_ends_with_slash(destination.as_ref()); | ||
| 136 | let mut destination = self.prepare_relative_destination(destination.as_ref())?; | ||
| 137 | if destination_ends_with_slash { | ||
| 138 | if let Some(filename) = origin.file_name() { | ||
| 139 | destination.push(filename); | ||
| 140 | } | ||
| 141 | } | ||
| 142 | self.depot.link_create(origin, destination)?; | ||
| 143 | }; | ||
| 144 | match link_result { | ||
| 145 | Ok(_) => {} | ||
| 146 | Err(e) => println!("Failed to create link : {e}"), | ||
| 147 | } | ||
| 148 | } | ||
| 149 | |||
| 150 | pub fn unlink(&mut self, paths: impl Iterator<Item = impl AsRef<Path>>, uninstall: bool) { | ||
| 151 | for origin in paths { | ||
| 152 | let unlink_result: anyhow::Result<()> = try { | ||
| 153 | let origin = self.prepare_relative_origin(origin.as_ref())?; | ||
| 154 | let links_under: Vec<_> = self.depot.links_under(&origin)?.collect(); | ||
| 155 | for link_id in links_under { | ||
| 156 | if uninstall && self.symlink_is_installed_by_link_id(link_id)? { | ||
| 157 | self.symlink_uninstall_by_link_id(link_id)?; | ||
| 158 | } | ||
| 159 | self.depot.link_remove(link_id); | ||
| 160 | } | ||
| 161 | }; | ||
| 162 | match unlink_result { | ||
| 163 | Ok(_) => {} | ||
| 164 | Err(e) => println!("Failed to unlink {} : {e}", origin.as_ref().display()), | ||
| 165 | } | ||
| 166 | } | ||
| 167 | } | ||
| 168 | |||
| 169 | pub fn install(&self, paths: impl Iterator<Item = impl AsRef<Path>>) { | ||
| 170 | let install_result: anyhow::Result<()> = try { | ||
| 171 | let link_ids = self.link_ids_from_paths_iter(paths)?; | ||
| 172 | self.depot.links_verify_install(link_ids.iter().copied())?; | ||
| 173 | |||
| 174 | for link_id in link_ids { | ||
| 175 | self.symlink_install_by_link_id(link_id)?; | ||
| 176 | } | ||
| 177 | }; | ||
| 178 | if let Err(e) = install_result { | ||
| 179 | println!("error while installing : {e}"); | ||
| 180 | } | ||
| 181 | } | ||
| 182 | |||
| 183 | pub fn uninstall(&self, paths: impl Iterator<Item = impl AsRef<Path>>) { | ||
| 184 | let uninstall_result: anyhow::Result<()> = try { | ||
| 185 | let link_ids = self.link_ids_from_paths_iter(paths)?; | ||
| 186 | for link_id in link_ids { | ||
| 187 | if self.symlink_is_installed_by_link_id(link_id)? { | ||
| 188 | self.symlink_uninstall_by_link_id(link_id)?; | ||
| 189 | } | ||
| 190 | } | ||
| 191 | }; | ||
| 192 | if let Err(e) = uninstall_result { | ||
| 193 | println!("error while uninstalling {e}",); | ||
| 194 | } | ||
| 195 | } | ||
| 196 | |||
| 197 | pub fn mv( | ||
| 198 | &mut self, | ||
| 199 | origins: impl Iterator<Item = impl AsRef<Path>>, | ||
| 200 | destination: impl AsRef<Path>, | ||
| 201 | ) { | ||
| 202 | let mv_result: anyhow::Result<()> = try { | ||
| 203 | let origins = { | ||
| 204 | let mut v = Vec::new(); | ||
| 205 | for origin in origins { | ||
| 206 | v.push( | ||
| 207 | origin | ||
| 208 | .as_ref() | ||
| 209 | .canonicalize() | ||
| 210 | .context("failed to canonicalize origin path")?, | ||
| 211 | ); | ||
| 212 | } | ||
| 213 | v | ||
| 214 | }; | ||
| 215 | let destination = utils::weakly_canonical(destination.as_ref()); | ||
| 216 | log::debug!("mv destination : {}", destination.display()); | ||
| 217 | |||
| 218 | // if we are moving multiple links then the destination must be a directory | ||
| 219 | if origins.len() > 1 && !destination.is_dir() { | ||
| 220 | println!("destination must be a directory"); | ||
| 221 | return; | ||
| 222 | } | ||
| 223 | |||
| 224 | for origin in origins { | ||
| 225 | let destination = if destination.is_dir() { | ||
| 226 | // unwrap: origin must have a filename | ||
| 227 | destination.join(origin.file_name().unwrap()) | ||
| 228 | } else { | ||
| 229 | destination.to_owned() | ||
| 230 | }; | ||
| 231 | self.mv_one(&origin, &destination)?; | ||
| 232 | } | ||
| 233 | }; | ||
| 234 | if let Err(e) = mv_result { | ||
| 235 | println!("error moving : {e}"); | ||
| 236 | } | ||
| 237 | } | ||
| 238 | |||
| 239 | fn mv_one(&mut self, origin: &Path, destination: &Path) -> anyhow::Result<()> { | ||
| 240 | log::debug!("mv_one : {} to {}", origin.display(), destination.display()); | ||
| 241 | |||
| 242 | let relative_origin = self.prepare_relative_origin(origin)?; | ||
| 243 | let relative_destination = self.prepare_relative_origin(destination)?; | ||
| 244 | match self.depot.link_find(&relative_origin)? { | ||
| 245 | Some(link_id) => { | ||
| 246 | let is_installed = self.symlink_is_installed_by_link_id(link_id)?; | ||
| 247 | let original_origin = self.depot.link_view(link_id).origin().to_owned(); | ||
| 248 | log::debug!("is_installed = {is_installed}",); | ||
| 249 | log::debug!("original_origin = {}", original_origin.display()); | ||
| 250 | log::debug!("link_destination = {}", relative_destination.display()); | ||
| 251 | |||
| 252 | self.depot.link_move(link_id, relative_destination)?; | ||
| 253 | if let Err(e) = std::fs::rename(origin, destination).context("Failed to move file") | ||
| 254 | { | ||
| 255 | // unwrap: moving the link back to its origin place has to work | ||
| 256 | self.depot.link_move(link_id, original_origin).unwrap(); | ||
| 257 | return Err(e); | ||
| 258 | } | ||
| 259 | // reinstall because we just moved the origin | ||
| 260 | if is_installed { | ||
| 261 | self.symlink_install_by_link_id(link_id) | ||
| 262 | .context("failed to reinstall link while moving")?; | ||
| 263 | } | ||
| 264 | } | ||
| 265 | None => { | ||
| 266 | if origin.is_dir() { | ||
| 267 | let mut links_installed: HashSet<_> = Default::default(); | ||
| 268 | if self.depot.has_links_under(&relative_origin)? { | ||
| 269 | let links_under: Vec<_> = | ||
| 270 | self.depot.links_under(&relative_origin)?.collect(); | ||
| 271 | for &link_id in links_under.iter() { | ||
| 272 | let link_view = self.depot.link_view(link_id); | ||
| 273 | if self.symlink_is_installed_by_link_id(link_id)? { | ||
| 274 | links_installed.insert(link_id); | ||
| 275 | } | ||
| 276 | // unwrap: the link is under `origin` so stripping the prefix should | ||
| 277 | // not fail | ||
| 278 | let origin_extra = | ||
| 279 | link_view.origin().strip_prefix(&relative_origin).unwrap(); | ||
| 280 | let new_destination = relative_destination.join(origin_extra); | ||
| 281 | self.depot.link_move(link_id, new_destination)?; | ||
| 282 | } | ||
| 283 | } | ||
| 284 | std::fs::rename(origin, destination)?; | ||
| 285 | for link_id in links_installed { | ||
| 286 | self.symlink_install_by_link_id(link_id)?; | ||
| 287 | } | ||
| 288 | } else { | ||
| 289 | std::fs::rename(origin, destination)?; | ||
| 290 | } | ||
| 291 | } | ||
| 292 | } | ||
| 293 | Ok(()) | ||
| 294 | } | ||
| 295 | |||
| 296 | pub fn status(&self, paths: impl Iterator<Item = impl AsRef<Path>>) { | ||
| 297 | let status_result: anyhow::Result<()> = try { | ||
| 298 | // canonicalize and remove paths whose parent we already have | ||
| 299 | let paths = paths.map(utils::weakly_canonical).collect::<HashSet<_>>(); | ||
| 300 | let paths = paths | ||
| 301 | .iter() | ||
| 302 | .filter(|p| !paths.iter().any(|x| p.starts_with(x) && p != &x)); | ||
| 303 | |||
| 304 | for path in paths { | ||
| 305 | let item = self.status_path_to_item(path)?; | ||
| 306 | self.status_print_item(item, 0)?; | ||
| 307 | } | ||
| 308 | }; | ||
| 309 | if let Err(e) = status_result { | ||
| 310 | println!("error while displaying status : {e}"); | ||
| 311 | } | ||
| 312 | } | ||
| 313 | fn status_path_to_item(&self, canonical_path: &Path) -> anyhow::Result<StatusItem> { | ||
| 314 | debug_assert!(canonical_path.is_absolute()); | ||
| 315 | debug_assert!(canonical_path.exists()); | ||
| 316 | let relative_path = self.prepare_relative_origin(canonical_path)?; | ||
| 317 | |||
| 318 | let item = if canonical_path.is_dir() { | ||
| 319 | if let Some(link_id) = self.depot.link_find(&relative_path)? { | ||
| 320 | let destination = self.depot.link_view(link_id).destination().to_owned(); | ||
| 321 | StatusItem::Link { | ||
| 322 | origin: relative_path, | ||
| 323 | destination, | ||
| 324 | is_directory: true, | ||
| 325 | } | ||
| 326 | } else if self.depot.has_links_under(&relative_path)? { | ||
| 327 | let mut items = Vec::new(); | ||
| 328 | let mut collected_rel_paths = HashSet::<PathBuf>::new(); | ||
| 329 | let directory_paths = utils::collect_paths_in_dir(&canonical_path)?; | ||
| 330 | for canonical_item_path in directory_paths { | ||
| 331 | let item = self.status_path_to_item(&canonical_item_path)?; | ||
| 332 | match &item { | ||
| 333 | StatusItem::Link { origin, .. } | StatusItem::Directory { origin, .. } => { | ||
| 334 | collected_rel_paths.insert(origin.to_owned()); | ||
| 335 | } | ||
| 336 | _ => {} | ||
| 337 | } | ||
| 338 | items.push(item); | ||
| 339 | } | ||
| 340 | |||
| 341 | for dir_node in self.depot.read_dir(&relative_path)? { | ||
| 342 | match dir_node { | ||
| 343 | DirNode::Link(link_id) => { | ||
| 344 | let link_view = self.depot.link_view(link_id); | ||
| 345 | let link_rel_path = link_view.origin(); | ||
| 346 | let link_rel_dest = link_view.destination(); | ||
| 347 | if !collected_rel_paths.contains(link_rel_path) { | ||
| 348 | items.push(StatusItem::Link { | ||
| 349 | origin: link_rel_path.to_owned(), | ||
| 350 | destination: link_rel_dest.to_owned(), | ||
| 351 | is_directory: false, | ||
| 352 | }); | ||
| 353 | } | ||
| 354 | } | ||
| 355 | DirNode::Directory(_) => {} | ||
| 356 | } | ||
| 357 | } | ||
| 358 | |||
| 359 | StatusItem::Directory { | ||
| 360 | origin: relative_path, | ||
| 361 | items, | ||
| 362 | } | ||
| 363 | } else { | ||
| 364 | StatusItem::Unlinked { | ||
| 365 | origin: relative_path, | ||
| 366 | is_directory: true, | ||
| 367 | } | ||
| 368 | } | ||
| 369 | } else if let Some(link_id) = self.depot.link_find(&relative_path)? { | ||
| 370 | let destination = self.depot.link_view(link_id).destination().to_owned(); | ||
| 371 | StatusItem::Link { | ||
| 372 | origin: relative_path, | ||
| 373 | destination, | ||
| 374 | is_directory: false, | ||
| 375 | } | ||
| 376 | } else { | ||
| 377 | StatusItem::Unlinked { | ||
| 378 | origin: relative_path, | ||
| 379 | is_directory: false, | ||
| 380 | } | ||
| 381 | }; | ||
| 382 | Ok(item) | ||
| 383 | } | ||
| 384 | fn status_print_item(&self, item: StatusItem, depth: u32) -> anyhow::Result<()> { | ||
| 385 | fn print_depth(d: u32) { | ||
| 386 | for _ in 0..d.saturating_sub(1) { | ||
| 387 | print!(" "); | ||
| 388 | } | ||
| 389 | } | ||
| 390 | fn origin_color(exists: bool, is_installed: bool) -> Color { | ||
| 391 | if !exists { | ||
| 392 | Color::Red | ||
| 393 | } else if is_installed { | ||
| 394 | Color::Green | ||
| 395 | } else { | ||
| 396 | Color::RGB(255, 127, 0) | ||
| 397 | } | ||
| 398 | } | ||
| 399 | |||
| 400 | let destination_color = Color::Blue; | ||
| 401 | |||
| 402 | print_depth(depth); | ||
| 403 | match item { | ||
| 404 | StatusItem::Link { | ||
| 405 | origin, | ||
| 406 | destination, | ||
| 407 | is_directory, | ||
| 408 | } => { | ||
| 409 | let canonical_origin = self.depot_dir.join(&origin); | ||
| 410 | let canonical_destination = self.install_base.join(&destination); | ||
| 411 | let file_name = Self::status_get_filename(&canonical_origin); | ||
| 412 | let is_installed = | ||
| 413 | self.symlink_is_installed(&canonical_origin, &canonical_destination)?; | ||
| 414 | let exists = canonical_origin.exists(); | ||
| 415 | let origin_color = origin_color(exists, is_installed); | ||
| 416 | let directory_extra = if is_directory { "/" } else { "" }; | ||
| 417 | println!( | ||
| 418 | "{}{} -> {}", | ||
| 419 | origin_color.paint(file_name), | ||
| 420 | directory_extra, | ||
| 421 | destination_color.paint(destination.display().to_string()) | ||
| 422 | ); | ||
| 423 | } | ||
| 424 | StatusItem::Directory { origin, mut items } => { | ||
| 425 | items.sort_by(|a, b| StatusItem::display_ord_cmp(a, b).reverse()); | ||
| 426 | let directory_name = Self::status_get_filename(&origin); | ||
| 427 | if depth != 0 { | ||
| 428 | println!("{}/", directory_name); | ||
| 429 | } | ||
| 430 | for item in items { | ||
| 431 | self.status_print_item(item, depth + 1)?; | ||
| 432 | } | ||
| 433 | } | ||
| 434 | StatusItem::Unlinked { | ||
| 435 | origin, | ||
| 436 | is_directory, | ||
| 437 | } => { | ||
| 438 | let file_name = Self::status_get_filename(&origin); | ||
| 439 | let directory_extra = if is_directory { "/" } else { "" }; | ||
| 440 | println!("{}{}", file_name, directory_extra); | ||
| 441 | } | ||
| 442 | } | ||
| 443 | Ok(()) | ||
| 444 | } | ||
| 445 | fn status_get_filename(path: &Path) -> &str { | ||
| 446 | path.file_name() | ||
| 447 | .and_then(|s| s.to_str()) | ||
| 448 | .unwrap_or_default() | ||
| 449 | } | ||
| 450 | |||
| 451 | fn prepare_relative_path(path: &Path, base: &Path) -> anyhow::Result<PathBuf> { | ||
| 452 | let canonical = utils::weakly_canonical(path); | ||
| 453 | let relative = canonical | ||
| 454 | .strip_prefix(base) | ||
| 455 | .context("Invalid origin path, not under depot directory")?; | ||
| 456 | Ok(relative.to_owned()) | ||
| 457 | } | ||
| 458 | |||
| 459 | fn prepare_relative_origin(&self, path: &Path) -> anyhow::Result<PathBuf> { | ||
| 460 | Self::prepare_relative_path(path, &self.depot_dir) | ||
| 461 | } | ||
| 462 | |||
| 463 | fn prepare_relative_destination(&self, path: &Path) -> anyhow::Result<PathBuf> { | ||
| 464 | Self::prepare_relative_path(path, &self.install_base) | ||
| 465 | } | ||
| 466 | |||
| 467 | fn link_ids_from_paths_iter( | ||
| 468 | &self, | ||
| 469 | paths: impl Iterator<Item = impl AsRef<Path>>, | ||
| 470 | ) -> anyhow::Result<Vec<LinkID>> { | ||
| 471 | let mut link_ids = HashSet::<LinkID>::default(); | ||
| 472 | for path in paths { | ||
| 473 | let path = self.prepare_relative_origin(path.as_ref())?; | ||
| 474 | link_ids.extend(self.depot.links_under(&path)?); | ||
| 475 | } | ||
| 476 | Ok(Vec::from_iter(link_ids.into_iter())) | ||
| 477 | } | ||
| 478 | |||
| 479 | fn symlink_is_installed_by_link_id(&self, link_id: LinkID) -> anyhow::Result<bool> { | ||
| 480 | let canonical_pair = self.canonical_pair_from_link_id(link_id); | ||
| 481 | self.symlink_is_installed(&canonical_pair.origin, &canonical_pair.destination) | ||
| 482 | } | ||
| 483 | |||
| 484 | fn symlink_is_installed(&self, origin: &Path, destination: &Path) -> anyhow::Result<bool> { | ||
| 485 | debug_assert!(origin.is_absolute()); | ||
| 486 | debug_assert!(destination.is_absolute()); | ||
| 487 | |||
| 488 | if destination.is_symlink() { | ||
| 489 | let symlink_destination = destination.read_link()?; | ||
| 490 | match symlink_destination.canonicalize() { | ||
| 491 | Ok(canonicalized) => Ok(origin == canonicalized), | ||
| 492 | Err(_) => Ok(false), | ||
| 493 | } | ||
| 494 | } else { | ||
| 495 | Ok(false) | ||
| 496 | } | ||
| 497 | } | ||
| 498 | |||
| 499 | fn symlink_install_by_link_id(&self, link_id: LinkID) -> anyhow::Result<()> { | ||
| 500 | let canonical_pair = self.canonical_pair_from_link_id(link_id); | ||
| 501 | self.symlink_install(&canonical_pair.origin, &canonical_pair.destination) | ||
| 502 | } | ||
| 503 | |||
| 504 | fn symlink_install(&self, origin: &Path, destination: &Path) -> anyhow::Result<()> { | ||
| 505 | debug_assert!(origin.is_absolute()); | ||
| 506 | debug_assert!(destination.is_absolute()); | ||
| 507 | log::debug!( | ||
| 508 | "symlink_install : {} -> {}", | ||
| 509 | origin.display(), | ||
| 510 | destination.display() | ||
| 511 | ); | ||
| 512 | |||
| 513 | let destination_parent = destination | ||
| 514 | .parent() | ||
| 515 | .ok_or_else(|| anyhow::anyhow!("destination has no parent component"))?; | ||
| 516 | std::fs::create_dir_all(destination_parent).context("Failed to create directories")?; | ||
| 517 | // need to do this beacause if the destination path ends in '/' because the symlink | ||
| 518 | // functions will treat it as a directory but we want a file with that name. | ||
| 519 | let destination = destination.with_file_name(destination.file_name().unwrap()); | ||
| 520 | |||
| 521 | let destination_exists = destination.exists(); | ||
| 522 | let destination_is_symlink = destination.is_symlink(); | ||
| 523 | |||
| 524 | if destination_exists && !destination_is_symlink { | ||
| 525 | return Err(anyhow::anyhow!("destination already exists")); | ||
| 526 | } | ||
| 527 | |||
| 528 | if destination_is_symlink { | ||
| 529 | log::debug!("symlink already exists, removing before recreating"); | ||
| 530 | std::fs::remove_file(&destination)?; | ||
| 531 | } | ||
| 532 | |||
| 533 | log::debug!( | ||
| 534 | "creating filesystem symlink {} -> {}", | ||
| 535 | origin.display(), | ||
| 536 | destination.display() | ||
| 537 | ); | ||
| 538 | std::os::unix::fs::symlink(origin, destination).context("failed to create symlink")?; | ||
| 539 | |||
| 540 | Ok(()) | ||
| 541 | } | ||
| 542 | |||
| 543 | fn symlink_uninstall(&self, origin: &Path, destination: &Path) -> anyhow::Result<()> { | ||
| 544 | debug_assert!(origin.is_absolute()); | ||
| 545 | debug_assert!(destination.is_absolute()); | ||
| 546 | let destination = destination.with_file_name(destination.file_name().unwrap()); | ||
| 547 | |||
| 548 | if destination.is_symlink() { | ||
| 549 | let symlink_destination = destination.read_link()?.canonicalize()?; | ||
| 550 | if symlink_destination == origin { | ||
| 551 | std::fs::remove_file(&destination)?; | ||
| 552 | } | ||
| 553 | } | ||
| 554 | |||
| 555 | Ok(()) | ||
| 556 | } | ||
| 557 | |||
| 558 | fn symlink_uninstall_by_link_id(&self, link_id: LinkID) -> anyhow::Result<()> { | ||
| 559 | let canonical_pair = self.canonical_pair_from_link_id(link_id); | ||
| 560 | self.symlink_uninstall(&canonical_pair.origin, &canonical_pair.destination) | ||
| 561 | } | ||
| 562 | |||
| 563 | fn canonical_pair_from_link_id(&self, link_id: LinkID) -> CanonicalPair { | ||
| 564 | let link_view = self.depot.link_view(link_id); | ||
| 565 | let relative_origin = link_view.origin(); | ||
| 566 | let relative_destination = link_view.destination(); | ||
| 567 | let canonical_origin = self.depot_dir.join(relative_origin); | ||
| 568 | let canonical_destination = self.install_base.join(relative_destination); | ||
| 569 | CanonicalPair { | ||
| 570 | origin: canonical_origin, | ||
| 571 | destination: canonical_destination, | ||
| 572 | } | ||
| 573 | } | ||
| 574 | } | ||
| 575 | |||
| 576 | pub fn read(depot_path: PathBuf, install_base: PathBuf) -> anyhow::Result<Dotup> { | ||
| 577 | let depot_path = depot_path | ||
| 578 | .canonicalize() | ||
| 579 | .context("Failed to canonicalize depot path")?; | ||
| 580 | let install_base = install_base | ||
| 581 | .canonicalize() | ||
| 582 | .context("Failed to canonicalize install base")?; | ||
| 583 | if !install_base.is_dir() { | ||
| 584 | return Err(anyhow::anyhow!("Install base must be a directory")); | ||
| 585 | } | ||
| 586 | let depot = depot::read(&depot_path)?; | ||
| 587 | Dotup::new(depot, depot_path, install_base) | ||
| 588 | } | ||
| 589 | |||
| 590 | pub fn write(dotup: &Dotup) -> anyhow::Result<()> { | ||
| 591 | depot::write(&dotup.depot_path, &dotup.depot)?; | ||
| 592 | Ok(()) | ||
| 593 | } | ||
diff --git a/src/dotup/action_tree.rs b/src/dotup/action_tree.rs new file mode 100644 index 0000000..de6eee8 --- /dev/null +++ b/src/dotup/action_tree.rs | |||
| @@ -0,0 +1,347 @@ | |||
| 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 | impl TreeNodeKind { | ||
| 59 | fn as_action(&self) -> ActionID { | ||
| 60 | match self { | ||
| 61 | Self::Action(id) => *id, | ||
| 62 | _ => unreachable!(), | ||
| 63 | } | ||
| 64 | } | ||
| 65 | |||
| 66 | fn as_action_mut(&mut self) -> &mut ActionID { | ||
| 67 | match self { | ||
| 68 | Self::Action(id) => id, | ||
| 69 | _ => unreachable!(), | ||
| 70 | } | ||
| 71 | } | ||
| 72 | |||
| 73 | fn as_subtree(&self) -> &HashSet<NodeID> { | ||
| 74 | match self { | ||
| 75 | Self::SubTree(ids) => ids, | ||
| 76 | _ => unreachable!(), | ||
| 77 | } | ||
| 78 | } | ||
| 79 | |||
| 80 | fn as_subtree_mut(&mut self) -> &mut HashSet<NodeID> { | ||
| 81 | match self { | ||
| 82 | Self::SubTree(ids) => ids, | ||
| 83 | _ => unreachable!(), | ||
| 84 | } | ||
| 85 | } | ||
| 86 | } | ||
| 87 | |||
| 88 | // -------------------- ActionTree -------------------- // | ||
| 89 | |||
| 90 | impl Index<ActionID> for ActionTree { | ||
| 91 | type Output = TreeAction; | ||
| 92 | |||
| 93 | fn index(&self, index: ActionID) -> &Self::Output { | ||
| 94 | self.action(index).unwrap() | ||
| 95 | } | ||
| 96 | } | ||
| 97 | |||
| 98 | impl ActionTree { | ||
| 99 | pub fn new() -> Self { | ||
| 100 | let mut nodes = SlotMap::with_key(); | ||
| 101 | let root_id = nodes.insert(TreeNode { | ||
| 102 | path: AbsPathBuf::default(), | ||
| 103 | component: OsString::new(), | ||
| 104 | kind: TreeNodeKind::SubTree(Default::default()), | ||
| 105 | }); | ||
| 106 | |||
| 107 | Self { | ||
| 108 | root_id, | ||
| 109 | nodes, | ||
| 110 | actions: Default::default(), | ||
| 111 | } | ||
| 112 | } | ||
| 113 | |||
| 114 | pub fn insert(&mut self, target: &AbsPath, action: Action) -> ActionID { | ||
| 115 | let action_id = self.actions.insert(TreeAction { | ||
| 116 | path: target.to_owned(), | ||
| 117 | action, | ||
| 118 | }); | ||
| 119 | self.force_insert_at(&target, TreeNodeKind::Action(action_id)); | ||
| 120 | action_id | ||
| 121 | } | ||
| 122 | |||
| 123 | pub fn install(&self) -> std::io::Result<()> { | ||
| 124 | for action_id in self.action_ids() { | ||
| 125 | self.install_action(action_id)?; | ||
| 126 | } | ||
| 127 | Ok(()) | ||
| 128 | } | ||
| 129 | |||
| 130 | pub fn is_installed(&self, action_id: ActionID) -> bool { | ||
| 131 | let action = &self.actions[action_id]; | ||
| 132 | let target = action.target(); | ||
| 133 | match action.action() { | ||
| 134 | Action::Link { source } => { | ||
| 135 | let link = match std::fs::read_link(target) { | ||
| 136 | Ok(link) => link, | ||
| 137 | Err(_) => return false, | ||
| 138 | }; | ||
| 139 | link.canonicalize().unwrap() == source.canonicalize().unwrap() | ||
| 140 | } | ||
| 141 | Action::Copy { .. } => target.as_ref().exists(), | ||
| 142 | } | ||
| 143 | } | ||
| 144 | |||
| 145 | pub fn uninstall(&self) -> std::io::Result<()> { | ||
| 146 | for action_id in self.action_ids() { | ||
| 147 | self.uninstall_action(action_id)?; | ||
| 148 | } | ||
| 149 | Ok(()) | ||
| 150 | } | ||
| 151 | |||
| 152 | pub fn install_action(&self, action_id: ActionID) -> std::io::Result<()> { | ||
| 153 | let action = &self[action_id]; | ||
| 154 | match &action.action { | ||
| 155 | Action::Link { source } => { | ||
| 156 | let target = action.target(); | ||
| 157 | log::info!("Linking {:?} -> {:?}", source, target); | ||
| 158 | if target.as_ref().is_symlink() { | ||
| 159 | log::trace!("{:?} is a symlink, removing it", target); | ||
| 160 | std::fs::remove_file(target)?; | ||
| 161 | } | ||
| 162 | if let Some(parent) = target.parent() { | ||
| 163 | log::trace!("creating all directories up to {:?}", parent); | ||
| 164 | std::fs::create_dir_all(parent.as_ref())?; | ||
| 165 | } | ||
| 166 | log::trace!("creating symlink {:?} -> {:?}", source, target); | ||
| 167 | std::os::unix::fs::symlink(source, target)?; | ||
| 168 | } | ||
| 169 | Action::Copy { source } => todo!(), | ||
| 170 | } | ||
| 171 | Ok(()) | ||
| 172 | } | ||
| 173 | |||
| 174 | pub fn uninstall_action(&self, action_id: ActionID) -> std::io::Result<()> { | ||
| 175 | let action = &self[action_id]; | ||
| 176 | if let Action::Link { ref source } = action.action { | ||
| 177 | let target = action.target(); | ||
| 178 | if target.as_ref().is_symlink() { | ||
| 179 | log::trace!("{:?} is a symlink", target); | ||
| 180 | let symlink_target = std::fs::read_link(target.as_ref())?; | ||
| 181 | if symlink_target == *source { | ||
| 182 | log::info!("symlink target is {:?}, removing it", source); | ||
| 183 | std::fs::remove_file(target)?; | ||
| 184 | } else { | ||
| 185 | log::trace!( | ||
| 186 | "symlink target is {:?}, not {:?}, not removing it", | ||
| 187 | symlink_target, | ||
| 188 | source | ||
| 189 | ); | ||
| 190 | } | ||
| 191 | } | ||
| 192 | } | ||
| 193 | Ok(()) | ||
| 194 | } | ||
| 195 | |||
| 196 | pub fn actions(&self) -> impl Iterator<Item = &TreeAction> { | ||
| 197 | self.actions.values() | ||
| 198 | } | ||
| 199 | |||
| 200 | pub fn action_ids(&self) -> impl Iterator<Item = ActionID> + '_ { | ||
| 201 | self.actions.keys() | ||
| 202 | } | ||
| 203 | |||
| 204 | pub fn action(&self, action_id: ActionID) -> Option<&TreeAction> { | ||
| 205 | self.actions.get(action_id) | ||
| 206 | } | ||
| 207 | |||
| 208 | /// Creates all nodes up to the given path. | ||
| 209 | /// If one of the nodes is an action node, it will be replaced with a subtree node. | ||
| 210 | fn force_insert_at(&mut self, target: &AbsPath, kind: TreeNodeKind) -> NodeID { | ||
| 211 | let mut curr = self.root_id; | ||
| 212 | for comp in target.components() { | ||
| 213 | { | ||
| 214 | // Try to find node if it exists | ||
| 215 | let curr_node = &mut self.nodes[curr]; | ||
| 216 | match curr_node.kind { | ||
| 217 | TreeNodeKind::Action(action) => { | ||
| 218 | self.actions.remove(action); | ||
| 219 | curr_node.kind = TreeNodeKind::SubTree(Default::default()); | ||
| 220 | match curr_node.kind { | ||
| 221 | TreeNodeKind::SubTree(ref mut children) => children, | ||
| 222 | _ => unreachable!(), | ||
| 223 | } | ||
| 224 | } | ||
| 225 | TreeNodeKind::SubTree(ref mut children) => children, | ||
| 226 | }; | ||
| 227 | |||
| 228 | let children = self.nodes[curr].kind.as_subtree(); | ||
| 229 | for &child_id in children.iter() { | ||
| 230 | let child_node = &self.nodes[child_id]; | ||
| 231 | if child_node.component == comp { | ||
| 232 | curr = child_id; | ||
| 233 | break; | ||
| 234 | } | ||
| 235 | } | ||
| 236 | } | ||
| 237 | { | ||
| 238 | // Create new node | ||
| 239 | let new_node = TreeNode { | ||
| 240 | path: self.nodes[curr].path.join(comp), | ||
| 241 | component: comp.to_owned(), | ||
| 242 | kind: TreeNodeKind::SubTree(Default::default()), | ||
| 243 | }; | ||
| 244 | let new_id = self.nodes.insert(new_node); | ||
| 245 | match &mut self.nodes[curr].kind { | ||
| 246 | TreeNodeKind::SubTree(children) => children.insert(new_id), | ||
| 247 | _ => unreachable!(), | ||
| 248 | }; | ||
| 249 | curr = new_id; | ||
| 250 | } | ||
| 251 | } | ||
| 252 | let prev_kind = std::mem::replace(&mut self.nodes[curr].kind, kind); | ||
| 253 | match prev_kind { | ||
| 254 | TreeNodeKind::SubTree(children) => { | ||
| 255 | for &child in children.iter() { | ||
| 256 | self.remove_node(child); | ||
| 257 | } | ||
| 258 | } | ||
| 259 | _ => {} | ||
| 260 | } | ||
| 261 | curr | ||
| 262 | } | ||
| 263 | |||
| 264 | /// Removes the given node. | ||
| 265 | /// Does not remove it from the parent's children node. | ||
| 266 | fn remove_node(&mut self, node_id: NodeID) { | ||
| 267 | let node = self | ||
| 268 | .nodes | ||
| 269 | .remove(node_id) | ||
| 270 | .expect("Node being removed does not exist"); | ||
| 271 | match node.kind { | ||
| 272 | TreeNodeKind::Action(action) => { | ||
| 273 | self.actions.remove(action); | ||
| 274 | } | ||
| 275 | TreeNodeKind::SubTree(children) => { | ||
| 276 | for child in children { | ||
| 277 | self.remove_node(child); | ||
| 278 | } | ||
| 279 | } | ||
| 280 | }; | ||
| 281 | } | ||
| 282 | } | ||
| 283 | |||
| 284 | #[cfg(test)] | ||
| 285 | mod tests { | ||
| 286 | use std::{convert::TryFrom, path::Path}; | ||
| 287 | |||
| 288 | use super::*; | ||
| 289 | |||
| 290 | #[test] | ||
| 291 | fn empty_tree() { | ||
| 292 | let _ = ActionTree::new(); | ||
| 293 | } | ||
| 294 | |||
| 295 | #[test] | ||
| 296 | fn single_action() { | ||
| 297 | let mut tree = ActionTree::new(); | ||
| 298 | |||
| 299 | let action_id = tree.insert( | ||
| 300 | TryFrom::try_from("/home/user/.config/nvim").unwrap(), | ||
| 301 | Action::Link { | ||
| 302 | source: PathBuf::from("nvim"), | ||
| 303 | }, | ||
| 304 | ); | ||
| 305 | |||
| 306 | let action = &tree[action_id]; | ||
| 307 | assert_eq!( | ||
| 308 | action.path.as_path(), | ||
| 309 | AbsPath::new(Path::new("/home/user/.config/nvim")) | ||
| 310 | ); | ||
| 311 | } | ||
| 312 | |||
| 313 | #[test] | ||
| 314 | fn subtree_replacement() { | ||
| 315 | let mut tree = ActionTree::new(); | ||
| 316 | |||
| 317 | let action_id = tree.insert( | ||
| 318 | TryFrom::try_from("/home/user/.config/nvim").unwrap(), | ||
| 319 | Action::Link { | ||
| 320 | source: PathBuf::from("nvim"), | ||
| 321 | }, | ||
| 322 | ); | ||
| 323 | let action_id_original = action_id; | ||
| 324 | |||
| 325 | let action = &tree[action_id]; | ||
| 326 | assert_eq!( | ||
| 327 | action.path.as_path(), | ||
| 328 | AbsPath::new(Path::new("/home/user/.config/nvim")) | ||
| 329 | ); | ||
| 330 | |||
| 331 | let action_id = tree.insert( | ||
| 332 | TryFrom::try_from("/home/user/.config/nvim/init.vim").unwrap(), | ||
| 333 | Action::Link { | ||
| 334 | source: PathBuf::from("nvim/init.vim"), | ||
| 335 | }, | ||
| 336 | ); | ||
| 337 | |||
| 338 | let action = &tree[action_id]; | ||
| 339 | assert_eq!( | ||
| 340 | action.path.as_path(), | ||
| 341 | AbsPath::new(Path::new("/home/user/.config/nvim/init.vim")) | ||
| 342 | ); | ||
| 343 | |||
| 344 | eprintln!("{:#?}", tree); | ||
| 345 | assert!(tree.action(action_id_original).is_none()); | ||
| 346 | } | ||
| 347 | } | ||
diff --git a/src/dotup/cfg.rs b/src/dotup/cfg.rs new file mode 100644 index 0000000..dbe8769 --- /dev/null +++ b/src/dotup/cfg.rs | |||
| @@ -0,0 +1,352 @@ | |||
| 1 | use std::fmt::Write; | ||
| 2 | |||
| 3 | use nom::{ | ||
| 4 | branch::alt, | ||
| 5 | bytes::complete::{tag, take_while}, | ||
| 6 | character::complete::{alphanumeric1, multispace0, multispace1, space1}, | ||
| 7 | combinator::map, | ||
| 8 | multi::separated_list0, | ||
| 9 | sequence::{delimited, preceded}, | ||
| 10 | }; | ||
| 11 | |||
| 12 | type Span<'s> = nom_locate::LocatedSpan<&'s str>; | ||
| 13 | type IResult<'s, I, O, E = ParseError> = nom::IResult<I, O, E>; | ||
| 14 | |||
| 15 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] | ||
| 16 | pub struct KeyValue { | ||
| 17 | pub key: String, | ||
| 18 | pub value: String, | ||
| 19 | } | ||
| 20 | |||
| 21 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] | ||
| 22 | pub struct Action { | ||
| 23 | pub location: Location, | ||
| 24 | pub kind: String, | ||
| 25 | pub keyvalues: Vec<KeyValue>, | ||
| 26 | } | ||
| 27 | |||
| 28 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] | ||
| 29 | pub struct Comment { | ||
| 30 | pub location: Location, | ||
| 31 | pub text: String, | ||
| 32 | } | ||
| 33 | |||
| 34 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] | ||
| 35 | pub enum GroupItem { | ||
| 36 | Group(Group), | ||
| 37 | Action(Action), | ||
| 38 | Comment(Comment), | ||
| 39 | } | ||
| 40 | |||
| 41 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] | ||
| 42 | pub struct Group { | ||
| 43 | pub location: Location, | ||
| 44 | pub name: String, | ||
| 45 | pub items: Vec<GroupItem>, | ||
| 46 | } | ||
| 47 | |||
| 48 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] | ||
| 49 | pub struct Config { | ||
| 50 | pub groups: Vec<Group>, | ||
| 51 | } | ||
| 52 | |||
| 53 | pub fn parse(content: &str) -> Result<Config, ParseError> { | ||
| 54 | match config(Span::new(content)) { | ||
| 55 | Ok((_, config)) => Ok(config), | ||
| 56 | Err(err) => match err { | ||
| 57 | nom::Err::Incomplete(_) => Err(ParseError::new(Default::default(), "unexpected EOF")), | ||
| 58 | nom::Err::Error(e) | nom::Err::Failure(e) => Err(e), | ||
| 59 | }, | ||
| 60 | } | ||
| 61 | } | ||
| 62 | |||
| 63 | pub fn format(content: &str) -> Result<String, ParseError> { | ||
| 64 | struct Ident(usize); | ||
| 65 | impl std::fmt::Display for Ident { | ||
| 66 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
| 67 | for _ in 0..self.0 { | ||
| 68 | write!(f, "\t")?; | ||
| 69 | } | ||
| 70 | Ok(()) | ||
| 71 | } | ||
| 72 | } | ||
| 73 | |||
| 74 | fn format_action(buffer: &mut String, action: &Action, ident: usize) -> std::fmt::Result { | ||
| 75 | write!(buffer, "{}{}", Ident(ident), action.kind)?; | ||
| 76 | for kv in action.keyvalues.iter() { | ||
| 77 | write!(buffer, " {}=\"{}\"", kv.key, kv.value)?; | ||
| 78 | } | ||
| 79 | writeln!(buffer)?; | ||
| 80 | Ok(()) | ||
| 81 | } | ||
| 82 | |||
| 83 | fn format_comment(buffer: &mut String, comment: &Comment, ident: usize) -> std::fmt::Result { | ||
| 84 | for line in comment.text.lines() { | ||
| 85 | writeln!(buffer, "{}# {}", Ident(ident), line)?; | ||
| 86 | } | ||
| 87 | Ok(()) | ||
| 88 | } | ||
| 89 | |||
| 90 | fn format_group(buffer: &mut String, group: &Group, ident: usize) -> std::fmt::Result { | ||
| 91 | writeln!(buffer, "{}group {} {{", Ident(ident), group.name)?; | ||
| 92 | for item in group.items.iter() { | ||
| 93 | match item { | ||
| 94 | GroupItem::Group(group) => format_group(buffer, group, ident + 1)?, | ||
| 95 | GroupItem::Action(action) => format_action(buffer, action, ident + 1)?, | ||
| 96 | GroupItem::Comment(comment) => format_comment(buffer, comment, ident + 1)?, | ||
| 97 | } | ||
| 98 | } | ||
| 99 | writeln!(buffer, "{}}}", Ident(ident))?; | ||
| 100 | writeln!(buffer)?; | ||
| 101 | Ok(()) | ||
| 102 | } | ||
| 103 | |||
| 104 | let config = parse(content)?; | ||
| 105 | let mut buffer = String::new(); | ||
| 106 | for group in config.groups { | ||
| 107 | format_group(&mut buffer, &group, 0).unwrap(); | ||
| 108 | } | ||
| 109 | assert!(parse(&buffer).is_ok()); | ||
| 110 | Ok(buffer) | ||
| 111 | } | ||
| 112 | |||
| 113 | #[derive(Debug)] | ||
| 114 | pub struct ParseError { | ||
| 115 | location: Location, | ||
| 116 | message: String, | ||
| 117 | } | ||
| 118 | |||
| 119 | impl ParseError { | ||
| 120 | fn new(location: Location, expected: impl Into<String>) -> Self { | ||
| 121 | Self { | ||
| 122 | location, | ||
| 123 | message: expected.into(), | ||
| 124 | } | ||
| 125 | } | ||
| 126 | |||
| 127 | pub fn location(&self) -> Location { | ||
| 128 | self.location | ||
| 129 | } | ||
| 130 | |||
| 131 | pub fn message(&self) -> &str { | ||
| 132 | &self.message | ||
| 133 | } | ||
| 134 | } | ||
| 135 | |||
| 136 | impl<'s> nom::error::ParseError<Span<'s>> for ParseError { | ||
| 137 | fn from_error_kind(input: Span<'s>, kind: nom::error::ErrorKind) -> Self { | ||
| 138 | Self::new(location_from_span(input), format!("error kind: {kind:?}")) | ||
| 139 | } | ||
| 140 | |||
| 141 | fn append(input: Span, kind: nom::error::ErrorKind, other: Self) -> Self { | ||
| 142 | other | ||
| 143 | } | ||
| 144 | |||
| 145 | fn or(self, other: Self) -> Self { | ||
| 146 | other | ||
| 147 | } | ||
| 148 | |||
| 149 | fn from_char(input: Span<'s>, c: char) -> Self { | ||
| 150 | Self::new(location_from_span(input), format!("invalid character: {c}")) | ||
| 151 | } | ||
| 152 | } | ||
| 153 | |||
| 154 | impl std::fmt::Display for ParseError { | ||
| 155 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
| 156 | write!(f, "parse error at {}: {}", self.location, self.message) | ||
| 157 | } | ||
| 158 | } | ||
| 159 | |||
| 160 | impl std::error::Error for ParseError {} | ||
| 161 | |||
| 162 | #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] | ||
| 163 | pub struct Location { | ||
| 164 | pub line: u32, | ||
| 165 | pub column: u32, | ||
| 166 | } | ||
| 167 | |||
| 168 | impl Location { | ||
| 169 | pub fn new(line: u32, column: u32) -> Self { | ||
| 170 | Self { line, column } | ||
| 171 | } | ||
| 172 | } | ||
| 173 | |||
| 174 | impl std::cmp::PartialOrd for Location { | ||
| 175 | fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { | ||
| 176 | Some(self.cmp(other)) | ||
| 177 | } | ||
| 178 | } | ||
| 179 | |||
| 180 | impl std::cmp::Ord for Location { | ||
| 181 | fn cmp(&self, other: &Self) -> std::cmp::Ordering { | ||
| 182 | match self.line.cmp(&other.line) { | ||
| 183 | core::cmp::Ordering::Equal => {} | ||
| 184 | ord => return ord, | ||
| 185 | } | ||
| 186 | self.column.cmp(&other.column) | ||
| 187 | } | ||
| 188 | } | ||
| 189 | |||
| 190 | impl std::fmt::Display for Location { | ||
| 191 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
| 192 | let Location { line, column } = self; | ||
| 193 | write!(f, "line {line} column {column}") | ||
| 194 | } | ||
| 195 | } | ||
| 196 | |||
| 197 | impl KeyValue { | ||
| 198 | fn new(key: impl Into<String>, value: impl Into<String>) -> Self { | ||
| 199 | Self { | ||
| 200 | key: key.into(), | ||
| 201 | value: value.into(), | ||
| 202 | } | ||
| 203 | } | ||
| 204 | } | ||
| 205 | |||
| 206 | fn is_value_char(c: char) -> bool { | ||
| 207 | c.is_alphanumeric() || c == '_' || c == '-' || c == '.' || c == '/' | ||
| 208 | } | ||
| 209 | |||
| 210 | fn linesep(i: Span) -> IResult<Span, Span> { | ||
| 211 | take_while(|c: char| c.is_whitespace() && c != '\n')(i)?; | ||
| 212 | take_while(|c: char| c == '\n')(i)?; | ||
| 213 | take_while(char::is_whitespace)(i) | ||
| 214 | } | ||
| 215 | |||
| 216 | fn keyvalue(i: Span) -> IResult<Span, KeyValue> { | ||
| 217 | let (i, key) = alphanumeric1(i)?; | ||
| 218 | let (i, _) = tag("=")(i)?; | ||
| 219 | let (i, val) = delimited(tag("\""), take_while(is_value_char), tag("\""))(i)?; | ||
| 220 | Ok(( | ||
| 221 | i, | ||
| 222 | KeyValue { | ||
| 223 | key: key.fragment().to_string(), | ||
| 224 | value: val.fragment().to_string(), | ||
| 225 | }, | ||
| 226 | )) | ||
| 227 | } | ||
| 228 | |||
| 229 | fn keyvalues(i: Span) -> IResult<Span, Vec<KeyValue>> { | ||
| 230 | separated_list0(space1, keyvalue)(i) | ||
| 231 | } | ||
| 232 | |||
| 233 | fn comment(i: Span) -> IResult<Span, Comment> { | ||
| 234 | let location = location_from_span(i); | ||
| 235 | preceded( | ||
| 236 | tag("#"), | ||
| 237 | preceded(multispace0, take_while(|c: char| c != '\n')), | ||
| 238 | )(i) | ||
| 239 | .map(|(i, text)| { | ||
| 240 | ( | ||
| 241 | i, | ||
| 242 | Comment { | ||
| 243 | location, | ||
| 244 | text: text.to_string(), | ||
| 245 | }, | ||
| 246 | ) | ||
| 247 | }) | ||
| 248 | } | ||
| 249 | |||
| 250 | fn action(i: Span) -> IResult<Span, Action> { | ||
| 251 | let location = location_from_span(i); | ||
| 252 | let (i, kind) = alphanumeric1(i)?; | ||
| 253 | let (i, keyvalues) = preceded(space1, keyvalues)(i)?; | ||
| 254 | Ok(( | ||
| 255 | i, | ||
| 256 | Action { | ||
| 257 | location, | ||
| 258 | kind: kind.to_string(), | ||
| 259 | keyvalues, | ||
| 260 | }, | ||
| 261 | )) | ||
| 262 | } | ||
| 263 | |||
| 264 | fn group_item(i: Span) -> IResult<Span, GroupItem> { | ||
| 265 | alt(( | ||
| 266 | map(group, GroupItem::Group), | ||
| 267 | map(action, GroupItem::Action), | ||
| 268 | map(comment, GroupItem::Comment), | ||
| 269 | ))(i) | ||
| 270 | } | ||
| 271 | |||
| 272 | fn group(i: Span) -> IResult<Span, Group> { | ||
| 273 | let location = location_from_span(i); | ||
| 274 | |||
| 275 | let (i, _) = tag("group")(i)?; | ||
| 276 | let (i, _) = multispace1(i)?; | ||
| 277 | let (i, name) = alphanumeric1(i)?; | ||
| 278 | let (i, _) = multispace0(i)?; | ||
| 279 | let (i, _) = tag("{")(i)?; | ||
| 280 | let (i, _) = multispace0(i)?; | ||
| 281 | let (i, items) = separated_list0(linesep, group_item)(i)?; | ||
| 282 | let (i, _) = multispace0(i)?; | ||
| 283 | let (i, _) = tag("}")(i)?; | ||
| 284 | |||
| 285 | Ok(( | ||
| 286 | i, | ||
| 287 | Group { | ||
| 288 | location, | ||
| 289 | name: name.to_string(), | ||
| 290 | items, | ||
| 291 | }, | ||
| 292 | )) | ||
| 293 | } | ||
| 294 | |||
| 295 | fn config(i: Span) -> IResult<Span, Config> { | ||
| 296 | let mut groups = Vec::new(); | ||
| 297 | let mut parser = delimited(multispace0, group, multispace0); | ||
| 298 | let mut curr_span = i; | ||
| 299 | while !curr_span.is_empty() { | ||
| 300 | match parser(curr_span) { | ||
| 301 | Ok((i, group)) => { | ||
| 302 | curr_span = i; | ||
| 303 | groups.push(group); | ||
| 304 | } | ||
| 305 | Err(nom::Err::Error(e)) | Err(nom::Err::Failure(e)) => { | ||
| 306 | return Err(nom::Err::Failure(e)) | ||
| 307 | } | ||
| 308 | Err(nom::Err::Incomplete(_)) => break, | ||
| 309 | } | ||
| 310 | } | ||
| 311 | Ok((i, Config { groups })) | ||
| 312 | } | ||
| 313 | |||
| 314 | fn location_from_span(span: Span) -> Location { | ||
| 315 | Location::new(span.location_line(), span.get_utf8_column() as u32) | ||
| 316 | } | ||
| 317 | |||
| 318 | #[cfg(test)] | ||
| 319 | mod tests { | ||
| 320 | use super::*; | ||
| 321 | |||
| 322 | #[test] | ||
| 323 | fn parse_keyvalue() { | ||
| 324 | let input = Span::new(r#"key="value""#); | ||
| 325 | let (rem, kv) = keyvalue(input).unwrap(); | ||
| 326 | assert!(rem.is_empty()); | ||
| 327 | assert_eq!(kv, KeyValue::new("key", "value"),); | ||
| 328 | } | ||
| 329 | |||
| 330 | #[test] | ||
| 331 | fn parse_keyvalues() { | ||
| 332 | let kvs = vec![ | ||
| 333 | KeyValue::new("key1", "value1"), | ||
| 334 | KeyValue::new("key2", "value2"), | ||
| 335 | ]; | ||
| 336 | |||
| 337 | let input = Span::new(r#"key1="value1" key2="value2""#); | ||
| 338 | let (rem, res) = keyvalues(input).unwrap(); | ||
| 339 | assert!(rem.is_empty()); | ||
| 340 | assert_eq!(res, kvs); | ||
| 341 | |||
| 342 | let kvs = vec![ | ||
| 343 | KeyValue::new("src", "tmux/"), | ||
| 344 | KeyValue::new("dst", ".config/tmux"), | ||
| 345 | ]; | ||
| 346 | |||
| 347 | let input = Span::new(r#"src="tmux/" dst=".config/tmux""#); | ||
| 348 | let (rem, res) = keyvalues(input).unwrap(); | ||
| 349 | assert!(rem.is_empty()); | ||
| 350 | assert_eq!(res, kvs); | ||
| 351 | } | ||
| 352 | } | ||
diff --git a/src/dotup/mod.rs b/src/dotup/mod.rs new file mode 100644 index 0000000..4e91c2f --- /dev/null +++ b/src/dotup/mod.rs | |||
| @@ -0,0 +1,380 @@ | |||
| 1 | mod action_tree; | ||
| 2 | mod cfg; | ||
| 3 | mod paths; | ||
| 4 | |||
| 5 | use std::collections::HashSet; | ||
| 6 | use std::{ | ||
| 7 | collections::HashMap, | ||
| 8 | path::{Path, PathBuf}, | ||
| 9 | }; | ||
| 10 | |||
| 11 | use slotmap::{Key, SlotMap}; | ||
| 12 | use thiserror::Error; | ||
| 13 | |||
| 14 | pub use paths::*; | ||
| 15 | |||
| 16 | type Result<T, E = Error> = std::result::Result<T, E>; | ||
| 17 | |||
| 18 | slotmap::new_key_type! { pub struct GroupID; } | ||
| 19 | |||
| 20 | #[derive(Debug, Error)] | ||
| 21 | pub enum Error { | ||
| 22 | #[error(transparent)] | ||
| 23 | ParseError(#[from] cfg::ParseError), | ||
| 24 | #[error("error: {0}")] | ||
| 25 | Custom(String), | ||
| 26 | #[error(transparent)] | ||
| 27 | IOError(#[from] std::io::Error), | ||
| 28 | } | ||
| 29 | |||
| 30 | #[derive(Debug, Default)] | ||
| 31 | pub struct Group { | ||
| 32 | name: String, | ||
| 33 | parent: GroupID, | ||
| 34 | children: HashMap<String, GroupID>, | ||
| 35 | actions: Vec<Action>, | ||
| 36 | } | ||
| 37 | |||
| 38 | #[derive(Debug)] | ||
| 39 | pub struct Dotup { | ||
| 40 | root_id: GroupID, | ||
| 41 | groups: SlotMap<GroupID, Group>, | ||
| 42 | } | ||
| 43 | |||
| 44 | #[derive(Debug, Clone, Copy)] | ||
| 45 | pub struct InstallParams<'p> { | ||
| 46 | pub cwd: &'p Path, | ||
| 47 | pub home: &'p Path, | ||
| 48 | } | ||
| 49 | |||
| 50 | #[derive(Debug, Clone, Copy)] | ||
| 51 | pub struct UninstallParams<'p> { | ||
| 52 | pub cwd: &'p Path, | ||
| 53 | pub home: &'p Path, | ||
| 54 | } | ||
| 55 | |||
| 56 | #[derive(Debug)] | ||
| 57 | struct KeyValueParser { | ||
| 58 | location: cfg::Location, | ||
| 59 | keyvalues: Vec<cfg::KeyValue>, | ||
| 60 | } | ||
| 61 | |||
| 62 | #[derive(Debug, Clone)] | ||
| 63 | struct IncludeAction { | ||
| 64 | group: String, | ||
| 65 | } | ||
| 66 | |||
| 67 | #[derive(Debug, Clone)] | ||
| 68 | struct LinkAction { | ||
| 69 | source: PathBuf, | ||
| 70 | target: PathBuf, | ||
| 71 | } | ||
| 72 | |||
| 73 | #[derive(Debug, Clone)] | ||
| 74 | struct CopyAction { | ||
| 75 | source: PathBuf, | ||
| 76 | target: PathBuf, | ||
| 77 | } | ||
| 78 | |||
| 79 | #[derive(Debug, Clone)] | ||
| 80 | enum Action { | ||
| 81 | Include(IncludeAction), | ||
| 82 | Link(LinkAction), | ||
| 83 | Copy(CopyAction), | ||
| 84 | } | ||
| 85 | |||
| 86 | pub fn load(content: &str) -> Result<Dotup> { | ||
| 87 | let config = cfg::parse(content)?; | ||
| 88 | Dotup::from_config(config) | ||
| 89 | } | ||
| 90 | |||
| 91 | pub fn load_file(path: impl AsRef<Path>) -> Result<Dotup> { | ||
| 92 | let content = std::fs::read_to_string(path)?; | ||
| 93 | load(&content) | ||
| 94 | } | ||
| 95 | |||
| 96 | pub fn format(content: &str) -> Result<String> { | ||
| 97 | Ok(cfg::format(content)?) | ||
| 98 | } | ||
| 99 | |||
| 100 | pub fn format_file(path: &Path) -> Result<String> { | ||
| 101 | let content = std::fs::read_to_string(path)?; | ||
| 102 | format(&content) | ||
| 103 | } | ||
| 104 | |||
| 105 | pub fn format_file_inplace(path: &Path) -> Result<()> { | ||
| 106 | let content = std::fs::read_to_string(path)?; | ||
| 107 | let formatted = format(&content)?; | ||
| 108 | std::fs::write(path, formatted)?; | ||
| 109 | Ok(()) | ||
| 110 | } | ||
| 111 | |||
| 112 | // -------------------- Dotup -------------------- // | ||
| 113 | |||
| 114 | impl Dotup { | ||
| 115 | pub fn find_group_by_name(&self, name: &str) -> Option<GroupID> { | ||
| 116 | self.find_group_by_name_rooted(self.root_id, name) | ||
| 117 | } | ||
| 118 | |||
| 119 | pub fn install(&self, params: InstallParams, group_id: GroupID) -> Result<()> { | ||
| 120 | let action_tree = self.build_action_tree(params.cwd, params.home, group_id)?; | ||
| 121 | action_tree.install()?; | ||
| 122 | Ok(()) | ||
| 123 | } | ||
| 124 | |||
| 125 | pub fn uninstall(&self, params: UninstallParams, group_id: GroupID) -> Result<()> { | ||
| 126 | let action_tree = self.build_action_tree(params.cwd, params.home, group_id)?; | ||
| 127 | action_tree.uninstall()?; | ||
| 128 | Ok(()) | ||
| 129 | } | ||
| 130 | |||
| 131 | pub fn status(&self, params: InstallParams, group_id: GroupID) -> Result<()> { | ||
| 132 | let action_tree = self.build_action_tree(params.cwd, params.home, group_id)?; | ||
| 133 | for action_id in action_tree.action_ids() { | ||
| 134 | let prefix = if action_tree.is_installed(action_id) { | ||
| 135 | "INSTALLED" | ||
| 136 | } else { | ||
| 137 | "NOT INSTALLED" | ||
| 138 | }; | ||
| 139 | let action = action_tree.action(action_id).unwrap(); | ||
| 140 | let source = match action.action() { | ||
| 141 | action_tree::Action::Link { ref source } => source, | ||
| 142 | action_tree::Action::Copy { ref source } => source, | ||
| 143 | }; | ||
| 144 | let target = action.target(); | ||
| 145 | println!("{}: {} -> {}", prefix, source.display(), target.display()); | ||
| 146 | } | ||
| 147 | Ok(()) | ||
| 148 | } | ||
| 149 | } | ||
| 150 | |||
| 151 | impl Dotup { | ||
| 152 | fn from_config(config: cfg::Config) -> Result<Self> { | ||
| 153 | let mut groups = SlotMap::default(); | ||
| 154 | let root_id = groups.insert(Default::default()); | ||
| 155 | let mut dotup = Self { root_id, groups }; | ||
| 156 | |||
| 157 | for group in config.groups { | ||
| 158 | dotup.insert_group(root_id, group)?; | ||
| 159 | } | ||
| 160 | |||
| 161 | Ok(dotup) | ||
| 162 | } | ||
| 163 | |||
| 164 | fn find_group_by_name_rooted(&self, root: GroupID, name: &str) -> Option<GroupID> { | ||
| 165 | let trimmed = name.trim_start_matches("."); | ||
| 166 | let rel_levels = name.len() - trimmed.len(); | ||
| 167 | let mut current = self.root_id; | ||
| 168 | |||
| 169 | if rel_levels != 0 { | ||
| 170 | current = root; | ||
| 171 | for _ in 0..rel_levels - 1 { | ||
| 172 | current = self.groups[current].parent; | ||
| 173 | if current == self.root_id { | ||
| 174 | break; | ||
| 175 | } | ||
| 176 | } | ||
| 177 | } | ||
| 178 | |||
| 179 | for comp in trimmed.split(".") { | ||
| 180 | let group = &self.groups[current]; | ||
| 181 | let child_id = group.children.get(comp)?; | ||
| 182 | current = *child_id; | ||
| 183 | } | ||
| 184 | Some(current) | ||
| 185 | } | ||
| 186 | |||
| 187 | fn insert_group(&mut self, parent_id: GroupID, mut group_cfg: cfg::Group) -> Result<()> { | ||
| 188 | let parent = &mut self.groups[parent_id]; | ||
| 189 | if parent.children.contains_key(&group_cfg.name) { | ||
| 190 | return Err(Error::Custom(format!( | ||
| 191 | "group '{}' at {} already exists", | ||
| 192 | group_cfg.name, group_cfg.location, | ||
| 193 | ))); | ||
| 194 | } | ||
| 195 | |||
| 196 | let mut group = Group { | ||
| 197 | name: group_cfg.name.clone(), | ||
| 198 | parent: parent_id, | ||
| 199 | children: Default::default(), | ||
| 200 | actions: Default::default(), | ||
| 201 | }; | ||
| 202 | |||
| 203 | for item in group_cfg | ||
| 204 | .items | ||
| 205 | .drain_filter(|item| std::matches!(item, cfg::GroupItem::Action(_))) | ||
| 206 | { | ||
| 207 | match item { | ||
| 208 | cfg::GroupItem::Action(action) => { | ||
| 209 | let action = cfg_action_to_action(action)?; | ||
| 210 | group.actions.push(action); | ||
| 211 | } | ||
| 212 | _ => {} | ||
| 213 | } | ||
| 214 | } | ||
| 215 | |||
| 216 | let group_id = self.groups.insert(group); | ||
| 217 | let parent = &mut self.groups[parent_id]; | ||
| 218 | parent.children.insert(group_cfg.name, group_id); | ||
| 219 | |||
| 220 | for item in group_cfg.items { | ||
| 221 | match item { | ||
| 222 | cfg::GroupItem::Group(group) => { | ||
| 223 | self.insert_group(group_id, group)?; | ||
| 224 | } | ||
| 225 | _ => {} | ||
| 226 | } | ||
| 227 | } | ||
| 228 | |||
| 229 | Ok(()) | ||
| 230 | } | ||
| 231 | |||
| 232 | fn build_action_tree( | ||
| 233 | &self, | ||
| 234 | cwd: &Path, | ||
| 235 | home: &Path, | ||
| 236 | group_id: GroupID, | ||
| 237 | ) -> Result<action_tree::ActionTree> { | ||
| 238 | fn inner_helper( | ||
| 239 | dotup: &Dotup, | ||
| 240 | cwd: &AbsPath, | ||
| 241 | home: &AbsPath, | ||
| 242 | group_id: GroupID, | ||
| 243 | tree: &mut action_tree::ActionTree, | ||
| 244 | visited: &mut HashSet<GroupID>, | ||
| 245 | ) -> Result<()> { | ||
| 246 | if visited.contains(&group_id) { | ||
| 247 | return Ok(()); | ||
| 248 | } | ||
| 249 | visited.insert(group_id); | ||
| 250 | |||
| 251 | let group = &dotup.groups[group_id]; | ||
| 252 | for action in group.actions.iter() { | ||
| 253 | match action { | ||
| 254 | Action::Include(action) => { | ||
| 255 | let include_id = dotup | ||
| 256 | .find_group_by_name_rooted(group_id, &action.group) | ||
| 257 | .ok_or_else(|| { | ||
| 258 | Error::Custom(format!( | ||
| 259 | "group '{}' not found in include from group '{}'", | ||
| 260 | action.group, dotup.groups[group_id].name, | ||
| 261 | )) | ||
| 262 | })?; | ||
| 263 | inner_helper(dotup, cwd, home, include_id, tree, visited)?; | ||
| 264 | } | ||
| 265 | Action::Link(action) => { | ||
| 266 | let source = make_absolute_path(cwd, &action.source).into(); | ||
| 267 | let target = make_absolute_path(home, &action.target); | ||
| 268 | tree.insert(&target, action_tree::Action::Link { source }); | ||
| 269 | } | ||
| 270 | Action::Copy(action) => { | ||
| 271 | let source = make_absolute_path(cwd, &action.source).into(); | ||
| 272 | let target = make_absolute_path(home, &action.target); | ||
| 273 | tree.insert(&target, action_tree::Action::Copy { source }); | ||
| 274 | } | ||
| 275 | } | ||
| 276 | } | ||
| 277 | |||
| 278 | Ok(()) | ||
| 279 | } | ||
| 280 | |||
| 281 | let cwd = AbsPathBuf::try_from( | ||
| 282 | cwd.canonicalize() | ||
| 283 | .expect("failed to canonicalize current workind directory path"), | ||
| 284 | ) | ||
| 285 | .unwrap(); | ||
| 286 | let home = AbsPathBuf::try_from( | ||
| 287 | home.canonicalize() | ||
| 288 | .expect("failed to canonicalize home directory path"), | ||
| 289 | ) | ||
| 290 | .unwrap(); | ||
| 291 | |||
| 292 | let mut tree = action_tree::ActionTree::new(); | ||
| 293 | inner_helper( | ||
| 294 | self, | ||
| 295 | &cwd, | ||
| 296 | &home, | ||
| 297 | group_id, | ||
| 298 | &mut tree, | ||
| 299 | &mut Default::default(), | ||
| 300 | )?; | ||
| 301 | Ok(tree) | ||
| 302 | } | ||
| 303 | } | ||
| 304 | |||
| 305 | // -------------------- KeyValueParser -------------------- // | ||
| 306 | |||
| 307 | impl KeyValueParser { | ||
| 308 | fn new(location: cfg::Location, keyvalues: Vec<cfg::KeyValue>) -> Self { | ||
| 309 | Self { | ||
| 310 | location, | ||
| 311 | keyvalues, | ||
| 312 | } | ||
| 313 | } | ||
| 314 | |||
| 315 | fn get(&mut self, key: &str) -> Option<String> { | ||
| 316 | let position = self.keyvalues.iter().position(|kv| kv.key == key)?; | ||
| 317 | let keyvalue = self.keyvalues.swap_remove(position); | ||
| 318 | Some(keyvalue.value) | ||
| 319 | } | ||
| 320 | |||
| 321 | fn expect(&mut self, key: &str) -> Result<String> { | ||
| 322 | self.get(key) | ||
| 323 | .ok_or_else(|| Error::Custom(format!("expected key '{}' at {}", key, self.location))) | ||
| 324 | } | ||
| 325 | |||
| 326 | fn finalize(&mut self) -> Result<()> { | ||
| 327 | if let Some(kv) = self.keyvalues.pop() { | ||
| 328 | return Err(Error::Custom(format!( | ||
| 329 | "unexpected key '{}' at {}", | ||
| 330 | kv.key, self.location | ||
| 331 | ))); | ||
| 332 | } | ||
| 333 | Ok(()) | ||
| 334 | } | ||
| 335 | } | ||
| 336 | |||
| 337 | // -------------------- Misc -------------------- // | ||
| 338 | |||
| 339 | fn cfg_action_to_action(cfg_action: cfg::Action) -> Result<Action> { | ||
| 340 | let mut parser = KeyValueParser::new(cfg_action.location, cfg_action.keyvalues); | ||
| 341 | match cfg_action.kind.as_str() { | ||
| 342 | "include" => { | ||
| 343 | let group = parser.expect("group")?; | ||
| 344 | parser.finalize()?; | ||
| 345 | Ok(Action::Include(IncludeAction { group })) | ||
| 346 | } | ||
| 347 | "link" => { | ||
| 348 | let source = parser.expect("source")?; | ||
| 349 | let target = parser.expect("target")?; | ||
| 350 | parser.finalize()?; | ||
| 351 | Ok(Action::Link(LinkAction { | ||
| 352 | source: PathBuf::from(source), | ||
| 353 | target: PathBuf::from(target), | ||
| 354 | })) | ||
| 355 | } | ||
| 356 | "copy" => { | ||
| 357 | let source = parser.expect("source")?; | ||
| 358 | let target = parser.expect("target")?; | ||
| 359 | parser.finalize()?; | ||
| 360 | Ok(Action::Copy(CopyAction { | ||
| 361 | source: PathBuf::from(source), | ||
| 362 | target: PathBuf::from(target), | ||
| 363 | })) | ||
| 364 | } | ||
| 365 | _ => Err(Error::Custom(format!( | ||
| 366 | "unknown action '{}' at {}", | ||
| 367 | cfg_action.kind, cfg_action.location | ||
| 368 | ))), | ||
| 369 | } | ||
| 370 | } | ||
| 371 | |||
| 372 | /// Returns `path` if it is already absolute. | ||
| 373 | /// Otherwise makes it absolute by prepending `self.root`. | ||
| 374 | fn make_absolute_path(root: &AbsPath, path: &Path) -> AbsPathBuf { | ||
| 375 | if path.is_absolute() { | ||
| 376 | AbsPathBuf::try_from(path).unwrap() | ||
| 377 | } else { | ||
| 378 | AbsPathBuf::from_rel(root, TryFrom::try_from(path).unwrap()) | ||
| 379 | } | ||
| 380 | } | ||
diff --git a/src/dotup/paths.rs b/src/dotup/paths.rs new file mode 100644 index 0000000..03a80be --- /dev/null +++ b/src/dotup/paths.rs | |||
| @@ -0,0 +1,365 @@ | |||
| 1 | use std::{ | ||
| 2 | borrow::Borrow, | ||
| 3 | convert::TryFrom, | ||
| 4 | ffi::OsStr, | ||
| 5 | ops::Deref, | ||
| 6 | path::{Component, Components, Display, Path, PathBuf}, | ||
| 7 | }; | ||
| 8 | |||
| 9 | use thiserror::Error; | ||
| 10 | |||
| 11 | #[derive(Debug, Error)] | ||
| 12 | #[error("invalid relative path")] | ||
| 13 | pub struct InvalidRelPath; | ||
| 14 | |||
| 15 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] | ||
| 16 | pub struct RelPathBuf(PathBuf); | ||
| 17 | |||
| 18 | #[derive(Debug, PartialEq, Eq, Hash)] | ||
| 19 | #[repr(transparent)] | ||
| 20 | pub struct RelPath(Path); | ||
| 21 | |||
| 22 | #[derive(Debug, Error)] | ||
| 23 | #[error("invalid absolute path")] | ||
| 24 | pub struct InvalidAbsPath; | ||
| 25 | |||
| 26 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] | ||
| 27 | pub struct AbsPathBuf(PathBuf); | ||
| 28 | |||
| 29 | #[derive(Debug, PartialEq, Eq, Hash)] | ||
| 30 | #[repr(transparent)] | ||
| 31 | pub struct AbsPath(Path); | ||
| 32 | |||
| 33 | pub struct AbsComponents<'p> { | ||
| 34 | inner: std::path::Components<'p>, | ||
| 35 | } | ||
| 36 | |||
| 37 | impl<'p> Iterator for AbsComponents<'p> { | ||
| 38 | type Item = &'p OsStr; | ||
| 39 | |||
| 40 | fn next(&mut self) -> Option<Self::Item> { | ||
| 41 | loop { | ||
| 42 | match self.inner.next()? { | ||
| 43 | Component::RootDir => continue, | ||
| 44 | Component::Normal(p) => break Some(p), | ||
| 45 | _ => unreachable!(), | ||
| 46 | } | ||
| 47 | } | ||
| 48 | } | ||
| 49 | } | ||
| 50 | |||
| 51 | // -------------------- RelPathBuf -------------------- // | ||
| 52 | |||
| 53 | impl From<RelPathBuf> for PathBuf { | ||
| 54 | fn from(path: RelPathBuf) -> Self { | ||
| 55 | path.0 | ||
| 56 | } | ||
| 57 | } | ||
| 58 | |||
| 59 | impl AsRef<Path> for RelPathBuf { | ||
| 60 | fn as_ref(&self) -> &Path { | ||
| 61 | self.0.as_ref() | ||
| 62 | } | ||
| 63 | } | ||
| 64 | |||
| 65 | impl Deref for RelPathBuf { | ||
| 66 | type Target = RelPath; | ||
| 67 | |||
| 68 | fn deref(&self) -> &Self::Target { | ||
| 69 | TryFrom::try_from(self.0.as_path()).unwrap() | ||
| 70 | } | ||
| 71 | } | ||
| 72 | |||
| 73 | impl TryFrom<&Path> for RelPathBuf { | ||
| 74 | type Error = InvalidRelPath; | ||
| 75 | |||
| 76 | fn try_from(path: &Path) -> Result<Self, Self::Error> { | ||
| 77 | if path.is_relative() { | ||
| 78 | Ok(Self(path.to_owned())) | ||
| 79 | } else { | ||
| 80 | Err(InvalidRelPath) | ||
| 81 | } | ||
| 82 | } | ||
| 83 | } | ||
| 84 | |||
| 85 | impl Borrow<RelPath> for RelPathBuf { | ||
| 86 | fn borrow(&self) -> &RelPath { | ||
| 87 | self.deref() | ||
| 88 | } | ||
| 89 | } | ||
| 90 | |||
| 91 | impl RelPathBuf {} | ||
| 92 | |||
| 93 | // -------------------- RelPath -------------------- // | ||
| 94 | |||
| 95 | impl ToOwned for RelPath { | ||
| 96 | type Owned = RelPathBuf; | ||
| 97 | |||
| 98 | fn to_owned(&self) -> Self::Owned { | ||
| 99 | RelPathBuf(self.0.to_owned()) | ||
| 100 | } | ||
| 101 | } | ||
| 102 | |||
| 103 | impl<'p> AsRef<Path> for &'p RelPath { | ||
| 104 | fn as_ref(&self) -> &Path { | ||
| 105 | self.0.as_ref() | ||
| 106 | } | ||
| 107 | } | ||
| 108 | |||
| 109 | impl<'p> TryFrom<&'p Path> for &'p RelPath { | ||
| 110 | type Error = InvalidRelPath; | ||
| 111 | |||
| 112 | fn try_from(value: &'p Path) -> Result<Self, Self::Error> { | ||
| 113 | if value.is_relative() { | ||
| 114 | Ok(unsafe { std::mem::transmute(value) }) | ||
| 115 | } else { | ||
| 116 | Err(InvalidRelPath) | ||
| 117 | } | ||
| 118 | } | ||
| 119 | } | ||
| 120 | |||
| 121 | impl RelPath { | ||
| 122 | pub fn components(&self) -> Components { | ||
| 123 | self.0.components() | ||
| 124 | } | ||
| 125 | |||
| 126 | pub fn display(&self) -> Display { | ||
| 127 | self.0.display() | ||
| 128 | } | ||
| 129 | } | ||
| 130 | |||
| 131 | // -------------------- AbsPathBuf -------------------- // | ||
| 132 | |||
| 133 | impl Default for AbsPathBuf { | ||
| 134 | fn default() -> Self { | ||
| 135 | Self(PathBuf::from("/")) | ||
| 136 | } | ||
| 137 | } | ||
| 138 | |||
| 139 | impl From<AbsPathBuf> for PathBuf { | ||
| 140 | fn from(p: AbsPathBuf) -> Self { | ||
| 141 | p.0 | ||
| 142 | } | ||
| 143 | } | ||
| 144 | |||
| 145 | impl AsRef<Path> for AbsPathBuf { | ||
| 146 | fn as_ref(&self) -> &Path { | ||
| 147 | self.0.as_ref() | ||
| 148 | } | ||
| 149 | } | ||
| 150 | |||
| 151 | impl AsRef<AbsPath> for AbsPathBuf { | ||
| 152 | fn as_ref(&self) -> &AbsPath { | ||
| 153 | AbsPath::new(&self.0) | ||
| 154 | } | ||
| 155 | } | ||
| 156 | |||
| 157 | impl Deref for AbsPathBuf { | ||
| 158 | type Target = AbsPath; | ||
| 159 | |||
| 160 | fn deref(&self) -> &Self::Target { | ||
| 161 | TryFrom::try_from(self.0.as_path()).unwrap() | ||
| 162 | } | ||
| 163 | } | ||
| 164 | |||
| 165 | impl TryFrom<&Path> for AbsPathBuf { | ||
| 166 | type Error = InvalidAbsPath; | ||
| 167 | |||
| 168 | fn try_from(path: &Path) -> Result<Self, Self::Error> { | ||
| 169 | if path.is_absolute() { | ||
| 170 | Ok(Self(path.to_owned())) | ||
| 171 | } else { | ||
| 172 | Err(InvalidAbsPath) | ||
| 173 | } | ||
| 174 | } | ||
| 175 | } | ||
| 176 | |||
| 177 | impl TryFrom<PathBuf> for AbsPathBuf { | ||
| 178 | type Error = InvalidAbsPath; | ||
| 179 | |||
| 180 | fn try_from(path: PathBuf) -> Result<Self, Self::Error> { | ||
| 181 | if path.is_absolute() { | ||
| 182 | Ok(Self(path)) | ||
| 183 | } else { | ||
| 184 | Err(InvalidAbsPath) | ||
| 185 | } | ||
| 186 | } | ||
| 187 | } | ||
| 188 | |||
| 189 | impl Borrow<AbsPath> for AbsPathBuf { | ||
| 190 | fn borrow(&self) -> &AbsPath { | ||
| 191 | self.deref() | ||
| 192 | } | ||
| 193 | } | ||
| 194 | |||
| 195 | impl AbsPathBuf { | ||
| 196 | pub fn from_rel(root: &AbsPath, rel: &RelPath) -> Self { | ||
| 197 | let p = weakly_canonical_cwd(rel, root.0.to_path_buf()); | ||
| 198 | Self::try_from(p).unwrap() | ||
| 199 | } | ||
| 200 | |||
| 201 | pub fn as_path(&self) -> &AbsPath { | ||
| 202 | TryFrom::try_from(self.0.as_path()).unwrap() | ||
| 203 | } | ||
| 204 | } | ||
| 205 | |||
| 206 | // -------------------- AbsPath -------------------- // | ||
| 207 | |||
| 208 | impl<'p> AsRef<Path> for &'p AbsPath { | ||
| 209 | fn as_ref(&self) -> &'p Path { | ||
| 210 | self.0.as_ref() | ||
| 211 | } | ||
| 212 | } | ||
| 213 | |||
| 214 | impl ToOwned for AbsPath { | ||
| 215 | type Owned = AbsPathBuf; | ||
| 216 | |||
| 217 | fn to_owned(&self) -> Self::Owned { | ||
| 218 | AbsPathBuf(self.0.to_owned()) | ||
| 219 | } | ||
| 220 | } | ||
| 221 | |||
| 222 | impl<'p> Default for &'p AbsPath { | ||
| 223 | fn default() -> Self { | ||
| 224 | Self::try_from(Path::new("/")).unwrap() | ||
| 225 | } | ||
| 226 | } | ||
| 227 | |||
| 228 | impl<'p> TryFrom<&'p Path> for &'p AbsPath { | ||
| 229 | type Error = InvalidAbsPath; | ||
| 230 | |||
| 231 | fn try_from(value: &'p Path) -> Result<Self, Self::Error> { | ||
| 232 | if value.is_absolute() { | ||
| 233 | Ok(unsafe { std::mem::transmute(value) }) | ||
| 234 | } else { | ||
| 235 | Err(InvalidAbsPath) | ||
| 236 | } | ||
| 237 | } | ||
| 238 | } | ||
| 239 | |||
| 240 | impl<'p> TryFrom<&'p str> for &'p AbsPath { | ||
| 241 | type Error = InvalidAbsPath; | ||
| 242 | |||
| 243 | fn try_from(value: &'p str) -> Result<Self, Self::Error> { | ||
| 244 | TryFrom::try_from(Path::new(value)) | ||
| 245 | } | ||
| 246 | } | ||
| 247 | |||
| 248 | impl AbsPath { | ||
| 249 | pub fn new(path: &Path) -> &Self { | ||
| 250 | TryFrom::try_from(path).unwrap() | ||
| 251 | } | ||
| 252 | |||
| 253 | pub fn join(&self, other: impl AsRef<Path>) -> AbsPathBuf { | ||
| 254 | AbsPathBuf::try_from(weakly_canonical_cwd(other, self.0.to_path_buf())).unwrap() | ||
| 255 | } | ||
| 256 | |||
| 257 | pub fn parent(&self) -> Option<&AbsPath> { | ||
| 258 | self.0.parent().map(|p| TryFrom::try_from(p).unwrap()) | ||
| 259 | } | ||
| 260 | |||
| 261 | pub fn components(&self) -> AbsComponents { | ||
| 262 | AbsComponents { | ||
| 263 | inner: self.0.components(), | ||
| 264 | } | ||
| 265 | } | ||
| 266 | |||
| 267 | pub fn display(&self) -> Display { | ||
| 268 | self.0.display() | ||
| 269 | } | ||
| 270 | } | ||
| 271 | |||
| 272 | // -------------------- Utils -------------------- // | ||
| 273 | |||
| 274 | pub fn current_working_directory() -> PathBuf { | ||
| 275 | std::env::current_dir().expect("Failed to obtain current working directory") | ||
| 276 | } | ||
| 277 | |||
| 278 | pub fn weakly_canonical(path: impl AsRef<Path>) -> PathBuf { | ||
| 279 | let cwd = current_working_directory(); | ||
| 280 | weakly_canonical_cwd(path, cwd) | ||
| 281 | } | ||
| 282 | |||
| 283 | pub fn weakly_canonical_cwd(path: impl AsRef<Path>, cwd: PathBuf) -> PathBuf { | ||
| 284 | // Adapated from | ||
| 285 | // https://github.com/rust-lang/cargo/blob/fede83ccf973457de319ba6fa0e36ead454d2e20/src/cargo/util/paths.rs#L61 | ||
| 286 | let path = path.as_ref(); | ||
| 287 | |||
| 288 | let mut components = path.components().peekable(); | ||
| 289 | let mut canonical = cwd; | ||
| 290 | let prefix = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() { | ||
| 291 | components.next(); | ||
| 292 | PathBuf::from(c.as_os_str()) | ||
| 293 | } else { | ||
| 294 | PathBuf::new() | ||
| 295 | }; | ||
| 296 | |||
| 297 | for component in components { | ||
| 298 | match component { | ||
| 299 | Component::Prefix(_) => unreachable!(), | ||
| 300 | Component::RootDir => { | ||
| 301 | canonical = prefix.clone(); | ||
| 302 | canonical.push(component.as_os_str()) | ||
| 303 | } | ||
| 304 | Component::CurDir => {} | ||
| 305 | Component::ParentDir => { | ||
| 306 | canonical.pop(); | ||
| 307 | } | ||
| 308 | Component::Normal(p) => canonical.push(p), | ||
| 309 | }; | ||
| 310 | } | ||
| 311 | |||
| 312 | canonical | ||
| 313 | } | ||
| 314 | |||
| 315 | pub fn ends_with_slash(path: impl AsRef<Path>) -> bool { | ||
| 316 | path.as_ref() | ||
| 317 | .to_str() | ||
| 318 | .map(|s| s.ends_with('/')) | ||
| 319 | .unwrap_or_default() | ||
| 320 | } | ||
| 321 | |||
| 322 | #[cfg(test)] | ||
| 323 | mod tests { | ||
| 324 | use super::*; | ||
| 325 | |||
| 326 | #[test] | ||
| 327 | fn test_weakly_canonical() { | ||
| 328 | let cwd = PathBuf::from("/home/user"); | ||
| 329 | assert_eq!( | ||
| 330 | PathBuf::from("/home/dest"), | ||
| 331 | weakly_canonical_cwd("../dest", cwd.clone()) | ||
| 332 | ); | ||
| 333 | assert_eq!( | ||
| 334 | PathBuf::from("/home/dest/configs/init.vim"), | ||
| 335 | weakly_canonical_cwd("../dest/configs/init.vim", cwd.clone()) | ||
| 336 | ); | ||
| 337 | assert_eq!( | ||
| 338 | PathBuf::from("/dest/configs/init.vim"), | ||
| 339 | weakly_canonical_cwd("/dest/configs/init.vim", cwd.clone()) | ||
| 340 | ); | ||
| 341 | assert_eq!( | ||
| 342 | PathBuf::from("/home/user/configs/nvim/lua/setup.lua"), | ||
| 343 | weakly_canonical_cwd("./configs/nvim/lua/setup.lua", cwd.clone()) | ||
| 344 | ); | ||
| 345 | assert_eq!( | ||
| 346 | PathBuf::from("/home/user/configs/nvim/lua/setup.lua"), | ||
| 347 | weakly_canonical_cwd("configs/nvim/lua/setup.lua", cwd) | ||
| 348 | ); | ||
| 349 | } | ||
| 350 | |||
| 351 | #[test] | ||
| 352 | fn test_path_ends_with_slash() { | ||
| 353 | assert!(!ends_with_slash("")); | ||
| 354 | assert!(!ends_with_slash("/f1")); | ||
| 355 | assert!(!ends_with_slash("/f1/f2")); | ||
| 356 | assert!(!ends_with_slash("./f1/f2")); | ||
| 357 | assert!(!ends_with_slash("./f1/f2/../f3")); | ||
| 358 | |||
| 359 | assert!(ends_with_slash("/")); | ||
| 360 | assert!(ends_with_slash("/f1/")); | ||
| 361 | assert!(ends_with_slash("f1/")); | ||
| 362 | assert!(ends_with_slash("f1/f2/")); | ||
| 363 | assert!(ends_with_slash("f1/f2/../f3/")); | ||
| 364 | } | ||
| 365 | } | ||
diff --git a/src/main.rs b/src/main.rs index a56238e..80a03b9 100644 --- a/src/main.rs +++ b/src/main.rs | |||
| @@ -1,226 +1,125 @@ | |||
| 1 | #![feature(try_blocks)] | 1 | #![feature(drain_filter)] |
| 2 | 2 | ||
| 3 | mod depot; | 3 | //pub mod config; |
| 4 | mod dotup; | 4 | pub mod dotup; |
| 5 | mod utils; | ||
| 6 | 5 | ||
| 7 | use std::path::PathBuf; | 6 | use std::path::PathBuf; |
| 8 | 7 | ||
| 9 | use clap::Parser; | 8 | use anyhow::Context; |
| 10 | use flexi_logger::Logger; | 9 | use clap::{Parser, Subcommand}; |
| 11 | use utils::DEFAULT_DEPOT_FILE_NAME; | ||
| 12 | 10 | ||
| 13 | #[derive(Parser, Debug)] | 11 | #[derive(Parser, Debug)] |
| 14 | pub struct Flags { | 12 | struct GlobalFlags { |
| 15 | /// Path to the depot file, default to `.depot`. | ||
| 16 | #[clap(long)] | 13 | #[clap(long)] |
| 17 | depot: Option<PathBuf>, | 14 | base: Option<PathBuf>, |
| 18 | 15 | ||
| 19 | /// Path to the install base, defaults to the home directory. | 16 | #[clap(long, default_value = "./dotup")] |
| 20 | #[clap(long)] | 17 | config: PathBuf, |
| 21 | install_base: Option<PathBuf>, | ||
| 22 | } | ||
| 23 | |||
| 24 | #[derive(Parser, Debug)] | ||
| 25 | #[clap(author, version, about, long_about = None)] | ||
| 26 | struct Args { | ||
| 27 | /// A level of verbosity, and can be used multiple times | ||
| 28 | /// | ||
| 29 | /// Level 1 - Info | ||
| 30 | /// | ||
| 31 | /// Level 2 - Debug | ||
| 32 | /// | ||
| 33 | /// Level 3 - Trace | ||
| 34 | #[clap(short, long, parse(from_occurrences))] | ||
| 35 | verbose: i32, | ||
| 36 | |||
| 37 | #[clap(flatten)] | ||
| 38 | flags: Flags, | ||
| 39 | |||
| 40 | #[clap(subcommand)] | ||
| 41 | command: SubCommand, | ||
| 42 | } | 18 | } |
| 43 | 19 | ||
| 44 | #[derive(Parser, Debug)] | 20 | #[derive(Subcommand, Debug)] |
| 45 | enum SubCommand { | 21 | enum SubCommand { |
| 46 | Init(InitArgs), | ||
| 47 | Link(LinkArgs), | ||
| 48 | Unlink(UnlinkArgs), | ||
| 49 | Install(InstallArgs), | 22 | Install(InstallArgs), |
| 50 | Uninstall(UninstallArgs), | 23 | Uninstall(UninstallArgs), |
| 51 | Mv(MvArgs), | ||
| 52 | Status(StatusArgs), | 24 | Status(StatusArgs), |
| 25 | Format(FormatArgs), | ||
| 53 | } | 26 | } |
| 54 | 27 | ||
| 55 | fn main() -> anyhow::Result<()> { | ||
| 56 | let args = Args::parse(); | ||
| 57 | |||
| 58 | let log_level = match args.verbose { | ||
| 59 | 0 => "warn", | ||
| 60 | 1 => "info", | ||
| 61 | 2 => "debug", | ||
| 62 | _ => "trace", | ||
| 63 | }; | ||
| 64 | |||
| 65 | Logger::try_with_env_or_str(log_level)? | ||
| 66 | .format(flexi_logger::colored_default_format) | ||
| 67 | .set_palette("196;208;32;198;15".to_string()) | ||
| 68 | .start()?; | ||
| 69 | |||
| 70 | match args.command { | ||
| 71 | SubCommand::Init(cmd_args) => command_init(args.flags, cmd_args), | ||
| 72 | SubCommand::Link(cmd_args) => command_link(args.flags, cmd_args), | ||
| 73 | SubCommand::Unlink(cmd_args) => command_unlink(args.flags, cmd_args), | ||
| 74 | SubCommand::Install(cmd_args) => command_install(args.flags, cmd_args), | ||
| 75 | SubCommand::Uninstall(cmd_args) => command_uninstall(args.flags, cmd_args), | ||
| 76 | SubCommand::Mv(cmd_args) => command_mv(args.flags, cmd_args), | ||
| 77 | SubCommand::Status(cmd_args) => command_status(args.flags, cmd_args), | ||
| 78 | } | ||
| 79 | } | ||
| 80 | |||
| 81 | /// Creates an empty depot file if one doesnt already exist. | ||
| 82 | /// | ||
| 83 | /// By default this will create the file in the current directory | ||
| 84 | /// but the `path` option can be used to change this path. | ||
| 85 | #[derive(Parser, Debug)] | 28 | #[derive(Parser, Debug)] |
| 86 | struct InitArgs { | 29 | struct InstallArgs { |
| 87 | path: Option<PathBuf>, | 30 | groups: Vec<String>, |
| 88 | } | ||
| 89 | |||
| 90 | fn command_init(_global_flags: Flags, args: InitArgs) -> anyhow::Result<()> { | ||
| 91 | let depot_path = { | ||
| 92 | let mut path = args.path.unwrap_or_else(utils::default_depot_path); | ||
| 93 | if path.is_dir() { | ||
| 94 | path = path.join(DEFAULT_DEPOT_FILE_NAME); | ||
| 95 | } | ||
| 96 | path | ||
| 97 | }; | ||
| 98 | |||
| 99 | if depot_path.exists() { | ||
| 100 | println!("Depot at {} already exists", depot_path.display()); | ||
| 101 | } else { | ||
| 102 | depot::write(&depot_path, &Default::default())?; | ||
| 103 | println!("Depot initialized at {}", depot_path.display()); | ||
| 104 | } | ||
| 105 | |||
| 106 | Ok(()) | ||
| 107 | } | 31 | } |
| 108 | 32 | ||
| 109 | /// Creates links | ||
| 110 | /// | ||
| 111 | /// If a link is created for a file that already had a link then the old link will be overwritten. | ||
| 112 | /// By default creating a link to a directory will recursively link all files under that | ||
| 113 | /// directory, to actually link a directory use the --directory flag. | ||
| 114 | #[derive(Parser, Debug)] | 33 | #[derive(Parser, Debug)] |
| 115 | struct LinkArgs { | 34 | struct UninstallArgs { |
| 116 | #[clap(long)] | 35 | groups: Vec<String>, |
| 117 | directory: bool, | ||
| 118 | |||
| 119 | #[clap(min_values = 1)] | ||
| 120 | origins: Vec<PathBuf>, | ||
| 121 | |||
| 122 | destination: PathBuf, | ||
| 123 | } | ||
| 124 | |||
| 125 | fn command_link(global_flags: Flags, args: LinkArgs) -> anyhow::Result<()> { | ||
| 126 | let mut dotup = utils::read_dotup(&global_flags)?; | ||
| 127 | for origin in args.origins { | ||
| 128 | if !args.directory && origin.is_dir() { | ||
| 129 | let directory = origin; | ||
| 130 | let origins = utils::collect_files_in_dir_recursive(&directory)?; | ||
| 131 | for origin in origins { | ||
| 132 | // unwrap: origin is under directory so stripping should not fail | ||
| 133 | let path_extra = origin.strip_prefix(&directory).unwrap(); | ||
| 134 | let destination = args.destination.join(path_extra); | ||
| 135 | dotup.link(&origin, &destination); | ||
| 136 | } | ||
| 137 | } else { | ||
| 138 | dotup.link(&origin, &args.destination); | ||
| 139 | }; | ||
| 140 | } | ||
| 141 | utils::write_dotup(&dotup)?; | ||
| 142 | Ok(()) | ||
| 143 | } | 36 | } |
| 144 | 37 | ||
| 145 | /// Unlinks files/directories. | ||
| 146 | /// | ||
| 147 | /// This will recursively remove links. If a path is a directory then it will remove all links | ||
| 148 | /// recursively. | ||
| 149 | /// The links are not uninstall by default, see the --uninstall parameter. | ||
| 150 | #[derive(Parser, Debug)] | 38 | #[derive(Parser, Debug)] |
| 151 | struct UnlinkArgs { | 39 | struct StatusArgs { |
| 152 | #[clap(long)] | 40 | groups: Vec<String>, |
| 153 | uninstall: bool, | ||
| 154 | |||
| 155 | paths: Vec<PathBuf>, | ||
| 156 | } | ||
| 157 | |||
| 158 | fn command_unlink(global_flags: Flags, args: UnlinkArgs) -> anyhow::Result<()> { | ||
| 159 | let mut dotup = utils::read_dotup(&global_flags)?; | ||
| 160 | dotup.unlink(args.paths.into_iter(), args.uninstall); | ||
| 161 | utils::write_dotup(&dotup)?; | ||
| 162 | Ok(()) | ||
| 163 | } | 41 | } |
| 164 | 42 | ||
| 165 | /// Install links. (Creates symlinks). | ||
| 166 | /// | ||
| 167 | /// Installing a link will create the necessary directories. | ||
| 168 | /// If a file or directory already exists at the location a link would be installed this command will fail. | ||
| 169 | #[derive(Parser, Debug)] | 43 | #[derive(Parser, Debug)] |
| 170 | struct InstallArgs { | 44 | struct FormatArgs {} |
| 171 | #[clap(long)] | ||
| 172 | directory: bool, | ||
| 173 | 45 | ||
| 174 | paths: Vec<PathBuf>, | 46 | #[derive(Parser, Debug)] |
| 47 | struct Args { | ||
| 48 | #[clap(flatten)] | ||
| 49 | globals: GlobalFlags, | ||
| 50 | #[clap(subcommand)] | ||
| 51 | command: SubCommand, | ||
| 175 | } | 52 | } |
| 176 | 53 | ||
| 177 | fn command_install(global_flags: Flags, args: InstallArgs) -> anyhow::Result<()> { | 54 | fn main() -> anyhow::Result<()> { |
| 178 | let dotup = utils::read_dotup(&global_flags)?; | 55 | env_logger::init(); |
| 179 | dotup.install(args.paths.into_iter()); | ||
| 180 | Ok(()) | ||
| 181 | } | ||
| 182 | 56 | ||
| 183 | /// Uninstalls links. (Removes symlinks). | 57 | let args = Args::parse(); |
| 184 | /// | 58 | match args.command { |
| 185 | /// Uninstalling a link for a file that didn't have a link will do nothing. | 59 | SubCommand::Install(install) => command_install(args.globals, install), |
| 186 | /// Uninstalling a directory will recursively uninstall all files under it. | 60 | SubCommand::Uninstall(uninstall) => command_uninstall(args.globals, uninstall), |
| 187 | /// Symlinks are only deleted if they were pointing to the correct file. | 61 | SubCommand::Status(status) => command_status(args.globals, status), |
| 188 | #[derive(Parser, Debug)] | 62 | SubCommand::Format(format) => command_format(args.globals, format), |
| 189 | struct UninstallArgs { | 63 | } |
| 190 | paths: Vec<PathBuf>, | ||
| 191 | } | 64 | } |
| 192 | 65 | ||
| 193 | fn command_uninstall(global_flags: Flags, args: UninstallArgs) -> anyhow::Result<()> { | 66 | impl GlobalFlags { |
| 194 | let dotup = utils::read_dotup(&global_flags)?; | 67 | fn base_path_or_default(&self) -> PathBuf { |
| 195 | dotup.uninstall(args.paths.into_iter()); | 68 | self.base.clone().unwrap_or_else(|| { |
| 196 | Ok(()) | 69 | PathBuf::from(std::env::var("HOME").expect("failed to get HOME directory")) |
| 70 | }) | ||
| 71 | } | ||
| 197 | } | 72 | } |
| 198 | 73 | ||
| 199 | /// Moves files/directories and updates links. | 74 | fn command_install(globals: GlobalFlags, args: InstallArgs) -> anyhow::Result<()> { |
| 200 | #[derive(Parser, Debug)] | 75 | let dotup = dotup::load_file(&globals.config).context("failed to parse config")?; |
| 201 | struct MvArgs { | 76 | let cwd = std::env::current_dir().context("failed to get current directory")?; |
| 202 | #[clap(min_values = 1)] | 77 | let install_params = dotup::InstallParams { |
| 203 | origins: Vec<PathBuf>, | 78 | cwd: &cwd, |
| 204 | 79 | home: &globals.base_path_or_default(), | |
| 205 | destination: PathBuf, | 80 | }; |
| 81 | for group in args.groups { | ||
| 82 | match dotup.find_group_by_name(&group) { | ||
| 83 | Some(group_id) => dotup.install(install_params, group_id)?, | ||
| 84 | None => log::error!("group not found: {}", group), | ||
| 85 | }; | ||
| 86 | } | ||
| 87 | Ok(()) | ||
| 206 | } | 88 | } |
| 207 | 89 | ||
| 208 | fn command_mv(global_flags: Flags, args: MvArgs) -> anyhow::Result<()> { | 90 | fn command_uninstall(globals: GlobalFlags, args: UninstallArgs) -> anyhow::Result<()> { |
| 209 | let mut dotup = utils::read_dotup(&global_flags)?; | 91 | let dotup = dotup::load_file(&globals.config).context("failed to parse config")?; |
| 210 | dotup.mv(args.origins.into_iter(), args.destination); | 92 | let cwd = std::env::current_dir().context("failed to get current directory")?; |
| 211 | utils::write_dotup(&dotup)?; | 93 | let uninstall_params = dotup::UninstallParams { |
| 94 | cwd: &cwd, | ||
| 95 | home: &globals.base_path_or_default(), | ||
| 96 | }; | ||
| 97 | for group in args.groups { | ||
| 98 | match dotup.find_group_by_name(&group) { | ||
| 99 | Some(group_id) => dotup.uninstall(uninstall_params, group_id)?, | ||
| 100 | None => log::error!("group not found: {}", group), | ||
| 101 | }; | ||
| 102 | } | ||
| 212 | Ok(()) | 103 | Ok(()) |
| 213 | } | 104 | } |
| 214 | 105 | ||
| 215 | /// Shows information about links | 106 | fn command_status(globals: GlobalFlags, args: StatusArgs) -> anyhow::Result<()> { |
| 216 | #[derive(Parser, Debug)] | 107 | let dotup = dotup::load_file(&globals.config).context("failed to parse config")?; |
| 217 | struct StatusArgs { | 108 | let cwd = std::env::current_dir().context("failed to get current directory")?; |
| 218 | #[clap(default_value = ".")] | 109 | let install_params = dotup::InstallParams { |
| 219 | paths: Vec<PathBuf>, | 110 | cwd: &cwd, |
| 111 | home: &globals.base_path_or_default(), | ||
| 112 | }; | ||
| 113 | for group in args.groups { | ||
| 114 | match dotup.find_group_by_name(&group) { | ||
| 115 | Some(group_id) => dotup.status(install_params, group_id)?, | ||
| 116 | None => log::error!("group not found: {}", group), | ||
| 117 | }; | ||
| 118 | } | ||
| 119 | Ok(()) | ||
| 220 | } | 120 | } |
| 221 | 121 | ||
| 222 | fn command_status(global_flags: Flags, args: StatusArgs) -> anyhow::Result<()> { | 122 | fn command_format(globals: GlobalFlags, _args: FormatArgs) -> anyhow::Result<()> { |
| 223 | let dotup = utils::read_dotup(&global_flags)?; | 123 | dotup::format_file_inplace(&globals.config).context("failed to format config")?; |
| 224 | dotup.status(args.paths.into_iter()); | ||
| 225 | Ok(()) | 124 | Ok(()) |
| 226 | } | 125 | } |
diff --git a/src/utils.rs b/src/utils.rs deleted file mode 100644 index 5abb491..0000000 --- a/src/utils.rs +++ /dev/null | |||
| @@ -1,178 +0,0 @@ | |||
| 1 | use std::{ | ||
| 2 | collections::VecDeque, | ||
| 3 | path::{Component, Path, PathBuf}, | ||
| 4 | }; | ||
| 5 | |||
| 6 | use crate::{ | ||
| 7 | dotup::{self, Dotup}, | ||
| 8 | Flags, | ||
| 9 | }; | ||
| 10 | |||
| 11 | pub const DEFAULT_DEPOT_FILE_NAME: &str = ".depot"; | ||
| 12 | |||
| 13 | /// Returns a list of canonical paths to all the files in `dir`. This includes files in | ||
| 14 | /// subdirectories. | ||
| 15 | /// Fails if dir isnt a directory or if there is some other io error. | ||
| 16 | pub fn collect_files_in_dir_recursive(dir: impl Into<PathBuf>) -> anyhow::Result<Vec<PathBuf>> { | ||
| 17 | let mut paths = Vec::new(); | ||
| 18 | let mut dirs = VecDeque::new(); | ||
| 19 | dirs.push_back(dir.into()); | ||
| 20 | |||
| 21 | while let Some(dir) = dirs.pop_front() { | ||
| 22 | for entry in std::fs::read_dir(dir)? { | ||
| 23 | let entry = entry?; | ||
| 24 | let filetype = entry.file_type()?; | ||
| 25 | if filetype.is_dir() { | ||
| 26 | dirs.push_back(entry.path()); | ||
| 27 | } else { | ||
| 28 | paths.push(entry.path()); | ||
| 29 | } | ||
| 30 | } | ||
| 31 | } | ||
| 32 | |||
| 33 | Ok(paths) | ||
| 34 | } | ||
| 35 | |||
| 36 | pub fn collect_paths_in_dir(dir: impl AsRef<Path>) -> anyhow::Result<Vec<PathBuf>> { | ||
| 37 | Ok(std::fs::read_dir(dir)? | ||
| 38 | .filter_map(|e| e.ok()) | ||
| 39 | .map(|e| e.path()) | ||
| 40 | .collect()) | ||
| 41 | } | ||
| 42 | |||
| 43 | pub fn read_dotup(flags: &Flags) -> anyhow::Result<Dotup> { | ||
| 44 | let depot_path = depot_path_from_flags(flags)?; | ||
| 45 | let install_base = install_base_from_flags(flags); | ||
| 46 | dotup::read(depot_path, install_base) | ||
| 47 | } | ||
| 48 | |||
| 49 | pub fn write_dotup(dotup: &Dotup) -> anyhow::Result<()> { | ||
| 50 | dotup::write(dotup) | ||
| 51 | } | ||
| 52 | |||
| 53 | pub fn depot_path_from_flags(flags: &Flags) -> anyhow::Result<PathBuf> { | ||
| 54 | match flags.depot { | ||
| 55 | Some(ref path) => Ok(path.clone()), | ||
| 56 | None => find_depot_path().ok_or_else(|| anyhow::anyhow!("Failed to find depot path")), | ||
| 57 | } | ||
| 58 | } | ||
| 59 | |||
| 60 | pub fn default_depot_path() -> PathBuf { | ||
| 61 | current_working_directory().join(DEFAULT_DEPOT_FILE_NAME) | ||
| 62 | } | ||
| 63 | |||
| 64 | pub fn find_depot_path() -> Option<PathBuf> { | ||
| 65 | let mut cwd = current_working_directory(); | ||
| 66 | loop { | ||
| 67 | let path = cwd.join(DEFAULT_DEPOT_FILE_NAME); | ||
| 68 | if path.exists() { | ||
| 69 | break Some(path); | ||
| 70 | } | ||
| 71 | if !cwd.pop() { | ||
| 72 | break None; | ||
| 73 | } | ||
| 74 | } | ||
| 75 | } | ||
| 76 | |||
| 77 | pub fn install_base_from_flags(flags: &Flags) -> PathBuf { | ||
| 78 | match flags.install_base { | ||
| 79 | Some(ref path) => path.clone(), | ||
| 80 | None => default_install_base(), | ||
| 81 | } | ||
| 82 | } | ||
| 83 | |||
| 84 | pub fn default_install_base() -> PathBuf { | ||
| 85 | PathBuf::from(std::env::var("HOME").expect("Failed to obtain HOME environment variable")) | ||
| 86 | } | ||
| 87 | pub fn weakly_canonical(path: impl AsRef<Path>) -> PathBuf { | ||
| 88 | let cwd = current_working_directory(); | ||
| 89 | weakly_canonical_cwd(path, cwd) | ||
| 90 | } | ||
| 91 | |||
| 92 | fn weakly_canonical_cwd(path: impl AsRef<Path>, cwd: PathBuf) -> PathBuf { | ||
| 93 | // Adapated from | ||
| 94 | // https://github.com/rust-lang/cargo/blob/fede83ccf973457de319ba6fa0e36ead454d2e20/src/cargo/util/paths.rs#L61 | ||
| 95 | let path = path.as_ref(); | ||
| 96 | |||
| 97 | let mut components = path.components().peekable(); | ||
| 98 | let mut canonical = cwd; | ||
| 99 | let prefix = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() { | ||
| 100 | components.next(); | ||
| 101 | PathBuf::from(c.as_os_str()) | ||
| 102 | } else { | ||
| 103 | PathBuf::new() | ||
| 104 | }; | ||
| 105 | |||
| 106 | for component in components { | ||
| 107 | match component { | ||
| 108 | Component::Prefix(_) => unreachable!(), | ||
| 109 | Component::RootDir => { | ||
| 110 | canonical = prefix.clone(); | ||
| 111 | canonical.push(component.as_os_str()) | ||
| 112 | } | ||
| 113 | Component::CurDir => {} | ||
| 114 | Component::ParentDir => { | ||
| 115 | canonical.pop(); | ||
| 116 | } | ||
| 117 | Component::Normal(p) => canonical.push(p), | ||
| 118 | }; | ||
| 119 | } | ||
| 120 | |||
| 121 | canonical | ||
| 122 | } | ||
| 123 | |||
| 124 | pub fn current_working_directory() -> PathBuf { | ||
| 125 | std::env::current_dir().expect("Failed to obtain current working directory") | ||
| 126 | } | ||
| 127 | |||
| 128 | pub fn path_ends_with_slash(path: impl AsRef<Path>) -> bool { | ||
| 129 | path.as_ref() | ||
| 130 | .to_str() | ||
| 131 | .map(|s| s.ends_with('/')) | ||
| 132 | .unwrap_or_default() | ||
| 133 | } | ||
| 134 | |||
| 135 | #[cfg(test)] | ||
| 136 | mod tests { | ||
| 137 | use super::*; | ||
| 138 | |||
| 139 | #[test] | ||
| 140 | fn test_weakly_canonical() { | ||
| 141 | let cwd = PathBuf::from("/home/user"); | ||
| 142 | assert_eq!( | ||
| 143 | PathBuf::from("/home/dest"), | ||
| 144 | weakly_canonical_cwd("../dest", cwd.clone()) | ||
| 145 | ); | ||
| 146 | assert_eq!( | ||
| 147 | PathBuf::from("/home/dest/configs/init.vim"), | ||
| 148 | weakly_canonical_cwd("../dest/configs/init.vim", cwd.clone()) | ||
| 149 | ); | ||
| 150 | assert_eq!( | ||
| 151 | PathBuf::from("/dest/configs/init.vim"), | ||
| 152 | weakly_canonical_cwd("/dest/configs/init.vim", cwd.clone()) | ||
| 153 | ); | ||
| 154 | assert_eq!( | ||
| 155 | PathBuf::from("/home/user/configs/nvim/lua/setup.lua"), | ||
| 156 | weakly_canonical_cwd("./configs/nvim/lua/setup.lua", cwd.clone()) | ||
| 157 | ); | ||
| 158 | assert_eq!( | ||
| 159 | PathBuf::from("/home/user/configs/nvim/lua/setup.lua"), | ||
| 160 | weakly_canonical_cwd("configs/nvim/lua/setup.lua", cwd) | ||
| 161 | ); | ||
| 162 | } | ||
| 163 | |||
| 164 | #[test] | ||
| 165 | fn test_path_ends_with_slash() { | ||
| 166 | assert!(!path_ends_with_slash("")); | ||
| 167 | assert!(!path_ends_with_slash("/f1")); | ||
| 168 | assert!(!path_ends_with_slash("/f1/f2")); | ||
| 169 | assert!(!path_ends_with_slash("./f1/f2")); | ||
| 170 | assert!(!path_ends_with_slash("./f1/f2/../f3")); | ||
| 171 | |||
| 172 | assert!(path_ends_with_slash("/")); | ||
| 173 | assert!(path_ends_with_slash("/f1/")); | ||
| 174 | assert!(path_ends_with_slash("f1/")); | ||
| 175 | assert!(path_ends_with_slash("f1/f2/")); | ||
| 176 | assert!(path_ends_with_slash("f1/f2/../f3/")); | ||
| 177 | } | ||
| 178 | } | ||
