From 05d015a7379e7253c4e6ff0eabeec6c6a8bd3c85 Mon Sep 17 00:00:00 2001 From: Seongmin Lee Date: Wed, 5 Aug 2026 14:12:22 +0300 Subject: [PATCH] gitmirror: xrpc grpc was a stupid idea Signed-off-by: Seongmin Lee --- Cargo.lock | 682 +++++++++++++++++++++++++++++--- Cargo.toml | 1 + docker-compose.yml | 1 + gitmirror/Cargo.toml | 7 + gitmirror/src/main.rs | 15 +- gitmirror/src/service.rs | 100 ++--- gitmirror/src/xrpc.rs | 591 +++++++++++++++++++++++++++ localinfra/gitmirror.Dockerfile | 2 +- 8 files changed, 1303 insertions(+), 96 deletions(-) create mode 100644 gitmirror/src/xrpc.rs diff --git a/Cargo.lock b/Cargo.lock index 4dc3e400..698c4722 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,16 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common 0.1.6", + "generic-array 0.14.9", +] + [[package]] name = "aead" version = "0.6.1" @@ -15,7 +25,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1973cfbc1a2daf9cf550e74e1f088c28e7f7d8c1e1418fb6c9dc5184b7e84c99" dependencies = [ "crypto-common 0.2.2", - "inout", + "inout 0.2.2", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher 0.4.4", + "cpufeatures 0.2.17", ] [[package]] @@ -24,23 +45,37 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1fc76eaeac4c9164506c466d4ffdd8ec9d0c5bf57ee97177c4d8eceb3a0e138" dependencies = [ - "cipher", + "cipher 0.5.2", "cpubits", "cpufeatures 0.3.0", "zeroize", ] +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead 0.5.2", + "aes 0.8.4", + "cipher 0.4.4", + "ctr 0.9.2", + "ghash 0.5.1", + "subtle", +] + [[package]] name = "aes-gcm" version = "0.11.0-rc.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da8c919c118108f144adecad74b425b804ad075580d605d9b33c2d6d1c62a2f8" dependencies = [ - "aead", - "aes", - "cipher", - "ctr", - "ghash", + "aead 0.6.1", + "aes 0.9.1", + "cipher 0.5.2", + "ctr 0.10.1", + "ghash 0.6.0", "subtle", "zeroize", ] @@ -501,6 +536,29 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-extra" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9963ff19f40c6102c76756ef0a46004c0d58957d87259fc9208ff8441c12ab96" +dependencies = [ + "axum", + "axum-core", + "bytes", + "cookie", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "serde_core", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "base-x" version = "0.2.11" @@ -666,7 +724,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62ce3946557b35e71d1bbe07ec385073ce9eda05043f95de134eb578fcf1a298" dependencies = [ "byteorder", - "cipher", + "cipher 0.5.2", ] [[package]] @@ -1073,7 +1131,7 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce2dc9ee5f88d11e0beb842c88b33c8a5cf0d1329c4b19494af42b07dbfe8896" dependencies = [ - "cipher", + "cipher 0.5.2", ] [[package]] @@ -1146,7 +1204,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" dependencies = [ "cfg-if", - "cipher", + "cipher 0.5.2", "cpufeatures 0.3.0", "rand_core 0.10.1", "zeroize", @@ -1206,6 +1264,16 @@ dependencies = [ "unsigned-varint", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common 0.1.6", + "inout 0.1.4", +] + [[package]] name = "cipher" version = "0.5.2" @@ -1214,7 +1282,7 @@ checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" dependencies = [ "block-buffer 0.12.1", "crypto-common 0.2.2", - "inout", + "inout 0.2.2", "zeroize", ] @@ -1384,6 +1452,21 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f421161cb492475f1661ddc9815a745a1c894592070661180fdec3d4872e9c3" +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "aes-gcm 0.10.3", + "base64", + "percent-encoding", + "rand 0.8.6", + "subtle", + "time", + "version_check", +] + [[package]] name = "cordyceps" version = "0.3.4" @@ -1568,6 +1651,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array 0.14.9", + "rand_core 0.6.4", "typenum", ] @@ -1592,13 +1676,22 @@ dependencies = [ "rand_core 0.10.1", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher 0.4.4", +] + [[package]] name = "ctr" version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baaca1c4b237092596f64d571e9db6ce4109c4ef9742e27590f1709594461f21" dependencies = [ - "cipher", + "cipher 0.5.2", ] [[package]] @@ -1611,6 +1704,22 @@ dependencies = [ "subtle", ] +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "curve25519-dalek-derive", + "digest 0.10.7", + "fiat-crypto 0.2.9", + "rustc_version", + "subtle", + "zeroize", +] + [[package]] name = "curve25519-dalek" version = "5.0.0-rc.0" @@ -1621,7 +1730,7 @@ dependencies = [ "cpufeatures 0.3.0", "curve25519-dalek-derive", "digest 0.11.3", - "fiat-crypto", + "fiat-crypto 0.3.0", "rustc_version", "subtle", "zeroize", @@ -1865,7 +1974,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "916a94e407b54f9034d71dd748234cd1e516ced6284009906ae246f177eafe5a" dependencies = [ - "cipher", + "cipher 0.5.2", ] [[package]] @@ -1975,6 +2084,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8 0.10.2", + "signature 2.2.0", +] + [[package]] name = "ed25519" version = "3.0.0" @@ -1985,14 +2104,29 @@ dependencies = [ "signature 3.0.0", ] +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek 4.1.3", + "ed25519 2.2.3", + "rand_core 0.6.4", + "serde", + "sha2 0.10.9", + "subtle", + "zeroize", +] + [[package]] name = "ed25519-dalek" version = "3.0.0-rc.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b011170fe4f04665565b4110afef66774fe9ffff278f3eb5b81cc73d26e27d60" dependencies = [ - "curve25519-dalek", - "ed25519", + "curve25519-dalek 5.0.0-rc.0", + "ed25519 3.0.0", "rand_core 0.10.1", "serde", "sha2 0.11.0", @@ -2019,6 +2153,7 @@ dependencies = [ "ff 0.13.1", "generic-array 0.14.9", "group 0.13.0", + "hkdf 0.12.4", "pem-rfc7468 0.7.0", "pkcs8 0.10.2", "rand_core 0.6.4", @@ -2039,7 +2174,7 @@ dependencies = [ "digest 0.11.3", "ff 0.14.0", "group 0.14.0", - "hkdf", + "hkdf 0.13.0", "hybrid-array", "once_cell", "pem-rfc7468 1.0.0", @@ -2197,6 +2332,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "fiat-crypto" version = "0.3.0" @@ -2306,6 +2447,16 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eeef4a9366aaf0ed0bb5292b7c489d80600a7431fca0d96271e10e23ac0bc2b0" +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + [[package]] name = "futures" version = "0.3.32" @@ -2530,13 +2681,23 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval 0.6.2", +] + [[package]] name = "ghash" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2eecf2d5dc9b66b732b97707a0210906b1d30523eb773193ab777c0c84b3e8d5" dependencies = [ - "polyval", + "polyval 0.7.1", ] [[package]] @@ -2544,12 +2705,19 @@ name = "gitmirror" version = "0.1.0" dependencies = [ "anyhow", + "axum", + "bobbin-types", + "chrono", "clap", "gix", + "jacquard-axum", + "jacquard-common", "line-numbers", "prost", "prost-types", "rustc-hash", + "serde", + "serde_json", "tempfile", "tokio", "tokio-stream", @@ -3505,6 +3673,34 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "gloo-storage" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc8031e8c92758af912f9bc08fbbadd3c6f3cfcbf6b64cdf3d6a81f0139277a" +dependencies = [ + "gloo-utils", + "js-sys", + "serde", + "serde_json", + "thiserror 1.0.69", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-utils" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "governor" version = "0.10.4" @@ -3771,6 +3967,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac 0.12.1", +] + [[package]] name = "hkdf" version = "0.13.0" @@ -3798,6 +4003,20 @@ dependencies = [ "digest 0.11.3", ] +[[package]] +name = "html5ever" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13771afe0e6e846f1e67d038d4cb29998a6779f93c809212e4e9c32efd244d4" +dependencies = [ + "log", + "mac", + "markup5ever", + "proc-macro2", + "quote", + "syn 2.0.118", +] + [[package]] name = "htmlescape" version = "0.3.1" @@ -4094,6 +4313,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array 0.14.9", +] + [[package]] name = "inout" version = "0.2.2" @@ -4195,6 +4423,76 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "jacquard" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5055461df426c9d0ac379bcf8f59177ff4c29b4ea2b23bd98c773061b863bbe" +dependencies = [ + "bytes", + "getrandom 0.2.17", + "gloo-storage", + "http", + "jacquard-api", + "jacquard-common", + "jacquard-identity", + "jacquard-oauth", + "jose-jwk", + "miette", + "regex", + "regex-lite", + "reqwest 0.12.28", + "serde", + "serde_html_form", + "serde_json", + "smol_str", + "thiserror 2.0.18", + "tokio", + "trait-variant", + "webpage", +] + +[[package]] +name = "jacquard-api" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5c803a3c097e3ef8aea63747b4fe3fc9e339cd18272dd0366b1d10dd90d5c3f" +dependencies = [ + "jacquard-common", + "jacquard-derive", + "jacquard-lexicon", + "miette", + "serde", + "thiserror 2.0.18", +] + +[[package]] +name = "jacquard-axum" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4fa141b5bc2c34a4070cdab72b93b605f3a452e7e6663920e7c6a87925cd5ee" +dependencies = [ + "axum", + "axum-extra", + "base64", + "bytes", + "chrono", + "jacquard", + "jacquard-common", + "jacquard-derive", + "jacquard-identity", + "miette", + "mini-moka-wasm", + "multibase", + "serde", + "serde_html_form", + "serde_json", + "thiserror 2.0.18", + "tokio", + "tower-http 0.6.11", + "tracing", +] + [[package]] name = "jacquard-common" version = "0.12.1" @@ -4311,6 +4609,40 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "jacquard-oauth" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5da2643242cbe906e707712117eefcc37b351a5ff12909f944ef556e523bfd9" +dependencies = [ + "base64", + "bytes", + "chrono", + "dashmap", + "ed25519-dalek 2.2.0", + "elliptic-curve 0.13.8", + "http", + "jacquard-common", + "jacquard-identity", + "jose-jwa", + "jose-jwk", + "k256", + "miette", + "p256 0.13.2", + "p384 0.13.1", + "rand 0.8.6", + "reqwest 0.12.28", + "serde", + "serde_html_form", + "serde_json", + "sha2 0.10.9", + "smallvec", + "smol_str", + "thiserror 2.0.18", + "tokio", + "trait-variant", +] + [[package]] name = "jiff" version = "0.2.29" @@ -4407,6 +4739,42 @@ dependencies = [ "libc", ] +[[package]] +name = "jose-b64" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec69375368709666b21c76965ce67549f2d2db7605f1f8707d17c9656801b56" +dependencies = [ + "base64ct", + "serde", + "subtle", + "zeroize", +] + +[[package]] +name = "jose-jwa" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ab78e053fe886a351d67cf0d194c000f9d0dcb92906eb34d853d7e758a4b3a7" +dependencies = [ + "serde", +] + +[[package]] +name = "jose-jwk" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "280fa263807fe0782ecb6f2baadc28dffc04e00558a58e33bfdb801d11fd58e7" +dependencies = [ + "jose-b64", + "jose-jwa", + "p256 0.13.2", + "p384 0.13.1", + "rsa 0.9.10", + "serde", + "zeroize", +] + [[package]] name = "js-sys" version = "0.3.102" @@ -4904,9 +5272,9 @@ dependencies = [ name = "knot-secrets" version = "2.0.0" dependencies = [ - "aes-gcm", + "aes-gcm 0.11.0-rc.4", "base64", - "hkdf", + "hkdf 0.13.0", "k256", "knot-resource", "knot-runtime", @@ -5141,6 +5509,9 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin 0.9.8", +] [[package]] name = "levenshtein_automata" @@ -5154,6 +5525,12 @@ version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + [[package]] name = "libsqlite3-sys" version = "0.36.0" @@ -5247,6 +5624,12 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ef0d4ed8669f8f8826eb00dc878084aa8f253506c4fd5e8f58f5bce72ddb97e" +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + [[package]] name = "maitake-sync" version = "0.1.2" @@ -5260,6 +5643,32 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "markup5ever" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16ce3abbeba692c8b8441d036ef91aea6df8da2c6b6e21c7e14d3c18e526be45" +dependencies = [ + "log", + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "markup5ever_rcdom" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edaa21ab3701bfee5099ade5f7e1f84553fd19228cf332f13cd6e964bf59be18" +dependencies = [ + "html5ever", + "markup5ever", + "tendril", + "xml5ever", +] + [[package]] name = "match-lookup" version = "0.1.2" @@ -5530,6 +5939,12 @@ dependencies = [ "web-time", ] +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + [[package]] name = "nix" version = "0.31.3" @@ -5590,6 +6005,22 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.6", + "smallvec", + "zeroize", +] + [[package]] name = "num-conv" version = "0.2.2" @@ -5605,6 +6036,16 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -5612,6 +6053,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -5666,6 +6108,12 @@ version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "openssl-probe" version = "0.2.1" @@ -5733,6 +6181,18 @@ dependencies = [ "sha2 0.11.0", ] +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa 0.16.9", + "elliptic-curve 0.13.8", + "primeorder 0.13.6", + "sha2 0.10.9", +] + [[package]] name = "p384" version = "0.14.0-rc.10" @@ -5741,7 +6201,7 @@ checksum = "9bd5333afa5ae0347f39e6a0f2c9c155da431583fd71fe5555bd0521b4ccaf02" dependencies = [ "ecdsa 0.17.0-rc.18", "elliptic-curve 0.14.0-rc.33", - "fiat-crypto", + "fiat-crypto 0.3.0", "primefield", "primeorder 0.14.0-rc.10", "sha2 0.11.0", @@ -5910,6 +6370,16 @@ dependencies = [ "phf_shared", ] +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared", +] + [[package]] name = "phf_generator" version = "0.11.3" @@ -5979,6 +6449,17 @@ dependencies = [ "futures-io", ] +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der 0.7.10", + "pkcs8 0.10.2", + "spki 0.7.3", +] + [[package]] name = "pkcs1" version = "0.8.0-rc.4" @@ -5995,7 +6476,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "279a91971a1d8eb1260a30938eae3be9cb67b472dffecb222fbbbe2fd2dc1453" dependencies = [ - "aes", + "aes 0.9.1", "cbc", "der 0.8.0", "pbkdf2", @@ -6054,10 +6535,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a00baa632505d05512f48a963e16051c54fda9a95cc9acea1a4e3c90991c4a2e" dependencies = [ "cpufeatures 0.3.0", - "universal-hash", + "universal-hash 0.6.1", "zeroize", ] +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash 0.5.1", +] + [[package]] name = "polyval" version = "0.7.1" @@ -6066,7 +6559,7 @@ checksum = "7dfc63250416fea14f5749b90725916a6c903f599d51cb635aa7a52bfd03eede" dependencies = [ "cpubits", "cpufeatures 0.3.0", - "universal-hash", + "universal-hash 0.6.1", ] [[package]] @@ -6121,6 +6614,12 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + [[package]] name = "prettyplease" version = "0.2.37" @@ -6812,6 +7311,26 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid 0.9.6", + "digest 0.10.7", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1 0.7.5", + "pkcs8 0.10.2", + "rand_core 0.6.4", + "signature 2.2.0", + "spki 0.7.3", + "subtle", + "zeroize", +] + [[package]] name = "rsa" version = "0.10.0-rc.18" @@ -6822,7 +7341,7 @@ dependencies = [ "crypto-bigint 0.7.4", "crypto-primes", "digest 0.11.3", - "pkcs1", + "pkcs1 0.8.0-rc.4", "pkcs8 0.11.0", "rand_core 0.10.1", "sha2 0.11.0", @@ -6862,33 +7381,33 @@ version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbf893f64684e58da8a68d56a5e84d1cf0440226274c515770fe267707a7d0b0" dependencies = [ - "aes", + "aes 0.9.1", "aws-lc-rs", "bitflags 2.13.0", "block-padding", "byteorder", "bytes", "cbc", - "cipher", + "cipher 0.5.2", "crypto-bigint 0.7.4", - "ctr", - "curve25519-dalek", + "ctr 0.10.1", + "curve25519-dalek 5.0.0-rc.0", "data-encoding", "delegate", "der 0.8.0", "digest 0.11.3", "ecdsa 0.17.0-rc.18", - "ed25519-dalek", + "ed25519-dalek 3.0.0-rc.0", "elliptic-curve 0.14.0-rc.33", "enum_dispatch", "flate2", "futures", "generic-array 1.4.3", "getrandom 0.4.3", - "ghash", + "ghash 0.6.0", "hex-literal", "hmac 0.13.0", - "inout", + "inout 0.2.2", "internal-russh-num-bigint", "keccak", "log", @@ -6897,17 +7416,17 @@ dependencies = [ "module-lattice", "num-bigint", "p256 0.14.0-rc.10", - "p384", + "p384 0.14.0-rc.10", "p521", "pageant", "pbkdf2", - "pkcs1", + "pkcs1 0.8.0-rc.4", "pkcs5", "pkcs8 0.11.0", - "polyval", + "polyval 0.7.1", "rand 0.10.1", "rand_core 0.10.1", - "rsa", + "rsa 0.10.0-rc.18", "russh-cryptovec", "russh-util", "salsa20", @@ -6924,7 +7443,7 @@ dependencies = [ "thiserror 2.0.18", "tokio", "typenum", - "universal-hash", + "universal-hash 0.6.1", "zeroize", ] @@ -7148,7 +7667,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f874456e72520ff1375a06c588eaf074b0f01f9e9e1aada45bd9b7954a6e42c" dependencies = [ "cfg-if", - "cipher", + "cipher 0.5.2", ] [[package]] @@ -7729,13 +8248,13 @@ version = "0.3.0-rc.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10db6f219196a8528f9ec904d9d45cdad692d65b0e57e72be4dedd1c5fddce36" dependencies = [ - "aead", - "aes", - "aes-gcm", + "aead 0.6.1", + "aes 0.9.1", + "aes-gcm 0.11.0-rc.4", "cbc", "chacha20", - "cipher", - "ctr", + "cipher 0.5.2", + "ctr 0.10.1", "ctutils", "des", "poly1305", @@ -7767,14 +8286,14 @@ dependencies = [ "argon2", "bcrypt-pbkdf", "ctutils", - "ed25519-dalek", + "ed25519-dalek 3.0.0-rc.0", "hex", "hmac 0.13.0", "p256 0.14.0-rc.10", - "p384", + "p384 0.14.0-rc.10", "p521", "rand_core 0.10.1", - "rsa", + "rsa 0.10.0-rc.18", "sec1 0.8.1", "sha1 0.11.0", "sha2 0.11.0", @@ -7796,6 +8315,31 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + [[package]] name = "strsim" version = "0.11.1" @@ -8049,6 +8593,17 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + [[package]] name = "terminal_size" version = "0.4.4" @@ -8477,6 +9032,7 @@ dependencies = [ "tower", "tower-layer", "tower-service", + "tracing", "url", ] @@ -8781,6 +9337,16 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common 0.1.6", + "subtle", +] + [[package]] name = "universal-hash" version = "0.6.1" @@ -9029,6 +9595,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpage" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70862efc041d46e6bbaa82bb9c34ae0596d090e86cbd14bd9e93b36ee6802eac" +dependencies = [ + "html5ever", + "markup5ever_rcdom", + "serde_json", + "url", +] + [[package]] name = "webpki-root-certs" version = "1.0.8" @@ -9537,6 +10115,17 @@ dependencies = [ "rustix", ] +[[package]] +name = "xml5ever" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bbb26405d8e919bc1547a5aa9abc95cbfa438f04844f5fdd9dc7596b748bf69" +dependencies = [ + "log", + "mac", + "markup5ever", +] + [[package]] name = "yasna" version = "0.5.2" @@ -9626,6 +10215,7 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" dependencies = [ + "serde", "zeroize_derive", ] diff --git a/Cargo.toml b/Cargo.toml index a32f3556..45707a87 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -74,6 +74,7 @@ knot-maintenance = { path = "knot2/crates/knot-maintenance" } knot-sim = { path = "knot2/crates/knot-sim" } knot-edge = { path = "knot2/crates/knot-edge" } +jacquard-axum = "0.12.1" jacquard-common = "0.12.1" jacquard-derive = "0.12.1" jacquard-lexicon = { version = "0.12.1", default-features = false } diff --git a/docker-compose.yml b/docker-compose.yml index 17b436b7..a443a065 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -309,6 +309,7 @@ services: environment: RUST_LOG: gitmirror=debug GITMIRROR_ADDR: 0.0.0.0:9000 + GITMIRROR_XRPC_ADDR: 0.0.0.0:9001 GITMIRROR_REPO_BASE: /data/repos volumes: - knotmirror-data:/data diff --git a/gitmirror/Cargo.toml b/gitmirror/Cargo.toml index ce618735..f25fe7d7 100644 --- a/gitmirror/Cargo.toml +++ b/gitmirror/Cargo.toml @@ -7,7 +7,14 @@ rust-version.workspace = true [dependencies] anyhow = { workspace = true } +axum = { workspace = true } +bobbin-types = { workspace = true } +chrono = { workspace = true } clap = { workspace = true } +jacquard-axum = { workspace = true } +jacquard-common = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } tonic = { workspace = true } tonic-prost = { workspace = true } prost = { workspace = true } diff --git a/gitmirror/src/main.rs b/gitmirror/src/main.rs index 52aa9bd9..52351bea 100644 --- a/gitmirror/src/main.rs +++ b/gitmirror/src/main.rs @@ -7,6 +7,7 @@ mod diff; mod merge; mod protocol; mod service; +mod xrpc; #[derive(Parser)] #[command(name = "gitmirror", about = "Git mirror gRPC service")] @@ -17,7 +18,7 @@ struct Cli { #[derive(Subcommand)] enum Command { - /// Run the gRPC server. + /// Run the gRPC and XRPC servers. Serve(ServeArgs), } @@ -27,6 +28,10 @@ struct ServeArgs { #[arg(long, env = "GITMIRROR_ADDR", default_value = "127.0.0.1:9000")] addr: SocketAddr, + /// Address to bind the HTTP XRPC server to. + #[arg(long, env = "GITMIRROR_XRPC_ADDR", default_value = "127.0.0.1:9001")] + xrpc_addr: SocketAddr, + /// Base directory holding bare mirror repos, one per DID (/). #[arg(long, env = "GITMIRROR_REPO_BASE", default_value = "repos")] repo_base: PathBuf, @@ -41,6 +46,12 @@ async fn main() -> anyhow::Result<()> { .init(); match Cli::parse().cmd { - Command::Serve(args) => service::serve(args.addr, args.repo_base).await, + Command::Serve(args) => { + tokio::try_join!( + service::serve(args.addr, args.repo_base.clone()), + xrpc::serve(args.xrpc_addr, args.repo_base), + ) + .map(|_| ()) + } } } diff --git a/gitmirror/src/service.rs b/gitmirror/src/service.rs index b3585c98..335c7e3d 100644 --- a/gitmirror/src/service.rs +++ b/gitmirror/src/service.rs @@ -5,6 +5,7 @@ use std::pin::Pin; use gix::bstr::ByteSlice as _; use gix::revision::plumbing::Spec as RevSpec; use gix::revision::walk::Sorting; +use jacquard_common::types::did::validate_did; use tempfile::TempDir; use tokio::sync::mpsc; use tokio_stream::wrappers::ReceiverStream; @@ -30,13 +31,13 @@ type CommitLogResponseStream = const BLOB_CHUNK_SIZE: usize = 64 * 1024; const COMMIT_LOG_BATCH_SIZE: usize = 128; -struct GitMirror { +pub(crate) struct GitMirror { repo_base: PathBuf, } /// A [`gix::Repository`] backed by a temporary directory that is removed on drop. Derefs to /// `gix::Repository`, so it is used just like one; keeping it alive keeps the scratch dir alive. -struct TempRepository { +pub(crate) struct TempRepository { repo: gix::Repository, // Declared AFTER `repo` so `repo` drops first: any file handles into the scratch dir close // before the dir itself is removed (Rust drops struct fields in declaration order). @@ -58,53 +59,57 @@ impl std::ops::Deref for TempRepository { impl GitMirror { fn open_repo(&self, did: &str) -> Result { - if !did.starts_with("did:") { - return Err(Status::invalid_argument("repo must be a DID")); - } + validate_did(did).map_err(|_| Status::invalid_argument("repo must be a DID"))?; gix::open(self.repo_base.join(did)) .map_err(|e| Status::not_found(format!("repo not found: {e}"))) } - /// Build a throwaway bare repo whose `objects/info/alternates` points read-only at each of the - /// given git repositories, so a single `gix::Repository` can see objects from all of them - /// without ever mutating them. fn open_scratch(&self, dids: &[&str]) -> Result { - let mut seen: Vec<&str> = Vec::new(); - let mut object_dirs = Vec::new(); - for &did in dids { - if seen.contains(&did) { - continue - } - if !did.starts_with("did:") { - return Err(Status::invalid_argument("repo must be a DID")); - } - seen.push(did); - let object_dir = std::fs::canonicalize(self.repo_base.join(did).join("objects")) - .map_err(|_| Status::not_found(format!("repo not found: {did}")))?; - object_dirs.push(object_dir); - } - if object_dirs.is_empty() { - return Err(Status::internal("open_scratch requires at least one repo")); - } + open_scratch(&self.repo_base, dids) + } +} - let scratch = tempfile::tempdir().map_err(|e| Status::internal(e.to_string()))?; - gix::init_bare(scratch.path()).map_err(|e| Status::internal(e.to_string()))?; - - let info_dir = scratch.path().join("objects").join("info"); - std::fs::create_dir_all(&info_dir).map_err(|e| Status::internal(e.to_string()))?; - let alternates = object_dirs - .iter() - .map(|p| p.display().to_string()) - .collect::>() - .join("\n"); - std::fs::write(info_dir.join("alternates"), format!("{alternates}\n")) - .map_err(|e| Status::internal(e.to_string()))?; - - let repo = gix::open(scratch.path()) - .map_err(|e| Status::internal(e.to_string()))? - .with_object_memory(); - Ok(TempRepository::new(repo, scratch)) +/// Build a throwaway bare repo whose `objects/info/alternates` points read-only at each of the +/// given git repositories, so a single `gix::Repository` can see objects from all of them +/// without ever mutating them. +pub(crate) fn open_scratch( + repo_base: &std::path::Path, + dids: &[&str], +) -> Result { + let mut seen: Vec<&str> = Vec::new(); + let mut object_dirs = Vec::new(); + for &did in dids { + if seen.contains(&did) { + continue + } + // A DID has no `/`, so this is also what keeps `repo_base.join(did)` inside `repo_base`. + validate_did(did).map_err(|_| Status::invalid_argument("repo must be a DID"))?; + seen.push(did); + let object_dir = std::fs::canonicalize(repo_base.join(did).join("objects")) + .map_err(|_| Status::not_found(format!("repo not found: {did}")))?; + object_dirs.push(object_dir); + } + if object_dirs.is_empty() { + return Err(Status::internal("open_scratch requires at least one repo")); } + + let scratch = tempfile::tempdir().map_err(|e| Status::internal(e.to_string()))?; + gix::init_bare(scratch.path()).map_err(|e| Status::internal(e.to_string()))?; + + let info_dir = scratch.path().join("objects").join("info"); + std::fs::create_dir_all(&info_dir).map_err(|e| Status::internal(e.to_string()))?; + let alternates = object_dirs + .iter() + .map(|p| p.display().to_string()) + .collect::>() + .join("\n"); + std::fs::write(info_dir.join("alternates"), format!("{alternates}\n")) + .map_err(|e| Status::internal(e.to_string()))?; + + let repo = gix::open(scratch.path()) + .map_err(|e| Status::internal(e.to_string()))? + .with_object_memory(); + Ok(TempRepository::new(repo, scratch)) } #[tonic::async_trait] @@ -287,7 +292,7 @@ impl GitMirrorService for GitMirror { let mut sent = 0usize; let mut batch = Vec::with_capacity(COMMIT_LOG_BATCH_SIZE); - for info in commit_log_walk(&repo, &req)? { + for info in commit_log_walk(&repo, &req.ranges, req.all_refs)? { let info = info?; let commit_time = info.commit_time(); if before.is_some_and(|b| commit_time > b) { @@ -413,14 +418,15 @@ fn find_commit_by_sha(repo: &gix::Repository, sha: &[u8]) -> Result( +pub(crate) fn commit_log_walk<'repo>( repo: &'repo gix::Repository, - req: &CommitLogRequest, + ranges: &[Vec], + all_refs: bool, ) -> anyhow::Result> { let mut tips = Vec::new(); let mut hidden = Vec::new(); - if req.all_refs { + if all_refs { for r in repo.references()?.all()? { let mut r = r.map_err(|e| anyhow::anyhow!(e))?; if let Ok(commit) = r.peel_to_commit() { @@ -428,7 +434,7 @@ fn commit_log_walk<'repo>( } } } else { - for range in &req.ranges { + for range in ranges { let revspec = repo.rev_parse(range.as_bstr())?; let spec = revspec.detach(); match spec { diff --git a/gitmirror/src/xrpc.rs b/gitmirror/src/xrpc.rs new file mode 100644 index 00000000..4b4282c3 --- /dev/null +++ b/gitmirror/src/xrpc.rs @@ -0,0 +1,591 @@ +use std::net::SocketAddr; +use std::path::PathBuf; +use std::sync::Arc; + +use axum::extract::State; +use axum::http::StatusCode; +use axum::response::{IntoResponse, Response}; +use axum::routing::get; +use axum::{Json, Router}; +use bobbin_types::sh_tangled; +use bobbin_types::sh_tangled::git::temp2::{get_diff, get_interdiff, list_commits, merge_check}; +use gix::ObjectId; +use jacquard_axum::{ExtractXrpc, XrpcResponse}; +use jacquard_common::types::string::Datetime; +use jacquard_common::ToSmolStr; +use serde_json::json; +use tracing::{error, info}; + +use crate::diff; +use crate::service::{commit_log_walk, open_scratch}; + +const DEFAULT_LIMIT: u32 = 50; +const MAX_LIMIT: u32 = 100; + +#[derive(Clone)] +struct XrpcState { + repo_base: Arc, +} + +#[derive(Debug)] +enum XrpcError { + InvalidRequest(String), + RepoNotFound { detail: String }, + RefNotFound { rev: String, detail: String }, + RevisionNotFound { rev: String }, + CompareError(String), + Internal(String), +} + +// TODO(boltless): this is stupid. deprecate grpc +impl From for XrpcError { + fn from(status: tonic::Status) -> Self { + match status.code() { + tonic::Code::NotFound => Self::RepoNotFound { + detail: status.message().to_owned(), + }, + tonic::Code::InvalidArgument => Self::InvalidRequest(status.message().to_owned()), + _ => Self::Internal(status.message().to_owned()), + } + } +} + +impl IntoResponse for XrpcError { + fn into_response(self) -> Response { + let (status, error, message) = match self { + Self::InvalidRequest(m) => (StatusCode::BAD_REQUEST, "InvalidRequest", m), + Self::RepoNotFound { detail } => { + error!(error = %detail, "repo not found"); + ( + StatusCode::NOT_FOUND, + "RepoNotFound", + "repository not found".to_owned(), + ) + } + Self::RefNotFound { rev, detail } => { + error!(error = %detail, rev = %rev, "revision not found"); + ( + StatusCode::NOT_FOUND, + "RefNotFound", + format!("revision not found: {rev}"), + ) + } + Self::RevisionNotFound { rev } => ( + StatusCode::NOT_FOUND, + "RevisionNotFound", + format!("commit not found: {rev}"), + ), + Self::CompareError(m) => { + error!(error = %m, "compare failed"); + ( + StatusCode::INTERNAL_SERVER_ERROR, + "CompareError", + "failed to compare revisions".to_owned(), + ) + } + Self::Internal(m) => { + error!(error = %m, "xrpc request failed"); + ( + StatusCode::INTERNAL_SERVER_ERROR, + "InternalServerError", + "internal error".to_owned(), + ) + } + }; + (status, Json(json!({ "error": error, "message": message }))).into_response() + } +} + +struct GixSignature<'a>(gix::actor::SignatureRef<'a>); + +impl TryFrom> for sh_tangled::git::Signature { + type Error = anyhow::Error; + + fn try_from(GixSignature(sig): GixSignature) -> Result { + let time = sig.time()?; + let offset = chrono::FixedOffset::east_opt(time.offset).ok_or_else(|| { + anyhow::anyhow!("commit timezone offset out of range: {}", time.offset) + })?; + let when = chrono::DateTime::from_timestamp(time.seconds, 0) + .ok_or_else(|| anyhow::anyhow!("commit timestamp out of range: {}", time.seconds))? + .with_timezone(&offset); + Ok(Self { + name: sig.name.to_smolstr(), + email: sig.email.to_smolstr(), + when: Datetime::new(when), + extra_data: Default::default(), + }) + } +} + +struct GixCommit<'a>(gix::Commit<'a>); + +impl TryFrom> for list_commits::Commit { + type Error = anyhow::Error; + + fn try_from(GixCommit(commit): GixCommit<'_>) -> Result { + let decoded = commit.decode()?; + Ok(Self { + oid: commit.id.to_smolstr(), + parents: decoded + .parents + .iter() + .map(|parent| parent.to_smolstr()) + .collect(), + tree: decoded.tree.to_smolstr(), + author: GixSignature(decoded.author()?).try_into()?, + committer: GixSignature(decoded.committer()?).try_into()?, + extra_headers: decoded + .extra_headers + .iter() + .map(|(key, value)| list_commits::Header { + key: key.to_smolstr(), + value: value.to_smolstr(), + extra_data: Default::default(), + }) + .collect(), + message: decoded.message.to_smolstr(), + extra_data: Default::default(), + }) + } +} + +impl From for sh_tangled::git::DiffSrc { + fn from(f: crate::diff::FileContent) -> Self { + Self { + path: f.path.into(), + oid: f.oid.into(), + size: f.size as i64, + is_binary: f.is_binary, + is_submodule: f.is_submodule, + content: f.content.map(|b| String::from_utf8_lossy(&b).into_owned().into()), + extra_data: Default::default(), + } + } +} + +impl From for sh_tangled::git::DiffHunk { + fn from(h: crate::diff::Hunk) -> Self { + // The novel sets are hash sets; sort them so the output is stable across runs. + let sorted = |set: rustc_hash::FxHashSet| -> Vec { + let mut v: Vec = set.into_iter().map(|n| i64::from(n.0)).collect(); + v.sort_unstable(); + v + }; + Self { + novel_lhs: sorted(h.novel_lhs), + novel_rhs: sorted(h.novel_rhs), + lines: h + .lines + .into_iter() + .map(|(lhs, rhs)| sh_tangled::git::LinePair { + lhs: lhs.map(|n| i64::from(n.0)), + rhs: rhs.map(|n| i64::from(n.0)), + extra_data: Default::default(), + }) + .collect(), + extra_data: Default::default(), + } + } +} + +impl From for sh_tangled::git::FileDiff { + fn from(d: crate::diff::Diff) -> Self { + Self { + lhs_src: d.lhs_src.into(), + rhs_src: d.rhs_src.into(), + hunks: d.hunks.into_iter().map(Into::into).collect(), + has_byte_changes: d + .has_byte_changes + .map(|(lhs, rhs)| sh_tangled::git::ByteChanges { + lhs: lhs as i64, + rhs: rhs as i64, + extra_data: Default::default(), + }), + has_syntactic_changes: d.has_syntactic_changes, + extra_data: Default::default(), + } + } +} + +// the merge check itself still speaks protobuf; convert at the edge rather than +// duplicating it, same as the `From` above +impl From for merge_check::MergeCheckOutput { + fn from(out: crate::protocol::v1::MergeCheckResponse) -> Self { + Self { + is_conflicted: out.is_conflicted, + conflicts: out + .conflicts + .into_iter() + .map(|c| merge_check::Conflict { + filename: c.filename.into(), + reason: c.reason.into(), + extra_data: Default::default(), + }) + .collect(), + message: out.message.map(Into::into), + extra_data: Default::default(), + } + } +} + +/// Resolve a full hex oid to a commit that actually exists in `repo`. +fn find_commit(repo: &gix::Repository, sha: &str) -> Result { + let oid = gix::ObjectId::from_hex(sha.as_bytes()) + .map_err(|e| XrpcError::InvalidRequest(format!("bad commit sha {sha:?}: {e}")))?; + repo.find_commit(oid).map_err(|_| XrpcError::RevisionNotFound { + rev: sha.to_owned(), + })?; + Ok(oid) +} + +fn get_diff_inner( + repo: &gix::Repository, + base: gix::ObjectId, + head: gix::ObjectId, +) -> Result, XrpcError> { + let compare = || -> anyhow::Result> { + let merge_base = repo.merge_base(base, head)?.detach(); + let old = repo.find_tree(repo.find_commit(merge_base)?.tree_id()?)?; + let new = repo.find_tree(repo.find_commit(head)?.tree_id()?)?; + diff::diff(repo, &old, &new, false)? + .map(|d| d.map(Into::into)) + .collect() + }; + compare().map_err(|e| XrpcError::CompareError(e.to_string())) +} + +async fn get_diff( + State(state): State, + ExtractXrpc(args): ExtractXrpc, +) -> Result, XrpcError> { + let scratch = open_scratch( + &state.repo_base, + &[args.head_repo.as_str(), args.base_repo.as_str()], + )?; + let base = find_commit(&scratch, args.base_commit.as_ref())?; + let head = find_commit(&scratch, args.head_commit.as_ref())?; + + tokio::task::spawn_blocking(move || get_diff_inner(&scratch, base, head)) + .await + .map_err(|e| XrpcError::Internal(e.to_string()))? + .map(|diffs| { + XrpcResponse(get_diff::GetDiffOutput { + diffs, + extra_data: Default::default(), + }) + }) +} + +async fn merge_check( + State(state): State, + ExtractXrpc(params): ExtractXrpc, +) -> Result, XrpcError> { + let scratch = open_scratch( + &state.repo_base, + &[params.target_repo.as_str(), params.source_repo.as_str()], + )?; + let target = find_commit(&scratch, params.target_commit.as_ref())?; + let source = find_commit(&scratch, params.source_commit.as_ref())?; + + tokio::task::spawn_blocking(move || crate::merge::merge_check(&scratch, target, source)) + .await + .map_err(|e| XrpcError::Internal(e.to_string()))? + .map(|out| XrpcResponse(out.into())) + .map_err(|e| XrpcError::Internal(e.to_string())) +} + +fn get_interdiff_inner( + repo: &gix::Repository, + (from_base_id, from_head_id): (ObjectId, ObjectId), + (to_base_id, to_head_id): (ObjectId, ObjectId), +) -> Result, XrpcError> { + let to_head = repo + .find_commit(to_head_id) + .map_err(|e| XrpcError::Internal(e.to_string()))?; + let to_head_tree = to_head + .tree() + .map_err(|e| XrpcError::Internal(e.to_string()))?; + let rebased_tree = diff::prepare_interdiff(&repo, (from_base_id, from_head_id), to_base_id) + .map_err(|e| XrpcError::Internal(e.to_string()))?; + let compare = || -> anyhow::Result> { + diff::diff(&repo, &rebased_tree, &to_head_tree, true)? + .map(|d| d.map(Into::into)) + .collect() + }; + compare().map_err(|e| XrpcError::CompareError(e.to_string())) +} + +async fn get_interdiff( + State(state): State, + ExtractXrpc(args): ExtractXrpc, +) -> Result, XrpcError> { + let from_base_id = ObjectId::from_hex(args.base_commit1.as_bytes()).map_err(|e| XrpcError::InvalidRequest(e.to_string()))?; + let from_head_id = ObjectId::from_hex(args.base_commit2.as_bytes()).map_err(|e| XrpcError::InvalidRequest(e.to_string()))?; + let to_base_id = ObjectId::from_hex(args.head_commit1.as_bytes()).map_err(|e| XrpcError::InvalidRequest(e.to_string()))?; + let to_head_id = ObjectId::from_hex(args.head_commit2.as_bytes()).map_err(|e| XrpcError::InvalidRequest(e.to_string()))?; + + let scratch = open_scratch( + &state.repo_base, + &[args.head_repo.as_str(), args.base_repo.as_str()], + )?; + + tokio::task::spawn_blocking(move || { + get_interdiff_inner(&scratch, (from_base_id, from_head_id), (to_base_id, to_head_id)) + }) + .await + .map_err(|e| XrpcError::Internal(e.to_string()))? + .map(|diffs| { + XrpcResponse(get_interdiff::GetInterdiffOutput { + diffs, + extra_data: Default::default(), + }) + }) +} + +fn list_commits_inner( + repo: &gix::Repository, + args: list_commits::ListCommits, +) -> Result, XrpcError> { + let ranges: Vec> = args + .ranges + .clone() + .unwrap_or_default() + .iter() + .map(|revspec| revspec.as_bytes().to_vec()) + .collect(); + let walk = commit_log_walk(repo, &ranges, args.all_refs.unwrap_or(false)).map_err(|e| { + XrpcError::RefNotFound { + rev: args.ranges.unwrap_or_default().join(", "), + detail: e.to_string(), + } + })?; + + let limit = args.limit.map(|limit| limit as u32).unwrap_or(DEFAULT_LIMIT); + if limit == 0 || limit > MAX_LIMIT { + return Err(XrpcError::InvalidRequest(format!( + "limit must be between 1 and {MAX_LIMIT}" + ))); + } + + let mut commits: Vec = + Vec::with_capacity(limit as usize); + let mut skipped = 0usize; + + for info in walk { + let info = info.map_err(|e| XrpcError::Internal(e.to_string()))?; + + if (skipped as i64) < args.skip.unwrap_or(0) { + skipped += 1; + continue; + } + if (commits.len() as i64) == args.limit.unwrap_or(50) { + break; + } + + let commit = info + .object() + .map_err(|e| XrpcError::Internal(e.to_string()))?; + commits.push( + list_commits::Commit::try_from(GixCommit(commit)) + .map_err(|e| XrpcError::Internal(e.to_string()))?, + ); + } + + Ok(commits) +} + +async fn list_commits( + State(state): State, + ExtractXrpc(args): ExtractXrpc, +) -> Result, XrpcError> { + let path = state.repo_base.join(args.repo.as_str()); + let repo = gix::open(path) + .map_err(|e| XrpcError::RepoNotFound{ detail: e.to_string() })? + .into_sync(); + + tokio::task::spawn_blocking(move || { + list_commits_inner(&repo.to_thread_local(), args) + }) + .await + .map_err(|e| XrpcError::Internal(e.to_string()))? + .map(|commits| { + XrpcResponse(list_commits::ListCommitsOutput { + commits, + extra_data: Default::default(), + }) + }) +} + +pub async fn serve(addr: SocketAddr, repo_base: PathBuf) -> anyhow::Result<()> { + let app = Router::new() + .route("/xrpc/sh.tangled.git.temp2.getDiff", get(get_diff)) + .route("/xrpc/sh.tangled.git.temp2.getInterdiff", get(get_interdiff)) + .route("/xrpc/sh.tangled.git.temp2.listCommits", get(list_commits)) + .route("/xrpc/sh.tangled.git.temp2.mergeCheck", get(merge_check)) + .with_state(XrpcState { + repo_base: Arc::new(repo_base), + }); + + let listener = tokio::net::TcpListener::bind(addr).await?; + info!(addr = %addr, "gitmirror XRPC server listening"); + axum::serve(listener, app).await?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::path::Path; + use std::process::Command; + + const BASE_DID: &str = "did:plc:upstream"; + const HEAD_DID: &str = "did:plc:fork"; + + fn git(dir: &Path, args: &[&str]) { + let status = Command::new("git") + .args(args) + .current_dir(dir) + .env("GIT_AUTHOR_NAME", "t") + .env("GIT_AUTHOR_EMAIL", "t@t") + .env("GIT_COMMITTER_NAME", "t") + .env("GIT_COMMITTER_EMAIL", "t@t") + .status() + .expect("run git"); + assert!(status.success(), "git {args:?} failed"); + } + + fn rev_parse(dir: &Path, rev: &str) -> String { + let out = Command::new("git") + .args(["rev-parse", rev]) + .current_dir(dir) + .output() + .expect("rev-parse"); + String::from_utf8(out.stdout).unwrap().trim().to_owned() + } + + /// The real fork shape: the base tip lives only in the upstream mirror and the head tip only + /// in the fork's, with a shared ancestor. Returns `(repo_base, base_commit, head_commit)`. + fn fork_fixture() -> (tempfile::TempDir, String, String) { + let root = tempfile::tempdir().unwrap(); + let repo_base = root.path().join("repos"); + std::fs::create_dir_all(&repo_base).unwrap(); + + let upstream = root.path().join("upstream"); + std::fs::create_dir_all(&upstream).unwrap(); + git(&upstream, &["init", "-q", "-b", "main"]); + std::fs::write(upstream.join("a.txt"), "line1\nline2\nline3\n").unwrap(); + std::fs::write(upstream.join("b.txt"), "keep\n").unwrap(); + git(&upstream, &["add", "."]); + git(&upstream, &["commit", "-q", "-m", "shared ancestor"]); + + // Fork before upstream moves on, so neither tip is reachable from the other. + let fork = root.path().join("fork"); + git( + root.path(), + &["clone", "-q", upstream.to_str().unwrap(), fork.to_str().unwrap()], + ); + + std::fs::write(upstream.join("upstream.txt"), "theirs\n").unwrap(); + git(&upstream, &["add", "."]); + git(&upstream, &["commit", "-q", "-m", "upstream only"]); + let base_commit = rev_parse(&upstream, "HEAD"); + + std::fs::write(fork.join("a.txt"), "line1\nCHANGED\nline3\n").unwrap(); + git(&fork, &["mv", "b.txt", "c.txt"]); + git(&fork, &["add", "-A"]); + git(&fork, &["commit", "-q", "-m", "fork only"]); + let head_commit = rev_parse(&fork, "HEAD"); + + for (did, src) in [(BASE_DID, &upstream), (HEAD_DID, &fork)] { + git( + root.path(), + &[ + "clone", + "-q", + "--bare", + src.to_str().unwrap(), + repo_base.join(did).to_str().unwrap(), + ], + ); + } + + (root, base_commit, head_commit) + } + + fn diff_fixture( + root: &Path, + base_commit: &str, + head_commit: &str, + ) -> Result, XrpcError> { + let repo_base = root.join("repos"); + let scratch = open_scratch(&repo_base, &[HEAD_DID, BASE_DID])?; + let base = find_commit(&scratch, base_commit)?; + let head = find_commit(&scratch, head_commit)?; + get_diff_inner(&scratch, base, head) + } + + #[test] + fn a_cross_repo_diff_reports_only_what_the_head_side_changed() { + let (root, base, head) = fork_fixture(); + let diffs = diff_fixture(root.path(), &base, &head).unwrap(); + + let mut paths: Vec = diffs + .iter() + .flat_map(|d| [d.lhs_src.path.to_string(), d.rhs_src.path.to_string()]) + .collect(); + paths.sort(); + paths.dedup(); + // `upstream.txt` is on the base side only: three-dot semantics exclude it. + assert_eq!(paths, ["a.txt", "b.txt", "c.txt"]); + + let a = diffs + .iter() + .find(|d| d.rhs_src.path == "a.txt") + .expect("a.txt is in the diff"); + // Only line 2 (0-based: 1) changed. + assert_eq!(a.hunks.len(), 1); + assert_eq!(a.hunks[0].novel_lhs, [1]); + assert_eq!(a.hunks[0].novel_rhs, [1]); + assert_eq!(a.hunks[0].lines, { + vec![sh_tangled::git::LinePair { + lhs: Some(1), + rhs: Some(1), + extra_data: Default::default(), + }] + }); + // "line2\n" -> "CHANGED\n" is two bytes longer. + assert_eq!( + a.has_byte_changes, + Some(sh_tangled::git::ByteChanges { + lhs: 18, + rhs: 20, + extra_data: Default::default(), + }) + ); + } + + #[test] + fn a_malformed_sha_is_a_client_error_and_an_absent_one_is_a_miss() { + let (root, base, head) = fork_fixture(); + + assert!(matches!( + diff_fixture(root.path(), &base, "not-a-sha"), + Err(XrpcError::InvalidRequest(_)) + )); + assert!(matches!( + diff_fixture(root.path(), &base, &"0".repeat(head.len())), + Err(XrpcError::RevisionNotFound { .. }) + )); + } + + #[test] + fn a_repo_that_is_not_a_did_never_reaches_the_filesystem() { + let (root, _, _) = fork_fixture(); + let repo_base = root.path().join("repos"); + assert!(matches!( + open_scratch(&repo_base, &["../../etc"]).map(|_| ()), + Err(status) if status.code() == tonic::Code::InvalidArgument + )); + } +} diff --git a/localinfra/gitmirror.Dockerfile b/localinfra/gitmirror.Dockerfile index 16f9fe66..5fd5700f 100644 --- a/localinfra/gitmirror.Dockerfile +++ b/localinfra/gitmirror.Dockerfile @@ -23,7 +23,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends git ca-certific COPY --from=build /usr/local/bin/gitmirror /usr/local/bin/gitmirror -EXPOSE 9000 +EXPOSE 9000 9001 ENTRYPOINT ["/usr/bin/tini", "--"] CMD ["sh", "-c", "if [ -f /usr/local/share/ca-certificates/caddy.crt ]; then update-ca-certificates; fi && exec /usr/local/bin/gitmirror serve"] -- 2.51.2