From e5a38bab67f790803ff98484fc5835adba7bf62a Mon Sep 17 00:00:00 2001 From: diogo464 Date: Fri, 23 Sep 2022 13:45:57 +0100 Subject: rewrite --- .gitignore | 7 + Cargo.lock | 272 +++++++-------- Cargo.toml | 18 +- README.md | 77 ----- src/config/mod.rs | 97 ++++++ src/config/parse.rs | 254 ++++++++++++++ src/config/parser.rs | 20 ++ src/depot.rs | 850 ----------------------------------------------- src/dotup.rs | 593 --------------------------------- src/dotup/action_tree.rs | 347 +++++++++++++++++++ src/dotup/cfg.rs | 352 ++++++++++++++++++++ src/dotup/mod.rs | 380 +++++++++++++++++++++ src/dotup/paths.rs | 365 ++++++++++++++++++++ src/main.rs | 265 +++++---------- src/utils.rs | 178 ---------- 15 files changed, 2042 insertions(+), 2033 deletions(-) delete mode 100644 README.md create mode 100644 src/config/mod.rs create mode 100644 src/config/parse.rs create mode 100644 src/config/parser.rs delete mode 100644 src/depot.rs delete mode 100644 src/dotup.rs create mode 100644 src/dotup/action_tree.rs create mode 100644 src/dotup/cfg.rs create mode 100644 src/dotup/mod.rs create mode 100644 src/dotup/paths.rs delete mode 100644 src/utils.rs diff --git a/.gitignore b/.gitignore index ea8c4bf..a5ff07f 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,8 @@ /target + + +# Added by cargo +# +# already existing elements were commented out + +#/target diff --git a/Cargo.lock b/Cargo.lock index f88aa27..eadaad2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,27 +4,18 @@ version = 3 [[package]] name = "aho-corasick" -version = "0.7.18" +version = "0.7.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +checksum = "b4f55bd91a0978cbfd91c457a164bab8b4001c833b7f323132c0a4e1922dd44e" dependencies = [ "memchr", ] -[[package]] -name = "ansi_term" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" -dependencies = [ - "winapi", -] - [[package]] name = "anyhow" -version = "1.0.53" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94a45b455c14666b85fc40a019e8ab9eb75e3a124e05494f5397122bc9eb06e0" +checksum = "98161a4e3e2184da77bb14f02184cdd111e83bbbcc9979dfee3c44b9a85f5602" [[package]] name = "atty" @@ -39,9 +30,9 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "bitflags" @@ -49,6 +40,21 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bstr" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" +dependencies = [ + "memchr", +] + +[[package]] +name = "bytecount" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c676a478f63e9fa2dd5368a42f28bba0d6c560b775f38583c8bbaa7fcd67c9c" + [[package]] name = "cfg-if" version = "1.0.0" @@ -57,16 +63,16 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "3.0.14" +version = "3.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b63edc3f163b3c71ec8aa23f9bd6070f77edbf3d1d198b164afa90ff00e4ec62" +checksum = "1ed5341b2301a26ab80be5cbdced622e80ed808483c52e45e3310a877d3b37d7" dependencies = [ "atty", "bitflags", "clap_derive", + "clap_lex", "indexmap", - "lazy_static", - "os_str_bytes", + "once_cell", "strsim", "termcolor", "textwrap", @@ -74,9 +80,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "3.0.14" +version = "3.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a1132dc3944b31c20dd8b906b3a9f0a5d0243e092d59171414969657ac6aa85" +checksum = "ea0c8bce528c4be4da13ea6fead8965e95b6073585a2f05204bd8f4119f82a65" dependencies = [ "heck", "proc-macro-error", @@ -85,49 +91,67 @@ dependencies = [ "syn", ] +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + [[package]] name = "dotup" -version = "0.0.0" +version = "0.1.0" dependencies = [ - "ansi_term", "anyhow", "clap", - "flexi_logger", + "env_logger", + "globset", "log", - "serde", + "nom", + "nom_locate", "slotmap", "thiserror", - "toml", ] [[package]] -name = "flexi_logger" -version = "0.22.3" +name = "env_logger" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "969940c39bc718475391e53a3a59b0157e64929c80cf83ad5dde5f770ecdc423" +checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3" dependencies = [ - "ansi_term", "atty", - "glob", - "lazy_static", + "humantime", "log", "regex", - "rustversion", - "thiserror", - "time", + "termcolor", ] [[package]] -name = "glob" -version = "0.3.0" +name = "fnv" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "globset" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a1e17342619edbc21a964c2afbeb6c820c6a2560032872f397bb97ea127bd0a" +dependencies = [ + "aho-corasick", + "bstr", + "fnv", + "log", + "regex", +] [[package]] name = "hashbrown" -version = "0.11.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "heck" @@ -144,67 +168,82 @@ dependencies = [ "libc", ] +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "indexmap" -version = "1.8.0" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223" +checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" dependencies = [ "autocfg", "hashbrown", ] -[[package]] -name = "itoa" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" - -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - [[package]] name = "libc" -version = "0.2.117" +version = "0.2.132" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e74d72e0f9b65b5b4ca49a346af3976df0f9c61d550727f349ecd559f251a26c" +checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" [[package]] name = "log" -version = "0.4.14" +version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" dependencies = [ "cfg-if", ] [[package]] name = "memchr" -version = "2.4.1" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] -name = "num_threads" -version = "0.1.3" +name = "nom" +version = "7.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97ba99ba6393e2c3734791401b66902d981cb03bf190af674ca69949b6d5fb15" +checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" dependencies = [ - "libc", + "memchr", + "minimal-lexical", ] [[package]] -name = "os_str_bytes" -version = "6.0.0" +name = "nom_locate" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" +checksum = "37794436ca3029a3089e0b95d42da1f0b565ad271e4d3bb4bad0c7bb70b10605" dependencies = [ + "bytecount", "memchr", + "nom", ] +[[package]] +name = "once_cell" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0" + +[[package]] +name = "os_str_bytes" +version = "6.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff" + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -231,27 +270,27 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.36" +version = "1.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" +checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" dependencies = [ - "unicode-xid", + "unicode-ident", ] [[package]] name = "quote" -version = "1.0.15" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" dependencies = [ "proc-macro2", ] [[package]] name = "regex" -version = "1.5.4" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" dependencies = [ "aho-corasick", "memchr", @@ -260,35 +299,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.25" +version = "0.6.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" - -[[package]] -name = "rustversion" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2cc38e8fa666e2de3c4aba7edeb5ffc5246c1c2ed0e3d17e560aeeba736b23f" - -[[package]] -name = "serde" -version = "1.0.136" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.136" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] +checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" [[package]] name = "slotmap" @@ -307,44 +320,44 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" -version = "1.0.86" +version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b" +checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13" dependencies = [ "proc-macro2", "quote", - "unicode-xid", + "unicode-ident", ] [[package]] name = "termcolor" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" dependencies = [ "winapi-util", ] [[package]] name = "textwrap" -version = "0.14.2" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80" +checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" [[package]] name = "thiserror" -version = "1.0.30" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" +checksum = "c53f98874615aea268107765aa1ed8f6116782501d18e53d08b471733bea6c85" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.30" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" +checksum = "f8b463991b4eab2d801e724172285ec4195c650e8ec79b149e6c2a8e6dd3f783" dependencies = [ "proc-macro2", "quote", @@ -352,37 +365,10 @@ dependencies = [ ] [[package]] -name = "time" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "004cbc98f30fa233c61a38bc77e96a9106e65c88f2d3bef182ae952027e5753d" -dependencies = [ - "itoa", - "libc", - "num_threads", - "time-macros", -] - -[[package]] -name = "time-macros" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25eb0ca3468fc0acc11828786797f6ef9aa1555e4a211a60d64cc8e4d1be47d6" - -[[package]] -name = "toml" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" -dependencies = [ - "serde", -] - -[[package]] -name = "unicode-xid" -version = "0.2.2" +name = "unicode-ident" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" +checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd" [[package]] name = "version_check" diff --git a/Cargo.toml b/Cargo.toml index 973d432..a45650f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,17 +1,17 @@ [package] name = "dotup" -version = "0.0.0" +version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -ansi_term = "0.12.1" -anyhow = "1.0.53" -clap = { version = "3.0.14", features = ["derive"] } -flexi_logger = "0.22.3" -log = "0.4.14" -serde = { version = "1.0.136", features = ["derive"] } +anyhow = "1.0.65" +clap = { version = "3.2.21", features = ["derive"] } +env_logger = "0.9.0" +globset = "0.4.9" +log = "0.4.17" +nom = "7.1.1" +nom_locate = "4.0.0" slotmap = "1.0.6" -thiserror = "1.0.30" -toml = "0.5.8" +thiserror = "1.0.35" diff --git a/README.md b/README.md deleted file mode 100644 index af77721..0000000 --- a/README.md +++ /dev/null @@ -1,77 +0,0 @@ -# dotup -A CLI tool to help symlink your dotfiles into place. - -## Installation -``` -$ cargo install --git https://github.com/diogo464/dotup -``` - -## Usage -Example file hierarchy with dotfiles. -``` - configs - nvim/ - init.vim - lua/ - setup.lua - alacritty/ - alacritty.yml - bash/ - .bashrc - scripts/ - script1.sh - script2.sh - script3.sh -``` - -### dotup init -Running `dotup init` will create an empty depot file in the current directory(by default). -``` -$ dotup init -``` -A new file, `depot.toml`, should have been created in the current directory. - -### dotup link -The `link` subcommand can be used to tell where a file should be linked when installed. -Running `dotup link nvim .config/nvim` will create two new links: -+ `nvim/init.vim` -> `.config/nvim/init.vim` -+ `nvim/lua/setup.lua` -> `.config/nvim/lua/setup.lua` -This subcommand will, by default, only link files. If a directory is passed as argument then it will recursively link all files under that directory. - -To link a directory use the flag `--directory`. -``` -$ dotup link --directory scripts .scripts -``` -This will create a new link -+ `scripts` -> `.scripts` - -### dotup unlink -The `unlink` subcommand unlinks files. -``` -$ dotup unlink nvim/lua/setup.lua -``` -will remove the link `nvim/lua/setup.lua` -> `.config/nvim/lua/setup.lua`. - -### dotup install -The `install` subcommand creates symlinks. -Like the `link` subcommand passing a directory as argument will recursively install anything under it, files and directories. - -By default install will use the home directory as the install-base but this can be changed with the `--install-base ` parameter. -``` -$ dotup install nvim -``` -will create two symlinks -+ `nvim/init.vim` -> `$HOME/.config/nvim/init.vim` -+ `nvim/lua/setup.lua` -> `$HOME/.config/nvim/lua/setup.lua` - -``` -$ dotup install scripts -``` -will create one symlink -+ `scripts` -> `$HOME/.scripts` -Any new scripts added to `$HOME/.scripts` will be created at `configs/scripts`. - -### dotup uninstall -The `uninstall` subcommand removes the symlinks. -This will only remove symlinks if they were pointing to the correct file. - 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 @@ +mod parse; + +use std::path::Path; + +pub struct Config { + groups: Vec, +} + +pub struct Group { + includes: Vec, + links: Vec, + copies: Vec, +} + +pub struct IncludeAction { + group: String, +} + +pub struct LinkAction { + source: String, + target: String, +} + +pub struct CopyAction { + source: String, + target: String, +} + +pub fn parse(content: &str) -> std::io::Result { + todo!() +} + +pub fn parse_path(path: impl AsRef) -> std::io::Result { + todo!() +} + +pub fn format(content: &str) -> std::io::Result { + todo!() +} + +pub fn format_path(path: impl AsRef) -> std::io::Result { + todo!() +} + +pub fn format_inplace(path: impl AsRef) -> std::io::Result<()> { + todo!() +} + +impl Config { + pub fn groups(&self) -> impl Iterator { + std::iter::empty() + } + + pub fn group(&self, name: &str) -> Option<&Group> { + todo!() + } +} + +impl Group { + pub fn groups(&self) -> impl Iterator { + std::iter::empty() + } + + pub fn links(&self) -> impl Iterator { + std::iter::empty() + } + + pub fn copies(&self) -> impl Iterator { + std::iter::empty() + } +} + +impl IncludeAction { + pub fn name(&self) -> &str { + todo!() + } +} + +impl LinkAction { + pub fn source(&self) -> &str { + todo!() + } + + pub fn dest(&self) -> &str { + todo!() + } +} + +impl CopyAction { + pub fn source(&self) -> &str { + todo!() + } + + pub fn dest(&self) -> &str { + todo!() + } +} 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 @@ +use nom::{ + branch::alt, + bytes::complete::{tag, take_while, take_while1}, + character::complete::{alphanumeric0, alphanumeric1, multispace0, space1}, + combinator::map, + multi::{many0, separated_list0}, + sequence::{delimited, preceded}, +}; + +type Span<'s> = nom_locate::LocatedSpan<&'s str>; +type IResult<'s, I, O, E = ParserError<'s>> = nom::IResult; + +#[derive(Debug, PartialEq, Eq)] +struct ParserError<'s> { + location: Span<'s>, + message: Option, +} + +#[derive(Debug)] +struct KeyValueParser<'s> { + span: Span<'s>, + kvs: Vec>, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct KeyValue<'s> { + key: &'s str, + value: &'s str, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct LinkAction { + source: String, + target: String, +} + +enum RichAction { + Link(LinkAction), +} + +struct RichGroup { + name: String, + items: Vec, +} + +enum RichItem { + Group(RichGroup), +} + +struct RichConfig {} + +impl<'s> ParserError<'s> { + fn custom(location: Span<'s>, message: impl Into) -> Self { + Self { + location, + message: Some(message.into()), + } + } + + fn missing_key(span: Span<'s>, key: &'s str) -> Self { + Self::custom(span, format!("missing key: {key}")) + } +} + +impl<'s> From> for nom::Err> { + fn from(e: ParserError<'s>) -> Self { + Self::Failure(e) + } +} + +impl<'s> nom::error::ParseError> for ParserError<'s> { + fn from_error_kind(input: Span<'s>, kind: nom::error::ErrorKind) -> Self { + Self::custom(input, format!("error kind: {kind:?}")) + } + + fn append(input: Span, kind: nom::error::ErrorKind, other: Self) -> Self { + todo!() + } + + fn or(self, other: Self) -> Self { + other + } + + fn from_char(input: Span<'s>, c: char) -> Self { + Self::custom(input, format!("invalid character: {c}")) + } +} + +impl<'s> KeyValueParser<'s> { + fn new(span: Span<'s>, kvs: Vec>) -> Self { + Self { span, kvs } + } + + fn get(&self, key: &'static str) -> Option<&'s str> { + self.kvs.iter().find(|kv| kv.key == key).map(|kv| kv.value) + } + + fn expect(&self, key: &'static str) -> Result<&'s str, ParserError<'s>> { + self.get(key) + .ok_or(ParserError::missing_key(self.span, key)) + } +} + +fn is_value_char(c: char) -> bool { + c.is_alphanumeric() || c == '_' || c == '-' || c == '.' || c == '/' +} + +fn whitespace0(i: Span) -> IResult { + take_while(char::is_whitespace)(i) +} + +fn whitespace1(i: Span) -> IResult { + take_while1(char::is_whitespace)(i) +} + +fn linesep(i: Span) -> IResult { + take_while(|c: char| c.is_whitespace() && c != '\n')(i)?; + take_while(|c: char| c == '\n')(i)?; + take_while(char::is_whitespace)(i) +} + +fn keyvalue(i: Span) -> IResult { + let (i, key) = alphanumeric1(i)?; + let (i, _) = tag("=")(i)?; + let (i, val) = delimited(tag("\""), take_while(is_value_char), tag("\""))(i)?; + Ok(( + i, + KeyValue { + key: key.fragment(), + value: val.fragment(), + }, + )) +} + +fn keyvalues(i: Span) -> IResult> { + separated_list0(space1, keyvalue)(i) +} + +fn link_action(i: Span) -> IResult { + let (i, kvs) = preceded(tag("link"), preceded(space1, keyvalues))(i)?; + eprintln!("{kvs:#?}"); + eprintln!("{i:?}"); + let kvparser = KeyValueParser::new(i, kvs); + let src = kvparser.expect("src")?.to_string(); + let dst = kvparser.expect("dst")?.to_string(); + Ok(( + i, + LinkAction { + source: src, + target: dst, + }, + )) +} + +fn rich_action(i: Span) -> IResult { + todo!() +} + +fn rich_group(i: Span) -> IResult { + let mut header = preceded(tag("group"), preceded(multispace0, alphanumeric1)); + let mut open_bracket = delimited(multispace0, tag("{"), multispace0); + let mut close_bracket = preceded(multispace0, tag("}")); + let mut body = separated_list0(linesep, rich_item); + + let (i, name) = header(i)?; + let (i, _) = open_bracket(i)?; + let (i, items) = body(i)?; + let (i, _) = close_bracket(i)?; + + Ok(( + i, + RichGroup { + name: name.to_string(), + items, + }, + )) +} + +fn rich_item(i: Span) -> IResult { + alt((map(rich_group, RichItem::Group),))(i) +} + +fn config(i: Span) -> IResult { + let (_, groups) = many0(rich_group)(i)?; + todo!() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_keyvalue() { + let input = Span::new(r#"key="value""#); + let (rem, kv) = keyvalue(input).unwrap(); + assert!(rem.is_empty()); + assert_eq!( + kv, + KeyValue { + key: "key", + value: "value" + } + ); + } + + #[test] + fn parse_keyvalues() { + let kvs = vec![ + KeyValue { + key: "key1", + value: "value1", + }, + KeyValue { + key: "key2", + value: "value2", + }, + ]; + + let input = Span::new(r#"key1="value1" key2="value2""#); + let (rem, res) = keyvalues(input).unwrap(); + assert!(rem.is_empty()); + assert_eq!(res, kvs); + + let kvs = vec![ + KeyValue { + key: "src", + value: "tmux/", + }, + KeyValue { + key: "dst", + value: ".config/tmux", + }, + ]; + + let input = Span::new(r#"src="tmux/" dst=".config/tmux""#); + let (rem, res) = keyvalues(input).unwrap(); + assert!(rem.is_empty()); + assert_eq!(res, kvs); + } + + #[test] + fn parse_link_action() { + let input = Span::new(r#"link src="tmux/" dst=".config/tmux""#); + let (rem, res) = link_action(input).unwrap(); + assert!(rem.is_empty()); + assert_eq!( + res, + LinkAction { + source: "tmux/".to_string(), + target: ".config/tmux".to_string() + } + ); + } +} 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 @@ +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +struct Location { + line: u32, + column: u32, +} + +#[derive(Debug, Clone)] +struct Scanner<'s> { + location: Location, + content: std::str::Chars<'s>, +} + +impl<'s> Scanner<'s> { + fn new(content: &'s str) -> Self { + Self { + location: Default::default(), + content: content.chars(), + } + } +} 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 @@ -use anyhow::Context; -use std::{ - collections::HashSet, - ffi::{OsStr, OsString}, - ops::Index, - path::{Path, PathBuf}, -}; -use thiserror::Error; - -use slotmap::{Key, SlotMap}; - -//pub type Result = std::result::Result; -pub use anyhow::Result; -pub use disk::{read, write}; - -slotmap::new_key_type! {pub struct LinkID;} -slotmap::new_key_type! {struct NodeID;} - -#[derive(Debug, Error)] -enum DepotError { - #[error("path must be relative")] - InvalidPath, - #[error("path must be relative and not empty")] - InvalidLinkPath, -} - -#[derive(Debug, Clone)] -struct Node { - comp: OsString, - parent: NodeID, - kind: NodeKind, -} - -#[derive(Debug, Clone)] -enum NodeKind { - Link(LinkID), - Directory(HashSet), -} - -#[derive(Debug, Clone, PartialEq, Eq)] -enum NodeSearchResult { - Found(NodeID), - /// the closest NodeID up the the search point. - NotFound(NodeID), -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum DirNode { - Link(LinkID), - Directory(PathBuf), -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum SearchResult { - Found(LinkID), - Ancestor(LinkID), - NotFound, -} - -#[derive(Debug, Clone)] -struct Link { - origin: PathBuf, - destination: PathBuf, - origin_id: NodeID, -} - -#[derive(Debug)] -pub struct LinkView<'a> { - link_id: LinkID, - depot: &'a Depot, -} - -impl<'a> LinkView<'a> { - pub fn origin(&self) -> &Path { - &self.depot.links[self.link_id].origin - } - - pub fn destination(&self) -> &Path { - &self.depot.links[self.link_id].destination - } -} - -#[derive(Debug, Clone)] -struct DepotTree { - root: NodeID, - nodes: SlotMap, -} - -impl Default for DepotTree { - fn default() -> Self { - let mut nodes = SlotMap::::default(); - let root = nodes.insert(Node { - comp: Default::default(), - parent: Default::default(), - kind: NodeKind::Directory(Default::default()), - }); - Self { root, nodes } - } -} - -impl Index for DepotTree { - type Output = Node; - - fn index(&self, index: NodeID) -> &Self::Output { - self.nodes.index(index) - } -} - -impl DepotTree { - /// create a node of kind [`NodeKind::Link`]. - pub fn link_create(&mut self, path: &Path, link_id: LinkID) -> Result { - debug_assert!(path_verify_link(path).is_ok()); - - let path_search_result = self.search(path); - - // handle the error cases - match path_search_result { - NodeSearchResult::Found(node_id) => { - let node = &self.nodes[node_id]; - match &node.kind { - NodeKind::Link(_) => Err(anyhow::anyhow!("link already exists")), - NodeKind::Directory(_) => { - Err(anyhow::anyhow!("path already has links under it")) - } - } - } - NodeSearchResult::NotFound(ancestor_node_id) => { - let ancestor_node = &self.nodes[ancestor_node_id]; - match &ancestor_node.kind { - NodeKind::Link(_) => Err(anyhow::anyhow!( - "an ancestor of this path is already linked" - )), - NodeKind::Directory(_) => Ok(()), - } - } - }?; - - // create the node - // unwrap: this is a verfied link path, it must have atleast one component - let filename = path.file_name().unwrap(); - let parent_path = path_parent_or_empty(path); - let node_id = self.nodes.insert(Node { - comp: filename.to_owned(), - parent: Default::default(), - kind: NodeKind::Link(link_id), - }); - let parent_id = self.directory_get_or_create(parent_path, node_id); - self.nodes[node_id].parent = parent_id; - Ok(node_id) - } - - pub fn link_update_id(&mut self, node_id: NodeID, link_id: LinkID) { - let node = &mut self.nodes[node_id]; - match &mut node.kind { - NodeKind::Link(lid) => *lid = link_id, - NodeKind::Directory(_) => unreachable!(), - } - } - - /// attempts to moves a node of kind [`NodeKind::Link`] to `destination`. - pub fn link_move(&mut self, node_id: NodeID, destination: &Path) -> Result<()> { - let parent_id = self.nodes[node_id].parent; - let parent = &mut self.nodes[parent_id]; - - // remove the node from its parent temporarily so that the search never returns this - // link and that way any link will find means an error. - // if an error does happen then we re-add this node to its parent to keep the data - // consistent. - match &mut parent.kind { - NodeKind::Link(_) => unreachable!(), - NodeKind::Directory(children) => children.remove(&node_id), - }; - - let search_result = self.search(destination); - // handle the error cases - match search_result { - NodeSearchResult::Found(found_id) => { - assert!(found_id != node_id); - self.directory_add_child(parent_id, node_id); - return Err(anyhow::anyhow!("link already exists at that path")); - } - NodeSearchResult::NotFound(ancestor_id) => { - let ancestor = &self.nodes[ancestor_id]; - match &ancestor.kind { - NodeKind::Link(_) => { - self.directory_add_child(parent_id, node_id); - return Err(anyhow::anyhow!("ancestor path is already linked")); - } - NodeKind::Directory(_) => {} - } - } - }; - - let destination_parent = path_parent_or_empty(destination); - let new_parent_id = self.directory_get_or_create(destination_parent, node_id); - if new_parent_id != parent_id { - self.nodes[node_id].parent = new_parent_id; - - // we have to re-add and call the remove function because it could lead to the removal - // of several directories if they become empty after this remove. - self.directory_add_child(parent_id, node_id); - self.directory_remove_child(parent_id, node_id); - } - - // unwrap: destination is a verified link path so it has atleast 1 component - let comp = destination.file_name().unwrap(); - let node = &mut self.nodes[node_id]; - if node.comp != comp { - node.comp = comp.to_owned(); - } - - Ok(()) - } - - pub fn link_search(&self, path: &Path) -> SearchResult { - match self.search(path) { - NodeSearchResult::Found(node_id) => match &self.nodes[node_id].kind { - NodeKind::Link(link_id) => SearchResult::Found(*link_id), - NodeKind::Directory(_) => SearchResult::NotFound, - }, - NodeSearchResult::NotFound(node_id) => match &self.nodes[node_id].kind { - NodeKind::Link(link_id) => SearchResult::Ancestor(*link_id), - NodeKind::Directory(_) => SearchResult::NotFound, - }, - } - } - - /// remove a node of kind [`NodeKind::Link`]. - pub fn link_remove(&mut self, node_id: NodeID) { - let node = &self.nodes[node_id]; - assert!(std::matches!(node.kind, NodeKind::Link(_))); - let parent_id = node.parent; - self.nodes.remove(node_id); - self.directory_remove_child(parent_id, node_id); - } - - pub fn links_under(&self, path: &Path) -> impl Iterator + '_ { - let links = match self.search(path) { - NodeSearchResult::Found(node_id) => { - let node = &self.nodes[node_id]; - match &node.kind { - NodeKind::Link(link_id) => vec![*link_id], - NodeKind::Directory(children) => { - let mut links = Vec::new(); - let mut node_ids = Vec::from_iter(children.iter().copied()); - while let Some(child_id) = node_ids.pop() { - let child = &self.nodes[child_id]; - match &child.kind { - NodeKind::Link(link_id) => links.push(*link_id), - NodeKind::Directory(extra_children) => { - node_ids.extend(extra_children.iter().copied()) - } - } - } - links - } - } - } - NodeSearchResult::NotFound(_) => vec![], - }; - links.into_iter() - } - - pub fn has_links_under(&self, path: &Path) -> bool { - // it does not matter what type of node is found. if a directory exists then there - // must be atleast one link under it. - match self.search(path) { - NodeSearchResult::Found(_) => true, - NodeSearchResult::NotFound(_) => false, - } - } - - pub fn read_dir(&self, path: &Path) -> Result + '_> { - match self.search(path) { - NodeSearchResult::Found(node_id) => match &self.nodes[node_id].kind { - NodeKind::Link(_) => Err(anyhow::anyhow!("read dir called on a link")), - NodeKind::Directory(children) => Ok(children.iter().map(|child_id| { - let child = &self.nodes[*child_id]; - match &child.kind { - NodeKind::Link(link_id) => DirNode::Link(*link_id), - NodeKind::Directory(_) => DirNode::Directory(self.build_path(*child_id)), - } - })), - }, - NodeSearchResult::NotFound(_) => Err(anyhow::anyhow!("directory not found")), - } - } - - pub fn build_path(&self, node_id: NodeID) -> PathBuf { - fn recursive_helper(nodes: &SlotMap, nid: NodeID, pbuf: &mut PathBuf) { - if nid.is_null() { - return; - } - let parent_id = nodes[nid].parent; - recursive_helper(nodes, parent_id, pbuf); - pbuf.push(&nodes[nid].comp); - } - - let mut node_path = PathBuf::default(); - recursive_helper(&self.nodes, node_id, &mut node_path); - node_path - } - - fn search(&self, path: &Path) -> NodeSearchResult { - debug_assert!(path_verify(path).is_ok()); - - let mut curr_node_id = self.root; - let mut comp_iter = path_iter_comps(path).peekable(); - while let Some(comp) = comp_iter.next() { - if let Some(child_id) = self.directory_search_children(curr_node_id, comp) { - let child = &self.nodes[child_id]; - match &child.kind { - NodeKind::Link(_) => { - if comp_iter.peek().is_some() { - return NodeSearchResult::NotFound(child_id); - } else { - return NodeSearchResult::Found(child_id); - } - } - NodeKind::Directory(_) => curr_node_id = child_id, - } - } else { - return NodeSearchResult::NotFound(curr_node_id); - } - } - NodeSearchResult::Found(curr_node_id) - } - - // creates directories all the way up to and including path. - // there cannot be any links up to `path`. - fn directory_get_or_create(&mut self, path: &Path, initial_child: NodeID) -> NodeID { - // TODO: this could be replaced if the search function also returned the depth of the - // node and we skip those components and just start creating directories up to the - // path. - let mut curr_node_id = self.root; - for comp in path_iter_comps(path) { - if let Some(child_id) = self.directory_search_children(curr_node_id, comp) { - debug_assert!(std::matches!( - self.nodes[child_id].kind, - NodeKind::Directory(_) - )); - curr_node_id = child_id; - } else { - let new_node_id = self.nodes.insert(Node { - comp: comp.to_owned(), - parent: curr_node_id, - kind: NodeKind::Directory(Default::default()), - }); - self.directory_add_child(curr_node_id, new_node_id); - curr_node_id = new_node_id; - } - } - self.directory_add_child(curr_node_id, initial_child); - curr_node_id - } - - fn directory_search_children(&self, node_id: NodeID, comp: &OsStr) -> Option { - let node = &self.nodes[node_id]; - match &node.kind { - NodeKind::Link(_) => unreachable!(), - NodeKind::Directory(children) => { - for &child_id in children { - let child = &self.nodes[child_id]; - if child.comp == comp { - return Some(child_id); - } - } - } - } - None - } - - fn directory_add_child(&mut self, node_id: NodeID, child_id: NodeID) { - let node = &mut self.nodes[node_id]; - match &mut node.kind { - NodeKind::Link(_) => unreachable!(), - NodeKind::Directory(children) => children.insert(child_id), - }; - } - - fn directory_remove_child(&mut self, node_id: NodeID, child_id: NodeID) { - let node = &mut self.nodes[node_id]; - match &mut node.kind { - NodeKind::Link(_) => unreachable!(), - NodeKind::Directory(children) => { - children.remove(&child_id); - if children.is_empty() && !node.parent.is_null() { - let parent_id = node.parent; - self.directory_remove_child(parent_id, node_id); - } - } - } - } -} - -#[derive(Debug, Default, Clone)] -pub struct Depot { - links: SlotMap, - origin: DepotTree, -} - -impl Depot { - pub fn link_create( - &mut self, - origin: impl AsRef, - destination: impl AsRef, - ) -> Result { - let origin = origin.as_ref(); - let destination = destination.as_ref(); - path_verify_link(origin)?; - path_verify_link(destination)?; - self.link_create_unchecked(origin, destination) - } - - pub fn link_remove(&mut self, link_id: LinkID) { - let node_id = self.links[link_id].origin_id; - self.links.remove(link_id); - self.origin.link_remove(node_id); - } - - /// moves the link specified by `link_id` to the path at `destination`. - /// if the link is already at the destination nothing is done. - /// if the destination is another link that that link is removed. - /// if the destination is under another link then an error is returned. - /// `destination` will be the link's new origin. - pub fn link_move(&mut self, link_id: LinkID, destination: impl AsRef) -> Result<()> { - let destination = destination.as_ref(); - path_verify_link(destination)?; - self.link_move_unchecked(link_id, destination) - } - - #[allow(unused)] - pub fn link_search(&self, path: impl AsRef) -> Result { - let path = path.as_ref(); - path_verify(path)?; - Ok(self.link_search_unchecked(path)) - } - - pub fn link_find(&self, path: impl AsRef) -> Result> { - let path = path.as_ref(); - path_verify(path)?; - Ok(self.link_find_unchecked(path)) - } - - pub fn links_under(&self, path: impl AsRef) -> Result + '_> { - let path = path.as_ref(); - path_verify(path)?; - Ok(self.links_under_unchecked(path)) - } - - pub fn has_links_under(&self, path: impl AsRef) -> Result { - let path = path.as_ref(); - path_verify(path)?; - Ok(self.has_links_under_unchecked(path)) - } - - pub fn links_verify_install(&self, link_ids: impl Iterator) -> Result<()> { - let mut destination = DepotTree::default(); - for link_id in link_ids { - let link = &self.links[link_id]; - destination - .link_create(&link.destination, link_id) - .context("link destinations overlap")?; - } - Ok(()) - } - - pub fn link_view(&self, link_id: LinkID) -> LinkView { - LinkView { - link_id, - depot: self, - } - } - - pub fn read_dir(&self, path: impl AsRef) -> Result + '_> { - let path = path.as_ref(); - path_verify(path)?; - self.read_dir_unchecked(path) - } - - fn link_create_unchecked(&mut self, origin: &Path, destination: &Path) -> Result { - let node_id = self.origin.link_create(origin, Default::default())?; - let link_id = self.links.insert(Link { - origin: origin.to_owned(), - destination: destination.to_owned(), - origin_id: node_id, - }); - self.origin.link_update_id(node_id, link_id); - Ok(link_id) - } - - fn link_move_unchecked(&mut self, link_id: LinkID, destination: &Path) -> Result<()> { - let link = &self.links[link_id]; - if link.origin == destination { - return Ok(()); - } - let node_id = link.origin_id; - self.origin.link_move(node_id, destination)?; - self.links[link_id].origin = destination.to_owned(); - Ok(()) - } - - fn link_search_unchecked(&self, path: &Path) -> SearchResult { - self.origin.link_search(path) - } - - fn link_find_unchecked(&self, path: &Path) -> Option { - match self.link_search_unchecked(path) { - SearchResult::Found(link_id) => Some(link_id), - _ => None, - } - } - - fn links_under_unchecked(&self, path: &Path) -> impl Iterator + '_ { - self.origin.links_under(path) - } - - fn has_links_under_unchecked(&self, path: &Path) -> bool { - self.origin.has_links_under(path) - } - - fn read_dir_unchecked(&self, path: &Path) -> Result + '_> { - self.origin.read_dir(path) - } -} - -/// a verified link path is a path that: -/// + is not empty -/// + is relative -/// + does not contain Prefix/RootDir/ParentDir -fn path_verify_link(path: &Path) -> Result<()> { - // make sure the path is not empty - if path.components().next().is_none() { - return Err(DepotError::InvalidLinkPath.into()); - } - path_verify(path).map_err(|_| DepotError::InvalidLinkPath.into()) -} - -/// a verified path is a path that: -/// + is not empty -/// + is relative -/// + does not contain Prefix/RootDir/ParentDir -fn path_verify(path: &Path) -> Result<()> { - // make sure the path is relative - // make sure the path does not contain '.' or '..' - for component in path.components() { - match component { - std::path::Component::Prefix(_) - | std::path::Component::RootDir - | std::path::Component::CurDir - | std::path::Component::ParentDir => return Err(DepotError::InvalidPath.into()), - std::path::Component::Normal(_) => {} - } - } - Ok(()) -} - -fn path_parent_or_empty(path: &Path) -> &Path { - path.parent().unwrap_or_else(|| Path::new("")) -} - -/// Iterate over the components of a path. -/// # Pre -/// The path can only have "Normal" components. -fn path_iter_comps(path: &Path) -> impl Iterator { - debug_assert!(path_verify(path).is_ok()); - path.components().map(|component| match component { - std::path::Component::Normal(comp) => comp, - _ => unreachable!(), - }) -} - -mod disk { - use std::path::{Path, PathBuf}; - - use anyhow::Context; - use serde::{Deserialize, Serialize}; - - use super::Depot; - - #[derive(Debug, Serialize, Deserialize)] - struct DiskLink { - origin: PathBuf, - destination: PathBuf, - } - - #[derive(Debug, Serialize, Deserialize)] - struct DiskLinks { - links: Vec, - } - - pub fn read(path: &Path) -> anyhow::Result { - let contents = std::fs::read_to_string(path).context("Failed to read depot file")?; - let disk_links = toml::from_str::(&contents) - .context("Failed to parse depot file")? - .links; - let mut depot = Depot::default(); - for disk_link in disk_links { - depot - .link_create(disk_link.origin, disk_link.destination) - .context("Failed to build depot from file. File is in an invalid state")?; - } - Ok(depot) - } - - pub fn write(path: &Path, depot: &Depot) -> anyhow::Result<()> { - let mut links = Vec::with_capacity(depot.links.len()); - for (_, link) in depot.links.iter() { - links.push(DiskLink { - origin: link.origin.clone(), - destination: link.destination.clone(), - }); - } - let contents = - toml::to_string_pretty(&DiskLinks { links }).context("Failed to serialize depot")?; - std::fs::write(path, contents).context("Failed to write depot to file")?; - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_depot_link_create() { - let mut depot = Depot::default(); - let f1 = depot.link_create("f1", "f1").unwrap(); - let f2 = depot.link_create("f2", "f2").unwrap(); - let f3 = depot.link_create("d1/f3", "d1/f3").unwrap(); - let f4 = depot.link_create("d1/d2/f4", "d1/d2/d4").unwrap(); - - assert_eq!(depot.link_find("f1").unwrap(), Some(f1)); - assert_eq!(depot.link_find("f2").unwrap(), Some(f2)); - assert_eq!(depot.link_find("d1/f3").unwrap(), Some(f3)); - assert_eq!(depot.link_find("d1/d2/f4").unwrap(), Some(f4)); - - depot.link_create("f2", "").unwrap_err(); - depot.link_create("", "d4").unwrap_err(); - depot.link_create("f1/f3", "f3").unwrap_err(); - } - - #[test] - fn test_depot_link_remove() { - let mut depot = Depot::default(); - let f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); - let f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); - let _f3 = depot.link_create("d1/f3", "d1/f3").unwrap(); - let f4 = depot.link_create("d1/d2/f4", "d2/f4").unwrap(); - let d3 = depot.link_create("d3", "d3").unwrap(); - - depot.link_remove(f2); - assert_eq!(depot.link_find("d1/f1").unwrap(), Some(f1)); - assert_eq!(depot.link_find("d1/f2").unwrap(), None); - depot.link_remove(f4); - assert_eq!(depot.link_find("d1/d2/f4").unwrap(), None); - depot.link_remove(d3); - assert_eq!(depot.link_find("d3").unwrap(), None); - } - - #[test] - fn test_depot_link_move() { - let mut depot = Depot::default(); - let f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); - let _f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); - - depot.link_move(f1, "").unwrap_err(); - depot.link_move(f1, "d1/f2/f1").unwrap_err(); - depot.link_move(f1, "d1/f2").unwrap_err(); - - depot.link_move(f1, "f1").unwrap(); - assert_eq!(depot.link_view(f1).origin(), Path::new("f1")); - depot.link_move(f1, "f2").unwrap(); - assert_eq!(depot.link_view(f1).origin(), Path::new("f2")); - assert_eq!(depot.link_find("f2").unwrap(), Some(f1)); - } - - #[test] - fn test_depot_link_search() { - let mut depot = Depot::default(); - let f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); - let _f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); - let _f3 = depot.link_create("d1/f3", "d1/f3").unwrap(); - let _f4 = depot.link_create("d1/d2/f4", "d2/f4").unwrap(); - let _d3 = depot.link_create("d3", "d3").unwrap(); - - assert_eq!(depot.link_search("d1/f1").unwrap(), SearchResult::Found(f1)); - assert_eq!( - depot.link_search("d1/f1/f5").unwrap(), - SearchResult::Ancestor(f1) - ); - assert_eq!(depot.link_search("d1").unwrap(), SearchResult::NotFound); - assert_eq!( - depot.link_search("d1/d2/f5").unwrap(), - SearchResult::NotFound - ); - } - - #[test] - fn test_depot_link_find() { - let mut depot = Depot::default(); - let f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); - let _f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); - let f3 = depot.link_create("d1/f3", "d1/f3").unwrap(); - let f4 = depot.link_create("d1/d2/f4", "d2/f4").unwrap(); - let d3 = depot.link_create("d3", "d3").unwrap(); - - assert_eq!(depot.link_find("d1/f1").unwrap(), Some(f1)); - assert_eq!(depot.link_find("d1/f3").unwrap(), Some(f3)); - assert_eq!(depot.link_find("d1/d2/f4").unwrap(), Some(f4)); - assert_eq!(depot.link_find("d3").unwrap(), Some(d3)); - - assert_eq!(depot.link_find("d5").unwrap(), None); - assert_eq!(depot.link_find("d3/d5").unwrap(), None); - assert_eq!(depot.link_find("d1/d2/f5").unwrap(), None); - } - - #[test] - fn test_depot_links_under() { - let mut depot = Depot::default(); - let f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); - let f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); - let f3 = depot.link_create("d1/f3", "d1/f3").unwrap(); - let f4 = depot.link_create("d1/d2/f4", "d2/f4").unwrap(); - let d3 = depot.link_create("d3", "d3").unwrap(); - - let under_f1 = depot.links_under("d1/f1").unwrap().collect::>(); - assert_eq!(under_f1, vec![f1]); - - let under_d1 = depot.links_under("d1").unwrap().collect::>(); - let expected_under_d1 = vec![f1, f2, f3, f4]; - assert!( - under_d1.len() == expected_under_d1.len() - && expected_under_d1.iter().all(|x| under_d1.contains(x)) - ); - - let under_d2 = depot.links_under("d2").unwrap().collect::>(); - assert_eq!(under_d2, vec![]); - - let under_d3 = depot.links_under("d3").unwrap().collect::>(); - assert_eq!(under_d3, vec![d3]); - - let under_root = depot.links_under("").unwrap().collect::>(); - let expected_under_root = vec![f1, f2, f3, f4, d3]; - assert!( - under_root.len() == expected_under_root.len() - && expected_under_root.iter().all(|x| under_root.contains(x)) - ); - } - - #[test] - fn test_depot_has_links_under() { - let mut depot = Depot::default(); - let _f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); - let _f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); - let _f3 = depot.link_create("d1/f3", "d1/f3").unwrap(); - let _f4 = depot.link_create("d1/d2/f4", "d2/f4").unwrap(); - let _d3 = depot.link_create("d3", "d3").unwrap(); - - assert!(depot.has_links_under("").unwrap()); - assert!(depot.has_links_under("d1").unwrap()); - assert!(depot.has_links_under("d3").unwrap()); - assert!(depot.has_links_under("d1/f1").unwrap()); - assert!(depot.has_links_under("d1/d2").unwrap()); - assert!(depot.has_links_under("d1/d2/f4").unwrap()); - - assert!(!depot.has_links_under("d2").unwrap()); - assert!(!depot.has_links_under("d4").unwrap()); - assert!(!depot.has_links_under("d1/d2/f4/f5").unwrap()); - } - - #[test] - fn test_depot_links_verify_install() { - let mut depot = Depot::default(); - let f1 = depot.link_create("nvim", ".config/nvim").unwrap(); - let f2 = depot.link_create("alacritty", ".config/alacritty").unwrap(); - let f3 = depot.link_create("bash/.bashrc", ".bashrc").unwrap(); - let f4 = depot.link_create("bash_laptop/.bashrc", ".bashrc").unwrap(); - - depot - .links_verify_install(vec![f1, f2, f3].into_iter()) - .unwrap(); - depot - .links_verify_install(vec![f1, f2, f3, f4].into_iter()) - .unwrap_err(); - } - - #[test] - fn test_depot_read_dir() { - let mut depot = Depot::default(); - let f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); - let f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); - let f3 = depot.link_create("d1/f3", "d1/f3").unwrap(); - let _f4 = depot.link_create("d1/d2/f4", "d2/f4").unwrap(); - let _d3 = depot.link_create("d3", "d3").unwrap(); - - let read_dir = depot.read_dir("d1").unwrap().collect::>(); - let expected_read_dir = vec![ - DirNode::Link(f1), - DirNode::Link(f2), - DirNode::Link(f3), - DirNode::Directory(PathBuf::from("d1/d2")), - ]; - assert!( - read_dir.len() == expected_read_dir.len() - && expected_read_dir.iter().all(|x| read_dir.contains(x)) - ); - } - - #[test] - fn test_path_verify() { - path_verify(Path::new("")).unwrap(); - path_verify(Path::new("f1")).unwrap(); - path_verify(Path::new("d1/f1")).unwrap(); - path_verify(Path::new("d1/f1.txt")).unwrap(); - path_verify(Path::new("d1/./f1.txt")).unwrap(); - - path_verify(Path::new("/")).unwrap_err(); - path_verify(Path::new("./f1")).unwrap_err(); - path_verify(Path::new("/d1/f1")).unwrap_err(); - path_verify(Path::new("d1/../f1.txt")).unwrap_err(); - path_verify(Path::new("/d1/../f1.txt")).unwrap_err(); - } - - #[test] - fn test_path_verify_link() { - path_verify_link(Path::new("f1")).unwrap(); - path_verify_link(Path::new("d1/f1")).unwrap(); - path_verify_link(Path::new("d1/f1.txt")).unwrap(); - path_verify_link(Path::new("d1/./f1.txt")).unwrap(); - - path_verify_link(Path::new("")).unwrap_err(); - path_verify_link(Path::new("/")).unwrap_err(); - path_verify_link(Path::new("./f1")).unwrap_err(); - path_verify_link(Path::new("/d1/f1")).unwrap_err(); - path_verify_link(Path::new("d1/../f1.txt")).unwrap_err(); - path_verify_link(Path::new("/d1/../f1.txt")).unwrap_err(); - } - - #[test] - fn test_path_iter_comps() { - let path = Path::new("comp1/comp2/./comp3/file.txt"); - let mut iter = path_iter_comps(path); - assert_eq!(iter.next(), Some(OsStr::new("comp1"))); - assert_eq!(iter.next(), Some(OsStr::new("comp2"))); - assert_eq!(iter.next(), Some(OsStr::new("comp3"))); - assert_eq!(iter.next(), Some(OsStr::new("file.txt"))); - assert_eq!(iter.next(), None); - } -} 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 @@ -use std::{ - cmp::Ordering, - collections::HashSet, - path::{Path, PathBuf}, -}; - -use ansi_term::Color; -use anyhow::Context; - -use crate::{ - depot::{self, Depot, DirNode, LinkID}, - utils, -}; - -#[derive(Debug)] -struct CanonicalPair { - origin: PathBuf, - destination: PathBuf, -} - -#[derive(Debug, Clone)] -enum StatusItem { - Link { - origin: PathBuf, - destination: PathBuf, - is_directory: bool, - }, - Directory { - origin: PathBuf, - items: Vec, - }, - Unlinked { - origin: PathBuf, - is_directory: bool, - }, -} - -impl StatusItem { - fn display_ord_cmp(&self, other: &Self) -> Ordering { - match (self, other) { - ( - StatusItem::Link { - origin: l_origin, .. - }, - StatusItem::Link { - origin: r_origin, .. - }, - ) => l_origin.cmp(r_origin), - (StatusItem::Link { .. }, StatusItem::Directory { .. }) => Ordering::Less, - ( - StatusItem::Link { - is_directory: l_is_dir, - .. - }, - StatusItem::Unlinked { - is_directory: u_is_dir, - .. - }, - ) => { - if *u_is_dir && !*l_is_dir { - Ordering::Less - } else { - Ordering::Greater - } - } - (StatusItem::Directory { .. }, StatusItem::Link { .. }) => Ordering::Greater, - ( - StatusItem::Directory { - origin: l_origin, .. - }, - StatusItem::Directory { - origin: r_origin, .. - }, - ) => l_origin.cmp(r_origin), - (StatusItem::Directory { .. }, StatusItem::Unlinked { .. }) => Ordering::Greater, - ( - StatusItem::Unlinked { - is_directory: u_is_dir, - .. - }, - StatusItem::Link { - is_directory: l_is_dir, - .. - }, - ) => { - if *u_is_dir && !*l_is_dir { - Ordering::Greater - } else { - Ordering::Less - } - } - (StatusItem::Unlinked { .. }, StatusItem::Directory { .. }) => Ordering::Less, - ( - StatusItem::Unlinked { - origin: l_origin, .. - }, - StatusItem::Unlinked { - origin: r_origin, .. - }, - ) => l_origin.cmp(r_origin), - } - } -} - -#[derive(Debug)] -pub struct Dotup { - depot: Depot, - depot_dir: PathBuf, - depot_path: PathBuf, - install_base: PathBuf, -} - -impl Dotup { - fn new(depot: Depot, depot_path: PathBuf, install_base: PathBuf) -> anyhow::Result { - assert!(depot_path.is_absolute()); - assert!(depot_path.is_file()); - assert!(install_base.is_absolute()); - assert!(install_base.is_dir()); - let depot_dir = { - let mut d = depot_path.clone(); - d.pop(); - d - }; - Ok(Self { - depot, - depot_dir, - depot_path, - install_base, - }) - } - - pub fn link(&mut self, origin: impl AsRef, destination: impl AsRef) { - let link_result: anyhow::Result<()> = try { - let origin = self.prepare_relative_origin(origin.as_ref())?; - let destination_ends_with_slash = utils::path_ends_with_slash(destination.as_ref()); - let mut destination = self.prepare_relative_destination(destination.as_ref())?; - if destination_ends_with_slash { - if let Some(filename) = origin.file_name() { - destination.push(filename); - } - } - self.depot.link_create(origin, destination)?; - }; - match link_result { - Ok(_) => {} - Err(e) => println!("Failed to create link : {e}"), - } - } - - pub fn unlink(&mut self, paths: impl Iterator>, uninstall: bool) { - for origin in paths { - let unlink_result: anyhow::Result<()> = try { - let origin = self.prepare_relative_origin(origin.as_ref())?; - let links_under: Vec<_> = self.depot.links_under(&origin)?.collect(); - for link_id in links_under { - if uninstall && self.symlink_is_installed_by_link_id(link_id)? { - self.symlink_uninstall_by_link_id(link_id)?; - } - self.depot.link_remove(link_id); - } - }; - match unlink_result { - Ok(_) => {} - Err(e) => println!("Failed to unlink {} : {e}", origin.as_ref().display()), - } - } - } - - pub fn install(&self, paths: impl Iterator>) { - let install_result: anyhow::Result<()> = try { - let link_ids = self.link_ids_from_paths_iter(paths)?; - self.depot.links_verify_install(link_ids.iter().copied())?; - - for link_id in link_ids { - self.symlink_install_by_link_id(link_id)?; - } - }; - if let Err(e) = install_result { - println!("error while installing : {e}"); - } - } - - pub fn uninstall(&self, paths: impl Iterator>) { - let uninstall_result: anyhow::Result<()> = try { - let link_ids = self.link_ids_from_paths_iter(paths)?; - for link_id in link_ids { - if self.symlink_is_installed_by_link_id(link_id)? { - self.symlink_uninstall_by_link_id(link_id)?; - } - } - }; - if let Err(e) = uninstall_result { - println!("error while uninstalling {e}",); - } - } - - pub fn mv( - &mut self, - origins: impl Iterator>, - destination: impl AsRef, - ) { - let mv_result: anyhow::Result<()> = try { - let origins = { - let mut v = Vec::new(); - for origin in origins { - v.push( - origin - .as_ref() - .canonicalize() - .context("failed to canonicalize origin path")?, - ); - } - v - }; - let destination = utils::weakly_canonical(destination.as_ref()); - log::debug!("mv destination : {}", destination.display()); - - // if we are moving multiple links then the destination must be a directory - if origins.len() > 1 && !destination.is_dir() { - println!("destination must be a directory"); - return; - } - - for origin in origins { - let destination = if destination.is_dir() { - // unwrap: origin must have a filename - destination.join(origin.file_name().unwrap()) - } else { - destination.to_owned() - }; - self.mv_one(&origin, &destination)?; - } - }; - if let Err(e) = mv_result { - println!("error moving : {e}"); - } - } - - fn mv_one(&mut self, origin: &Path, destination: &Path) -> anyhow::Result<()> { - log::debug!("mv_one : {} to {}", origin.display(), destination.display()); - - let relative_origin = self.prepare_relative_origin(origin)?; - let relative_destination = self.prepare_relative_origin(destination)?; - match self.depot.link_find(&relative_origin)? { - Some(link_id) => { - let is_installed = self.symlink_is_installed_by_link_id(link_id)?; - let original_origin = self.depot.link_view(link_id).origin().to_owned(); - log::debug!("is_installed = {is_installed}",); - log::debug!("original_origin = {}", original_origin.display()); - log::debug!("link_destination = {}", relative_destination.display()); - - self.depot.link_move(link_id, relative_destination)?; - if let Err(e) = std::fs::rename(origin, destination).context("Failed to move file") - { - // unwrap: moving the link back to its origin place has to work - self.depot.link_move(link_id, original_origin).unwrap(); - return Err(e); - } - // reinstall because we just moved the origin - if is_installed { - self.symlink_install_by_link_id(link_id) - .context("failed to reinstall link while moving")?; - } - } - None => { - if origin.is_dir() { - let mut links_installed: HashSet<_> = Default::default(); - if self.depot.has_links_under(&relative_origin)? { - let links_under: Vec<_> = - self.depot.links_under(&relative_origin)?.collect(); - for &link_id in links_under.iter() { - let link_view = self.depot.link_view(link_id); - if self.symlink_is_installed_by_link_id(link_id)? { - links_installed.insert(link_id); - } - // unwrap: the link is under `origin` so stripping the prefix should - // not fail - let origin_extra = - link_view.origin().strip_prefix(&relative_origin).unwrap(); - let new_destination = relative_destination.join(origin_extra); - self.depot.link_move(link_id, new_destination)?; - } - } - std::fs::rename(origin, destination)?; - for link_id in links_installed { - self.symlink_install_by_link_id(link_id)?; - } - } else { - std::fs::rename(origin, destination)?; - } - } - } - Ok(()) - } - - pub fn status(&self, paths: impl Iterator>) { - let status_result: anyhow::Result<()> = try { - // canonicalize and remove paths whose parent we already have - let paths = paths.map(utils::weakly_canonical).collect::>(); - let paths = paths - .iter() - .filter(|p| !paths.iter().any(|x| p.starts_with(x) && p != &x)); - - for path in paths { - let item = self.status_path_to_item(path)?; - self.status_print_item(item, 0)?; - } - }; - if let Err(e) = status_result { - println!("error while displaying status : {e}"); - } - } - fn status_path_to_item(&self, canonical_path: &Path) -> anyhow::Result { - debug_assert!(canonical_path.is_absolute()); - debug_assert!(canonical_path.exists()); - let relative_path = self.prepare_relative_origin(canonical_path)?; - - let item = if canonical_path.is_dir() { - if let Some(link_id) = self.depot.link_find(&relative_path)? { - let destination = self.depot.link_view(link_id).destination().to_owned(); - StatusItem::Link { - origin: relative_path, - destination, - is_directory: true, - } - } else if self.depot.has_links_under(&relative_path)? { - let mut items = Vec::new(); - let mut collected_rel_paths = HashSet::::new(); - let directory_paths = utils::collect_paths_in_dir(&canonical_path)?; - for canonical_item_path in directory_paths { - let item = self.status_path_to_item(&canonical_item_path)?; - match &item { - StatusItem::Link { origin, .. } | StatusItem::Directory { origin, .. } => { - collected_rel_paths.insert(origin.to_owned()); - } - _ => {} - } - items.push(item); - } - - for dir_node in self.depot.read_dir(&relative_path)? { - match dir_node { - DirNode::Link(link_id) => { - let link_view = self.depot.link_view(link_id); - let link_rel_path = link_view.origin(); - let link_rel_dest = link_view.destination(); - if !collected_rel_paths.contains(link_rel_path) { - items.push(StatusItem::Link { - origin: link_rel_path.to_owned(), - destination: link_rel_dest.to_owned(), - is_directory: false, - }); - } - } - DirNode::Directory(_) => {} - } - } - - StatusItem::Directory { - origin: relative_path, - items, - } - } else { - StatusItem::Unlinked { - origin: relative_path, - is_directory: true, - } - } - } else if let Some(link_id) = self.depot.link_find(&relative_path)? { - let destination = self.depot.link_view(link_id).destination().to_owned(); - StatusItem::Link { - origin: relative_path, - destination, - is_directory: false, - } - } else { - StatusItem::Unlinked { - origin: relative_path, - is_directory: false, - } - }; - Ok(item) - } - fn status_print_item(&self, item: StatusItem, depth: u32) -> anyhow::Result<()> { - fn print_depth(d: u32) { - for _ in 0..d.saturating_sub(1) { - print!(" "); - } - } - fn origin_color(exists: bool, is_installed: bool) -> Color { - if !exists { - Color::Red - } else if is_installed { - Color::Green - } else { - Color::RGB(255, 127, 0) - } - } - - let destination_color = Color::Blue; - - print_depth(depth); - match item { - StatusItem::Link { - origin, - destination, - is_directory, - } => { - let canonical_origin = self.depot_dir.join(&origin); - let canonical_destination = self.install_base.join(&destination); - let file_name = Self::status_get_filename(&canonical_origin); - let is_installed = - self.symlink_is_installed(&canonical_origin, &canonical_destination)?; - let exists = canonical_origin.exists(); - let origin_color = origin_color(exists, is_installed); - let directory_extra = if is_directory { "/" } else { "" }; - println!( - "{}{} -> {}", - origin_color.paint(file_name), - directory_extra, - destination_color.paint(destination.display().to_string()) - ); - } - StatusItem::Directory { origin, mut items } => { - items.sort_by(|a, b| StatusItem::display_ord_cmp(a, b).reverse()); - let directory_name = Self::status_get_filename(&origin); - if depth != 0 { - println!("{}/", directory_name); - } - for item in items { - self.status_print_item(item, depth + 1)?; - } - } - StatusItem::Unlinked { - origin, - is_directory, - } => { - let file_name = Self::status_get_filename(&origin); - let directory_extra = if is_directory { "/" } else { "" }; - println!("{}{}", file_name, directory_extra); - } - } - Ok(()) - } - fn status_get_filename(path: &Path) -> &str { - path.file_name() - .and_then(|s| s.to_str()) - .unwrap_or_default() - } - - fn prepare_relative_path(path: &Path, base: &Path) -> anyhow::Result { - let canonical = utils::weakly_canonical(path); - let relative = canonical - .strip_prefix(base) - .context("Invalid origin path, not under depot directory")?; - Ok(relative.to_owned()) - } - - fn prepare_relative_origin(&self, path: &Path) -> anyhow::Result { - Self::prepare_relative_path(path, &self.depot_dir) - } - - fn prepare_relative_destination(&self, path: &Path) -> anyhow::Result { - Self::prepare_relative_path(path, &self.install_base) - } - - fn link_ids_from_paths_iter( - &self, - paths: impl Iterator>, - ) -> anyhow::Result> { - let mut link_ids = HashSet::::default(); - for path in paths { - let path = self.prepare_relative_origin(path.as_ref())?; - link_ids.extend(self.depot.links_under(&path)?); - } - Ok(Vec::from_iter(link_ids.into_iter())) - } - - fn symlink_is_installed_by_link_id(&self, link_id: LinkID) -> anyhow::Result { - let canonical_pair = self.canonical_pair_from_link_id(link_id); - self.symlink_is_installed(&canonical_pair.origin, &canonical_pair.destination) - } - - fn symlink_is_installed(&self, origin: &Path, destination: &Path) -> anyhow::Result { - debug_assert!(origin.is_absolute()); - debug_assert!(destination.is_absolute()); - - if destination.is_symlink() { - let symlink_destination = destination.read_link()?; - match symlink_destination.canonicalize() { - Ok(canonicalized) => Ok(origin == canonicalized), - Err(_) => Ok(false), - } - } else { - Ok(false) - } - } - - fn symlink_install_by_link_id(&self, link_id: LinkID) -> anyhow::Result<()> { - let canonical_pair = self.canonical_pair_from_link_id(link_id); - self.symlink_install(&canonical_pair.origin, &canonical_pair.destination) - } - - fn symlink_install(&self, origin: &Path, destination: &Path) -> anyhow::Result<()> { - debug_assert!(origin.is_absolute()); - debug_assert!(destination.is_absolute()); - log::debug!( - "symlink_install : {} -> {}", - origin.display(), - destination.display() - ); - - let destination_parent = destination - .parent() - .ok_or_else(|| anyhow::anyhow!("destination has no parent component"))?; - std::fs::create_dir_all(destination_parent).context("Failed to create directories")?; - // need to do this beacause if the destination path ends in '/' because the symlink - // functions will treat it as a directory but we want a file with that name. - let destination = destination.with_file_name(destination.file_name().unwrap()); - - let destination_exists = destination.exists(); - let destination_is_symlink = destination.is_symlink(); - - if destination_exists && !destination_is_symlink { - return Err(anyhow::anyhow!("destination already exists")); - } - - if destination_is_symlink { - log::debug!("symlink already exists, removing before recreating"); - std::fs::remove_file(&destination)?; - } - - log::debug!( - "creating filesystem symlink {} -> {}", - origin.display(), - destination.display() - ); - std::os::unix::fs::symlink(origin, destination).context("failed to create symlink")?; - - Ok(()) - } - - fn symlink_uninstall(&self, origin: &Path, destination: &Path) -> anyhow::Result<()> { - debug_assert!(origin.is_absolute()); - debug_assert!(destination.is_absolute()); - let destination = destination.with_file_name(destination.file_name().unwrap()); - - if destination.is_symlink() { - let symlink_destination = destination.read_link()?.canonicalize()?; - if symlink_destination == origin { - std::fs::remove_file(&destination)?; - } - } - - Ok(()) - } - - fn symlink_uninstall_by_link_id(&self, link_id: LinkID) -> anyhow::Result<()> { - let canonical_pair = self.canonical_pair_from_link_id(link_id); - self.symlink_uninstall(&canonical_pair.origin, &canonical_pair.destination) - } - - fn canonical_pair_from_link_id(&self, link_id: LinkID) -> CanonicalPair { - let link_view = self.depot.link_view(link_id); - let relative_origin = link_view.origin(); - let relative_destination = link_view.destination(); - let canonical_origin = self.depot_dir.join(relative_origin); - let canonical_destination = self.install_base.join(relative_destination); - CanonicalPair { - origin: canonical_origin, - destination: canonical_destination, - } - } -} - -pub fn read(depot_path: PathBuf, install_base: PathBuf) -> anyhow::Result { - let depot_path = depot_path - .canonicalize() - .context("Failed to canonicalize depot path")?; - let install_base = install_base - .canonicalize() - .context("Failed to canonicalize install base")?; - if !install_base.is_dir() { - return Err(anyhow::anyhow!("Install base must be a directory")); - } - let depot = depot::read(&depot_path)?; - Dotup::new(depot, depot_path, install_base) -} - -pub fn write(dotup: &Dotup) -> anyhow::Result<()> { - depot::write(&dotup.depot_path, &dotup.depot)?; - Ok(()) -} 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 @@ +use std::{collections::HashSet, ffi::OsString, ops::Index, path::PathBuf}; + +use slotmap::SlotMap; + +use super::{AbsPath, AbsPathBuf}; + +slotmap::new_key_type! { + pub struct NodeID; + pub struct ActionID; +} + +#[derive(Debug)] +pub enum Action { + Link { source: PathBuf }, + Copy { source: PathBuf }, +} + +#[derive(Debug)] +pub struct TreeAction { + path: AbsPathBuf, + action: Action, +} + +#[derive(Debug)] +enum TreeNodeKind { + Action(ActionID), + SubTree(HashSet), +} + +#[derive(Debug)] +struct TreeNode { + path: AbsPathBuf, + component: OsString, + kind: TreeNodeKind, +} + +#[derive(Debug)] +pub struct ActionTree { + root_id: NodeID, + nodes: SlotMap, + actions: SlotMap, +} + +// -------------------- TreeAction -------------------- // + +impl TreeAction { + pub fn target(&self) -> &AbsPath { + &self.path + } + + pub fn action(&self) -> &Action { + &self.action + } +} + +// -------------------- TreeNodeKind -------------------- // + +impl TreeNodeKind { + fn as_action(&self) -> ActionID { + match self { + Self::Action(id) => *id, + _ => unreachable!(), + } + } + + fn as_action_mut(&mut self) -> &mut ActionID { + match self { + Self::Action(id) => id, + _ => unreachable!(), + } + } + + fn as_subtree(&self) -> &HashSet { + match self { + Self::SubTree(ids) => ids, + _ => unreachable!(), + } + } + + fn as_subtree_mut(&mut self) -> &mut HashSet { + match self { + Self::SubTree(ids) => ids, + _ => unreachable!(), + } + } +} + +// -------------------- ActionTree -------------------- // + +impl Index for ActionTree { + type Output = TreeAction; + + fn index(&self, index: ActionID) -> &Self::Output { + self.action(index).unwrap() + } +} + +impl ActionTree { + pub fn new() -> Self { + let mut nodes = SlotMap::with_key(); + let root_id = nodes.insert(TreeNode { + path: AbsPathBuf::default(), + component: OsString::new(), + kind: TreeNodeKind::SubTree(Default::default()), + }); + + Self { + root_id, + nodes, + actions: Default::default(), + } + } + + pub fn insert(&mut self, target: &AbsPath, action: Action) -> ActionID { + let action_id = self.actions.insert(TreeAction { + path: target.to_owned(), + action, + }); + self.force_insert_at(&target, TreeNodeKind::Action(action_id)); + action_id + } + + pub fn install(&self) -> std::io::Result<()> { + for action_id in self.action_ids() { + self.install_action(action_id)?; + } + Ok(()) + } + + pub fn is_installed(&self, action_id: ActionID) -> bool { + let action = &self.actions[action_id]; + let target = action.target(); + match action.action() { + Action::Link { source } => { + let link = match std::fs::read_link(target) { + Ok(link) => link, + Err(_) => return false, + }; + link.canonicalize().unwrap() == source.canonicalize().unwrap() + } + Action::Copy { .. } => target.as_ref().exists(), + } + } + + pub fn uninstall(&self) -> std::io::Result<()> { + for action_id in self.action_ids() { + self.uninstall_action(action_id)?; + } + Ok(()) + } + + pub fn install_action(&self, action_id: ActionID) -> std::io::Result<()> { + let action = &self[action_id]; + match &action.action { + Action::Link { source } => { + let target = action.target(); + log::info!("Linking {:?} -> {:?}", source, target); + if target.as_ref().is_symlink() { + log::trace!("{:?} is a symlink, removing it", target); + std::fs::remove_file(target)?; + } + if let Some(parent) = target.parent() { + log::trace!("creating all directories up to {:?}", parent); + std::fs::create_dir_all(parent.as_ref())?; + } + log::trace!("creating symlink {:?} -> {:?}", source, target); + std::os::unix::fs::symlink(source, target)?; + } + Action::Copy { source } => todo!(), + } + Ok(()) + } + + pub fn uninstall_action(&self, action_id: ActionID) -> std::io::Result<()> { + let action = &self[action_id]; + if let Action::Link { ref source } = action.action { + let target = action.target(); + if target.as_ref().is_symlink() { + log::trace!("{:?} is a symlink", target); + let symlink_target = std::fs::read_link(target.as_ref())?; + if symlink_target == *source { + log::info!("symlink target is {:?}, removing it", source); + std::fs::remove_file(target)?; + } else { + log::trace!( + "symlink target is {:?}, not {:?}, not removing it", + symlink_target, + source + ); + } + } + } + Ok(()) + } + + pub fn actions(&self) -> impl Iterator { + self.actions.values() + } + + pub fn action_ids(&self) -> impl Iterator + '_ { + self.actions.keys() + } + + pub fn action(&self, action_id: ActionID) -> Option<&TreeAction> { + self.actions.get(action_id) + } + + /// Creates all nodes up to the given path. + /// If one of the nodes is an action node, it will be replaced with a subtree node. + fn force_insert_at(&mut self, target: &AbsPath, kind: TreeNodeKind) -> NodeID { + let mut curr = self.root_id; + for comp in target.components() { + { + // Try to find node if it exists + let curr_node = &mut self.nodes[curr]; + match curr_node.kind { + TreeNodeKind::Action(action) => { + self.actions.remove(action); + curr_node.kind = TreeNodeKind::SubTree(Default::default()); + match curr_node.kind { + TreeNodeKind::SubTree(ref mut children) => children, + _ => unreachable!(), + } + } + TreeNodeKind::SubTree(ref mut children) => children, + }; + + let children = self.nodes[curr].kind.as_subtree(); + for &child_id in children.iter() { + let child_node = &self.nodes[child_id]; + if child_node.component == comp { + curr = child_id; + break; + } + } + } + { + // Create new node + let new_node = TreeNode { + path: self.nodes[curr].path.join(comp), + component: comp.to_owned(), + kind: TreeNodeKind::SubTree(Default::default()), + }; + let new_id = self.nodes.insert(new_node); + match &mut self.nodes[curr].kind { + TreeNodeKind::SubTree(children) => children.insert(new_id), + _ => unreachable!(), + }; + curr = new_id; + } + } + let prev_kind = std::mem::replace(&mut self.nodes[curr].kind, kind); + match prev_kind { + TreeNodeKind::SubTree(children) => { + for &child in children.iter() { + self.remove_node(child); + } + } + _ => {} + } + curr + } + + /// Removes the given node. + /// Does not remove it from the parent's children node. + fn remove_node(&mut self, node_id: NodeID) { + let node = self + .nodes + .remove(node_id) + .expect("Node being removed does not exist"); + match node.kind { + TreeNodeKind::Action(action) => { + self.actions.remove(action); + } + TreeNodeKind::SubTree(children) => { + for child in children { + self.remove_node(child); + } + } + }; + } +} + +#[cfg(test)] +mod tests { + use std::{convert::TryFrom, path::Path}; + + use super::*; + + #[test] + fn empty_tree() { + let _ = ActionTree::new(); + } + + #[test] + fn single_action() { + let mut tree = ActionTree::new(); + + let action_id = tree.insert( + TryFrom::try_from("/home/user/.config/nvim").unwrap(), + Action::Link { + source: PathBuf::from("nvim"), + }, + ); + + let action = &tree[action_id]; + assert_eq!( + action.path.as_path(), + AbsPath::new(Path::new("/home/user/.config/nvim")) + ); + } + + #[test] + fn subtree_replacement() { + let mut tree = ActionTree::new(); + + let action_id = tree.insert( + TryFrom::try_from("/home/user/.config/nvim").unwrap(), + Action::Link { + source: PathBuf::from("nvim"), + }, + ); + let action_id_original = action_id; + + let action = &tree[action_id]; + assert_eq!( + action.path.as_path(), + AbsPath::new(Path::new("/home/user/.config/nvim")) + ); + + let action_id = tree.insert( + TryFrom::try_from("/home/user/.config/nvim/init.vim").unwrap(), + Action::Link { + source: PathBuf::from("nvim/init.vim"), + }, + ); + + let action = &tree[action_id]; + assert_eq!( + action.path.as_path(), + AbsPath::new(Path::new("/home/user/.config/nvim/init.vim")) + ); + + eprintln!("{:#?}", tree); + assert!(tree.action(action_id_original).is_none()); + } +} diff --git a/src/dotup/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 @@ +use std::fmt::Write; + +use nom::{ + branch::alt, + bytes::complete::{tag, take_while}, + character::complete::{alphanumeric1, multispace0, multispace1, space1}, + combinator::map, + multi::separated_list0, + sequence::{delimited, preceded}, +}; + +type Span<'s> = nom_locate::LocatedSpan<&'s str>; +type IResult<'s, I, O, E = ParseError> = nom::IResult; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct KeyValue { + pub key: String, + pub value: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Action { + pub location: Location, + pub kind: String, + pub keyvalues: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Comment { + pub location: Location, + pub text: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum GroupItem { + Group(Group), + Action(Action), + Comment(Comment), +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Group { + pub location: Location, + pub name: String, + pub items: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Config { + pub groups: Vec, +} + +pub fn parse(content: &str) -> Result { + match config(Span::new(content)) { + Ok((_, config)) => Ok(config), + Err(err) => match err { + nom::Err::Incomplete(_) => Err(ParseError::new(Default::default(), "unexpected EOF")), + nom::Err::Error(e) | nom::Err::Failure(e) => Err(e), + }, + } +} + +pub fn format(content: &str) -> Result { + struct Ident(usize); + impl std::fmt::Display for Ident { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for _ in 0..self.0 { + write!(f, "\t")?; + } + Ok(()) + } + } + + fn format_action(buffer: &mut String, action: &Action, ident: usize) -> std::fmt::Result { + write!(buffer, "{}{}", Ident(ident), action.kind)?; + for kv in action.keyvalues.iter() { + write!(buffer, " {}=\"{}\"", kv.key, kv.value)?; + } + writeln!(buffer)?; + Ok(()) + } + + fn format_comment(buffer: &mut String, comment: &Comment, ident: usize) -> std::fmt::Result { + for line in comment.text.lines() { + writeln!(buffer, "{}# {}", Ident(ident), line)?; + } + Ok(()) + } + + fn format_group(buffer: &mut String, group: &Group, ident: usize) -> std::fmt::Result { + writeln!(buffer, "{}group {} {{", Ident(ident), group.name)?; + for item in group.items.iter() { + match item { + GroupItem::Group(group) => format_group(buffer, group, ident + 1)?, + GroupItem::Action(action) => format_action(buffer, action, ident + 1)?, + GroupItem::Comment(comment) => format_comment(buffer, comment, ident + 1)?, + } + } + writeln!(buffer, "{}}}", Ident(ident))?; + writeln!(buffer)?; + Ok(()) + } + + let config = parse(content)?; + let mut buffer = String::new(); + for group in config.groups { + format_group(&mut buffer, &group, 0).unwrap(); + } + assert!(parse(&buffer).is_ok()); + Ok(buffer) +} + +#[derive(Debug)] +pub struct ParseError { + location: Location, + message: String, +} + +impl ParseError { + fn new(location: Location, expected: impl Into) -> Self { + Self { + location, + message: expected.into(), + } + } + + pub fn location(&self) -> Location { + self.location + } + + pub fn message(&self) -> &str { + &self.message + } +} + +impl<'s> nom::error::ParseError> for ParseError { + fn from_error_kind(input: Span<'s>, kind: nom::error::ErrorKind) -> Self { + Self::new(location_from_span(input), format!("error kind: {kind:?}")) + } + + fn append(input: Span, kind: nom::error::ErrorKind, other: Self) -> Self { + other + } + + fn or(self, other: Self) -> Self { + other + } + + fn from_char(input: Span<'s>, c: char) -> Self { + Self::new(location_from_span(input), format!("invalid character: {c}")) + } +} + +impl std::fmt::Display for ParseError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "parse error at {}: {}", self.location, self.message) + } +} + +impl std::error::Error for ParseError {} + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] +pub struct Location { + pub line: u32, + pub column: u32, +} + +impl Location { + pub fn new(line: u32, column: u32) -> Self { + Self { line, column } + } +} + +impl std::cmp::PartialOrd for Location { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl std::cmp::Ord for Location { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + match self.line.cmp(&other.line) { + core::cmp::Ordering::Equal => {} + ord => return ord, + } + self.column.cmp(&other.column) + } +} + +impl std::fmt::Display for Location { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let Location { line, column } = self; + write!(f, "line {line} column {column}") + } +} + +impl KeyValue { + fn new(key: impl Into, value: impl Into) -> Self { + Self { + key: key.into(), + value: value.into(), + } + } +} + +fn is_value_char(c: char) -> bool { + c.is_alphanumeric() || c == '_' || c == '-' || c == '.' || c == '/' +} + +fn linesep(i: Span) -> IResult { + take_while(|c: char| c.is_whitespace() && c != '\n')(i)?; + take_while(|c: char| c == '\n')(i)?; + take_while(char::is_whitespace)(i) +} + +fn keyvalue(i: Span) -> IResult { + let (i, key) = alphanumeric1(i)?; + let (i, _) = tag("=")(i)?; + let (i, val) = delimited(tag("\""), take_while(is_value_char), tag("\""))(i)?; + Ok(( + i, + KeyValue { + key: key.fragment().to_string(), + value: val.fragment().to_string(), + }, + )) +} + +fn keyvalues(i: Span) -> IResult> { + separated_list0(space1, keyvalue)(i) +} + +fn comment(i: Span) -> IResult { + let location = location_from_span(i); + preceded( + tag("#"), + preceded(multispace0, take_while(|c: char| c != '\n')), + )(i) + .map(|(i, text)| { + ( + i, + Comment { + location, + text: text.to_string(), + }, + ) + }) +} + +fn action(i: Span) -> IResult { + let location = location_from_span(i); + let (i, kind) = alphanumeric1(i)?; + let (i, keyvalues) = preceded(space1, keyvalues)(i)?; + Ok(( + i, + Action { + location, + kind: kind.to_string(), + keyvalues, + }, + )) +} + +fn group_item(i: Span) -> IResult { + alt(( + map(group, GroupItem::Group), + map(action, GroupItem::Action), + map(comment, GroupItem::Comment), + ))(i) +} + +fn group(i: Span) -> IResult { + let location = location_from_span(i); + + let (i, _) = tag("group")(i)?; + let (i, _) = multispace1(i)?; + let (i, name) = alphanumeric1(i)?; + let (i, _) = multispace0(i)?; + let (i, _) = tag("{")(i)?; + let (i, _) = multispace0(i)?; + let (i, items) = separated_list0(linesep, group_item)(i)?; + let (i, _) = multispace0(i)?; + let (i, _) = tag("}")(i)?; + + Ok(( + i, + Group { + location, + name: name.to_string(), + items, + }, + )) +} + +fn config(i: Span) -> IResult { + let mut groups = Vec::new(); + let mut parser = delimited(multispace0, group, multispace0); + let mut curr_span = i; + while !curr_span.is_empty() { + match parser(curr_span) { + Ok((i, group)) => { + curr_span = i; + groups.push(group); + } + Err(nom::Err::Error(e)) | Err(nom::Err::Failure(e)) => { + return Err(nom::Err::Failure(e)) + } + Err(nom::Err::Incomplete(_)) => break, + } + } + Ok((i, Config { groups })) +} + +fn location_from_span(span: Span) -> Location { + Location::new(span.location_line(), span.get_utf8_column() as u32) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_keyvalue() { + let input = Span::new(r#"key="value""#); + let (rem, kv) = keyvalue(input).unwrap(); + assert!(rem.is_empty()); + assert_eq!(kv, KeyValue::new("key", "value"),); + } + + #[test] + fn parse_keyvalues() { + let kvs = vec![ + KeyValue::new("key1", "value1"), + KeyValue::new("key2", "value2"), + ]; + + let input = Span::new(r#"key1="value1" key2="value2""#); + let (rem, res) = keyvalues(input).unwrap(); + assert!(rem.is_empty()); + assert_eq!(res, kvs); + + let kvs = vec![ + KeyValue::new("src", "tmux/"), + KeyValue::new("dst", ".config/tmux"), + ]; + + let input = Span::new(r#"src="tmux/" dst=".config/tmux""#); + let (rem, res) = keyvalues(input).unwrap(); + assert!(rem.is_empty()); + assert_eq!(res, kvs); + } +} 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 @@ +mod action_tree; +mod cfg; +mod paths; + +use std::collections::HashSet; +use std::{ + collections::HashMap, + path::{Path, PathBuf}, +}; + +use slotmap::{Key, SlotMap}; +use thiserror::Error; + +pub use paths::*; + +type Result = std::result::Result; + +slotmap::new_key_type! { pub struct GroupID; } + +#[derive(Debug, Error)] +pub enum Error { + #[error(transparent)] + ParseError(#[from] cfg::ParseError), + #[error("error: {0}")] + Custom(String), + #[error(transparent)] + IOError(#[from] std::io::Error), +} + +#[derive(Debug, Default)] +pub struct Group { + name: String, + parent: GroupID, + children: HashMap, + actions: Vec, +} + +#[derive(Debug)] +pub struct Dotup { + root_id: GroupID, + groups: SlotMap, +} + +#[derive(Debug, Clone, Copy)] +pub struct InstallParams<'p> { + pub cwd: &'p Path, + pub home: &'p Path, +} + +#[derive(Debug, Clone, Copy)] +pub struct UninstallParams<'p> { + pub cwd: &'p Path, + pub home: &'p Path, +} + +#[derive(Debug)] +struct KeyValueParser { + location: cfg::Location, + keyvalues: Vec, +} + +#[derive(Debug, Clone)] +struct IncludeAction { + group: String, +} + +#[derive(Debug, Clone)] +struct LinkAction { + source: PathBuf, + target: PathBuf, +} + +#[derive(Debug, Clone)] +struct CopyAction { + source: PathBuf, + target: PathBuf, +} + +#[derive(Debug, Clone)] +enum Action { + Include(IncludeAction), + Link(LinkAction), + Copy(CopyAction), +} + +pub fn load(content: &str) -> Result { + let config = cfg::parse(content)?; + Dotup::from_config(config) +} + +pub fn load_file(path: impl AsRef) -> Result { + let content = std::fs::read_to_string(path)?; + load(&content) +} + +pub fn format(content: &str) -> Result { + Ok(cfg::format(content)?) +} + +pub fn format_file(path: &Path) -> Result { + let content = std::fs::read_to_string(path)?; + format(&content) +} + +pub fn format_file_inplace(path: &Path) -> Result<()> { + let content = std::fs::read_to_string(path)?; + let formatted = format(&content)?; + std::fs::write(path, formatted)?; + Ok(()) +} + +// -------------------- Dotup -------------------- // + +impl Dotup { + pub fn find_group_by_name(&self, name: &str) -> Option { + self.find_group_by_name_rooted(self.root_id, name) + } + + pub fn install(&self, params: InstallParams, group_id: GroupID) -> Result<()> { + let action_tree = self.build_action_tree(params.cwd, params.home, group_id)?; + action_tree.install()?; + Ok(()) + } + + pub fn uninstall(&self, params: UninstallParams, group_id: GroupID) -> Result<()> { + let action_tree = self.build_action_tree(params.cwd, params.home, group_id)?; + action_tree.uninstall()?; + Ok(()) + } + + pub fn status(&self, params: InstallParams, group_id: GroupID) -> Result<()> { + let action_tree = self.build_action_tree(params.cwd, params.home, group_id)?; + for action_id in action_tree.action_ids() { + let prefix = if action_tree.is_installed(action_id) { + "INSTALLED" + } else { + "NOT INSTALLED" + }; + let action = action_tree.action(action_id).unwrap(); + let source = match action.action() { + action_tree::Action::Link { ref source } => source, + action_tree::Action::Copy { ref source } => source, + }; + let target = action.target(); + println!("{}: {} -> {}", prefix, source.display(), target.display()); + } + Ok(()) + } +} + +impl Dotup { + fn from_config(config: cfg::Config) -> Result { + let mut groups = SlotMap::default(); + let root_id = groups.insert(Default::default()); + let mut dotup = Self { root_id, groups }; + + for group in config.groups { + dotup.insert_group(root_id, group)?; + } + + Ok(dotup) + } + + fn find_group_by_name_rooted(&self, root: GroupID, name: &str) -> Option { + let trimmed = name.trim_start_matches("."); + let rel_levels = name.len() - trimmed.len(); + let mut current = self.root_id; + + if rel_levels != 0 { + current = root; + for _ in 0..rel_levels - 1 { + current = self.groups[current].parent; + if current == self.root_id { + break; + } + } + } + + for comp in trimmed.split(".") { + let group = &self.groups[current]; + let child_id = group.children.get(comp)?; + current = *child_id; + } + Some(current) + } + + fn insert_group(&mut self, parent_id: GroupID, mut group_cfg: cfg::Group) -> Result<()> { + let parent = &mut self.groups[parent_id]; + if parent.children.contains_key(&group_cfg.name) { + return Err(Error::Custom(format!( + "group '{}' at {} already exists", + group_cfg.name, group_cfg.location, + ))); + } + + let mut group = Group { + name: group_cfg.name.clone(), + parent: parent_id, + children: Default::default(), + actions: Default::default(), + }; + + for item in group_cfg + .items + .drain_filter(|item| std::matches!(item, cfg::GroupItem::Action(_))) + { + match item { + cfg::GroupItem::Action(action) => { + let action = cfg_action_to_action(action)?; + group.actions.push(action); + } + _ => {} + } + } + + let group_id = self.groups.insert(group); + let parent = &mut self.groups[parent_id]; + parent.children.insert(group_cfg.name, group_id); + + for item in group_cfg.items { + match item { + cfg::GroupItem::Group(group) => { + self.insert_group(group_id, group)?; + } + _ => {} + } + } + + Ok(()) + } + + fn build_action_tree( + &self, + cwd: &Path, + home: &Path, + group_id: GroupID, + ) -> Result { + fn inner_helper( + dotup: &Dotup, + cwd: &AbsPath, + home: &AbsPath, + group_id: GroupID, + tree: &mut action_tree::ActionTree, + visited: &mut HashSet, + ) -> Result<()> { + if visited.contains(&group_id) { + return Ok(()); + } + visited.insert(group_id); + + let group = &dotup.groups[group_id]; + for action in group.actions.iter() { + match action { + Action::Include(action) => { + let include_id = dotup + .find_group_by_name_rooted(group_id, &action.group) + .ok_or_else(|| { + Error::Custom(format!( + "group '{}' not found in include from group '{}'", + action.group, dotup.groups[group_id].name, + )) + })?; + inner_helper(dotup, cwd, home, include_id, tree, visited)?; + } + Action::Link(action) => { + let source = make_absolute_path(cwd, &action.source).into(); + let target = make_absolute_path(home, &action.target); + tree.insert(&target, action_tree::Action::Link { source }); + } + Action::Copy(action) => { + let source = make_absolute_path(cwd, &action.source).into(); + let target = make_absolute_path(home, &action.target); + tree.insert(&target, action_tree::Action::Copy { source }); + } + } + } + + Ok(()) + } + + let cwd = AbsPathBuf::try_from( + cwd.canonicalize() + .expect("failed to canonicalize current workind directory path"), + ) + .unwrap(); + let home = AbsPathBuf::try_from( + home.canonicalize() + .expect("failed to canonicalize home directory path"), + ) + .unwrap(); + + let mut tree = action_tree::ActionTree::new(); + inner_helper( + self, + &cwd, + &home, + group_id, + &mut tree, + &mut Default::default(), + )?; + Ok(tree) + } +} + +// -------------------- KeyValueParser -------------------- // + +impl KeyValueParser { + fn new(location: cfg::Location, keyvalues: Vec) -> Self { + Self { + location, + keyvalues, + } + } + + fn get(&mut self, key: &str) -> Option { + let position = self.keyvalues.iter().position(|kv| kv.key == key)?; + let keyvalue = self.keyvalues.swap_remove(position); + Some(keyvalue.value) + } + + fn expect(&mut self, key: &str) -> Result { + self.get(key) + .ok_or_else(|| Error::Custom(format!("expected key '{}' at {}", key, self.location))) + } + + fn finalize(&mut self) -> Result<()> { + if let Some(kv) = self.keyvalues.pop() { + return Err(Error::Custom(format!( + "unexpected key '{}' at {}", + kv.key, self.location + ))); + } + Ok(()) + } +} + +// -------------------- Misc -------------------- // + +fn cfg_action_to_action(cfg_action: cfg::Action) -> Result { + let mut parser = KeyValueParser::new(cfg_action.location, cfg_action.keyvalues); + match cfg_action.kind.as_str() { + "include" => { + let group = parser.expect("group")?; + parser.finalize()?; + Ok(Action::Include(IncludeAction { group })) + } + "link" => { + let source = parser.expect("source")?; + let target = parser.expect("target")?; + parser.finalize()?; + Ok(Action::Link(LinkAction { + source: PathBuf::from(source), + target: PathBuf::from(target), + })) + } + "copy" => { + let source = parser.expect("source")?; + let target = parser.expect("target")?; + parser.finalize()?; + Ok(Action::Copy(CopyAction { + source: PathBuf::from(source), + target: PathBuf::from(target), + })) + } + _ => Err(Error::Custom(format!( + "unknown action '{}' at {}", + cfg_action.kind, cfg_action.location + ))), + } +} + +/// Returns `path` if it is already absolute. +/// Otherwise makes it absolute by prepending `self.root`. +fn make_absolute_path(root: &AbsPath, path: &Path) -> AbsPathBuf { + if path.is_absolute() { + AbsPathBuf::try_from(path).unwrap() + } else { + AbsPathBuf::from_rel(root, TryFrom::try_from(path).unwrap()) + } +} 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 @@ +use std::{ + borrow::Borrow, + convert::TryFrom, + ffi::OsStr, + ops::Deref, + path::{Component, Components, Display, Path, PathBuf}, +}; + +use thiserror::Error; + +#[derive(Debug, Error)] +#[error("invalid relative path")] +pub struct InvalidRelPath; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct RelPathBuf(PathBuf); + +#[derive(Debug, PartialEq, Eq, Hash)] +#[repr(transparent)] +pub struct RelPath(Path); + +#[derive(Debug, Error)] +#[error("invalid absolute path")] +pub struct InvalidAbsPath; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct AbsPathBuf(PathBuf); + +#[derive(Debug, PartialEq, Eq, Hash)] +#[repr(transparent)] +pub struct AbsPath(Path); + +pub struct AbsComponents<'p> { + inner: std::path::Components<'p>, +} + +impl<'p> Iterator for AbsComponents<'p> { + type Item = &'p OsStr; + + fn next(&mut self) -> Option { + loop { + match self.inner.next()? { + Component::RootDir => continue, + Component::Normal(p) => break Some(p), + _ => unreachable!(), + } + } + } +} + +// -------------------- RelPathBuf -------------------- // + +impl From for PathBuf { + fn from(path: RelPathBuf) -> Self { + path.0 + } +} + +impl AsRef for RelPathBuf { + fn as_ref(&self) -> &Path { + self.0.as_ref() + } +} + +impl Deref for RelPathBuf { + type Target = RelPath; + + fn deref(&self) -> &Self::Target { + TryFrom::try_from(self.0.as_path()).unwrap() + } +} + +impl TryFrom<&Path> for RelPathBuf { + type Error = InvalidRelPath; + + fn try_from(path: &Path) -> Result { + if path.is_relative() { + Ok(Self(path.to_owned())) + } else { + Err(InvalidRelPath) + } + } +} + +impl Borrow for RelPathBuf { + fn borrow(&self) -> &RelPath { + self.deref() + } +} + +impl RelPathBuf {} + +// -------------------- RelPath -------------------- // + +impl ToOwned for RelPath { + type Owned = RelPathBuf; + + fn to_owned(&self) -> Self::Owned { + RelPathBuf(self.0.to_owned()) + } +} + +impl<'p> AsRef for &'p RelPath { + fn as_ref(&self) -> &Path { + self.0.as_ref() + } +} + +impl<'p> TryFrom<&'p Path> for &'p RelPath { + type Error = InvalidRelPath; + + fn try_from(value: &'p Path) -> Result { + if value.is_relative() { + Ok(unsafe { std::mem::transmute(value) }) + } else { + Err(InvalidRelPath) + } + } +} + +impl RelPath { + pub fn components(&self) -> Components { + self.0.components() + } + + pub fn display(&self) -> Display { + self.0.display() + } +} + +// -------------------- AbsPathBuf -------------------- // + +impl Default for AbsPathBuf { + fn default() -> Self { + Self(PathBuf::from("/")) + } +} + +impl From for PathBuf { + fn from(p: AbsPathBuf) -> Self { + p.0 + } +} + +impl AsRef for AbsPathBuf { + fn as_ref(&self) -> &Path { + self.0.as_ref() + } +} + +impl AsRef for AbsPathBuf { + fn as_ref(&self) -> &AbsPath { + AbsPath::new(&self.0) + } +} + +impl Deref for AbsPathBuf { + type Target = AbsPath; + + fn deref(&self) -> &Self::Target { + TryFrom::try_from(self.0.as_path()).unwrap() + } +} + +impl TryFrom<&Path> for AbsPathBuf { + type Error = InvalidAbsPath; + + fn try_from(path: &Path) -> Result { + if path.is_absolute() { + Ok(Self(path.to_owned())) + } else { + Err(InvalidAbsPath) + } + } +} + +impl TryFrom for AbsPathBuf { + type Error = InvalidAbsPath; + + fn try_from(path: PathBuf) -> Result { + if path.is_absolute() { + Ok(Self(path)) + } else { + Err(InvalidAbsPath) + } + } +} + +impl Borrow for AbsPathBuf { + fn borrow(&self) -> &AbsPath { + self.deref() + } +} + +impl AbsPathBuf { + pub fn from_rel(root: &AbsPath, rel: &RelPath) -> Self { + let p = weakly_canonical_cwd(rel, root.0.to_path_buf()); + Self::try_from(p).unwrap() + } + + pub fn as_path(&self) -> &AbsPath { + TryFrom::try_from(self.0.as_path()).unwrap() + } +} + +// -------------------- AbsPath -------------------- // + +impl<'p> AsRef for &'p AbsPath { + fn as_ref(&self) -> &'p Path { + self.0.as_ref() + } +} + +impl ToOwned for AbsPath { + type Owned = AbsPathBuf; + + fn to_owned(&self) -> Self::Owned { + AbsPathBuf(self.0.to_owned()) + } +} + +impl<'p> Default for &'p AbsPath { + fn default() -> Self { + Self::try_from(Path::new("/")).unwrap() + } +} + +impl<'p> TryFrom<&'p Path> for &'p AbsPath { + type Error = InvalidAbsPath; + + fn try_from(value: &'p Path) -> Result { + if value.is_absolute() { + Ok(unsafe { std::mem::transmute(value) }) + } else { + Err(InvalidAbsPath) + } + } +} + +impl<'p> TryFrom<&'p str> for &'p AbsPath { + type Error = InvalidAbsPath; + + fn try_from(value: &'p str) -> Result { + TryFrom::try_from(Path::new(value)) + } +} + +impl AbsPath { + pub fn new(path: &Path) -> &Self { + TryFrom::try_from(path).unwrap() + } + + pub fn join(&self, other: impl AsRef) -> AbsPathBuf { + AbsPathBuf::try_from(weakly_canonical_cwd(other, self.0.to_path_buf())).unwrap() + } + + pub fn parent(&self) -> Option<&AbsPath> { + self.0.parent().map(|p| TryFrom::try_from(p).unwrap()) + } + + pub fn components(&self) -> AbsComponents { + AbsComponents { + inner: self.0.components(), + } + } + + pub fn display(&self) -> Display { + self.0.display() + } +} + +// -------------------- Utils -------------------- // + +pub fn current_working_directory() -> PathBuf { + std::env::current_dir().expect("Failed to obtain current working directory") +} + +pub fn weakly_canonical(path: impl AsRef) -> PathBuf { + let cwd = current_working_directory(); + weakly_canonical_cwd(path, cwd) +} + +pub fn weakly_canonical_cwd(path: impl AsRef, cwd: PathBuf) -> PathBuf { + // Adapated from + // https://github.com/rust-lang/cargo/blob/fede83ccf973457de319ba6fa0e36ead454d2e20/src/cargo/util/paths.rs#L61 + let path = path.as_ref(); + + let mut components = path.components().peekable(); + let mut canonical = cwd; + let prefix = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() { + components.next(); + PathBuf::from(c.as_os_str()) + } else { + PathBuf::new() + }; + + for component in components { + match component { + Component::Prefix(_) => unreachable!(), + Component::RootDir => { + canonical = prefix.clone(); + canonical.push(component.as_os_str()) + } + Component::CurDir => {} + Component::ParentDir => { + canonical.pop(); + } + Component::Normal(p) => canonical.push(p), + }; + } + + canonical +} + +pub fn ends_with_slash(path: impl AsRef) -> bool { + path.as_ref() + .to_str() + .map(|s| s.ends_with('/')) + .unwrap_or_default() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_weakly_canonical() { + let cwd = PathBuf::from("/home/user"); + assert_eq!( + PathBuf::from("/home/dest"), + weakly_canonical_cwd("../dest", cwd.clone()) + ); + assert_eq!( + PathBuf::from("/home/dest/configs/init.vim"), + weakly_canonical_cwd("../dest/configs/init.vim", cwd.clone()) + ); + assert_eq!( + PathBuf::from("/dest/configs/init.vim"), + weakly_canonical_cwd("/dest/configs/init.vim", cwd.clone()) + ); + assert_eq!( + PathBuf::from("/home/user/configs/nvim/lua/setup.lua"), + weakly_canonical_cwd("./configs/nvim/lua/setup.lua", cwd.clone()) + ); + assert_eq!( + PathBuf::from("/home/user/configs/nvim/lua/setup.lua"), + weakly_canonical_cwd("configs/nvim/lua/setup.lua", cwd) + ); + } + + #[test] + fn test_path_ends_with_slash() { + assert!(!ends_with_slash("")); + assert!(!ends_with_slash("/f1")); + assert!(!ends_with_slash("/f1/f2")); + assert!(!ends_with_slash("./f1/f2")); + assert!(!ends_with_slash("./f1/f2/../f3")); + + assert!(ends_with_slash("/")); + assert!(ends_with_slash("/f1/")); + assert!(ends_with_slash("f1/")); + assert!(ends_with_slash("f1/f2/")); + assert!(ends_with_slash("f1/f2/../f3/")); + } +} 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 @@ -#![feature(try_blocks)] +#![feature(drain_filter)] -mod depot; -mod dotup; -mod utils; +//pub mod config; +pub mod dotup; use std::path::PathBuf; -use clap::Parser; -use flexi_logger::Logger; -use utils::DEFAULT_DEPOT_FILE_NAME; +use anyhow::Context; +use clap::{Parser, Subcommand}; #[derive(Parser, Debug)] -pub struct Flags { - /// Path to the depot file, default to `.depot`. +struct GlobalFlags { #[clap(long)] - depot: Option, + base: Option, - /// Path to the install base, defaults to the home directory. - #[clap(long)] - install_base: Option, -} - -#[derive(Parser, Debug)] -#[clap(author, version, about, long_about = None)] -struct Args { - /// A level of verbosity, and can be used multiple times - /// - /// Level 1 - Info - /// - /// Level 2 - Debug - /// - /// Level 3 - Trace - #[clap(short, long, parse(from_occurrences))] - verbose: i32, - - #[clap(flatten)] - flags: Flags, - - #[clap(subcommand)] - command: SubCommand, + #[clap(long, default_value = "./dotup")] + config: PathBuf, } -#[derive(Parser, Debug)] +#[derive(Subcommand, Debug)] enum SubCommand { - Init(InitArgs), - Link(LinkArgs), - Unlink(UnlinkArgs), Install(InstallArgs), Uninstall(UninstallArgs), - Mv(MvArgs), Status(StatusArgs), + Format(FormatArgs), } -fn main() -> anyhow::Result<()> { - let args = Args::parse(); - - let log_level = match args.verbose { - 0 => "warn", - 1 => "info", - 2 => "debug", - _ => "trace", - }; - - Logger::try_with_env_or_str(log_level)? - .format(flexi_logger::colored_default_format) - .set_palette("196;208;32;198;15".to_string()) - .start()?; - - match args.command { - SubCommand::Init(cmd_args) => command_init(args.flags, cmd_args), - SubCommand::Link(cmd_args) => command_link(args.flags, cmd_args), - SubCommand::Unlink(cmd_args) => command_unlink(args.flags, cmd_args), - SubCommand::Install(cmd_args) => command_install(args.flags, cmd_args), - SubCommand::Uninstall(cmd_args) => command_uninstall(args.flags, cmd_args), - SubCommand::Mv(cmd_args) => command_mv(args.flags, cmd_args), - SubCommand::Status(cmd_args) => command_status(args.flags, cmd_args), - } -} - -/// Creates an empty depot file if one doesnt already exist. -/// -/// By default this will create the file in the current directory -/// but the `path` option can be used to change this path. #[derive(Parser, Debug)] -struct InitArgs { - path: Option, -} - -fn command_init(_global_flags: Flags, args: InitArgs) -> anyhow::Result<()> { - let depot_path = { - let mut path = args.path.unwrap_or_else(utils::default_depot_path); - if path.is_dir() { - path = path.join(DEFAULT_DEPOT_FILE_NAME); - } - path - }; - - if depot_path.exists() { - println!("Depot at {} already exists", depot_path.display()); - } else { - depot::write(&depot_path, &Default::default())?; - println!("Depot initialized at {}", depot_path.display()); - } - - Ok(()) +struct InstallArgs { + groups: Vec, } -/// Creates links -/// -/// If a link is created for a file that already had a link then the old link will be overwritten. -/// By default creating a link to a directory will recursively link all files under that -/// directory, to actually link a directory use the --directory flag. #[derive(Parser, Debug)] -struct LinkArgs { - #[clap(long)] - directory: bool, - - #[clap(min_values = 1)] - origins: Vec, - - destination: PathBuf, -} - -fn command_link(global_flags: Flags, args: LinkArgs) -> anyhow::Result<()> { - let mut dotup = utils::read_dotup(&global_flags)?; - for origin in args.origins { - if !args.directory && origin.is_dir() { - let directory = origin; - let origins = utils::collect_files_in_dir_recursive(&directory)?; - for origin in origins { - // unwrap: origin is under directory so stripping should not fail - let path_extra = origin.strip_prefix(&directory).unwrap(); - let destination = args.destination.join(path_extra); - dotup.link(&origin, &destination); - } - } else { - dotup.link(&origin, &args.destination); - }; - } - utils::write_dotup(&dotup)?; - Ok(()) +struct UninstallArgs { + groups: Vec, } -/// Unlinks files/directories. -/// -/// This will recursively remove links. If a path is a directory then it will remove all links -/// recursively. -/// The links are not uninstall by default, see the --uninstall parameter. #[derive(Parser, Debug)] -struct UnlinkArgs { - #[clap(long)] - uninstall: bool, - - paths: Vec, -} - -fn command_unlink(global_flags: Flags, args: UnlinkArgs) -> anyhow::Result<()> { - let mut dotup = utils::read_dotup(&global_flags)?; - dotup.unlink(args.paths.into_iter(), args.uninstall); - utils::write_dotup(&dotup)?; - Ok(()) +struct StatusArgs { + groups: Vec, } -/// Install links. (Creates symlinks). -/// -/// Installing a link will create the necessary directories. -/// If a file or directory already exists at the location a link would be installed this command will fail. #[derive(Parser, Debug)] -struct InstallArgs { - #[clap(long)] - directory: bool, +struct FormatArgs {} - paths: Vec, +#[derive(Parser, Debug)] +struct Args { + #[clap(flatten)] + globals: GlobalFlags, + #[clap(subcommand)] + command: SubCommand, } -fn command_install(global_flags: Flags, args: InstallArgs) -> anyhow::Result<()> { - let dotup = utils::read_dotup(&global_flags)?; - dotup.install(args.paths.into_iter()); - Ok(()) -} +fn main() -> anyhow::Result<()> { + env_logger::init(); -/// Uninstalls links. (Removes symlinks). -/// -/// Uninstalling a link for a file that didn't have a link will do nothing. -/// Uninstalling a directory will recursively uninstall all files under it. -/// Symlinks are only deleted if they were pointing to the correct file. -#[derive(Parser, Debug)] -struct UninstallArgs { - paths: Vec, + let args = Args::parse(); + match args.command { + SubCommand::Install(install) => command_install(args.globals, install), + SubCommand::Uninstall(uninstall) => command_uninstall(args.globals, uninstall), + SubCommand::Status(status) => command_status(args.globals, status), + SubCommand::Format(format) => command_format(args.globals, format), + } } -fn command_uninstall(global_flags: Flags, args: UninstallArgs) -> anyhow::Result<()> { - let dotup = utils::read_dotup(&global_flags)?; - dotup.uninstall(args.paths.into_iter()); - Ok(()) +impl GlobalFlags { + fn base_path_or_default(&self) -> PathBuf { + self.base.clone().unwrap_or_else(|| { + PathBuf::from(std::env::var("HOME").expect("failed to get HOME directory")) + }) + } } -/// Moves files/directories and updates links. -#[derive(Parser, Debug)] -struct MvArgs { - #[clap(min_values = 1)] - origins: Vec, - - destination: PathBuf, +fn command_install(globals: GlobalFlags, args: InstallArgs) -> anyhow::Result<()> { + let dotup = dotup::load_file(&globals.config).context("failed to parse config")?; + let cwd = std::env::current_dir().context("failed to get current directory")?; + let install_params = dotup::InstallParams { + cwd: &cwd, + home: &globals.base_path_or_default(), + }; + for group in args.groups { + match dotup.find_group_by_name(&group) { + Some(group_id) => dotup.install(install_params, group_id)?, + None => log::error!("group not found: {}", group), + }; + } + Ok(()) } -fn command_mv(global_flags: Flags, args: MvArgs) -> anyhow::Result<()> { - let mut dotup = utils::read_dotup(&global_flags)?; - dotup.mv(args.origins.into_iter(), args.destination); - utils::write_dotup(&dotup)?; +fn command_uninstall(globals: GlobalFlags, args: UninstallArgs) -> anyhow::Result<()> { + let dotup = dotup::load_file(&globals.config).context("failed to parse config")?; + let cwd = std::env::current_dir().context("failed to get current directory")?; + let uninstall_params = dotup::UninstallParams { + cwd: &cwd, + home: &globals.base_path_or_default(), + }; + for group in args.groups { + match dotup.find_group_by_name(&group) { + Some(group_id) => dotup.uninstall(uninstall_params, group_id)?, + None => log::error!("group not found: {}", group), + }; + } Ok(()) } -/// Shows information about links -#[derive(Parser, Debug)] -struct StatusArgs { - #[clap(default_value = ".")] - paths: Vec, +fn command_status(globals: GlobalFlags, args: StatusArgs) -> anyhow::Result<()> { + let dotup = dotup::load_file(&globals.config).context("failed to parse config")?; + let cwd = std::env::current_dir().context("failed to get current directory")?; + let install_params = dotup::InstallParams { + cwd: &cwd, + home: &globals.base_path_or_default(), + }; + for group in args.groups { + match dotup.find_group_by_name(&group) { + Some(group_id) => dotup.status(install_params, group_id)?, + None => log::error!("group not found: {}", group), + }; + } + Ok(()) } -fn command_status(global_flags: Flags, args: StatusArgs) -> anyhow::Result<()> { - let dotup = utils::read_dotup(&global_flags)?; - dotup.status(args.paths.into_iter()); +fn command_format(globals: GlobalFlags, _args: FormatArgs) -> anyhow::Result<()> { + dotup::format_file_inplace(&globals.config).context("failed to format config")?; Ok(()) } 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 @@ -use std::{ - collections::VecDeque, - path::{Component, Path, PathBuf}, -}; - -use crate::{ - dotup::{self, Dotup}, - Flags, -}; - -pub const DEFAULT_DEPOT_FILE_NAME: &str = ".depot"; - -/// Returns a list of canonical paths to all the files in `dir`. This includes files in -/// subdirectories. -/// Fails if dir isnt a directory or if there is some other io error. -pub fn collect_files_in_dir_recursive(dir: impl Into) -> anyhow::Result> { - let mut paths = Vec::new(); - let mut dirs = VecDeque::new(); - dirs.push_back(dir.into()); - - while let Some(dir) = dirs.pop_front() { - for entry in std::fs::read_dir(dir)? { - let entry = entry?; - let filetype = entry.file_type()?; - if filetype.is_dir() { - dirs.push_back(entry.path()); - } else { - paths.push(entry.path()); - } - } - } - - Ok(paths) -} - -pub fn collect_paths_in_dir(dir: impl AsRef) -> anyhow::Result> { - Ok(std::fs::read_dir(dir)? - .filter_map(|e| e.ok()) - .map(|e| e.path()) - .collect()) -} - -pub fn read_dotup(flags: &Flags) -> anyhow::Result { - let depot_path = depot_path_from_flags(flags)?; - let install_base = install_base_from_flags(flags); - dotup::read(depot_path, install_base) -} - -pub fn write_dotup(dotup: &Dotup) -> anyhow::Result<()> { - dotup::write(dotup) -} - -pub fn depot_path_from_flags(flags: &Flags) -> anyhow::Result { - match flags.depot { - Some(ref path) => Ok(path.clone()), - None => find_depot_path().ok_or_else(|| anyhow::anyhow!("Failed to find depot path")), - } -} - -pub fn default_depot_path() -> PathBuf { - current_working_directory().join(DEFAULT_DEPOT_FILE_NAME) -} - -pub fn find_depot_path() -> Option { - let mut cwd = current_working_directory(); - loop { - let path = cwd.join(DEFAULT_DEPOT_FILE_NAME); - if path.exists() { - break Some(path); - } - if !cwd.pop() { - break None; - } - } -} - -pub fn install_base_from_flags(flags: &Flags) -> PathBuf { - match flags.install_base { - Some(ref path) => path.clone(), - None => default_install_base(), - } -} - -pub fn default_install_base() -> PathBuf { - PathBuf::from(std::env::var("HOME").expect("Failed to obtain HOME environment variable")) -} -pub fn weakly_canonical(path: impl AsRef) -> PathBuf { - let cwd = current_working_directory(); - weakly_canonical_cwd(path, cwd) -} - -fn weakly_canonical_cwd(path: impl AsRef, cwd: PathBuf) -> PathBuf { - // Adapated from - // https://github.com/rust-lang/cargo/blob/fede83ccf973457de319ba6fa0e36ead454d2e20/src/cargo/util/paths.rs#L61 - let path = path.as_ref(); - - let mut components = path.components().peekable(); - let mut canonical = cwd; - let prefix = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() { - components.next(); - PathBuf::from(c.as_os_str()) - } else { - PathBuf::new() - }; - - for component in components { - match component { - Component::Prefix(_) => unreachable!(), - Component::RootDir => { - canonical = prefix.clone(); - canonical.push(component.as_os_str()) - } - Component::CurDir => {} - Component::ParentDir => { - canonical.pop(); - } - Component::Normal(p) => canonical.push(p), - }; - } - - canonical -} - -pub fn current_working_directory() -> PathBuf { - std::env::current_dir().expect("Failed to obtain current working directory") -} - -pub fn path_ends_with_slash(path: impl AsRef) -> bool { - path.as_ref() - .to_str() - .map(|s| s.ends_with('/')) - .unwrap_or_default() -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_weakly_canonical() { - let cwd = PathBuf::from("/home/user"); - assert_eq!( - PathBuf::from("/home/dest"), - weakly_canonical_cwd("../dest", cwd.clone()) - ); - assert_eq!( - PathBuf::from("/home/dest/configs/init.vim"), - weakly_canonical_cwd("../dest/configs/init.vim", cwd.clone()) - ); - assert_eq!( - PathBuf::from("/dest/configs/init.vim"), - weakly_canonical_cwd("/dest/configs/init.vim", cwd.clone()) - ); - assert_eq!( - PathBuf::from("/home/user/configs/nvim/lua/setup.lua"), - weakly_canonical_cwd("./configs/nvim/lua/setup.lua", cwd.clone()) - ); - assert_eq!( - PathBuf::from("/home/user/configs/nvim/lua/setup.lua"), - weakly_canonical_cwd("configs/nvim/lua/setup.lua", cwd) - ); - } - - #[test] - fn test_path_ends_with_slash() { - assert!(!path_ends_with_slash("")); - assert!(!path_ends_with_slash("/f1")); - assert!(!path_ends_with_slash("/f1/f2")); - assert!(!path_ends_with_slash("./f1/f2")); - assert!(!path_ends_with_slash("./f1/f2/../f3")); - - assert!(path_ends_with_slash("/")); - assert!(path_ends_with_slash("/f1/")); - assert!(path_ends_with_slash("f1/")); - assert!(path_ends_with_slash("f1/f2/")); - assert!(path_ends_with_slash("f1/f2/../f3/")); - } -} -- cgit