diff --git a/Cargo.lock b/Cargo.lock index f0a7310..b204eaa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,23 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + [[package]] name = "aho-corasick" version = "1.1.4" @@ -47,7 +64,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -58,14 +75,25 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] name = "anyhow" -version = "1.0.103" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "apple-native-keyring-store" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" +checksum = "797f94b6a53d7d10b56dc18290e0d40a2158352f108bb4ff32350825081a9f29" +dependencies = [ + "keyring-core", + "log", + "security-framework", +] [[package]] name = "assert_cmd" @@ -82,28 +110,229 @@ dependencies = [ "wait-timeout", ] +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bitflags" -version = "2.13.0" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + +[[package]] +name = "blocking" +version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] [[package]] name = "bstr" -version = "1.12.3" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cee35f73844aa3014bb606320a6c1f010249dbdf43342fe54b5a4f6a8ed4b79" +checksum = "1f7dc094d718f2e1c1559ad110e27eeaae14a5465d3d56dd6dbd793079fbd530" dependencies = [ "memchr", "regex-automata", "serde_core", ] +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + [[package]] name = "cc" -version = "1.2.66" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5d6cac793997bd970000024b2934968efe83b382de4fdcf4fcb46b6ee4ad996" +checksum = "c89588d05638b5b4594a3348a2d6c20277e43a7f5c5202b05cc56888475a47b8" dependencies = [ "find-msvc-tools", "jobserver", @@ -117,11 +346,21 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clap" -version = "4.6.1" +version = "4.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +checksum = "d91e0c145792ef73a6ad36d27c75ac09f1832222a3c209689d90f534685ee5b7" dependencies = [ "clap_builder", "clap_derive", @@ -129,9 +368,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.6.0" +version = "4.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +checksum = "f09628afdcc538b57f3c6341e9c8e9970f18e4a481690a64974d7023bd33548b" dependencies = [ "anstream", "anstyle", @@ -141,14 +380,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.6.1" +version = "4.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 3.0.3", ] [[package]] @@ -164,224 +403,844 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] -name = "difflib" -version = "0.4.0" +name = "concurrent-queue" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] [[package]] -name = "errno" -version = "0.3.14" +name = "core-foundation" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" dependencies = [ + "core-foundation-sys", "libc", - "windows-sys", ] [[package]] -name = "fastrand" -version = "2.4.1" +name = "core-foundation-sys" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] -name = "find-msvc-tools" -version = "0.1.9" +name = "cpufeatures" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] [[package]] -name = "getrandom" -version = "0.4.3" +name = "crc32fast" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", - "libc", - "r-efi", ] [[package]] -name = "git2" -version = "0.21.0" +name = "crossbeam-utils" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddddbf932745a6be37109b6112d3ee09696106f848449069d3a57bba937ab82e" -dependencies = [ - "bitflags", - "libc", - "libgit2-sys", - "log", -] +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" [[package]] -name = "gitlance" -version = "0.1.0" +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ - "anyhow", - "assert_cmd", - "clap", - "git2", - "regex", - "tempfile", + "generic-array", + "typenum", ] [[package]] -name = "heck" -version = "0.5.0" +name = "difflib" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" [[package]] -name = "is_terminal_polyfill" -version = "1.70.2" +name = "digest" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] [[package]] -name = "jobserver" -version = "0.1.35" +name = "displaydoc" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ - "getrandom", - "libc", + "proc-macro2", + "quote", + "syn 2.0.119", ] [[package]] -name = "libc" -version = "0.2.186" +name = "endi" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" [[package]] -name = "libgit2-sys" -version = "0.18.5+1.9.4" +name = "enumflags2" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "005d6ae6eac1912906073e069f7db60b1fa98e052a68227824afe3e3a1c59ca2" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" dependencies = [ - "cc", - "libc", - "libz-sys", - "pkg-config", + "enumflags2_derive", + "serde", ] [[package]] -name = "libz-sys" -version = "1.1.29" +name = "enumflags2_derive" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85bc9657773828b90eeb625adff10eeac83cc21bbfd8e23a03eaa8a33c9e28d9" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", + "proc-macro2", + "quote", + "syn 2.0.119", ] [[package]] -name = "linux-raw-sys" -version = "0.12.1" +name = "equivalent" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] -name = "log" -version = "0.4.33" +name = "errno" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] [[package]] -name = "memchr" -version = "2.8.2" +name = "event-listener" +version = "5.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] [[package]] -name = "once_cell" -version = "1.21.4" +name = "event-listener-strategy" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] [[package]] -name = "once_cell_polyfill" -version = "1.70.2" +name = "fastrand" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" [[package]] -name = "pkg-config" -version = "0.3.33" +name = "find-msvc-tools" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] -name = "predicates" -version = "3.1.4" +name = "flate2" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ - "anstyle", - "difflib", - "predicates-core", + "crc32fast", + "miniz_oxide", ] [[package]] -name = "predicates-core" -version = "1.0.10" +name = "float-cmp" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144" +checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" +dependencies = [ + "num-traits", +] [[package]] -name = "predicates-tree" -version = "1.0.13" +name = "form_urlencoded" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ - "predicates-core", - "termtree", + "percent-encoding", ] [[package]] -name = "proc-macro2" -version = "1.0.106" +name = "futures-core" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" + +[[package]] +name = "futures-io" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" + +[[package]] +name = "futures-lite" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" dependencies = [ - "unicode-ident", + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", ] [[package]] -name = "quote" -version = "1.0.46" +name = "futures-macro" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" dependencies = [ "proc-macro2", + "quote", + "syn 2.0.119", ] [[package]] -name = "r-efi" -version = "6.0.0" +name = "futures-task" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" [[package]] -name = "regex" -version = "1.12.4" +name = "futures-util" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", + "futures-core", + "futures-macro", + "futures-task", + "pin-project-lite", + "slab", ] [[package]] -name = "regex-automata" -version = "0.4.14" +name = "generic-array" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", +] + +[[package]] +name = "git2" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddddbf932745a6be37109b6112d3ee09696106f848449069d3a57bba937ab82e" +dependencies = [ + "bitflags", + "libc", + "libgit2-sys", + "log", +] + +[[package]] +name = "gitlance" +version = "0.1.0" +dependencies = [ + "anyhow", + "assert_cmd", + "clap", + "git2", + "keyring", + "predicates", + "regex", + "rpassword", + "serde", + "serde_json", + "tempfile", + "toml", + "ureq", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "block-padding", + "generic-array", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "keyring" +version = "4.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0298a59b384c540e408a600c8b375a09b49c3f97debc080e2c30675d79a6368a" +dependencies = [ + "apple-native-keyring-store", + "keyring-core", + "windows-native-keyring-store", + "zbus-secret-service-keyring-store", +] + +[[package]] +name = "keyring-core" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb1e621458ca9c51aa110bd0339d4751a056b9576bf1253aee1aa560dda0fc9d" +dependencies = [ + "log", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libgit2-sys" +version = "0.18.7+1.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23c7391e4b9f4ffab1a624223cc1d7385ff9a678f490768add717de7ea2f4d89" +dependencies = [ + "cc", + "libc", + "libz-sys", + "pkg-config", +] + +[[package]] +name = "libz-sys" +version = "1.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85bc9657773828b90eeb625adff10eeac83cc21bbfd8e23a03eaa8a33c9e28d9" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "normalize-line-endings" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "predicates" +version = "3.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe" +dependencies = [ + "anstyle", + "difflib", + "float-cmp", + "normalize-line-endings", + "predicates-core", + "regex", +] + +[[package]] +name = "predicates-core" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144" + +[[package]] +name = "predicates-tree" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2" +dependencies = [ + "predicates-core", + "termtree", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.13+spec-1.1.0", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" dependencies = [ "aho-corasick", "memchr", @@ -394,80 +1253,443 @@ version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rpassword" +version = "7.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2da316a15f47e3d053de9cb2c439650bd8fa4aaeb9365f2e5f27f492ff73c196" +dependencies = [ + "libc", + "rtoolbox", + "windows-sys 0.61.2", +] + +[[package]] +name = "rtoolbox" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50a0e551c1e27e1731aba276dbeaeac73f53c7cd34d1bda485d02bd1e0f36844" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "rustix" version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "secret-service" +version = "5.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a62d7f86047af0077255a29494136b9aaaf697c76ff70b8e49cded4e2623c14" +dependencies = [ + "aes", + "cbc", + "futures-util", + "generic-array", + "getrandom 0.2.17", + "hkdf", + "num", + "once_cell", + "serde", + "sha2", + "zbus", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "termtree" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys", + "indexmap", + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "toml_write", + "winnow 0.7.15", ] [[package]] -name = "serde_core" -version = "1.0.228" +name = "toml_edit" +version = "0.25.13+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" dependencies = [ - "serde_derive", + "indexmap", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.4", ] [[package]] -name = "serde_derive" -version = "1.0.228" +name = "toml_parser" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "proc-macro2", - "quote", - "syn", + "winnow 1.0.4", ] [[package]] -name = "shlex" -version = "2.0.1" +name = "toml_write" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] -name = "strsim" -version = "0.11.1" +name = "tracing" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] [[package]] -name = "syn" -version = "2.0.118" +name = "tracing-attributes" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "unicode-ident", + "syn 2.0.119", ] [[package]] -name = "tempfile" -version = "3.27.0" +name = "tracing-core" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ - "fastrand", - "getrandom", "once_cell", - "rustix", - "windows-sys", ] [[package]] -name = "termtree" -version = "0.5.1" +name = "typenum" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] [[package]] name = "unicode-ident" @@ -475,18 +1697,77 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" +dependencies = [ + "base64", + "flate2", + "log", + "once_cell", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "url", + "webpki-roots 0.26.11", +] + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" +dependencies = [ + "js-sys", + "serde_core", + "wasm-bindgen", +] + [[package]] name = "vcpkg" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "wait-timeout" version = "0.2.1" @@ -496,12 +1777,112 @@ dependencies = [ "libc", ] +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.9", +] + +[[package]] +name = "webpki-roots" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-native-keyring-store" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063426e76fdec7438d56bb777f67e318a84a25c707b07e575cb8b78e10c028f8" +dependencies = [ + "byteorder", + "keyring-core", + "regex", + "windows-sys 0.61.2", + "zeroize", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -510,3 +1891,292 @@ checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ "windows-link", ] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zbus" +version = "5.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe18fb60dc696039e738717b76eaea21e7a4489bbb1885020b43c94236d7e98a" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow 1.0.4", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus-secret-service-keyring-store" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ccede190ba363386a24e8021c7f3848393976609ec9f5d1f8c6c09ef37075b4" +dependencies = [ + "keyring-core", + "secret-service", + "zbus", +] + +[[package]] +name = "zbus_macros" +version = "5.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe96480bed92df2b442a1a30df364e12d08eed03aeb061f2b8dc6afb2be91119" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8bf88b4a3ff53e883001e0e0115b297a9d53c31b9c1edd2bfdd853e3428624e" +dependencies = [ + "serde", + "winnow 1.0.4", + "zvariant", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" + +[[package]] +name = "zvariant" +version = "5.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee2a0bcd2a907786a456fff45aaaaf54c9ba5f50b71ae9ec1a4edd200c94911" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow 1.0.4", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38a708216a18780796770bfe3f4739c7c83a3e8f789b755534bbbc06e4e23e12" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90cb9383f9b45290407a1258b202d3f8f01db719eb60b4e4055c6375af4fc7c7" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.119", + "winnow 1.0.4", +] diff --git a/Cargo.toml b/Cargo.toml index 39a276d..d5406ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,10 +25,17 @@ git2 = "0.21.0" regex = "1.12.4" clap = { version = "4.6.1", features = ["derive"] } anyhow = "1.0.103" +keyring = "4" +rpassword = "7.3" +serde = { version = "1.0", features = ["derive"] } +toml = "0.8" +serde_json = "1.0" +ureq = { version = "2.10", features = ["json"] } [dev-dependencies] tempfile = "3.27.0" assert_cmd = "2.2.2" +predicates = "3.1.4" [profile.release] # opt-level = 3: Maximum optimization level (valid range: 0-3, or "s"/"z" for size) diff --git a/gitlance.toml b/gitlance.toml new file mode 100644 index 0000000..f910582 --- /dev/null +++ b/gitlance.toml @@ -0,0 +1 @@ +model = "openai/gpt-oss-20b" diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..f12a654 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,85 @@ +// SPDX-FileCopyrightText: Canonical Ltd. +// +// SPDX-License-Identifier: Apache-2.0 + +//! Repo-local configuration for `gitlance suggest` (`gitlance.toml`). + +use serde::Deserialize; +use std::path::Path; + +/// Built-in default model, used when no config file or CLI override is present. +pub const DEFAULT_MODEL: &str = "openai/gpt-oss-20b"; + +const CONFIG_FILE_NAME: &str = "gitlance.toml"; + +#[derive(Debug, Deserialize, Default)] +struct RawConfig { + model: Option, +} + +/// Resolved gitlance configuration. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Config { + pub model: String, +} + +impl Default for Config { + fn default() -> Self { + Self { + model: DEFAULT_MODEL.to_string(), + } + } +} + +impl Config { + /// Loads configuration from `gitlance.toml` in `repo_path`, falling back + /// to built-in defaults for any missing/absent values. + pub fn load(repo_path: &str) -> Self { + let config_path = Path::new(repo_path).join(CONFIG_FILE_NAME); + + let raw = std::fs::read_to_string(&config_path) + .ok() + .and_then(|contents| toml::from_str::(&contents).ok()) + .unwrap_or_default(); + + Self { + model: raw.model.unwrap_or_else(|| DEFAULT_MODEL.to_string()), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::TempDir; + + #[test] + fn test_load_defaults_when_no_config_file() { + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let config = Config::load(temp_dir.path().to_str().expect("valid path")); + assert_eq!(config.model, DEFAULT_MODEL); + } + + #[test] + fn test_load_reads_model_from_config_file() { + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + std::fs::write( + temp_dir.path().join("gitlance.toml"), + "model = \"openai/gpt-4o\"\n", + ) + .expect("Failed to write config file"); + + let config = Config::load(temp_dir.path().to_str().expect("valid path")); + assert_eq!(config.model, "openai/gpt-4o"); + } + + #[test] + fn test_load_defaults_on_invalid_toml() { + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + std::fs::write(temp_dir.path().join("gitlance.toml"), "not valid toml =") + .expect("Failed to write config file"); + + let config = Config::load(temp_dir.path().to_str().expect("valid path")); + assert_eq!(config.model, DEFAULT_MODEL); + } +} diff --git a/src/credential_store/error.rs b/src/credential_store/error.rs new file mode 100644 index 0000000..098b013 --- /dev/null +++ b/src/credential_store/error.rs @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: Canonical Ltd. +// +// SPDX-License-Identifier: Apache-2.0 + +use std::fmt; + +/// Errors that can occur while working with a credential store. +#[derive(Debug)] +pub enum CredentialStoreError { + /// The store could not be initialized (e.g. no keyring backend available). + InitStore(String), + /// Failed to read the token from the store. + TokenRead(String), + /// Failed to write the token to the store. + TokenWrite(String), + /// Failed to delete the token from the store. + TokenDelete(String), +} + +impl fmt::Display for CredentialStoreError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + CredentialStoreError::InitStore(msg) => { + write!(f, "Credential store init error: {}", msg) + } + CredentialStoreError::TokenRead(msg) => write!(f, "Token read error: {}", msg), + CredentialStoreError::TokenWrite(msg) => write!(f, "Token write error: {}", msg), + CredentialStoreError::TokenDelete(msg) => write!(f, "Token delete error: {}", msg), + } + } +} + +impl std::error::Error for CredentialStoreError {} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_init_store_display() { + let err = CredentialStoreError::InitStore("no backend".to_string()); + assert_eq!(err.to_string(), "Credential store init error: no backend"); + } + + #[test] + fn test_token_read_display() { + let err = CredentialStoreError::TokenRead("missing".to_string()); + assert_eq!(err.to_string(), "Token read error: missing"); + } + + #[test] + fn test_token_write_display() { + let err = CredentialStoreError::TokenWrite("denied".to_string()); + assert_eq!(err.to_string(), "Token write error: denied"); + } + + #[test] + fn test_token_delete_display() { + let err = CredentialStoreError::TokenDelete("not present".to_string()); + assert_eq!(err.to_string(), "Token delete error: not present"); + } +} diff --git a/src/credential_store/factory.rs b/src/credential_store/factory.rs new file mode 100644 index 0000000..552aa54 --- /dev/null +++ b/src/credential_store/factory.rs @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: Canonical Ltd. +// +// SPDX-License-Identifier: Apache-2.0 + +use crate::credential_store::{ + CredentialStore, CredentialStoreError, FileCredentialStore, KeyringCredentialStore, +}; + +/// Creates the best available credential store: tries the OS keyring first, +/// falling back to a plain file under `$XDG_RUNTIME_DIR` if the keyring is +/// unavailable (e.g. locked, no D-Bus session), mirroring kfactory's +/// `CredentialStoreFactory.create_store()`. +pub struct CredentialStoreFactory; + +impl CredentialStoreFactory { + /// Returns an error only if neither the keyring nor the file fallback + /// could be initialized (e.g. no keyring backend AND `XDG_RUNTIME_DIR` unset). + pub fn create_store() -> Result, CredentialStoreError> { + match KeyringCredentialStore::new() { + Ok(store) => Ok(Box::new(store)), + Err(keyring_err) => FileCredentialStore::new() + .map(|store| Box::new(store) as Box) + .map_err(|file_err| { + CredentialStoreError::InitStore(format!( + "no usable credential store (keyring: {}; file: {})", + keyring_err, file_err + )) + }), + } + } +} diff --git a/src/credential_store/file_store.rs b/src/credential_store/file_store.rs new file mode 100644 index 0000000..4547575 --- /dev/null +++ b/src/credential_store/file_store.rs @@ -0,0 +1,179 @@ +// SPDX-FileCopyrightText: Canonical Ltd. +// +// SPDX-License-Identifier: Apache-2.0 + +use crate::credential_store::{CredentialStore, CredentialStoreError}; +use std::fs; +use std::io::ErrorKind; +use std::path::PathBuf; + +const XDG_RUNTIME_DIR_ENV: &str = "XDG_RUNTIME_DIR"; + +/// Credential store backed by a plain file, used as a fallback when no OS +/// keyring is available. Mirrors kfactory's `FileCredentialStore`, storing the +/// token under `$XDG_RUNTIME_DIR/gitlance/application_token` (cleared on +/// logout/reboot). +pub struct FileCredentialStore { + token_path: PathBuf, +} + +impl FileCredentialStore { + /// Creates a new file-backed store, failing if `XDG_RUNTIME_DIR` is unset. + pub fn new() -> Result { + let runtime_dir = std::env::var(XDG_RUNTIME_DIR_ENV).map_err(|_| { + CredentialStoreError::InitStore(format!("{} is not set", XDG_RUNTIME_DIR_ENV)) + })?; + + Ok(Self { + token_path: PathBuf::from(runtime_dir) + .join("gitlance") + .join("application_token"), + }) + } + + #[cfg(test)] + fn with_path(token_path: PathBuf) -> Self { + Self { token_path } + } +} + +impl CredentialStore for FileCredentialStore { + fn get_name(&self) -> &str { + "file" + } + + fn read_token(&self) -> Result, CredentialStoreError> { + match fs::read_to_string(&self.token_path) { + Ok(token) => Ok(Some(token)), + Err(e) if e.kind() == ErrorKind::NotFound => Ok(None), + Err(e) => Err(CredentialStoreError::TokenRead(e.to_string())), + } + } + + fn write_token(&self, token: &str) -> Result<(), CredentialStoreError> { + if let Some(parent) = self.token_path.parent() { + fs::create_dir_all(parent) + .map_err(|e| CredentialStoreError::TokenWrite(e.to_string()))?; + } + + // Create the file with owner-only permissions from the start (rather + // than writing with default permissions and chmod'ing afterwards), + // since this file holds a live API key and the write-then-chmod + // sequence leaves a brief window where it may be readable by + // group/other depending on umask. + #[cfg(unix)] + { + use std::io::Write; + use std::os::unix::fs::OpenOptionsExt; + + let mut file = fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .mode(0o600) + .open(&self.token_path) + .map_err(|e| CredentialStoreError::TokenWrite(e.to_string()))?; + + file.write_all(token.as_bytes()) + .map_err(|e| CredentialStoreError::TokenWrite(e.to_string()))?; + } + + #[cfg(not(unix))] + { + fs::write(&self.token_path, token) + .map_err(|e| CredentialStoreError::TokenWrite(e.to_string()))?; + } + + Ok(()) + } + + fn delete_token(&self) -> Result<(), CredentialStoreError> { + match fs::remove_file(&self.token_path) { + Ok(()) => Ok(()), + Err(e) if e.kind() == ErrorKind::NotFound => Ok(()), + Err(e) => Err(CredentialStoreError::TokenDelete(e.to_string())), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::TempDir; + + #[test] + fn test_read_token_missing_returns_none() { + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let store = FileCredentialStore::with_path(temp_dir.path().join("application_token")); + + assert_eq!(store.read_token().expect("read_token failed"), None); + } + + #[test] + fn test_write_then_read_token_round_trip() { + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let store = FileCredentialStore::with_path(temp_dir.path().join("application_token")); + + store + .write_token("sk-test-token") + .expect("write_token failed"); + assert_eq!( + store.read_token().expect("read_token failed"), + Some("sk-test-token".to_string()) + ); + } + + #[test] + fn test_write_token_creates_parent_dir() { + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let store = + FileCredentialStore::with_path(temp_dir.path().join("nested/dir/application_token")); + + store + .write_token("sk-test-token") + .expect("write_token failed"); + assert_eq!( + store.read_token().expect("read_token failed"), + Some("sk-test-token".to_string()) + ); + } + + #[test] + fn test_delete_token_removes_file() { + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let store = FileCredentialStore::with_path(temp_dir.path().join("application_token")); + + store + .write_token("sk-test-token") + .expect("write_token failed"); + store.delete_token().expect("delete_token failed"); + assert_eq!(store.read_token().expect("read_token failed"), None); + } + + #[test] + fn test_delete_token_missing_is_ok() { + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let store = FileCredentialStore::with_path(temp_dir.path().join("application_token")); + + assert!(store.delete_token().is_ok()); + } + + #[test] + #[cfg(unix)] + fn test_write_token_sets_restrictive_permissions() { + use std::os::unix::fs::PermissionsExt; + + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let store = FileCredentialStore::with_path(temp_dir.path().join("application_token")); + + store + .write_token("sk-test-token") + .expect("write_token failed"); + let mode = fs::metadata(&store.token_path) + .expect("metadata failed") + .permissions() + .mode() + & 0o777; + assert_eq!(mode, 0o600); + } +} diff --git a/src/credential_store/keyring_store.rs b/src/credential_store/keyring_store.rs new file mode 100644 index 0000000..3eb44a8 --- /dev/null +++ b/src/credential_store/keyring_store.rs @@ -0,0 +1,191 @@ +// SPDX-FileCopyrightText: Canonical Ltd. +// +// SPDX-License-Identifier: Apache-2.0 + +use crate::credential_store::{CredentialStore, CredentialStoreError}; +use keyring::Entry; + +/// Service/username under which the token is stored in the OS keyring, +/// analogous to kfactory's `SERVICE_NAME = "kernelfactory-cli"`. +const SERVICE_NAME: &str = "gitlance-cli"; +const CLI_USERNAME: &str = "gitlance-cli-user"; + +/// Narrow interface over the OS keyring operations we need. Lets tests +/// substitute a mock backend, since the real `keyring` crate has no +/// built-in mock support and requires a real OS keyring session. +trait KeyringBackend { + fn get_password(&self) -> Result; + fn set_password(&self, token: &str) -> Result<(), keyring::Error>; + fn delete_credential(&self) -> Result<(), keyring::Error>; +} + +impl KeyringBackend for Entry { + fn get_password(&self) -> Result { + Entry::get_password(self) + } + + fn set_password(&self, token: &str) -> Result<(), keyring::Error> { + Entry::set_password(self, token) + } + + fn delete_credential(&self) -> Result<(), keyring::Error> { + Entry::delete_credential(self) + } +} + +/// Credential store backed by the OS keyring (Secret Service on Linux, +/// Keychain on macOS, Credential Manager on Windows), via the `keyring` crate. +pub struct KeyringCredentialStore { + entry: Box, +} + +impl KeyringCredentialStore { + /// Creates a new keyring-backed store, failing if no usable keyring + /// backend is available (e.g. no D-Bus session, locked keyring). + pub fn new() -> Result { + let entry = Entry::new(SERVICE_NAME, CLI_USERNAME) + .map_err(|e| CredentialStoreError::InitStore(e.to_string()))?; + + Ok(Self { + entry: Box::new(entry), + }) + } +} + +impl CredentialStore for KeyringCredentialStore { + fn get_name(&self) -> &str { + "keyring" + } + + fn read_token(&self) -> Result, CredentialStoreError> { + match self.entry.get_password() { + Ok(token) => Ok(Some(token)), + Err(keyring::Error::NoEntry) => Ok(None), + Err(e) => Err(CredentialStoreError::TokenRead(e.to_string())), + } + } + + fn write_token(&self, token: &str) -> Result<(), CredentialStoreError> { + self.entry + .set_password(token) + .map_err(|e| CredentialStoreError::TokenWrite(e.to_string())) + } + + fn delete_token(&self) -> Result<(), CredentialStoreError> { + match self.entry.delete_credential() { + Ok(()) | Err(keyring::Error::NoEntry) => Ok(()), + Err(e) => Err(CredentialStoreError::TokenDelete(e.to_string())), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::cell::RefCell; + + /// Mock keyring backend, taking inspiration from kfactory's + /// `MockKeystore` (tests/unit/kfactory/credential_store/test_keyring_credential_store.py): + /// an in-memory cache standing in for the real OS keyring, with the + /// ability to force set/delete failures to exercise error handling. + #[derive(Default)] + struct MockKeyringBackend { + cache: RefCell>, + fail_set: bool, + fail_delete: bool, + } + + impl KeyringBackend for MockKeyringBackend { + fn get_password(&self) -> Result { + self.cache.borrow().clone().ok_or(keyring::Error::NoEntry) + } + + fn set_password(&self, token: &str) -> Result<(), keyring::Error> { + if self.fail_set { + return Err(keyring::Error::Invalid( + "password".to_string(), + "mocked set failure".to_string(), + )); + } + *self.cache.borrow_mut() = Some(token.to_string()); + Ok(()) + } + + fn delete_credential(&self) -> Result<(), keyring::Error> { + if self.fail_delete { + return Err(keyring::Error::Invalid( + "password".to_string(), + "mocked delete failure".to_string(), + )); + } + *self.cache.borrow_mut() = None; + Ok(()) + } + } + + fn store_with(backend: MockKeyringBackend) -> KeyringCredentialStore { + KeyringCredentialStore { + entry: Box::new(backend), + } + } + + #[test] + fn test_get_name() { + let store = store_with(MockKeyringBackend::default()); + assert_eq!(store.get_name(), "keyring"); + } + + #[test] + fn test_read_token_none_when_nothing_stored() { + let store = store_with(MockKeyringBackend::default()); + assert_eq!(store.read_token().expect("read_token failed"), None); + } + + #[test] + fn test_write_then_read_token() { + let store = store_with(MockKeyringBackend::default()); + + store.write_token("secret").expect("write_token failed"); + + assert_eq!( + store.read_token().expect("read_token failed"), + Some("secret".to_string()) + ); + } + + #[test] + fn test_write_token_propagates_backend_error() { + let store = store_with(MockKeyringBackend { + fail_set: true, + ..Default::default() + }); + + let err = store + .write_token("secret") + .expect_err("expected write_token to fail"); + assert!(matches!(err, CredentialStoreError::TokenWrite(_))); + } + + #[test] + fn test_delete_token_clears_stored_value() { + let store = store_with(MockKeyringBackend::default()); + store.write_token("secret").expect("write_token failed"); + + store.delete_token().expect("delete_token failed"); + + assert_eq!(store.read_token().expect("read_token failed"), None); + } + + #[test] + fn test_delete_token_propagates_backend_error() { + let store = store_with(MockKeyringBackend { + fail_delete: true, + ..Default::default() + }); + + let err = store + .delete_token() + .expect_err("expected delete_token to fail"); + assert!(matches!(err, CredentialStoreError::TokenDelete(_))); + } +} diff --git a/src/credential_store/mod.rs b/src/credential_store/mod.rs new file mode 100644 index 0000000..01d5835 --- /dev/null +++ b/src/credential_store/mod.rs @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: Canonical Ltd. +// +// SPDX-License-Identifier: Apache-2.0 + +//! Secure storage for the OpenRouter API key. +//! +//! Mirrors the credential store design used by `kfactory` +//! (`kfactory.credential_store`): a `CredentialStore` trait with a +//! keyring-backed implementation as the primary store and a plain-file +//! implementation as a fallback for environments without a usable keyring +//! (e.g. headless CI without a D-Bus session). + +pub mod error; +pub mod factory; +pub mod file_store; +pub mod keyring_store; + +pub use error::CredentialStoreError; +pub use factory::CredentialStoreFactory; +pub use file_store::FileCredentialStore; +pub use keyring_store::KeyringCredentialStore; + +/// A store capable of persisting a single secret token (the OpenRouter API key). +pub trait CredentialStore { + /// Returns a human-readable name for this store (e.g. "keyring", "file"). + fn get_name(&self) -> &str; + + /// Reads the stored token, if any. + fn read_token(&self) -> Result, CredentialStoreError>; + + /// Stores a token, overwriting any existing value. + fn write_token(&self, token: &str) -> Result<(), CredentialStoreError>; + + /// Deletes the stored token, if any. + fn delete_token(&self) -> Result<(), CredentialStoreError>; +} diff --git a/src/generate.rs b/src/generate.rs new file mode 100644 index 0000000..b9f8dc9 --- /dev/null +++ b/src/generate.rs @@ -0,0 +1,293 @@ +// SPDX-FileCopyrightText: Canonical Ltd. +// +// SPDX-License-Identifier: Apache-2.0 + +//! Generates and validates AI-suggested commit messages. +//! +//! Builds a prompt that encodes gitlance's own check rules (conventional +//! commit format, Signed-off-by trailer, no WIP/fixup/squash/amend prefixes), +//! asks an [`LlmProvider`] for a candidate message, and validates the result +//! by running it through the same `check_commits()` functions used to +//! validate real commits. Failing candidates are retried with feedback. + +use crate::checks; +use crate::git::Commit; +use std::fmt; + +/// Maximum number of attempts (1 initial + retries) before giving up. +const MAX_ATTEMPTS: usize = 3; + +#[derive(Debug)] +pub enum GenerateError { + /// The LLM provider request failed. + Provider(String), + /// The provider returned no usable message. + EmptyResponse, + /// All attempts produced a message that failed validation. + ValidationFailed(Vec<(String, String)>), +} + +impl fmt::Display for GenerateError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + GenerateError::Provider(msg) => write!(f, "LLM provider error: {}", msg), + GenerateError::EmptyResponse => write!(f, "LLM provider returned an empty response"), + GenerateError::ValidationFailed(failures) => { + write!(f, "Generated message failed validation: ")?; + for (i, (_, reason)) in failures.iter().enumerate() { + if i > 0 { + write!(f, "; ")?; + } + write!(f, "{}", reason)?; + } + Ok(()) + } + } + } +} + +impl std::error::Error for GenerateError {} + +/// Abstraction over an LLM backend, so providers other than OpenRouter can be +/// added later without touching the CLI or validation logic. +pub trait LlmProvider { + fn generate(&self, prompt: &str) -> Result; +} + +const OPENROUTER_API_URL: &str = "https://openrouter.ai/api/v1/chat/completions"; + +/// LLM provider backed by OpenRouter's chat completions API. +pub struct OpenRouterProvider { + api_key: String, + model: String, +} + +impl OpenRouterProvider { + pub fn new(api_key: String, model: String) -> Self { + Self { api_key, model } + } +} + +#[derive(serde::Serialize)] +struct ChatMessage<'a> { + role: &'a str, + content: &'a str, +} + +#[derive(serde::Serialize)] +struct ChatRequest<'a> { + model: &'a str, + messages: Vec>, +} + +#[derive(serde::Deserialize)] +struct ChatChoice { + message: ChatResponseMessage, +} + +#[derive(serde::Deserialize)] +struct ChatResponseMessage { + content: String, +} + +#[derive(serde::Deserialize)] +struct ChatResponse { + choices: Vec, +} + +impl LlmProvider for OpenRouterProvider { + fn generate(&self, prompt: &str) -> Result { + let request = ChatRequest { + model: &self.model, + messages: vec![ChatMessage { + role: "user", + content: prompt, + }], + }; + + let response = ureq::post(OPENROUTER_API_URL) + .set("Authorization", &format!("Bearer {}", self.api_key)) + .send_json(&request) + .map_err(|e| GenerateError::Provider(e.to_string()))?; + + let chat_response: ChatResponse = response + .into_json() + .map_err(|e| GenerateError::Provider(e.to_string()))?; + + chat_response + .choices + .into_iter() + .next() + .map(|choice| choice.message.content.trim().to_string()) + .filter(|content| !content.is_empty()) + .ok_or(GenerateError::EmptyResponse) + } +} + +/// Builds the prompt sent to the LLM, encoding the exact rules enforced by +/// `checks::conventional`, `checks::wip_fixup`, and `checks::signed_off_by`. +pub fn build_prompt(diff: &str, identity: &(String, String)) -> String { + let (name, email) = identity; + format!( + "You are writing a git commit message for the following staged diff. \ +The message MUST satisfy ALL of these rules:\n\ +\n\ +1. Conventional Commits format: the first line must match \ + `type(scope)?: description`, where type is one of: feat, fix, docs, style, \ + refactor, perf, test, build, ci, chore, revert. A `!` before the colon \ + marks a breaking change (e.g. `feat!: description` or `feat(scope)!: description`).\n\ +2. The first line must NOT start with `fixup!`, `squash!`, `amend!`, or `WIP`/`wip` \ + (case-insensitive), and must not be exactly `WIP`.\n\ +3. The message must include a trailer line, on its own line, exactly matching: \ + `Signed-off-by: {name} <{email}>`.\n\ +\n\ +Respond with ONLY the commit message text (first line + blank line + body, \ +ending with the Signed-off-by trailer). Do not include any explanation, \ +markdown formatting, or code fences.\n\ +\n\ +Staged diff:\n\ +{diff}" + ) +} + +/// Wraps a candidate message as a `Commit` and runs it through all three +/// existing checks, returning any failures (empty = valid). +pub fn validate_candidate(message: &str) -> Vec<(String, String)> { + let commit = Commit { + sha: String::new(), + message: message.to_string(), + }; + let commits = std::slice::from_ref(&commit); + + let mut failures = checks::wip_fixup::check_commits(commits); + failures.extend(checks::signed_off_by::check_commits(commits)); + failures.extend(checks::conventional::check_commits(commits)); + failures +} + +/// Generates a commit message for the given staged diff, validating it +/// against all checks and retrying (with feedback) up to `MAX_ATTEMPTS` +/// times. Never returns a message that fails validation. +pub fn suggest_commit_message( + provider: &dyn LlmProvider, + diff: &str, + identity: &(String, String), +) -> Result { + let base_prompt = build_prompt(diff, identity); + let mut prompt = base_prompt.clone(); + let mut last_failures = Vec::new(); + + for _ in 0..MAX_ATTEMPTS { + let candidate = provider.generate(&prompt)?; + let failures = validate_candidate(&candidate); + + if failures.is_empty() { + return Ok(candidate); + } + + let feedback = failures + .iter() + .map(|(_, reason)| reason.as_str()) + .collect::>() + .join("; "); + + prompt = format!( + "{base_prompt}\n\n\ +The previous attempt failed validation for these reasons: {feedback}. \ +Please produce a corrected message that satisfies ALL rules." + ); + last_failures = failures; + } + + Err(GenerateError::ValidationFailed(last_failures)) +} + +#[cfg(test)] +mod tests { + use super::*; + + struct StubProvider { + responses: std::cell::RefCell>, + } + + impl LlmProvider for StubProvider { + fn generate(&self, _prompt: &str) -> Result { + self.responses + .borrow_mut() + .pop() + .ok_or(GenerateError::EmptyResponse) + } + } + + fn identity() -> (String, String) { + ("Test User".to_string(), "test@example.com".to_string()) + } + + #[test] + fn test_build_prompt_contains_rules_and_identity() { + let prompt = build_prompt("diff --git a/f b/f", &identity()); + assert!(prompt.contains("Conventional Commits")); + assert!(prompt.contains("fixup!")); + assert!(prompt.contains("Signed-off-by: Test User ")); + assert!(prompt.contains("diff --git a/f b/f")); + } + + #[test] + fn test_validate_candidate_passes_valid_message() { + let message = "feat: add new feature\n\nSigned-off-by: Test User "; + assert!(validate_candidate(message).is_empty()); + } + + #[test] + fn test_validate_candidate_fails_missing_signoff() { + let failures = validate_candidate("feat: add new feature"); + assert!(!failures.is_empty()); + } + + #[test] + fn test_validate_candidate_fails_wip() { + let failures = + validate_candidate("WIP: add feature\n\nSigned-off-by: Test User "); + assert!(!failures.is_empty()); + } + + #[test] + fn test_suggest_commit_message_returns_valid_first_try() { + let provider = StubProvider { + responses: std::cell::RefCell::new(vec![ + "feat: add new feature\n\nSigned-off-by: Test User ".to_string(), + ]), + }; + + let result = suggest_commit_message(&provider, "diff", &identity()); + assert!(result.is_ok()); + } + + #[test] + fn test_suggest_commit_message_retries_then_succeeds() { + // Responses are popped in reverse order (Vec::pop takes the last item). + let provider = StubProvider { + responses: std::cell::RefCell::new(vec![ + "feat: add new feature\n\nSigned-off-by: Test User ".to_string(), + "not conventional at all".to_string(), + ]), + }; + + let result = suggest_commit_message(&provider, "diff", &identity()); + assert!(result.is_ok()); + } + + #[test] + fn test_suggest_commit_message_fails_after_max_attempts() { + let provider = StubProvider { + responses: std::cell::RefCell::new(vec![ + "still not valid".to_string(), + "still not valid".to_string(), + "still not valid".to_string(), + ]), + }; + + let result = suggest_commit_message(&provider, "diff", &identity()); + assert!(matches!(result, Err(GenerateError::ValidationFailed(_)))); + } +} diff --git a/src/git.rs b/src/git.rs index d55e205..913f8c8 100644 --- a/src/git.rs +++ b/src/git.rs @@ -18,6 +18,64 @@ pub fn open_repo(repo_path: &str) -> Result { .map_err(|e| CheckError::Repository(format!("Failed to open repository: {}", e))) } +/// Returns the staged changes (index vs. HEAD) as a unified diff string. +/// +/// Returns an empty string if nothing is staged. If the repository has no +/// commits yet (unborn HEAD), diffs the index against an empty tree so +/// initial staged additions are still picked up. +pub fn get_staged_diff(repo: &Repository) -> Result { + let head_tree = match repo.head() { + Ok(head_ref) => Some( + head_ref + .peel_to_tree() + .map_err(|e| CheckError::Git(format!("Failed to peel HEAD to tree: {}", e)))?, + ), + Err(e) + if e.code() == git2::ErrorCode::UnbornBranch + || e.code() == git2::ErrorCode::NotFound => + { + None + } + Err(e) => return Err(CheckError::Git(format!("Failed to read HEAD: {}", e))), + }; + + let diff = repo + .diff_tree_to_index(head_tree.as_ref(), None, None) + .map_err(|e| CheckError::Git(format!("Failed to diff index against HEAD: {}", e)))?; + + let mut buf = String::new(); + diff.print(git2::DiffFormat::Patch, |_delta, _hunk, line| { + let content = String::from_utf8_lossy(line.content()); + if matches!(line.origin(), '+' | '-' | ' ') { + buf.push(line.origin()); + } + buf.push_str(&content); + true + }) + .map_err(|e| CheckError::Git(format!("Failed to render diff: {}", e)))?; + + Ok(buf) +} + +/// Reads the configured `user.name`/`user.email` identity, used for the +/// auto-appended `Signed-off-by` trailer. +pub fn get_git_identity(repo: &Repository) -> Result<(String, String), CheckError> { + let signature = repo + .signature() + .map_err(|e| CheckError::Git(format!("Failed to read git identity: {}", e)))?; + + let name = signature + .name() + .map_err(|e| CheckError::Git(format!("user.name is not valid UTF-8: {}", e)))? + .to_string(); + let email = signature + .email() + .map_err(|e| CheckError::Git(format!("user.email is not valid UTF-8: {}", e)))? + .to_string(); + + Ok((name, email)) +} + /// Resolves a git reference (SHA, branch, tag, HEAD~n, etc.) to a commit OID. /// /// Accepts any valid git revision specification: @@ -99,6 +157,117 @@ mod tests { use crate::test_utils::*; use tempfile::TempDir; + #[test] + fn test_get_staged_diff_empty_when_nothing_staged() { + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let repo_path = create_test_repo( + temp_dir + .path() + .to_str() + .expect("Failed to convert temp dir path to string"), + ); + create_commit(&repo_path, "initial"); + let repo = open_repo(&repo_path).expect("Failed to open repo"); + + let diff = get_staged_diff(&repo).expect("get_staged_diff failed"); + assert!(diff.is_empty()); + } + + #[test] + fn test_get_staged_diff_contains_staged_content() { + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let repo_path = create_test_repo( + temp_dir + .path() + .to_str() + .expect("Failed to convert temp dir path to string"), + ); + create_commit(&repo_path, "initial"); + + std::fs::write( + std::path::Path::new(&repo_path).join("new_file.txt"), + "hello world\n", + ) + .expect("Failed to write file"); + run_cmd(&repo_path, "git", &["add", "new_file.txt"]); + + let repo = open_repo(&repo_path).expect("Failed to open repo"); + let diff = get_staged_diff(&repo).expect("get_staged_diff failed"); + + assert!(diff.contains("new_file.txt")); + assert!(diff.contains("hello world")); + } + + #[test] + fn test_get_staged_diff_no_commits_yet() { + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let repo_path = create_test_repo( + temp_dir + .path() + .to_str() + .expect("Failed to convert temp dir path to string"), + ); + + std::fs::write( + std::path::Path::new(&repo_path).join("new_file.txt"), + "hello world\n", + ) + .expect("Failed to write file"); + run_cmd(&repo_path, "git", &["add", "new_file.txt"]); + + let repo = open_repo(&repo_path).expect("Failed to open repo"); + let diff = get_staged_diff(&repo).expect("get_staged_diff failed"); + + assert!(diff.contains("new_file.txt")); + } + + #[test] + fn test_get_staged_diff_includes_non_utf8_content_lossily() { + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let repo_path = create_test_repo( + temp_dir + .path() + .to_str() + .expect("Failed to convert temp dir path to string"), + ); + create_commit(&repo_path, "initial"); + + // Invalid UTF-8 bytes (no NUL byte, so git still treats it as text + // rather than binary) surrounded by valid ASCII. + let mut content = b"hello ".to_vec(); + content.extend_from_slice(&[0xFF, 0xFE]); + content.extend_from_slice(b" world\n"); + std::fs::write( + std::path::Path::new(&repo_path).join("non_utf8.txt"), + &content, + ) + .expect("Failed to write file"); + run_cmd(&repo_path, "git", &["add", "non_utf8.txt"]); + + let repo = open_repo(&repo_path).expect("Failed to open repo"); + let diff = get_staged_diff(&repo).expect("get_staged_diff failed"); + + assert!(diff.contains("non_utf8.txt")); + assert!(diff.contains("hello")); + assert!(diff.contains("world")); + } + + #[test] + fn test_get_git_identity() { + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let repo_path = create_test_repo( + temp_dir + .path() + .to_str() + .expect("Failed to convert temp dir path to string"), + ); + let repo = open_repo(&repo_path).expect("Failed to open repo"); + + let (name, email) = get_git_identity(&repo).expect("get_git_identity failed"); + assert_eq!(name, "Test User"); + assert_eq!(email, "test@example.com"); + } + #[test] fn test_open_repo_success() { let temp_dir = TempDir::new().expect("Failed to create temp dir"); diff --git a/src/lib.rs b/src/lib.rs index 9740cb3..b24f2df 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,7 +3,10 @@ // SPDX-License-Identifier: Apache-2.0 pub mod checks; +pub mod config; +pub mod credential_store; pub mod error; +pub mod generate; pub mod git; pub mod output; diff --git a/src/main.rs b/src/main.rs index b7fcc60..d0c8957 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,9 @@ // SPDX-License-Identifier: Apache-2.0 use clap::{Parser, Subcommand}; -use gitlance::{checks, git, output, run_check}; +use gitlance::credential_store::CredentialStoreFactory; +use gitlance::generate::{suggest_commit_message, OpenRouterProvider}; +use gitlance::{checks, config::Config, git, output, run_check}; use std::process::exit; #[derive(Parser)] @@ -43,17 +45,173 @@ enum Commands { /// Run all checks (default if no command given) All, + + /// Store your OpenRouter API key for `gitlance suggest` (overwrites any existing key) + Init, + + /// Generate a compliant commit message for the currently staged changes + Suggest { + /// Commit directly with the validated message instead of printing it + #[arg(long)] + commit: bool, + }, } fn main() { - let cli = Cli::parse(); + let mut cli = Cli::parse(); + let command = cli.command.take().unwrap_or(Commands::All); + + match command { + Commands::Init => run_init(), + Commands::Suggest { commit } => run_suggest(&cli.repo, commit), + command => run_checks(command, &cli), + } +} + +/// Resolves the OpenRouter API key: `OPENROUTER_API_KEY` env var > stored credential. +fn resolve_api_key() -> Option { + std::env::var("OPENROUTER_API_KEY").ok().or_else(|| { + CredentialStoreFactory::create_store() + .ok() + .and_then(|store| store.read_token().ok().flatten()) + }) +} + +fn run_init() { + let store = match CredentialStoreFactory::create_store() { + Ok(store) => store, + Err(e) => { + output::error(&format!("Failed to initialize credential store: {}", e)); + exit(1); + } + }; + + let overwriting = matches!(store.read_token(), Ok(Some(_))); + + let api_key = match rpassword::prompt_password("OpenRouter API key: ") { + Ok(key) if !key.trim().is_empty() => key.trim().to_string(), + Ok(_) => { + output::error("No API key provided"); + exit(1); + } + Err(e) => { + output::error(&format!("Failed to read API key: {}", e)); + exit(1); + } + }; + + if let Err(e) = store.write_token(&api_key) { + output::error(&format!("Failed to store API key: {}", e)); + exit(1); + } + + let verb = if overwriting { "replaced" } else { "stored" }; + output::notice(&format!( + "API key {} ({} credential store)", + verb, + store.get_name() + )); +} + +fn run_suggest(repo_path: &str, commit: bool) { + let api_key = match resolve_api_key() { + Some(key) => key, + None => { + output::error( + "No OpenRouter API key found. Set OPENROUTER_API_KEY or run `gitlance init`.", + ); + exit(1); + } + }; + + let model = Config::load(repo_path).model; + + let repo = match git::open_repo(repo_path) { + Ok(r) => r, + Err(e) => { + output::error(&format!("Failed to open repository: {}", e)); + exit(1); + } + }; + + let diff = match git::get_staged_diff(&repo) { + Ok(diff) => diff, + Err(e) => { + output::error(&format!("Failed to read staged diff: {}", e)); + exit(1); + } + }; + + if diff.trim().is_empty() { + output::error("No staged changes found. Stage changes with `git add` first."); + exit(1); + } + + let identity = match git::get_git_identity(&repo) { + Ok(identity) => identity, + Err(e) => { + output::error(&format!("Failed to read git identity: {}", e)); + exit(1); + } + }; + + let provider = OpenRouterProvider::new(api_key, model); + + let message = match suggest_commit_message(&provider, &diff, &identity) { + Ok(message) => message, + Err(e) => { + output::error(&format!("Failed to generate commit message: {}", e)); + exit(1); + } + }; - // Determine which checks to run - let command = cli.command.unwrap_or(Commands::All); + if commit { + use std::io::Write; + use std::process::{Command, Stdio}; + let mut child = match Command::new("git") + .current_dir(repo_path) + .args(["commit", "-F", "-"]) + .stdin(Stdio::piped()) + .spawn() + { + Ok(child) => child, + Err(e) => { + output::error(&format!("Failed to run git commit: {}", e)); + exit(1); + } + }; + + if let Some(mut stdin) = child.stdin.take() { + if let Err(e) = stdin.write_all(message.as_bytes()) { + output::error(&format!("Failed to write commit message: {}", e)); + exit(1); + } + } + + match child.wait() { + Ok(status) if status.success() => println!("{}", message), + Ok(status) => { + output::error(&format!( + "git commit exited with status {:?}", + status.code() + )); + exit(1); + } + Err(e) => { + output::error(&format!("Failed to wait for git commit: {}", e)); + exit(1); + } + } + } else { + println!("{}", message); + } +} + +fn run_checks(command: Commands, cli: &Cli) { // Get refs, with environment variable fallback - let base = cli.base.or_else(|| std::env::var("BASE_REF").ok()); - let head = cli.head.or_else(|| std::env::var("HEAD_REF").ok()); + let base = cli.base.clone().or_else(|| std::env::var("BASE_REF").ok()); + let head = cli.head.clone().or_else(|| std::env::var("HEAD_REF").ok()); // Validate both refs before proceeding let mut has_errors = false; @@ -126,6 +284,9 @@ fn main() { && signoff_failures.is_empty() && conventional_failures.is_empty() } + Commands::Init | Commands::Suggest { .. } => { + unreachable!("handled before run_checks") + } }; exit(if overall_passed { 0 } else { 1 }); diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 1664210..84f2e0b 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -296,4 +296,63 @@ mod tests { "Expected check to pass with HEAD~2 and HEAD references" ); } + + // ===== `suggest` command tests ===== + + #[test] + fn test_suggest_no_staged_changes() { + use assert_cmd::Command; + + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let repo_path = create_test_repo( + temp_dir + .path() + .to_str() + .expect("Failed to convert temp dir path to string"), + ); + create_commit(&repo_path, "initial"); + + let mut cmd = Command::cargo_bin("gitlance").expect("Failed to find binary"); + cmd.env("OPENROUTER_API_KEY", "dummy-key"); + cmd.args(["--repo", &repo_path, "suggest"]); + + cmd.assert() + .failure() + .stdout(predicates::str::contains("No staged changes")); + } + + #[test] + fn test_suggest_missing_api_key() { + use assert_cmd::Command; + + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let repo_path = create_test_repo( + temp_dir + .path() + .to_str() + .expect("Failed to convert temp dir path to string"), + ); + create_commit(&repo_path, "initial"); + + std::fs::write( + std::path::Path::new(&repo_path).join("new_file.txt"), + "hello\n", + ) + .expect("Failed to write file"); + run_cmd(&repo_path, "git", &["add", "new_file.txt"]); + + let mut cmd = Command::cargo_bin("gitlance").expect("Failed to find binary"); + cmd.env_remove("OPENROUTER_API_KEY"); + // Isolate the credential store fallback so a real developer-machine + // credential (if any) doesn't leak into this test. + cmd.env( + "XDG_RUNTIME_DIR", + temp_dir.path().to_str().expect("valid path"), + ); + cmd.args(["--repo", &repo_path, "suggest"]); + + cmd.assert() + .failure() + .stdout(predicates::str::contains("No OpenRouter API key found")); + } }