Compare commits

...

18 commits

Author SHA1 Message Date
8614bfacbd
making docker a lot better 2025-05-12 00:03:54 -06:00
736b62528b
update lock, use build stages for smaller images 2025-04-28 20:38:41 -06:00
9d5639462a
add draft man page 2025-03-25 23:01:05 -06:00
fb25f0c0f4
starting to add ctrlc checks for main run fn 2025-03-25 21:45:09 -06:00
fb8ed1ae8c
clean up work dirs 2025-03-25 21:21:11 -06:00
b98a3beec4
do more things if a repo exists 2025-03-24 21:07:26 -06:00
1690131e4f
add fetchoptions to ff 2025-03-23 22:29:31 -06:00
21080610a7
build major docker images, change fast forward params 2025-03-23 22:27:22 -06:00
6fda40ab6d
bump patch 2025-03-23 21:41:31 -06:00
6c36757c51
fetch before fast forward on loop 2025-03-23 21:39:45 -06:00
5fcfbcb11c
bump version 2025-03-23 20:39:01 -06:00
e86793bfdd
multi configs seem to work now 2025-03-23 20:30:25 -06:00
445ba1fe1f
consolidate common remotecallbacks code to a single fn 2025-03-23 17:56:58 -06:00
81f825b9d3
make pushing tags optional 2025-03-23 17:15:18 -06:00
0ec62a34d4
lock update for recent git2 commit 2025-03-20 22:06:21 -06:00
d317dae413
bump version 2025-03-20 22:04:25 -06:00
f37549748c
fix formatting, use git2 fork until/if merged in 2025-03-20 22:01:50 -06:00
5d06e08f5b
update readme, example config 2025-03-18 23:14:28 -06:00
16 changed files with 894 additions and 435 deletions

View file

@ -1,4 +1,4 @@
edition = "2021"
tab_spaces = 2
match_block_trailing_comma = true
wrap_comments = true
#wrap_comments = true

276
Cargo.lock generated
View file

@ -79,9 +79,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.16"
version = "1.2.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c"
checksum = "32db95edf998450acc7881c932f94cd9b05c87b4b2599e8bab064753da4acfd1"
dependencies = [
"jobserver",
"libc",
@ -102,9 +102,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "clap"
version = "4.5.32"
version = "4.5.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6088f3ae8c3608d19260cd7445411865a485688711b78b5be70d78cd96136f83"
checksum = "ed93b9805f8ba930df42c2590f05453d5ec36cbb85d018868a5b24d31f6ac000"
dependencies = [
"clap_builder",
"clap_derive",
@ -112,9 +112,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.32"
version = "4.5.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22a7ef7f676155edfb82daa97f99441f3ebf4a58d5e32f295a56259f1b6facc8"
checksum = "379026ff283facf611b0ea629334361c4211d1b12ee01024eec1591133b04120"
dependencies = [
"anstream",
"anstyle",
@ -176,9 +176,9 @@ dependencies = [
[[package]]
name = "ctrlc"
version = "3.4.5"
version = "3.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90eeab0aa92f3f9b4e87f258c72b139c207d251f9cbc1080a0086b86a8870dd3"
checksum = "46f93780a459b7d656ef7f071fe699c4d3d2cb201c4b24d085b6ddc505276e73"
dependencies = [
"nix",
"windows-sys",
@ -231,10 +231,21 @@ dependencies = [
]
[[package]]
name = "git2"
version = "0.20.0"
name = "getrandom"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fda788993cc341f69012feba8bf45c0ba4f3291fcc08e214b4d5a7332d88aff"
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasi",
]
[[package]]
name = "git2"
version = "0.20.1"
source = "git+https://github.com/brysonsteck/git2-rs?branch=certificates#1ca81270e632ac30c6e1367d3032ca3043068b2f"
dependencies = [
"bitflags",
"libc",
@ -247,9 +258,9 @@ dependencies = [
[[package]]
name = "hashbrown"
version = "0.15.2"
version = "0.15.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3"
[[package]]
name = "heck"
@ -265,21 +276,22 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "icu_collections"
version = "1.5.0"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526"
checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47"
dependencies = [
"displaydoc",
"potential_utf",
"yoke",
"zerofrom",
"zerovec",
]
[[package]]
name = "icu_locid"
version = "1.5.0"
name = "icu_locale_core"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637"
checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a"
dependencies = [
"displaydoc",
"litemap",
@ -288,31 +300,11 @@ dependencies = [
"zerovec",
]
[[package]]
name = "icu_locid_transform"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e"
dependencies = [
"displaydoc",
"icu_locid",
"icu_locid_transform_data",
"icu_provider",
"tinystr",
"zerovec",
]
[[package]]
name = "icu_locid_transform_data"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e"
[[package]]
name = "icu_normalizer"
version = "1.5.0"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f"
checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979"
dependencies = [
"displaydoc",
"icu_collections",
@ -320,67 +312,54 @@ dependencies = [
"icu_properties",
"icu_provider",
"smallvec",
"utf16_iter",
"utf8_iter",
"write16",
"zerovec",
]
[[package]]
name = "icu_normalizer_data"
version = "1.5.0"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516"
checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3"
[[package]]
name = "icu_properties"
version = "1.5.1"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5"
checksum = "2549ca8c7241c82f59c80ba2a6f415d931c5b58d24fb8412caa1a1f02c49139a"
dependencies = [
"displaydoc",
"icu_collections",
"icu_locid_transform",
"icu_locale_core",
"icu_properties_data",
"icu_provider",
"tinystr",
"potential_utf",
"zerotrie",
"zerovec",
]
[[package]]
name = "icu_properties_data"
version = "1.5.0"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569"
checksum = "8197e866e47b68f8f7d95249e172903bec06004b18b2937f1095d40a0c57de04"
[[package]]
name = "icu_provider"
version = "1.5.0"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9"
checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af"
dependencies = [
"displaydoc",
"icu_locid",
"icu_provider_macros",
"icu_locale_core",
"stable_deref_trait",
"tinystr",
"writeable",
"yoke",
"zerofrom",
"zerotrie",
"zerovec",
]
[[package]]
name = "icu_provider_macros"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "idna"
version = "1.0.3"
@ -394,9 +373,9 @@ dependencies = [
[[package]]
name = "idna_adapter"
version = "1.2.0"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71"
checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
dependencies = [
"icu_normalizer",
"icu_properties",
@ -404,9 +383,9 @@ dependencies = [
[[package]]
name = "indexmap"
version = "2.8.0"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058"
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
dependencies = [
"equivalent",
"hashbrown",
@ -420,24 +399,24 @@ checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "jobserver"
version = "0.1.32"
version = "0.1.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0"
checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a"
dependencies = [
"getrandom",
"libc",
]
[[package]]
name = "libc"
version = "0.2.171"
version = "0.2.172"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
[[package]]
name = "libgit2-sys"
version = "0.18.0+1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1a117465e7e1597e8febea8bb0c410f1c7fb93b1e1cddf34363f8390367ffec"
version = "0.18.1+1.9.0"
source = "git+https://github.com/brysonsteck/git2-rs?branch=certificates#1ca81270e632ac30c6e1367d3032ca3043068b2f"
dependencies = [
"cc",
"libc",
@ -475,15 +454,15 @@ dependencies = [
[[package]]
name = "litemap"
version = "0.7.5"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856"
checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956"
[[package]]
name = "log"
version = "0.4.26"
version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]]
name = "memchr"
@ -493,9 +472,9 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "nix"
version = "0.29.0"
version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
dependencies = [
"bitflags",
"cfg-if",
@ -505,9 +484,9 @@ dependencies = [
[[package]]
name = "once_cell"
version = "1.21.1"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "openssl-probe"
@ -517,9 +496,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
[[package]]
name = "openssl-sys"
version = "0.9.106"
version = "0.9.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8bb61ea9811cc39e3c2069f40b8b8e2e70d8569b361f879786cc7ed48b777cdd"
checksum = "e145e1651e858e820e4860f7b9c5e169bc1d8ce1c86043be79fa7b7634821847"
dependencies = [
"cc",
"libc",
@ -540,10 +519,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]]
name = "proc-macro2"
version = "1.0.94"
name = "potential_utf"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84"
checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585"
dependencies = [
"zerovec",
]
[[package]]
name = "proc-macro2"
version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
dependencies = [
"unicode-ident",
]
@ -572,9 +560,15 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "5.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
[[package]]
name = "refractr"
version = "0.5.0"
version = "0.6.2"
dependencies = [
"clap",
"colored",
@ -621,9 +615,9 @@ dependencies = [
[[package]]
name = "sha2"
version = "0.10.8"
version = "0.10.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures",
@ -638,9 +632,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "smallvec"
version = "1.14.0"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd"
checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
[[package]]
name = "stable_deref_trait"
@ -656,9 +650,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
version = "2.0.100"
version = "2.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0"
checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
dependencies = [
"proc-macro2",
"quote",
@ -667,9 +661,9 @@ dependencies = [
[[package]]
name = "synstructure"
version = "0.13.1"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
dependencies = [
"proc-macro2",
"quote",
@ -678,9 +672,9 @@ dependencies = [
[[package]]
name = "tinystr"
version = "0.7.6"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f"
checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b"
dependencies = [
"displaydoc",
"zerovec",
@ -688,9 +682,9 @@ dependencies = [
[[package]]
name = "toml"
version = "0.8.20"
version = "0.8.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148"
checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae"
dependencies = [
"serde",
"serde_spanned",
@ -700,26 +694,33 @@ dependencies = [
[[package]]
name = "toml_datetime"
version = "0.6.8"
version = "0.6.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.22.24"
version = "0.22.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474"
checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
"toml_write",
"winnow",
]
[[package]]
name = "toml_write"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076"
[[package]]
name = "typenum"
version = "1.18.0"
@ -763,12 +764,6 @@ dependencies = [
"log",
]
[[package]]
name = "utf16_iter"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246"
[[package]]
name = "utf8_iter"
version = "1.0.4"
@ -793,6 +788,15 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wasi"
version = "0.14.2+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
dependencies = [
"wit-bindgen-rt",
]
[[package]]
name = "winapi"
version = "0.2.8"
@ -880,30 +884,33 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.7.4"
version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36"
checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec"
dependencies = [
"memchr",
]
[[package]]
name = "write16"
version = "1.0.0"
name = "wit-bindgen-rt"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936"
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
dependencies = [
"bitflags",
]
[[package]]
name = "writeable"
version = "0.5.5"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
[[package]]
name = "yoke"
version = "0.7.5"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40"
checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc"
dependencies = [
"serde",
"stable_deref_trait",
@ -913,9 +920,9 @@ dependencies = [
[[package]]
name = "yoke-derive"
version = "0.7.5"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154"
checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6"
dependencies = [
"proc-macro2",
"quote",
@ -945,10 +952,21 @@ dependencies = [
]
[[package]]
name = "zerovec"
version = "0.10.4"
name = "zerotrie"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079"
checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595"
dependencies = [
"displaydoc",
"yoke",
"zerofrom",
]
[[package]]
name = "zerovec"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428"
dependencies = [
"yoke",
"zerofrom",
@ -957,9 +975,9 @@ dependencies = [
[[package]]
name = "zerovec-derive"
version = "0.10.3"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f"
dependencies = [
"proc-macro2",
"quote",

View file

@ -1,14 +1,14 @@
[package]
name = "refractr"
license = "MPL-2.0"
version = "0.5.0"
version = "0.6.2"
edition = "2021"
[dependencies]
clap = { version = "4.5.29", features = ["derive"] }
colored = "3.0.0"
ctrlc = "3.4.5"
git2 = "0.20.0"
git2 = { git = "https://github.com/brysonsteck/git2-rs", branch = "certificates" }
hex = "0.4.3"
quit = "2.0.0"
serde = "1.0.217"

View file

@ -1,12 +0,0 @@
FROM git.brysonsteck.xyz/brysonsteck/refractr:latest
# replace "path/" with the path containing your refractr configs
COPY path/ /etc/refractr
# use --secret with docker build to specify your ssh key
# make sure your configs use the location below
RUN --mount=type=secret,id=key,target=/id.pub \
cp /id.pub /etc/refractr && chmod 400 /etc/refractr/id.pub
# add arguments to specify verbosity and configs as needed
CMD ["refractr", "-c", "/etc/refractr/config.json"]

View file

@ -37,7 +37,16 @@ docker build -t refractr --build-arg UID=$(id -u) --build-arg GID=$(id -g) -f pa
### Windows
Coming soon...
You can download pre-built binaries from the [Releases page](https://git.brysonsteck.xyz/brysonsteck/refractr/releases) or from my package mirror:
```powershell
# Download the exe with PowerShell
Invoke-WebRequest -Uri https://pkg.brysonsteck.xyz/dist/refractr/latest/windows/x86_64/refractr.exe -OutFile refractr.exe
# Download the SHA256 checksum
Invoke-WebRequest -Uri https://pkg.brysonsteck.xyz/dist/refractr/latest/windows/x86_64/refractr.exe.sha256.txt -OutFile refractr.exe.sha256.txt
# Verify hashes match (should return "True")
(Get-Content .\refractr.exe.sha256.txt) -eq (Get-FileHash .\refractr.exe).hash
```
### macOS
@ -45,15 +54,26 @@ Coming soon...
### Build from Source
To build and run refractr from source, run:
To build and run refractr from source, you may need some packages installed on your system. On vanilla installations of FreeBSD, OpenBSD, Windows and macOS, you should be able to compile refractr without issue. On Linux, however, you may need to install additional packages that provide the `pkg-config` binary and OpenSSL headers. Refer to your package manager for the correct package names that you will need to verify are installed.
```sh
# the apt command only applies to Ubuntu/Debian
# you will need to install the correct packages using your package manager otherwise
apt install git pkg-config libssl-dev
# clone the repository
git clone https://git.brysonsteck.xyz/brysonsteck/refractr && cd refractr
# build the release binary
cargo build --release
# build the debug binary (not recommended for normal use)
cargo build
# you can run the binary in the target directory
# or move it wherever you wish
./target/release/refractr
```
## FAQ
### Couldn't I do this with a shell script and a cron job or (insert alternative method here)?
Yes.
### Then why reinvent the wheel?
Stop asking such asinine questions.

24
build
View file

@ -2,22 +2,32 @@
# Create all the different builds for refractr
version=$(cat Cargo.toml | grep -m1 version | awk -F' ' '{print $3}' | sed 's|"||g')
major_version=$(echo $version | awk -F'.' '{print $1}')
uid=$(id -u)
gid=$(id -g)
cargo update
cargo clean
date=$(date -u --rfc-3339=seconds)
cargo=$(which cargo 2> /dev/null)
if [ -n "$cargo" ]; then
cargo update
cargo clean
fi
# docker builds
docker build -t refractr:$version --build-arg UID=$uid --build-arg GID=$gid --build-arg VERSION=$version -f package.Dockerfile .
docker tag refractr:$version refractr:latest
if test "$1" = "push"; then
docker build -t refractr:$version -t refractr:$major_version -t refractr:latest \
--build-arg VERSION=$version --build-arg DATE="$date" -f docker/Dockerfile .
if [ "$1" = "push" ]; then
docker tag refractr:$version git.brysonsteck.xyz/brysonsteck/refractr:latest
docker tag refractr:$version git.brysonsteck.xyz/brysonsteck/refractr:$version
docker tag refractr:$version git.brysonsteck.xyz/brysonsteck/refractr:$major_version
docker push -a git.brysonsteck.xyz/brysonsteck/refractr
docker image rm git.brysonsteck.xyz/brysonsteck/refractr:latest
docker image rm git.brysonsteck.xyz/brysonsteck/refractr:$version
docker image rm git.brysonsteck.xyz/brysonsteck/refractr:$major_version
fi
# rust build
cargo build
cargo build --release
if [ -n "$cargo" ]; then
cargo build
cargo build --release
fi

31
docker/Dockerfile Normal file
View file

@ -0,0 +1,31 @@
FROM rust:1-alpine AS build
ENV REFRACTR_DOCKER="true"
WORKDIR /usr/src/refractr
COPY . .
RUN apk upgrade --no-cache && apk add --no-cache pkgconfig libc-dev openssl-dev openssl openssl-libs-static
RUN cargo install --path . && cargo clean
FROM alpine:3 AS package
ARG VERSION
ARG DATE
LABEL org.opencontainers.image.title="refractr"
LABEL org.opencontainers.image.authors="me@brysonsteck.xyz"
LABEL org.opencontainers.image.version="${VERSION}"
LABEL org.opencontainers.image.url="https://git.brysonsteck.xyz/brysonsteck/-/packages/container/refractr/${VERSION}"
LABEL org.opencontainers.image.source="https://git.brysonsteck.xyz/brysonsteck/refractr"
LABEL org.opencontainers.image.licenses="MPL-2.0"
LABEL org.opencontainers.image.created="${DATE}"
RUN apk upgrade --no-cache && apk add --no-cache openssl
RUN mkdir /etc/refractr
COPY ./docker/entrypoint.sh /usr/local/bin/entrypoint.sh
COPY --from=build /usr/src/refractr /usr/src
COPY --from=build /usr/local/cargo/bin/refractr /usr/local/bin
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]

View file

@ -0,0 +1,17 @@
services:
refractr:
image: git.brysonsteck.xyz/brysonsteck/refractr:latest
environment:
# change these to your uid/gid
# if omitted, the container will guess
- UID=1000
- GID=1000
volumes:
- /home/bryson/configs:/etc/refractr:ro
secrets:
- ssh_key
secrets:
ssh_key:
# available in /run/secrets/ssh_key
file: /home/bryson/.ssh/id_rsa

38
docker/entrypoint.sh Executable file
View file

@ -0,0 +1,38 @@
#!/bin/sh
# Entrypoint file for refractr
# Runs refractr with some verbosity and all configs in /etc/refractr
#
# Copyright 2025 Bryson Steck <me@brysonsteck.xyz>
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
#
if [ -z "$UID" ]; then
set=$(ls -lnd /etc/refractr/ | awk '{print $3}')
echo "UID not set! Setting to id $set (owner of /etc/refractr)"
export UID=$set
fi
if [ -z "$GID" ]; then
set=$(ls -lnd /etc/refractr/ | awk '{print $4}')
echo "GID not set! Setting to id $set (group of /etc/refractr)"
export GID=$set
fi
if ! ls /etc/refractr/*.toml &> /dev/null; then
echo "Failed to find any toml config files for refractr!"
echo "Make sure you copied configs or set up volumes correctly."
exit 1
fi
args=""
for config in /etc/refractr/*.toml; do
args="${args}-c '${config}' "
done
addgroup -g $GID dockeruser
adduser -u $UID -G $(grep :$GID: /etc/group | awk -F: '{print $1}') -D dockeruser
su -p - dockeruser -c "refractr -vv ${args}"

101
man/refractr.1 Normal file
View file

@ -0,0 +1,101 @@
.TH REFRACTR 1 "Updated 2025-03-25" "Bryson Steck"
.SH NAME
refractr \- an automated push mirroring utility for Git repositories
.SH SYNOPSIS
.B refractr
[\fBOPTIONS\fR]
.SH DESCRIPTION
refractr is an automated push mirroring utility for Git repositories.
With no options, refractr will attempt to load the config located at /etc/refractr/config.toml and run with that configuration.
For details for configuration files, see CONFIGURATION OPTIONS.
.SH OPTIONS
.sp
.TP 0.5i
\fB\-h\fR, \fB\-\-help\fR
Display a brief help message on the command line.
.TP 0.5i
\fB\-v\fR, \fB\-\-version\fR
Display the version of refractr on the command line.
.TP 0.5i
\fB\-c\fR, \fB\-\-config\fR [FILE]
Specify the absolute or relative path to a configuration file. Multiple configuration files can be specified by using multiple flags.
.P
.TP 0.5i
\fB\-v\fR, \fB\-\-verbose...\fR
Specify the level of verbose messages for refractr to output.
The functionality of this flag is similar to ssh(1); adding multiple flags will increase the amount of output with specific messages to what refractr is doing.
The more flags specified, the more low-level messages will appear.
.TP 0.5i
\fB\-e\fR, \fB\-\-create\fR
Print a full, commented config file to the standard output and exit. You can use this to generate config files that you can fill in and pass to refractr with the -c flag.
Information on the values for a refractr configuration can be found in the CONFIGURATION section.
.TP 0.5i
\fB\-s\fR, \fB\-\-strict\fR
Enable strict mode.
By design, refractr will ignore problems that occur when pushing to remotes and verifying host signatures when using SSH remotes.
Some of these errors may include networking issues and a host missing from your SSH known_hosts file, but refractr takes your configurations as
implicit trust that the remotes you pull/push from are correct and reachable. This also makes it so refractr will continuously try to
update the remotes with changes from the upstream repository to avoid synchronization issues.
If you wish for refractr to exit on these kinds of errors as a way to know of these issues, you can use this flag to enable "strict" mode.
Doing so will make refractr exit instead of continue or loop forever when encountering these problems.
.TP 0.5i
\fB\-p\fR, \fB\-\-persist\fR
Do not recursively delete the working directory as defined in your configuration file(s).
This can be helpful for troubleshooting issues with your repository, a potential bug with refractr, or general development.
.P
.SH CONFIGURATION
Below are the values for defining a refractr configuration.
.TP 0.5i
\fBfrom (string, REQUIRED)\fR
The original/upstream repository URI. This field MUST START with "https://" or "ssh://".
.TP 0.5i
\fBto (array, REQUIRED)\fR
The list of remotes you wish to push the repo to as cloned from the "from" field.
This field must be SSH remotes and the repository must exist on the remotes.
.TP 0.5i
\fBbranches (array, REQUIRED)\fR
The list of branches from the original/upstream repository you wish to push to the remotes.
.TP 0.5i
\fBwork_dir (string, OPTIONAL)\fR
The path on the filesystem refractr will clone this repository to and work from.
If omitted, this field defaults to "/tmp/refractr" on UNIX(-like) systems and "$env:TEMP\\refractr" on Windows.
.TP 0.5i
\fBpush_tags (boolean, REQUIRED)\fR
Push tags pulled from the original/upstream repository to the remotes.
.TP 0.5i
\fBgit.ssh_identity_file (string, REQUIRED)\fR
The path to your SSH private key for authenticating to the remotes specified in the "from" (if using SSH) and "to" fields.
If using Docker, you will need to copy your private key to your container or build it in an image using secrets in your Dockerfile.
.TP 0.5i
\fBschedule.enabled (bool, REQUIRED)\fR
Enable the schedule feature of refractr. This allows you to regularly pull updates and push them out on a regular basis.
.TP 0.5i
\fBschedule.interval (integer, DEPENDS ON schedule.enabled)\fR
The amount of time in seconds to pull updates and push them out to remotes.
To avoid creating unneeded stress on servers, this value must be greater than or equal to 60.
If "schedule.enabled" is false, refractr will not check for this value.
If "scheduled.enabled" is true, refractr will check this value. If it does not exist or is less than 60, refractr will return an error.
.SH AUTHORS
.B Bryson Steck <me@brysonsteck.xyz> (https://brysonsteck.xyz)
.P
.SH CONTRIBUTE
refractr is free and open source! Please report any bugs in, provide feedback for, or contribute to refractr by visiting the Codeberg repository:
.IR https://codeberg.org/brysonsteck/refractr
The Codeberg repository is proof of refractr at work. An upstream, read-only version of the repository is also available at:
.IR https://git.brysonsteck.xyz/brysonsteck/refractr
.P
.SH LICENSE
Copyright 2025 Bryson Steck
The binaries and source code of refractr is distributed under and subject to the terms of the Mozilla Public License v2 (MPL-2.0).
A copy of this license should have been distributed with this software. If you did not receive a copy, you can view the license at:
.IR https://www.mozilla.org/en-US/MPL/2.0/
License violations can be reported by creating an issue in the Codeberg repository under the CONTRIBUTE section.
.EFRATR

View file

@ -1,22 +0,0 @@
FROM rust:slim
ARG UID="1000"
ARG GID="1000"
ARG VERSION
ENV REFRACTR_DOCKER="true"
LABEL org.opencontainers.image.authors="me@brysonsteck.xyz"
LABEL version="${VERSION}"
LABEL license="MPL-2.0"
WORKDIR /usr/src/refractr
COPY . .
RUN apt update && apt install pkg-config libssl-dev -y
RUN cargo install --path .
RUN groupadd -g $GID refractr
RUN useradd -u $UID -g $GID -mN refractr
RUN mkdir /etc/refractr && chown refractr:refractr /etc/refractr
USER refractr
CMD ["refractr"]

View file

@ -15,7 +15,8 @@ pub enum ExitCode {
RemoteError = 5,
PushError = 6,
FetchError = 7,
ConfigError = 8
ConfigError = 8,
HaltError = 130,
}
pub struct ReturnData {
@ -23,17 +24,23 @@ pub struct ReturnData {
pub msg: String,
}
pub fn error(msg: String, code: ExitCode) {
pub fn error_quit(msg: String, code: ExitCode) {
eprintln!("{} {}", "error:".red().bold(), msg);
quit::with_code(code as u8)
}
pub fn error(msg: String) {
eprintln!("{} {}", "error:".red().bold(), msg);
}
pub fn warning(msg: String) {
eprintln!("{} {}", "warning:".yellow().bold(), msg)
}
pub fn verbose(level: u8, msg_lvl: u8, msg: String) {
if level < msg_lvl { return };
if level < msg_lvl {
return;
};
let mut prefix = String::new();
for _ in 0..msg_lvl {
prefix += "=";

View file

@ -20,7 +20,7 @@ use toml;
pub struct ConfigFile {
pub path: String,
pub file: Metadata,
pub config: Config
pub config: Config,
}
impl fmt::Display for ConfigFile {
@ -40,21 +40,24 @@ impl fmt::Display for ConfigFile {
if i < self.config.to.len() - 1 {
to_list.push_str(", ");
}
};
}
to_list.push(']');
let work_dir_path = match &self.config.work_dir {
None => {
if cfg!(windows) {
format!("Using default \"{}\\refractr\"", match env::var("TEMP") {
Ok(val) => val,
Err(_) => format!("This shouldn't happen!")
})
format!(
"Using default \"{}\\refractr\"",
match env::var("TEMP") {
Ok(val) => val,
Err(_) => format!("This shouldn't happen!"),
}
)
} else {
format!("Using default: /tmp/refractr")
}
},
Some(path) => format!("{}", path)
Some(path) => format!("{}", path),
};
let schedule_interval = match self.config.schedule.interval {
@ -65,10 +68,15 @@ impl fmt::Display for ConfigFile {
String::from("This shouldn't happen!\n")
}
},
Some(int) => format!("\n Scheduled interval in seconds: {}", int.to_string())
Some(int) => format!(
"\n Scheduled interval in seconds: {}",
int.to_string()
),
};
write!(f, "Config file: \"{}\"\n \
write!(
f,
"Config file: \"{}\"\n \
Is a file: {}\n \
Read only: {}\n \
Configuration:\n \
@ -78,82 +86,146 @@ impl fmt::Display for ConfigFile {
Working directory: {}\n \
SSH key for pushing clone: {}\n \
Schedule enabled: {}\
{}"
, self.path, self.file.is_file(), self.file.permissions().readonly(), self.config.from
, to_list, branches_list, work_dir_path, self.config.git.ssh_identity_file, self.config.schedule.enabled
, schedule_interval)
{}",
self.path,
self.file.is_file(),
self.file.permissions().readonly(),
self.config.from,
to_list,
branches_list,
work_dir_path,
self.config.git.ssh_identity_file,
self.config.schedule.enabled,
schedule_interval
)
}
}
#[derive(PartialEq)]
#[derive(Deserialize)]
#[derive(PartialEq, Deserialize)]
pub struct Config {
pub from: String,
pub to: Vec<String>,
pub branches: Vec<String>,
pub work_dir: Option<String>,
pub push_tags: bool,
pub git: Git,
pub schedule: Schedule
pub schedule: Schedule,
}
#[derive(PartialEq)]
#[derive(Deserialize)]
#[derive(PartialEq, Deserialize)]
pub struct Git {
pub ssh_identity_file: String,
}
#[derive(PartialEq)]
#[derive(Deserialize)]
#[derive(PartialEq, Deserialize)]
pub struct Schedule {
pub enabled: bool,
pub interval: Option<i32>,
}
pub fn get_work_dir(work_dir: &Option<String>) -> String {
match work_dir {
None => {
if cfg!(windows) {
format!("{}\\refractr", env::var("TEMP").unwrap())
} else {
format!("/tmp/refractr")
}
},
Some(path) => path.clone(),
}
}
pub fn read_config(paths: Vec<PathBuf>, refractr: &Refractr) -> Result<Vec<ConfigFile>, String> {
let mut config_files: Vec<ConfigFile> = vec![];
for path in paths {
common::verbose(refractr.verbose, 1, format!("Reading config file: \"{}\"", String::from(path.to_string_lossy())));
common::verbose(
refractr.verbose,
1,
format!(
"Reading config file: \"{}\"",
String::from(path.to_string_lossy())
),
);
let mut data = String::new();
let mut file = match File::open(path.as_path()) {
Err(e) => return Err(format!("unable to open {}: {}", path.as_path().display(), e)),
Ok(file) => file
Err(e) => {
return Err(format!(
"unable to open {}: {}",
path.as_path().display(),
e
))
},
Ok(file) => file,
};
if let Err(e) = file.read_to_string(&mut data) {
return Err(format!("unable to read {}: {}", path.as_path().display(), e))
return Err(format!(
"unable to read {}: {}",
path.as_path().display(),
e
));
}
let config: Config = match toml::from_str(&data) {
Ok(c) => c,
Err(e) => return Err(format!("issues parsing toml file {}: {}", path.as_path().display(), e))
Err(e) => {
return Err(format!(
"issues parsing toml file {}: {}",
path.as_path().display(),
e
))
},
};
let config_file = ConfigFile {
path: match fs::canonicalize(&path) {
Err(_) => return Err(format!("cannot get absolute path of config file: {}", path.as_path().display())),
Ok(abs) => abs.to_string_lossy().to_string()
Err(_) => {
return Err(format!(
"cannot get absolute path of config file: {}",
path.as_path().display()
))
},
Ok(abs) => abs.to_string_lossy().to_string(),
},
file: match fs::metadata(&path) {
Err(_) => return Err(format!("cannot obtain metadata for config file: {}", path.as_path().display())),
Ok(metadata) => metadata
Err(_) => {
return Err(format!(
"cannot obtain metadata for config file: {}",
path.as_path().display()
))
},
Ok(metadata) => metadata,
},
config: match verify_config(&config) {
Err(e) => return Err(format!("invalid config {}: {}", path.as_path().display(), e)),
Ok(_) => config
}
Err(e) => {
return Err(format!(
"invalid config {}: {}",
path.as_path().display(),
e
))
},
Ok(_) => config,
},
};
let mut dup = false;
for i in &config_files {
if i.path == config_file.path {
common::warning(format!("skipping config file \"{}\" as it was already read", path.as_path().display()));
common::warning(format!(
"skipping config file \"{}\" as it was already read",
path.as_path().display()
));
dup = true;
break;
} else if i.config == config_file.config {
common::warning(format!("config files \"{}\" and \"{}\" appear to have the same config", i.path, config_file.path));
common::warning(format!(
"config files \"{}\" and \"{}\" appear to have the same config",
i.path, config_file.path
));
}
}
if !dup {
config_files.push(config_file);
}
@ -167,31 +239,26 @@ fn verify_config(config: &Config) -> Result<(), String> {
match config.schedule.interval {
Some(i) => {
if i < 60 {
return Err(format!("schedule is enabled, but less than 60"))
return Err(format!("schedule is enabled, but less than 60"));
}
},
None => return Err(format!("schedule is enabled, but no interval was defined"))
None => return Err(format!("schedule is enabled, but no interval was defined")),
}
}
match &config.work_dir {
Some(path) => format!("{}", path),
None => {
if cfg!(windows) {
match env::var("TEMP") {
Ok(val) => val,
Err(_) => return Err(format!("cannot determine the default temp dir"))
}
} else {
format!("/tmp/refractr")
if let None = &config.work_dir {
if cfg!(windows) {
if let Err(e) = env::var("TEMP") {
return Err(format!("cannot determine the default temp dir: {}", e));
}
},
}
};
if !&config.from.starts_with("ssh://")
&& !&config.from.starts_with("https://")
&& !&config.from.starts_with("http://") {
return Err(format!("'from' value does not use a supported protocol"))
&& !&config.from.starts_with("http://")
{
return Err(format!("'from' value does not use a supported protocol"));
}
Ok(())

View file

@ -2,40 +2,37 @@
# Example configuration for refractr
# ***********************************
[config]
# The "from" field is a string of the original/main repository you want to pull
# This field is REQUIRED
# This field is REQUIRED and MUST start with "https://" or "ssh://"
#from = "https://git.brysonsteck.xyz/brysonsteck/refractr"
# The "to" field is a list of strings of the repositories you want to push the repo
# from the "from" field to. These repositories must exist on the remote server
# It's recommended that you use SSH to avoid HTTPS authentication
# This field is REQUIRED
# The "to" field is a list of strings of the remotes you want to push the repo from the "from" field to
# These repositories must exist on the remote server
# This field is REQUIRED and MUST be SSH remotes
#to = ["git@codeberg.org:brysonsteck/refractr.git", "git@github.com:brysonsteck/refractr.git"]
# The "branches" field is a list of branches you want to mirror from the original
# repository.
# The "branches" field is a list of branches you want to mirror from the original repository
# This field is REQUIRED
#branches = ["master"]
# The "work_dir" field is where refractr will write the clone to
# This field is OPTIONAL, will default to /tmp/refractr on *NIX and
# $env:TEMP\refractr on Windows
# This field is OPTIONAL, will default to /tmp/refractr on *NIX and $env:TEMP\refractr on Windows
#work_dir = /tmp/refractr
[git]
# The "ssh_identity_file" is your private SSH key that you will use to push updates
# from the original repository.
# This field is REQUIRED if you are using SSH to push, otherwise OPTIONAL
# The "ssh_identity_file" is your private SSH key that you will use to push updates from the original
# repository and, if your "from" field is an SSH remote, for cloning the original repository
# If you are running from Docker, you will need to copy your private key to your container or image
# This field is REQUIRED
#ssh_identity_file = "/path/to/.ssh/id_rsa"
[schedule]
# The "enabled" field turns on the schedule feature of refractr.
# The "enabled" field turns on the schedule feature of refractr
# This field is REQUIRED.
#enabled = false
# The "interval" field is the amount of seconds refractor will wait before
# pulling updates from the original repository if the schedule feature is enabled.
# This field is REQUIRED if "enabled" is set to true, otherwise OPTIONAL
# To avoid overwhelming servers, this is set to only accept values of >=60
# The "interval" field is the amount of seconds refractor will wait before pulling updates from the
# original repository if the schedule feature is enabled
# To avoid creating a DoS attack, this is set to only accept values of >=60
# This field is REQUIRED if "enabled" is set to true, UNUSED if false
#interval = 300

View file

@ -15,10 +15,12 @@ use crate::refractr::Refractr;
use clap::Parser;
use std::path::PathBuf;
use std::process;
#[cfg(target_family = "unix")]
use users;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
#[cfg(target_family = "windows")]
use username;
#[cfg(target_family = "unix")]
use users;
#[derive(Parser)]
#[command(name = "refractr")]
@ -32,11 +34,26 @@ struct Args {
#[arg(short, long, help = "Specify the level of verbosity", action = clap::ArgAction::Count)]
verbose: u8,
#[arg(short = 'e', long, help = "Output a full, commented config file and exit")]
#[arg(
short = 'e',
long,
help = "Output a full, commented config file and exit"
)]
create: bool,
#[arg(short = 's', long, help = "Exit on push errors instead of ignoring")]
#[arg(
short = 's',
long,
help = "Exit on push and unknown host errors instead of ignoring them"
)]
strict: bool,
#[arg(
short = 'p',
long,
help = "Do not clean the working directories specified in config(s)"
)]
persist: bool,
}
fn get_config_default() -> &'static str {
@ -44,7 +61,8 @@ fn get_config_default() -> &'static str {
return "C:\\ProgramData\\refractr\\config.toml";
#[cfg(target_os = "linux")]
return "/etc/refractr/config.toml";
#[cfg(any(target_os = "freebsd",
#[cfg(any(
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "macos"
@ -61,12 +79,14 @@ fn main() -> Result<(), String> {
"true" => true,
_ => false,
},
None => false
None => false,
},
persist: args.persist,
pid: process::id(),
strict: args.strict,
unix: cfg!(unix),
verbose: args.verbose
verbose: args.verbose,
run: Arc::new(AtomicBool::new(true))
};
// warn to avoid root/admin
@ -86,22 +106,37 @@ fn main() -> Result<(), String> {
Err(_) => common::warning(format!("failed to get process username")),
}
common::verbose(refractr.verbose, 1, format!("refractr started with level {} verbosity enabled", refractr.verbose.to_string()));
common::verbose(
refractr.verbose,
1,
format!(
"refractr started with level {} verbosity enabled",
refractr.verbose.to_string()
),
);
common::verbose(refractr.verbose, 3, format!("Process ID: {}", refractr.pid));
common::verbose(refractr.verbose, 3, format!("Running in Docker: {}", refractr.docker));
common::verbose(refractr.verbose, 3, format!("System is UNIX(-like): {}", refractr.unix));
common::verbose(
refractr.verbose,
3,
format!("Running in Docker: {}", refractr.docker),
);
common::verbose(
refractr.verbose,
3,
format!("System is UNIX(-like): {}", refractr.unix),
);
common::verbose(refractr.verbose, 2, format!("Checking for create flag"));
if args.create {
common::verbose(refractr.verbose, 3, format!("Printing sample config"));
let example = include_str!("example/config.toml");
println!("{}", example);
return Ok(())
return Ok(());
}
let mut cfgs = vec![];
match config::read_config(args.config, &refractr) {
Ok(c) => cfgs = c,
Err(e) => common::error(format!("{}", e), common::ExitCode::ConfigError)
Err(e) => common::error_quit(format!("{}", e), common::ExitCode::ConfigError),
};
if refractr.verbose >= 2 {
@ -111,10 +146,14 @@ fn main() -> Result<(), String> {
}
}
common::verbose(refractr.verbose, 1, format!("Config file(s) read successfully"));
common::verbose(
refractr.verbose,
1,
format!("Config file(s) read successfully"),
);
match refractr.run(cfgs) {
Ok(_) => (),
Err(e) => common::error(format!("{}", e.msg), e.code)
Err(e) => common::error_quit(format!("{}", e.msg), e.code),
};
Ok(())

View file

@ -6,62 +6,95 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
use crate::common::{self, ReturnData, ExitCode};
use crate::config::{Config, ConfigFile};
use crate::common::{self, ExitCode, ReturnData};
use crate::config::{self, Config, ConfigFile};
use git2::string_array::StringArray;
use git2::{FetchOptions, CertificateCheckStatus, Cred, PushOptions, RemoteCallbacks, Repository};
use git2::{Error, ErrorCode};
use git2::build::RepoBuilder;
use git2::string_array::StringArray;
use git2::{CertificateCheckStatus, Cred, FetchOptions, PushOptions, RemoteCallbacks, Repository};
use git2::{Error, ErrorCode};
use hex;
use sha2::{Sha256, Digest};
use std::env;
use std::fs;
use sha2::{Digest, Sha256};
use std::fs::{self, remove_dir_all};
use std::path::{Path, PathBuf, MAIN_SEPARATOR_STR};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::thread;
use std::time;
pub struct Refractr {
pub docker: bool,
pub persist: bool,
pub pid: u32,
pub strict: bool,
pub unix: bool,
pub verbose: u8
pub verbose: u8,
pub run: Arc<AtomicBool>
}
struct OpenedRepository {
repo: Repository,
path: String,
remotes: Vec<String>,
cfg: Config,
ssh: bool,
}
impl Refractr {
fn set_up_work_dir(&self, work_dir: PathBuf) -> Result<String, String> {
fn set_up_work_dir(&self, work_dir: PathBuf) -> Result<(), String> {
if let Err(e) = fs::create_dir_all(&work_dir) {
return Err(format!("could not create working directory: {}: {}", work_dir.to_string_lossy().to_string(), e))
return Err(format!(
"could not create working directory: {}: {}",
work_dir.to_string_lossy().to_string(),
e
));
}
Ok(work_dir.to_string_lossy().to_string())
Ok(())
}
fn get_refs(&self, branches: &Vec<String>, tags: StringArray) -> Vec<String> {
fn set_up_ssh(&self, key_path: String, strict: bool) -> Result<RemoteCallbacks, String> {
let mut cb = RemoteCallbacks::new();
cb.credentials(move |_, _, _| Cred::ssh_key("git", None, Path::new(&key_path), None));
cb.certificate_check(move |cert, url| {
let sha256 = hex::encode(cert.as_hostkey().unwrap().hash_sha256().unwrap().to_vec());
if strict {
common::error_quit(
format!(
"unknown host {} with sha256 host key {}, exiting",
url, sha256
),
ExitCode::RemoteError,
);
} else {
common::warning(format!(
"unknown host {} with sha256 host key {}, implicitly trusting",
url, sha256
));
common::warning(format!(
"to suppress this warning in the future, add this host to your known_hosts file"
));
}
Ok(CertificateCheckStatus::CertificateOk)
});
Ok(cb)
}
fn get_refs(&self, branches: &Vec<String>, tags: Option<StringArray>) -> Vec<String> {
let mut refs_branches = Vec::new();
for branch in branches {
refs_branches.push(format!("refs/heads/{}", branch));
}
for tag in &tags {
refs_branches.push(format!("refs/tags/{}", tag.unwrap()))
if let Some(tags) = tags {
for tag in &tags {
refs_branches.push(format!("refs/tags/{}", tag.unwrap()))
}
}
refs_branches
}
fn fast_forward(&self, repo_dir: &str, branches: &Vec<String>) -> Result<(), Error> {
let repo = Repository::open(repo_dir)?;
common::verbose(self.verbose, 2, format!("Pulling origin"));
repo.find_remote("origin")?.fetch(&branches, None, None)?;
fn fast_forward(&self, repo: &Repository, branches: &Vec<String>) -> Result<(), Error> {
common::verbose(self.verbose, 2, format!("Fast forwarding repo"));
let mut fo = FetchOptions::new();
fo.download_tags(git2::AutotagOption::All);
for branch in branches {
let refname = format!("refs/remotes/origin/{}", branch);
@ -72,47 +105,51 @@ impl Refractr {
Ok(())
}
fn fetch(&self, repo: &Repository, branches: &Vec<String>, ssh: bool, ssh_key: &String) -> Result<(), Error> {
let mut cb = RemoteCallbacks::new();
fn fetch(
&self,
repo: &Repository,
branches: &Vec<String>,
ssh: bool,
ssh_key: &String,
strict: bool,
) -> Result<(), Error> {
common::verbose(self.verbose, 2, format!("Fetching repo"));
let mut fo = FetchOptions::new();
if ssh {
let key_string: String = ssh_key.clone();
cb.credentials(move |_,_,_| Cred::ssh_key(
"git",
None,
Path::new(&key_string),
None));
cb.certificate_check(|cert, url| {
let mut sha256 = String::new();
for i in cert.as_hostkey().unwrap().hash_sha256().unwrap().to_vec() {
sha256.push_str(&hex::encode(i.to_string()));
}
common::warning(
format!("implicitly trusting unknown host {} with sha256 host key {}",
url,
hex::encode(cert.as_hostkey().unwrap().hash_sha256().unwrap().to_vec())));
common::warning(
format!("to ignore this error in the future, add this host to your known_hosts file"));
Ok(CertificateCheckStatus::CertificateOk)
});
match self.set_up_ssh(ssh_key.clone(), strict.clone()) {
Ok(cb) => {
fo.remote_callbacks(cb);
()
},
Err(e) => common::error_quit(
format!("error setting up ssh: {}", e),
ExitCode::ConfigError,
),
};
}
fo.download_tags(git2::AutotagOption::All);
fo.remote_callbacks(cb);
repo.find_remote("origin")?.fetch(&branches, Some(&mut fo), None)?;
repo
.find_remote("origin")?
.fetch(&branches, Some(&mut fo), None)?;
Ok(())
}
fn set_up_refs(&self, repo: &Repository, branches: &Vec<String>) -> Result<(), Error> {
for branch in branches {
let mut fetch_head = repo.find_reference(format!("refs/remotes/origin/{}", branch).as_str())?;
let mut fetch_head =
repo.find_reference(format!("refs/remotes/origin/{}", branch).as_str())?;
fetch_head.rename(format!("refs/heads/{}", branch).as_str(), true, "")?;
}
Ok(())
}
fn make_remotes<'a> (&self, repo: &'a Repository, cfg: &ConfigFile) -> Result<Vec<String>, String> {
fn make_remotes<'a>(
&self,
repo: &'a Repository,
cfg: &ConfigFile,
) -> Result<Vec<String>, String> {
// create remotes for each "to" repo
let mut remote_list = Vec::new();
for to in &cfg.config.to {
@ -122,7 +159,8 @@ impl Refractr {
common::verbose(
self.verbose,
2,
format!("Attempting to create remote {} for url {}", remote_id, to));
format!("Attempting to create remote {} for url {}", remote_id, to),
);
match repo.remote(remote_id.as_str(), to) {
Ok(_) => remote_list.push(remote_id),
Err(e) => {
@ -132,61 +170,70 @@ impl Refractr {
} else {
return Err(format!("failed to create remote: {}", e));
}
}
},
}
}
Ok(remote_list)
}
fn push_remotes(&self, cfg: &Config, repo: &Repository, remote_list: &Vec<String>) -> Result<(), String> {
fn push_remotes(
&self,
cfg: &Config,
repo: &Repository,
remote_list: &Vec<String>,
) -> Result<(), String> {
for id in remote_list {
let mut remote = repo.find_remote(&id).unwrap();
common::verbose(
self.verbose,
1,
format!("Pushing to remote: {}", remote.url().unwrap()));
let mut callbacks = RemoteCallbacks::new();
callbacks.credentials(|_,_,_| Cred::ssh_key(
"git",
None,
&Path::new(&cfg.git.ssh_identity_file),
None));
callbacks.certificate_check(|cert, url| {
let mut sha256 = String::new();
for i in cert.as_hostkey().unwrap().hash_sha256().unwrap().to_vec() {
sha256.push_str(&hex::encode(i.to_string()));
}
common::warning(
format!("implicitly trusting unknown host {} with sha256 host key {}",
url,
hex::encode(cert.as_hostkey().unwrap().hash_sha256().unwrap().to_vec())));
common::warning(
format!("to ignore this error in the future, add this host to your known_hosts file"));
Ok(CertificateCheckStatus::CertificateOk)
});
let mut push_options = PushOptions::new();
push_options.remote_callbacks(callbacks);
self.verbose,
1,
format!("Pushing to remote: {}", remote.url().unwrap()),
);
let mut po = PushOptions::new();
match self.set_up_ssh(cfg.git.ssh_identity_file.clone(), self.strict.clone()) {
Ok(cb) => {
po.remote_callbacks(cb);
()
},
Err(e) => common::error_quit(
format!("error setting up ssh: {}", e),
ExitCode::ConfigError,
),
};
let mut refs = Vec::new();
let mut refs_str = String::new();
let tags = repo.tag_names(None).unwrap();
let strings = self.get_refs(&cfg.branches, tags);
let strings = self.get_refs(
&cfg.branches,
match cfg.push_tags {
true => Some(repo.tag_names(None).unwrap()),
false => None,
},
);
for branch in &strings {
refs.push(branch.as_str());
refs_str.push_str(format!("{} ", branch).as_str());
}
common::verbose(self.verbose, 4, format!("ref list: {}", refs_str));
match remote.push::<&str>(&refs, Some(&mut push_options)) {
match remote.push::<&str>(&refs, Some(&mut po)) {
Ok(_) => (),
Err(e) => {
if self.strict {
return Err(format!("failed to push to remote: {}: {}", remote.url().unwrap(), e))
return Err(format!(
"failed to push to remote: {}: {}",
remote.url().unwrap(),
e
));
} else {
common::warning(format!("failed to push to remote: {}: {}", remote.url().unwrap(), e))
common::warning(format!(
"failed to push to remote: {}: {}",
remote.url().unwrap(),
e
))
}
}
},
}
}
@ -199,50 +246,75 @@ impl Refractr {
let r = running.clone();
let count = repos.len();
for i in 0..repos.len() {
current_ints.push(u64::from(repos[i].cfg.schedule.interval.unwrap().unsigned_abs()));
};
current_ints.push(i64::from(
repos[i].cfg.schedule.interval.unwrap().unsigned_abs(),
));
}
let original_ints = current_ints.clone();
ctrlc::set_handler(move || {
r.store(false, Ordering::SeqCst);
}).expect("Failed to set ^C handler");
})
.expect("Failed to set ^C handler");
common::verbose(self.verbose, 1, format!("Starting scheduled loop"));
let min = *current_ints.iter().min().unwrap();
let mut do_break = false;
while !do_break {
do_break = true;
let sleep_int = time::Duration::from_secs(min);
let min = *current_ints.iter().min().unwrap();
let sleep_int = time::Duration::from_secs(min as u64);
let now = time::Instant::now();
common::verbose(
self.verbose,
2,
format!("Sleeping for {} seconds", sleep_int.as_secs()));
format!("Sleeping for {} seconds", sleep_int.as_secs()),
);
while running.load(Ordering::SeqCst) {
thread::sleep(time::Duration::from_secs(1));
thread::sleep(time::Duration::from_millis(200));
if now.elapsed().as_secs() >= sleep_int.as_secs() {
common::verbose(self.verbose, 3, format!("Thread has awoken!"));
for i in 0..count {
current_ints[i] -= now.elapsed().as_secs();
if i <= 0 {
current_ints[i] = original_ints[i].clone();
current_ints[i] -= now.elapsed().as_secs() as i64;
common::verbose(
self.verbose,
4,
format!("checking repo: {}", repos[i].cfg.from),
);
if current_ints[i] <= 0 {
common::verbose(self.verbose, 4, format!("repo is ready for push"));
common::verbose(
self.verbose,
2,
format!("Interval for {} has arrived, pulling", repos[i].cfg.from));
let _ = self.fast_forward(&repos[i].path, &repos[i].cfg.branches);
if let Err(e) = self.push_remotes(
&repos[i].cfg,
format!("Interval for {} has arrived, pulling", repos[i].cfg.from),
);
if let Err(e) = self.fetch(
&repos[i].repo,
&repos[i].remotes) {
common::error(e, ExitCode::PushError)
&repos[i].cfg.branches,
repos[i].ssh,
&repos[i].cfg.git.ssh_identity_file,
self.strict.clone(),
) {
common::error_quit(
format!("failed to fetch repo {}: {}", repos[i].cfg.from, e),
ExitCode::FetchError,
);
}
let _ = self.fast_forward(&repos[i].repo, &repos[i].cfg.branches);
if let Err(e) = self.push_remotes(&repos[i].cfg, &repos[i].repo, &repos[i].remotes) {
common::error_quit(e, ExitCode::PushError)
};
current_ints[i] = original_ints[i].clone();
}
common::verbose(
self.verbose,
4,
format!("repo remaining time is now {}", current_ints[i]),
);
}
do_break = false;
break
break;
}
}
}
@ -251,144 +323,220 @@ impl Refractr {
Ok(())
}
fn clean(&self, dirs: Vec<String>) -> Result<(), ReturnData> {
for dir in dirs {
common::verbose(self.verbose, 1, format!("removing working dir {}", dir));
if let Err(e) = remove_dir_all(&dir) {
common::warning(format!("could not clean up working dir: {}: {}", dir, e));
}
}
Ok(())
}
pub fn run(&self, cfgs: Vec<ConfigFile>) -> Result<(), ReturnData> {
let r = self.run.clone();
ctrlc::set_handler(move ||
r.store(true, Ordering::SeqCst)
).expect("failed to set ^c handler");
common::verbose(self.verbose, 3, format!("Starting main refractr loop"));
let mut loop_repos = Vec::new();
let mut work_dirs = Vec::new();
for cfg in cfgs {
// set up the working directory
common::verbose(self.verbose, 3, format!("Loading config: {}", cfg.path));
let work_dir = self.set_up_work_dir(match &cfg.config.work_dir {
None => {
if cfg!(windows) {
PathBuf::from(format!("{}\\refractr", env::var("TEMP").unwrap()))
} else {
PathBuf::from("/tmp/refractr")
}
},
Some(path) => PathBuf::from(path)
});
let path_str = match work_dir {
Ok(p) => p,
Err(e) => return Err(ReturnData {
let work_dir = config::get_work_dir(&cfg.config.work_dir);
if let Err(e) = self.set_up_work_dir(PathBuf::from(&work_dir)) {
return Err(ReturnData {
msg: e,
code: ExitCode::FilesystemError,
msg: e
})
};
});
}
common::verbose(
self.verbose,
2,
format!("Created working directory: {}", &path_str));
format!("Created working directory: {}", work_dir),
);
let repo_name = match &cfg.config.from.split("/").last() {
Some(split) => split.to_string(),
None => return Err(ReturnData {
code: ExitCode::ParseError,
msg: format!("failed to parse repository name")
})
None => {
return Err(ReturnData {
code: ExitCode::ParseError,
msg: format!("failed to parse repository name"),
})
},
};
let ssh = cfg.config.from.starts_with("ssh://");
let mut builder = RepoBuilder::new();
let mut cb = RemoteCallbacks::new();
let mut fo = FetchOptions::new();
// make initial clone
if ssh {
let key_string = cfg.config.git.ssh_identity_file.clone();
cb.credentials(move |_,_,_| Cred::ssh_key(
"git",
None,
Path::new(&key_string),
None));
cb.certificate_check(|cert, url| {
let mut sha256 = String::new();
for i in cert.as_hostkey().unwrap().hash_sha256().unwrap().to_vec() {
sha256.push_str(&hex::encode(i.to_string()));
}
common::warning(
format!("implicitly trusting unknown host {} with sha256 host key {}",
url,
hex::encode(cert.as_hostkey().unwrap().hash_sha256().unwrap().to_vec())));
common::warning(
format!("to ignore this error in the future, add this host to your known_hosts file"));
Ok(CertificateCheckStatus::CertificateOk)
});
match self.set_up_ssh(
cfg.config.git.ssh_identity_file.clone(),
self.strict.clone(),
) {
Ok(cb) => {
fo.remote_callbacks(cb);
()
},
Err(e) => common::error_quit(
format!("error setting up ssh: {}", e),
ExitCode::ConfigError,
),
};
}
fo.download_tags(git2::AutotagOption::All);
fo.remote_callbacks(cb);
builder.fetch_options(fo);
let repo_dir = format!("{}{}{}", &path_str, MAIN_SEPARATOR_STR, repo_name);
let repo_dir = format!("{}{}{}", &work_dir, MAIN_SEPARATOR_STR, repo_name);
common::verbose(
self.verbose,
1,
format!("Cloning repository {} to {}", &cfg.config.from, &repo_dir));
format!("Cloning repository {} to {}", &cfg.config.from, &repo_dir),
);
let repo = match builder.clone(&cfg.config.from, Path::new(&repo_dir)) {
Ok(repo) => repo,
Err(e) => {
if e.code() != ErrorCode::Exists {
common::error(format!("failed to clone repo to {}: {}", repo_dir, e), ExitCode::FilesystemError);
common::error_quit(
format!("failed to clone repo to {}: {}", repo_dir, e),
ExitCode::FilesystemError,
);
}
common::warning(format!("found existing repo at {}, attempting to use", repo_dir));
match self.fast_forward(&repo_dir, &cfg.config.branches) {
Ok(_) => if let Ok(repo) = Repository::open(Path::new(&repo_dir)) {
repo
} else {
common::warning(format!(
"found existing repo at {}, attempting to use",
repo_dir
));
match Repository::open(Path::new(&repo_dir)) {
Ok(r) => {
if let Ok(rem) = r.find_remote("origin") {
match rem.url() {
Some(url) => {
if url != &cfg.config.from {
return Err(ReturnData {
code: ExitCode::RepositoryError,
msg: format!(
"existing repo's origin does not match 'from' value in config: {}",
url
),
});
}
},
None => {
return Err(ReturnData {
code: ExitCode::RepositoryError,
msg: format!("could not obtain existing repo's origin: {}", repo_dir),
})
},
}
}
// fetch updates for the repo
if let Err(e) = self.fetch(
&r,
&cfg.config.branches,
ssh,
&cfg.config.git.ssh_identity_file,
self.strict.clone(),
) {
common::error_quit(
format!("failed to fetch repo {}: {}", cfg.config.from, e),
ExitCode::FetchError,
);
}
// fast forward
match self.fast_forward(&r, &cfg.config.branches) {
Ok(_) => r,
Err(e) => {
return Err(ReturnData {
code: ExitCode::RepositoryError,
msg: format!("failed to fast forward existing repo: {}", e),
});
},
}
},
Err(e) => {
return Err(ReturnData {
code: ExitCode::RepositoryError,
msg: format!("failed to obtain existing repo")
msg: format!("failed to obtain existing repo: {}", e),
})
},
Err(e) => return Err(ReturnData {
code: ExitCode::RepositoryError,
msg: format!("failed to obtain existing repo: {}", e)
})
}
}
},
};
self.set_up_refs(&repo, &cfg.config.branches).unwrap();
if let Err(e) = self.fetch(&repo,
&cfg.config.branches,
ssh,
&cfg.config.git.ssh_identity_file) {
common::error(format!("failed to fetch repo {}: {}", cfg.config.from, e), ExitCode::FetchError);
if let Err(e) = self.set_up_refs(&repo, &cfg.config.branches) {
common::error_quit(
format!("failed to set up refs: {}", e),
ExitCode::RepositoryError,
);
}
let remotes = match self.make_remotes(&repo, &cfg) {
Ok(v) => v,
Err(e) => return Err(ReturnData {
code: ExitCode::RemoteError,
msg: e
})
Err(e) => {
return Err(ReturnData {
code: ExitCode::RemoteError,
msg: e,
})
},
};
if let Err(e) = self.push_remotes(&cfg.config, &repo, &remotes) {
common::error(e, ExitCode::PushError);
common::error_quit(e, ExitCode::PushError);
}
if cfg.config.schedule.enabled {
loop_repos.push(OpenedRepository {
repo,
path: repo_dir,
remotes,
cfg: cfg.config
cfg: cfg.config,
ssh,
});
if !work_dirs.contains(&work_dir) {
work_dirs.push(work_dir);
}
} else {
if let Err(e) = self.clean(vec![work_dir]) {
return Err(e);
}
}
if self.run.load(Ordering::SeqCst) {
common::error_quit(format!("exiting"), ExitCode::HaltError);
}
}
// end for
let mut result = Ok(());
if loop_repos.len() >= 1 {
common::verbose(
self.verbose,
2,
format!("{} configs have schedules enabled, setting up looper", loop_repos.len()));
return self.looper(loop_repos);
format!(
"{} configs have schedules enabled, setting up looper",
loop_repos.len()
),
);
result = self.looper(loop_repos);
} else {
common::verbose(
self.verbose,
2,
format!("No scheduled configs found, exiting refractr"));
format!("No scheduled configs found, exiting refractr"),
);
}
Ok(())
if !self.persist {
match result {
Ok(()) => return self.clean(work_dirs),
Err(_) => result = self.clean(work_dirs),
}
}
result
}
}