From fa9f338f9b3f87ef0bcd2f36ac7d00567692d454 Mon Sep 17 00:00:00 2001 From: Pau Minguet Date: Wed, 7 Oct 2026 14:16:51 +0200 Subject: [PATCH 01/11] fix: change rust version and add gix --- backend/Cargo.lock | 3102 ++++++++++++++++++++++++++++++-- backend/Cargo.toml | 2 +- backend/crates/core/Cargo.toml | 1 + 3 files changed, 2999 insertions(+), 106 deletions(-) diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 94186abdf..0a8ad6631 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -11,191 +11,2524 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "arc-swap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" +dependencies = [ + "rustversion", +] + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "aws-lc-rs" +version = "1.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b281d307588d634de920874890732659e2e7672f72b5e10e81badc1a8a83621e" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bff6c3b54fad79a2e60b8102caf565819711497c1f5f092f49508e2f5c31b27" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", + "pkg-config", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bstr" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb31b46c14244e20ee9984b11bf5c992b91fb6939fea616e3512c8baecdbe5f" +dependencies = [ + "memchr", + "regex-automata", + "serde_core", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "bytesize" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7354288c522e7e980fafd2075d63d1285794c3a6a16cdd492f189ea406e5f18b" + +[[package]] +name = "cc" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f74872d07caf508b30a21f6836e7d7016a2eaf7d9ff4f48deaa58cd8a0407630" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "rand_core", +] + +[[package]] +name = "clru" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "197fd99cb113a8d5d9b6376f3aa817f32c1078f2343b714fff7d2ca44fdf67d5" +dependencies = [ + "hashbrown 0.16.1", +] + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "combine" +version = "4.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core_detect" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "dashmap" +version = "6.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "defmt" +version = "0.3.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0963443817029b2024136fc4dd07a5107eb8f977eaf18fcd1fdeb11306b64ad" +dependencies = [ + "defmt 1.1.1", +] + +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "encoding_rs" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e985e0451871ad22fb8d2b6b076e2028a502a0d3950998c2c5c0a4f9b5d9679" +dependencies = [ + "cfg-if", + "core_detect", + "multiversion_no_op", + "rustversion", + "scopeguard", + "simdutf8", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "faster-hex" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04839bdf9d8c10f66806fad16b852fc72aab80873aebc3cb69d85b4fa41543ed" +dependencies = [ + "autocfg", + "defmt 0.3.100", + "heapless", + "serde", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[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 = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "rand_core", + "wasm-bindgen", +] + +[[package]] +name = "gix" +version = "0.87.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdefc1465d8631807deaf504dacdeff628b978de515653b47976ca7c28ab2a7a" +dependencies = [ + "gix-actor", + "gix-archive", + "gix-attributes", + "gix-blame", + "gix-command", + "gix-commitgraph", + "gix-config", + "gix-credentials", + "gix-date", + "gix-diff", + "gix-dir", + "gix-discover", + "gix-error", + "gix-features", + "gix-filter", + "gix-fs", + "gix-glob", + "gix-hash", + "gix-hashtable", + "gix-ignore", + "gix-index", + "gix-lock", + "gix-mailmap", + "gix-merge", + "gix-negotiate", + "gix-note", + "gix-object", + "gix-odb", + "gix-pack", + "gix-path", + "gix-pathspec", + "gix-prompt", + "gix-protocol", + "gix-quote", + "gix-ref", + "gix-refspec", + "gix-revision", + "gix-revwalk", + "gix-sec", + "gix-shallow", + "gix-status", + "gix-submodule", + "gix-tempfile", + "gix-trace", + "gix-transport", + "gix-traverse", + "gix-url", + "gix-utils", + "gix-validate", + "gix-worktree", + "gix-worktree-state", + "gix-worktree-stream", + "gix-zlib", + "nonempty", + "parking_lot", + "regex", + "signal-hook", + "smallvec", + "thiserror", +] + +[[package]] +name = "gix-actor" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e37efa99929ac62f980fb1a7dcd09dea85b3aa6ead6ba0498362add3f4e698de" +dependencies = [ + "bstr", + "gix-date", + "gix-error", +] + +[[package]] +name = "gix-archive" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c4ef97efd3920d3525104c5f253dec94dc935f953a50055954939177aed2cdd" +dependencies = [ + "bstr", + "gix-date", + "gix-error", + "gix-object", + "gix-worktree-stream", +] + +[[package]] +name = "gix-attributes" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76b83782ac69ae28921a6e8fbcf2e24069613f1518030b97ae622b079abffa54" +dependencies = [ + "bstr", + "gix-features", + "gix-glob", + "gix-path", + "gix-quote", + "gix-trace", + "smallvec", + "thiserror", + "unicode-bom", +] + +[[package]] +name = "gix-bitmap" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17013c7ef5cb95ebb4bf4978201e6a8a42ffea9d666fd5388f1b7e742307d80" +dependencies = [ + "gix-error", +] + +[[package]] +name = "gix-blame" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54fe27f8037c58cb4b6d60fe0aacf7ee2c998633cedf4e989351c07d9a3a575d" +dependencies = [ + "gix-commitgraph", + "gix-date", + "gix-diff", + "gix-error", + "gix-hash", + "gix-object", + "gix-revwalk", + "gix-trace", + "gix-traverse", + "gix-worktree", + "smallvec", + "thiserror", +] + +[[package]] +name = "gix-chunk" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00e32f938b3745ac93d73bc7490b6a15a661b2fc81973bfe90e275cc53c908e1" +dependencies = [ + "gix-error", +] + +[[package]] +name = "gix-command" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa00adbd2c77f584cfe41f348772d2f131af1fa519a7f6b98638f1144ead96a4" +dependencies = [ + "bstr", + "gix-path", + "gix-quote", + "gix-trace", +] + +[[package]] +name = "gix-commitgraph" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b93c9fb1f5be01bba54a7a3b7455d359efc68555539c75ef74b8021bb6db497" +dependencies = [ + "bstr", + "gix-chunk", + "gix-error", + "gix-hash", + "memmap2", + "nonempty", +] + +[[package]] +name = "gix-config" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f973c28c0a4871a7926fc90c8377d4458fd6cbb19692f56582b4d2022313ae90" +dependencies = [ + "bstr", + "gix-config-value", + "gix-features", + "gix-glob", + "gix-path", + "gix-ref", + "gix-sec", + "gix-utils", + "smallvec", + "thiserror", + "unicode-bom", +] + +[[package]] +name = "gix-config-value" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f6af5321bfd3711a279d6b244d58532ba1cfabf9eb6374791f19929d8970082" +dependencies = [ + "bitflags 2.13.2", + "bstr", + "gix-path", + "libc", + "thiserror", +] + +[[package]] +name = "gix-credentials" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16c11822054fd4b80a1ab722b703f2e7c691b20484d898374289b742c1f49e50" +dependencies = [ + "bstr", + "gix-command", + "gix-config-value", + "gix-date", + "gix-path", + "gix-prompt", + "gix-quote", + "gix-sec", + "gix-trace", + "gix-url", + "thiserror", +] + +[[package]] +name = "gix-date" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e63d9aa18f29facd571c8953e66224ee0075b9e16622024794555ed4acceea85" +dependencies = [ + "bstr", + "gix-error", + "itoa", + "jiff", +] + +[[package]] +name = "gix-diff" +version = "0.67.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1689ff5ddeee4acfb2a43e875a1072a76d208624b15a9065c938b39cb0da2a" +dependencies = [ + "bstr", + "gix-attributes", + "gix-command", + "gix-filter", + "gix-fs", + "gix-hash", + "gix-imara-diff", + "gix-index", + "gix-object", + "gix-path", + "gix-pathspec", + "gix-tempfile", + "gix-trace", + "gix-traverse", + "gix-worktree", + "thiserror", +] + +[[package]] +name = "gix-dir" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8342d5eb0ea054ed6ef807e628e38f06dd51eceeec9529767d8b23e33eff9c09" +dependencies = [ + "bstr", + "gix-discover", + "gix-fs", + "gix-ignore", + "gix-index", + "gix-object", + "gix-path", + "gix-pathspec", + "gix-trace", + "gix-utils", + "gix-worktree", + "thiserror", +] + +[[package]] +name = "gix-discover" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec32a30ec2934735599c2545f30067e80bd255fd3454b52a5d59bc02b52874a3" +dependencies = [ + "bstr", + "dunce", + "gix-fs", + "gix-path", + "gix-ref", + "gix-sec", + "thiserror", +] + +[[package]] +name = "gix-error" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a1ba536602db507119eb5f3cdf80bef3ca5a5ec0dfc6e9c7f46dc70ade7c112" +dependencies = [ + "bstr", +] + +[[package]] +name = "gix-features" +version = "0.49.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39c0e59d9d253dcccc38c3a46b91bfb9b46bd63eed54fe1a719e12194884d52a" +dependencies = [ + "bytes", + "bytesize", + "crc32fast", + "crossbeam-channel", + "gix-path", + "gix-trace", + "gix-utils", + "libc", + "once_cell", + "parking_lot", + "prodash", + "walkdir", +] + +[[package]] +name = "gix-filter" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c30b28d957e2e6f1e1bbfbfd26b8e0bb0aab68d8a5e730fb4fd3049943575e3" +dependencies = [ + "bstr", + "encoding_rs", + "gix-attributes", + "gix-command", + "gix-hash", + "gix-object", + "gix-packetline", + "gix-path", + "gix-quote", + "gix-trace", + "gix-utils", + "smallvec", + "thiserror", +] + +[[package]] +name = "gix-fs" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebcfa9fd253f25350a3b21b3dd74034a446098e373c6123d4cee3519894f12ef" +dependencies = [ + "bstr", + "gix-features", + "gix-path", + "gix-utils", + "thiserror", +] + +[[package]] +name = "gix-glob" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b417cf515fd8c91468b578071f76d6cba716f8a1eccd853906bff4908b2c1413" +dependencies = [ + "bitflags 2.13.2", + "bstr", + "gix-features", + "gix-path", +] + +[[package]] +name = "gix-hash" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "380b9c423a54f0821064b954b5a5ab25c660812f6a91da03c3f1cb4b10762aa5" +dependencies = [ + "faster-hex", + "gix-features", + "sha1-checked", + "thiserror", +] + +[[package]] +name = "gix-hashtable" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78fccd6fea3bcf0b39c076bae60ae49b08daaf538b950202101a981f9d3c01d3" +dependencies = [ + "gix-hash", + "hashbrown 0.17.1", + "parking_lot", +] + +[[package]] +name = "gix-ignore" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65859a2f7de5e159d4344a5486ebf53b6bac22d5ec6afa836e47c10ae88a7f0f" +dependencies = [ + "bstr", + "gix-glob", + "gix-path", + "gix-trace", + "unicode-bom", +] + +[[package]] +name = "gix-imara-diff" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c91d8cffac8849493a82233811bd02b2b183b8cf39bf704de0fa0841b737595" +dependencies = [ + "bstr", + "hashbrown 0.17.1", +] + +[[package]] +name = "gix-index" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "632e16cb48b0e88a747e106924cb2765194105d8070a7a961b0d080ed806c632" +dependencies = [ + "bitflags 2.13.2", + "bstr", + "filetime", + "fnv", + "gix-bitmap", + "gix-features", + "gix-fs", + "gix-hash", + "gix-lock", + "gix-object", + "gix-traverse", + "gix-utils", + "gix-validate", + "hashbrown 0.17.1", + "itoa", + "libc", + "memmap2", + "rustix", + "smallvec", + "thiserror", +] + +[[package]] +name = "gix-lock" +version = "24.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c69157820343bf1c6e4b88b9808e920900de02e18aaf5862b30ada43814848" +dependencies = [ + "gix-tempfile", + "gix-utils", + "thiserror", +] + +[[package]] +name = "gix-macros" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3836b4b051393464a753c5a08fe19c7ce0d8b77574a92bd558972941d4553cb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "gix-mailmap" +version = "0.34.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a1c6ca2f9481e5e98b3600b859bc595d46cdf3b600709d5f91b8c118ae452a1" +dependencies = [ + "bstr", + "gix-actor", + "gix-date", + "gix-error", +] + +[[package]] +name = "gix-merge" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72fad4279c940a0fa460ffdf3a3c0e48b7ca5ccb2b08f51a02b22ffcd1efb900" +dependencies = [ + "bstr", + "gix-command", + "gix-diff", + "gix-filter", + "gix-fs", + "gix-hash", + "gix-imara-diff", + "gix-index", + "gix-object", + "gix-path", + "gix-quote", + "gix-revision", + "gix-revwalk", + "gix-tempfile", + "gix-trace", + "gix-worktree", + "nonempty", + "thiserror", +] + +[[package]] +name = "gix-negotiate" +version = "0.35.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91068ecee956121d663635b873ae87ad9c5d3f2d6788facfd12829140b39c897" +dependencies = [ + "bitflags 2.13.2", + "gix-commitgraph", + "gix-date", + "gix-hash", + "gix-object", + "gix-revwalk", +] + +[[package]] +name = "gix-note" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9af6bbdb3f3c62b4920407c479af1cc0beb27fd75a762782d48baed53d330958" +dependencies = [ + "gix-error", + "gix-hash", + "gix-hashtable", + "gix-object", +] + +[[package]] +name = "gix-object" +version = "0.64.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56fef799ca40cdeab4de5a3e74a696d8fa9b9c6264592646bf022e026f13ce2c" +dependencies = [ + "bstr", + "gix-actor", + "gix-command", + "gix-date", + "gix-features", + "gix-hash", + "gix-hashtable", + "gix-tempfile", + "gix-utils", + "gix-validate", + "itoa", + "smallvec", + "thiserror", +] + +[[package]] +name = "gix-odb" +version = "0.84.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf45d195b71cb6363886e7b466821bf4dffd7aba1b6b814ff75488e0061d72a7" +dependencies = [ + "arc-swap", + "gix-features", + "gix-fs", + "gix-hash", + "gix-hashtable", + "gix-object", + "gix-pack", + "gix-path", + "gix-quote", + "gix-zlib", + "memmap2", + "parking_lot", + "tempfile", + "thiserror", +] + +[[package]] +name = "gix-pack" +version = "0.74.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d199d515cbdcf05f531f3859be9771fc12acac7b7fe0f5b581b2c1ab1c3a61e" +dependencies = [ + "clru", + "crossbeam-deque", + "gix-chunk", + "gix-error", + "gix-features", + "gix-hash", + "gix-hashtable", + "gix-object", + "gix-path", + "gix-tempfile", + "gix-zlib", + "memmap2", + "parking_lot", + "smallvec", + "thiserror", + "uluru", +] + +[[package]] +name = "gix-packetline" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "792bd92bea390087e6223b18d4d023decc36467de31e6b066b69b94beea1b9e2" +dependencies = [ + "bstr", + "faster-hex", + "gix-trace", + "thiserror", +] + +[[package]] +name = "gix-path" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38fc6f029ea67de83cbcbd33fd98c05a48360d2932b39d9fdacbc6eae802d475" +dependencies = [ + "bstr", + "gix-trace", + "gix-validate", + "thiserror", +] + +[[package]] +name = "gix-pathspec" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92e44c63ba53bb55aa88ee48847664beae9446417621582fb14dfc69b2f8c72" +dependencies = [ + "bitflags 2.13.2", + "bstr", + "gix-attributes", + "gix-config-value", + "gix-glob", + "gix-path", + "thiserror", +] + +[[package]] +name = "gix-prompt" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "802256e13ecb2133ba5f9e6acbaa6b08bac027b1c5929f85f6adbb56ba3a4f28" +dependencies = [ + "gix-command", + "gix-config-value", + "parking_lot", + "rustix", + "thiserror", +] + +[[package]] +name = "gix-protocol" +version = "0.65.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68878c37ae168b474c762d70adc8a8e0f9323ed0d80444a672e2eb561fa16691" +dependencies = [ + "bstr", + "gix-credentials", + "gix-date", + "gix-features", + "gix-hash", + "gix-lock", + "gix-macros", + "gix-negotiate", + "gix-object", + "gix-ref", + "gix-refspec", + "gix-revwalk", + "gix-shallow", + "gix-trace", + "gix-transport", + "gix-utils", + "nonempty", + "thiserror", +] + +[[package]] +name = "gix-quote" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f74795eb76eab8313063849f9cbd2bbcdc6e4692f71c1d7d0244ab11cd777329" +dependencies = [ + "bstr", + "gix-error", + "gix-utils", +] + +[[package]] +name = "gix-ref" +version = "0.67.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8328387e7ab354e1dc3afb63e6b4d1866f82d7ce035222bc9d5baaf36bc88020" +dependencies = [ + "gix-actor", + "gix-features", + "gix-fs", + "gix-hash", + "gix-lock", + "gix-object", + "gix-path", + "gix-tempfile", + "gix-utils", + "gix-validate", + "memmap2", + "thiserror", +] + +[[package]] +name = "gix-refspec" +version = "0.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7de280d46e8fd9e4d3e7e2ab4276b965e377ba68b2b9a8fce56371015e8bb059" +dependencies = [ + "bstr", + "gix-hash", + "gix-validate", + "smallvec", + "thiserror", +] + +[[package]] +name = "gix-revision" +version = "0.49.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4577b864c3e134697e91564553e43230c2e401bb1bcc51cfa998edca21c95078" +dependencies = [ + "bitflags 2.13.2", + "bstr", + "gix-commitgraph", + "gix-date", + "gix-error", + "gix-hash", + "gix-hashtable", + "gix-object", + "gix-revwalk", + "gix-trace", + "nonempty", +] + +[[package]] +name = "gix-revwalk" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "248823eefe405e2c0754b74f6f43ab6faab68670cdbf2d6e114cb0471f766450" +dependencies = [ + "gix-commitgraph", + "gix-date", + "gix-error", + "gix-hash", + "gix-hashtable", + "gix-object", + "smallvec", + "thiserror", +] + +[[package]] +name = "gix-sec" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af4fe6c152c1d50aea36f299825702cd37e303307832fec1d0fdd5844e47ce2f" +dependencies = [ + "bitflags 2.13.2", + "gix-path", + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "gix-shallow" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ecc9f4b40537043e4bbd7d3d1760e74fb8e7b07a546166b558acaa73ad97f4a" +dependencies = [ + "bstr", + "gix-hash", + "gix-lock", + "nonempty", + "thiserror", +] + +[[package]] +name = "gix-status" +version = "0.34.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1401d871c01d82add5a9f654439b00722d0c632b332cd2cb63193f94327c8458" +dependencies = [ + "bstr", + "filetime", + "gix-diff", + "gix-dir", + "gix-features", + "gix-filter", + "gix-fs", + "gix-hash", + "gix-index", + "gix-object", + "gix-path", + "gix-pathspec", + "gix-worktree", + "hashbrown 0.16.1", + "portable-atomic", + "thiserror", + "windows-sys 0.61.2", +] + +[[package]] +name = "gix-submodule" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da85564d6725c483b8d95d7b31d1db0b78c29e462a2b481949e2f18e1ed55a6a" +dependencies = [ + "bstr", + "gix-config", + "gix-path", + "gix-pathspec", + "gix-refspec", + "gix-url", + "thiserror", +] + +[[package]] +name = "gix-tempfile" +version = "24.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b675b920bd5a61d17ad542772f03ec34c60feb8ff683e1560c03ae967363731e" +dependencies = [ + "dashmap", + "gix-fs", + "libc", + "parking_lot", + "signal-hook", + "signal-hook-registry", + "tempfile", +] + +[[package]] +name = "gix-trace" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be3eb81d9dc914335923e50d52829c551feefd6a72d176c4130c546b67a60814" + +[[package]] +name = "gix-transport" +version = "0.59.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3f41a64939953ff49117df4eb91334d299abc59ff57a90fb2193d8ffa5886f1" +dependencies = [ + "base64 0.22.1", + "bstr", + "gix-command", + "gix-credentials", + "gix-features", + "gix-packetline", + "gix-path", + "gix-quote", + "gix-sec", + "gix-url", + "parking_lot", + "reqwest", + "thiserror", +] + +[[package]] +name = "gix-traverse" +version = "0.61.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9af3503668739f4de5dba57fe15cfd9adc443b08bcbbf195e5de16b648872245" +dependencies = [ + "bitflags 2.13.2", + "gix-commitgraph", + "gix-date", + "gix-hash", + "gix-hashtable", + "gix-object", + "gix-revwalk", + "smallvec", + "thiserror", +] + +[[package]] +name = "gix-url" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bda80ccb4bc81fb9fb07a975916eb0c7a889bcbcb0c8a239c3f82a9cc2abdda" +dependencies = [ + "bstr", + "gix-path", + "gix-utils", + "percent-encoding", + "thiserror", +] + +[[package]] +name = "gix-utils" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0da1c46491b49458a446cc76f0085860f8164c2290742e0aa8c653ce67240a97" +dependencies = [ + "bstr", + "fastrand", + "getrandom 0.4.3", + "unicode-normalization", +] + +[[package]] +name = "gix-validate" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dae8780f63ed8a803b8bdabbd7aa5f5c5d74592c8b50eed875c1bb4f6545a6a" +dependencies = [ + "bstr", +] + +[[package]] +name = "gix-worktree" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed36b627476e2072c129900a17c977a38052b5497e27308159bc881fbf4eecf" +dependencies = [ + "bstr", + "gix-attributes", + "gix-features", + "gix-fs", + "gix-glob", + "gix-hash", + "gix-ignore", + "gix-index", + "gix-object", + "gix-path", + "gix-validate", +] + +[[package]] +name = "gix-worktree-state" +version = "0.34.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c14faa5b9527823c656a033e841dde11ca5b774f1bca7a9cdc23db677e55d56" +dependencies = [ + "bstr", + "gix-features", + "gix-filter", + "gix-fs", + "gix-index", + "gix-object", + "gix-path", + "gix-worktree", + "io-close", + "thiserror", +] + +[[package]] +name = "gix-worktree-stream" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de5ec0ccbab39c9994656ea031ded8ffd90b370a3cc8e360fbe3c4ebe7c64964" +dependencies = [ + "gix-attributes", + "gix-error", + "gix-features", + "gix-filter", + "gix-fs", + "gix-hash", + "gix-object", + "gix-path", + "gix-traverse", + "parking_lot", +] + +[[package]] +name = "gix-zlib" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e8813f5579b3075ff9c90f7c59cd2b62b4ebb639361f0911648b22d7446cc7c" +dependencies = [ + "thiserror", + "zlib-rs", +] + +[[package]] +name = "h2" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d29020232d6aa3fb1daca64c1127cf662cf97f254ae16c18c05b8ab635fc118" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32", + "stable_deref_trait", +] + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +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 = "human_format" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaec953f16e5bcf6b8a3cb3aa959b17e5577dbd2693e94554c462c08be22624b" + +[[package]] +name = "hyper" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c3e324da4c95177d6291d4c8730197c0d1822f8a9766814a4a44fa5ab797c9c" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa8e654703247911e29c23fbeaa261834bd9bb74efba2f9acddc37bfb127f53" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff" +dependencies = [ + "base64 0.23.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "httparse", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[package]] +name = "io-close" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cadcf447f06744f8ce713d2d6239bb5bde2c357a452397a9ed90c625da390bc" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "ipnet" +version = "2.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jiff" +version = "0.2.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b005715dcbeb0089a3c0dab99f2ff1cc3b2525323552703d648585d342a383" +dependencies = [ + "defmt 1.1.1", + "jiff-core", + "jiff-static", + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", + "windows-link", +] + +[[package]] +name = "jiff-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e52fe76043ccecc9005d2305ebaadf7d7fc0cc89ca6baa10a94d6bc68c7128c" +dependencies = [ + "defmt 1.1.1", + "log", +] + +[[package]] +name = "jiff-static" +version = "0.2.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cc9817253cf7c7ee4684451bd327e88d6f3658014e54a29198625590650695c" +dependencies = [ + "jiff-core", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa8377070c6bae868759445e5a77f66d84f0b72f3a054bfb00e6d038b8282da7" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys", + "log", + "simd_cesu8", + "thiserror", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.119", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb" + +[[package]] +name = "libc" +version = "0.2.190" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "lru-slab" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4050469837a6ff301cd14c1f8f24f88549e6d548f24f64e2148eb0f72cebc51f" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memmap2" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" +dependencies = [ + "libc", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mio" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "multiversion_no_op" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d" + +[[package]] +name = "nonempty" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9737e026353e5cd0736f98eddae28665118eb6f6600902a7f50db585621fecb6" + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" [[package]] -name = "cfg-if" -version = "1.0.5" +name = "portable-atomic" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" [[package]] -name = "crossbeam-channel" -version = "0.5.17" +name = "portable-atomic-util" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1" +checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" dependencies = [ - "crossbeam-utils", + "portable-atomic", ] [[package]] -name = "crossbeam-utils" -version = "0.8.23" +name = "potential_utf" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] [[package]] -name = "deranged" -version = "0.5.8" +name = "powerfmt" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +checksum = "4a6394b9e965e73d0a289ee54f589087e2c676aedf60885baf52c76b771e4958" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ - "powerfmt", + "unicode-ident", ] [[package]] -name = "equivalent" -version = "1.0.2" +name = "prodash" +version = "31.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +checksum = "cf52d87428247c5aa3760ac8e7d8b13ec7b3579b955a8c3f311668b6860acba5" +dependencies = [ + "bytesize", + "human_format", + "parking_lot", +] [[package]] -name = "errno" -version = "0.3.14" +name = "quinn" +version = "0.11.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +checksum = "4051e23e9185c255a7e33ef59cdbca87a22d359052eecd22fc6b901fb37d9d11" dependencies = [ - "libc", - "windows-sys", + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", + "web-time", ] [[package]] -name = "hashbrown" -version = "0.17.1" +name = "quinn-proto" +version = "0.11.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +checksum = "0e750cca55fe4f0439a15d0bb529da9651e79993e8e72c61a899a36d462befbe" +dependencies = [ + "aws-lc-rs", + "bytes", + "getrandom 0.4.3", + "lru-slab", + "rand", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] [[package]] -name = "indexmap" -version = "2.14.2" +name = "quinn-udp" +version = "0.5.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +checksum = "af66907df18639dcf4db56ca65490cabc4b27a97dbadd96f2926cca73298f016" dependencies = [ - "equivalent", - "hashbrown", + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.61.2", ] [[package]] -name = "itoa" -version = "1.0.18" +name = "quote" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] [[package]] -name = "lazy_static" -version = "1.5.0" +name = "r-efi" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] -name = "libc" -version = "0.2.189" +name = "rand" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core", +] [[package]] -name = "log" -version = "0.4.34" +name = "rand_core" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" [[package]] -name = "matchers" -version = "0.2.0" +name = "rand_pcg" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" dependencies = [ + "aho-corasick", + "memchr", "regex-automata", + "regex-syntax", ] [[package]] -name = "memchr" -version = "2.8.3" +name = "regex-automata" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] [[package]] -name = "mio" -version = "1.2.3" +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16a1cfa75cc186dd73d5818e510e042e40927bccc9c236b061cea97e1eb08029" +dependencies = [ + "base64 0.23.1", + "bytes", + "encoding_rs", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "mime", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", "libc", - "wasi", - "windows-sys", + "untrusted", + "windows-sys 0.52.0", ] [[package]] -name = "nu-ansi-term" -version = "0.50.3" +name = "rustc-hash" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ - "windows-sys", + "semver", ] [[package]] -name = "num-conv" -version = "0.1.0" +name = "rustix" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" +dependencies = [ + "bitflags 2.13.2", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] [[package]] -name = "once_cell" -version = "1.21.4" +name = "rustls" +version = "0.23.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" +dependencies = [ + "aws-lc-rs", + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] [[package]] -name = "pin-project-lite" -version = "0.2.17" +name = "rustls-native-certs" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] [[package]] -name = "powerfmt" +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1167586491e2b18b8bfbb293e8180ec17c201c4f076d7cb3070ca964e7598f98" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +checksum = "eec689c0bc40ff2458a5977b6619cb718087084a18e02a131c599b62d05e1a5f" [[package]] -name = "proc-macro2" -version = "1.0.107" +name = "rustls-webpki" +version = "0.103.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" dependencies = [ - "unicode-ident", + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", ] [[package]] -name = "quote" -version = "1.0.47" +name = "rustversion" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" dependencies = [ - "proc-macro2", + "winapi-util", ] [[package]] -name = "regex-automata" -version = "0.4.18" +name = "schannel" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", + "windows-sys 0.61.2", ] [[package]] -name = "regex-syntax" -version = "0.8.11" +name = "scopeguard" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.13.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" @@ -249,6 +2582,27 @@ dependencies = [ "serde_core", ] +[[package]] +name = "sha1" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "sha1-checked" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89f599ac0c323ebb1c6082821a54962b839832b03984598375bff3975b804423" +dependencies = [ + "digest", + "sha1", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -258,6 +2612,22 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b29ffa27d3d59a049bb328258de100f8012848d1b08d444e08bbc02ac468435e" +dependencies = [ + "libc", + "signal-hook-registry", +] + [[package]] name = "signal-hook-registry" version = "1.4.8" @@ -268,6 +2638,28 @@ dependencies = [ "libc", ] +[[package]] +name = "simd_cesu8" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + [[package]] name = "smallvec" version = "1.16.2" @@ -281,7 +2673,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -289,6 +2681,7 @@ name = "software-core" version = "0.1.0" dependencies = [ "bytes", + "gix", "serde", "thiserror", "tokio", @@ -297,6 +2690,12 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "station" version = "0.1.0" @@ -310,6 +2709,12 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "symlink" version = "0.1.0" @@ -320,22 +2725,55 @@ checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a" name = "syn" version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +dependencies = [ + "proc-macro2", + "quote", + "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.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" dependencies = [ "proc-macro2", "quote", - "unicode-ident", + "syn 3.0.6", ] [[package]] -name = "syn" -version = "3.0.6" +name = "tempfile" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", ] [[package]] @@ -369,12 +2807,11 @@ dependencies = [ [[package]] name = "time" -version = "0.3.45" +version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" dependencies = [ "deranged", - "itoa", "num-conv", "powerfmt", "serde_core", @@ -384,33 +2821,50 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.7" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.25" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e552d1249bf61ac2a52db88179fd0673def1e1ad8243a00d9ec9ed71fee3dd" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" dependencies = [ "num-conv", "time-core", ] +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" + [[package]] name = "tokio" -version = "1.53.1" +version = "1.53.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +checksum = "e95f91fcc7a621e8b030f6aa23c71fe9838ae2fb4d8118b75602a328f5144044" dependencies = [ + "bytes", "libc", "mio", "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -424,6 +2878,30 @@ dependencies = [ "syn 3.0.6", ] +[[package]] +name = "tokio-rustls" +version = "0.26.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9cc2678c2cdd569ef8215e2afd7954ada2ae20b4fdd2c5fe6139a3b02d105db" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "libc", + "pin-project-lite", + "tokio", +] + [[package]] name = "toml" version = "0.9.12+spec-1.1.0" @@ -463,6 +2941,51 @@ version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.13.2", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[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.44" @@ -550,30 +3073,240 @@ dependencies = [ "tracing-serde", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "uluru" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c8a2469e56e6e5095c82ccd3afb98dad95f7af7929aab6d8ba8d6e0f73657da" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "unicode-bom" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eec5d1121208364f6793f7d2e222bf75a915c19557537745b195b253dd64217" + [[package]] name = "unicode-ident" version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[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.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "valuable" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec4cdd0dd910afe868b7ef477227d8d538b46b3075031afee8a9f2acb0a2ed0b" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasm-bindgen" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbab34de2d982e9b48e18d216d04c4a6f641066ff19ffb699980f591ee3610e" +dependencies = [ + "js-sys", + "tokio", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.6", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88261b9deccee56594c11a3460c462c41f58d148598fe70ad77070126a68aba4" +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-root-certs" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -583,6 +3316,70 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "winnow" version = "0.7.15" @@ -595,6 +3392,101 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879" + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "zlib-rs" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" + [[package]] name = "zmij" version = "1.0.23" diff --git a/backend/Cargo.toml b/backend/Cargo.toml index d9aeeb64e..bc5d0e4cd 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -5,7 +5,7 @@ default-members = ["crates/station"] [workspace.package] edition = "2024" -rust-version = "1.85" +rust-version = "1.98" license = "MIT" authors = [ "Hyperloop UPV Software Subsystem", diff --git a/backend/crates/core/Cargo.toml b/backend/crates/core/Cargo.toml index 1b38a623e..878c62e3c 100644 --- a/backend/crates/core/Cargo.toml +++ b/backend/crates/core/Cargo.toml @@ -29,6 +29,7 @@ tracing.workspace = true tokio = { workspace = true, optional = true } tracing-subscriber = { workspace = true, optional = true } tracing-appender = { workspace = true, optional = true } +gix = { version = "0.87.1", features = ["blocking-http-transport-reqwest-rust-tls"] } # `workspace = true` can't be combined with extra lints in the same table, # so the workspace list is repeated here with `missing_docs` added: this is From 9bfceab4680463570458a2608d96e36764136c11 Mon Sep 17 00:00:00 2001 From: Pau Minguet Date: Sun, 11 Oct 2026 18:38:04 +0200 Subject: [PATCH 02/11] feat(backend, git): implementation of ADJ update handling --- backend/Cargo.lock | 48 ++++ backend/crates/core/Cargo.toml | 8 +- backend/crates/core/src/git.rs | 396 +++++++++++++++++++++++++++++++++ backend/crates/core/src/lib.rs | 6 + 4 files changed, 455 insertions(+), 3 deletions(-) create mode 100644 backend/crates/core/src/git.rs diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 0a8ad6631..147430cc4 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -361,6 +361,27 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dirs" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d57d423b3c82e89b9a24ca3091fee61f456a26edbd28d26c65906f4bc1dcd8f" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + [[package]] name = "displaydoc" version = "0.2.7" @@ -1974,6 +1995,15 @@ version = "0.2.190" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" +[[package]] +name = "libredox" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" +dependencies = [ + "libc", +] + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -2087,6 +2117,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "parking_lot" version = "0.12.5" @@ -2285,6 +2321,16 @@ dependencies = [ "bitflags 2.13.2", ] +[[package]] +name = "redox_users" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60dc65c0ff1a7ae1294b0c67b9f14baf70b644404010370171787bfac1038fc0" +dependencies = [ + "libredox", + "thiserror", +] + [[package]] name = "regex" version = "1.13.1" @@ -2681,8 +2727,10 @@ name = "software-core" version = "0.1.0" dependencies = [ "bytes", + "dirs", "gix", "serde", + "tempfile", "thiserror", "tokio", "tracing", diff --git a/backend/crates/core/Cargo.toml b/backend/crates/core/Cargo.toml index 878c62e3c..baa5e3012 100644 --- a/backend/crates/core/Cargo.toml +++ b/backend/crates/core/Cargo.toml @@ -12,8 +12,8 @@ repository.workspace = true default = [] # TCP/UDP transport (pulls in tokio) net = ["dep:tokio"] -# ADJ download through git (dependency added when the module is written) -git = [] +# ADJ download through git +git = ["dep:tempfile"] # Packet sniffing through pcap (dependency added when the module is written) pcap = ["net"] # CSV data logger (records, sessions, subloggers) @@ -29,7 +29,9 @@ tracing.workspace = true tokio = { workspace = true, optional = true } tracing-subscriber = { workspace = true, optional = true } tracing-appender = { workspace = true, optional = true } -gix = { version = "0.87.1", features = ["blocking-http-transport-reqwest-rust-tls"] } +gix = { version = "0.87.1", features = ["blocking-http-transport-reqwest-rust-tls", "blocking-network-client", "worktree-mutation"] } +dirs = "7.0.0" +tempfile = { version = "3.27", optional = true } # `workspace = true` can't be combined with extra lints in the same table, # so the workspace list is repeated here with `missing_docs` added: this is diff --git a/backend/crates/core/src/git.rs b/backend/crates/core/src/git.rs new file mode 100644 index 000000000..155fc3a1b --- /dev/null +++ b/backend/crates/core/src/git.rs @@ -0,0 +1,396 @@ +//! Blocking Git helpers for ADJ, an application-owned, replaceable data cache. + +use std::{ + fs, io, + net::{Ipv4Addr, SocketAddr, TcpStream}, + path::{Path, PathBuf}, + sync::atomic::AtomicBool, + time::Duration, +}; + +use gix::Repository; +use tracing::{debug, info, instrument, warn}; + +type BoxError = Box; + +const ADJ_URL: &str = "https://github.com/HyperloopUPV/adj.git"; +const GIT_REMOTE: &str = "origin"; + +/// Downloads the requested ADJ branch, replacing the application-managed cache. +/// +/// `path` is the repository directory, not its parent. If omitted, it defaults to +/// `/hyperloop-control-station/adj`. `branch` selects the branch +/// to clone when the connectivity probe succeeds. +/// +/// Online, local edits and commits are discarded only after a fresh checkout +/// succeeds. Offline, the current cached checkout is used without switching +/// branches, so it may differ from `branch`. Returns its full commit hash and +/// commit message, in that order. +/// +/// This performs blocking network and filesystem operations. Call during startup, +/// before readers use ADJ, with only one updater at a time. Replacement uses two +/// renames and is not atomic across a process crash. +/// +/// # Errors +/// +/// Returns an error if ADJ is missing offline, the path or repository is invalid +/// or inaccessible, cloning or checkout fails, or replacement fails. Clone errors +/// do not automatically fall back to the cache. If rollback fails, the error +/// includes the preserved backup's location. +#[instrument(level = "debug", err(level = "warn"))] +pub fn update_adj(path: Option, branch: &str) -> Result<(String, String), BoxError> { + let (repo, dir) = adj_exists_repo(path)?; + + if check_internet_connection() { + info!(path = ?dir, branch, "starting ADJ update"); + // Release repository handles before moving the existing cache directory. + drop(repo); + git_replace(&dir, branch, ADJ_URL) + } else if let Some(repo) = repo { + warn!(path = ?dir, requested_branch = branch, + "connectivity probe failed; using cached ADJ without switching branches"); + get_hash_and_message(&repo) + } else { + Err(io::Error::new( + io::ErrorKind::NotFound, + "ADJ is not available locally and cannot be downloaded", + ) + .into()) + } +} + +/// Lists locally cached remote-tracking branches without contacting the remote. +/// +/// `path` identifies the repository directory. Names retain the remote prefix, +/// such as `origin/main`; the results can be stale, are not explicitly sorted, +/// and may include symbolic references such as `origin/HEAD`. +/// +/// # Errors +/// +/// Returns an error if the repository cannot be opened or its references cannot +/// be read. +#[instrument(level = "debug", err(level = "warn"))] +pub fn list_remote_branches(path: PathBuf) -> Result, BoxError> { + let repo = gix::open(&path)?; + + let mut branches = Vec::new(); + + // Inspect local remote-tracking refs, not the remote server's advertised refs. + for reference in repo.references()?.remote_branches()? { + let reference = reference?; + branches.push(reference.name().shorten().to_string()); + } + + debug!( + ?path, + count = branches.len(), + "listed cached remote-tracking branches" + ); + Ok(branches) +} + +/// Opens an existing repository without fetching or changing its checkout. +/// +/// `path` is the repository directory. If omitted, it defaults to +/// `/hyperloop-control-station/adj`. This does not create ADJ. +/// +/// # Errors +/// +/// Returns an error if the cache directory cannot be resolved or the repository +/// is missing, invalid, or inaccessible. +#[instrument(level = "debug", err(level = "warn"))] +pub fn open_repo(path: Option) -> Result { + let path = unwrap_repo_path(path)?; + + let repo = gix::open(path)?; + + debug!(git_dir = ?repo.path(), "opened repository"); + Ok(repo) +} + +// Checks external TCP connectivity with a three-second timeout, not GitHub availability. +#[instrument(level = "debug")] +fn check_internet_connection() -> bool { + // Use a fixed IPv4 address to avoid a separate DNS lookup and its timeout. + let address = SocketAddr::from((Ipv4Addr::new(1, 1, 1, 1), 443)); + let timeout = Duration::from_secs(3); + + // Attempt only the TCP handshake; no application data is sent. + match TcpStream::connect_timeout(&address, timeout) { + Ok(_stream) => { + debug!(%address, "connectivity probe succeeded"); + true + } + Err(error) => { + // Record why the probe failed without treating it as an application error. + debug!(%address, %error, "connectivity probe failed"); + false + } + } +} + +// Preserve explicit paths; otherwise use the platform's user cache directory. +fn unwrap_repo_path(path: Option) -> Result { + let dir = match path { + Some(path) => path, + None => { + let path = dirs::cache_dir().ok_or_else(|| { + io::Error::new(io::ErrorKind::NotFound, "cache directory not found") + })?; + + path.join("hyperloop-control-station").join("adj") + } + }; + + debug!(path = ?dir, "resolved ADJ repository path"); + Ok(dir) +} + +// Only an absent directory is a cache miss; invalid repositories remain errors. +fn adj_exists_repo(path: Option) -> Result<(Option, PathBuf), BoxError> { + let dir = unwrap_repo_path(path)?; + + let res = if dir.try_exists()? { + Some(gix::open(&dir)?) + } else { + None + }; + + debug!(path = ?dir, exists = res.is_some(), "checked local ADJ repository"); + Ok((res, dir)) +} + +// Stage beside ADJ so both installation and rollback use same-filesystem renames. +#[instrument(level = "debug", skip(url))] +fn git_replace(path: &Path, branch: &str, url: &str) -> Result<(String, String), BoxError> { + let parent = path + .parent() + .filter(|p| !p.as_os_str().is_empty()) + .unwrap_or(Path::new(".")); + fs::create_dir_all(parent)?; + let staging = tempfile::Builder::new() + .prefix(".adj-") + .tempdir_in(parent)?; + let checkout = staging.path().join("new"); + let backup = staging.path().join("old"); + debug!(?checkout, ?backup, "created ADJ staging directory"); + + // Validate the new checkout and commit before touching the current cache. + // Early errors drop the staging directory, removing any incomplete clone. + let repo = git_clone(&checkout, branch, url)?; + let commit = get_hash_and_message(&repo)?; + // The repository contains paths into staging and must not survive the move. + drop(repo); + + let had_cache = path.try_exists()?; + if had_cache { + fs::rename(path, &backup)?; + debug!(?path, ?backup, "moved cached ADJ aside for replacement"); + } + if let Err(error) = fs::rename(&checkout, path) { + debug!(?path, %error, had_cache, "ADJ installation failed; restoring cache if present"); + if had_cache && let Err(rollback) = fs::rename(&backup, path) { + // Do not let temporary-directory cleanup delete the only remaining cache. + let saved = staging.keep(); + debug!(?backup, %rollback, "preserved ADJ backup after failed rollback"); + return Err(io::Error::other(format!( + "ADJ installation failed: {error}; rollback failed: {rollback}; backup kept at {}", + saved.join("old").display() + )) + .into()); + } + if had_cache { + debug!(?path, "restored cached ADJ after installation failure"); + } + return Err(error.into()); + } + + // Installation succeeded; cleanup now discards the old cache. A cleanup + // failure does not turn a successfully installed checkout into an error. + if let Err(error) = staging.close() { + warn!(?path, %error, "ADJ updated, but temporary directory cleanup failed"); + } + info!(?path, branch, commit = %commit.0, "ADJ updated"); + Ok(commit) +} + +/// Checks whether a remote-tracking branch is cached in `repo`. +/// +/// `remote` is the remote name, such as `origin`; `name` is the branch name, such +/// as `main` or `feature/example`. Looks up `refs/remotes//` without +/// contacting the server or checking local branches. `false` means the reference +/// is absent locally, not necessarily that the branch is absent on the server. +/// +/// # Errors +/// +/// Returns an error if the reference name is invalid or the reference cannot be +/// read. +#[instrument(level = "debug", skip(repo), err(level = "warn"))] +pub fn branch_exists(repo: &gix::Repository, remote: &str, name: &str) -> Result { + let full = format!("refs/remotes/{remote}/{name}"); + let exists = repo.try_find_reference(full.as_str())?.is_some(); + debug!(reference = %full, exists, "checked cached remote-tracking branch"); + Ok(exists) +} + +// Fetch and check out the requested branch instead of switching an existing repo. +#[instrument(level = "debug", skip(url))] +fn git_clone(path: &Path, branch: &str, url: &str) -> Result { + // These synchronous startup operations do not currently expose cancellation. + let interrupt = AtomicBool::new(false); + + debug!(?path, branch, "preparing shallow ADJ clone"); + // A depth of one keeps the current snapshot without downloading full history. + let mut prepare = gix::prepare_clone(url, path)? + .with_remote_name(GIT_REMOTE)? + .with_ref_name(Some(branch))? + .with_shallow(gix::remote::fetch::Shallow::DepthAtRemote( + std::num::NonZeroU32::MIN, + )); + + let (mut checkout, _) = prepare.fetch_then_checkout(gix::progress::Discard, &interrupt)?; + debug!( + ?path, + branch, "ADJ fetch completed; checking out working tree" + ); + let (repo, _) = checkout.main_worktree(gix::progress::Discard, &interrupt)?; + + debug!(?path, branch, "ADJ clone and checkout completed"); + Ok(repo) +} + +// Report the checked-out HEAD, which can differ from the requested branch offline. +fn get_hash_and_message(repo: &Repository) -> Result<(String, String), BoxError> { + let hash = repo.head_id()?.to_string(); + let message = repo.head_commit()?.decode()?.message.to_string(); + + // Log only the commit ID, not potentially large or sensitive message text. + debug!(commit = %hash, "read checked-out ADJ commit"); + Ok((hash, message)) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::process::Command; + + fn git(path: &Path, args: &[&str]) -> Result<(), BoxError> { + // Fixture commits need identity; disable signing, hooks, and templates + // so tests do not depend on the user's normal Git setup. + let output = Command::new("git") + .current_dir(path) + .args([ + "-c", + "user.name=ADJ tests", + "-c", + "user.email=adj@example.test", + "-c", + "commit.gpgSign=false", + "-c", + "core.hooksPath=", + "-c", + "init.templateDir=", + ]) + .args(args) + .output()?; + if !output.status.success() { + return Err(io::Error::other(String::from_utf8_lossy(&output.stderr)).into()); + } + Ok(()) + } + + fn source_repo(path: &Path) -> Result<(), BoxError> { + fs::create_dir(path)?; + git(path, &["init", "--initial-branch=main"])?; + fs::write(path.join("data.txt"), "main")?; + git(path, &["add", "."])?; + git(path, &["commit", "-m", "main data"])?; + // Leave HEAD on another branch to verify explicit clone branch selection. + git(path, &["checkout", "-b", "testing"])?; + fs::write(path.join("data.txt"), "testing")?; + git(path, &["commit", "-am", "testing data"]) + } + + // Distinguish missing, invalid, and valid local repositories. + #[test] + fn detects_missing_and_invalid_repositories() -> Result<(), BoxError> { + let temp = tempfile::tempdir()?; + let path = temp.path().join("adj"); + assert!(adj_exists_repo(Some(path.clone()))?.0.is_none()); + fs::create_dir(&path)?; + assert!(adj_exists_repo(Some(path.clone())).is_err()); + git(&path, &["init"])?; + assert!(adj_exists_repo(Some(path))?.0.is_some()); + Ok(()) + } + + // Verify branch selection, newer commits, and removal of stale cache files. + #[test] + fn clones_and_replaces_with_requested_branch() -> Result<(), BoxError> { + let temp = tempfile::tempdir()?; + let source = temp.path().join("source"); + let cache = temp.path().join("cache").join("adj"); + source_repo(&source)?; + let url = source.to_str().ok_or("non-UTF-8 test path")?; + + let first = git_replace(&cache, "main", url)?; + assert_eq!(first.1, "main data\n"); + assert_eq!(fs::read_to_string(cache.join("data.txt"))?, "main"); + // Replacement must discard old cache files, not just update tracked ones. + fs::write(cache.join("stale.txt"), "discard this cache file")?; + + // Add a newer remote commit and request a different branch on replacement. + fs::write(source.join("data.txt"), "latest testing")?; + git(&source, &["commit", "-am", "updated testing data"])?; + let expected = get_hash_and_message(&gix::open(&source)?)?; + assert_eq!(git_replace(&cache, "testing", url)?, expected); + assert_ne!(first.0, expected.0); + assert_eq!(get_hash_and_message(&gix::open(&cache)?)?, expected); + assert_eq!( + fs::read_to_string(cache.join("data.txt"))?, + "latest testing" + ); + assert!(!cache.join("stale.txt").exists()); + // Successful replacement must also remove staging and backup directories. + assert_eq!( + fs::read_dir(cache.parent().ok_or("missing parent")?)?.count(), + 1 + ); + Ok(()) + } + + // Verify clone failures preserve cached ADJ and leave no staging debris. + #[test] + fn failed_clone_preserves_existing_cache() -> Result<(), BoxError> { + let temp = tempfile::tempdir()?; + let source = temp.path().join("source"); + let cache = temp.path().join("adj"); + source_repo(&source)?; + let url = source.to_str().ok_or("non-UTF-8 test path")?; + + // A failed first clone must not install a partial checkout. + assert!(git_replace(&cache, "missing-branch", url).is_err()); + assert!(!cache.exists()); + let expected = git_replace(&cache, "main", url)?; + fs::write(cache.join("stale.txt"), "keep on failure")?; + // Both an invalid branch and an unavailable source must leave ADJ intact. + assert!(git_replace(&cache, "missing-branch", url).is_err()); + let missing = temp.path().join("missing-source"); + assert!( + git_replace( + &cache, + "main", + missing.to_str().ok_or("non-UTF-8 test path")? + ) + .is_err() + ); + assert_eq!(get_hash_and_message(&gix::open(&cache)?)?, expected); + assert_eq!(fs::read_to_string(cache.join("data.txt"))?, "main"); + assert_eq!( + fs::read_to_string(cache.join("stale.txt"))?, + "keep on failure" + ); + assert_eq!(fs::read_dir(temp.path())?.count(), 2); + Ok(()) + } +} diff --git a/backend/crates/core/src/lib.rs b/backend/crates/core/src/lib.rs index a85bf01dd..0420745c0 100644 --- a/backend/crates/core/src/lib.rs +++ b/backend/crates/core/src/lib.rs @@ -1,3 +1,9 @@ //! Componentes reutilizables de Hyperloop UPV. pub mod hello_world; + +/// # Git module +/// Handles updates and changes within the ADJ. +/// Checks the ADJ Github repository and +#[cfg(feature = "git")] +pub mod git; \ No newline at end of file From 58d320bd4678da2ae7707bae383ce6f5a8ac6e36 Mon Sep 17 00:00:00 2001 From: Pau Minguet Date: Sun, 11 Oct 2026 18:38:43 +0200 Subject: [PATCH 03/11] docs(backend, git): README --- backend/crates/core/src/git/README.md | 60 +++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 backend/crates/core/src/git/README.md diff --git a/backend/crates/core/src/git/README.md b/backend/crates/core/src/git/README.md new file mode 100644 index 000000000..950249435 --- /dev/null +++ b/backend/crates/core/src/git/README.md @@ -0,0 +1,60 @@ +# ADJ Git helpers + +This module manages ADJ as an application-owned, replaceable data cache using +`gix`, with `tracing` for logging. It does not preserve local edits or commits +when updating online. + +## Workflow + +`update_adj(path, branch)` returns the checked-out commit hash and full message. +A custom path identifies the ADJ repository itself. The default is +`/hyperloop-control-station/adj`, using `dirs::cache_dir()`. +An existing invalid repository or inaccessible path returns an error. + +The existing connectivity check attempts a TCP connection to `1.1.1.1:443` +with a three-second timeout. This checks external TCP connectivity, not GitHub +availability. + +### Online + +- Shallow-clone the requested branch into a temporary directory beside ADJ. +- Complete checkout and read its commit information before touching the cache. +- Move the old cache aside and install the new checkout. +- Restore the old cache if installation fails. If restoration also fails, + preserve the backup and include its location in the error. +- Remove the old cache after successful installation. + +A clone error leaves the old cache untouched and is returned directly; it does +not automatically fall back to offline operation. No fetch, merge, or branch +switch is needed because the checkout is replaced. + +### Offline + +- If ADJ exists, return its current commit without updating or switching branches. +- Otherwise, return `ADJ is not available locally and cannot be downloaded`. + +The cached branch may differ from the requested branch when offline. + +## Other Helpers + +- `open_repo` opens the custom or default repository path. +- `list_remote_branches` lists cached remote-tracking references, without making + a network request. Results can be stale and include the remote name. +- `branch_exists` checks for a cached `refs/remotes//` reference. + +## Limitations + +Replacement is intended for startup, before the backend reads ADJ, with one +updater at a time. The backup and installation renames are not jointly atomic +across a process crash. The connectivity probe can succeed when GitHub is +unreachable, or fail when GitHub is reachable. + +## Verification + +Tests use temporary local Git repositories and the Git CLI to create fixtures; +they do not require external network access. Production Git operations use `gix`. + +```bash +cargo test -p software-core -F git +cargo doc --open -p software-core -F git --no-deps +``` From db7782341df2ff04a79cc34cb884d1c93f3af7d3 Mon Sep 17 00:00:00 2001 From: Pau Minguet Date: Sun, 11 Oct 2026 18:48:36 +0200 Subject: [PATCH 04/11] docs(backend, git): add some more comments --- backend/crates/core/src/git.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/backend/crates/core/src/git.rs b/backend/crates/core/src/git.rs index 155fc3a1b..ca56adf36 100644 --- a/backend/crates/core/src/git.rs +++ b/backend/crates/core/src/git.rs @@ -11,6 +11,7 @@ use std::{ use gix::Repository; use tracing::{debug, info, instrument, warn}; +// Common error type for filesystem and Git failures; Send + Sync allows cross-thread use. type BoxError = Box; const ADJ_URL: &str = "https://github.com/HyperloopUPV/adj.git"; @@ -37,6 +38,7 @@ const GIT_REMOTE: &str = "origin"; /// or inaccessible, cloning or checkout fails, or replacement fails. Clone errors /// do not automatically fall back to the cache. If rollback fails, the error /// includes the preserved backup's location. +// Log returned errors at the public boundary; helpers trace progress without duplicate warnings. #[instrument(level = "debug", err(level = "warn"))] pub fn update_adj(path: Option, branch: &str) -> Result<(String, String), BoxError> { let (repo, dir) = adj_exists_repo(path)?; @@ -163,6 +165,7 @@ fn adj_exists_repo(path: Option) -> Result<(Option, PathBuf // Stage beside ADJ so both installation and rollback use same-filesystem renames. #[instrument(level = "debug", skip(url))] fn git_replace(path: &Path, branch: &str, url: &str) -> Result<(String, String), BoxError> { + // A relative path like "adj" has an empty parent; stage in the current directory. let parent = path .parent() .filter(|p| !p.as_os_str().is_empty()) From 71ccb294e8a5b904452d96f9254be6bb26e98751 Mon Sep 17 00:00:00 2001 From: Pau Minguet Date: Mon, 12 Oct 2026 17:09:57 +0200 Subject: [PATCH 05/11] feat(backend, git): separate module in submodules and propagate GitError instead of BoxError --- backend/crates/core/src/git.rs | 447 +++------------------- backend/crates/core/src/git/clone.rs | 245 ++++++++++++ backend/crates/core/src/git/repository.rs | 113 ++++++ backend/crates/core/src/git/update.rs | 135 +++++++ 4 files changed, 554 insertions(+), 386 deletions(-) create mode 100644 backend/crates/core/src/git/clone.rs create mode 100644 backend/crates/core/src/git/repository.rs create mode 100644 backend/crates/core/src/git/update.rs diff --git a/backend/crates/core/src/git.rs b/backend/crates/core/src/git.rs index ca56adf36..f73566257 100644 --- a/backend/crates/core/src/git.rs +++ b/backend/crates/core/src/git.rs @@ -1,399 +1,74 @@ //! Blocking Git helpers for ADJ, an application-owned, replaceable data cache. -use std::{ - fs, io, - net::{Ipv4Addr, SocketAddr, TcpStream}, - path::{Path, PathBuf}, - sync::atomic::AtomicBool, - time::Duration, -}; +mod clone; +mod repository; +mod update; -use gix::Repository; -use tracing::{debug, info, instrument, warn}; +pub use clone::CloneError; +pub use repository::{RepositoryError, branch_exists, list_remote_branches, open_repo}; +pub use update::update_adj; -// Common error type for filesystem and Git failures; Send + Sync allows cross-thread use. -type BoxError = Box; - -const ADJ_URL: &str = "https://github.com/HyperloopUPV/adj.git"; -const GIT_REMOTE: &str = "origin"; - -/// Downloads the requested ADJ branch, replacing the application-managed cache. -/// -/// `path` is the repository directory, not its parent. If omitted, it defaults to -/// `/hyperloop-control-station/adj`. `branch` selects the branch -/// to clone when the connectivity probe succeeds. -/// -/// Online, local edits and commits are discarded only after a fresh checkout -/// succeeds. Offline, the current cached checkout is used without switching -/// branches, so it may differ from `branch`. Returns its full commit hash and -/// commit message, in that order. -/// -/// This performs blocking network and filesystem operations. Call during startup, -/// before readers use ADJ, with only one updater at a time. Replacement uses two -/// renames and is not atomic across a process crash. +/// Errors produced while reading, downloading, or replacing the ADJ cache. /// -/// # Errors +/// Five categories identify the failed operation and preserve its underlying +/// error as [`std::error::Error::source`]. [`RepositoryError`] and [`CloneError`] +/// retain concrete `gix` causes and are boxed to keep this enum small. Only +/// [`RepositoryError::ReferenceItem`] uses a dynamically typed cause, as exposed +/// by the `gix` iterator. /// -/// Returns an error if ADJ is missing offline, the path or repository is invalid -/// or inaccessible, cloning or checkout fails, or replacement fails. Clone errors -/// do not automatically fall back to the cache. If rollback fails, the error -/// includes the preserved backup's location. -// Log returned errors at the public boundary; helpers trace progress without duplicate warnings. -#[instrument(level = "debug", err(level = "warn"))] -pub fn update_adj(path: Option, branch: &str) -> Result<(String, String), BoxError> { - let (repo, dir) = adj_exists_repo(path)?; - - if check_internet_connection() { - info!(path = ?dir, branch, "starting ADJ update"); - // Release repository handles before moving the existing cache directory. - drop(repo); - git_replace(&dir, branch, ADJ_URL) - } else if let Some(repo) = repo { - warn!(path = ?dir, requested_branch = branch, - "connectivity probe failed; using cached ADJ without switching branches"); - get_hash_and_message(&repo) - } else { - Err(io::Error::new( - io::ErrorKind::NotFound, - "ADJ is not available locally and cannot be downloaded", - ) - .into()) - } +/// A failed connectivity probe is not itself an error when cached ADJ exists. +/// If no cache exists, the operation returns [`GitError::UnavailableOffline`]. +#[derive(Debug, thiserror::Error)] +pub enum GitError { + /// Resolving, inspecting, or preparing the cache directory failed. + #[error("ADJ path operation failed: {0}")] + Path(#[source] std::io::Error), + + /// Opening the repository or reading its references or HEAD failed. + /// An absent reference is not itself an error. + #[error("ADJ repository operation failed: {0}")] + Repository(#[source] Box), + + /// Preparing, downloading, or checking out the requested branch failed. + /// + /// This can indicate a network, configuration, or remote-reference failure; + /// it does not necessarily mean that internet connectivity is unavailable. + #[error("ADJ clone failed: {0}")] + Clone(#[source] Box), + + /// Moving the old cache aside or installing the new checkout failed. + /// + /// When restoration also fails, `rollback` holds its error and the preserved + /// backup's location. Otherwise, the old cache was restored or never moved. + #[error( + "ADJ cache replacement failed: {source}{recovery}", + recovery = .rollback.as_ref().map(|(error, path)| { + format!("; rollback failed: {error}; backup kept at {}", path.display()) + }).unwrap_or_default() + )] + Replacement { + /// The original error moving the old cache or installing the new checkout. + #[source] + source: std::io::Error, + /// Restoration error and preserved backup path, if rollback failed. + rollback: Option<(std::io::Error, std::path::PathBuf)>, + }, + + /// The connectivity probe failed and no local ADJ cache was available. + #[error("ADJ is not available locally and cannot be downloaded")] + UnavailableOffline, } -/// Lists locally cached remote-tracking branches without contacting the remote. -/// -/// `path` identifies the repository directory. Names retain the remote prefix, -/// such as `origin/main`; the results can be stale, are not explicitly sorted, -/// and may include symbolic references such as `origin/HEAD`. -/// -/// # Errors -/// -/// Returns an error if the repository cannot be opened or its references cannot -/// be read. -#[instrument(level = "debug", err(level = "warn"))] -pub fn list_remote_branches(path: PathBuf) -> Result, BoxError> { - let repo = gix::open(&path)?; - - let mut branches = Vec::new(); - - // Inspect local remote-tracking refs, not the remote server's advertised refs. - for reference in repo.references()?.remote_branches()? { - let reference = reference?; - branches.push(reference.name().shorten().to_string()); +// Convert repository errors for '?' propagation, boxing the concrete error to keep GitError small. +impl From for GitError { + fn from(error: RepositoryError) -> Self { + Self::Repository(Box::new(error)) } - - debug!( - ?path, - count = branches.len(), - "listed cached remote-tracking branches" - ); - Ok(branches) } -/// Opens an existing repository without fetching or changing its checkout. -/// -/// `path` is the repository directory. If omitted, it defaults to -/// `/hyperloop-control-station/adj`. This does not create ADJ. -/// -/// # Errors -/// -/// Returns an error if the cache directory cannot be resolved or the repository -/// is missing, invalid, or inaccessible. -#[instrument(level = "debug", err(level = "warn"))] -pub fn open_repo(path: Option) -> Result { - let path = unwrap_repo_path(path)?; - - let repo = gix::open(path)?; - - debug!(git_dir = ?repo.path(), "opened repository"); - Ok(repo) -} - -// Checks external TCP connectivity with a three-second timeout, not GitHub availability. -#[instrument(level = "debug")] -fn check_internet_connection() -> bool { - // Use a fixed IPv4 address to avoid a separate DNS lookup and its timeout. - let address = SocketAddr::from((Ipv4Addr::new(1, 1, 1, 1), 443)); - let timeout = Duration::from_secs(3); - - // Attempt only the TCP handshake; no application data is sent. - match TcpStream::connect_timeout(&address, timeout) { - Ok(_stream) => { - debug!(%address, "connectivity probe succeeded"); - true - } - Err(error) => { - // Record why the probe failed without treating it as an application error. - debug!(%address, %error, "connectivity probe failed"); - false - } - } -} - -// Preserve explicit paths; otherwise use the platform's user cache directory. -fn unwrap_repo_path(path: Option) -> Result { - let dir = match path { - Some(path) => path, - None => { - let path = dirs::cache_dir().ok_or_else(|| { - io::Error::new(io::ErrorKind::NotFound, "cache directory not found") - })?; - - path.join("hyperloop-control-station").join("adj") - } - }; - - debug!(path = ?dir, "resolved ADJ repository path"); - Ok(dir) -} - -// Only an absent directory is a cache miss; invalid repositories remain errors. -fn adj_exists_repo(path: Option) -> Result<(Option, PathBuf), BoxError> { - let dir = unwrap_repo_path(path)?; - - let res = if dir.try_exists()? { - Some(gix::open(&dir)?) - } else { - None - }; - - debug!(path = ?dir, exists = res.is_some(), "checked local ADJ repository"); - Ok((res, dir)) -} - -// Stage beside ADJ so both installation and rollback use same-filesystem renames. -#[instrument(level = "debug", skip(url))] -fn git_replace(path: &Path, branch: &str, url: &str) -> Result<(String, String), BoxError> { - // A relative path like "adj" has an empty parent; stage in the current directory. - let parent = path - .parent() - .filter(|p| !p.as_os_str().is_empty()) - .unwrap_or(Path::new(".")); - fs::create_dir_all(parent)?; - let staging = tempfile::Builder::new() - .prefix(".adj-") - .tempdir_in(parent)?; - let checkout = staging.path().join("new"); - let backup = staging.path().join("old"); - debug!(?checkout, ?backup, "created ADJ staging directory"); - - // Validate the new checkout and commit before touching the current cache. - // Early errors drop the staging directory, removing any incomplete clone. - let repo = git_clone(&checkout, branch, url)?; - let commit = get_hash_and_message(&repo)?; - // The repository contains paths into staging and must not survive the move. - drop(repo); - - let had_cache = path.try_exists()?; - if had_cache { - fs::rename(path, &backup)?; - debug!(?path, ?backup, "moved cached ADJ aside for replacement"); - } - if let Err(error) = fs::rename(&checkout, path) { - debug!(?path, %error, had_cache, "ADJ installation failed; restoring cache if present"); - if had_cache && let Err(rollback) = fs::rename(&backup, path) { - // Do not let temporary-directory cleanup delete the only remaining cache. - let saved = staging.keep(); - debug!(?backup, %rollback, "preserved ADJ backup after failed rollback"); - return Err(io::Error::other(format!( - "ADJ installation failed: {error}; rollback failed: {rollback}; backup kept at {}", - saved.join("old").display() - )) - .into()); - } - if had_cache { - debug!(?path, "restored cached ADJ after installation failure"); - } - return Err(error.into()); - } - - // Installation succeeded; cleanup now discards the old cache. A cleanup - // failure does not turn a successfully installed checkout into an error. - if let Err(error) = staging.close() { - warn!(?path, %error, "ADJ updated, but temporary directory cleanup failed"); - } - info!(?path, branch, commit = %commit.0, "ADJ updated"); - Ok(commit) -} - -/// Checks whether a remote-tracking branch is cached in `repo`. -/// -/// `remote` is the remote name, such as `origin`; `name` is the branch name, such -/// as `main` or `feature/example`. Looks up `refs/remotes//` without -/// contacting the server or checking local branches. `false` means the reference -/// is absent locally, not necessarily that the branch is absent on the server. -/// -/// # Errors -/// -/// Returns an error if the reference name is invalid or the reference cannot be -/// read. -#[instrument(level = "debug", skip(repo), err(level = "warn"))] -pub fn branch_exists(repo: &gix::Repository, remote: &str, name: &str) -> Result { - let full = format!("refs/remotes/{remote}/{name}"); - let exists = repo.try_find_reference(full.as_str())?.is_some(); - debug!(reference = %full, exists, "checked cached remote-tracking branch"); - Ok(exists) -} - -// Fetch and check out the requested branch instead of switching an existing repo. -#[instrument(level = "debug", skip(url))] -fn git_clone(path: &Path, branch: &str, url: &str) -> Result { - // These synchronous startup operations do not currently expose cancellation. - let interrupt = AtomicBool::new(false); - - debug!(?path, branch, "preparing shallow ADJ clone"); - // A depth of one keeps the current snapshot without downloading full history. - let mut prepare = gix::prepare_clone(url, path)? - .with_remote_name(GIT_REMOTE)? - .with_ref_name(Some(branch))? - .with_shallow(gix::remote::fetch::Shallow::DepthAtRemote( - std::num::NonZeroU32::MIN, - )); - - let (mut checkout, _) = prepare.fetch_then_checkout(gix::progress::Discard, &interrupt)?; - debug!( - ?path, - branch, "ADJ fetch completed; checking out working tree" - ); - let (repo, _) = checkout.main_worktree(gix::progress::Discard, &interrupt)?; - - debug!(?path, branch, "ADJ clone and checkout completed"); - Ok(repo) -} - -// Report the checked-out HEAD, which can differ from the requested branch offline. -fn get_hash_and_message(repo: &Repository) -> Result<(String, String), BoxError> { - let hash = repo.head_id()?.to_string(); - let message = repo.head_commit()?.decode()?.message.to_string(); - - // Log only the commit ID, not potentially large or sensitive message text. - debug!(commit = %hash, "read checked-out ADJ commit"); - Ok((hash, message)) -} - -#[cfg(test)] -mod tests { - use super::*; - use std::process::Command; - - fn git(path: &Path, args: &[&str]) -> Result<(), BoxError> { - // Fixture commits need identity; disable signing, hooks, and templates - // so tests do not depend on the user's normal Git setup. - let output = Command::new("git") - .current_dir(path) - .args([ - "-c", - "user.name=ADJ tests", - "-c", - "user.email=adj@example.test", - "-c", - "commit.gpgSign=false", - "-c", - "core.hooksPath=", - "-c", - "init.templateDir=", - ]) - .args(args) - .output()?; - if !output.status.success() { - return Err(io::Error::other(String::from_utf8_lossy(&output.stderr)).into()); - } - Ok(()) - } - - fn source_repo(path: &Path) -> Result<(), BoxError> { - fs::create_dir(path)?; - git(path, &["init", "--initial-branch=main"])?; - fs::write(path.join("data.txt"), "main")?; - git(path, &["add", "."])?; - git(path, &["commit", "-m", "main data"])?; - // Leave HEAD on another branch to verify explicit clone branch selection. - git(path, &["checkout", "-b", "testing"])?; - fs::write(path.join("data.txt"), "testing")?; - git(path, &["commit", "-am", "testing data"]) - } - - // Distinguish missing, invalid, and valid local repositories. - #[test] - fn detects_missing_and_invalid_repositories() -> Result<(), BoxError> { - let temp = tempfile::tempdir()?; - let path = temp.path().join("adj"); - assert!(adj_exists_repo(Some(path.clone()))?.0.is_none()); - fs::create_dir(&path)?; - assert!(adj_exists_repo(Some(path.clone())).is_err()); - git(&path, &["init"])?; - assert!(adj_exists_repo(Some(path))?.0.is_some()); - Ok(()) - } - - // Verify branch selection, newer commits, and removal of stale cache files. - #[test] - fn clones_and_replaces_with_requested_branch() -> Result<(), BoxError> { - let temp = tempfile::tempdir()?; - let source = temp.path().join("source"); - let cache = temp.path().join("cache").join("adj"); - source_repo(&source)?; - let url = source.to_str().ok_or("non-UTF-8 test path")?; - - let first = git_replace(&cache, "main", url)?; - assert_eq!(first.1, "main data\n"); - assert_eq!(fs::read_to_string(cache.join("data.txt"))?, "main"); - // Replacement must discard old cache files, not just update tracked ones. - fs::write(cache.join("stale.txt"), "discard this cache file")?; - - // Add a newer remote commit and request a different branch on replacement. - fs::write(source.join("data.txt"), "latest testing")?; - git(&source, &["commit", "-am", "updated testing data"])?; - let expected = get_hash_and_message(&gix::open(&source)?)?; - assert_eq!(git_replace(&cache, "testing", url)?, expected); - assert_ne!(first.0, expected.0); - assert_eq!(get_hash_and_message(&gix::open(&cache)?)?, expected); - assert_eq!( - fs::read_to_string(cache.join("data.txt"))?, - "latest testing" - ); - assert!(!cache.join("stale.txt").exists()); - // Successful replacement must also remove staging and backup directories. - assert_eq!( - fs::read_dir(cache.parent().ok_or("missing parent")?)?.count(), - 1 - ); - Ok(()) - } - - // Verify clone failures preserve cached ADJ and leave no staging debris. - #[test] - fn failed_clone_preserves_existing_cache() -> Result<(), BoxError> { - let temp = tempfile::tempdir()?; - let source = temp.path().join("source"); - let cache = temp.path().join("adj"); - source_repo(&source)?; - let url = source.to_str().ok_or("non-UTF-8 test path")?; - - // A failed first clone must not install a partial checkout. - assert!(git_replace(&cache, "missing-branch", url).is_err()); - assert!(!cache.exists()); - let expected = git_replace(&cache, "main", url)?; - fs::write(cache.join("stale.txt"), "keep on failure")?; - // Both an invalid branch and an unavailable source must leave ADJ intact. - assert!(git_replace(&cache, "missing-branch", url).is_err()); - let missing = temp.path().join("missing-source"); - assert!( - git_replace( - &cache, - "main", - missing.to_str().ok_or("non-UTF-8 test path")? - ) - .is_err() - ); - assert_eq!(get_hash_and_message(&gix::open(&cache)?)?, expected); - assert_eq!(fs::read_to_string(cache.join("data.txt"))?, "main"); - assert_eq!( - fs::read_to_string(cache.join("stale.txt"))?, - "keep on failure" - ); - assert_eq!(fs::read_dir(temp.path())?.count(), 2); - Ok(()) +// Convert clone errors for '?' propagation, boxing the concrete error to keep GitError small. +impl From for GitError { + fn from(error: CloneError) -> Self { + Self::Clone(Box::new(error)) } } diff --git a/backend/crates/core/src/git/clone.rs b/backend/crates/core/src/git/clone.rs new file mode 100644 index 000000000..006f127ab --- /dev/null +++ b/backend/crates/core/src/git/clone.rs @@ -0,0 +1,245 @@ +use super::{GitError, update::get_commit_hash}; +use gix::Repository; +use std::{fs, path::Path, sync::atomic::AtomicBool}; +use tracing::{debug, info, instrument, warn}; + +const GIT_REMOTE: &str = "origin"; + +/// Concrete clone failures wrapped by [`GitError::Clone`]. +/// +/// Preparation, name validation, fetching, and checkout expose different `gix` +/// error types. These variants retain each original error as their source. +#[derive(Debug, thiserror::Error)] +pub enum CloneError { + /// Clone preparation failed before downloading repository data. + #[error("clone preparation failed: {0}")] + Prepare(#[from] gix::clone::Error), + + /// The configured remote name was invalid. + #[error("remote name is invalid: {0}")] + RemoteName(#[from] gix::remote::name::Error), + + /// The requested branch name was not a valid Git reference name. + #[error("branch name is invalid: {0}")] + BranchName(#[from] gix::validate::reference::name::Error), + + /// Fetching failed, including a missing requested branch. + /// This can indicate network, configuration, or remote-reference failures. + #[error("fetch failed: {0}")] + Fetch(#[from] gix::clone::fetch::Error), + + /// Downloaded data could not be checked out into the working tree. + #[error("checkout failed: {0}")] + Checkout(#[from] gix::clone::checkout::main_worktree::Error), +} + +// Fetch and check out the requested branch instead of switching an existing repo. +#[instrument(level = "debug", skip(url))] +fn git_clone(path: &Path, branch: &str, url: &str) -> Result { + // These synchronous startup operations do not currently expose cancellation. + let interrupt = AtomicBool::new(false); + + debug!(?path, branch, "preparing shallow ADJ clone"); + // A depth of one keeps the current snapshot without downloading full history. + let mut prepare = gix::prepare_clone(url, path) + .map_err(CloneError::from)? + .with_remote_name(GIT_REMOTE) + .map_err(CloneError::from)? + .with_ref_name(Some(branch)) + .map_err(CloneError::from)? + .with_shallow(gix::remote::fetch::Shallow::DepthAtRemote( + std::num::NonZeroU32::MIN, + )); + + let (mut checkout, _) = prepare + .fetch_then_checkout(gix::progress::Discard, &interrupt) + .map_err(CloneError::from)?; + debug!( + ?path, + branch, "ADJ fetch completed; checking out working tree" + ); + let (repo, _) = checkout + .main_worktree(gix::progress::Discard, &interrupt) + .map_err(CloneError::from)?; + + debug!(?path, branch, "ADJ clone and checkout completed"); + Ok(repo) +} + +// Stage beside ADJ so both installation and rollback use same-filesystem renames. +#[instrument(level = "debug", skip(url))] +pub(crate) fn git_replace(path: &Path, branch: &str, url: &str) -> Result { + // A relative path like "adj" has an empty parent; stage in the current directory. + let parent = path + .parent() + .filter(|p| !p.as_os_str().is_empty()) + .unwrap_or(Path::new(".")); + fs::create_dir_all(parent).map_err(GitError::Path)?; + let staging = tempfile::Builder::new() + .prefix(".adj-") + .tempdir_in(parent) + .map_err(GitError::Path)?; + let checkout = staging.path().join("new"); + let backup = staging.path().join("old"); + debug!(?checkout, ?backup, "created ADJ staging directory"); + + // Validate the new checkout and commit before touching the current cache. + // Early errors drop the staging directory, removing any incomplete clone. + let repo = git_clone(&checkout, branch, url)?; + let commit = get_commit_hash(&repo)?; + // The repository contains paths into staging and must not survive the move. + drop(repo); + + let had_cache = path.try_exists().map_err(GitError::Path)?; + if had_cache { + fs::rename(path, &backup).map_err(|source| GitError::Replacement { + source, + rollback: None, + })?; + debug!(?path, ?backup, "moved cached ADJ aside for replacement"); + } + if let Err(error) = fs::rename(&checkout, path) { + debug!(?path, %error, had_cache, "ADJ installation failed; restoring cache if present"); + if had_cache && let Err(rollback) = fs::rename(&backup, path) { + // Do not let temporary-directory cleanup delete the only remaining cache. + let saved = staging.keep(); + debug!(?backup, %rollback, "preserved ADJ backup after failed rollback"); + return Err(GitError::Replacement { + source: error, + rollback: Some((rollback, saved.join("old"))), + }); + } + if had_cache { + debug!(?path, "restored cached ADJ after installation failure"); + } + return Err(GitError::Replacement { + source: error, + rollback: None, + }); + } + + // Installation succeeded; cleanup now discards the old cache. A cleanup + // failure does not turn a successfully installed checkout into an error. + if let Err(error) = staging.close() { + warn!(?path, %error, "ADJ updated, but temporary directory cleanup failed"); + } + info!(?path, branch, commit = %commit, "ADJ updated"); + Ok(commit) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::{io, process::Command}; + + fn git(path: &Path, args: &[&str]) -> Result<(), Box> { + // Fixture commits need identity; disable signing, hooks, and templates + // so tests do not depend on the user's normal Git setup. + let output = Command::new("git") + .current_dir(path) + .args([ + "-c", + "user.name=ADJ tests", + "-c", + "user.email=adj@example.test", + "-c", + "commit.gpgSign=false", + "-c", + "core.hooksPath=", + "-c", + "init.templateDir=", + ]) + .args(args) + .output()?; + if !output.status.success() { + return Err(io::Error::other(String::from_utf8_lossy(&output.stderr)).into()); + } + Ok(()) + } + + fn source_repo(path: &Path) -> Result<(), Box> { + fs::create_dir(path)?; + git(path, &["init", "--initial-branch=main"])?; + fs::write(path.join("data.txt"), "main")?; + git(path, &["add", "."])?; + git(path, &["commit", "-m", "main data"])?; + // Leave HEAD on another branch to verify explicit clone branch selection. + git(path, &["checkout", "-b", "testing"])?; + fs::write(path.join("data.txt"), "testing")?; + git(path, &["commit", "-am", "testing data"]) + } + + // Verify branch selection, newer commits, and removal of stale cache files. + #[test] + fn clones_and_replaces_with_requested_branch() -> Result<(), Box> { + let temp = tempfile::tempdir()?; + let source = temp.path().join("source"); + let cache = temp.path().join("cache").join("adj"); + source_repo(&source)?; + let url = source.to_str().ok_or("non-UTF-8 test path")?; + + let first = git_replace(&cache, "main", url)?; + let main_hash = gix::open(&source)? + .find_reference("refs/heads/main")? + .id() + .to_string(); + assert_eq!(first, main_hash); + assert_eq!(fs::read_to_string(cache.join("data.txt"))?, "main"); + // Replacement must discard old cache files, not just update tracked ones. + fs::write(cache.join("stale.txt"), "discard this cache file")?; + + // Add a newer remote commit and request a different branch on replacement. + fs::write(source.join("data.txt"), "latest testing")?; + git(&source, &["commit", "-am", "updated testing data"])?; + let expected = get_commit_hash(&gix::open(&source)?)?; + assert_eq!(git_replace(&cache, "testing", url)?, expected); + assert_ne!(first, expected); + assert_eq!(get_commit_hash(&gix::open(&cache)?)?, expected); + assert_eq!( + fs::read_to_string(cache.join("data.txt"))?, + "latest testing" + ); + assert!(!cache.join("stale.txt").exists()); + // Successful replacement must also remove staging and backup directories. + assert_eq!( + fs::read_dir(cache.parent().ok_or("missing parent")?)?.count(), + 1 + ); + Ok(()) + } + + // Verify clone failures preserve cached ADJ and leave no staging debris. + #[test] + fn failed_clone_preserves_existing_cache() -> Result<(), Box> { + let temp = tempfile::tempdir()?; + let source = temp.path().join("source"); + let cache = temp.path().join("adj"); + source_repo(&source)?; + let url = source.to_str().ok_or("non-UTF-8 test path")?; + + // A failed first clone must not install a partial checkout. + assert!(git_replace(&cache, "missing-branch", url).is_err()); + assert!(!cache.exists()); + let expected = git_replace(&cache, "main", url)?; + fs::write(cache.join("stale.txt"), "keep on failure")?; + // Both an invalid branch and an unavailable source must leave ADJ intact. + assert!(git_replace(&cache, "missing-branch", url).is_err()); + let missing = temp.path().join("missing-source"); + assert!( + git_replace( + &cache, + "main", + missing.to_str().ok_or("non-UTF-8 test path")? + ) + .is_err() + ); + assert_eq!(get_commit_hash(&gix::open(&cache)?)?, expected); + assert_eq!(fs::read_to_string(cache.join("data.txt"))?, "main"); + assert_eq!( + fs::read_to_string(cache.join("stale.txt"))?, + "keep on failure" + ); + assert_eq!(fs::read_dir(temp.path())?.count(), 2); + Ok(()) + } +} diff --git a/backend/crates/core/src/git/repository.rs b/backend/crates/core/src/git/repository.rs new file mode 100644 index 000000000..b844c29e9 --- /dev/null +++ b/backend/crates/core/src/git/repository.rs @@ -0,0 +1,113 @@ +use super::{GitError, update::unwrap_repo_path}; +use gix::Repository; +use std::path::PathBuf; +use tracing::{debug, instrument}; + +/// Concrete repository failures wrapped by [`GitError::Repository`]. +/// +/// Variants identify the failed read operation and preserve the original `gix` +/// error as their source. Only [`RepositoryError::ReferenceItem`] has a dynamic +/// source because the reference iterator returns boxed errors. +#[derive(Debug, thiserror::Error)] +pub enum RepositoryError { + /// An existing repository could not be opened or validated. + #[error("repository could not be opened: {0}")] + Open(#[from] gix::open::Error), + + /// HEAD could not be resolved to a commit ID, including an unborn HEAD. + #[error("HEAD could not be resolved: {0}")] + Head(#[from] gix::reference::head_id::Error), + + /// The repository's reference storage could not be opened. + #[error("reference storage could not be opened: {0}")] + References(#[from] gix::reference::iter::Error), + + /// Iteration over cached remote-tracking references could not be initialized. + #[error("reference iteration could not be initialized: {0}")] + InitReferences(#[from] gix::reference::iter::init::Error), + + /// An individual reference could not be read during iteration. + /// The boxed source retains its original type and error chain. + #[error("reference could not be read: {0}")] + ReferenceItem(#[from] Box), + + /// Looking up a reference failed; an absent reference is not an error. + #[error("reference lookup failed: {0}")] + FindReference(#[from] gix::reference::find::Error), +} + +/// Opens an existing repository without fetching or changing its checkout. +/// +/// `path` is the repository directory. If omitted, it defaults to +/// `/hyperloop-control-station/adj`. This does not create ADJ. +/// +/// # Errors +/// +/// Returns [`GitError::Path`] if the cache directory cannot be resolved, or +/// [`GitError::Repository`] if the repository is missing, invalid, or inaccessible. +#[instrument(level = "debug", err(level = "warn"))] +pub fn open_repo(path: Option) -> Result { + let path = unwrap_repo_path(path)?; + + let repo = gix::open(path).map_err(RepositoryError::from)?; + + debug!(git_dir = ?repo.path(), "opened repository"); + Ok(repo) +} + +/// Lists locally cached remote-tracking branches without contacting the remote. +/// +/// `path` identifies the repository directory. Names retain the remote prefix, +/// such as `origin/main`; the results can be stale, are not explicitly sorted, +/// and may include symbolic references such as `origin/HEAD`. +/// +/// # Errors +/// +/// Returns [`GitError::Repository`] if the repository cannot be opened or its +/// references cannot be read. +#[instrument(level = "debug", err(level = "warn"))] +pub fn list_remote_branches(path: PathBuf) -> Result, GitError> { + let repo = gix::open(&path).map_err(RepositoryError::from)?; + + let mut branches = Vec::new(); + + // Inspect local remote-tracking refs, not the remote server's advertised refs. + for reference in repo + .references() + .map_err(RepositoryError::from)? + .remote_branches() + .map_err(RepositoryError::from)? + { + let reference = reference.map_err(RepositoryError::from)?; + branches.push(reference.name().shorten().to_string()); + } + + debug!( + ?path, + count = branches.len(), + "listed cached remote-tracking branches" + ); + Ok(branches) +} + +/// Checks whether a remote-tracking branch is cached in `repo`. +/// +/// `remote` is the remote name, such as `origin`; `name` is the branch name, such +/// as `main` or `feature/example`. Looks up `refs/remotes//` without +/// contacting the server or checking local branches. `false` means the reference +/// is absent locally, not necessarily that the branch is absent on the server. +/// +/// # Errors +/// +/// Returns [`GitError::Repository`] if the reference name is invalid or the +/// reference cannot be read. A missing reference returns `Ok(false)`. +#[instrument(level = "debug", skip(repo), err(level = "warn"))] +pub fn branch_exists(repo: &gix::Repository, remote: &str, name: &str) -> Result { + let full = format!("refs/remotes/{remote}/{name}"); + let exists = repo + .try_find_reference(full.as_str()) + .map_err(RepositoryError::from)? + .is_some(); + debug!(reference = %full, exists, "checked cached remote-tracking branch"); + Ok(exists) +} diff --git a/backend/crates/core/src/git/update.rs b/backend/crates/core/src/git/update.rs new file mode 100644 index 000000000..8fa1fd1fb --- /dev/null +++ b/backend/crates/core/src/git/update.rs @@ -0,0 +1,135 @@ +use super::{GitError, RepositoryError, clone::git_replace}; +use gix::Repository; +use std::{ + io, + net::{Ipv4Addr, SocketAddr, TcpStream}, + path::PathBuf, + time::Duration, +}; +use tracing::{debug, info, instrument, warn}; + +const ADJ_URL: &str = "https://github.com/Hyperloop-UPV/adj.git"; + +/// Downloads the requested ADJ branch, replacing the application-managed cache. +/// +/// `path` is the repository directory, not its parent. If omitted, it defaults to +/// `/hyperloop-control-station/adj`. `branch` selects the branch +/// to clone when the connectivity probe succeeds. +/// +/// Online, local edits and commits are discarded only after a fresh checkout +/// succeeds. Offline, the current cached checkout is used without switching +/// branches, so it may differ from `branch`. Returns its full commit hash. +/// +/// This performs blocking network and filesystem operations. Call during startup, +/// before readers use ADJ, with only one updater at a time. Replacement uses two +/// renames and is not atomic across a process crash. +/// +/// # Errors +/// +/// Returns [`GitError::UnavailableOffline`] if ADJ is missing offline. Other +/// variants identify path, repository, cloning, checkout, or replacement failures. +/// Clone errors do not automatically fall back to the cache. If restoration fails, +/// [`GitError::Replacement`] preserves both errors and the backup's location. +// Log returned errors at the public boundary; helpers trace progress without duplicate warnings. +#[instrument(level = "debug", err(level = "warn"))] +pub fn update_adj(path: Option, branch: &str) -> Result { + let (repo, dir) = adj_exists_repo(path)?; + + if check_internet_connection() { + info!(path = ?dir, branch, "starting ADJ update"); + // Release repository handles before moving the existing cache directory. + drop(repo); + git_replace(&dir, branch, ADJ_URL) + } else if let Some(repo) = repo { + warn!(path = ?dir, requested_branch = branch, + "connectivity probe failed; using cached ADJ without switching branches"); + get_commit_hash(&repo) + } else { + Err(GitError::UnavailableOffline) + } +} + +// Checks external TCP connectivity with a three-second timeout, not GitHub availability. +#[instrument(level = "info")] +fn check_internet_connection() -> bool { + // Use a fixed IPv4 address to avoid a separate DNS lookup and its timeout. + let address = SocketAddr::from((Ipv4Addr::new(1, 1, 1, 1), 443)); + let timeout = Duration::from_secs(3); + + // Attempt only the TCP handshake; no application data is sent. + match TcpStream::connect_timeout(&address, timeout) { + Ok(_stream) => { + info!(%address, "connectivity probe succeeded"); + true + } + Err(error) => { + // Record why the probe failed without treating it as an application error. + info!(%address, %error, "connectivity probe failed"); + false + } + } +} + +// Preserve explicit paths; otherwise use the platform's user cache directory. +pub(crate) fn unwrap_repo_path(path: Option) -> Result { + let dir = match path { + Some(path) => path, + None => { + let path = dirs::cache_dir().ok_or_else(|| { + GitError::Path(io::Error::new( + io::ErrorKind::NotFound, + "cache directory not found", + )) + })?; + + path.join("hyperloop-control-station").join("adj") + } + }; + + debug!(path = ?dir, "resolved ADJ repository path"); + Ok(dir) +} + +// Only an absent directory is a cache miss; invalid repositories remain errors. +pub(crate) fn adj_exists_repo( + path: Option, +) -> Result<(Option, PathBuf), GitError> { + let dir = unwrap_repo_path(path)?; + + let res = if dir.try_exists().map_err(GitError::Path)? { + Some(gix::open(&dir).map_err(RepositoryError::from)?) + } else { + None + }; + + debug!(path = ?dir, exists = res.is_some(), "checked local ADJ repository"); + Ok((res, dir)) +} + +// Report the checked-out HEAD, which can differ from the requested branch offline. +pub(crate) fn get_commit_hash(repo: &Repository) -> Result { + let hash = repo.head_id().map_err(RepositoryError::from)?.to_string(); + + // Log only the commit ID, not potentially large or sensitive message text. + debug!(commit = %hash, "read checked-out ADJ commit"); + Ok(hash) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + + // Distinguish missing, invalid, and valid local repositories. + #[test] + fn detects_missing_and_invalid_repositories() -> Result<(), Box> { + let temp = tempfile::tempdir()?; + let path = temp.path().join("adj"); + assert!(adj_exists_repo(Some(path.clone()))?.0.is_none()); + fs::create_dir(&path)?; + assert!(adj_exists_repo(Some(path.clone())).is_err()); + gix::init(&path)?; + assert!(adj_exists_repo(Some(path))?.0.is_some()); + Ok(()) + } +} From f8f04b02849e91b818380b0e6aebfad48827e624 Mon Sep 17 00:00:00 2001 From: Pau Minguet Date: Mon, 12 Oct 2026 17:11:06 +0200 Subject: [PATCH 06/11] docs(backend, git): update README --- backend/crates/core/src/git/README.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/backend/crates/core/src/git/README.md b/backend/crates/core/src/git/README.md index 950249435..f2e3615ed 100644 --- a/backend/crates/core/src/git/README.md +++ b/backend/crates/core/src/git/README.md @@ -6,7 +6,7 @@ when updating online. ## Workflow -`update_adj(path, branch)` returns the checked-out commit hash and full message. +`update_adj(path, branch)` returns the full hash of the checked-out commit. A custom path identifies the ADJ repository itself. The default is `/hyperloop-control-station/adj`, using `dirs::cache_dir()`. An existing invalid repository or inaccessible path returns an error. @@ -18,7 +18,7 @@ availability. ### Online - Shallow-clone the requested branch into a temporary directory beside ADJ. -- Complete checkout and read its commit information before touching the cache. +- Complete checkout and read its commit hash before touching the cache. - Move the old cache aside and install the new checkout. - Restore the old cache if installation fails. If restoration also fails, preserve the backup and include its location in the error. @@ -30,7 +30,7 @@ switch is needed because the checkout is replaced. ### Offline -- If ADJ exists, return its current commit without updating or switching branches. +- If ADJ exists, return its current commit hash without updating or switching branches. - Otherwise, return `ADJ is not available locally and cannot be downloaded`. The cached branch may differ from the requested branch when offline. @@ -49,6 +49,15 @@ updater at a time. The backup and installation renames are not jointly atomic across a process crash. The connectivity probe can succeed when GitHub is unreachable, or fail when GitHub is reachable. +## Errors + +Public helpers return `GitError` with five categories: `Path`, `Repository`, +`Clone`, `Replacement`, and `UnavailableOffline`. `RepositoryError` and +`CloneError` group the concrete `gix` causes inside the corresponding categories. +Only individual reference-iteration errors retain a dynamic source, as exposed +by `gix`. If rollback fails, `Replacement` also retains the restoration error +and backup path for recovery. + ## Verification Tests use temporary local Git repositories and the Git CLI to create fixtures; From a33f6505876c2c3d522a9c509b6635156622845a Mon Sep 17 00:00:00 2001 From: Pau Minguet Date: Mon, 12 Oct 2026 17:11:30 +0200 Subject: [PATCH 07/11] update(backend, git): update cargo packages --- backend/Cargo.lock | 62 +++++++++++++++++++++++----------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 5a9a19ea9..3d6daa45d 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -46,9 +46,9 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-lc-rs" -version = "1.18.1" +version = "1.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b281d307588d634de920874890732659e2e7672f72b5e10e81badc1a8a83621e" +checksum = "faac5829c2b74c28f830747e7818ccfb684261b5f48a1118b1e2a13d36dfab13" dependencies = [ "aws-lc-sys", "zeroize", @@ -56,9 +56,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.45.0" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bff6c3b54fad79a2e60b8102caf565819711497c1f5f092f49508e2f5c31b27" +checksum = "f1622d8446a2d4b2ce0c7eefc73dd43a99779028d5ee5c2dd8073a658ba8a2bc" dependencies = [ "cc", "cmake", @@ -137,9 +137,9 @@ checksum = "7354288c522e7e980fafd2075d63d1285794c3a6a16cdd492f189ea406e5f18b" [[package]] name = "cc" -version = "1.6.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f74872d07caf508b30a21f6836e7d7016a2eaf7d9ff4f48deaa58cd8a0407630" +checksum = "6651c9ed80effdc7db0ff72512157f901af5e3549e341e24b1dd4887d836d838" dependencies = [ "find-msvc-tools", "jobserver", @@ -390,7 +390,7 @@ checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -2624,14 +2624,14 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] name = "serde_json" -version = "1.0.151" +version = "1.0.154" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" dependencies = [ "itoa", "memchr", @@ -2642,9 +2642,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +checksum = "4e7523beb55eece201a2356bee0bbca0d1ab466c14c07703b2e0ee6d42cb0c2c" dependencies = [ "serde_core", ] @@ -2729,9 +2729,9 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.16.2" +version = "1.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e" +checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b" [[package]] name = "socket2" @@ -2805,9 +2805,9 @@ dependencies = [ [[package]] name = "syn" -version = "3.0.6" +version = "3.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" dependencies = [ "proc-macro2", "quote", @@ -2831,7 +2831,7 @@ checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -2864,7 +2864,7 @@ checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -2946,7 +2946,7 @@ checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -2961,9 +2961,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.19" +version = "0.7.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +checksum = "e464cf451ba96ebfc6f9b6542f17ee8b8956e33f1e40d9690624e59d7a7f8a4b" dependencies = [ "bytes", "futures-core", @@ -2999,18 +2999,18 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.1.3+spec-1.1.0" +version = "1.1.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c" dependencies = [ "winnow 1.0.4", ] [[package]] name = "toml_writer" -version = "1.1.2+spec-1.1.0" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" +checksum = "06bdbd8cfc056b8d2e2e85f29b56a3bdbecb527cef81eb39e3e7b98af4652770" [[package]] name = "tower" @@ -3174,9 +3174,9 @@ checksum = "7eec5d1121208364f6793f7d2e222bf75a915c19557537745b195b253dd64217" [[package]] name = "unicode-ident" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" [[package]] name = "unicode-normalization" @@ -3291,7 +3291,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", "wasm-bindgen-shared", ] @@ -3489,7 +3489,7 @@ checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", "synstructure", ] @@ -3510,7 +3510,7 @@ checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", "synstructure", ] @@ -3550,7 +3550,7 @@ checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] From 149be35ab50be950cfb59d97cc370b682d953208 Mon Sep 17 00:00:00 2001 From: Pau Minguet Date: Mon, 12 Oct 2026 21:28:58 +0200 Subject: [PATCH 08/11] fix(backend, git): fix implementation of list_remote_branches and branch_exists. Also add tests for new implementation --- backend/crates/core/src/git.rs | 8 +- backend/crates/core/src/git/repository.rs | 283 +++++++++++++++++----- 2 files changed, 229 insertions(+), 62 deletions(-) diff --git a/backend/crates/core/src/git.rs b/backend/crates/core/src/git.rs index f73566257..46c854cef 100644 --- a/backend/crates/core/src/git.rs +++ b/backend/crates/core/src/git.rs @@ -12,9 +12,7 @@ pub use update::update_adj; /// /// Five categories identify the failed operation and preserve its underlying /// error as [`std::error::Error::source`]. [`RepositoryError`] and [`CloneError`] -/// retain concrete `gix` causes and are boxed to keep this enum small. Only -/// [`RepositoryError::ReferenceItem`] uses a dynamically typed cause, as exposed -/// by the `gix` iterator. +/// retain concrete `gix` causes and are boxed to keep this enum small. /// /// A failed connectivity probe is not itself an error when cached ADJ exists. /// If no cache exists, the operation returns [`GitError::UnavailableOffline`]. @@ -24,8 +22,8 @@ pub enum GitError { #[error("ADJ path operation failed: {0}")] Path(#[source] std::io::Error), - /// Opening the repository or reading its references or HEAD failed. - /// An absent reference is not itself an error. + /// Opening the repository, reading HEAD, or querying local or remote refs failed. + /// A branch missing from a successful remote listing is not an error. #[error("ADJ repository operation failed: {0}")] Repository(#[source] Box), diff --git a/backend/crates/core/src/git/repository.rs b/backend/crates/core/src/git/repository.rs index b844c29e9..eeb8b2ff6 100644 --- a/backend/crates/core/src/git/repository.rs +++ b/backend/crates/core/src/git/repository.rs @@ -5,9 +5,8 @@ use tracing::{debug, instrument}; /// Concrete repository failures wrapped by [`GitError::Repository`]. /// -/// Variants identify the failed read operation and preserve the original `gix` -/// error as their source. Only [`RepositoryError::ReferenceItem`] has a dynamic -/// source because the reference iterator returns boxed errors. +/// Variants identify local reads or remote-reference queries and preserve the +/// original `gix` error as their source. #[derive(Debug, thiserror::Error)] pub enum RepositoryError { /// An existing repository could not be opened or validated. @@ -18,22 +17,21 @@ pub enum RepositoryError { #[error("HEAD could not be resolved: {0}")] Head(#[from] gix::reference::head_id::Error), - /// The repository's reference storage could not be opened. - #[error("reference storage could not be opened: {0}")] - References(#[from] gix::reference::iter::Error), + /// No default fetch remote could be determined, including ambiguous configurations. + #[error("no default fetch remote could be determined")] + NoDefaultRemote, - /// Iteration over cached remote-tracking references could not be initialized. - #[error("reference iteration could not be initialized: {0}")] - InitReferences(#[from] gix::reference::iter::init::Error), + /// The selected default remote could not be loaded from repository configuration. + #[error("default fetch remote could not be loaded: {0}")] + FindDefaultRemote(#[from] gix::remote::find::existing::Error), - /// An individual reference could not be read during iteration. - /// The boxed source retains its original type and error chain. - #[error("reference could not be read: {0}")] - ReferenceItem(#[from] Box), + /// A transport connection to the selected remote could not be prepared. + #[error("remote connection failed: {0}")] + RemoteConnect(#[from] gix::remote::connect::Error), - /// Looking up a reference failed; an absent reference is not an error. - #[error("reference lookup failed: {0}")] - FindReference(#[from] gix::reference::find::Error), + /// The remote handshake or query of advertised references failed. + #[error("remote reference query failed: {0}")] + RemoteRefMap(#[from] gix::remote::ref_map::Error), } /// Opens an existing repository without fetching or changing its checkout. @@ -55,59 +53,230 @@ pub fn open_repo(path: Option) -> Result { Ok(repo) } -/// Lists locally cached remote-tracking branches without contacting the remote. +/// Lists the branch names advertised by the repository's default fetch remote. /// -/// `path` identifies the repository directory. Names retain the remote prefix, -/// such as `origin/main`; the results can be stale, are not explicitly sorted, -/// and may include symbolic references such as `origin/HEAD`. +/// `repo` supplies the configuration used to select the default fetch remote. +/// Returns sorted short names such as `main` or `feature/example`, excluding tags +/// and `HEAD`. Queries the remote without downloading objects or updating local +/// references. Configured fetch refspecs do not limit the advertised branches. +/// This is a blocking operation and requires access to the remote. /// /// # Errors /// -/// Returns [`GitError::Repository`] if the repository cannot be opened or its -/// references cannot be read. -#[instrument(level = "debug", err(level = "warn"))] -pub fn list_remote_branches(path: PathBuf) -> Result, GitError> { - let repo = gix::open(&path).map_err(RepositoryError::from)?; - - let mut branches = Vec::new(); - - // Inspect local remote-tracking refs, not the remote server's advertised refs. - for reference in repo - .references() - .map_err(RepositoryError::from)? - .remote_branches() - .map_err(RepositoryError::from)? - { - let reference = reference.map_err(RepositoryError::from)?; - branches.push(reference.name().shorten().to_string()); - } +/// Returns [`GitError::Repository`] if no default remote can be determined, or +/// remote configuration, connection, or reference discovery fails. Does not fall +/// back to cached branches on failure. +#[instrument(level = "debug", skip(repo), err(level = "warn"))] +pub fn list_remote_branches(repo: &Repository) -> Result, GitError> { + let remote = repo + .find_default_remote(gix::remote::Direction::Fetch) + .ok_or(RepositoryError::NoDefaultRemote)? + .map_err(RepositoryError::from)?; + + debug!( + git_dir = ?repo.path(), + "querying branches advertised by the default fetch remote" + ); + let conexion = remote + .connect(gix::remote::Direction::Fetch) + .map_err(RepositoryError::from)?; + let (mapa, _handshake) = conexion + .ref_map( + gix::progress::Discard, + gix::remote::ref_map::Options { + // Protocol v2 must not restrict ls-refs to a single-branch refspec. + prefix_from_spec_as_filter_on_remote: false, + ..Default::default() + }, + ) + .map_err(RepositoryError::from)?; + + let mut ramas: Vec = mapa + .remote_refs + .iter() + .filter_map(|r| { + let (nombre, _, _) = r.unpack(); + nombre + .to_string() + .strip_prefix("refs/heads/") + .map(str::to_string) + }) + .collect(); + ramas.sort(); debug!( - ?path, - count = branches.len(), - "listed cached remote-tracking branches" + git_dir = ?repo.path(), + count = ramas.len(), + "listed advertised remote branches" ); - Ok(branches) + Ok(ramas) } -/// Checks whether a remote-tracking branch is cached in `repo`. +/// Checks whether the default fetch remote advertises the requested branch. /// -/// `remote` is the remote name, such as `origin`; `name` is the branch name, such -/// as `main` or `feature/example`. Looks up `refs/remotes//` without -/// contacting the server or checking local branches. `false` means the reference -/// is absent locally, not necessarily that the branch is absent on the server. +/// `repo` selects the remote and `name` is its exact short branch name, such as +/// `main` or `feature/example`, without an `origin/` prefix. Calls +/// [`list_remote_branches`] and returns `true` if the name is in the result. +/// `false` means the query succeeded but the remote did not advertise that name. +/// This is a blocking remote query, not a check of cached references. /// /// # Errors /// -/// Returns [`GitError::Repository`] if the reference name is invalid or the -/// reference cannot be read. A missing reference returns `Ok(false)`. -#[instrument(level = "debug", skip(repo), err(level = "warn"))] -pub fn branch_exists(repo: &gix::Repository, remote: &str, name: &str) -> Result { - let full = format!("refs/remotes/{remote}/{name}"); - let exists = repo - .try_find_reference(full.as_str()) - .map_err(RepositoryError::from)? - .is_some(); - debug!(reference = %full, exists, "checked cached remote-tracking branch"); +/// Propagates the original [`GitError`] from [`list_remote_branches`]. A missing +/// remote or failed connection is an error, never `Ok(false)`. +// The delegated listing logs failures; do not emit the same warning twice. +#[instrument(level = "debug", skip(repo))] +pub fn branch_exists(repo: &Repository, name: &str) -> Result { + let branches = list_remote_branches(repo)?; + let exists = branches.iter().any(|branch| branch == name); + debug!(name, exists, "checked advertised remote branch"); Ok(exists) } + +#[cfg(test)] +mod tests { + use super::*; + use std::{fs, io, path::Path, process::Command}; + + type TestResult = Result>; + + fn git(path: &Path, args: &[&str]) -> TestResult { + // Provide fixture identity and disable user signing, hooks, and templates. + let output = Command::new("git") + .current_dir(path) + .args([ + "-c", + "user.name=ADJ tests", + "-c", + "user.email=adj@example.test", + "-c", + "commit.gpgSign=false", + "-c", + "tag.gpgSign=false", + "-c", + "core.hooksPath=", + "-c", + "init.templateDir=", + ]) + .args(args) + .output()?; + if !output.status.success() { + return Err(io::Error::other(String::from_utf8_lossy(&output.stderr)).into()); + } + Ok(()) + } + + fn local_remote() -> TestResult<(tempfile::TempDir, Repository)> { + let temp = tempfile::tempdir()?; + let remote = temp.path().join("remote"); + let client = temp.path().join("client"); + fs::create_dir(&remote)?; + fs::create_dir(&client)?; + git(&remote, &["init", "--initial-branch=main"])?; + fs::write(remote.join("data.txt"), "ADJ fixture")?; + git(&remote, &["add", "."])?; + git(&remote, &["commit", "-m", "fixture data"])?; + git(&remote, &["branch", "software"])?; + git(&remote, &["branch", "feature/example"])?; + git(&remote, &["tag", "v1"])?; + + // No clone or fetch: only main is tracked, and no remote refs are cached. + git(&client, &["init", "--initial-branch=main"])?; + git( + &client, + &[ + "remote", + "add", + "-t", + "main", + "origin", + remote.to_str().ok_or("non-UTF-8 test path")?, + ], + )?; + // Exercise ls-refs prefix filtering rather than protocol v1's full advertisement. + git(&client, &["config", "protocol.version", "2"])?; + let repo = gix::open(&client)?; + Ok((temp, repo)) + } + + // List all advertised branches despite a main-only refspec, without fetching. + #[test] + fn lists_all_remote_branches_without_fetching() -> TestResult { + let (temp, repo) = local_remote()?; + assert_eq!( + list_remote_branches(&repo)?, + ["feature/example", "main", "software"] + ); + for name in ["feature/example", "main", "software"] { + let reference = format!("refs/remotes/origin/{name}"); + assert!(repo.try_find_reference(reference.as_str())?.is_none()); + } + let remote_commit = gix::open(temp.path().join("remote"))?.head_id()?.detach(); + assert!(repo.find_object(remote_commit).is_err()); + Ok(()) + } + + // Remote existence must not depend on having the branch cached locally. + #[test] + fn finds_uncached_remote_branch() -> TestResult { + let (_temp, repo) = local_remote()?; + assert!( + repo.try_find_reference("refs/remotes/origin/feature/example")? + .is_none() + ); + assert!(branch_exists(&repo, "feature/example")?); + Ok(()) + } + + // Only exact short names match after a successful remote query. + #[test] + fn rejects_missing_and_remote_prefixed_branch_names() -> TestResult { + let (_temp, repo) = local_remote()?; + assert!(!branch_exists(&repo, "missing-branch")?); + assert!(!branch_exists(&repo, "origin/main")?); + Ok(()) + } + + // Missing remote configuration is an explicit error for both helpers. + #[test] + fn propagates_missing_default_remote() -> TestResult { + let temp = tempfile::tempdir()?; + let repo = gix::init(temp.path())?; + for result in [ + list_remote_branches(&repo).map(|_| ()), + branch_exists(&repo, "main").map(|_| ()), + ] { + assert!(matches!(result, Err(GitError::Repository(error)) + if matches!(*error, RepositoryError::NoDefaultRemote))); + } + Ok(()) + } + + // An unreachable source must remain an error, not false or an empty listing. + #[test] + fn propagates_inaccessible_remote() -> TestResult { + let (temp, _) = local_remote()?; + let client = temp.path().join("client"); + let missing = temp.path().join("missing-remote"); + git( + &client, + &[ + "remote", + "set-url", + "origin", + missing.to_str().ok_or("non-UTF-8 test path")?, + ], + )?; + // Reopen to pick up the changed remote URL instead of the old config snapshot. + let repo = gix::open(&client)?; + for result in [ + list_remote_branches(&repo).map(|_| ()), + branch_exists(&repo, "main").map(|_| ()), + ] { + assert!(matches!(result, Err(GitError::Repository(error)) + if matches!(*error, + RepositoryError::RemoteConnect(_) | RepositoryError::RemoteRefMap(_)))); + } + Ok(()) + } +} From cda8fb6aa3acfbeb24f47141d69aaf9caf8d2b66 Mon Sep 17 00:00:00 2001 From: Pau Minguet Date: Mon, 12 Oct 2026 21:29:07 +0200 Subject: [PATCH 09/11] docs(backend, git): small changes in README --- backend/crates/core/src/git/README.md | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/backend/crates/core/src/git/README.md b/backend/crates/core/src/git/README.md index f2e3615ed..e06a8f6fe 100644 --- a/backend/crates/core/src/git/README.md +++ b/backend/crates/core/src/git/README.md @@ -1,8 +1,6 @@ # ADJ Git helpers -This module manages ADJ as an application-owned, replaceable data cache using -`gix`, with `tracing` for logging. It does not preserve local edits or commits -when updating online. +This module downloads and updates the local [ADJ repository](https://github.com/Hyperloop-UPV/adj) using [`gix`](https://crates.io/crates/gix), with [`tracing`](https://crates.io/crates/tracing) for logging. It supports branch selection and offline use of an existing local checkout. ## Workflow @@ -17,6 +15,8 @@ availability. ### Online +Online updates replace the local checkout and do not preserve local edits or commits. + - Shallow-clone the requested branch into a temporary directory beside ADJ. - Complete checkout and read its commit hash before touching the cache. - Move the old cache aside and install the new checkout. @@ -25,8 +25,8 @@ availability. - Remove the old cache after successful installation. A clone error leaves the old cache untouched and is returned directly; it does -not automatically fall back to offline operation. No fetch, merge, or branch -switch is needed because the checkout is replaced. +not automatically fall back to offline operation. No separate fetch of the +existing cache, merge, or branch switch is needed because the checkout is replaced. ### Offline @@ -38,9 +38,12 @@ The cached branch may differ from the requested branch when offline. ## Other Helpers - `open_repo` opens the custom or default repository path. -- `list_remote_branches` lists cached remote-tracking references, without making - a network request. Results can be stale and include the remote name. -- `branch_exists` checks for a cached `refs/remotes//` reference. +- `list_remote_branches(repo)` queries the default fetch remote and returns sorted short + branch names, without downloading objects or updating local references. It + requires remote access and does not fall back to cached branches on failure. +- `branch_exists(repo, name)` uses that listing to check an exact short branch + name. Query errors are propagated; `false` means a successful query did not + find the branch, not that the remote was unreachable. ## Limitations @@ -54,9 +57,8 @@ unreachable, or fail when GitHub is reachable. Public helpers return `GitError` with five categories: `Path`, `Repository`, `Clone`, `Replacement`, and `UnavailableOffline`. `RepositoryError` and `CloneError` group the concrete `gix` causes inside the corresponding categories. -Only individual reference-iteration errors retain a dynamic source, as exposed -by `gix`. If rollback fails, `Replacement` also retains the restoration error -and backup path for recovery. +If rollback fails, `Replacement` also retains the restoration error and backup +path for recovery. ## Verification From f5821af746ecd441fe1835dd199737be93249037 Mon Sep 17 00:00:00 2001 From: Pau Minguet Date: Wed, 14 Oct 2026 18:33:26 +0200 Subject: [PATCH 10/11] refactor(backend, git): separete errors in specific file --- backend/crates/core/src/git.rs | 69 +----------- backend/crates/core/src/git/clone.rs | 39 ++----- backend/crates/core/src/git/error.rs | 121 ++++++++++++++++++++++ backend/crates/core/src/git/repository.rs | 33 +----- 4 files changed, 134 insertions(+), 128 deletions(-) create mode 100644 backend/crates/core/src/git/error.rs diff --git a/backend/crates/core/src/git.rs b/backend/crates/core/src/git.rs index 46c854cef..3418342c0 100644 --- a/backend/crates/core/src/git.rs +++ b/backend/crates/core/src/git.rs @@ -1,72 +1,11 @@ //! Blocking Git helpers for ADJ, an application-owned, replaceable data cache. mod clone; +mod error; mod repository; mod update; -pub use clone::CloneError; -pub use repository::{RepositoryError, branch_exists, list_remote_branches, open_repo}; +pub use error::{CloneError, GitError, RepositoryError}; +pub use repository::{branch_exists, list_remote_branches, open_repo}; +pub(crate) use update::unwrap_repo_path; pub use update::update_adj; - -/// Errors produced while reading, downloading, or replacing the ADJ cache. -/// -/// Five categories identify the failed operation and preserve its underlying -/// error as [`std::error::Error::source`]. [`RepositoryError`] and [`CloneError`] -/// retain concrete `gix` causes and are boxed to keep this enum small. -/// -/// A failed connectivity probe is not itself an error when cached ADJ exists. -/// If no cache exists, the operation returns [`GitError::UnavailableOffline`]. -#[derive(Debug, thiserror::Error)] -pub enum GitError { - /// Resolving, inspecting, or preparing the cache directory failed. - #[error("ADJ path operation failed: {0}")] - Path(#[source] std::io::Error), - - /// Opening the repository, reading HEAD, or querying local or remote refs failed. - /// A branch missing from a successful remote listing is not an error. - #[error("ADJ repository operation failed: {0}")] - Repository(#[source] Box), - - /// Preparing, downloading, or checking out the requested branch failed. - /// - /// This can indicate a network, configuration, or remote-reference failure; - /// it does not necessarily mean that internet connectivity is unavailable. - #[error("ADJ clone failed: {0}")] - Clone(#[source] Box), - - /// Moving the old cache aside or installing the new checkout failed. - /// - /// When restoration also fails, `rollback` holds its error and the preserved - /// backup's location. Otherwise, the old cache was restored or never moved. - #[error( - "ADJ cache replacement failed: {source}{recovery}", - recovery = .rollback.as_ref().map(|(error, path)| { - format!("; rollback failed: {error}; backup kept at {}", path.display()) - }).unwrap_or_default() - )] - Replacement { - /// The original error moving the old cache or installing the new checkout. - #[source] - source: std::io::Error, - /// Restoration error and preserved backup path, if rollback failed. - rollback: Option<(std::io::Error, std::path::PathBuf)>, - }, - - /// The connectivity probe failed and no local ADJ cache was available. - #[error("ADJ is not available locally and cannot be downloaded")] - UnavailableOffline, -} - -// Convert repository errors for '?' propagation, boxing the concrete error to keep GitError small. -impl From for GitError { - fn from(error: RepositoryError) -> Self { - Self::Repository(Box::new(error)) - } -} - -// Convert clone errors for '?' propagation, boxing the concrete error to keep GitError small. -impl From for GitError { - fn from(error: CloneError) -> Self { - Self::Clone(Box::new(error)) - } -} diff --git a/backend/crates/core/src/git/clone.rs b/backend/crates/core/src/git/clone.rs index 006f127ab..bccc73c9f 100644 --- a/backend/crates/core/src/git/clone.rs +++ b/backend/crates/core/src/git/clone.rs @@ -1,38 +1,13 @@ -use super::{GitError, update::get_commit_hash}; +use super::{ + error::{CloneError, GitError}, + update::get_commit_hash, +}; use gix::Repository; use std::{fs, path::Path, sync::atomic::AtomicBool}; use tracing::{debug, info, instrument, warn}; const GIT_REMOTE: &str = "origin"; -/// Concrete clone failures wrapped by [`GitError::Clone`]. -/// -/// Preparation, name validation, fetching, and checkout expose different `gix` -/// error types. These variants retain each original error as their source. -#[derive(Debug, thiserror::Error)] -pub enum CloneError { - /// Clone preparation failed before downloading repository data. - #[error("clone preparation failed: {0}")] - Prepare(#[from] gix::clone::Error), - - /// The configured remote name was invalid. - #[error("remote name is invalid: {0}")] - RemoteName(#[from] gix::remote::name::Error), - - /// The requested branch name was not a valid Git reference name. - #[error("branch name is invalid: {0}")] - BranchName(#[from] gix::validate::reference::name::Error), - - /// Fetching failed, including a missing requested branch. - /// This can indicate network, configuration, or remote-reference failures. - #[error("fetch failed: {0}")] - Fetch(#[from] gix::clone::fetch::Error), - - /// Downloaded data could not be checked out into the working tree. - #[error("checkout failed: {0}")] - Checkout(#[from] gix::clone::checkout::main_worktree::Error), -} - // Fetch and check out the requested branch instead of switching an existing repo. #[instrument(level = "debug", skip(url))] fn git_clone(path: &Path, branch: &str, url: &str) -> Result { @@ -171,7 +146,8 @@ mod tests { // Verify branch selection, newer commits, and removal of stale cache files. #[test] - fn clones_and_replaces_with_requested_branch() -> Result<(), Box> { + fn clones_and_replaces_with_requested_branch() + -> Result<(), Box> { let temp = tempfile::tempdir()?; let source = temp.path().join("source"); let cache = temp.path().join("cache").join("adj"); @@ -210,7 +186,8 @@ mod tests { // Verify clone failures preserve cached ADJ and leave no staging debris. #[test] - fn failed_clone_preserves_existing_cache() -> Result<(), Box> { + fn failed_clone_preserves_existing_cache() + -> Result<(), Box> { let temp = tempfile::tempdir()?; let source = temp.path().join("source"); let cache = temp.path().join("adj"); diff --git a/backend/crates/core/src/git/error.rs b/backend/crates/core/src/git/error.rs new file mode 100644 index 000000000..ec3c3f69b --- /dev/null +++ b/backend/crates/core/src/git/error.rs @@ -0,0 +1,121 @@ +/// Errors produced while reading, downloading, or replacing the ADJ cache. +/// +/// Five categories identify the failed operation and preserve its underlying +/// error as [`std::error::Error::source`]. [`RepositoryError`] and [`CloneError`] +/// retain concrete `gix` causes and are boxed to keep this enum small. +/// +/// A failed connectivity probe is not itself an error when cached ADJ exists. +/// If no cache exists, the operation returns [`GitError::UnavailableOffline`]. +#[derive(Debug, thiserror::Error)] +pub enum GitError { + /// Resolving, inspecting, or preparing the cache directory failed. + #[error("ADJ path operation failed: {0}")] + Path(#[source] std::io::Error), + + /// Opening the repository, reading HEAD, or querying local or remote refs failed. + /// A branch missing from a successful remote listing is not an error. + #[error("ADJ repository operation failed: {0}")] + Repository(#[source] Box), + + /// Preparing, downloading, or checking out the requested branch failed. + /// + /// This can indicate a network, configuration, or remote-reference failure; + /// it does not necessarily mean that internet connectivity is unavailable. + #[error("ADJ clone failed: {0}")] + Clone(#[source] Box), + + /// Moving the old cache aside or installing the new checkout failed. + /// + /// When restoration also fails, `rollback` holds its error and the preserved + /// backup's location. Otherwise, the old cache was restored or never moved. + #[error( + "ADJ cache replacement failed: {source}{recovery}", + recovery = .rollback.as_ref().map(|(error, path)| { + format!("; rollback failed: {error}; backup kept at {}", path.display()) + }).unwrap_or_default() + )] + Replacement { + /// The original error moving the old cache or installing the new checkout. + #[source] + source: std::io::Error, + /// Restoration error and preserved backup path, if rollback failed. + rollback: Option<(std::io::Error, std::path::PathBuf)>, + }, + + /// The connectivity probe failed and no local ADJ cache was available. + #[error("ADJ is not available locally and cannot be downloaded")] + UnavailableOffline, +} + +// Convert repository errors for '?' propagation, boxing the concrete error to keep GitError small. +impl From for GitError { + fn from(error: RepositoryError) -> Self { + Self::Repository(Box::new(error)) + } +} + +// Convert clone errors for '?' propagation, boxing the concrete error to keep GitError small. +impl From for GitError { + fn from(error: CloneError) -> Self { + Self::Clone(Box::new(error)) + } +} + +/// Concrete clone failures wrapped by [`GitError::Clone`]. +/// +/// Preparation, name validation, fetching, and checkout expose different `gix` +/// error types. These variants retain each original error as their source. +#[derive(Debug, thiserror::Error)] +pub enum CloneError { + /// Clone preparation failed before downloading repository data. + #[error("clone preparation failed: {0}")] + Prepare(#[from] gix::clone::Error), + + /// The configured remote name was invalid. + #[error("remote name is invalid: {0}")] + RemoteName(#[from] gix::remote::name::Error), + + /// The requested branch name was not a valid Git reference name. + #[error("branch name is invalid: {0}")] + BranchName(#[from] gix::validate::reference::name::Error), + + /// Fetching failed, including a missing requested branch. + /// This can indicate network, configuration, or remote-reference failures. + #[error("fetch failed: {0}")] + Fetch(#[from] gix::clone::fetch::Error), + + /// Downloaded data could not be checked out into the working tree. + #[error("checkout failed: {0}")] + Checkout(#[from] gix::clone::checkout::main_worktree::Error), +} + +/// Concrete repository failures wrapped by [`GitError::Repository`]. +/// +/// Variants identify local reads or remote-reference queries and preserve the +/// original `gix` error as their source. +#[derive(Debug, thiserror::Error)] +pub enum RepositoryError { + /// An existing repository could not be opened or validated. + #[error("repository could not be opened: {0}")] + Open(#[from] gix::open::Error), + + /// HEAD could not be resolved to a commit ID, including an unborn HEAD. + #[error("HEAD could not be resolved: {0}")] + Head(#[from] gix::reference::head_id::Error), + + /// No default fetch remote could be determined, including ambiguous configurations. + #[error("no default fetch remote could be determined")] + NoDefaultRemote, + + /// The selected default remote could not be loaded from repository configuration. + #[error("default fetch remote could not be loaded: {0}")] + FindDefaultRemote(#[from] gix::remote::find::existing::Error), + + /// A transport connection to the selected remote could not be prepared. + #[error("remote connection failed: {0}")] + RemoteConnect(#[from] gix::remote::connect::Error), + + /// The remote handshake or query of advertised references failed. + #[error("remote reference query failed: {0}")] + RemoteRefMap(#[from] gix::remote::ref_map::Error), +} diff --git a/backend/crates/core/src/git/repository.rs b/backend/crates/core/src/git/repository.rs index eeb8b2ff6..cd8d83b1a 100644 --- a/backend/crates/core/src/git/repository.rs +++ b/backend/crates/core/src/git/repository.rs @@ -1,39 +1,8 @@ -use super::{GitError, update::unwrap_repo_path}; +use super::{error::{RepositoryError, GitError}, update::unwrap_repo_path}; use gix::Repository; use std::path::PathBuf; use tracing::{debug, instrument}; -/// Concrete repository failures wrapped by [`GitError::Repository`]. -/// -/// Variants identify local reads or remote-reference queries and preserve the -/// original `gix` error as their source. -#[derive(Debug, thiserror::Error)] -pub enum RepositoryError { - /// An existing repository could not be opened or validated. - #[error("repository could not be opened: {0}")] - Open(#[from] gix::open::Error), - - /// HEAD could not be resolved to a commit ID, including an unborn HEAD. - #[error("HEAD could not be resolved: {0}")] - Head(#[from] gix::reference::head_id::Error), - - /// No default fetch remote could be determined, including ambiguous configurations. - #[error("no default fetch remote could be determined")] - NoDefaultRemote, - - /// The selected default remote could not be loaded from repository configuration. - #[error("default fetch remote could not be loaded: {0}")] - FindDefaultRemote(#[from] gix::remote::find::existing::Error), - - /// A transport connection to the selected remote could not be prepared. - #[error("remote connection failed: {0}")] - RemoteConnect(#[from] gix::remote::connect::Error), - - /// The remote handshake or query of advertised references failed. - #[error("remote reference query failed: {0}")] - RemoteRefMap(#[from] gix::remote::ref_map::Error), -} - /// Opens an existing repository without fetching or changing its checkout. /// /// `path` is the repository directory. If omitted, it defaults to From 791cad7f6b892d24999388da6689389abbd79d4f Mon Sep 17 00:00:00 2001 From: Pau Minguet Date: Wed, 14 Oct 2026 18:43:37 +0200 Subject: [PATCH 11/11] feat(backend, adj): implement auto_adj() and create new LoadError for it --- backend/crates/core/src/adj.rs | 48 +++++++++++++++++++++-- backend/crates/core/src/adj/error.rs | 12 +++++- backend/crates/core/src/git/repository.rs | 5 ++- 3 files changed, 58 insertions(+), 7 deletions(-) diff --git a/backend/crates/core/src/adj.rs b/backend/crates/core/src/adj.rs index 839f4fa4a..91179897a 100644 --- a/backend/crates/core/src/adj.rs +++ b/backend/crates/core/src/adj.rs @@ -2,23 +2,35 @@ //! [`crate::model::PodData`], plus backend-session metadata not modeled as //! vehicle data ([`crate::adj::AdjInfo`]). //! -//! Loading (and if `git` feature is enabled also downloading) is not specific to `station`: a sniffer, a -//! simulator, or any other tool that reads an ADJ needs exactly the same -//! parsing. Scope for this iteration: no `sockets.json`, no +//! [`load_from_dir`] loads an existing ADJ directory. With the `git` feature +//! enabled, `auto_adj` updates the ADJ repository before loading its data +//! and returns the checked-out commit hash. +//! +//! Loading is not specific to `station`: a sniffer, a simulator, or any +//! other tool that reads an ADJ needs exactly the same parsing. +//! +//! Scope for this iteration: no `sockets.json`, no //! `period`/`period_type`/`socket` packet fields (matches //! [`crate::model::PacketDef`]'s own omission), and no schema validation //! (`adj::validate` is a separate, deliberately out-of-scope concern for -//! now) — this module only surfaces structural errors it cannot avoid, +//! now). This module only surfaces structural errors it cannot avoid, //! e.g. a packet referencing a measurement alias that doesn't exist. mod error; mod info; mod raw; +#[cfg(feature = "git")] +use std::path::PathBuf; +#[cfg(feature = "git")] +use tracing::{info, instrument}; + pub use error::{LoadError, MeasurementError, ProtectionError}; pub use info::{AdjInfo, ProtectionTypeInfo}; pub use raw::load_from_dir; +#[cfg(feature = "git")] +use crate::git::{unwrap_repo_path, update_adj}; use crate::model::PodData; /// The result of loading an ADJ: the vehicle's data (as [`crate::model`] @@ -31,3 +43,31 @@ pub struct Adj { /// Backend-session metadata from `general_info.json`. pub info: AdjInfo, } + +/// Updates the ADJ repository, loads its data, and returns its full commit hash. +/// +/// `path` identifies the repository directory, not its parent. If omitted, +/// the platform's user cache directory is used. +/// +/// Online, the repository is replaced and local edits and commits are discarded. +/// Offline, the current cached checkout is loaded without switching branches, +/// so it may differ from `branch`. Clone failures do not fall back to the cache. +/// +/// This performs blocking network and filesystem operations. Call during startup, +/// before readers use ADJ, with only one updater at a time. +/// +/// # Errors +/// +/// Returns [`LoadError::Git`] if path resolution or the update fails. +/// Other variants report failures loading the checked-out ADJ data. +/// Loading failures do not restore the previous cache. +#[cfg(feature = "git")] +#[instrument(level = "debug")] +pub fn auto_adj(path: Option, branch: &str) -> Result<(Adj, String), LoadError> { + let repo_path = unwrap_repo_path(path)?; + let hash = update_adj(Some(repo_path.clone()), branch)?; + let adj = load_from_dir(&repo_path)?; + + info!(path = ?repo_path, %hash, "ADJ loaded"); + Ok((adj, hash)) +} diff --git a/backend/crates/core/src/adj/error.rs b/backend/crates/core/src/adj/error.rs index 2f6d31764..957c3a662 100644 --- a/backend/crates/core/src/adj/error.rs +++ b/backend/crates/core/src/adj/error.rs @@ -1,4 +1,4 @@ -//! Errors produced while loading an ADJ. +//! Errors produced while acquiring or loading an ADJ. //! //! Layered on purpose: [`LoadError`] is the only public error type here — //! the one a composition root's own aggregate error would `#[from]`-chain @@ -10,7 +10,10 @@ use std::net::AddrParseError; use std::path::PathBuf; -/// An error produced while loading an ADJ directory into an [`crate::adj::Adj`]. +#[cfg(feature = "git")] +use crate::git::GitError; + +/// An error produced while acquiring or loading an [`crate::adj::Adj`]. #[derive(Debug, thiserror::Error)] pub enum LoadError { /// Couldn't read a file. @@ -108,6 +111,11 @@ pub enum LoadError { #[source] source: MeasurementError, }, + + /// Couldn't resolve the ADJ repository path or update its checkout. + #[cfg(feature = "git")] + #[error("couldn't prepare ADJ repository: {0}")] + Git(#[from] GitError), } /// An error converting a raw measurement into [`crate::model::Measurement`]. diff --git a/backend/crates/core/src/git/repository.rs b/backend/crates/core/src/git/repository.rs index cd8d83b1a..22e16b294 100644 --- a/backend/crates/core/src/git/repository.rs +++ b/backend/crates/core/src/git/repository.rs @@ -1,4 +1,7 @@ -use super::{error::{RepositoryError, GitError}, update::unwrap_repo_path}; +use super::{ + error::{GitError, RepositoryError}, + update::unwrap_repo_path, +}; use gix::Repository; use std::path::PathBuf; use tracing::{debug, instrument};