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