Improve CLI
+ Add repository URL as valid input for dotfiles + Add regex, chrono crates + Add custom error types for Kot + Add uninstallation of dotfiles to revert changes when error is encountered + Update README, help text
This commit is contained in:
		
							parent
							
								
									eabc227c09
								
							
						
					
					
						commit
						a01ab6b532
					
				
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@ -1,2 +1,4 @@
 | 
				
			|||||||
**/.idea/**
 | 
					**/.idea/**
 | 
				
			||||||
/target
 | 
					/target
 | 
				
			||||||
 | 
					dotfiles/**
 | 
				
			||||||
 | 
					dry-runs/**
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										3
									
								
								.gitmodules
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitmodules
									
									
									
									
										vendored
									
									
								
							@ -1,3 +0,0 @@
 | 
				
			|||||||
[submodule "dotfiles/dot"]
 | 
					 | 
				
			||||||
	path = dotfiles/dot
 | 
					 | 
				
			||||||
	url = https://gitlab.com/shaunrd0/dot
 | 
					 | 
				
			||||||
							
								
								
									
										153
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										153
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							@ -3,10 +3,19 @@
 | 
				
			|||||||
version = 3
 | 
					version = 3
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "ansi_term"
 | 
					name = "aho-corasick"
 | 
				
			||||||
version = "0.11.0"
 | 
					version = "0.7.18"
 | 
				
			||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
 | 
					checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "memchr",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "ansi_term"
 | 
				
			||||||
 | 
					version = "0.12.1"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
 | 
				
			||||||
dependencies = [
 | 
					dependencies = [
 | 
				
			||||||
 "winapi",
 | 
					 "winapi",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
@ -22,6 +31,12 @@ dependencies = [
 | 
				
			|||||||
 "winapi",
 | 
					 "winapi",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "autocfg"
 | 
				
			||||||
 | 
					version = "1.1.0"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "bitflags"
 | 
					name = "bitflags"
 | 
				
			||||||
version = "1.3.2"
 | 
					version = "1.3.2"
 | 
				
			||||||
@ -29,10 +44,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			|||||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
 | 
					checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "clap"
 | 
					name = "chrono"
 | 
				
			||||||
version = "2.33.3"
 | 
					version = "0.4.19"
 | 
				
			||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002"
 | 
					checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "libc",
 | 
				
			||||||
 | 
					 "num-integer",
 | 
				
			||||||
 | 
					 "num-traits",
 | 
				
			||||||
 | 
					 "time",
 | 
				
			||||||
 | 
					 "winapi",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "clap"
 | 
				
			||||||
 | 
					version = "2.34.0"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c"
 | 
				
			||||||
dependencies = [
 | 
					dependencies = [
 | 
				
			||||||
 "ansi_term",
 | 
					 "ansi_term",
 | 
				
			||||||
 "atty",
 | 
					 "atty",
 | 
				
			||||||
@ -69,9 +97,11 @@ dependencies = [
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "kot"
 | 
					name = "kot"
 | 
				
			||||||
version = "0.1.0"
 | 
					version = "0.1.5"
 | 
				
			||||||
dependencies = [
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "chrono",
 | 
				
			||||||
 "fs_extra",
 | 
					 "fs_extra",
 | 
				
			||||||
 | 
					 "regex",
 | 
				
			||||||
 "structopt",
 | 
					 "structopt",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -83,9 +113,34 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "libc"
 | 
					name = "libc"
 | 
				
			||||||
version = "0.2.102"
 | 
					version = "0.2.126"
 | 
				
			||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
checksum = "a2a5ac8f984bfcf3a823267e5fde638acc3325f6496633a5da6bb6eb2171e103"
 | 
					checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "memchr"
 | 
				
			||||||
 | 
					version = "2.5.0"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "num-integer"
 | 
				
			||||||
 | 
					version = "0.1.45"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "autocfg",
 | 
				
			||||||
 | 
					 "num-traits",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "num-traits"
 | 
				
			||||||
 | 
					version = "0.2.15"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "autocfg",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "proc-macro-error"
 | 
					name = "proc-macro-error"
 | 
				
			||||||
@ -113,22 +168,39 @@ dependencies = [
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "proc-macro2"
 | 
					name = "proc-macro2"
 | 
				
			||||||
version = "1.0.29"
 | 
					version = "1.0.39"
 | 
				
			||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
checksum = "b9f5105d4fdaab20335ca9565e106a5d9b82b6219b5ba735731124ac6711d23d"
 | 
					checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f"
 | 
				
			||||||
dependencies = [
 | 
					dependencies = [
 | 
				
			||||||
 "unicode-xid",
 | 
					 "unicode-ident",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "quote"
 | 
					name = "quote"
 | 
				
			||||||
version = "1.0.9"
 | 
					version = "1.0.18"
 | 
				
			||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7"
 | 
					checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1"
 | 
				
			||||||
dependencies = [
 | 
					dependencies = [
 | 
				
			||||||
 "proc-macro2",
 | 
					 "proc-macro2",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "regex"
 | 
				
			||||||
 | 
					version = "1.5.6"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "aho-corasick",
 | 
				
			||||||
 | 
					 "memchr",
 | 
				
			||||||
 | 
					 "regex-syntax",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "regex-syntax"
 | 
				
			||||||
 | 
					version = "0.6.26"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "strsim"
 | 
					name = "strsim"
 | 
				
			||||||
version = "0.8.0"
 | 
					version = "0.8.0"
 | 
				
			||||||
@ -137,9 +209,9 @@ checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "structopt"
 | 
					name = "structopt"
 | 
				
			||||||
version = "0.3.23"
 | 
					version = "0.3.26"
 | 
				
			||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
checksum = "bf9d950ef167e25e0bdb073cf1d68e9ad2795ac826f2f3f59647817cf23c0bfa"
 | 
					checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10"
 | 
				
			||||||
dependencies = [
 | 
					dependencies = [
 | 
				
			||||||
 "clap",
 | 
					 "clap",
 | 
				
			||||||
 "lazy_static",
 | 
					 "lazy_static",
 | 
				
			||||||
@ -148,9 +220,9 @@ dependencies = [
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "structopt-derive"
 | 
					name = "structopt-derive"
 | 
				
			||||||
version = "0.4.16"
 | 
					version = "0.4.18"
 | 
				
			||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
checksum = "134d838a2c9943ac3125cf6df165eda53493451b719f3255b2a26b85f772d0ba"
 | 
					checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0"
 | 
				
			||||||
dependencies = [
 | 
					dependencies = [
 | 
				
			||||||
 "heck",
 | 
					 "heck",
 | 
				
			||||||
 "proc-macro-error",
 | 
					 "proc-macro-error",
 | 
				
			||||||
@ -161,13 +233,13 @@ dependencies = [
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "syn"
 | 
					name = "syn"
 | 
				
			||||||
version = "1.0.77"
 | 
					version = "1.0.95"
 | 
				
			||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
checksum = "5239bc68e0fef57495900cfea4e8dc75596d9a319d7e16b1e0a440d24e6fe0a0"
 | 
					checksum = "fbaf6116ab8924f39d52792136fb74fd60a80194cf1b1c6ffa6453eef1c3f942"
 | 
				
			||||||
dependencies = [
 | 
					dependencies = [
 | 
				
			||||||
 "proc-macro2",
 | 
					 "proc-macro2",
 | 
				
			||||||
 "quote",
 | 
					 "quote",
 | 
				
			||||||
 "unicode-xid",
 | 
					 "unicode-ident",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
@ -180,10 +252,27 @@ dependencies = [
 | 
				
			|||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "unicode-segmentation"
 | 
					name = "time"
 | 
				
			||||||
version = "1.8.0"
 | 
					version = "0.1.44"
 | 
				
			||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b"
 | 
					checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "libc",
 | 
				
			||||||
 | 
					 "wasi",
 | 
				
			||||||
 | 
					 "winapi",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "unicode-ident"
 | 
				
			||||||
 | 
					version = "1.0.0"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "unicode-segmentation"
 | 
				
			||||||
 | 
					version = "1.9.0"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "unicode-width"
 | 
					name = "unicode-width"
 | 
				
			||||||
@ -191,12 +280,6 @@ version = "0.1.9"
 | 
				
			|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
 | 
					checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					 | 
				
			||||||
name = "unicode-xid"
 | 
					 | 
				
			||||||
version = "0.2.2"
 | 
					 | 
				
			||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					 | 
				
			||||||
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "vec_map"
 | 
					name = "vec_map"
 | 
				
			||||||
version = "0.8.2"
 | 
					version = "0.8.2"
 | 
				
			||||||
@ -205,9 +288,15 @@ checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "version_check"
 | 
					name = "version_check"
 | 
				
			||||||
version = "0.9.3"
 | 
					version = "0.9.4"
 | 
				
			||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
 | 
					checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "wasi"
 | 
				
			||||||
 | 
					version = "0.10.0+wasi-snapshot-preview1"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "winapi"
 | 
					name = "winapi"
 | 
				
			||||||
 | 
				
			|||||||
@ -1,11 +1,13 @@
 | 
				
			|||||||
[package]
 | 
					[package]
 | 
				
			||||||
name = "kot"
 | 
					name = "kot"
 | 
				
			||||||
version = "0.1.0"
 | 
					version = "0.1.5"
 | 
				
			||||||
edition = "2018"
 | 
					edition = "2018"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# See more keys and their definitions at
 | 
					# See more keys and their definitions at
 | 
				
			||||||
# https://doc.rust-lang.org/cargo/reference/manifest.html
 | 
					# https://doc.rust-lang.org/cargo/reference/manifest.html
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[dependencies]
 | 
					[dependencies]
 | 
				
			||||||
 | 
					regex = "1"
 | 
				
			||||||
structopt = "0.3.23"
 | 
					structopt = "0.3.23"
 | 
				
			||||||
fs_extra = "1.2.0"
 | 
					fs_extra = "1.2.0"
 | 
				
			||||||
 | 
					chrono = "0.4"
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										203
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										203
									
								
								README.md
									
									
									
									
									
								
							@ -6,50 +6,61 @@ This helps to protect against installing broken dotfiles by providing a way to r
 | 
				
			|||||||
and return the system back to the previous state.
 | 
					and return the system back to the previous state.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
The installation process creates symbolic links, much like what you would expect when using [stow](https://linux.die.net/man/8/stow).
 | 
					The installation process creates symbolic links, much like what you would expect when using [stow](https://linux.die.net/man/8/stow).
 | 
				
			||||||
`kot` can install dotfiles from any directory, using any target directory. To test how `kot` might behave, 
 | 
					`kot` can install dotfiles from any source directory, to any target directory. 
 | 
				
			||||||
you could point `--install-dir` to any directory that you've created for testing. 
 | 
					To test how `kot` might behave, you could point `--install` to any directory that you've created for testing. 
 | 
				
			||||||
This directory could be empty, or it could contain another set of dotfiles. `kot` will attempt
 | 
					This directory could be empty, or it could contain another set of dotfiles.
 | 
				
			||||||
 to install the configurations. If conflicts are detected, output shows the conflicts and 
 | 
					Alternatively, you could set the `--dry-run` flag that will automatically install to a predefined path (`$HOME/.local/share/kot/dry-runs/$USER`)
 | 
				
			||||||
 | 
					Note that this directory will never be cleared automatically, each subsequent `--dry-run` 
 | 
				
			||||||
 | 
					will stack configurations into this default directory until it is manually cleared.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					If conflicts are detected, `kot` shows the conflicts found and 
 | 
				
			||||||
prompts to abort or continue. An example of this is seen below.
 | 
					prompts to abort or continue. An example of this is seen below.
 | 
				
			||||||
 | 
					This prompt will be skipped if the `--force` flag is set.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
```bash
 | 
					```bash
 | 
				
			||||||
kot dotfiles/dot/
 | 
					kot --dry-run dotfiles/dot/
 | 
				
			||||||
 | 
					 | 
				
			||||||
args: Cli { dotfiles_dir: "/home/kapper/Code/kot/dotfiles/dot", install_dir: "/home/kapper/Code/kot/dry-runs/kapper", backup_dir: "/home/kapper/Code/kot/backups/kapper", force: false }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					args: Cli { dotfiles: "/home/kapper/Code/kot/dotfiles/dot", install_dir: "/home/kapper/.local/share/kot/dry-runs/kapper", backup_dir: Some("/home/kapper/.local/share/kot/backups/dot:2022-05-29T19:03:27"), clone_dir: None, force: false, dry_run: true, is_repo: false, conflicts: [] }
 | 
				
			||||||
The following configurations already exist:
 | 
					The following configurations already exist:
 | 
				
			||||||
  "/home/kapper/Code/kot/dry-runs/kapper/.bashrc"
 | 
					  "/home/kapper/.local/share/kot/dry-runs/kapper/.git"
 | 
				
			||||||
  "/home/kapper/Code/kot/dry-runs/kapper/.config"
 | 
					  "/home/kapper/.local/share/kot/dry-runs/kapper/.vimrc"
 | 
				
			||||||
  "/home/kapper/Code/kot/dry-runs/kapper/README.md"
 | 
					  "/home/kapper/.local/share/kot/dry-runs/kapper/.bash_aliases"
 | 
				
			||||||
  "/home/kapper/Code/kot/dry-runs/kapper/VimScreenshot.png"
 | 
					  "/home/kapper/.local/share/kot/dry-runs/kapper/.vim"
 | 
				
			||||||
  "/home/kapper/Code/kot/dry-runs/kapper/fix-vbox.sh"
 | 
					  "/home/kapper/.local/share/kot/dry-runs/kapper/VimScreenshot.png"
 | 
				
			||||||
  "/home/kapper/Code/kot/dry-runs/kapper/.git"
 | 
					  "/home/kapper/.local/share/kot/dry-runs/kapper/.gitignore"
 | 
				
			||||||
  "/home/kapper/Code/kot/dry-runs/kapper/.bash_aliases"
 | 
					  "/home/kapper/.local/share/kot/dry-runs/kapper/.config"
 | 
				
			||||||
  "/home/kapper/Code/kot/dry-runs/kapper/.gitignore"
 | 
					  "/home/kapper/.local/share/kot/dry-runs/kapper/fix-vbox.sh"
 | 
				
			||||||
  "/home/kapper/Code/kot/dry-runs/kapper/.gitmodules"
 | 
					  "/home/kapper/.local/share/kot/dry-runs/kapper/.gitmodules"
 | 
				
			||||||
  "/home/kapper/Code/kot/dry-runs/kapper/.vimrc"
 | 
					  "/home/kapper/.local/share/kot/dry-runs/kapper/.bashrc"
 | 
				
			||||||
  "/home/kapper/Code/kot/dry-runs/kapper/.vim"
 | 
					  "/home/kapper/.local/share/kot/dry-runs/kapper/README.md"
 | 
				
			||||||
If you continue, backups will be made in "/home/kapper/Code/kot/backups/kapper". Any configurations there will be overwritten.
 | 
					If you continue, backups will be made in "/home/kapper/.local/share/kot/backups/dot:2022-05-29T19:03:27". 
 | 
				
			||||||
Abort? Enter y/n or Y/N:
 | 
					Any configurations there will be overwritten.
 | 
				
			||||||
 | 
					Continue? Enter Y/y or N/n:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
If there are already files within the backup directory, `kot` will exit and show an error message.
 | 
					#### User Data
 | 
				
			||||||
This is to protect existing backups from being merged with configs from subsequent runs.
 | 
					 | 
				
			||||||
If you want to erase these backups and create a new backup, rerun the command with the `--force` flag set.
 | 
					 | 
				
			||||||
Otherwise, specify a different backup directory with the `--backup-dir` option. 
 | 
					 | 
				
			||||||
If the backup directory does not exist, it will be created.
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					`kot` stores user data within `$HOME/.local/share/kot/`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
```bash
 | 
					When we provide a repository URL as our `dotfiles` to install, the repo will be *recursively* cloned into 
 | 
				
			||||||
kot dotfiles/dot/
 | 
					`$HOME/.local/share/kot/dotfiles/<REPO_NAME>`. 
 | 
				
			||||||
 | 
					This is to ensure each user of `kot` maintains their own dotfiles in a location that is accessible but not easy to accidentally modify or erase.
 | 
				
			||||||
 | 
					If needed, the user can provide a preferred clone directory to the CLI by setting the `--clone-dir` option
 | 
				
			||||||
 | 
					
 | 
				
			||||||
thread 'main' panicked at '
 | 
					When we encounter conflicts during installation of these dotfiles, backups will be created in
 | 
				
			||||||
  Error: Backups already exist at "/home/kapper/Code/kot/backups/kapper"
 | 
					`$HOME/.local/share/kot/backups/<DOTFILES_NAME>:<DATE(%Y-%m-%dT%H:%M:%S)>`
 | 
				
			||||||
  Set the --force flag to overwrite configurations stored here', src/kot/kcli.rs:94:17
 | 
					If there are no conflicts found during installation, no backup is created.
 | 
				
			||||||
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
 | 
					Configurations are said to be conflicting if the `--install` path contains configuration files that are
 | 
				
			||||||
```
 | 
					also within the dotfiles we are currently installing.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#### Installation
 | 
					Backups are intended to reverse changes applied during installation of dotfiles.
 | 
				
			||||||
 | 
					These backups are not exhaustive of all configurations tied to the system or user.
 | 
				
			||||||
 | 
					The backups only include files that were direct conflicts with configurations being installed.
 | 
				
			||||||
 | 
					When we reach an error during installation, `kot` will restore the configurations within the last backup, and then removes unused configurations.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### Installing kot
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Follow [Rustup instructions](https://rustup.rs/) to setup the Rust toolchain
 | 
					Follow [Rustup instructions](https://rustup.rs/) to setup the Rust toolchain
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -60,65 +71,97 @@ git clone https://gitlab.com/shaunrd0/kot && cd kot
 | 
				
			|||||||
cargo install --path .
 | 
					cargo install --path .
 | 
				
			||||||
kot --help
 | 
					kot --help
 | 
				
			||||||
 | 
					
 | 
				
			||||||
kot 0.1.0
 | 
					kot 0.1.5
 | 
				
			||||||
CLI for managing Linux user configurations
 | 
					CLI for managing Linux user configurations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
USAGE:
 | 
					USAGE:
 | 
				
			||||||
    kot [FLAGS] [OPTIONS] <dotfiles-dir>
 | 
					    kot [FLAGS] [OPTIONS] <dotfiles> --install <install>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
FLAGS:
 | 
					FLAGS:
 | 
				
			||||||
 | 
					    -d, --dry-run
 | 
				
			||||||
 | 
					            Installs configurations to $HOME/.local/shared/kot/dry-runs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            Useful flag to set when testing what an install would do to your home directory. This is synonymous with
 | 
				
			||||||
 | 
					            setting --install $HOME/.local/shared/kot/dry-runs/$USER. Subsequent runs with this flag set will not delete
 | 
				
			||||||
 | 
					            the contents of this directory.
 | 
				
			||||||
 | 
					    -f, --force
 | 
				
			||||||
 | 
					            Overwrites existing backups
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            This flag will replace existing backups if during installation we encounter conflicts and the backup-dir
 | 
				
			||||||
 | 
					            provided already contains previous backups.
 | 
				
			||||||
 | 
					    -h, --help
 | 
				
			||||||
 | 
					            Prints help information
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    -V, --version
 | 
				
			||||||
 | 
					            Prints version information
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					OPTIONS:
 | 
				
			||||||
 | 
					    -b, --backup-dir <backup-dir>
 | 
				
			||||||
 | 
					            The location to store backups for this user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            If no backup-dir is provided, we create one within the default kot data directory:
 | 
				
			||||||
 | 
					            $HOME/.local/share/kot/backups/
 | 
				
			||||||
 | 
					    -c, --clone-dir <clone-dir>
 | 
				
			||||||
 | 
					            An alternate path to clone a dotfiles repository to
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            If the clone-dir option is provided to the CLI, kot will clone the dotfiles repository into this directory.
 | 
				
			||||||
 | 
					            If clone-dir is not provided, the repository is cloned into $HOME/.local/share/kot/dotfiles Custom clone-dir
 | 
				
			||||||
 | 
					            will be used literally, and no subdirectory is created to store the cloned repository For example, clone-dir
 | 
				
			||||||
 | 
					            of $HOME/clonedir for repo named Dotfiles We will clone into $HOME/clonedir, and NOT $HOME/clonedir/Dotfiles
 | 
				
			||||||
 | 
					            The default path for cloned repos is $HOME/.local/share/kot/dotfiles/
 | 
				
			||||||
 | 
					    -i, --install <install>
 | 
				
			||||||
 | 
					            The location to attempt installation of user configurations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            The desired installation directory for user configurations. By default this is your $HOME directory This
 | 
				
			||||||
 | 
					            could optionally point to some other directory to perform a dry run, or the --dry-run flag could be set
 | 
				
			||||||
 | 
					            [env: HOME=/home/kapper]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ARGS:
 | 
				
			||||||
 | 
					    <dotfiles>
 | 
				
			||||||
 | 
					            Local or full path to user configurations to install. Can also be a git repository.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            System path or repository URL for dotfiles we want to install. If a path is used, it can either be local to
 | 
				
			||||||
 | 
					            CWD or absolute. If a URL is used for a dotfiles repository, the repo is cloned into
 | 
				
			||||||
 | 
					            $HOME/.local/shared/kot/dotfiles/
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					If you don't want to install `kot`, you can also use the following `cargo` command
 | 
				
			||||||
 | 
					 where all arguments after the `--` are passed as arguments to `kot` and not `cargo`.
 | 
				
			||||||
 | 
					Below is an example of the short-help output text provided with the `-h` flag
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					cd path/to/kot
 | 
				
			||||||
 | 
					cargo build
 | 
				
			||||||
 | 
					cargo run -- --help
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					kot 0.1.5
 | 
				
			||||||
 | 
					CLI for managing Linux user configurations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					USAGE:
 | 
				
			||||||
 | 
					    kot [FLAGS] [OPTIONS] <dotfiles> --install <install>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					FLAGS:
 | 
				
			||||||
 | 
					    -d, --dry-run    Installs configurations to $HOME/.local/shared/kot/dry-runs
 | 
				
			||||||
    -f, --force      Overwrites existing backups
 | 
					    -f, --force      Overwrites existing backups
 | 
				
			||||||
    -h, --help       Prints help information
 | 
					    -h, --help       Prints help information
 | 
				
			||||||
    -V, --version    Prints version information
 | 
					    -V, --version    Prints version information
 | 
				
			||||||
 | 
					
 | 
				
			||||||
OPTIONS:
 | 
					OPTIONS:
 | 
				
			||||||
    -b, --backup-dir <backup-dir>      The location to store backups for this user [default: backups/kapper]
 | 
					    -b, --backup-dir <backup-dir>    The location to store backups for this user
 | 
				
			||||||
    -i, --install-dir <install-dir>    The location to attempt installation of user configurations [default: dry-
 | 
					    -c, --clone-dir <clone-dir>      An alternate path to clone a dotfiles repository to
 | 
				
			||||||
                                       runs/kapper]
 | 
					    -i, --install <install>          The location to attempt installation of user configurations [env:
 | 
				
			||||||
 | 
					                                     HOME=/home/kapper]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ARGS:
 | 
					ARGS:
 | 
				
			||||||
    <dotfiles-dir>    Local or full path to user configurations to install
 | 
					    <dotfiles>    Local or full path to user configurations to install. Can also be a git repository
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#### Dotfiles Management
 | 
					#### TODO
 | 
				
			||||||
 | 
					
 | 
				
			||||||
To store dotfiles, this repository uses submodules. To update surface-level submodules, we can run the following commands
 | 
					* Ensure empty backups are not created
 | 
				
			||||||
```bash
 | 
					* Provide interface for managing agreed-upon /etc/skel/ configurations
 | 
				
			||||||
git submodule update --init
 | 
					* Provide more CLI options for git functionality; Branches, update submodules, etc
 | 
				
			||||||
 | 
					* Clean up warnings during build / installation
 | 
				
			||||||
Submodule path 'dot': checked out '7877117d5bd413ecf35c86efb4514742d8136843'
 | 
					* Automate testing
 | 
				
			||||||
```
 | 
					* 
 | 
				
			||||||
 | 
					 | 
				
			||||||
But in the case of my dotfiles repository, [shaunrd0/dot](https://gitlab.com/shaunrd0/dot), I use submodules to clone repositories for vim plugins. To update all submodules *and their nested submodules*, we can run the following commands
 | 
					 | 
				
			||||||
```bash
 | 
					 | 
				
			||||||
git submodule update --init --recursive
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Submodule 'dotfiles/dot' (https://gitlab.com/shaunrd0/dot) registered for path 'dotfiles/dot'
 | 
					 | 
				
			||||||
Cloning into '/home/kapper/Code/kotd/dotfiles/dot'...
 | 
					 | 
				
			||||||
warning: redirecting to https://gitlab.com/shaunrd0/dot.git/
 | 
					 | 
				
			||||||
Submodule path 'dotfiles/dot': checked out '7877117d5bd413ecf35c86efb4514742d8136843'
 | 
					 | 
				
			||||||
Submodule '.vim/bundle/Colorizer' (https://github.com/chrisbra/Colorizer) registered for path 'dotfiles/dot/.vim/bundle/Colorizer'
 | 
					 | 
				
			||||||
Submodule '.vim/bundle/ale' (https://github.com/dense-analysis/ale) registered for path 'dotfiles/dot/.vim/bundle/ale'
 | 
					 | 
				
			||||||
Submodule '.vim/bundle/clang_complete' (https://github.com/xavierd/clang_complete) registered for path 'dotfiles/dot/.vim/bundle/clang_complete'
 | 
					 | 
				
			||||||
Submodule '.vim/bundle/supertab' (https://github.com/ervandew/supertab) registered for path 'dotfiles/dot/.vim/bundle/supertab'
 | 
					 | 
				
			||||||
Submodule '.vim/bundle/unicode.vim' (https://github.com/chrisbra/unicode.vim) registered for path 'dotfiles/dot/.vim/bundle/unicode.vim'
 | 
					 | 
				
			||||||
Submodule '.vim/bundle/vim-airline' (https://github.com/vim-airline/vim-airline) registered for path 'dotfiles/dot/.vim/bundle/vim-airline'
 | 
					 | 
				
			||||||
Submodule '.vim/bundle/vim-airline-themes' (https://github.com/vim-airline/vim-airline-themes) registered for path 'dotfiles/dot/.vim/bundle/vim-airline-themes'
 | 
					 | 
				
			||||||
Submodule '.vim/bundle/vim-signify' (https://github.com/mhinz/vim-signify) registered for path 'dotfiles/dot/.vim/bundle/vim-signify'
 | 
					 | 
				
			||||||
Cloning into '/home/kapper/Code/kotd/dotfiles/dot/.vim/bundle/Colorizer'...
 | 
					 | 
				
			||||||
Cloning into '/home/kapper/Code/kotd/dotfiles/dot/.vim/bundle/ale'...
 | 
					 | 
				
			||||||
Cloning into '/home/kapper/Code/kotd/dotfiles/dot/.vim/bundle/clang_complete'...
 | 
					 | 
				
			||||||
Cloning into '/home/kapper/Code/kotd/dotfiles/dot/.vim/bundle/supertab'...
 | 
					 | 
				
			||||||
Cloning into '/home/kapper/Code/kotd/dotfiles/dot/.vim/bundle/unicode.vim'...
 | 
					 | 
				
			||||||
Cloning into '/home/kapper/Code/kotd/dotfiles/dot/.vim/bundle/vim-airline'...
 | 
					 | 
				
			||||||
Cloning into '/home/kapper/Code/kotd/dotfiles/dot/.vim/bundle/vim-airline-themes'...
 | 
					 | 
				
			||||||
Cloning into '/home/kapper/Code/kotd/dotfiles/dot/.vim/bundle/vim-signify'...
 | 
					 | 
				
			||||||
Submodule path 'dotfiles/dot/.vim/bundle/Colorizer': checked out '826d5691ac7d36589591314621047b1b9d89ed34'
 | 
					 | 
				
			||||||
Submodule path 'dotfiles/dot/.vim/bundle/ale': checked out '3ea887d2f4d43dd55d81213517344226f6399ed6'
 | 
					 | 
				
			||||||
Submodule path 'dotfiles/dot/.vim/bundle/clang_complete': checked out '293a1062274a06be61797612034bd8d87851406e'
 | 
					 | 
				
			||||||
Submodule path 'dotfiles/dot/.vim/bundle/supertab': checked out 'd80e8e2c1fa08607fa34c0ca5f1b66d8a906c5ef'
 | 
					 | 
				
			||||||
Submodule path 'dotfiles/dot/.vim/bundle/unicode.vim': checked out 'afb8db4f81580771c39967e89bc5772e72b9018e'
 | 
					 | 
				
			||||||
Submodule path 'dotfiles/dot/.vim/bundle/vim-airline': checked out 'cb1bc19064d3762e4e08103afb37a246b797d902'
 | 
					 | 
				
			||||||
Submodule path 'dotfiles/dot/.vim/bundle/vim-airline-themes': checked out 'd148d42d9caf331ff08b6cae683d5b210003cde7'
 | 
					 | 
				
			||||||
Submodule path 'dotfiles/dot/.vim/bundle/vim-signify': checked out 'b2a0450e23c63b75bbeabf4f0c28f9b4b2480689'
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
				
			|||||||
@ -1 +0,0 @@
 | 
				
			|||||||
Subproject commit 7877117d5bd413ecf35c86efb4514742d8136843
 | 
					 | 
				
			||||||
@ -1,2 +0,0 @@
 | 
				
			|||||||
This is a test directory to test config collisions
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@ -1 +0,0 @@
 | 
				
			|||||||
This directory is for testing the installation of user configurations against an existing configuration set.
 | 
					 | 
				
			||||||
							
								
								
									
										208
									
								
								src/kot.rs
									
									
									
									
									
								
							
							
						
						
									
										208
									
								
								src/kot.rs
									
									
									
									
									
								
							@ -6,15 +6,31 @@
 | 
				
			|||||||
## Contact: shaunrd0@gmail.com  | URL: www.shaunreed.com | GitHub: shaunrd0   ##
 | 
					## Contact: shaunrd0@gmail.com  | URL: www.shaunreed.com | GitHub: shaunrd0   ##
 | 
				
			||||||
##############################################################################*/
 | 
					##############################################################################*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use std::collections::HashMap;
 | 
				
			||||||
use std::path::PathBuf;
 | 
					use std::path::PathBuf;
 | 
				
			||||||
use crate::kot::kfs::check_collisions;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub mod kcli;
 | 
					pub mod kcli;
 | 
				
			||||||
pub mod kfs;
 | 
					pub mod kfs;
 | 
				
			||||||
pub mod kio;
 | 
					pub mod kio;
 | 
				
			||||||
 | 
					pub mod kgit;
 | 
				
			||||||
 | 
					pub mod kerror;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use kerror::Error;
 | 
				
			||||||
/// Result alias to return result with Error of various types
 | 
					/// Result alias to return result with Error of various types
 | 
				
			||||||
pub type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
 | 
					pub type Result<T> = std::result::Result<T, kerror::Error>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					macro_rules! err {
 | 
				
			||||||
 | 
					  ($type:expr, $msg:expr) => {
 | 
				
			||||||
 | 
					    return Err(Error::new($type, $msg))
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ($msg:expr) => {
 | 
				
			||||||
 | 
					    return Err(Error::new(ErrorKind::Other("Unclassified kot error"), $msg))
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					pub (crate) use err;
 | 
				
			||||||
 | 
					use crate::ErrorKind::Other;
 | 
				
			||||||
 | 
					use crate::kot::kfs::get_target_paths;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// =============================================================================
 | 
					// =============================================================================
 | 
				
			||||||
// IMPLEMENTATION
 | 
					// IMPLEMENTATION
 | 
				
			||||||
@ -22,24 +38,55 @@ pub type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// -----------------------------------------------------------------------------
 | 
					// -----------------------------------------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Creates symbolic links to the configurations we're installing
 | 
					pub fn handle_args(args: &mut kcli::Cli) -> Result<()> {
 | 
				
			||||||
// TODO: On error, revert to last good state
 | 
					  if args.is_repo {
 | 
				
			||||||
// TODO: User script to execute after installing configs successfully
 | 
					    // Attempt to install dotfiles from a dotfiles repository
 | 
				
			||||||
pub fn install_configs(args: & kcli::Cli) -> Result<()> {
 | 
					    // + No specific configuration required on behalf of dotfiles repo
 | 
				
			||||||
    // Get the configurations and their target installation paths
 | 
					    kgit::clone(&args.dotfiles.to_str().unwrap(),
 | 
				
			||||||
    // + Checks for conflicts and prompts user to abort or continue
 | 
					                &args.clone_dir.as_ref().unwrap())?;
 | 
				
			||||||
    let config_map = kfs::get_target_paths(&args)?;
 | 
					  }
 | 
				
			||||||
 | 
					  return match install_configs(args) {
 | 
				
			||||||
 | 
					    Ok(_) => Ok(()),
 | 
				
			||||||
 | 
					    Err(e) => {
 | 
				
			||||||
 | 
					      // If we reach an error, use our backup_dir to restore configs
 | 
				
			||||||
 | 
					      // + Remove configs we applied that weren't previously on the system
 | 
				
			||||||
 | 
					      uninstall_configs(args)?;
 | 
				
			||||||
 | 
					      Err(e)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Creates symbolic links to the configurations we're installing
 | 
				
			||||||
 | 
					pub fn install_configs(args: &mut kcli::Cli) -> Result<()> {
 | 
				
			||||||
 | 
					  //
 | 
				
			||||||
 | 
					  // Find path that points us to the dotfiles we are installing
 | 
				
			||||||
 | 
					  let dotfiles = match args.is_repo {
 | 
				
			||||||
 | 
					    // If the dotfiles were provided as a system path, use it
 | 
				
			||||||
 | 
					    false => args.dotfiles.to_owned(),
 | 
				
			||||||
 | 
					    // If the dotfiles to install was a repository, find the path we cloned to
 | 
				
			||||||
 | 
					    true => args.clone_dir.as_ref().unwrap().to_path_buf()
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  //
 | 
				
			||||||
  // Check if there are any existing files in the install directory that are also within the dotfiles to install
 | 
					  // Check if there are any existing files in the install directory that are also within the dotfiles to install
 | 
				
			||||||
    handle_collisions(&args, &config_map)?;
 | 
					
 | 
				
			||||||
 | 
					  // Get the configurations and their target installation paths in a hashmap<config, target_path>
 | 
				
			||||||
 | 
					  // + Using target_path, check for conflicts and prompts user to abort or continue
 | 
				
			||||||
 | 
					  let config_map = kfs::get_target_paths(&args.install_dir, &dotfiles)?;
 | 
				
			||||||
 | 
					  handle_collisions(args, &config_map)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  //
 | 
				
			||||||
 | 
					  // Install the dotfiles configurations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // At this point there are either no conflicts or the user agreed to them
 | 
					  // At this point there are either no conflicts or the user agreed to them
 | 
				
			||||||
  println!("Installing configs:");
 | 
					  println!("Installing configs:");
 | 
				
			||||||
  for (config_path, target_path) in &config_map {
 | 
					  for (config_path, target_path) in &config_map {
 | 
				
			||||||
    println!("  + {:?}", target_path);
 | 
					    println!("  + {:?}", target_path);
 | 
				
			||||||
        match std::os::unix::fs::symlink(config_path, target_path) {
 | 
					    std::os::unix::fs::symlink(config_path, target_path)
 | 
				
			||||||
            Ok(()) => (), // Configuration installed successfully
 | 
					        .or_else(|err| -> Result<()> {
 | 
				
			||||||
            Err(_e) => {
 | 
					          eprintln!("Error: Unable to create symlink {:?} -> {:?} ({:?})",
 | 
				
			||||||
 | 
					                    target_path, config_path, err);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          // Attempt to remove the file or directory first, and then symlink the new config
 | 
					          // Attempt to remove the file or directory first, and then symlink the new config
 | 
				
			||||||
          match target_path.is_dir() {
 | 
					          match target_path.is_dir() {
 | 
				
			||||||
            true => fs_extra::dir::remove(target_path)
 | 
					            true => fs_extra::dir::remove(target_path)
 | 
				
			||||||
@ -48,23 +95,30 @@ pub fn install_configs(args: & kcli::Cli) -> Result<()> {
 | 
				
			|||||||
                .expect(&format!("Error: Unable to remove file: {:?}", target_path)),
 | 
					                .expect(&format!("Error: Unable to remove file: {:?}", target_path)),
 | 
				
			||||||
          };
 | 
					          };
 | 
				
			||||||
          // Try to symlink the config again, if failure exit with error
 | 
					          // Try to symlink the config again, if failure exit with error
 | 
				
			||||||
                std::os::unix::fs::symlink(config_path, target_path)?;
 | 
					          std::os::unix::fs::symlink(config_path, target_path).or_else(|err| {
 | 
				
			||||||
            },
 | 
					            eprintln!("Error: Unable to symlink config: {:?} -> {:?}",
 | 
				
			||||||
        }
 | 
					                      target_path, config_path);
 | 
				
			||||||
 | 
					            return Err(err);
 | 
				
			||||||
 | 
					          })?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          return Ok(());
 | 
				
			||||||
 | 
					        })?;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Ok(())
 | 
					  return Ok(());
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Handles collisions between existing files and dotfiles we're installing
 | 
					/// Handles collisions between existing files and dotfiles we're installing
 | 
				
			||||||
fn handle_collisions(args : & kcli::Cli,
 | 
					/// + If --force is not set, prompt user to continue based on conflicts found
 | 
				
			||||||
 | 
					/// + If --force is set or user chooses to continue,
 | 
				
			||||||
 | 
					///     move conflicting files to a backup directory
 | 
				
			||||||
 | 
					fn handle_collisions(args: &mut kcli::Cli,
 | 
				
			||||||
                     config_map: &kfs::HashMap<PathBuf, PathBuf>) -> Result<()> {
 | 
					                     config_map: &kfs::HashMap<PathBuf, PathBuf>) -> Result<()> {
 | 
				
			||||||
  // Check if we found any collisions in the configurations
 | 
					  // Check if we found any collisions in the configurations
 | 
				
			||||||
    match check_collisions(&config_map) {
 | 
					  return match check_collisions(&config_map) {
 | 
				
			||||||
        None => {
 | 
					    None => Ok(()), // There were no collisions, configurations pass pre-install checks
 | 
				
			||||||
            return Ok(()) // There were no collisions, configurations pass pre-install checks
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
    Some(conflicts) => {
 | 
					    Some(conflicts) => {
 | 
				
			||||||
 | 
					      args.conflicts = conflicts.to_owned();
 | 
				
			||||||
      // Ask client if they would like to abort given the config collisions
 | 
					      // Ask client if they would like to abort given the config collisions
 | 
				
			||||||
      let mut msg = format!("The following configurations already exist:");
 | 
					      let mut msg = format!("The following configurations already exist:");
 | 
				
			||||||
      for config in conflicts.iter() {
 | 
					      for config in conflicts.iter() {
 | 
				
			||||||
@ -72,24 +126,39 @@ fn handle_collisions(args : & kcli::Cli,
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
      msg += format!("\nIf you continue, backups will be made in {:?}. \
 | 
					      msg += format!("\nIf you continue, backups will be made in {:?}. \
 | 
				
			||||||
                      Any configurations there will be overwritten.\
 | 
					                      Any configurations there will be overwritten.\
 | 
				
			||||||
                            \nAbort? Enter y/n or Y/N: ", &args.backup_dir).as_str();
 | 
					                      \nContinue? Enter Y/y or N/n: ",
 | 
				
			||||||
 | 
					                     args.backup_dir.as_ref().unwrap()).as_str();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // If we abort, exit; If we continue, back up the configs
 | 
					      // If the --force flag is set, short-circuit boolean and skip prompt
 | 
				
			||||||
            // TODO: Group this in with the --force flag?; Or make a new --adopt flag?
 | 
					      match args.force || kio::prompt(msg) {
 | 
				
			||||||
            match kio::prompt(msg) {
 | 
					        true => {
 | 
				
			||||||
                true => return Ok(()),
 | 
					 | 
				
			||||||
                false => {
 | 
					 | 
				
			||||||
          // Backup each conflicting config at the install location
 | 
					          // Backup each conflicting config at the install location
 | 
				
			||||||
          for backup_target in conflicts.iter() {
 | 
					          for backup_target in conflicts.iter() {
 | 
				
			||||||
            backup_config(backup_target, &args)?;
 | 
					            backup_config(backup_target, &args)?;
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
            };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          Ok(())
 | 
					          Ok(())
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        false => err!(Other("User aborted installation".to_string()), "Aborted".to_string())
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Checks if any config to install collides with existing files or directories
 | 
				
			||||||
 | 
					/// + Returns a list of collisions within Some(), else returns None
 | 
				
			||||||
 | 
					pub fn check_collisions(config_map: &HashMap<PathBuf, PathBuf>)
 | 
				
			||||||
 | 
					                        -> Option<Vec<PathBuf>> {
 | 
				
			||||||
 | 
					  let mut config_conflicts = vec![];
 | 
				
			||||||
 | 
					  for (_path, target_config) in config_map.iter() {
 | 
				
			||||||
 | 
					    // If the target configuration file or directory already exists
 | 
				
			||||||
 | 
					    if target_config.exists() {
 | 
				
			||||||
 | 
					      config_conflicts.push(target_config.to_owned());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if !config_conflicts.is_empty() {
 | 
				
			||||||
 | 
					    return Some(config_conflicts);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return None;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Creates a backup of configurations that conflict
 | 
					// Creates a backup of configurations that conflict
 | 
				
			||||||
@ -97,36 +166,71 @@ fn handle_collisions(args : & kcli::Cli,
 | 
				
			|||||||
// TODO: .kotignore in dotfiles repo to specify files to not install / backup
 | 
					// TODO: .kotignore in dotfiles repo to specify files to not install / backup
 | 
				
			||||||
// TODO: .kotrc in dotfiles repo or home dir to set backup-dir and install-dir?
 | 
					// TODO: .kotrc in dotfiles repo or home dir to set backup-dir and install-dir?
 | 
				
			||||||
fn backup_config(config_path: &kfs::PathBuf, args: &kcli::Cli) -> Result<()> {
 | 
					fn backup_config(config_path: &kfs::PathBuf, args: &kcli::Cli) -> Result<()> {
 | 
				
			||||||
    let mut backup_path = args.backup_dir.to_owned();
 | 
					  let mut backup_path = args.backup_dir.as_ref().unwrap().to_owned();
 | 
				
			||||||
    backup_path.push(config_path.file_name().unwrap());
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Check if the configuration we're backing up is a directory or a single file
 | 
					  // Check if the configuration we're backing up is a directory or a single file
 | 
				
			||||||
  match config_path.is_dir() {
 | 
					  match config_path.is_dir() {
 | 
				
			||||||
    true => {
 | 
					    true => {
 | 
				
			||||||
      // Copy directory with recursion using move_dir() wrapper function
 | 
					      // Copy directory with recursion using move_dir() wrapper function
 | 
				
			||||||
            let mut options = kfs::dir::CopyOptions::new();
 | 
					      let mut options = fs_extra::dir::CopyOptions::new();
 | 
				
			||||||
      options.copy_inside = true;
 | 
					      options.copy_inside = true;
 | 
				
			||||||
      options.overwrite = args.force;
 | 
					      options.overwrite = args.force;
 | 
				
			||||||
            if let Err(e) = kfs::move_dir(config_path, &backup_path, Some(&options))
 | 
					      kfs::move_dir(config_path, &backup_path, Some(&options))?;
 | 
				
			||||||
                .map_err(|e| e.into()) {
 | 
					 | 
				
			||||||
                return Err(e)
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
    false => {
 | 
					    false => {
 | 
				
			||||||
 | 
					      backup_path.push(config_path.file_name().unwrap());
 | 
				
			||||||
      // Copy single configuration file
 | 
					      // Copy single configuration file
 | 
				
			||||||
            let mut options = fs_extra::file::CopyOptions::new();
 | 
					      kfs::move_file(config_path, &backup_path)?;
 | 
				
			||||||
            options.overwrite = args.force;
 | 
					 | 
				
			||||||
            if let Err(e) = kfs::move_file(config_path, &backup_path, Some(&options))
 | 
					 | 
				
			||||||
                .map_err(|e| e.into()) {
 | 
					 | 
				
			||||||
                return Err(e)
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
    Ok(())
 | 
					  return Ok(());
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// TODO: Function to uninstall configs.
 | 
					// Loops through dotfiles to restore backup files or delete unused configs
 | 
				
			||||||
// + Loops through dotfiles and restore backup files or delete configs
 | 
					pub fn uninstall_configs(args: &kcli::Cli) -> Result<()> {
 | 
				
			||||||
fn _uninstall_configs() -> Result<()> {
 | 
					  //
 | 
				
			||||||
    Ok(())
 | 
					  // Replace previous configs we stored in backup_dir
 | 
				
			||||||
 | 
					  for config in args.backup_dir.as_ref().unwrap().read_dir()? {
 | 
				
			||||||
 | 
					    match config.as_ref().unwrap().path().is_dir() {
 | 
				
			||||||
 | 
					      true => {
 | 
				
			||||||
 | 
					        let mut options = fs_extra::dir::CopyOptions::new();
 | 
				
			||||||
 | 
					        options.copy_inside = true;
 | 
				
			||||||
 | 
					        options.overwrite = args.force;
 | 
				
			||||||
 | 
					        kfs::move_dir(&config.as_ref().unwrap().path(), &args.install_dir,
 | 
				
			||||||
 | 
					                      Some(&options)
 | 
				
			||||||
 | 
					        )?;
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      false => {
 | 
				
			||||||
 | 
					        kfs::move_file(&config.unwrap().path(), &args.install_dir)?;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  //
 | 
				
			||||||
 | 
					  // Remove configurations only required by the dotfiles we attempted to install
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Check each config in the dotfiles we want to uninstall
 | 
				
			||||||
 | 
					  let dotfile_path = match args.is_repo {
 | 
				
			||||||
 | 
					    true => args.clone_dir.as_ref().unwrap(),
 | 
				
			||||||
 | 
					    false => &args.dotfiles
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  for dotfile in dotfile_path.read_dir()? {
 | 
				
			||||||
 | 
					    let path = dotfile.unwrap().path();
 | 
				
			||||||
 | 
					    // If the configuration was not a conflict initially
 | 
				
			||||||
 | 
					    //   then we didn't have it before we installed; It is not being used
 | 
				
			||||||
 | 
					    if !args.conflicts.contains(&path) {
 | 
				
			||||||
 | 
					      let mut unused_config: PathBuf = args.install_dir.to_owned();
 | 
				
			||||||
 | 
					      unused_config.push(std::path::Path::new(&path.file_name().unwrap()));
 | 
				
			||||||
 | 
					      // Verify the file was already installed before we hit an error
 | 
				
			||||||
 | 
					      if !unused_config.exists() {
 | 
				
			||||||
 | 
					        continue;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Remove the unused config from install_dir
 | 
				
			||||||
 | 
					      std::fs::remove_file(unused_config)?;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return Ok(());
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										188
									
								
								src/kot/kcli.rs
									
									
									
									
									
								
							
							
						
						
									
										188
									
								
								src/kot/kcli.rs
									
									
									
									
									
								
							@ -6,8 +6,16 @@
 | 
				
			|||||||
## Contact: shaunrd0@gmail.com  | URL: www.shaunreed.com | GitHub: shaunrd0   ##
 | 
					## Contact: shaunrd0@gmail.com  | URL: www.shaunreed.com | GitHub: shaunrd0   ##
 | 
				
			||||||
##############################################################################*/
 | 
					##############################################################################*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use std::path::Path;
 | 
					use std::borrow::Borrow;
 | 
				
			||||||
 | 
					use std::path::{Path, PathBuf};
 | 
				
			||||||
 | 
					use regex::Regex;
 | 
				
			||||||
use structopt::StructOpt;
 | 
					use structopt::StructOpt;
 | 
				
			||||||
 | 
					use crate::kot::kerror::{Error, ErrorKind};
 | 
				
			||||||
 | 
					use crate::kot::err;
 | 
				
			||||||
 | 
					use crate::kot::kfs::create_dir_all;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use chrono;
 | 
				
			||||||
 | 
					use super::kfs;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// =============================================================================
 | 
					// =============================================================================
 | 
				
			||||||
// STRUCTS
 | 
					// STRUCTS
 | 
				
			||||||
@ -15,43 +23,86 @@ use structopt::StructOpt;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// -----------------------------------------------------------------------------
 | 
					// -----------------------------------------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Struct to outline behavior and features of kot CLI
 | 
					/// CLI for managing Linux user configurations
 | 
				
			||||||
#[derive(Debug, StructOpt)]
 | 
					#[derive(Debug, StructOpt)]
 | 
				
			||||||
#[structopt(
 | 
					#[structopt(name = "kot")]
 | 
				
			||||||
    name="kot",
 | 
					 | 
				
			||||||
    about="CLI for managing Linux user configurations"
 | 
					 | 
				
			||||||
)]
 | 
					 | 
				
			||||||
pub struct Cli {
 | 
					pub struct Cli {
 | 
				
			||||||
    #[structopt(
 | 
					  /// Local or full path to user configurations to install. Can also be a git repository.
 | 
				
			||||||
        help="Local or full path to user configurations to install",
 | 
					  ///
 | 
				
			||||||
        parse(from_os_str)
 | 
					  /// System path or repository URL for dotfiles we want to install.
 | 
				
			||||||
    )]
 | 
					  /// If a path is used, it can either be local to CWD or absolute.
 | 
				
			||||||
    pub dotfiles_dir: std::path::PathBuf,
 | 
					  /// If a URL is used for a dotfiles repository, the repo is cloned into $HOME/.local/shared/kot/dotfiles/
 | 
				
			||||||
 | 
					  #[structopt(parse(from_os_str))]
 | 
				
			||||||
 | 
					  pub dotfiles: PathBuf,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// The location to attempt installation of user configurations
 | 
				
			||||||
 | 
					  ///
 | 
				
			||||||
 | 
					  /// The desired installation directory for user configurations.
 | 
				
			||||||
 | 
					  /// By default this is your $HOME directory
 | 
				
			||||||
 | 
					  /// This could optionally point to some other directory to perform a dry run, or the --dry-run flag could be set
 | 
				
			||||||
  #[structopt(
 | 
					  #[structopt(
 | 
				
			||||||
        help="The location to attempt installation of user configurations",
 | 
					  env = "HOME", // Default value to env variable $HOME
 | 
				
			||||||
        default_value="dry-runs/kapper", // TODO: Remove temp default value after tests
 | 
					  name = "install",
 | 
				
			||||||
        // env = "HOME", // Default value to env variable $HOME
 | 
					 | 
				
			||||||
        name="install-dir",
 | 
					 | 
				
			||||||
  short, long,
 | 
					  short, long,
 | 
				
			||||||
  parse(from_os_str)
 | 
					  parse(from_os_str)
 | 
				
			||||||
  )]
 | 
					  )]
 | 
				
			||||||
    pub install_dir: std::path::PathBuf,
 | 
					  pub install_dir: PathBuf,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// The location to store backups for this user
 | 
				
			||||||
 | 
					  ///
 | 
				
			||||||
 | 
					  /// If no backup-dir is provided, we create one within the default kot data directory:
 | 
				
			||||||
 | 
					  /// $HOME/.local/share/kot/backups/
 | 
				
			||||||
  #[structopt(
 | 
					  #[structopt(
 | 
				
			||||||
        help="The location to store backups for this user",
 | 
					 | 
				
			||||||
        default_value="backups/kapper",
 | 
					 | 
				
			||||||
  name = "backup-dir",
 | 
					  name = "backup-dir",
 | 
				
			||||||
  short, long,
 | 
					  short, long,
 | 
				
			||||||
  parse(from_os_str)
 | 
					  parse(from_os_str)
 | 
				
			||||||
  )]
 | 
					  )]
 | 
				
			||||||
    pub backup_dir: std::path::PathBuf,
 | 
					  pub backup_dir: Option<PathBuf>,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// An alternate path to clone a dotfiles repository to
 | 
				
			||||||
 | 
					  ///
 | 
				
			||||||
 | 
					  /// If the clone-dir option is provided to the CLI, kot will clone the dotfiles repository into this directory.
 | 
				
			||||||
 | 
					  /// If clone-dir is not provided, the repository is cloned into $HOME/.local/share/kot/dotfiles
 | 
				
			||||||
 | 
					  /// Custom clone-dir will be used literally, and no subdirectory is created to store the cloned repository
 | 
				
			||||||
 | 
					  /// For example, clone-dir of $HOME/clonedir for repo named Dotfiles
 | 
				
			||||||
 | 
					  /// We will clone into $HOME/clonedir, and NOT $HOME/clonedir/Dotfiles
 | 
				
			||||||
 | 
					  /// The default path for cloned repos is $HOME/.local/share/kot/dotfiles/
 | 
				
			||||||
  #[structopt(
 | 
					  #[structopt(
 | 
				
			||||||
        help="Overwrites existing backups",
 | 
					  name = "clone-dir",
 | 
				
			||||||
 | 
					  short, long,
 | 
				
			||||||
 | 
					  parse(from_os_str)
 | 
				
			||||||
 | 
					  )]
 | 
				
			||||||
 | 
					  pub clone_dir: Option<PathBuf>,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Overwrites existing backups
 | 
				
			||||||
 | 
					  ///
 | 
				
			||||||
 | 
					  /// This flag will replace existing backups if during installation we encounter conflicts
 | 
				
			||||||
 | 
					  /// and the backup-dir provided already contains previous backups.
 | 
				
			||||||
 | 
					  #[structopt(
 | 
				
			||||||
 | 
					  name = "force",
 | 
				
			||||||
  short, long
 | 
					  short, long
 | 
				
			||||||
  )]
 | 
					  )]
 | 
				
			||||||
  pub force: bool,
 | 
					  pub force: bool,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Installs configurations to $HOME/.local/shared/kot/dry-runs
 | 
				
			||||||
 | 
					  ///
 | 
				
			||||||
 | 
					  /// Useful flag to set when testing what an install would do to your home directory.
 | 
				
			||||||
 | 
					  /// This is synonymous with setting --install $HOME/.local/shared/kot/dry-runs/$USER.
 | 
				
			||||||
 | 
					  /// Subsequent runs with this flag set will not delete the contents of this directory.
 | 
				
			||||||
 | 
					  #[structopt(
 | 
				
			||||||
 | 
					  name = "dry-run",
 | 
				
			||||||
 | 
					  short, long
 | 
				
			||||||
 | 
					  )]
 | 
				
			||||||
 | 
					  pub dry_run: bool,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Indicates if dotfiles is a git repository URL; Not used by CLI directly
 | 
				
			||||||
 | 
					  // + Initialized with result of regex pattern matching
 | 
				
			||||||
 | 
					  #[structopt(skip)]
 | 
				
			||||||
 | 
					  pub is_repo: bool,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Not used by CLI, used to uninstall dotfiles when error is hit
 | 
				
			||||||
 | 
					  #[structopt(skip)]
 | 
				
			||||||
 | 
					  pub conflicts: Vec<PathBuf>,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// =============================================================================
 | 
					// =============================================================================
 | 
				
			||||||
@ -70,44 +121,73 @@ pub fn from_args() -> super::Result<Cli> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
impl Cli {
 | 
					impl Cli {
 | 
				
			||||||
  /// Helper function to normalize arguments passed to program
 | 
					  /// Helper function to normalize arguments passed to program
 | 
				
			||||||
 | 
					  /// + Checks if dotfiles path is a repository URL
 | 
				
			||||||
 | 
					  /// + If dotfiles path is not a repo URL, checks the path exists on the system
 | 
				
			||||||
 | 
					  /// + Verifies install directory exists
 | 
				
			||||||
 | 
					  /// + Verifies backup directory exists and does not already contain backups
 | 
				
			||||||
  pub fn normalize(mut self) -> super::Result<Self> {
 | 
					  pub fn normalize(mut self) -> super::Result<Self> {
 | 
				
			||||||
        // If the path to the dotfiles doesn't exist, exit with error
 | 
					    // Determine if the dotfiles were provided as a github repository URL
 | 
				
			||||||
        if !&self.dotfiles_dir.exists() {
 | 
					    let re_git = Regex::new(
 | 
				
			||||||
            panic!("Error: Dotfiles configuration at {:?} does not exist", self.dotfiles_dir);
 | 
					      r"^(([A-Za-z0-9]+@|http(|s)://)|(http(|s)://[A-Za-z0-9]+@))([A-Za-z0-9.]+(:\d+)?)(?::|/)([\d/\w.-]+?)(\.git){1}$"
 | 
				
			||||||
        }
 | 
					    );
 | 
				
			||||||
        self.dotfiles_dir = self.dotfiles_dir.canonicalize()?;
 | 
					    self.is_repo = re_git.unwrap().is_match(&self.dotfiles.to_str().unwrap());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // If either the install or backup dir don't exist, create them
 | 
					    if self.is_repo {
 | 
				
			||||||
        std::fs::create_dir_all(&self.install_dir)?;
 | 
					      // If the dotfiles were provided as a repository URL initialize clone_dir
 | 
				
			||||||
        self.install_dir = self.install_dir.canonicalize()?;
 | 
					      self.clone_dir = match &self.clone_dir {
 | 
				
			||||||
        std::fs::create_dir_all(&self.backup_dir)?;
 | 
					        Some(d) => {
 | 
				
			||||||
        self.backup_dir = self.backup_dir.canonicalize()?;
 | 
					          kfs::create_dir_all(d)?;
 | 
				
			||||||
 | 
					          Some(kfs::abs(d)?)
 | 
				
			||||||
        // + To enforce the correction when error is encountered
 | 
					        },
 | 
				
			||||||
        // Get the number of configs currently in backup directory
 | 
					        None => Some(kfs::get_repo_path(self.dotfiles.to_str().unwrap()))
 | 
				
			||||||
        // + An empty backup directory returns a count of 1
 | 
					      };
 | 
				
			||||||
        let current_backups = self.backup_dir.read_dir()?.count();
 | 
					 | 
				
			||||||
        // If there are files in the backup directory already
 | 
					 | 
				
			||||||
        if current_backups > 1 {
 | 
					 | 
				
			||||||
            // If the --force flag is not set, warn and abort
 | 
					 | 
				
			||||||
            if !self.force {
 | 
					 | 
				
			||||||
                panic!("\n  Error: Backups already exist at {:?}\
 | 
					 | 
				
			||||||
                \n  Set the --force flag to overwrite configurations stored here" , self.backup_dir)
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
            // If the --force flag is set, remove backups and create new
 | 
					    else {
 | 
				
			||||||
            // + Move backups to /tmp/<BACKUP_DIRNAME>
 | 
					      // If the dotfiles were provided as a path, canonicalize it
 | 
				
			||||||
            // + If we encounter an error, we can move these temp files back to args.backup_dir
 | 
					      self.dotfiles = kfs::abs(&self.dotfiles)?;
 | 
				
			||||||
            // + On success we can delete them since new backups will have been created at args.backup_dir
 | 
					 | 
				
			||||||
            let mut options = fs_extra::dir::CopyOptions::new();
 | 
					 | 
				
			||||||
            options.copy_inside = true;
 | 
					 | 
				
			||||||
            options.overwrite = true;
 | 
					 | 
				
			||||||
            let mut temp_path = Path::new("/tmp/").to_path_buf();
 | 
					 | 
				
			||||||
            temp_path.push(self.backup_dir.file_name().unwrap());
 | 
					 | 
				
			||||||
            // Move the old backups to /tmp/ and create a new empty backup directory
 | 
					 | 
				
			||||||
            super::kfs::move_dir(&self.backup_dir, &temp_path, Some(&options))?;
 | 
					 | 
				
			||||||
            std::fs::create_dir_all(&self.backup_dir)?;
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Ok(self)
 | 
					    //
 | 
				
			||||||
 | 
					    // If either the install, backup, or clone dir does not exist, create them
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if self.dry_run {
 | 
				
			||||||
 | 
					      self.install_dir = Path::new(
 | 
				
			||||||
 | 
					        &(env!("HOME").to_owned() + &"/.local/share/kot/dry-runs/" + env!("USER"))
 | 
				
			||||||
 | 
					      ).to_path_buf();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    self.install_dir = kfs::create_dir_all(&self.install_dir)?;
 | 
				
			||||||
 | 
					    // If the CLI was not provided a backup_dir, use default naming convention
 | 
				
			||||||
 | 
					    match self.backup_dir {
 | 
				
			||||||
 | 
					      None => {
 | 
				
			||||||
 | 
					        let mut backup_dir = kfs::get_data_dir();
 | 
				
			||||||
 | 
					        backup_dir.push("backups/");
 | 
				
			||||||
 | 
					        backup_dir.push(self.dotfiles.file_name().unwrap().to_str().unwrap().to_owned()
 | 
				
			||||||
 | 
					            + ":" + &*chrono::offset::Local::now()
 | 
				
			||||||
 | 
					            .format("%Y-%m-%dT%H:%M:%S").to_string()
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        self.backup_dir = Some(kfs::create_dir_all(&backup_dir)?);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      Some(dir) => {
 | 
				
			||||||
 | 
					        // If a backup_dir was given to CLI, use it instead of default
 | 
				
			||||||
 | 
					        self.backup_dir = Some(kfs::create_dir_all(&dir)?);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    //
 | 
				
			||||||
 | 
					    // Check if the backup directory provided is empty
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // If there are files and the --force flag is not set, warn and abort
 | 
				
			||||||
 | 
					    if !self.force && kfs::dir_entries(&self.backup_dir.as_ref().unwrap())? > 1 {
 | 
				
			||||||
 | 
					      return err!(
 | 
				
			||||||
 | 
					        ErrorKind::ConfigError(format!("Backups already exist at: {:?}", &self.backup_dir)),
 | 
				
			||||||
 | 
					        "Set the --force flag to overwrite configurations stored here".to_owned()
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    // If the --force flag is set, stash backup files in /tmp/ and create new
 | 
				
			||||||
 | 
					    kfs::stash_dir(&self.backup_dir.as_ref().unwrap())?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Available CLI options pass initial checks; Return them to caller
 | 
				
			||||||
 | 
					    return Ok(self);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										78
									
								
								src/kot/kerror.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								src/kot/kerror.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,78 @@
 | 
				
			|||||||
 | 
					/*##############################################################################
 | 
				
			||||||
 | 
					## Author: Shaun Reed                                                         ##
 | 
				
			||||||
 | 
					## Legal: All Content (c) 2021 Shaun Reed, all rights reserved                ##
 | 
				
			||||||
 | 
					## About: Error module for dotfiles manager kot                               ##
 | 
				
			||||||
 | 
					##   This module supports converting errors to custom types using ? operator  ##
 | 
				
			||||||
 | 
					##                                                                            ##
 | 
				
			||||||
 | 
					## Contact: shaunrd0@gmail.com  | URL: www.shaunreed.com | GitHub: shaunrd0   ##
 | 
				
			||||||
 | 
					##############################################################################*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use std::fmt::{Debug, Display, Formatter};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Error types for kot application
 | 
				
			||||||
 | 
					#[derive(Debug)]
 | 
				
			||||||
 | 
					pub enum ErrorKind {
 | 
				
			||||||
 | 
					  ConfigError(String),
 | 
				
			||||||
 | 
					  GitError(String),
 | 
				
			||||||
 | 
					  IOError(String),
 | 
				
			||||||
 | 
					  FileError(String),
 | 
				
			||||||
 | 
					  DirError(String),
 | 
				
			||||||
 | 
					  Other(String),
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// =============================================================================
 | 
				
			||||||
 | 
					// IMPLEMENTATION
 | 
				
			||||||
 | 
					// =============================================================================
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug)]
 | 
				
			||||||
 | 
					pub struct Error {
 | 
				
			||||||
 | 
					  pub kind: ErrorKind,
 | 
				
			||||||
 | 
					  message: String,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Implement Display trait for printing found errors
 | 
				
			||||||
 | 
					impl std::fmt::Display for ErrorKind {
 | 
				
			||||||
 | 
					  fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
 | 
				
			||||||
 | 
					    write!(f, "{:?}", self)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Display for Error {
 | 
				
			||||||
 | 
					  fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
 | 
				
			||||||
 | 
					    write!(f, "Kot {:?}", self)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl std::error::Error for Error { }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Error {
 | 
				
			||||||
 | 
					  pub fn new(kind: ErrorKind, message: String) -> Error {
 | 
				
			||||||
 | 
					    Error {
 | 
				
			||||||
 | 
					      kind: kind,
 | 
				
			||||||
 | 
					      message: message.to_string(),
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Implement From<T> for each error type T that we want to handle
 | 
				
			||||||
 | 
					// These implementations handle converting from T to kot::kerror::Error using ?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Converting from std::io::Error to kot::kerror::Error::GitError
 | 
				
			||||||
 | 
					impl std::convert::From<std::io::Error> for Error {
 | 
				
			||||||
 | 
					  fn from(error: std::io::Error) -> Self {
 | 
				
			||||||
 | 
					    return Error::new(ErrorKind::IOError(error.to_string()),
 | 
				
			||||||
 | 
					                      "(std::io error)".to_owned());
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Converting from fs_extra::error::Error to kot::kerror::Error::GitError
 | 
				
			||||||
 | 
					impl std::convert::From<fs_extra::error::Error> for Error {
 | 
				
			||||||
 | 
					  fn from(error: fs_extra::error::Error) -> Self {
 | 
				
			||||||
 | 
					    return Error::new(ErrorKind::FileError(error.to_string()),
 | 
				
			||||||
 | 
					                      "(fs_extra error)".to_owned());
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// -----------------------------------------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										149
									
								
								src/kot/kfs.rs
									
									
									
									
									
								
							
							
						
						
									
										149
									
								
								src/kot/kfs.rs
									
									
									
									
									
								
							@ -6,12 +6,15 @@
 | 
				
			|||||||
## Contact: shaunrd0@gmail.com  | URL: www.shaunreed.com | GitHub: shaunrd0   ##
 | 
					## Contact: shaunrd0@gmail.com  | URL: www.shaunreed.com | GitHub: shaunrd0   ##
 | 
				
			||||||
##############################################################################*/
 | 
					##############################################################################*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Allow the use of kot::fs::Path and kot::fs::PathBuf from std::path::
 | 
					 | 
				
			||||||
pub use std::path::{Path, PathBuf};
 | 
					pub use std::path::{Path, PathBuf};
 | 
				
			||||||
pub use std::collections::HashMap;
 | 
					pub use std::collections::HashMap;
 | 
				
			||||||
pub use fs_extra::dir;
 | 
					pub use fs_extra::dir;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use std::fs;
 | 
					use std::fs;
 | 
				
			||||||
 | 
					use crate::kot::err;
 | 
				
			||||||
 | 
					use crate::kot::kerror::{Error, ErrorKind};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use super::kgit;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// =============================================================================
 | 
					// =============================================================================
 | 
				
			||||||
// IMPLEMENTATION
 | 
					// IMPLEMENTATION
 | 
				
			||||||
@ -19,16 +22,29 @@ use std::fs;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// -----------------------------------------------------------------------------
 | 
					// -----------------------------------------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn abs(dir: &PathBuf) -> super::Result<PathBuf> {
 | 
				
			||||||
 | 
					  return match dir.canonicalize() {
 | 
				
			||||||
 | 
					    Ok(result) => Ok(result),
 | 
				
			||||||
 | 
					    Err(e) => {
 | 
				
			||||||
 | 
					      err!(
 | 
				
			||||||
 | 
					        ErrorKind::IOError(e.to_string()),
 | 
				
			||||||
 | 
					        format!("Unable to canonicalize dir: {:?}", dir)
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Initialize and return a HashMap<config_dir, config_install_location>
 | 
					/// Initialize and return a HashMap<config_dir, config_install_location>
 | 
				
			||||||
/// + Later used to check each install location for conflicts before installing
 | 
					/// + Later used to check each install location for conflicts before installing
 | 
				
			||||||
/// + This function does not create or modify any files or directories
 | 
					/// + This function does not create or modify any files or directories
 | 
				
			||||||
pub fn get_target_paths(args: & super::kcli::Cli) -> super::Result<HashMap<PathBuf, PathBuf>> {
 | 
					pub fn get_target_paths(install_dir: &PathBuf, dotfiles: &PathBuf)
 | 
				
			||||||
 | 
					                        -> super::Result<HashMap<PathBuf, PathBuf>> {
 | 
				
			||||||
  let mut config_map = HashMap::new();
 | 
					  let mut config_map = HashMap::new();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Local variable for the installation directory as an absolute path
 | 
					  // Local variable for the installation directory as an absolute path
 | 
				
			||||||
    let mut config_target = args.install_dir.to_owned();
 | 
					  let mut config_target = install_dir.to_owned();
 | 
				
			||||||
  // For each file or directory within the dotfiles we're installing
 | 
					  // For each file or directory within the dotfiles we're installing
 | 
				
			||||||
    for config_entry in fs::read_dir(&args.dotfiles_dir)? {
 | 
					  for config_entry in fs::read_dir(&dotfiles)? {
 | 
				
			||||||
    let entry = config_entry?;
 | 
					    let entry = config_entry?;
 | 
				
			||||||
    // Create full path to target config file (or directory) by push onto install path
 | 
					    // Create full path to target config file (or directory) by push onto install path
 | 
				
			||||||
    config_target.push(entry.file_name());
 | 
					    config_target.push(entry.file_name());
 | 
				
			||||||
@ -36,55 +52,116 @@ pub fn get_target_paths(args: & super::kcli::Cli) -> super::Result<HashMap<PathB
 | 
				
			|||||||
    // If the entry doesn't already exist, insert it into the config_map
 | 
					    // If the entry doesn't already exist, insert it into the config_map
 | 
				
			||||||
    // + Key is full path to source config from dotfiles repo we're installing
 | 
					    // + Key is full path to source config from dotfiles repo we're installing
 | 
				
			||||||
    // + Value is desired full path to config at final install location
 | 
					    // + Value is desired full path to config at final install location
 | 
				
			||||||
        // TODO: If the entry does exist, should there be an exception?
 | 
					 | 
				
			||||||
    config_map.entry(entry.path().to_owned())
 | 
					    config_map.entry(entry.path().to_owned())
 | 
				
			||||||
        .or_insert(config_target.to_owned());
 | 
					        .or_insert(config_target.to_owned());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Reset config_target to be equal to requested install_dir
 | 
					    // Reset config_target to be equal to requested install_dir
 | 
				
			||||||
    config_target.pop();
 | 
					    config_target.pop();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
    Ok(config_map)
 | 
					  return Ok(config_map);
 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/// Checks if any config to install collides with existing files or directories
 | 
					 | 
				
			||||||
/// + Returns a count of collisions within Some(), else returns None
 | 
					 | 
				
			||||||
pub fn check_collisions(config_map : & HashMap<PathBuf, PathBuf>) -> Option<Vec<PathBuf>> {
 | 
					 | 
				
			||||||
    let mut config_conflicts = vec![];
 | 
					 | 
				
			||||||
    for (_path, target_config) in config_map.iter() {
 | 
					 | 
				
			||||||
        // If the target configuration file or directory already exists
 | 
					 | 
				
			||||||
        if target_config.exists() {
 | 
					 | 
				
			||||||
            config_conflicts.push(target_config.to_owned());
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if config_conflicts.len() > 0 {
 | 
					 | 
				
			||||||
        return Some(config_conflicts)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    return None
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Moves a single file from one location to another; Can be used to rename files
 | 
					/// Moves a single file from one location to another; Can be used to rename files
 | 
				
			||||||
 | 
					/// + Overwrites file at the dst location with the src file
 | 
				
			||||||
/// + To specify options such as overwrite for the copy operation, a custom CopyOptions can be provided
 | 
					/// + To specify options such as overwrite for the copy operation, a custom CopyOptions can be provided
 | 
				
			||||||
pub fn move_file(src: & PathBuf, dst: & PathBuf,
 | 
					pub fn move_file(src: &PathBuf, dst: &PathBuf) -> super::Result<()> {
 | 
				
			||||||
                options: Option< & fs_extra::file::CopyOptions>) -> super::Result<()> {
 | 
					  std::fs::copy(src, dst)?;
 | 
				
			||||||
    if options.is_none() {
 | 
					  std::fs::remove_file(src)?;
 | 
				
			||||||
        // Default CopyOptions for moving files
 | 
					  return Ok(());
 | 
				
			||||||
        let mut options = fs_extra::file::CopyOptions::new();
 | 
					 | 
				
			||||||
        options.overwrite = false;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    fs_extra::file::move_file(src, dst, options.unwrap())?;
 | 
					 | 
				
			||||||
    Ok(())
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Moves a directory and all of it's contents recursively
 | 
					/// Moves a directory and all of it's contents recursively
 | 
				
			||||||
/// + To specify options such as overwrite for the copy operation, a custom CopyOptions can be provided
 | 
					/// + To specify options such as overwrite for the copy operation, a custom CopyOptions can be provided
 | 
				
			||||||
 | 
					/// TODO: Implement this using std::fs to remove fs_extra dependency
 | 
				
			||||||
pub fn move_dir(src: &PathBuf, dst: &PathBuf,
 | 
					pub fn move_dir(src: &PathBuf, dst: &PathBuf,
 | 
				
			||||||
                options: Option< & fs_extra::dir::CopyOptions>) -> super::Result<()> {
 | 
					                options: Option<&fs_extra::dir::CopyOptions>)
 | 
				
			||||||
    if options.is_none() {
 | 
					                -> super::Result<()> {
 | 
				
			||||||
 | 
					  let copy_options = match options {
 | 
				
			||||||
 | 
					    Some(opts) => opts.to_owned(),
 | 
				
			||||||
 | 
					    None => {
 | 
				
			||||||
      // Default CopyOptions for moving directories
 | 
					      // Default CopyOptions for moving directories
 | 
				
			||||||
 | 
					      let mut opts = fs_extra::dir::CopyOptions::new();
 | 
				
			||||||
 | 
					      opts.copy_inside = true;
 | 
				
			||||||
 | 
					      opts.overwrite = false;
 | 
				
			||||||
 | 
					      opts
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if let Err(e) = fs_extra::dir::move_dir(src, dst, ©_options) {
 | 
				
			||||||
 | 
					    return err!(
 | 
				
			||||||
 | 
					        ErrorKind::DirError(e.to_string()),
 | 
				
			||||||
 | 
					        format!("Cannot move directory from {:?} to {:?}", src, dst)
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return Ok(());
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Recursively creates a directory
 | 
				
			||||||
 | 
					/// Returns a result that contains the absolute path to the new directory
 | 
				
			||||||
 | 
					pub fn create_dir_all(dir: &PathBuf) -> super::Result<PathBuf> {
 | 
				
			||||||
 | 
					  return match fs::create_dir_all(dir) {
 | 
				
			||||||
 | 
					    Ok(_) => {
 | 
				
			||||||
 | 
					      Ok(dir.to_owned())
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    Err(e) => {
 | 
				
			||||||
 | 
					      err!(
 | 
				
			||||||
 | 
					        ErrorKind::IOError(e.to_string()),
 | 
				
			||||||
 | 
					        format!("Unable to create directory: {:?}", dir)
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Returns the total number of entries within a directory
 | 
				
			||||||
 | 
					/// + Returns 1 for empty directories
 | 
				
			||||||
 | 
					pub fn dir_entries(dir: &PathBuf) -> super::Result<usize> {
 | 
				
			||||||
 | 
					  if !dir.exists() {
 | 
				
			||||||
 | 
					    return Ok(0)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  let count = dir.read_dir().and_then(|dir| Ok(dir.count()))?;
 | 
				
			||||||
 | 
					  return Ok(count);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Stash a directory in the temp folder, staging it for deletion
 | 
				
			||||||
 | 
					/// + We stash first instead of delete to allow recovery of these files if we run into an error
 | 
				
			||||||
 | 
					pub fn stash_dir(dir: &PathBuf) -> super::Result<()> {
 | 
				
			||||||
 | 
					  // Get the number of configs currently in backup directory
 | 
				
			||||||
 | 
					  // + An empty backup directory returns a count of 1
 | 
				
			||||||
 | 
					  if dir_entries(&dir)? > 1 {
 | 
				
			||||||
 | 
					    // Move backups to /tmp/<BACKUP_DIRNAME>
 | 
				
			||||||
 | 
					    // + If we encounter an error, we can move these temp files back to args.backup_dir
 | 
				
			||||||
 | 
					    // + On success we can delete them since new backups will have been created at args.backup_dir
 | 
				
			||||||
    let mut options = fs_extra::dir::CopyOptions::new();
 | 
					    let mut options = fs_extra::dir::CopyOptions::new();
 | 
				
			||||||
    options.copy_inside = true;
 | 
					    options.copy_inside = true;
 | 
				
			||||||
        options.overwrite = false;
 | 
					    options.overwrite = true;
 | 
				
			||||||
 | 
					    let mut temp_path = get_temp_dir();
 | 
				
			||||||
 | 
					    temp_path.push(dir.file_name().unwrap());
 | 
				
			||||||
 | 
					    // Move the old backups to /tmp/ and create a new empty backup directory
 | 
				
			||||||
 | 
					    super::kfs::move_dir(&dir, &temp_path, Some(&options))?;
 | 
				
			||||||
 | 
					    std::fs::create_dir_all(&dir)?;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
    fs_extra::dir::move_dir(src, dst, options.unwrap())?;
 | 
					  return Ok(());
 | 
				
			||||||
    Ok(())
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Gets the root temp directory used by kot to store expired files as an owned PathBuf
 | 
				
			||||||
 | 
					pub fn get_temp_dir() -> PathBuf {
 | 
				
			||||||
 | 
					  // Get temp directory from current user environment
 | 
				
			||||||
 | 
					  let mut temp = std::env::temp_dir();
 | 
				
			||||||
 | 
					  temp.push("kot/expired/");
 | 
				
			||||||
 | 
					  return temp;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Constructs a new PathBuf pointing to the default data directory used by kot
 | 
				
			||||||
 | 
					pub fn get_data_dir() -> PathBuf {
 | 
				
			||||||
 | 
					  let mut data_dir = std::path::Path::new(env!("HOME")).to_path_buf();
 | 
				
			||||||
 | 
					  data_dir.push(".local/share/kot/");
 | 
				
			||||||
 | 
					  return data_dir;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Constructs a new PathBuf pointing to the default clone directory used by kot
 | 
				
			||||||
 | 
					pub fn get_repo_path(repo_url: &str) -> PathBuf {
 | 
				
			||||||
 | 
					  let mut repo_path = get_data_dir();
 | 
				
			||||||
 | 
					  // Store the new dotfiles repo in a subdirectory using it's name
 | 
				
			||||||
 | 
					  repo_path.push("dotfiles/".to_owned() + &kgit::repo_name(repo_url) + "/");
 | 
				
			||||||
 | 
					  return repo_path;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										47
									
								
								src/kot/kgit.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								src/kot/kgit.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,47 @@
 | 
				
			|||||||
 | 
					/*##############################################################################
 | 
				
			||||||
 | 
					## Author: Shaun Reed                                                         ##
 | 
				
			||||||
 | 
					## Legal: All Content (c) 2021 Shaun Reed, all rights reserved                ##
 | 
				
			||||||
 | 
					## About: Wrapper module for git written in Rust                              ##
 | 
				
			||||||
 | 
					##                                                                            ##
 | 
				
			||||||
 | 
					## Contact: shaunrd0@gmail.com  | URL: www.shaunreed.com | GitHub: shaunrd0   ##
 | 
				
			||||||
 | 
					##############################################################################*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use std::os::linux::raw::stat;
 | 
				
			||||||
 | 
					use std::path::{PathBuf};
 | 
				
			||||||
 | 
					use std::process::{Command};
 | 
				
			||||||
 | 
					use crate::kot::err;
 | 
				
			||||||
 | 
					use super::kerror::{Error, ErrorKind};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use super::kfs;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// =============================================================================
 | 
				
			||||||
 | 
					// IMPLEMENTATION
 | 
				
			||||||
 | 
					// =============================================================================
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// -----------------------------------------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Clones a Git repository using https or ssh
 | 
				
			||||||
 | 
					/// + By default, cloned repositories are stored in $HOME/.local/share/kot/dotfiles/
 | 
				
			||||||
 | 
					pub fn clone(repo_url: &str, clone_dir: &PathBuf)
 | 
				
			||||||
 | 
					  -> super::Result<PathBuf> {
 | 
				
			||||||
 | 
					  // Clone the repository, check that status return value is 0
 | 
				
			||||||
 | 
					  let status = Command::new("git")
 | 
				
			||||||
 | 
					      .args(["clone", repo_url, clone_dir.to_str().unwrap(), "--recursive"])
 | 
				
			||||||
 | 
					      .status().unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return match status.code() {
 | 
				
			||||||
 | 
					    Some(0) => Ok(clone_dir.to_owned()),
 | 
				
			||||||
 | 
					    _ => {
 | 
				
			||||||
 | 
					      return
 | 
				
			||||||
 | 
					          err!(ErrorKind::GitError(status.to_string()),
 | 
				
			||||||
 | 
					                   format!("Unable to clone repository"));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Extracts repository name from URL
 | 
				
			||||||
 | 
					pub fn repo_name(repo_url: &str) -> String {
 | 
				
			||||||
 | 
					  return repo_url.rsplit_once('/').unwrap().1
 | 
				
			||||||
 | 
					      .strip_suffix(".git").unwrap().to_owned();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -6,9 +6,6 @@
 | 
				
			|||||||
## Contact: shaunrd0@gmail.com  | URL: www.shaunreed.com | GitHub: shaunrd0   ##
 | 
					## Contact: shaunrd0@gmail.com  | URL: www.shaunreed.com | GitHub: shaunrd0   ##
 | 
				
			||||||
##############################################################################*/
 | 
					##############################################################################*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Allow use of kot::io::Result
 | 
					 | 
				
			||||||
pub use std::io::Result;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use std::io;
 | 
					use std::io;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// =============================================================================
 | 
					// =============================================================================
 | 
				
			||||||
@ -28,6 +25,6 @@ pub fn prompt(msg: String) -> bool {
 | 
				
			|||||||
    "y" | "Y" => true,
 | 
					    "y" | "Y" => true,
 | 
				
			||||||
    "n" | "N" => false,
 | 
					    "n" | "N" => false,
 | 
				
			||||||
    // Handle garbage input
 | 
					    // Handle garbage input
 | 
				
			||||||
        _ => prompt("Please enter y/n or Y/N\n".to_owned()),
 | 
					    _ => prompt("Please enter Y/y or N/n\n".to_owned()),
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										27
									
								
								src/main.rs
									
									
									
									
									
								
							
							
						
						
									
										27
									
								
								src/main.rs
									
									
									
									
									
								
							@ -6,7 +6,7 @@
 | 
				
			|||||||
## Contact: shaunrd0@gmail.com  | URL: www.shaunreed.com | GitHub: shaunrd0   ##
 | 
					## Contact: shaunrd0@gmail.com  | URL: www.shaunreed.com | GitHub: shaunrd0   ##
 | 
				
			||||||
##############################################################################*/
 | 
					##############################################################################*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use std::path::PathBuf;
 | 
					use crate::kot::kerror::ErrorKind;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
mod kot;
 | 
					mod kot;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -18,29 +18,10 @@ mod kot;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
fn main() -> kot::Result<()> {
 | 
					fn main() -> kot::Result<()> {
 | 
				
			||||||
    // Call augmented kot::cli::from_args() to parse CLI arguments
 | 
					    // Call augmented kot::cli::from_args() to parse CLI arguments
 | 
				
			||||||
    let args = kot::kcli::from_args()?;
 | 
					    let mut args = kot::kcli::from_args()?;
 | 
				
			||||||
    // At this point all paths exist and have been converted to absolute paths
 | 
					    // At this point all paths exist and have been converted to absolute paths
 | 
				
			||||||
    println!("args: {:?}\n", args);
 | 
					    println!("args: {:?}\n", args);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Attempt to install the configurations, checking for collisions
 | 
					    // Apply CLI arguments and attempt to install dotfiles
 | 
				
			||||||
    match kot::install_configs(&args) {
 | 
					    return kot::handle_args(&mut args);
 | 
				
			||||||
        Err(e) => {
 | 
					 | 
				
			||||||
            // If there was an error, show the error type and run settings
 | 
					 | 
				
			||||||
            println!(
 | 
					 | 
				
			||||||
                "Error: {:?}\n+ Configs used: {:?}\n+ Install directory: {:?}\n",
 | 
					 | 
				
			||||||
                e, args.dotfiles_dir, args.install_dir
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            // If we were forcing a backup and met some error, revert backups to last good state
 | 
					 | 
				
			||||||
            // TODO: Isolate this to limit error scope to backup related functions
 | 
					 | 
				
			||||||
            if args.force {
 | 
					 | 
				
			||||||
                let mut temp_path : PathBuf = kot::kfs::Path::new("/tmp/").to_path_buf();
 | 
					 | 
				
			||||||
                temp_path.push(args.backup_dir.file_name().unwrap());
 | 
					 | 
				
			||||||
                kot::kfs::move_dir(&temp_path, &args.backup_dir, None)?;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        _ => ()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    // Configurations installed successfully
 | 
					 | 
				
			||||||
    Ok(())
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user