Compare commits

...

14 commits

13 changed files with 611 additions and 310 deletions

266
Cargo.lock generated
View file

@ -79,9 +79,9 @@ dependencies = [
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.16" version = "1.2.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" checksum = "32db95edf998450acc7881c932f94cd9b05c87b4b2599e8bab064753da4acfd1"
dependencies = [ dependencies = [
"jobserver", "jobserver",
"libc", "libc",
@ -102,9 +102,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.32" version = "4.5.38"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6088f3ae8c3608d19260cd7445411865a485688711b78b5be70d78cd96136f83" checksum = "ed93b9805f8ba930df42c2590f05453d5ec36cbb85d018868a5b24d31f6ac000"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive", "clap_derive",
@ -112,9 +112,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_builder" name = "clap_builder"
version = "4.5.32" version = "4.5.38"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22a7ef7f676155edfb82daa97f99441f3ebf4a58d5e32f295a56259f1b6facc8" checksum = "379026ff283facf611b0ea629334361c4211d1b12ee01024eec1591133b04120"
dependencies = [ dependencies = [
"anstream", "anstream",
"anstyle", "anstyle",
@ -176,9 +176,9 @@ dependencies = [
[[package]] [[package]]
name = "ctrlc" name = "ctrlc"
version = "3.4.5" version = "3.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90eeab0aa92f3f9b4e87f258c72b139c207d251f9cbc1080a0086b86a8870dd3" checksum = "46f93780a459b7d656ef7f071fe699c4d3d2cb201c4b24d085b6ddc505276e73"
dependencies = [ dependencies = [
"nix", "nix",
"windows-sys", "windows-sys",
@ -230,6 +230,18 @@ dependencies = [
"version_check", "version_check",
] ]
[[package]]
name = "getrandom"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasi",
]
[[package]] [[package]]
name = "git2" name = "git2"
version = "0.20.1" version = "0.20.1"
@ -246,9 +258,9 @@ dependencies = [
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.15.2" version = "0.15.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3"
[[package]] [[package]]
name = "heck" name = "heck"
@ -264,21 +276,22 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]] [[package]]
name = "icu_collections" name = "icu_collections"
version = "1.5.0" version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47"
dependencies = [ dependencies = [
"displaydoc", "displaydoc",
"potential_utf",
"yoke", "yoke",
"zerofrom", "zerofrom",
"zerovec", "zerovec",
] ]
[[package]] [[package]]
name = "icu_locid" name = "icu_locale_core"
version = "1.5.0" version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a"
dependencies = [ dependencies = [
"displaydoc", "displaydoc",
"litemap", "litemap",
@ -287,31 +300,11 @@ dependencies = [
"zerovec", "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]] [[package]]
name = "icu_normalizer" name = "icu_normalizer"
version = "1.5.0" version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979"
dependencies = [ dependencies = [
"displaydoc", "displaydoc",
"icu_collections", "icu_collections",
@ -319,67 +312,54 @@ dependencies = [
"icu_properties", "icu_properties",
"icu_provider", "icu_provider",
"smallvec", "smallvec",
"utf16_iter",
"utf8_iter",
"write16",
"zerovec", "zerovec",
] ]
[[package]] [[package]]
name = "icu_normalizer_data" name = "icu_normalizer_data"
version = "1.5.0" version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3"
[[package]] [[package]]
name = "icu_properties" name = "icu_properties"
version = "1.5.1" version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" checksum = "2549ca8c7241c82f59c80ba2a6f415d931c5b58d24fb8412caa1a1f02c49139a"
dependencies = [ dependencies = [
"displaydoc", "displaydoc",
"icu_collections", "icu_collections",
"icu_locid_transform", "icu_locale_core",
"icu_properties_data", "icu_properties_data",
"icu_provider", "icu_provider",
"tinystr", "potential_utf",
"zerotrie",
"zerovec", "zerovec",
] ]
[[package]] [[package]]
name = "icu_properties_data" name = "icu_properties_data"
version = "1.5.0" version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" checksum = "8197e866e47b68f8f7d95249e172903bec06004b18b2937f1095d40a0c57de04"
[[package]] [[package]]
name = "icu_provider" name = "icu_provider"
version = "1.5.0" version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af"
dependencies = [ dependencies = [
"displaydoc", "displaydoc",
"icu_locid", "icu_locale_core",
"icu_provider_macros",
"stable_deref_trait", "stable_deref_trait",
"tinystr", "tinystr",
"writeable", "writeable",
"yoke", "yoke",
"zerofrom", "zerofrom",
"zerotrie",
"zerovec", "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]] [[package]]
name = "idna" name = "idna"
version = "1.0.3" version = "1.0.3"
@ -393,9 +373,9 @@ dependencies = [
[[package]] [[package]]
name = "idna_adapter" name = "idna_adapter"
version = "1.2.0" version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
dependencies = [ dependencies = [
"icu_normalizer", "icu_normalizer",
"icu_properties", "icu_properties",
@ -403,9 +383,9 @@ dependencies = [
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.8.0" version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown", "hashbrown",
@ -419,18 +399,19 @@ checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]] [[package]]
name = "jobserver" name = "jobserver"
version = "0.1.32" version = "0.1.33"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a"
dependencies = [ dependencies = [
"getrandom",
"libc", "libc",
] ]
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.171" version = "0.2.172"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
[[package]] [[package]]
name = "libgit2-sys" name = "libgit2-sys"
@ -473,15 +454,15 @@ dependencies = [
[[package]] [[package]]
name = "litemap" name = "litemap"
version = "0.7.5" version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956"
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.26" version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]] [[package]]
name = "memchr" name = "memchr"
@ -491,9 +472,9 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]] [[package]]
name = "nix" name = "nix"
version = "0.29.0" version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"cfg-if", "cfg-if",
@ -503,9 +484,9 @@ dependencies = [
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.21.1" version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]] [[package]]
name = "openssl-probe" name = "openssl-probe"
@ -515,9 +496,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
[[package]] [[package]]
name = "openssl-sys" name = "openssl-sys"
version = "0.9.106" version = "0.9.108"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8bb61ea9811cc39e3c2069f40b8b8e2e70d8569b361f879786cc7ed48b777cdd" checksum = "e145e1651e858e820e4860f7b9c5e169bc1d8ce1c86043be79fa7b7634821847"
dependencies = [ dependencies = [
"cc", "cc",
"libc", "libc",
@ -538,10 +519,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]] [[package]]
name = "proc-macro2" name = "potential_utf"
version = "1.0.94" version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" 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 = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@ -570,9 +560,15 @@ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]]
name = "r-efi"
version = "5.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
[[package]] [[package]]
name = "refractr" name = "refractr"
version = "0.5.1" version = "0.6.2"
dependencies = [ dependencies = [
"clap", "clap",
"colored", "colored",
@ -619,9 +615,9 @@ dependencies = [
[[package]] [[package]]
name = "sha2" name = "sha2"
version = "0.10.8" version = "0.10.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"cpufeatures", "cpufeatures",
@ -636,9 +632,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]] [[package]]
name = "smallvec" name = "smallvec"
version = "1.14.0" version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
[[package]] [[package]]
name = "stable_deref_trait" name = "stable_deref_trait"
@ -654,9 +650,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.100" version = "2.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -665,9 +661,9 @@ dependencies = [
[[package]] [[package]]
name = "synstructure" name = "synstructure"
version = "0.13.1" version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -676,9 +672,9 @@ dependencies = [
[[package]] [[package]]
name = "tinystr" name = "tinystr"
version = "0.7.6" version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b"
dependencies = [ dependencies = [
"displaydoc", "displaydoc",
"zerovec", "zerovec",
@ -686,9 +682,9 @@ dependencies = [
[[package]] [[package]]
name = "toml" name = "toml"
version = "0.8.20" version = "0.8.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae"
dependencies = [ dependencies = [
"serde", "serde",
"serde_spanned", "serde_spanned",
@ -698,26 +694,33 @@ dependencies = [
[[package]] [[package]]
name = "toml_datetime" name = "toml_datetime"
version = "0.6.8" version = "0.6.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3"
dependencies = [ dependencies = [
"serde", "serde",
] ]
[[package]] [[package]]
name = "toml_edit" name = "toml_edit"
version = "0.22.24" version = "0.22.26"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"serde", "serde",
"serde_spanned", "serde_spanned",
"toml_datetime", "toml_datetime",
"toml_write",
"winnow", "winnow",
] ]
[[package]]
name = "toml_write"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076"
[[package]] [[package]]
name = "typenum" name = "typenum"
version = "1.18.0" version = "1.18.0"
@ -761,12 +764,6 @@ dependencies = [
"log", "log",
] ]
[[package]]
name = "utf16_iter"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246"
[[package]] [[package]]
name = "utf8_iter" name = "utf8_iter"
version = "1.0.4" version = "1.0.4"
@ -791,6 +788,15 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 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]] [[package]]
name = "winapi" name = "winapi"
version = "0.2.8" version = "0.2.8"
@ -878,30 +884,33 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "0.7.4" version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36" checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
[[package]] [[package]]
name = "write16" name = "wit-bindgen-rt"
version = "1.0.0" version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
dependencies = [
"bitflags",
]
[[package]] [[package]]
name = "writeable" name = "writeable"
version = "0.5.5" version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
[[package]] [[package]]
name = "yoke" name = "yoke"
version = "0.7.5" version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc"
dependencies = [ dependencies = [
"serde", "serde",
"stable_deref_trait", "stable_deref_trait",
@ -911,9 +920,9 @@ dependencies = [
[[package]] [[package]]
name = "yoke-derive" name = "yoke-derive"
version = "0.7.5" version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -943,10 +952,21 @@ dependencies = [
] ]
[[package]] [[package]]
name = "zerovec" name = "zerotrie"
version = "0.10.4" version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" 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 = [ dependencies = [
"yoke", "yoke",
"zerofrom", "zerofrom",
@ -955,9 +975,9 @@ dependencies = [
[[package]] [[package]]
name = "zerovec-derive" name = "zerovec-derive"
version = "0.10.3" version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",

View file

@ -1,7 +1,7 @@
[package] [package]
name = "refractr" name = "refractr"
license = "MPL-2.0" license = "MPL-2.0"
version = "0.5.1" version = "0.6.2"
edition = "2021" edition = "2021"
[dependencies] [dependencies]

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"]

24
build
View file

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

@ -16,6 +16,7 @@ pub enum ExitCode {
PushError = 6, PushError = 6,
FetchError = 7, FetchError = 7,
ConfigError = 8, ConfigError = 8,
HaltError = 130,
} }
pub struct ReturnData { pub struct ReturnData {
@ -23,11 +24,15 @@ pub struct ReturnData {
pub msg: String, pub msg: String,
} }
pub fn error(msg: String, code: ExitCode) { pub fn error_quit(msg: String, code: ExitCode) {
eprintln!("{} {}", "error:".red().bold(), msg); eprintln!("{} {}", "error:".red().bold(), msg);
quit::with_code(code as u8) quit::with_code(code as u8)
} }
pub fn error(msg: String) {
eprintln!("{} {}", "error:".red().bold(), msg);
}
pub fn warning(msg: String) { pub fn warning(msg: String) {
eprintln!("{} {}", "warning:".yellow().bold(), msg) eprintln!("{} {}", "warning:".yellow().bold(), msg)
} }

View file

@ -107,6 +107,7 @@ pub struct Config {
pub to: Vec<String>, pub to: Vec<String>,
pub branches: Vec<String>, pub branches: Vec<String>,
pub work_dir: Option<String>, pub work_dir: Option<String>,
pub push_tags: bool,
pub git: Git, pub git: Git,
pub schedule: Schedule, pub schedule: Schedule,
} }
@ -122,6 +123,19 @@ pub struct Schedule {
pub interval: Option<i32>, 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> { pub fn read_config(paths: Vec<PathBuf>, refractr: &Refractr) -> Result<Vec<ConfigFile>, String> {
let mut config_files: Vec<ConfigFile> = vec![]; let mut config_files: Vec<ConfigFile> = vec![];
for path in paths { for path in paths {
@ -232,18 +246,12 @@ fn verify_config(config: &Config) -> Result<(), String> {
} }
} }
match &config.work_dir { if let None = &config.work_dir {
Some(path) => format!("{}", path), if cfg!(windows) {
None => { if let Err(e) = env::var("TEMP") {
if cfg!(windows) { return Err(format!("cannot determine the default temp dir: {}", e));
match env::var("TEMP") {
Ok(val) => val,
Err(_) => return Err(format!("cannot determine the default temp dir")),
}
} else {
format!("/tmp/refractr")
} }
}, }
}; };
if !&config.from.starts_with("ssh://") if !&config.from.starts_with("ssh://")

View file

@ -15,6 +15,8 @@ use crate::refractr::Refractr;
use clap::Parser; use clap::Parser;
use std::path::PathBuf; use std::path::PathBuf;
use std::process; use std::process;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
#[cfg(target_family = "windows")] #[cfg(target_family = "windows")]
use username; use username;
#[cfg(target_family = "unix")] #[cfg(target_family = "unix")]
@ -39,8 +41,19 @@ struct Args {
)] )]
create: bool, 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, 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 { fn get_config_default() -> &'static str {
@ -68,10 +81,12 @@ fn main() -> Result<(), String> {
}, },
None => false, None => false,
}, },
persist: args.persist,
pid: process::id(), pid: process::id(),
strict: args.strict, strict: args.strict,
unix: cfg!(unix), unix: cfg!(unix),
verbose: args.verbose, verbose: args.verbose,
run: Arc::new(AtomicBool::new(true))
}; };
// warn to avoid root/admin // warn to avoid root/admin
@ -121,7 +136,7 @@ fn main() -> Result<(), String> {
let mut cfgs = vec![]; let mut cfgs = vec![];
match config::read_config(args.config, &refractr) { match config::read_config(args.config, &refractr) {
Ok(c) => cfgs = c, 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 { if refractr.verbose >= 2 {
@ -138,7 +153,7 @@ fn main() -> Result<(), String> {
); );
match refractr.run(cfgs) { match refractr.run(cfgs) {
Ok(_) => (), Ok(_) => (),
Err(e) => common::error(format!("{}", e.msg), e.code), Err(e) => common::error_quit(format!("{}", e.msg), e.code),
}; };
Ok(()) Ok(())

View file

@ -7,7 +7,7 @@
*/ */
use crate::common::{self, ExitCode, ReturnData}; use crate::common::{self, ExitCode, ReturnData};
use crate::config::{Config, ConfigFile}; use crate::config::{self, Config, ConfigFile};
use git2::build::RepoBuilder; use git2::build::RepoBuilder;
use git2::string_array::StringArray; use git2::string_array::StringArray;
@ -15,8 +15,7 @@ use git2::{CertificateCheckStatus, Cred, FetchOptions, PushOptions, RemoteCallba
use git2::{Error, ErrorCode}; use git2::{Error, ErrorCode};
use hex; use hex;
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use std::env; use std::fs::{self, remove_dir_all};
use std::fs;
use std::path::{Path, PathBuf, MAIN_SEPARATOR_STR}; use std::path::{Path, PathBuf, MAIN_SEPARATOR_STR};
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc; use std::sync::Arc;
@ -25,21 +24,23 @@ use std::time;
pub struct Refractr { pub struct Refractr {
pub docker: bool, pub docker: bool,
pub persist: bool,
pub pid: u32, pub pid: u32,
pub strict: bool, pub strict: bool,
pub unix: bool, pub unix: bool,
pub verbose: u8, pub verbose: u8,
pub run: Arc<AtomicBool>
} }
struct OpenedRepository { struct OpenedRepository {
repo: Repository, repo: Repository,
path: String,
remotes: Vec<String>, remotes: Vec<String>,
cfg: Config, cfg: Config,
ssh: bool,
} }
impl Refractr { 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) { if let Err(e) = fs::create_dir_all(&work_dir) {
return Err(format!( return Err(format!(
"could not create working directory: {}: {}", "could not create working directory: {}: {}",
@ -47,25 +48,53 @@ impl Refractr {
e 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(); let mut refs_branches = Vec::new();
for branch in branches { for branch in branches {
refs_branches.push(format!("refs/heads/{}", branch)); refs_branches.push(format!("refs/heads/{}", branch));
} }
for tag in &tags { if let Some(tags) = tags {
refs_branches.push(format!("refs/tags/{}", tag.unwrap())) for tag in &tags {
refs_branches.push(format!("refs/tags/{}", tag.unwrap()))
}
} }
refs_branches refs_branches
} }
fn fast_forward(&self, repo_dir: &str, branches: &Vec<String>) -> Result<(), Error> { fn fast_forward(&self, repo: &Repository, branches: &Vec<String>) -> Result<(), Error> {
let repo = Repository::open(repo_dir)?; common::verbose(self.verbose, 2, format!("Fast forwarding repo"));
let mut fo = FetchOptions::new();
common::verbose(self.verbose, 2, format!("Pulling origin")); fo.download_tags(git2::AutotagOption::All);
repo.find_remote("origin")?.fetch(&branches, None, None)?;
for branch in branches { for branch in branches {
let refname = format!("refs/remotes/origin/{}", branch); let refname = format!("refs/remotes/origin/{}", branch);
@ -82,30 +111,23 @@ impl Refractr {
branches: &Vec<String>, branches: &Vec<String>,
ssh: bool, ssh: bool,
ssh_key: &String, ssh_key: &String,
strict: bool,
) -> Result<(), Error> { ) -> Result<(), Error> {
let mut cb = RemoteCallbacks::new(); common::verbose(self.verbose, 2, format!("Fetching repo"));
let mut fo = FetchOptions::new(); let mut fo = FetchOptions::new();
if ssh { if ssh {
let key_string: String = ssh_key.clone(); match self.set_up_ssh(ssh_key.clone(), strict.clone()) {
cb.credentials(move |_, _, _| Cred::ssh_key("git", None, Path::new(&key_string), None)); Ok(cb) => {
cb.certificate_check(|cert, url| { fo.remote_callbacks(cb);
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())); Err(e) => common::error_quit(
} format!("error setting up ssh: {}", e),
common::warning(format!( ExitCode::ConfigError,
"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)
});
} }
fo.download_tags(git2::AutotagOption::All); fo.download_tags(git2::AutotagOption::All);
fo.remote_callbacks(cb);
repo repo
.find_remote("origin")? .find_remote("origin")?
.fetch(&branches, Some(&mut fo), None)?; .fetch(&branches, Some(&mut fo), None)?;
@ -168,39 +190,34 @@ impl Refractr {
1, 1,
format!("Pushing to remote: {}", remote.url().unwrap()), format!("Pushing to remote: {}", remote.url().unwrap()),
); );
let mut callbacks = RemoteCallbacks::new(); let mut po = PushOptions::new();
callbacks.credentials(|_, _, _| { match self.set_up_ssh(cfg.git.ssh_identity_file.clone(), self.strict.clone()) {
Cred::ssh_key("git", None, &Path::new(&cfg.git.ssh_identity_file), None) Ok(cb) => {
}); po.remote_callbacks(cb);
callbacks.certificate_check(|cert, url| { ()
let mut sha256 = String::new(); },
for i in cert.as_hostkey().unwrap().hash_sha256().unwrap().to_vec() { Err(e) => common::error_quit(
sha256.push_str(&hex::encode(i.to_string())); format!("error setting up ssh: {}", e),
} ExitCode::ConfigError,
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);
let mut refs = Vec::new(); let mut refs = Vec::new();
let mut refs_str = String::new(); let mut refs_str = String::new();
let tags = repo.tag_names(None).unwrap(); let strings = self.get_refs(
let strings = self.get_refs(&cfg.branches, tags); &cfg.branches,
match cfg.push_tags {
true => Some(repo.tag_names(None).unwrap()),
false => None,
},
);
for branch in &strings { for branch in &strings {
refs.push(branch.as_str()); refs.push(branch.as_str());
refs_str.push_str(format!("{} ", branch).as_str()); refs_str.push_str(format!("{} ", branch).as_str());
} }
common::verbose(self.verbose, 4, format!("ref list: {}", refs_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(_) => (), Ok(_) => (),
Err(e) => { Err(e) => {
if self.strict { if self.strict {
@ -229,7 +246,7 @@ impl Refractr {
let r = running.clone(); let r = running.clone();
let count = repos.len(); let count = repos.len();
for i in 0..repos.len() { for i in 0..repos.len() {
current_ints.push(u64::from( current_ints.push(i64::from(
repos[i].cfg.schedule.interval.unwrap().unsigned_abs(), repos[i].cfg.schedule.interval.unwrap().unsigned_abs(),
)); ));
} }
@ -241,11 +258,11 @@ impl Refractr {
.expect("Failed to set ^C handler"); .expect("Failed to set ^C handler");
common::verbose(self.verbose, 1, format!("Starting scheduled loop")); common::verbose(self.verbose, 1, format!("Starting scheduled loop"));
let min = *current_ints.iter().min().unwrap();
let mut do_break = false; let mut do_break = false;
while !do_break { while !do_break {
do_break = true; 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(); let now = time::Instant::now();
common::verbose( common::verbose(
@ -254,24 +271,47 @@ impl Refractr {
format!("Sleeping for {} seconds", sleep_int.as_secs()), format!("Sleeping for {} seconds", sleep_int.as_secs()),
); );
while running.load(Ordering::SeqCst) { 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() { if now.elapsed().as_secs() >= sleep_int.as_secs() {
common::verbose(self.verbose, 3, format!("Thread has awoken!")); common::verbose(self.verbose, 3, format!("Thread has awoken!"));
for i in 0..count { for i in 0..count {
current_ints[i] -= now.elapsed().as_secs(); current_ints[i] -= now.elapsed().as_secs() as i64;
if i <= 0 { common::verbose(
current_ints[i] = original_ints[i].clone(); 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( common::verbose(
self.verbose, self.verbose,
2, 2,
format!("Interval for {} has arrived, pulling", repos[i].cfg.from), format!("Interval for {} has arrived, pulling", repos[i].cfg.from),
); );
if let Err(e) = self.fetch(
&repos[i].repo,
&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].path, &repos[i].cfg.branches); 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) { if let Err(e) = self.push_remotes(&repos[i].cfg, &repos[i].repo, &repos[i].remotes) {
common::error(e, ExitCode::PushError) 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; do_break = false;
break; break;
@ -283,37 +323,43 @@ impl Refractr {
Ok(()) 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> { 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")); common::verbose(self.verbose, 3, format!("Starting main refractr loop"));
let mut loop_repos = Vec::new(); let mut loop_repos = Vec::new();
let mut work_dirs = Vec::new();
for cfg in cfgs { for cfg in cfgs {
// set up the working directory // set up the working directory
common::verbose(self.verbose, 3, format!("Loading config: {}", cfg.path)); common::verbose(self.verbose, 3, format!("Loading config: {}", cfg.path));
let work_dir = self.set_up_work_dir(match &cfg.config.work_dir { let work_dir = config::get_work_dir(&cfg.config.work_dir);
None => { if let Err(e) = self.set_up_work_dir(PathBuf::from(&work_dir)) {
if cfg!(windows) { return Err(ReturnData {
PathBuf::from(format!("{}\\refractr", env::var("TEMP").unwrap())) msg: e,
} else { code: ExitCode::FilesystemError,
PathBuf::from("/tmp/refractr") });
} }
},
Some(path) => PathBuf::from(path),
});
let path_str = match work_dir {
Ok(p) => p,
Err(e) => {
return Err(ReturnData {
code: ExitCode::FilesystemError,
msg: e,
})
},
};
common::verbose( common::verbose(
self.verbose, self.verbose,
2, 2,
format!("Created working directory: {}", &path_str), format!("Created working directory: {}", work_dir),
); );
let repo_name = match &cfg.config.from.split("/").last() { let repo_name = match &cfg.config.from.split("/").last() {
Some(split) => split.to_string(), Some(split) => split.to_string(),
None => { None => {
@ -326,34 +372,28 @@ impl Refractr {
let ssh = cfg.config.from.starts_with("ssh://"); let ssh = cfg.config.from.starts_with("ssh://");
let mut builder = RepoBuilder::new(); let mut builder = RepoBuilder::new();
let mut cb = RemoteCallbacks::new();
let mut fo = FetchOptions::new(); let mut fo = FetchOptions::new();
// make initial clone // make initial clone
if ssh { if ssh {
let key_string = cfg.config.git.ssh_identity_file.clone(); match self.set_up_ssh(
cb.credentials(move |_, _, _| Cred::ssh_key("git", None, Path::new(&key_string), None)); cfg.config.git.ssh_identity_file.clone(),
cb.certificate_check(|cert, url| { self.strict.clone(),
let mut sha256 = String::new(); ) {
for i in cert.as_hostkey().unwrap().hash_sha256().unwrap().to_vec() { Ok(cb) => {
sha256.push_str(&hex::encode(i.to_string())); fo.remote_callbacks(cb);
} ()
common::warning(format!( },
"implicitly trusting unknown host {} with sha256 host key {}", Err(e) => common::error_quit(
url, format!("error setting up ssh: {}", e),
hex::encode(cert.as_hostkey().unwrap().hash_sha256().unwrap().to_vec()) ExitCode::ConfigError,
)); ),
common::warning(format!( };
"to ignore this error in the future, add this host to your known_hosts file"
));
Ok(CertificateCheckStatus::CertificateOk)
});
} }
fo.download_tags(git2::AutotagOption::All); fo.download_tags(git2::AutotagOption::All);
fo.remote_callbacks(cb);
builder.fetch_options(fo); 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( common::verbose(
self.verbose, self.verbose,
1, 1,
@ -363,7 +403,7 @@ impl Refractr {
Ok(repo) => repo, Ok(repo) => repo,
Err(e) => { Err(e) => {
if e.code() != ErrorCode::Exists { if e.code() != ErrorCode::Exists {
common::error( common::error_quit(
format!("failed to clone repo to {}: {}", repo_dir, e), format!("failed to clone repo to {}: {}", repo_dir, e),
ExitCode::FilesystemError, ExitCode::FilesystemError,
); );
@ -372,15 +412,51 @@ impl Refractr {
"found existing repo at {}, attempting to use", "found existing repo at {}, attempting to use",
repo_dir repo_dir
)); ));
match self.fast_forward(&repo_dir, &cfg.config.branches) { match Repository::open(Path::new(&repo_dir)) {
Ok(_) => { Ok(r) => {
if let Ok(repo) = Repository::open(Path::new(&repo_dir)) { if let Ok(rem) = r.find_remote("origin") {
repo match rem.url() {
} else { Some(url) => {
return Err(ReturnData { if url != &cfg.config.from {
code: ExitCode::RepositoryError, return Err(ReturnData {
msg: format!("failed to obtain existing repo"), 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) => { Err(e) => {
@ -393,16 +469,10 @@ impl Refractr {
}, },
}; };
self.set_up_refs(&repo, &cfg.config.branches).unwrap(); if let Err(e) = self.set_up_refs(&repo, &cfg.config.branches) {
if let Err(e) = self.fetch( common::error_quit(
&repo, format!("failed to set up refs: {}", e),
&cfg.config.branches, ExitCode::RepositoryError,
ssh,
&cfg.config.git.ssh_identity_file,
) {
common::error(
format!("failed to fetch repo {}: {}", cfg.config.from, e),
ExitCode::FetchError,
); );
} }
@ -415,20 +485,33 @@ impl Refractr {
}) })
}, },
}; };
if let Err(e) = self.push_remotes(&cfg.config, &repo, &remotes) { 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 { if cfg.config.schedule.enabled {
loop_repos.push(OpenedRepository { loop_repos.push(OpenedRepository {
repo, repo,
path: repo_dir,
remotes, 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 { if loop_repos.len() >= 1 {
common::verbose( common::verbose(
self.verbose, self.verbose,
@ -438,7 +521,7 @@ impl Refractr {
loop_repos.len() loop_repos.len()
), ),
); );
return self.looper(loop_repos); result = self.looper(loop_repos);
} else { } else {
common::verbose( common::verbose(
self.verbose, self.verbose,
@ -447,6 +530,13 @@ impl Refractr {
); );
} }
Ok(()) if !self.persist {
match result {
Ok(()) => return self.clean(work_dirs),
Err(_) => result = self.clean(work_dirs),
}
}
result
} }
} }