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