From c7e9df60c996f0b8c2c798c4181cb674ce70cbd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Tue, 16 Sep 2025 14:20:41 +0200 Subject: [PATCH 1/3] stub youtrack module --- Cargo.lock | 1368 ++++++++++++++++++++++++++++++++++++++++++++--- Cargo.toml | 5 + src/main.rs | 3 + src/youtrack.rs | 11 + 4 files changed, 1323 insertions(+), 64 deletions(-) create mode 100644 src/youtrack.rs diff --git a/Cargo.lock b/Cargo.lock index 09da715..ffa24da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,21 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "aho-corasick" version = "1.1.3" @@ -76,12 +91,39 @@ version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" +[[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.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "backtrace" +version = "0.3.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bitflags" version = "1.3.2" @@ -100,6 +142,12 @@ version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + [[package]] name = "cc" version = "1.2.37" @@ -116,6 +164,12 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.42" @@ -168,6 +222,7 @@ dependencies = [ "indexmap", "inquire", "regex", + "reqwest", "serde", "serde_json", "smart-default", @@ -186,7 +241,17 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.59.0", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", ] [[package]] @@ -220,12 +285,32 @@ dependencies = [ "winapi", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "dyn-clone" version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -265,6 +350,86 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fd99930f64d146689264c637b5af2f0233a933bef0d8570e2526bf9e083192d" +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + [[package]] name = "fuzzy-matcher" version = "0.3.7" @@ -274,6 +439,19 @@ dependencies = [ "thread_local", ] +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", +] + [[package]] name = "getrandom" version = "0.3.3" @@ -281,9 +459,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", "wasi 0.14.5+wasi-0.2.4", + "wasm-bindgen", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "h2" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", ] [[package]] @@ -292,6 +497,127 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + [[package]] name = "iana-time-zone" version = "0.1.64" @@ -316,6 +642,113 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + [[package]] name = "indexmap" version = "2.11.1" @@ -342,6 +775,33 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "io-uring" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" +dependencies = [ + "bitflags 2.9.4", + "cfg-if", + "libc", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -382,6 +842,12 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + [[package]] name = "lock_api" version = "0.4.13" @@ -398,12 +864,33 @@ version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "memchr" version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + [[package]] name = "mio" version = "1.0.4" @@ -416,6 +903,23 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -425,6 +929,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -438,13 +951,57 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" [[package]] -name = "parking_lot" -version = "0.12.4" +name = "openssl" +version = "0.10.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" dependencies = [ - "lock_api", - "parking_lot_core", + "bitflags 2.9.4", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "parking_lot" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +dependencies = [ + "lock_api", + "parking_lot_core", ] [[package]] @@ -460,6 +1017,48 @@ dependencies = [ "windows-targets 0.52.6", ] +[[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.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "potential_utf" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "proc-macro2" version = "1.0.101" @@ -469,6 +1068,61 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "bytes", + "getrandom 0.3.3", + "lru-slab", + "rand", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + [[package]] name = "quote" version = "1.0.40" @@ -484,6 +1138,35 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.3", +] + [[package]] name = "redox_syscall" version = "0.5.17" @@ -522,6 +1205,78 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" +[[package]] +name = "reqwest" +version = "0.12.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] + +[[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.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + [[package]] name = "rustix" version = "0.38.44" @@ -548,6 +1303,41 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "rustls" +version = "0.23.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8572f3c2cb9934231157b45499fc41e1f58c589fdfb81a844ba873265e80f8eb" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -560,12 +1350,44 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.0", +] + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.9.4", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "serde" version = "1.0.219" @@ -607,6 +1429,18 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "shlex" version = "1.3.0" @@ -643,6 +1477,12 @@ dependencies = [ "libc", ] +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + [[package]] name = "smallvec" version = "1.15.1" @@ -660,12 +1500,34 @@ dependencies = [ "syn", ] +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[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.106" @@ -677,6 +1539,47 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.9.4", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tempfile" version = "3.21.0" @@ -684,12 +1587,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" dependencies = [ "fastrand", - "getrandom", + "getrandom 0.3.3", "once_cell", "rustix 1.0.8", "windows-sys 0.60.2", ] +[[package]] +name = "thiserror" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "thread_local" version = "1.1.9" @@ -699,6 +1622,81 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.47.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +dependencies = [ + "backtrace", + "bytes", + "io-uring", + "libc", + "mio", + "pin-project-lite", + "slab", + "socket2", + "windows-sys 0.59.0", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + [[package]] name = "toml" version = "0.9.5" @@ -738,6 +1736,76 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64" +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags 2.9.4", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "unicode-ident" version = "1.0.19" @@ -756,12 +1824,51 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +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 = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -813,6 +1920,19 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca85039a9b469b38336411d6d6ced91f3fc87109a2a27b0c197663f5144dffe" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.101" @@ -845,6 +1965,35 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "web-sys" +version = "0.3.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77e4b637749ff0d92b8fad63aa1f7cff3cbe125fd49c175cd6345e7272638b12" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "winapi" version = "0.3.9" @@ -876,8 +2025,8 @@ dependencies = [ "windows-implement", "windows-interface", "windows-link 0.2.0", - "windows-result", - "windows-strings", + "windows-result 0.4.0", + "windows-strings 0.5.0", ] [[package]] @@ -914,6 +2063,26 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" +[[package]] +name = "windows-registry" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +dependencies = [ + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + [[package]] name = "windows-result" version = "0.4.0" @@ -923,6 +2092,15 @@ dependencies = [ "windows-link 0.2.0", ] +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + [[package]] name = "windows-strings" version = "0.5.0" @@ -934,11 +2112,11 @@ dependencies = [ [[package]] name = "windows-sys" -version = "0.48.0" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] @@ -960,18 +2138,12 @@ dependencies = [ ] [[package]] -name = "windows-targets" -version = "0.48.5" +name = "windows-sys" +version = "0.61.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +checksum = "e201184e40b2ede64bc2ea34968b28e33622acdbbf37104f0e4a33f7abe657aa" dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", + "windows-link 0.2.0", ] [[package]] @@ -1007,12 +2179,6 @@ dependencies = [ "windows_x86_64_msvc 0.53.0", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -1025,12 +2191,6 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -1043,12 +2203,6 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -1073,12 +2227,6 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -1091,12 +2239,6 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -1109,12 +2251,6 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -1127,12 +2263,6 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -1156,3 +2286,113 @@ name = "wit-bindgen" version = "0.45.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c573471f125075647d03df72e026074b7203790d41351cd6edc96f46bcccd36" + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index c157b14..af55b4f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,3 +18,8 @@ faccess = "0.2" chrono = "0.4" indexmap = { version = "2.11", features = ["serde"] } inquire = { version = "0.8.0", features = ["editor"] } + +reqwest = { version = "0.12", features = ["rustls-tls", "blocking"], optional = true } + +[features] +youtrack = ["reqwest"] diff --git a/src/main.rs b/src/main.rs index b13aa8f..43df5c4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -24,6 +24,9 @@ mod store; mod utils; +#[cfg(feature = "youtrack")] +mod youtrack; + #[derive(Debug)] pub struct AppContext { /// Name of the cl binary diff --git a/src/youtrack.rs b/src/youtrack.rs new file mode 100644 index 0000000..6c6b553 --- /dev/null +++ b/src/youtrack.rs @@ -0,0 +1,11 @@ +//! Youtrack integration (mark issues as Released when packing to changelog) + +#[cfg(test)] +mod tests { + #[test] + fn test_youtrack_communication() { + let token = ""; + + + } +} From 753cbebd4842013f26ff91b6dd1463ba8b98ee15 Mon Sep 17 00:00:00 2001 From: ondra Date: Tue, 23 Sep 2025 18:02:52 +0200 Subject: [PATCH 2/3] implement youtrack client --- .gitignore | 1 + Cargo.lock | 168 +++++++++++++--- Cargo.toml | 12 +- src/config.rs | 4 + src/integrations/mod.rs | 3 + src/integrations/youtrack.rs | 370 +++++++++++++++++++++++++++++++++++ src/main.rs | 3 +- src/youtrack.rs | 11 -- 8 files changed, 523 insertions(+), 49 deletions(-) create mode 100644 src/integrations/mod.rs create mode 100644 src/integrations/youtrack.rs delete mode 100644 src/youtrack.rs diff --git a/.gitignore b/.gitignore index c403c34..ddbac2f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target .idea/ +.env diff --git a/Cargo.lock b/Cargo.lock index ffa24da..5c22a24 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -218,13 +218,17 @@ dependencies = [ "chrono", "clap", "colored", + "dotenv", "faccess", "indexmap", "inquire", + "json_dotpath", + "log", "regex", "reqwest", "serde", "serde_json", + "simple-logging", "smart-default", "toml", ] @@ -244,6 +248,15 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "convert_case" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -262,15 +275,17 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "crossterm" -version = "0.28.1" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ "bitflags 2.9.4", "crossterm_winapi", + "derive_more", + "document-features", "mio", "parking_lot", - "rustix 0.38.44", + "rustix", "signal-hook", "signal-hook-mio", "winapi", @@ -285,6 +300,27 @@ dependencies = [ "winapi", ] +[[package]] +name = "derive_more" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -296,6 +332,21 @@ dependencies = [ "syn", ] +[[package]] +name = "document-features" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" +dependencies = [ + "litrs", +] + +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + [[package]] name = "dyn-clone" version = "1.0.20" @@ -762,9 +813,9 @@ dependencies = [ [[package]] name = "inquire" -version = "0.8.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95b8b5b4fd6d0ef1235f11c2e8ce9734be5736c21230ff585c3bae2e940abced" +checksum = "2628910d0114e9139056161d8644a2026be7b117f8498943f9437748b04c9e0a" dependencies = [ "bitflags 2.9.4", "crossterm", @@ -825,16 +876,28 @@ dependencies = [ ] [[package]] -name = "libc" -version = "0.2.175" +name = "json_dotpath" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" +checksum = "dbdcfef3cf5591f0cef62da413ae795e3d1f5a00936ccec0b2071499a32efd1a" +dependencies = [ + "serde", + "serde_derive", + "serde_json", + "thiserror 1.0.69", +] [[package]] -name = "linux-raw-sys" -version = "0.4.15" +name = "lazy_static" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.175" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" [[package]] name = "linux-raw-sys" @@ -848,6 +911,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +[[package]] +name = "litrs" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed" + [[package]] name = "lock_api" version = "0.4.13" @@ -1012,7 +1081,7 @@ checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.17", "smallvec", "windows-targets 0.52.6", ] @@ -1082,7 +1151,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror", + "thiserror 2.0.16", "tokio", "tracing", "web-time", @@ -1103,7 +1172,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror", + "thiserror 2.0.16", "tinyvec", "tracing", "web-time", @@ -1167,6 +1236,12 @@ dependencies = [ "getrandom 0.3.3", ] +[[package]] +name = "redox_syscall" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" + [[package]] name = "redox_syscall" version = "0.5.17" @@ -1277,19 +1352,6 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" -[[package]] -name = "rustix" -version = "0.38.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" -dependencies = [ - "bitflags 2.9.4", - "errno", - "libc", - "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", -] - [[package]] name = "rustix" version = "1.0.8" @@ -1299,7 +1361,7 @@ dependencies = [ "bitflags 2.9.4", "errno", "libc", - "linux-raw-sys 0.9.4", + "linux-raw-sys", "windows-sys 0.60.2", ] @@ -1477,6 +1539,17 @@ dependencies = [ "libc", ] +[[package]] +name = "simple-logging" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00d48e85675326bb182a2286ea7c1a0b264333ae10f27a937a72be08628b542" +dependencies = [ + "lazy_static", + "log", + "thread-id", +] + [[package]] name = "slab" version = "0.4.11" @@ -1589,17 +1662,37 @@ dependencies = [ "fastrand", "getrandom 0.3.3", "once_cell", - "rustix 1.0.8", + "rustix", "windows-sys 0.60.2", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.16", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -1613,6 +1706,17 @@ dependencies = [ "syn", ] +[[package]] +name = "thread-id" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7fbf4c9d56b320106cd64fd024dadfa0be7cb4706725fc44a7d7ce952d820c1" +dependencies = [ + "libc", + "redox_syscall 0.1.57", + "winapi", +] + [[package]] name = "thread_local" version = "1.1.9" @@ -1820,9 +1924,9 @@ checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-width" -version = "0.1.14" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" [[package]] name = "untrusted" diff --git a/Cargo.toml b/Cargo.toml index af55b4f..219abe1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ authors = ["Ondřej Hruška "] description = "Manage changelog across multiple release channels" [dependencies] +log = "0.4" clap = { version = "4.5", features = ["string"] } serde = { version = "1", features = ["derive"] } serde_json = "1" @@ -17,9 +18,12 @@ colored = "3" faccess = "0.2" chrono = "0.4" indexmap = { version = "2.11", features = ["serde"] } -inquire = { version = "0.8.0", features = ["editor"] } +inquire = { version = "0.9", features = ["editor"] } -reqwest = { version = "0.12", features = ["rustls-tls", "blocking"], optional = true } +# For integrations +reqwest = { version = "0.12", features = ["rustls-tls", "blocking"] } #, optional = true +json_dotpath = "1.1.0" +dotenv = "0.15.0" -[features] -youtrack = ["reqwest"] +[dev-dependencies] +simple-logging = "2" diff --git a/src/config.rs b/src/config.rs index 5ff023b..dc77f27 100644 --- a/src/config.rs +++ b/src/config.rs @@ -13,6 +13,10 @@ pub type EntryName = String; pub const CONFIG_FILE_TEMPLATE: &str = include_str!("assets/config_file_template.toml"); +pub const ENV_YOUTRACK_URL : &str = "CLPACK_YOUTRACK_URL"; + +pub const ENV_YOUTRACK_TOKEN : &str = "CLPACK_YOUTRACK_TOKEN"; + #[cfg(test)] #[test] fn test_template_file() { diff --git a/src/integrations/mod.rs b/src/integrations/mod.rs new file mode 100644 index 0000000..6cafcde --- /dev/null +++ b/src/integrations/mod.rs @@ -0,0 +1,3 @@ +mod youtrack; + +pub use youtrack::YouTrackClient; diff --git a/src/integrations/youtrack.rs b/src/integrations/youtrack.rs new file mode 100644 index 0000000..518af5c --- /dev/null +++ b/src/integrations/youtrack.rs @@ -0,0 +1,370 @@ +//! Youtrack integration (mark issues as Released when packing to changelog) + +use crate::config::VersionName; +use anyhow::bail; +use chrono::{DateTime, Utc}; +use json_dotpath::DotPaths; +use log::debug; +use reqwest::header::{HeaderMap, HeaderValue}; +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::borrow::Cow; + +type ProjectId = String; +type BundleID = String; +type FieldID = String; + +pub struct YouTrackClient { + client: reqwest::blocking::Client, + pub url: String, +} + +#[derive(Deserialize)] +struct YoutrackErrorResponse { + error: String, + error_description: String, +} + +impl YouTrackClient { + pub fn new(url: impl ToString, token: &str) -> anyhow::Result { + let token_bearer = format!("Bearer {token}"); + + let mut hm = HeaderMap::new(); + hm.insert("Authorization", HeaderValue::from_str(&token_bearer)?); + hm.insert("Content-Type", HeaderValue::from_str("application/json")?); + hm.insert("Accept", HeaderValue::from_str("application/json")?); + + Ok(YouTrackClient { + url: url.to_string(), + client: reqwest::blocking::Client::builder() + .default_headers(hm) + .build()?, + }) + } + + fn get_json( + &self, + api_path: String, + query: &T, + ) -> anyhow::Result { + let url = format!( + "{base}/api/{path}", + base = self.url.trim_end_matches('/'), + path = api_path.trim_start_matches('/') + ); + + debug!("GET {}", url); + + let response = self.client.get(&url).query(query).send()?; + + let response_text = response.text()?; + + debug!("Resp = {}", response_text); + + if let Ok(e) = serde_json::from_str::(&response_text) { + bail!("Error from YouTrack: {} - {}", e.error, e.error_description); + } + + Ok(serde_json::from_str(&response_text)?) + } + + fn post_json( + &self, + api_path: String, + body: &B, + query: &T, + ) -> anyhow::Result { + let url = format!( + "{base}/api/{path}", + base = self.url.trim_end_matches('/'), + path = api_path.trim_start_matches('/') + ); + + debug!("POST {}", url); + + let body_serialized = serde_json::to_string(body)?; + let response = self + .client + .post(&url) + .query(query) + .body(body_serialized.into_bytes()) + .send()?; + + let response_text = response.text()?; + + debug!("Resp = {}", response_text); + + if let Ok(e) = serde_json::from_str::(&response_text) { + bail!("Error from YouTrack: {} - {}", e.error, e.error_description); + } + + Ok(serde_json::from_str(&response_text)?) + } + + pub fn find_project_id(&self, issue: &str) -> anyhow::Result { + #[derive(Deserialize)] + struct Issue { + project: Project, + } + + #[derive(Deserialize)] + struct Project { + id: ProjectId, + } + + let issue: Issue = + self.get_json(format!("issues/{issue}"), &[("fields", "project(id)")])?; + + // example: + // {"project":{"id":"0-172","$type":"Project"},"$type":"Issue"} + + Ok(issue.project.id) + } + + /// Try to find a version by name in a YouTrack project. + /// If it is not found but we find the field where to add it, the version will be created. + /// + /// project_id - obtained by `find_project_id()` + /// field_name - name of the version field, e.g. "Available in version" + /// version_to_create - name of the version to find or create + /// release_date - if creating, a date YYYY-MM-DD can be passed here. It will be stored into the + /// newly created version & this marked as released. + pub fn ensure_version_exists_in_project( + &self, + project_id: &str, + field_name: &str, + version_to_create: &str, + release_date: Option>, + ) -> anyhow::Result<()> { + #[derive(Deserialize)] + struct BudleDescription { + id: BundleID, + } + #[derive(Deserialize)] + struct FieldDescription { + name: String, + id: FieldID, + } + #[derive(Deserialize)] + struct YTCustomField { + // Bundle is sometimes missing - we skip these entries + bundle: Option, + field: FieldDescription, + } + + // Find field description + let fields: Vec = self.get_json( + format!("admin/projects/{project_id}/customFields"), + &[("fields", "field(name,id),bundle(id)"), ("top", "200")], + )?; + + let mut field_bundle = None; + for entry in fields { + if &entry.field.name == field_name + && let Some(bundle) = entry.bundle + { + field_bundle = Some((entry.field.id, bundle.id)); + break; + } + } + + let Some((field_id, bundle_id)) = field_bundle else { + bail!("YouTrack version field {field_name} not found in the project {project_id}"); + }; + + println!("Found YouTrack version field, checking defined versions"); + + #[derive(Deserialize)] + struct YTVersion { + name: VersionName, + id: String, + } + + let versions: Vec = self.get_json( + format!("admin/customFieldSettings/bundles/version/{bundle_id}/values"), + &[("fields", "id,name"), ("top", "500")], + )?; + + // Find the version we want + for version in versions { + if &version.name == version_to_create { + eprintln!("Version {version_to_create} already exists in YouTrack"); + return Ok(()); + } + } + + println!("Creating version in YouTrack: {version_to_create}"); + + /* + $body = ['name' => $name]; + if ($released !== null) { + $body['released'] = $released; + } + if ($releaseDate !== null) { + $body['releaseDate'] = $releaseDate; + } + if ($archived !== null) { + $body['archived'] = $archived; + } + + return $this->postJson( + "admin/customFieldSettings/bundles/version/$bundleId/values", + $body, + ['fields' => 'id,name,released,releaseDate,archived'], + ); + */ + + #[derive(Serialize)] + struct CreateVersionBody { + name: String, + archived: bool, + released: bool, + #[allow(non_snake_case)] + releaseDate: Option, + // archived + } + + let body = CreateVersionBody { + name: version_to_create.to_string(), + archived: false, + released: release_date.is_some(), + releaseDate: release_date.map(|d| d.timestamp()), + }; + + #[derive(Deserialize, Debug)] + struct CreateVersionResponse { + #[allow(non_snake_case)] + releaseDate: Option, + released: bool, + archived: bool, + name: String, + id: String, + } + + let resp: CreateVersionResponse = self.post_json( + format!("admin/customFieldSettings/bundles/version/{bundle_id}/values"), + &body, + &[("fields", "id,name,released,releaseDate,archived")], + )?; + + // Example response: + // {"releaseDate":null,"released":false,"archived":false,"name":"TEST1","id":"232-356","$type":"VersionBundleElement"} + // {"releaseDate":1758619201,"released":true,"archived":false,"name":"TEST2","id":"232-358","$type":"VersionBundleElement"} + + debug!("Created version entry = {:#?}", resp); + println!("Version {version_to_create} created in YouTrack."); + + Ok(()) + } + + pub fn set_issue_version_and_state( + &self, + issue_id: &str, + version_field_name: &str, + version_name: &str, + target_state_name: &str, + ) -> anyhow::Result<()> { + #[derive(Serialize)] + struct PatchIssueBody { + #[allow(non_snake_case)] + customFields: Vec, + } + + #[derive(Serialize)] + struct EnumValue { + name: String, + } + + #[derive(Serialize)] + struct CustomFieldValue { + name: String, + #[serde(rename = "$type")] + field_type: String, + value: EnumValue, + } + + let body = PatchIssueBody { + customFields: vec![ + CustomFieldValue { + name: version_field_name.to_string(), + field_type: "SingleVersionIssueCustomField".to_string(), + value: EnumValue { + name: version_name.to_string(), + }, + }, + CustomFieldValue { + name: "State".to_string(), + field_type: "StateIssueCustomField".to_string(), + value: EnumValue { + name: target_state_name.to_string(), + }, + }, + ], + }; + + let resp: Value = self.post_json( + format!("issues/{issue_id}"), + &body, + &[("fields", "id,customFields(name,value(name))")], + )?; + + // TODO? Do something with the fields + + // Example success: + // {"customFields":[{"value":null,"name":"Type","$type":"SingleEnumIssueCustomField"},{"value":{"name":"Released","$type":"StateBundleElement"},"name":"State","$type":"StateIssueCustomField"},{"value":null,"name":"Assignee","$type":"SingleUserIssueCustomField"},{"value":null,"name":"Priority","$type":"SingleEnumIssueCustomField"},{"value":{"name":"Internal tooling","$type":"EnumBundleElement"},"name":"Category","$type":"SingleEnumIssueCustomField"},{"value":[],"name":"Customer","$type":"MultiEnumIssueCustomField"},{"value":null,"name":"Customer Funding","$type":"SingleEnumIssueCustomField"},{"value":null,"name":"Product Stream","$type":"SingleEnumIssueCustomField"},{"value":null,"name":"Estimation","$type":"PeriodIssueCustomField"},{"value":{"$type":"PeriodValue"},"name":"Spent time","$type":"PeriodIssueCustomField"},{"value":null,"name":"Due Date","$type":"DateIssueCustomField"},{"value":[],"name":"Affected version","$type":"MultiVersionIssueCustomField"},{"value":{"name":"TEST2","$type":"VersionBundleElement"},"name":"Available in version","$type":"SingleVersionIssueCustomField"},{"value":null,"name":"SlackAlertSent","$type":"SimpleIssueCustomField"},{"value":13.0,"name":"Dev costs","$type":"SimpleIssueCustomField"}],"id":"2-25820","$type":"Issue"} + + println!("YouTrack issue {issue_id} updated."); + + debug!("Response to request to edit issue: {resp:?}"); + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::YouTrackClient; + use crate::config::{ENV_YOUTRACK_TOKEN, ENV_YOUTRACK_URL}; + use log::{LevelFilter, debug}; + use serde_json::Value; + + // #[test] // Disabled + fn test_youtrack_communication() { + simple_logging::log_to_stderr(LevelFilter::Debug); + + let url = dotenv::var(ENV_YOUTRACK_URL).expect("Missing youtrack URL from env"); + let token = dotenv::var(ENV_YOUTRACK_TOKEN).expect("Missing youtrack token from env"); + + // this must match the config in the connected youtrack + let issue_id = "SW-4739"; + let version_field_name = "Available in version"; + let target_state_name = "Released"; + let version_name = "TEST2"; + + let mut client = YouTrackClient::new(url, &token).unwrap(); + + let project_id = client.find_project_id(issue_id).unwrap(); + + debug!("Found YouTrack project ID = {project_id}"); + + let date = chrono::Utc::now(); + + client + .ensure_version_exists_in_project( + &project_id, + version_field_name, + version_name, + Some(date), + ) + .unwrap(); + + client + .set_issue_version_and_state( + issue_id, + version_field_name, + version_name, + target_state_name, + ) + .unwrap(); + } +} diff --git a/src/main.rs b/src/main.rs index 43df5c4..0678dff 100644 --- a/src/main.rs +++ b/src/main.rs @@ -24,8 +24,7 @@ mod store; mod utils; -#[cfg(feature = "youtrack")] -mod youtrack; +mod integrations; #[derive(Debug)] pub struct AppContext { diff --git a/src/youtrack.rs b/src/youtrack.rs deleted file mode 100644 index 6c6b553..0000000 --- a/src/youtrack.rs +++ /dev/null @@ -1,11 +0,0 @@ -//! Youtrack integration (mark issues as Released when packing to changelog) - -#[cfg(test)] -mod tests { - #[test] - fn test_youtrack_communication() { - let token = ""; - - - } -} From 9810ecc886124adff3243d16fd5049abd8a92dc2 Mon Sep 17 00:00:00 2001 From: ondra Date: Tue, 23 Sep 2025 22:42:45 +0200 Subject: [PATCH 3/3] clpack integration --- .../SW-4712-clpack-youtrack-integration.md | 2 + .../entries/SW-4716-add-cl-status.md | 0 clpack.toml | 40 +++ src/action_log.rs | 2 +- src/action_pack.rs | 31 +- src/assets/config_file_template.toml | 39 +++ src/config.rs | 42 ++- src/git.rs | 31 +- src/integrations/mod.rs | 5 +- src/integrations/youtrack.rs | 319 ++++++++++++------ 10 files changed, 389 insertions(+), 122 deletions(-) create mode 100644 changelog/entries/SW-4712-clpack-youtrack-integration.md rename "changelog/entries/Add \"cl status\".md" => changelog/entries/SW-4716-add-cl-status.md (100%) diff --git a/changelog/entries/SW-4712-clpack-youtrack-integration.md b/changelog/entries/SW-4712-clpack-youtrack-integration.md new file mode 100644 index 0000000..d8413cb --- /dev/null +++ b/changelog/entries/SW-4712-clpack-youtrack-integration.md @@ -0,0 +1,2 @@ +# New features +- Add integration to JetBrains YouTrack (#SW-4712) diff --git "a/changelog/entries/Add \"cl status\".md" b/changelog/entries/SW-4716-add-cl-status.md similarity index 100% rename from "changelog/entries/Add \"cl status\".md" rename to changelog/entries/SW-4716-add-cl-status.md diff --git a/clpack.toml b/clpack.toml index 78faa89..320c99b 100644 --- a/clpack.toml +++ b/clpack.toml @@ -76,3 +76,43 @@ branch_version_pattern = '/^rel\/([\d.]+)$/' # To specify a regex pattern (wildcard name), enclose it in slashes, e.g. '/^release\//' [channels] default = '/^(?:main|master)$/' + +[integrations.youtrack] + +# When creating a release, clpack can mark the included issues +# as "Released" and record the versions into YouTrack. +# +# clpack will ask for confirmation before doing this. +# +# Requirements: +# +# This integration only works if your changelog entry file names (by default taken from branch names) +# contain the issue numbers - e.g. SW-1234-added-stuff.md. If the issue number can't be recognized, +# the entry will be skipped. +# +# You must not combine multiple tickets into one file if you want the numbers to be found +# - the file content is not parsed by this integration. +# +# Each developer who wants to use this integration when packing changelog must set their YouTrack +# API token in an env variable CLPACK_YOUTRACK_TOKEN (in their environment or in an .env file). +# It is also possible to change the server URL by setting CLPACK_YOUTRACK_URL, if needed. + +# Enable the YouTrack integration +enabled = true + +# YouTrack server URL. Can be changed locally by setting env var CLPACK_YOUTRACK_URL +url = "SET IN YOUR ENV" + +# Channels filter - release on those channels will trigger the YouTrack integration +# (i.e. don't mark as Released if it's only in beta) +channels = [ + "default" +] + +# Change the "State" field of the released issues. +# Uncomment to enable, change to fit your project +released_state = "Released" + +# Change a custom version field of the released issues. +# Uncomment to enable, change to fit your project +version_field = "Available in version" diff --git a/src/action_log.rs b/src/action_log.rs index 85bd8d6..4936d0e 100644 --- a/src/action_log.rs +++ b/src/action_log.rs @@ -12,7 +12,7 @@ pub(crate) fn cl_log(ctx: AppContext) -> anyhow::Result<()> { let branch = get_branch_name(&ctx); let issue = branch .as_ref() - .map(|b| b.parse_issue(&ctx)) + .map(|b| b.parse_issue(&ctx.config)) .transpose()? .flatten(); diff --git a/src/action_pack.rs b/src/action_pack.rs index 53af397..3ed5a59 100644 --- a/src/action_pack.rs +++ b/src/action_pack.rs @@ -1,8 +1,12 @@ use crate::AppContext; -use crate::config::ChannelName; +use crate::config::{ChannelName, ENV_YOUTRACK_TOKEN, ENV_YOUTRACK_URL}; use crate::git::{BranchName, get_branch_name}; +use crate::integrations::youtrack::{ + YouTrackClient, youtrack_integration_enabled, youtrack_integration_on_release, +}; use crate::store::{Release, Store}; -use anyhow::bail; +use crate::utils::empty_to_none::EmptyToNone; +use anyhow::{Context, bail}; use colored::Colorize; pub fn pack_resolve_and_show_preview( @@ -50,7 +54,7 @@ fn resolve_channel( None => ( branch .as_ref() - .map(|b| b.parse_channel(ctx)) + .map(|b| b.parse_channel(&ctx.config)) .transpose()? .flatten(), false, @@ -107,7 +111,7 @@ pub(crate) fn cl_pack( // TODO try to get something better from git! let version_base = branch .as_ref() - .map(|b| b.parse_version(&ctx)) + .map(|b| b.parse_version(&ctx.config)) .transpose()? .flatten(); @@ -130,7 +134,7 @@ pub(crate) fn cl_pack( } } - release.version = version; + release.version = version.clone(); if !inquire::Confirm::new("Continue - write to changelog file?") .with_default(true) @@ -140,8 +144,23 @@ pub(crate) fn cl_pack( return Ok(()); } - store.create_release(channel, release)?; + store.create_release(channel.clone(), release.clone())?; println!("{}", "Changelog written.".green()); + + // YouTrack + if youtrack_integration_enabled(&ctx.config, &channel) { + if inquire::Confirm::new("Update released issues in YouTrack?") + .with_default(true) + .prompt()? + { + youtrack_integration_on_release(&ctx.config, release)?; + println!("{}", "YouTrack updated.".green()); + } else { + eprintln!("{}", "YouTrack changes skipped.".yellow()); + return Ok(()); + } + } + Ok(()) } diff --git a/src/assets/config_file_template.toml b/src/assets/config_file_template.toml index 78faa89..339f549 100644 --- a/src/assets/config_file_template.toml +++ b/src/assets/config_file_template.toml @@ -76,3 +76,42 @@ branch_version_pattern = '/^rel\/([\d.]+)$/' # To specify a regex pattern (wildcard name), enclose it in slashes, e.g. '/^release\//' [channels] default = '/^(?:main|master)$/' + +[integrations.youtrack] +# When creating a release, clpack can mark the included issues +# as "Released" and record the versions into YouTrack. +# +# clpack will ask for confirmation before doing this. +# +# Requirements: +# +# This integration only works if your changelog entry file names (by default taken from branch names) +# contain the issue numbers - e.g. SW-1234-added-stuff.md. If the issue number can't be recognized, +# the entry will be skipped. +# +# You must not combine multiple tickets into one file if you want the numbers to be found +# - the file content is not parsed by this integration. +# +# Each developer who wants to use this integration when packing changelog must set their YouTrack +# API token in an env variable CLPACK_YOUTRACK_TOKEN (in their environment or in an .env file). +# It is also possible to change the server URL by setting CLPACK_YOUTRACK_URL, if needed. + +# Enable the YouTrack integration +enabled = false + +# YouTrack server URL. Can be changed locally by setting env var CLPACK_YOUTRACK_URL +url = "https://example.youtrack.cloud" + +# Channels filter - release on those channels will trigger the YouTrack integration +# (i.e. don't mark as Released if it's only in beta) +channels = [ + "default" +] + +# Change the "State" field of the released issues. +# Uncomment to enable, change to fit your project +#released_state = "Released" + +# Change a custom version field of the released issues. +# Uncomment to enable, change to fit your project +#version_field = "Available in version" diff --git a/src/config.rs b/src/config.rs index dc77f27..77a2226 100644 --- a/src/config.rs +++ b/src/config.rs @@ -11,11 +11,15 @@ pub type VersionName = String; /// e.g. SW-1234-stuff-is-broken (without .md) pub type EntryName = String; +/// Config file with nice comments pub const CONFIG_FILE_TEMPLATE: &str = include_str!("assets/config_file_template.toml"); -pub const ENV_YOUTRACK_URL : &str = "CLPACK_YOUTRACK_URL"; +/// ENV / dotenv key for the youtrack integration server URL +/// This is only for unit tests +pub const ENV_YOUTRACK_URL: &str = "CLPACK_YOUTRACK_URL"; -pub const ENV_YOUTRACK_TOKEN : &str = "CLPACK_YOUTRACK_TOKEN"; +/// ENV / dotenv key for the youtrack integration API token +pub const ENV_YOUTRACK_TOKEN: &str = "CLPACK_YOUTRACK_TOKEN"; #[cfg(test)] #[test] @@ -107,4 +111,38 @@ pub struct Config { /// TODO attempt to parse version from package.json, composer.json, Cargo.toml and others #[default(Some(r"/^rel\/([\d.]+)$/".to_string()))] pub branch_version_pattern: Option, + + /// Integrations config + pub integrations: IntegrationsConfig, +} + +/// Integrations config +#[derive(Debug, Serialize, Deserialize, SmartDefault, PartialEq, Clone)] +#[serde(deny_unknown_fields, default)] +pub struct IntegrationsConfig { + /// YouTrack integration + pub youtrack: YouTrackIntegrationConfig, +} + +#[derive(Debug, Serialize, Deserialize, SmartDefault, PartialEq, Clone)] +#[serde(deny_unknown_fields, default)] +pub struct YouTrackIntegrationConfig { + /// Enable the integration + pub enabled: bool, + + /// URL of the youtrack server (just https://domain) + #[default = "https://example.youtrack.cloud"] + pub url: String, + + /// Channels filter + #[default(vec![ + "default".to_string(), + ])] + pub channels: Vec, + + /// Name of the State option to switch to when generating changelog (e.g. Released) + pub released_state: Option, + + /// Name of the version field (Available in version) + pub version_field: Option, } diff --git a/src/git.rs b/src/git.rs index b6ba08c..d6c6ba6 100644 --- a/src/git.rs +++ b/src/git.rs @@ -1,4 +1,5 @@ use crate::AppContext; +use crate::config::Config; use crate::utils::empty_to_none::EmptyToNone; use anyhow::bail; use std::fmt::Display; @@ -79,8 +80,8 @@ impl BranchName { /// Parse version from this branch name. /// /// Aborts if the configured regex pattern is invalid. - pub fn parse_version(&self, ctx: &AppContext) -> anyhow::Result> { - let Some(pat) = ctx.config.branch_version_pattern.as_ref().empty_to_none() else { + pub fn parse_version(&self, config: &Config) -> anyhow::Result> { + let Some(pat) = config.branch_version_pattern.as_ref().empty_to_none() else { return Ok(None); }; self.parse_using_regex(pat, "branch_version_pattern") @@ -89,16 +90,16 @@ impl BranchName { /// Parse issue number from this branch name. /// /// Aborts if the configured regex pattern is invalid. - pub fn parse_issue(&self, ctx: &AppContext) -> anyhow::Result> { - let Some(pat) = ctx.config.branch_issue_pattern.as_ref().empty_to_none() else { + pub fn parse_issue(&self, config: &Config) -> anyhow::Result> { + let Some(pat) = config.branch_issue_pattern.as_ref().empty_to_none() else { return Ok(None); }; self.parse_using_regex(pat, "branch_issue_pattern") } /// Try to detect a release channel from this branch name (e.g. stable, EAP) - pub fn parse_channel(&self, ctx: &AppContext) -> anyhow::Result> { - for (channel_id, template) in &ctx.config.channels { + pub fn parse_channel(&self, config: &Config) -> anyhow::Result> { + for (channel_id, template) in &config.channels { if template.is_empty() { // Channel only for manual choosing continue; @@ -165,14 +166,14 @@ mod test { assert_eq!( BranchName("rel/3.14".to_string()) - .parse_version(&ctx) + .parse_version(&ctx.config) .unwrap(), Some("3.14".to_string()) ); assert_eq!( BranchName("rel/foo".to_string()) - .parse_version(&ctx) + .parse_version(&ctx.config) .unwrap(), None ); @@ -188,21 +189,21 @@ mod test { assert_eq!( BranchName("1234-bober-kurwa".to_string()) - .parse_issue(&ctx) + .parse_issue(&ctx.config) .unwrap(), Some("1234".to_string()) ); assert_eq!( BranchName("SW-778-jakie-bydłe-jebane".to_string()) - .parse_issue(&ctx) + .parse_issue(&ctx.config) .unwrap(), Some("SW-778".to_string()) ); assert_eq!( BranchName("nie-spierdalaj-mordo".to_string()) - .parse_issue(&ctx) + .parse_issue(&ctx.config) .unwrap(), None ); @@ -217,20 +218,22 @@ mod test { }; assert_eq!( - BranchName("main".to_string()).parse_channel(&ctx).unwrap(), + BranchName("main".to_string()) + .parse_channel(&ctx.config) + .unwrap(), Some("default".to_string()) ); assert_eq!( BranchName("master".to_string()) - .parse_channel(&ctx) + .parse_channel(&ctx.config) .unwrap(), Some("default".to_string()) ); assert_eq!( BranchName("my-cool-feature".to_string()) - .parse_version(&ctx) + .parse_version(&ctx.config) .unwrap(), None ); diff --git a/src/integrations/mod.rs b/src/integrations/mod.rs index 6cafcde..ea7c22d 100644 --- a/src/integrations/mod.rs +++ b/src/integrations/mod.rs @@ -1,3 +1,2 @@ -mod youtrack; - -pub use youtrack::YouTrackClient; +/// Third party service (e.g. issue trackers) integrations +pub mod youtrack; diff --git a/src/integrations/youtrack.rs b/src/integrations/youtrack.rs index 518af5c..75332f1 100644 --- a/src/integrations/youtrack.rs +++ b/src/integrations/youtrack.rs @@ -1,48 +1,152 @@ -//! Youtrack integration (mark issues as Released when packing to changelog) +//! Youtrack integration (mark issues as Released when packing to changelog, change Available in version) -use crate::config::VersionName; -use anyhow::bail; +use crate::config::{ChannelName, ENV_YOUTRACK_TOKEN, ENV_YOUTRACK_URL, VersionName}; +use crate::git::BranchName; +use crate::store::Release; +use anyhow::{Context, bail}; use chrono::{DateTime, Utc}; -use json_dotpath::DotPaths; use log::debug; use reqwest::header::{HeaderMap, HeaderValue}; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use serde_json::Value; -use std::borrow::Cow; +/// ID of a youtrack project type ProjectId = String; -type BundleID = String; -type FieldID = String; +pub fn youtrack_integration_enabled(config: &crate::Config, channel: &ChannelName) -> bool { + let ytconf = &config.integrations.youtrack; + ytconf.enabled + // Channel filter + && ytconf.channels.contains(&channel) + // URL is required + && (!ytconf.url.is_empty() || dotenv::var(ENV_YOUTRACK_URL).is_ok_and(|v| !v.is_empty())) + // Token is required + && dotenv::var(ENV_YOUTRACK_TOKEN).is_ok_and(|v| !v.is_empty()) + // Check if we have something to do + && (ytconf.version_field.as_ref().is_some_and(|v| !v.is_empty()) + || ytconf + .released_state + .as_ref() + .is_some_and(|v| !v.is_empty())) +} + +pub fn youtrack_integration_on_release( + config: &crate::Config, + release: Release, +) -> anyhow::Result<()> { + let ytconf = &config.integrations.youtrack; + let url = dotenv::var(ENV_YOUTRACK_URL) + .ok() + .unwrap_or_else(|| ytconf.url.clone()); + + if url.is_empty() { + bail!("YouTrack URL is empty!"); + } + let token = dotenv::var(ENV_YOUTRACK_TOKEN).context("Error getting YouTrack token")?; + + if token.is_empty() { + bail!("YouTrack token is empty!"); + } + + let client = YouTrackClient::new(url, &token)?; + + let mut project_id_opt = None; + + let date = chrono::Utc::now(); + for entry in release.entries { + let branch_name = BranchName(entry); + let Ok(Some(issue_num)) = branch_name.parse_issue(config) else { + eprintln!("No issue number recognized in {}", branch_name.0); + continue; + }; + + // Assume all tickets belong to the same project + + if project_id_opt.is_none() { + match client.find_project_id(&issue_num) { + Ok(project_id) => { + project_id_opt = Some(project_id); + } + Err(e) => { + eprintln!("Failed to find project number from {issue_num}: {e}"); + continue; + } + } + } + + let project_id = project_id_opt.as_ref().unwrap(); // We know it is set now + + let mut set_version_opt = None; + if let Some(field) = &ytconf.version_field { + let set_version = SetVersion { + field_name: field, + version: &release.version, + }; + + client.ensure_version_exists_in_project(&project_id, &set_version, Some(date))?; + + set_version_opt = Some(set_version); + } + + println!("Update issue {issue_num} ({}) in YouTrack", branch_name.0); + client.set_issue_version_and_state_by_name( + &issue_num, + set_version_opt.as_ref(), + ytconf.released_state.as_deref(), + )?; + } + + Ok(()) +} + +/// YouTrack API client (with only the bare minimum of the API implemented to satisfy clpack's needs) pub struct YouTrackClient { + /// HTTPS client with default presets to access the API client: reqwest::blocking::Client, - pub url: String, + /// Base URL of the API server + url: String, } +/// Error received from the API instead of the normal response #[derive(Deserialize)] struct YoutrackErrorResponse { + /// Error ID error: String, + /// Error message error_description: String, } impl YouTrackClient { + /// Create a YouTrack client + /// + /// url - API server base URL (e.g. https://mycompany.youtrack.cloud) + /// token - JWT-like token, starts with "perm-". Obtained from YouTrack profile settings pub fn new(url: impl ToString, token: &str) -> anyhow::Result { - let token_bearer = format!("Bearer {token}"); + let token_bearer = format!("Bearer {token}"); // 🐻 - let mut hm = HeaderMap::new(); - hm.insert("Authorization", HeaderValue::from_str(&token_bearer)?); - hm.insert("Content-Type", HeaderValue::from_str("application/json")?); - hm.insert("Accept", HeaderValue::from_str("application/json")?); + let mut headers = HeaderMap::new(); + headers.insert("Authorization", HeaderValue::from_str(&token_bearer)?); + headers.insert("Content-Type", HeaderValue::from_str("application/json")?); + headers.insert("Accept", HeaderValue::from_str("application/json")?); Ok(YouTrackClient { url: url.to_string(), client: reqwest::blocking::Client::builder() - .default_headers(hm) + .default_headers(headers) .build()?, }) } + fn parse_youtrack_error_response(payload: &str) -> anyhow::Error { + if let Ok(e) = serde_json::from_str::(&payload) { + anyhow::format_err!("Error from YouTrack: {} - {}", e.error, e.error_description) + } else { + anyhow::format_err!("Error from YouTrack (unknown response format): {payload}") + } + } + + /// Send a GET request with query parameters. Deserialize response. fn get_json( &self, api_path: String, @@ -57,18 +161,19 @@ impl YouTrackClient { debug!("GET {}", url); let response = self.client.get(&url).query(query).send()?; - + let is_ok = response.status().is_success(); let response_text = response.text()?; debug!("Resp = {}", response_text); - if let Ok(e) = serde_json::from_str::(&response_text) { - bail!("Error from YouTrack: {} - {}", e.error, e.error_description); + if !is_ok { + return Err(Self::parse_youtrack_error_response(&response_text)); } Ok(serde_json::from_str(&response_text)?) } + /// Send a POST request with query parameters and serializable (JSON) body. Deserialize response. fn post_json( &self, api_path: String, @@ -91,18 +196,20 @@ impl YouTrackClient { .body(body_serialized.into_bytes()) .send()?; + let is_ok = response.status().is_success(); let response_text = response.text()?; debug!("Resp = {}", response_text); - if let Ok(e) = serde_json::from_str::(&response_text) { - bail!("Error from YouTrack: {} - {}", e.error, e.error_description); + if !is_ok { + return Err(Self::parse_youtrack_error_response(&response_text)); } Ok(serde_json::from_str(&response_text)?) } - pub fn find_project_id(&self, issue: &str) -> anyhow::Result { + /// Find YouTrack project ID from an issue name + pub fn find_project_id(&self, issue_name: &str) -> anyhow::Result { #[derive(Deserialize)] struct Issue { project: Project, @@ -114,7 +221,7 @@ impl YouTrackClient { } let issue: Issue = - self.get_json(format!("issues/{issue}"), &[("fields", "project(id)")])?; + self.get_json(format!("issues/{issue_name}"), &[("fields", "project(id)")])?; // example: // {"project":{"id":"0-172","$type":"Project"},"$type":"Issue"} @@ -125,27 +232,30 @@ impl YouTrackClient { /// Try to find a version by name in a YouTrack project. /// If it is not found but we find the field where to add it, the version will be created. /// - /// project_id - obtained by `find_project_id()` - /// field_name - name of the version field, e.g. "Available in version" - /// version_to_create - name of the version to find or create - /// release_date - if creating, a date YYYY-MM-DD can be passed here. It will be stored into the - /// newly created version & this marked as released. + /// - project_id - obtained by `find_project_id()` + /// - version_info - version name and field name + /// - release_date - if creating, a date YYYY-MM-DD can be passed here. It will be stored into the + /// newly created version & it will be marked as released. pub fn ensure_version_exists_in_project( &self, project_id: &str, - field_name: &str, - version_to_create: &str, + version_info: &SetVersion, release_date: Option>, ) -> anyhow::Result<()> { + type BundleID = String; + type FieldID = String; + #[derive(Deserialize)] struct BudleDescription { id: BundleID, } + #[derive(Deserialize)] struct FieldDescription { name: String, id: FieldID, } + #[derive(Deserialize)] struct YTCustomField { // Bundle is sometimes missing - we skip these entries @@ -159,9 +269,10 @@ impl YouTrackClient { &[("fields", "field(name,id),bundle(id)"), ("top", "200")], )?; + // Find the field we want in the list (XXX this can probably be done with some API query?) let mut field_bundle = None; for entry in fields { - if &entry.field.name == field_name + if &entry.field.name == version_info.field_name && let Some(bundle) = entry.bundle { field_bundle = Some((entry.field.id, bundle.id)); @@ -169,8 +280,12 @@ impl YouTrackClient { } } + // Got something? let Some((field_id, bundle_id)) = field_bundle else { - bail!("YouTrack version field {field_name} not found in the project {project_id}"); + bail!( + "YouTrack version field {field_name} not found in the project {project_id}", + field_name = version_info.field_name + ); }; println!("Found YouTrack version field, checking defined versions"); @@ -181,60 +296,46 @@ impl YouTrackClient { id: String, } + // Look at options already defined on the field let versions: Vec = self.get_json( format!("admin/customFieldSettings/bundles/version/{bundle_id}/values"), &[("fields", "id,name"), ("top", "500")], )?; - // Find the version we want - for version in versions { - if &version.name == version_to_create { - eprintln!("Version {version_to_create} already exists in YouTrack"); - return Ok(()); - } + // Is our version defined? + if versions.iter().any(|v| v.name == version_info.version) { + eprintln!( + "Version {v} already exists in YouTrack", + v = version_info.version + ); + return Ok(()); } - println!("Creating version in YouTrack: {version_to_create}"); - - /* - $body = ['name' => $name]; - if ($released !== null) { - $body['released'] = $released; - } - if ($releaseDate !== null) { - $body['releaseDate'] = $releaseDate; - } - if ($archived !== null) { - $body['archived'] = $archived; - } - - return $this->postJson( - "admin/customFieldSettings/bundles/version/$bundleId/values", - $body, - ['fields' => 'id,name,released,releaseDate,archived'], - ); - */ + println!( + "Creating version in YouTrack: {v}", + v = version_info.version + ); #[derive(Serialize)] + #[allow(non_snake_case)] struct CreateVersionBody { name: String, archived: bool, released: bool, - #[allow(non_snake_case)] releaseDate: Option, // archived } - let body = CreateVersionBody { - name: version_to_create.to_string(), + let request_body = CreateVersionBody { + name: version_info.version.to_string(), archived: false, released: release_date.is_some(), releaseDate: release_date.map(|d| d.timestamp()), }; #[derive(Deserialize, Debug)] + #[allow(non_snake_case)] struct CreateVersionResponse { - #[allow(non_snake_case)] releaseDate: Option, released: bool, archived: bool, @@ -244,7 +345,7 @@ impl YouTrackClient { let resp: CreateVersionResponse = self.post_json( format!("admin/customFieldSettings/bundles/version/{bundle_id}/values"), - &body, + &request_body, &[("fields", "id,name,released,releaseDate,archived")], )?; @@ -253,21 +354,28 @@ impl YouTrackClient { // {"releaseDate":1758619201,"released":true,"archived":false,"name":"TEST2","id":"232-358","$type":"VersionBundleElement"} debug!("Created version entry = {:#?}", resp); - println!("Version {version_to_create} created in YouTrack."); + println!("Version {v} created in YouTrack.", v = version_info.version); Ok(()) } - pub fn set_issue_version_and_state( + /// Modify a YouTrack issue by changing its State and setting "Available in version". + /// + /// Before calling this, make sure the version exists, e.g. using `ensure_version_exists_in_project` + /// + /// - issue_id - e.g. SW-1234 + /// - version_field_name - name of the YT custom field to modify + /// - version_name - name of the version, e.g. 1.0.0 + /// - target_state_name name of the State to switch the issue to (None for no-op) + pub fn set_issue_version_and_state_by_name( &self, issue_id: &str, - version_field_name: &str, - version_name: &str, - target_state_name: &str, + version: Option<&SetVersion>, + state: Option<&str>, ) -> anyhow::Result<()> { #[derive(Serialize)] + #[allow(non_snake_case)] struct PatchIssueBody { - #[allow(non_snake_case)] customFields: Vec, } @@ -284,23 +392,35 @@ impl YouTrackClient { value: EnumValue, } - let body = PatchIssueBody { - customFields: vec![ - CustomFieldValue { - name: version_field_name.to_string(), - field_type: "SingleVersionIssueCustomField".to_string(), - value: EnumValue { - name: version_name.to_string(), - }, + let mut custom_fields = Vec::new(); + + if let Some(version) = version { + custom_fields.push(CustomFieldValue { + name: version.field_name.to_string(), + field_type: "SingleVersionIssueCustomField".to_string(), + value: EnumValue { + name: version.version.to_string(), }, - CustomFieldValue { - name: "State".to_string(), - field_type: "StateIssueCustomField".to_string(), - value: EnumValue { - name: target_state_name.to_string(), - }, + }); + } + + if let Some(target_state_name) = state { + custom_fields.push(CustomFieldValue { + name: "State".to_string(), + field_type: "StateIssueCustomField".to_string(), + value: EnumValue { + name: target_state_name.to_string(), }, - ], + }); + } + + if custom_fields.is_empty() { + eprintln!("Nothing to do in YouTrack - no version field, no target state."); + return Ok(()); + } + + let body = PatchIssueBody { + customFields: custom_fields, }; let resp: Value = self.post_json( @@ -309,7 +429,7 @@ impl YouTrackClient { &[("fields", "id,customFields(name,value(name))")], )?; - // TODO? Do something with the fields + // Do something with the requested fields? // Example success: // {"customFields":[{"value":null,"name":"Type","$type":"SingleEnumIssueCustomField"},{"value":{"name":"Released","$type":"StateBundleElement"},"name":"State","$type":"StateIssueCustomField"},{"value":null,"name":"Assignee","$type":"SingleUserIssueCustomField"},{"value":null,"name":"Priority","$type":"SingleEnumIssueCustomField"},{"value":{"name":"Internal tooling","$type":"EnumBundleElement"},"name":"Category","$type":"SingleEnumIssueCustomField"},{"value":[],"name":"Customer","$type":"MultiEnumIssueCustomField"},{"value":null,"name":"Customer Funding","$type":"SingleEnumIssueCustomField"},{"value":null,"name":"Product Stream","$type":"SingleEnumIssueCustomField"},{"value":null,"name":"Estimation","$type":"PeriodIssueCustomField"},{"value":{"$type":"PeriodValue"},"name":"Spent time","$type":"PeriodIssueCustomField"},{"value":null,"name":"Due Date","$type":"DateIssueCustomField"},{"value":[],"name":"Affected version","$type":"MultiVersionIssueCustomField"},{"value":{"name":"TEST2","$type":"VersionBundleElement"},"name":"Available in version","$type":"SingleVersionIssueCustomField"},{"value":null,"name":"SlackAlertSent","$type":"SimpleIssueCustomField"},{"value":13.0,"name":"Dev costs","$type":"SimpleIssueCustomField"}],"id":"2-25820","$type":"Issue"} @@ -321,12 +441,20 @@ impl YouTrackClient { } } +/// Params for YT to change version field +#[derive(Clone)] +pub struct SetVersion<'a> { + /// Field name, e.g. Available in version + pub field_name: &'a str, + /// Version name, e.g. 1.0.0 + pub version: &'a str, +} + #[cfg(test)] mod tests { - use super::YouTrackClient; + use super::{SetVersion, YouTrackClient}; use crate::config::{ENV_YOUTRACK_TOKEN, ENV_YOUTRACK_URL}; use log::{LevelFilter, debug}; - use serde_json::Value; // #[test] // Disabled fn test_youtrack_communication() { @@ -341,7 +469,12 @@ mod tests { let target_state_name = "Released"; let version_name = "TEST2"; - let mut client = YouTrackClient::new(url, &token).unwrap(); + let set_version = SetVersion { + field_name: version_field_name, + version: version_name, + }; + + let client = YouTrackClient::new(url, &token).unwrap(); let project_id = client.find_project_id(issue_id).unwrap(); @@ -350,20 +483,14 @@ mod tests { let date = chrono::Utc::now(); client - .ensure_version_exists_in_project( - &project_id, - version_field_name, - version_name, - Some(date), - ) + .ensure_version_exists_in_project(&project_id, &set_version, Some(date)) .unwrap(); client - .set_issue_version_and_state( + .set_issue_version_and_state_by_name( issue_id, - version_field_name, - version_name, - target_state_name, + Some(&set_version), + Some(target_state_name), ) .unwrap(); }