From e7588a7f175ca9b9604ce35d72086489da7a66e3 Mon Sep 17 00:00:00 2001 From: diogo464 Date: Fri, 7 Apr 2023 20:49:26 +0100 Subject: version bump: 0.2.0 --- Cargo.lock | 530 ++++++++++++++++++++++++++++++----- Cargo.toml | 12 +- src/dotup/action_tree.rs | 341 ----------------------- src/dotup/mod.rs | 697 ++++++++++++++++++++++++++++++++--------------- src/main.rs | 57 ++-- 5 files changed, 979 insertions(+), 658 deletions(-) delete mode 100644 src/dotup/action_tree.rs diff --git a/Cargo.lock b/Cargo.lock index 5b3fb5f..de9af33 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,46 @@ dependencies = [ "memchr", ] +[[package]] +name = "anstream" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "342258dd14006105c2b75ab1bd7543a03bdf0cfc94383303ac212a04939dff6f" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-wincon", + "concolor-override", + "concolor-query", + "is-terminal", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23ea9e81bd02e310c216d080f6223c179012256e5151c41db88d12c88a1684d2" + +[[package]] +name = "anstyle-parse" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7d1bb534e9efed14f3e5f44e7dd1a4f709384023a4165199a4241e18dff0116" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-wincon" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3127af6145b149f3287bb9a0d10ad9c5692dba8c53ad48285e5bec4063834fa" +dependencies = [ + "anstyle", + "windows-sys 0.45.0", +] + [[package]] name = "anyhow" version = "1.0.70" @@ -23,7 +63,7 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ - "hermit-abi", + "hermit-abi 0.1.19", "libc", "winapi", ] @@ -56,6 +96,12 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c676a478f63e9fa2dd5368a42f28bba0d6c560b775f38583c8bbaa7fcd67c9c" +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" + [[package]] name = "cfg-if" version = "1.0.0" @@ -64,51 +110,107 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "3.2.23" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5" +checksum = "046ae530c528f252094e4a77886ee1374437744b2bff1497aa898bbddbbb29b3" dependencies = [ - "atty", - "bitflags", + "clap_builder", "clap_derive", - "clap_lex", - "indexmap", "once_cell", +] + +[[package]] +name = "clap_builder" +version = "4.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "223163f58c9a40c3b0a43e1c4b50a9ce09f007ea2cb1ec258a687945b4b7929f" +dependencies = [ + "anstream", + "anstyle", + "bitflags", + "clap_lex", "strsim", - "termcolor", - "textwrap", ] [[package]] name = "clap_derive" -version = "3.2.18" +version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea0c8bce528c4be4da13ea6fead8965e95b6073585a2f05204bd8f4119f82a65" +checksum = "3f9644cd56d6b87dbe899ef8b053e331c0637664e9e21a33dfcdc36093f5c5c4" dependencies = [ "heck", - "proc-macro-error", "proc-macro2", "quote", - "syn 1.0.109", + "syn", ] [[package]] name = "clap_lex" -version = "0.2.4" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +checksum = "8a2dd5a6fe8c6e3502f568a6353e5273bbb15193ad9a89e457b9970798efbea1" + +[[package]] +name = "colored" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3616f750b84d8f0de8a58bda93e08e2a81ad3f523089b05f1dffecab48c6cbd" +dependencies = [ + "atty", + "lazy_static", + "winapi", +] + +[[package]] +name = "concolor-override" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a855d4a1978dc52fb0536a04d384c2c0c1aa273597f08b77c8c4d3b2eec6037f" + +[[package]] +name = "concolor-query" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d11d52c3d7ca2e6d0040212be9e4dbbcd78b6447f535b6b561f449427944cf" +dependencies = [ + "windows-sys 0.45.0", +] + +[[package]] +name = "crossterm" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" +dependencies = [ + "bitflags", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c" dependencies = [ - "os_str_bytes", + "winapi", ] [[package]] name = "dotup" -version = "0.1.0" +version = "0.2.0" dependencies = [ "anyhow", "clap", + "colored", "env_logger", "globset", + "inquire", "log", "nom", "nom_locate", @@ -116,6 +218,12 @@ dependencies = [ "thiserror", ] +[[package]] +name = "dyn-clone" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b0cf012f1230e43cd00ebb729c6bb58707ecfa8ad08b52ef3a4ccd2697fc30" + [[package]] name = "env_logger" version = "0.9.3" @@ -129,6 +237,27 @@ dependencies = [ "termcolor", ] +[[package]] +name = "errno" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d6a0976c999d473fe89ad888d5a284e55366d9dc9038b1ba2aa15128c4afa0" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys 0.45.0", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "fnv" version = "1.0.7" @@ -148,12 +277,6 @@ dependencies = [ "regex", ] -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - [[package]] name = "heck" version = "0.4.1" @@ -169,6 +292,12 @@ dependencies = [ "libc", ] +[[package]] +name = "hermit-abi" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" + [[package]] name = "humantime" version = "2.1.0" @@ -176,21 +305,72 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] -name = "indexmap" -version = "1.9.3" +name = "inquire" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +checksum = "fd079157ad94a32f7511b2e13037f3ae417ad80a6a9b0de29154d48b86f5d6c8" dependencies = [ - "autocfg", - "hashbrown", + "bitflags", + "crossterm", + "dyn-clone", + "lazy_static", + "newline-converter", + "thiserror", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220" +dependencies = [ + "hermit-abi 0.3.1", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "is-terminal" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" +dependencies = [ + "hermit-abi 0.3.1", + "io-lifetimes", + "rustix", + "windows-sys 0.48.0", ] +[[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.141" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3304a64d199bb964be99741b7a14d26972741915b3649639149b2479bb46f4b5" +[[package]] +name = "linux-raw-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59d8c75012853d2e872fb56bc8a2e53718e2cafe1a4c823143141c6d90c322f" + +[[package]] +name = "lock_api" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.17" @@ -212,6 +392,27 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "mio" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.45.0", +] + +[[package]] +name = "newline-converter" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f71d09d5c87634207f894c6b31b6a2b2c64ea3bdcf71bd5599fdbbe1600c00f" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "nom" version = "7.1.3" @@ -240,33 +441,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" [[package]] -name = "os_str_bytes" -version = "6.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ceedf44fb00f2d1984b0bc98102627ce622e083e49a5bacdb3e514fa4238e267" - -[[package]] -name = "proc-macro-error" -version = "1.0.4" +name = "parking_lot" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn 1.0.109", - "version_check", + "lock_api", + "parking_lot_core", ] [[package]] -name = "proc-macro-error-attr" -version = "1.0.4" +name = "parking_lot_core" +version = "0.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" dependencies = [ - "proc-macro2", - "quote", - "version_check", + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-sys 0.45.0", ] [[package]] @@ -287,6 +481,15 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + [[package]] name = "regex" version = "1.7.3" @@ -304,12 +507,62 @@ version = "0.6.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" +[[package]] +name = "rustix" +version = "0.37.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aef160324be24d31a62147fae491c14d2204a3865c7ca8c3b0d7f7bcb3ea635" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys 0.48.0", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + [[package]] name = "serde" version = "1.0.159" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c04e8343c3daeec41f58990b9d77068df31209f2af111e059e9fe9646693065" +[[package]] +name = "signal-hook" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "732768f1176d21d09e076c23a93123d40bba92d50c4058da34d45c8de8e682b9" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + [[package]] name = "slotmap" version = "1.0.6" @@ -320,21 +573,16 @@ dependencies = [ ] [[package]] -name = "strsim" -version = "0.10.0" +name = "smallvec" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" [[package]] -name = "syn" -version = "1.0.109" +name = "strsim" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" @@ -356,12 +604,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "textwrap" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" - [[package]] name = "thiserror" version = "1.0.40" @@ -379,7 +621,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.13", + "syn", ] [[package]] @@ -388,12 +630,36 @@ version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + +[[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + [[package]] name = "version_check" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + [[package]] name = "winapi" version = "0.3.9" @@ -424,3 +690,135 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.0", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +dependencies = [ + "windows_aarch64_gnullvm 0.48.0", + "windows_aarch64_msvc 0.48.0", + "windows_i686_gnu 0.48.0", + "windows_i686_msvc 0.48.0", + "windows_x86_64_gnu 0.48.0", + "windows_x86_64_gnullvm 0.48.0", + "windows_x86_64_msvc 0.48.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" diff --git a/Cargo.toml b/Cargo.toml index 2045a92..2c2a332 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,17 +1,19 @@ [package] name = "dotup" -version = "0.1.0" +version = "0.2.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] anyhow = "1" -clap = { version = "3.2.21", features = ["derive"] } +clap = { version = "4", features = ["derive"] } +colored = "2" env_logger = "0.9" -globset = "0.4.9" -log = "0.4.17" -nom = "7.1.1" +globset = "0.4" +inquire = "0.6" +log = "0.4" +nom = "7" nom_locate = "4" slotmap = "1" thiserror = "1" diff --git a/src/dotup/action_tree.rs b/src/dotup/action_tree.rs deleted file mode 100644 index 44d787e..0000000 --- a/src/dotup/action_tree.rs +++ /dev/null @@ -1,341 +0,0 @@ -use std::{collections::HashSet, ffi::OsString, ops::Index, path::PathBuf}; - -use slotmap::SlotMap; - -use super::{AbsPath, AbsPathBuf}; - -slotmap::new_key_type! { - pub struct NodeID; - pub struct ActionID; -} - -#[derive(Debug)] -pub enum Action { - Link { source: PathBuf }, - Copy { source: PathBuf }, -} - -#[derive(Debug)] -pub struct TreeAction { - path: AbsPathBuf, - action: Action, -} - -#[derive(Debug)] -enum TreeNodeKind { - Action(ActionID), - SubTree(HashSet), -} - -#[derive(Debug)] -struct TreeNode { - path: AbsPathBuf, - component: OsString, - kind: TreeNodeKind, -} - -#[derive(Debug)] -pub struct ActionTree { - root_id: NodeID, - nodes: SlotMap, - actions: SlotMap, -} - -// -------------------- TreeAction -------------------- // - -impl TreeAction { - pub fn target(&self) -> &AbsPath { - &self.path - } - - pub fn action(&self) -> &Action { - &self.action - } -} - -// -------------------- TreeNodeKind -------------------- // - -#[allow(unused)] -impl TreeNodeKind { - fn as_action(&self) -> ActionID { - match self { - Self::Action(id) => *id, - _ => unreachable!(), - } - } - - fn as_action_mut(&mut self) -> &mut ActionID { - match self { - Self::Action(id) => id, - _ => unreachable!(), - } - } - - fn as_subtree(&self) -> &HashSet { - match self { - Self::SubTree(ids) => ids, - _ => unreachable!(), - } - } - - fn as_subtree_mut(&mut self) -> &mut HashSet { - match self { - Self::SubTree(ids) => ids, - _ => unreachable!(), - } - } -} - -// -------------------- ActionTree -------------------- // - -impl Index for ActionTree { - type Output = TreeAction; - - fn index(&self, index: ActionID) -> &Self::Output { - self.action(index).unwrap() - } -} - -impl ActionTree { - pub fn new() -> Self { - let mut nodes = SlotMap::with_key(); - let root_id = nodes.insert(TreeNode { - path: AbsPathBuf::default(), - component: OsString::new(), - kind: TreeNodeKind::SubTree(Default::default()), - }); - - Self { - root_id, - nodes, - actions: Default::default(), - } - } - - pub fn insert(&mut self, target: &AbsPath, action: Action) -> ActionID { - let action_id = self.actions.insert(TreeAction { - path: target.to_owned(), - action, - }); - self.force_insert_at(target, TreeNodeKind::Action(action_id)); - action_id - } - - pub fn install(&self) -> std::io::Result<()> { - for action_id in self.action_ids() { - self.install_action(action_id)?; - } - Ok(()) - } - - pub fn is_installed(&self, action_id: ActionID) -> bool { - let action = &self.actions[action_id]; - let target = action.target(); - match action.action() { - Action::Link { source } => { - let link = match std::fs::read_link(target) { - Ok(link) => link, - Err(_) => return false, - }; - link.canonicalize().unwrap() == source.canonicalize().unwrap() - } - Action::Copy { .. } => target.as_ref().exists(), - } - } - - pub fn uninstall(&self) -> std::io::Result<()> { - for action_id in self.action_ids() { - self.uninstall_action(action_id)?; - } - Ok(()) - } - - pub fn install_action(&self, action_id: ActionID) -> std::io::Result<()> { - let action = &self[action_id]; - match &action.action { - Action::Link { source } => { - let target = action.target(); - log::info!("Linking {:?} -> {:?}", source, target); - if target.as_ref().is_symlink() { - log::trace!("{:?} is a symlink, removing it", target); - std::fs::remove_file(target)?; - } - if let Some(parent) = target.parent() { - log::trace!("creating all directories up to {:?}", parent); - std::fs::create_dir_all(parent.as_ref())?; - } - log::trace!("creating symlink {:?} -> {:?}", source, target); - std::os::unix::fs::symlink(source, target)?; - } - Action::Copy { source: _ } => todo!(), - } - Ok(()) - } - - pub fn uninstall_action(&self, action_id: ActionID) -> std::io::Result<()> { - let action = &self[action_id]; - if let Action::Link { ref source } = action.action { - let target = action.target(); - if target.as_ref().is_symlink() { - log::trace!("{:?} is a symlink", target); - let symlink_target = std::fs::read_link(target.as_ref())?; - if symlink_target == *source { - log::info!("symlink target is {:?}, removing it", source); - std::fs::remove_file(target)?; - } else { - log::trace!( - "symlink target is {:?}, not {:?}, not removing it", - symlink_target, - source - ); - } - } - } - Ok(()) - } - - pub fn action_ids(&self) -> impl Iterator + '_ { - self.actions.keys() - } - - pub fn action(&self, action_id: ActionID) -> Option<&TreeAction> { - self.actions.get(action_id) - } - - /// Creates all nodes up to the given path. - /// If one of the nodes is an action node, it will be replaced with a subtree node. - fn force_insert_at(&mut self, target: &AbsPath, kind: TreeNodeKind) -> NodeID { - let mut curr = self.root_id; - for comp in target.components() { - { - // Try to find node if it exists - let curr_node = &mut self.nodes[curr]; - match curr_node.kind { - TreeNodeKind::Action(action) => { - self.actions.remove(action); - curr_node.kind = TreeNodeKind::SubTree(Default::default()); - match curr_node.kind { - TreeNodeKind::SubTree(ref mut children) => children, - _ => unreachable!(), - } - } - TreeNodeKind::SubTree(ref mut children) => children, - }; - - let children = self.nodes[curr].kind.as_subtree(); - for &child_id in children.iter() { - let child_node = &self.nodes[child_id]; - if child_node.component == comp { - curr = child_id; - break; - } - } - } - { - // Create new node - let new_node = TreeNode { - path: self.nodes[curr].path.join(comp), - component: comp.to_owned(), - kind: TreeNodeKind::SubTree(Default::default()), - }; - let new_id = self.nodes.insert(new_node); - match &mut self.nodes[curr].kind { - TreeNodeKind::SubTree(children) => children.insert(new_id), - _ => unreachable!(), - }; - curr = new_id; - } - } - let prev_kind = std::mem::replace(&mut self.nodes[curr].kind, kind); - if let TreeNodeKind::SubTree(children) = prev_kind { - for &child in children.iter() { - self.remove_node(child); - } - } - curr - } - - /// Removes the given node. - /// Does not remove it from the parent's children node. - fn remove_node(&mut self, node_id: NodeID) { - let node = self - .nodes - .remove(node_id) - .expect("Node being removed does not exist"); - match node.kind { - TreeNodeKind::Action(action) => { - self.actions.remove(action); - } - TreeNodeKind::SubTree(children) => { - for child in children { - self.remove_node(child); - } - } - }; - } -} - -#[cfg(test)] -mod tests { - use std::{convert::TryFrom, path::Path}; - - use super::*; - - #[test] - fn empty_tree() { - let _ = ActionTree::new(); - } - - #[test] - fn single_action() { - let mut tree = ActionTree::new(); - - let action_id = tree.insert( - TryFrom::try_from("/home/user/.config/nvim").unwrap(), - Action::Link { - source: PathBuf::from("nvim"), - }, - ); - - let action = &tree[action_id]; - assert_eq!( - action.path.as_path(), - AbsPath::new(Path::new("/home/user/.config/nvim")) - ); - } - - #[test] - fn subtree_replacement() { - let mut tree = ActionTree::new(); - - let action_id = tree.insert( - TryFrom::try_from("/home/user/.config/nvim").unwrap(), - Action::Link { - source: PathBuf::from("nvim"), - }, - ); - let action_id_original = action_id; - - let action = &tree[action_id]; - assert_eq!( - action.path.as_path(), - AbsPath::new(Path::new("/home/user/.config/nvim")) - ); - - let action_id = tree.insert( - TryFrom::try_from("/home/user/.config/nvim/init.vim").unwrap(), - Action::Link { - source: PathBuf::from("nvim/init.vim"), - }, - ); - - let action = &tree[action_id]; - assert_eq!( - action.path.as_path(), - AbsPath::new(Path::new("/home/user/.config/nvim/init.vim")) - ); - - eprintln!("{:#?}", tree); - assert!(tree.action(action_id_original).is_none()); - } -} diff --git a/src/dotup/mod.rs b/src/dotup/mod.rs index a70cde5..cb163db 100644 --- a/src/dotup/mod.rs +++ b/src/dotup/mod.rs @@ -1,13 +1,12 @@ -mod action_tree; mod cfg; mod paths; -use std::collections::HashSet; use std::{ - collections::HashMap, + collections::{HashMap, HashSet, VecDeque}, path::{Path, PathBuf}, }; +use colored::Colorize; use slotmap::SlotMap; use thiserror::Error; @@ -20,37 +19,59 @@ slotmap::new_key_type! { pub struct GroupID; } #[derive(Debug, Error)] pub enum Error { #[error(transparent)] - ParseError(#[from] cfg::ParseError), + InvalidConfig(#[from] cfg::ParseError), #[error("error: {0}")] Custom(String), #[error(transparent)] IOError(#[from] std::io::Error), } -#[derive(Debug, Default)] -pub struct Group { - name: String, - parent: GroupID, - children: HashMap, - actions: Vec, +impl Error { + fn custom(e: impl std::fmt::Display) -> Self { + Self::Custom(e.to_string()) + } +} + +#[derive(Debug, Clone)] +pub struct Context { + working_directory: AbsPathBuf, + destination_directory: AbsPathBuf, +} + +impl Context { + pub fn new( + working_directory: impl Into, + destination_directory: impl Into, + ) -> std::io::Result { + let working_directory = working_directory.into().canonicalize()?; + let destination_directory = destination_directory.into().canonicalize()?; + let working_directory = + AbsPathBuf::try_from(working_directory).map_err(std::io::Error::other)?; + let destination_directory = + AbsPathBuf::try_from(destination_directory).map_err(std::io::Error::other)?; + Ok(Self { + working_directory, + destination_directory, + }) + } } #[derive(Debug)] -pub struct Dotup { - root_id: GroupID, - groups: SlotMap, +pub struct InstallParams { + pub force: bool, } -#[derive(Debug, Clone, Copy)] -pub struct InstallParams<'p> { - pub cwd: &'p Path, - pub home: &'p Path, +impl Default for InstallParams { + fn default() -> Self { + Self { force: false } + } } -#[derive(Debug, Clone, Copy)] -pub struct UninstallParams<'p> { - pub cwd: &'p Path, - pub home: &'p Path, +#[derive(Debug)] +pub struct Dotup { + context: Context, + root_id: GroupID, + groups: SlotMap, } #[derive(Debug)] @@ -59,21 +80,31 @@ struct KeyValueParser { keyvalues: Vec, } +#[derive(Debug, Default)] +struct Group { + name: String, + parent: GroupID, + children: HashMap, + actions: Vec, +} + #[derive(Debug, Clone)] struct IncludeAction { group: String, + group_id: GroupID, } #[derive(Debug, Clone)] struct LinkAction { - source: PathBuf, - target: PathBuf, + source: AbsPathBuf, + target: AbsPathBuf, } +#[allow(dead_code)] #[derive(Debug, Clone)] struct CopyAction { - source: PathBuf, - target: PathBuf, + source: AbsPathBuf, + target: AbsPathBuf, } #[derive(Debug, Clone)] @@ -83,14 +114,20 @@ enum Action { Copy(CopyAction), } -pub fn load(content: &str) -> Result { +#[derive(Debug, Clone)] +enum ExecutableAction { + Link(LinkAction), + Copy(CopyAction), +} + +pub fn load(context: Context, content: &str) -> Result { let config = cfg::parse(content)?; - Dotup::from_config(config) + new(context, config) } -pub fn load_file(path: impl AsRef) -> Result { +pub fn load_file(context: Context, path: impl AsRef) -> Result { let content = std::fs::read_to_string(path)?; - load(&content) + load(context, &content) } pub fn format(content: &str) -> Result { @@ -109,190 +146,339 @@ pub fn format_file_inplace(path: &Path) -> Result<()> { Ok(()) } -// -------------------- Dotup -------------------- // +pub fn install(dotup: &Dotup, params: &InstallParams, group: &str) -> Result<()> { + fn prompt_overwrite(params: &InstallParams, target: &AbsPath) -> Result { + if params.force { + return Ok(true); + } -impl Dotup { - pub fn find_group_by_name(&self, name: &str) -> Option { - self.find_group_by_name_rooted(self.root_id, name) + let result = inquire::Confirm::new(&format!( + "overwrite existing file/directory '{}'?", + target.display() + )) + .with_default(false) + .with_help_message("Delete the existing file/directory") + .prompt(); + + match result { + Ok(overwrite) => Ok(overwrite), + Err(e) => match e { + inquire::InquireError::NotTTY => Ok(false), + _ => Err(Error::custom(e)), + }, + } } - pub fn install(&self, params: InstallParams, group_id: GroupID) -> Result<()> { - let action_tree = self.build_action_tree(params.cwd, params.home, group_id)?; - action_tree.install()?; - Ok(()) + let group_id = get_group_by_name(dotup, group)?; + let executable = collect_group_executable_actions(dotup, group_id)?; + + for action in executable { + match action { + ExecutableAction::Link(LinkAction { source, target }) => { + log::debug!("linking '{}' to '{}'", source.display(), target.display()); + if fs_exists(&target)? { + let metadata = fs_symlink_metadata(&target)?; + + // Early return if the symlink already points to the correct source + if metadata.is_symlink() && fs_symlink_points_to(&target, &source)? { + return Ok(()); + } + + if !prompt_overwrite(params, &target)? { + return Ok(()); + } + + fs_remove(&target)?; + } + + fs_create_dir_all_upto(&target)?; + fs_create_symlink(&source, &target)?; + } + ExecutableAction::Copy(_) => todo!(), + } } - pub fn uninstall(&self, params: UninstallParams, group_id: GroupID) -> Result<()> { - let action_tree = self.build_action_tree(params.cwd, params.home, group_id)?; - action_tree.uninstall()?; - Ok(()) + Ok(()) +} + +pub fn uninstall(dotup: &Dotup, group: &str) -> Result<()> { + let group_id = get_group_by_name(dotup, group)?; + let executable = collect_group_executable_actions(dotup, group_id)?; + + for action in executable { + match action { + ExecutableAction::Link(LinkAction { source, target }) => { + if !fs_exists(&target)? { + return Ok(()); + } + + if fs_symlink_points_to(&target, &source)? { + fs_remove(&target)?; + } + } + ExecutableAction::Copy(_) => todo!(), + } } - pub fn status(&self, params: InstallParams, group_id: GroupID) -> Result<()> { - let action_tree = self.build_action_tree(params.cwd, params.home, group_id)?; - for action_id in action_tree.action_ids() { - let prefix = if action_tree.is_installed(action_id) { - "INSTALLED" - } else { - "NOT INSTALLED" - }; - let action = action_tree.action(action_id).unwrap(); - let source = match action.action() { - action_tree::Action::Link { ref source } => source, - action_tree::Action::Copy { ref source } => source, - }; - let target = action.target(); - println!("{}: {} -> {}", prefix, source.display(), target.display()); + Ok(()) +} + +pub fn status(dotup: &Dotup, group: &str) -> Result<()> { + fn display_status(dotup: &Dotup, group_id: GroupID, depth: u32) -> Result<()> { + let group = &dotup.groups[group_id]; + + println!("{}{}", " ".repeat(depth as usize), group.name.blue()); + log::trace!("displaying status for group '{}'", group.name); + + for action in group.actions.iter() { + match action { + Action::Include(include) => { + log::trace!("displaying status for included group '{}'", include.group); + display_status(dotup, include.group_id, depth + 1)?; + } + Action::Link(link) => { + log::trace!("displaying status for link '{}'", link.target.display()); + + let target = link.target.display(); + let source = link.source.display(); + let installed = is_link_installed(&link)?; + let output = format!( + "{}{} -> {}", + " ".repeat(depth as usize + 1), + target, + source + ); + println!( + "{}", + if installed { + output.green() + } else { + output.red() + } + ); + } + Action::Copy(_) => todo!(), + } } Ok(()) } + + let group_id = get_group_by_name(dotup, group)?; + display_status(dotup, group_id, 0) +} + +fn new(context: Context, config: cfg::Config) -> Result { + let mut groups = SlotMap::default(); + let root_id = groups.insert(Default::default()); + let mut dotup = Dotup { + context, + root_id, + groups, + }; + + for group_cfg in config.groups { + insert_config_group(&mut dotup, root_id, group_cfg)?; + } + + resolve_includes(&mut dotup)?; + + Ok(dotup) } -impl Dotup { - fn from_config(config: cfg::Config) -> Result { - let mut groups = SlotMap::default(); - let root_id = groups.insert(Default::default()); - let mut dotup = Self { root_id, groups }; +fn insert_config_group( + dotup: &mut Dotup, + parent_id: GroupID, + mut group_cfg: cfg::Group, +) -> Result<()> { + let parent = &mut dotup.groups[parent_id]; + if parent.children.contains_key(&group_cfg.name) { + return Err(Error::Custom(format!( + "group '{}' at {} already exists", + group_cfg.name, group_cfg.location, + ))); + } - for group in config.groups { - dotup.insert_group(root_id, group)?; + let mut group = Group { + name: group_cfg.name.clone(), + parent: parent_id, + children: Default::default(), + actions: Default::default(), + }; + + for item in group_cfg + .items + .drain_filter(|item| std::matches!(item, cfg::GroupItem::Action(_))) + { + if let cfg::GroupItem::Action(action) = item { + let action = convert_config_action(&dotup.context, action)?; + group.actions.push(action); } + } + + let group_id = dotup.groups.insert(group); + let parent = &mut dotup.groups[parent_id]; + parent.children.insert(group_cfg.name, group_id); + + for item in group_cfg.items { + if let cfg::GroupItem::Group(group) = item { + insert_config_group(dotup, group_id, group)?; + } + } + + Ok(()) +} - Ok(dotup) +fn resolve_includes(dotup: &mut Dotup) -> Result<()> { + struct Patch { + group_id: GroupID, + action_idx: usize, + target_id: GroupID, } - fn find_group_by_name_rooted(&self, root: GroupID, name: &str) -> Option { - let trimmed = name.trim_start_matches('.'); - let rel_levels = name.len() - trimmed.len(); - let mut current = self.root_id; - - if rel_levels != 0 { - current = root; - for _ in 0..rel_levels - 1 { - current = self.groups[current].parent; - if current == self.root_id { - break; + let mut patches = Vec::new(); + for group_id in dotup.groups.keys() { + for idx in 0..dotup.groups[group_id].actions.len() { + let action = &dotup.groups[group_id].actions[idx]; + let target = match action { + Action::Include(include) => include.group.as_str(), + _ => continue, + }; + + let target_id = match find_group_by_name_rooted(dotup, group_id, target) { + Some(target_id) => target_id, + None => { + return Err(Error::Custom(format!("group '{}' not found", target))); } - } - } + }; - for comp in trimmed.split('.') { - let group = &self.groups[current]; - let child_id = group.children.get(comp)?; - current = *child_id; + patches.push(Patch { + group_id, + action_idx: idx, + target_id, + }); } - Some(current) } - fn insert_group(&mut self, parent_id: GroupID, mut group_cfg: cfg::Group) -> Result<()> { - let parent = &mut self.groups[parent_id]; - if parent.children.contains_key(&group_cfg.name) { - return Err(Error::Custom(format!( - "group '{}' at {} already exists", - group_cfg.name, group_cfg.location, - ))); + for patch in patches { + let group = &mut dotup.groups[patch.group_id]; + let action = &mut group.actions[patch.action_idx]; + if let Action::Include(include) = action { + include.group_id = patch.target_id; } + } - let mut group = Group { - name: group_cfg.name.clone(), - parent: parent_id, - children: Default::default(), - actions: Default::default(), - }; - - for item in group_cfg - .items - .drain_filter(|item| std::matches!(item, cfg::GroupItem::Action(_))) - { - if let cfg::GroupItem::Action(action) = item { - let action = cfg_action_to_action(action)?; - group.actions.push(action); - } + Ok(()) +} + +fn convert_config_action(context: &Context, cfg_action: cfg::Action) -> Result { + let mut parser = KeyValueParser::new(cfg_action.location, cfg_action.keyvalues); + match cfg_action.kind.as_str() { + "include" => { + let group = parser.expect("group")?; + parser.finalize()?; + Ok(Action::Include(IncludeAction { + group, + group_id: Default::default(), + })) + } + "link" => { + let source = PathBuf::from(parser.expect("source")?); + let target = PathBuf::from(parser.expect("target")?); + parser.finalize()?; + Ok(Action::Link(LinkAction { + source: make_path_absolute(&context.working_directory, &source), + target: make_path_absolute(&context.destination_directory, &target), + })) } + "copy" => { + let source = PathBuf::from(parser.expect("source")?); + let target = PathBuf::from(parser.expect("target")?); + parser.finalize()?; + Ok(Action::Copy(CopyAction { + source: make_path_absolute(&context.working_directory, &source), + target: make_path_absolute(&context.destination_directory, &target), + })) + } + _ => Err(Error::Custom(format!( + "unknown action '{}' at {}", + cfg_action.kind, cfg_action.location + ))), + } +} - let group_id = self.groups.insert(group); - let parent = &mut self.groups[parent_id]; - parent.children.insert(group_cfg.name, group_id); +fn get_group_by_name(dotup: &Dotup, name: &str) -> Result { + find_group_by_name(dotup, name) + .ok_or_else(|| Error::Custom(format!("group '{}' not found", name,))) +} - for item in group_cfg.items { - if let cfg::GroupItem::Group(group) = item { - self.insert_group(group_id, group)?; +fn find_group_by_name(dotup: &Dotup, name: &str) -> Option { + find_group_by_name_rooted(dotup, dotup.root_id, name) +} + +fn find_group_by_name_rooted(dotup: &Dotup, root: GroupID, name: &str) -> Option { + let trimmed = name.trim_start_matches('.'); + let rel_levels = name.len() - trimmed.len(); + let mut current = dotup.root_id; + + if rel_levels != 0 { + current = root; + for _ in 0..rel_levels - 1 { + current = dotup.groups[current].parent; + if current == dotup.root_id { + break; } } + } - Ok(()) + for comp in trimmed.split('.') { + let group = &dotup.groups[current]; + let child_id = group.children.get(comp)?; + current = *child_id; } + Some(current) +} - fn build_action_tree( - &self, - cwd: &Path, - home: &Path, - group_id: GroupID, - ) -> Result { - fn inner_helper( - dotup: &Dotup, - cwd: &AbsPath, - home: &AbsPath, - group_id: GroupID, - tree: &mut action_tree::ActionTree, - visited: &mut HashSet, - ) -> Result<()> { - if visited.contains(&group_id) { - return Ok(()); - } - visited.insert(group_id); - - let group = &dotup.groups[group_id]; - for action in group.actions.iter() { - match action { - Action::Include(action) => { - let include_id = dotup - .find_group_by_name_rooted(group_id, &action.group) - .ok_or_else(|| { - Error::Custom(format!( - "group '{}' not found in include from group '{}'", - action.group, dotup.groups[group_id].name, - )) - })?; - inner_helper(dotup, cwd, home, include_id, tree, visited)?; - } - Action::Link(action) => { - let source = make_absolute_path(cwd, &action.source).into(); - let target = make_absolute_path(home, &action.target); - tree.insert(&target, action_tree::Action::Link { source }); - } - Action::Copy(action) => { - let source = make_absolute_path(cwd, &action.source).into(); - let target = make_absolute_path(home, &action.target); - tree.insert(&target, action_tree::Action::Copy { source }); - } +fn collect_group_executable_actions( + dotup: &Dotup, + group_id: GroupID, +) -> Result> { + let mut executable = Vec::new(); + let mut visited = HashSet::new(); + let mut queue = VecDeque::from_iter(std::iter::once(group_id)); + + while let Some(group_id) = queue.pop_front() { + if !visited.insert(group_id) { + continue; + } + + let group = &dotup.groups[group_id]; + for action in &group.actions { + match action { + Action::Include(include) => { + queue.push_back(include.group_id); } + Action::Link(action) => executable.push(ExecutableAction::Link(action.clone())), + Action::Copy(action) => executable.push(ExecutableAction::Copy(action.clone())), } - - Ok(()) } + } + + Ok(executable) +} + +fn is_link_installed(link: &LinkAction) -> Result { + if !fs_exists(&link.target)? { + Ok(false) + } else { + fs_symlink_points_to(&link.target, &link.source) + } +} - let cwd = AbsPathBuf::try_from( - cwd.canonicalize() - .expect("failed to canonicalize current working directory path"), - ) - .unwrap(); - let home = AbsPathBuf::try_from( - home.canonicalize() - .expect("failed to canonicalize home directory path"), - ) - .unwrap(); - - let mut tree = action_tree::ActionTree::new(); - inner_helper( - self, - &cwd, - &home, - group_id, - &mut tree, - &mut Default::default(), - )?; - Ok(tree) +fn make_path_absolute(root: &AbsPath, path: &Path) -> AbsPathBuf { + if path.is_absolute() { + AbsPathBuf::try_from(path.to_owned()).unwrap() + } else { + root.join(path) } } @@ -328,47 +514,128 @@ impl KeyValueParser { } } -// -------------------- Misc -------------------- // +// -------------------- Filesystem -------------------- // -fn cfg_action_to_action(cfg_action: cfg::Action) -> Result { - let mut parser = KeyValueParser::new(cfg_action.location, cfg_action.keyvalues); - match cfg_action.kind.as_str() { - "include" => { - let group = parser.expect("group")?; - parser.finalize()?; - Ok(Action::Include(IncludeAction { group })) - } - "link" => { - let source = parser.expect("source")?; - let target = parser.expect("target")?; - parser.finalize()?; - Ok(Action::Link(LinkAction { - source: PathBuf::from(source), - target: PathBuf::from(target), - })) - } - "copy" => { - let source = parser.expect("source")?; - let target = parser.expect("target")?; - parser.finalize()?; - Ok(Action::Copy(CopyAction { - source: PathBuf::from(source), - target: PathBuf::from(target), - })) - } - _ => Err(Error::Custom(format!( - "unknown action '{}' at {}", - cfg_action.kind, cfg_action.location - ))), - } +fn fs_exists(path: impl AsRef) -> Result { + path.as_ref().try_exists().map_err(|err| { + Error::Custom(format!( + "failed to check existence of target '{}': {}", + path.as_ref().display(), + err + )) + }) } -/// Returns `path` if it is already absolute. -/// Otherwise makes it absolute by prepending `self.root`. -fn make_absolute_path(root: &AbsPath, path: &Path) -> AbsPathBuf { - if path.is_absolute() { - AbsPathBuf::try_from(path).unwrap() +#[allow(unused)] +fn fs_metadata(path: impl AsRef) -> Result { + let path = path.as_ref(); + std::fs::metadata(path).map_err(|err| { + Error::Custom(format!( + "failed to get metadata of target '{}': {}", + path.display(), + err + )) + }) +} + +fn fs_symlink_metadata(path: impl AsRef) -> Result { + let path = path.as_ref(); + std::fs::symlink_metadata(path).map_err(|err| { + Error::Custom(format!( + "failed to get metadata of target '{}': {}", + path.display(), + err + )) + }) +} + +fn fs_read_symlink(path: impl AsRef) -> Result { + let path = path.as_ref(); + std::fs::read_link(path).map_err(|err| { + Error::Custom(format!( + "failed to read symlink '{}': {}", + path.display(), + err + )) + }) +} + +fn fs_canonicalize(path: impl AsRef) -> Result { + let path = path.as_ref(); + path.canonicalize().map_err(|err| { + Error::Custom(format!( + "failed to canonicalize path '{}': {}", + path.display(), + err + )) + }) +} + +fn fs_symlink_points_to(path: impl AsRef, target: impl AsRef) -> Result { + let path = path.as_ref(); + let target = target.as_ref(); + let link_target = fs_read_symlink(path)?; + let target_canonical = fs_canonicalize(target)?; + let link_target_canonical = fs_canonicalize(link_target)?; + Ok(target_canonical == link_target_canonical) +} + +fn fs_remove(path: impl AsRef) -> Result<()> { + let path = path.as_ref(); + log::debug!("removing target '{}'", path.display()); + + if !fs_exists(path)? { + return Ok(()); + } + + let metadata = fs_symlink_metadata(path)?; + if metadata.is_dir() { + std::fs::remove_dir_all(path).map_err(|err| { + Error::Custom(format!( + "failed to remove target '{}': {}", + path.display(), + err + )) + }) } else { - AbsPathBuf::from_rel(root, TryFrom::try_from(path).unwrap()) + std::fs::remove_file(path).map_err(|err| { + Error::Custom(format!( + "failed to remove target '{}': {}", + path.display(), + err + )) + }) } } + +fn fs_create_symlink(source: impl AsRef, target: impl AsRef) -> Result<()> { + let source = source.as_ref(); + let target = target.as_ref(); + log::debug!( + "creating symlink '{}' -> '{}'", + target.display(), + source.display() + ); + std::os::unix::fs::symlink(source, target).map_err(|err| { + Error::Custom(format!( + "failed to create symlink '{}' -> '{}': {}", + target.display(), + source.display(), + err + )) + }) +} + +fn fs_create_dir_all_upto(path: impl AsRef) -> Result<()> { + let path = path.as_ref(); + let parent = path.parent().ok_or_else(|| { + Error::Custom(format!("failed to get parent of path '{}'", path.display())) + })?; + std::fs::create_dir_all(parent).map_err(|err| { + Error::Custom(format!( + "failed to create directory '{}': {}", + parent.display(), + err + )) + }) +} diff --git a/src/main.rs b/src/main.rs index 80a03b9..f035a45 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,13 @@ #![feature(drain_filter)] +#![feature(io_error_other)] -//pub mod config; pub mod dotup; use std::path::PathBuf; use anyhow::Context; use clap::{Parser, Subcommand}; +use dotup::InstallParams; #[derive(Parser, Debug)] struct GlobalFlags { @@ -27,6 +28,9 @@ enum SubCommand { #[derive(Parser, Debug)] struct InstallArgs { + #[clap(short, long)] + force: bool, + groups: Vec, } @@ -47,6 +51,7 @@ struct FormatArgs {} struct Args { #[clap(flatten)] globals: GlobalFlags, + #[clap(subcommand)] command: SubCommand, } @@ -64,6 +69,10 @@ fn main() -> anyhow::Result<()> { } impl GlobalFlags { + fn get_working_dir(&self) -> PathBuf { + self.config.parent().unwrap().to_path_buf() + } + fn base_path_or_default(&self) -> PathBuf { self.base.clone().unwrap_or_else(|| { PathBuf::from(std::env::var("HOME").expect("failed to get HOME directory")) @@ -72,49 +81,29 @@ impl GlobalFlags { } fn command_install(globals: GlobalFlags, args: InstallArgs) -> anyhow::Result<()> { - let dotup = dotup::load_file(&globals.config).context("failed to parse config")?; - let cwd = std::env::current_dir().context("failed to get current directory")?; - let install_params = dotup::InstallParams { - cwd: &cwd, - home: &globals.base_path_or_default(), - }; + let context = helper_new_context(&globals)?; + let dotup = dotup::load_file(context, &globals.config).context("failed to parse config")?; + let params = InstallParams { force: args.force }; for group in args.groups { - match dotup.find_group_by_name(&group) { - Some(group_id) => dotup.install(install_params, group_id)?, - None => log::error!("group not found: {}", group), - }; + dotup::install(&dotup, ¶ms, &group)?; } Ok(()) } fn command_uninstall(globals: GlobalFlags, args: UninstallArgs) -> anyhow::Result<()> { - let dotup = dotup::load_file(&globals.config).context("failed to parse config")?; - let cwd = std::env::current_dir().context("failed to get current directory")?; - let uninstall_params = dotup::UninstallParams { - cwd: &cwd, - home: &globals.base_path_or_default(), - }; + let context = helper_new_context(&globals)?; + let dotup = dotup::load_file(context, &globals.config).context("failed to parse config")?; for group in args.groups { - match dotup.find_group_by_name(&group) { - Some(group_id) => dotup.uninstall(uninstall_params, group_id)?, - None => log::error!("group not found: {}", group), - }; + dotup::uninstall(&dotup, &group)?; } Ok(()) } fn command_status(globals: GlobalFlags, args: StatusArgs) -> anyhow::Result<()> { - let dotup = dotup::load_file(&globals.config).context("failed to parse config")?; - let cwd = std::env::current_dir().context("failed to get current directory")?; - let install_params = dotup::InstallParams { - cwd: &cwd, - home: &globals.base_path_or_default(), - }; + let context = helper_new_context(&globals)?; + let dotup = dotup::load_file(context, &globals.config).context("failed to parse config")?; for group in args.groups { - match dotup.find_group_by_name(&group) { - Some(group_id) => dotup.status(install_params, group_id)?, - None => log::error!("group not found: {}", group), - }; + dotup::status(&dotup, &group)?; } Ok(()) } @@ -123,3 +112,9 @@ fn command_format(globals: GlobalFlags, _args: FormatArgs) -> anyhow::Result<()> dotup::format_file_inplace(&globals.config).context("failed to format config")?; Ok(()) } + +fn helper_new_context(globals: &GlobalFlags) -> anyhow::Result { + let cwd = globals.get_working_dir(); + let home = globals.base_path_or_default(); + Ok(dotup::Context::new(cwd, home)?) +} -- cgit