collin/anvil · 2b6114fa
Initial commit: anvil — a minimal git forge in Rust on gitoxide
Collin Richards · 2026-06-09 18:03 UTC · 2b6114fae9e5cf8bb66835db9ec6db2a961651c7 · browse files
added.gitignore+6 −0
| 1 | + | /target | |
| 2 | + | /data | |
| 3 | + | *.db | |
| 4 | + | *.db-wal | |
| 5 | + | *.db-shm | |
| 6 | + | anvil.toml |
addedCargo.lock+4860 −0
| 1 | + | # This file is automatically @generated by Cargo. | |
| 2 | + | # It is not intended for manual editing. | |
| 3 | + | version = 4 | |
| 4 | + | ||
| 5 | + | [[package]] | |
| 6 | + | name = "adler2" | |
| 7 | + | version = "2.0.1" | |
| 8 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 9 | + | checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" | |
| 10 | + | ||
| 11 | + | [[package]] | |
| 12 | + | name = "aead" | |
| 13 | + | version = "0.6.0" | |
| 14 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 15 | + | checksum = "ef60ac202874e574ce7a7158cc8bca7313dd344322482e4fadee288bf4a306b8" | |
| 16 | + | dependencies = [ | |
| 17 | + | "crypto-common 0.2.2", | |
| 18 | + | "inout", | |
| 19 | + | ] | |
| 20 | + | ||
| 21 | + | [[package]] | |
| 22 | + | name = "aes" | |
| 23 | + | version = "0.9.1" | |
| 24 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 25 | + | checksum = "f1fc76eaeac4c9164506c466d4ffdd8ec9d0c5bf57ee97177c4d8eceb3a0e138" | |
| 26 | + | dependencies = [ | |
| 27 | + | "cipher", | |
| 28 | + | "cpubits", | |
| 29 | + | "cpufeatures 0.3.0", | |
| 30 | + | "zeroize", | |
| 31 | + | ] | |
| 32 | + | ||
| 33 | + | [[package]] | |
| 34 | + | name = "aes-gcm" | |
| 35 | + | version = "0.11.0-rc.4" | |
| 36 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 37 | + | checksum = "da8c919c118108f144adecad74b425b804ad075580d605d9b33c2d6d1c62a2f8" | |
| 38 | + | dependencies = [ | |
| 39 | + | "aead", | |
| 40 | + | "aes", | |
| 41 | + | "cipher", | |
| 42 | + | "ctr", | |
| 43 | + | "ghash", | |
| 44 | + | "subtle", | |
| 45 | + | "zeroize", | |
| 46 | + | ] | |
| 47 | + | ||
| 48 | + | [[package]] | |
| 49 | + | name = "aho-corasick" | |
| 50 | + | version = "1.1.4" | |
| 51 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 52 | + | checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" | |
| 53 | + | dependencies = [ | |
| 54 | + | "memchr", | |
| 55 | + | ] | |
| 56 | + | ||
| 57 | + | [[package]] | |
| 58 | + | name = "allocator-api2" | |
| 59 | + | version = "0.2.21" | |
| 60 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 61 | + | checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" | |
| 62 | + | ||
| 63 | + | [[package]] | |
| 64 | + | name = "android_system_properties" | |
| 65 | + | version = "0.1.5" | |
| 66 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 67 | + | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" | |
| 68 | + | dependencies = [ | |
| 69 | + | "libc", | |
| 70 | + | ] | |
| 71 | + | ||
| 72 | + | [[package]] | |
| 73 | + | name = "anstream" | |
| 74 | + | version = "1.0.0" | |
| 75 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 76 | + | checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" | |
| 77 | + | dependencies = [ | |
| 78 | + | "anstyle", | |
| 79 | + | "anstyle-parse", | |
| 80 | + | "anstyle-query", | |
| 81 | + | "anstyle-wincon", | |
| 82 | + | "colorchoice", | |
| 83 | + | "is_terminal_polyfill", | |
| 84 | + | "utf8parse", | |
| 85 | + | ] | |
| 86 | + | ||
| 87 | + | [[package]] | |
| 88 | + | name = "anstyle" | |
| 89 | + | version = "1.0.14" | |
| 90 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 91 | + | checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" | |
| 92 | + | ||
| 93 | + | [[package]] | |
| 94 | + | name = "anstyle-parse" | |
| 95 | + | version = "1.0.0" | |
| 96 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 97 | + | checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" | |
| 98 | + | dependencies = [ | |
| 99 | + | "utf8parse", | |
| 100 | + | ] | |
| 101 | + | ||
| 102 | + | [[package]] | |
| 103 | + | name = "anstyle-query" | |
| 104 | + | version = "1.1.5" | |
| 105 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 106 | + | checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" | |
| 107 | + | dependencies = [ | |
| 108 | + | "windows-sys", | |
| 109 | + | ] | |
| 110 | + | ||
| 111 | + | [[package]] | |
| 112 | + | name = "anstyle-wincon" | |
| 113 | + | version = "3.0.11" | |
| 114 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 115 | + | checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" | |
| 116 | + | dependencies = [ | |
| 117 | + | "anstyle", | |
| 118 | + | "once_cell_polyfill", | |
| 119 | + | "windows-sys", | |
| 120 | + | ] | |
| 121 | + | ||
| 122 | + | [[package]] | |
| 123 | + | name = "anvil" | |
| 124 | + | version = "0.0.0" | |
| 125 | + | dependencies = [ | |
| 126 | + | "anvil-core", | |
| 127 | + | "anvil-ssh", | |
| 128 | + | "anvil-web", | |
| 129 | + | "anyhow", | |
| 130 | + | "clap", | |
| 131 | + | "tokio", | |
| 132 | + | "tracing", | |
| 133 | + | "tracing-subscriber", | |
| 134 | + | ] | |
| 135 | + | ||
| 136 | + | [[package]] | |
| 137 | + | name = "anvil-core" | |
| 138 | + | version = "0.0.0" | |
| 139 | + | dependencies = [ | |
| 140 | + | "argon2 0.5.3", | |
| 141 | + | "gix", | |
| 142 | + | "serde", | |
| 143 | + | "ssh-key", | |
| 144 | + | "tempfile", | |
| 145 | + | "thiserror", | |
| 146 | + | "toasty", | |
| 147 | + | "tokio", | |
| 148 | + | "toml", | |
| 149 | + | "tracing", | |
| 150 | + | ] | |
| 151 | + | ||
| 152 | + | [[package]] | |
| 153 | + | name = "anvil-git" | |
| 154 | + | version = "0.0.0" | |
| 155 | + | dependencies = [ | |
| 156 | + | "anvil-core", | |
| 157 | + | "gitserver-core", | |
| 158 | + | "gix", | |
| 159 | + | "thiserror", | |
| 160 | + | "tokio", | |
| 161 | + | "tracing", | |
| 162 | + | ] | |
| 163 | + | ||
| 164 | + | [[package]] | |
| 165 | + | name = "anvil-ssh" | |
| 166 | + | version = "0.0.0" | |
| 167 | + | dependencies = [ | |
| 168 | + | "anvil-core", | |
| 169 | + | "anvil-git", | |
| 170 | + | "rand", | |
| 171 | + | "russh", | |
| 172 | + | "tokio", | |
| 173 | + | "tracing", | |
| 174 | + | ] | |
| 175 | + | ||
| 176 | + | [[package]] | |
| 177 | + | name = "anvil-web" | |
| 178 | + | version = "0.0.0" | |
| 179 | + | dependencies = [ | |
| 180 | + | "anvil-core", | |
| 181 | + | "anvil-git", | |
| 182 | + | "axum", | |
| 183 | + | "axum-extra", | |
| 184 | + | "base64", | |
| 185 | + | "maud", | |
| 186 | + | "serde", | |
| 187 | + | "similar", | |
| 188 | + | "syntect", | |
| 189 | + | "time", | |
| 190 | + | "tokio", | |
| 191 | + | "tokio-util", | |
| 192 | + | "tower-http", | |
| 193 | + | "tracing", | |
| 194 | + | ] | |
| 195 | + | ||
| 196 | + | [[package]] | |
| 197 | + | name = "anyhow" | |
| 198 | + | version = "1.0.102" | |
| 199 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 200 | + | checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" | |
| 201 | + | ||
| 202 | + | [[package]] | |
| 203 | + | name = "arc-swap" | |
| 204 | + | version = "1.9.1" | |
| 205 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 206 | + | checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" | |
| 207 | + | dependencies = [ | |
| 208 | + | "rustversion", | |
| 209 | + | ] | |
| 210 | + | ||
| 211 | + | [[package]] | |
| 212 | + | name = "argon2" | |
| 213 | + | version = "0.5.3" | |
| 214 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 215 | + | checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" | |
| 216 | + | dependencies = [ | |
| 217 | + | "base64ct", | |
| 218 | + | "blake2 0.10.6", | |
| 219 | + | "cpufeatures 0.2.17", | |
| 220 | + | "password-hash 0.5.0", | |
| 221 | + | ] | |
| 222 | + | ||
| 223 | + | [[package]] | |
| 224 | + | name = "argon2" | |
| 225 | + | version = "0.6.0-rc.8" | |
| 226 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 227 | + | checksum = "7af50940b73bf4e16c15c448a2b121c63f2d68e3e54b6a8731673cb4aa0cdff5" | |
| 228 | + | dependencies = [ | |
| 229 | + | "base64ct", | |
| 230 | + | "blake2 0.11.0-rc.6", | |
| 231 | + | "cpufeatures 0.3.0", | |
| 232 | + | "password-hash 0.6.1", | |
| 233 | + | ] | |
| 234 | + | ||
| 235 | + | [[package]] | |
| 236 | + | name = "arrayvec" | |
| 237 | + | version = "0.7.6" | |
| 238 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 239 | + | checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" | |
| 240 | + | ||
| 241 | + | [[package]] | |
| 242 | + | name = "async-trait" | |
| 243 | + | version = "0.1.89" | |
| 244 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 245 | + | checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" | |
| 246 | + | dependencies = [ | |
| 247 | + | "proc-macro2", | |
| 248 | + | "quote", | |
| 249 | + | "syn", | |
| 250 | + | ] | |
| 251 | + | ||
| 252 | + | [[package]] | |
| 253 | + | name = "atomic-waker" | |
| 254 | + | version = "1.1.2" | |
| 255 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 256 | + | checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" | |
| 257 | + | ||
| 258 | + | [[package]] | |
| 259 | + | name = "autocfg" | |
| 260 | + | version = "1.5.1" | |
| 261 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 262 | + | checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" | |
| 263 | + | ||
| 264 | + | [[package]] | |
| 265 | + | name = "aws-lc-rs" | |
| 266 | + | version = "1.17.0" | |
| 267 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 268 | + | checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" | |
| 269 | + | dependencies = [ | |
| 270 | + | "aws-lc-sys", | |
| 271 | + | "untrusted", | |
| 272 | + | "zeroize", | |
| 273 | + | ] | |
| 274 | + | ||
| 275 | + | [[package]] | |
| 276 | + | name = "aws-lc-sys" | |
| 277 | + | version = "0.41.0" | |
| 278 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 279 | + | checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" | |
| 280 | + | dependencies = [ | |
| 281 | + | "cc", | |
| 282 | + | "cmake", | |
| 283 | + | "dunce", | |
| 284 | + | "fs_extra", | |
| 285 | + | ] | |
| 286 | + | ||
| 287 | + | [[package]] | |
| 288 | + | name = "axum" | |
| 289 | + | version = "0.8.9" | |
| 290 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 291 | + | checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" | |
| 292 | + | dependencies = [ | |
| 293 | + | "axum-core", | |
| 294 | + | "bytes", | |
| 295 | + | "form_urlencoded", | |
| 296 | + | "futures-util", | |
| 297 | + | "http", | |
| 298 | + | "http-body", | |
| 299 | + | "http-body-util", | |
| 300 | + | "hyper", | |
| 301 | + | "hyper-util", | |
| 302 | + | "itoa", | |
| 303 | + | "matchit", | |
| 304 | + | "memchr", | |
| 305 | + | "mime", | |
| 306 | + | "percent-encoding", | |
| 307 | + | "pin-project-lite", | |
| 308 | + | "serde_core", | |
| 309 | + | "serde_json", | |
| 310 | + | "serde_path_to_error", | |
| 311 | + | "serde_urlencoded", | |
| 312 | + | "sync_wrapper", | |
| 313 | + | "tokio", | |
| 314 | + | "tower", | |
| 315 | + | "tower-layer", | |
| 316 | + | "tower-service", | |
| 317 | + | "tracing", | |
| 318 | + | ] | |
| 319 | + | ||
| 320 | + | [[package]] | |
| 321 | + | name = "axum-core" | |
| 322 | + | version = "0.5.6" | |
| 323 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 324 | + | checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" | |
| 325 | + | dependencies = [ | |
| 326 | + | "bytes", | |
| 327 | + | "futures-core", | |
| 328 | + | "http", | |
| 329 | + | "http-body", | |
| 330 | + | "http-body-util", | |
| 331 | + | "mime", | |
| 332 | + | "pin-project-lite", | |
| 333 | + | "sync_wrapper", | |
| 334 | + | "tower-layer", | |
| 335 | + | "tower-service", | |
| 336 | + | "tracing", | |
| 337 | + | ] | |
| 338 | + | ||
| 339 | + | [[package]] | |
| 340 | + | name = "axum-extra" | |
| 341 | + | version = "0.10.3" | |
| 342 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 343 | + | checksum = "9963ff19f40c6102c76756ef0a46004c0d58957d87259fc9208ff8441c12ab96" | |
| 344 | + | dependencies = [ | |
| 345 | + | "axum", | |
| 346 | + | "axum-core", | |
| 347 | + | "bytes", | |
| 348 | + | "cookie", | |
| 349 | + | "futures-util", | |
| 350 | + | "http", | |
| 351 | + | "http-body", | |
| 352 | + | "http-body-util", | |
| 353 | + | "mime", | |
| 354 | + | "pin-project-lite", | |
| 355 | + | "rustversion", | |
| 356 | + | "serde_core", | |
| 357 | + | "tower-layer", | |
| 358 | + | "tower-service", | |
| 359 | + | "tracing", | |
| 360 | + | ] | |
| 361 | + | ||
| 362 | + | [[package]] | |
| 363 | + | name = "base16ct" | |
| 364 | + | version = "1.0.0" | |
| 365 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 366 | + | checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6" | |
| 367 | + | ||
| 368 | + | [[package]] | |
| 369 | + | name = "base64" | |
| 370 | + | version = "0.22.1" | |
| 371 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 372 | + | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" | |
| 373 | + | ||
| 374 | + | [[package]] | |
| 375 | + | name = "base64ct" | |
| 376 | + | version = "1.8.3" | |
| 377 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 378 | + | checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" | |
| 379 | + | ||
| 380 | + | [[package]] | |
| 381 | + | name = "bcrypt-pbkdf" | |
| 382 | + | version = "0.11.0" | |
| 383 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 384 | + | checksum = "144e573728da132683b9488acd528274c790e07fc06ff81ee29f9d8f8b1041e0" | |
| 385 | + | dependencies = [ | |
| 386 | + | "blowfish", | |
| 387 | + | "pbkdf2", | |
| 388 | + | "sha2", | |
| 389 | + | ] | |
| 390 | + | ||
| 391 | + | [[package]] | |
| 392 | + | name = "bincode" | |
| 393 | + | version = "1.3.3" | |
| 394 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 395 | + | checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" | |
| 396 | + | dependencies = [ | |
| 397 | + | "serde", | |
| 398 | + | ] | |
| 399 | + | ||
| 400 | + | [[package]] | |
| 401 | + | name = "bit-set" | |
| 402 | + | version = "0.8.0" | |
| 403 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 404 | + | checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" | |
| 405 | + | dependencies = [ | |
| 406 | + | "bit-vec 0.8.0", | |
| 407 | + | ] | |
| 408 | + | ||
| 409 | + | [[package]] | |
| 410 | + | name = "bit-set" | |
| 411 | + | version = "0.10.0" | |
| 412 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 413 | + | checksum = "09ec2f926cc3060f09db9ebc5b52823d85268d24bb917e472c0c4bea35780a7d" | |
| 414 | + | dependencies = [ | |
| 415 | + | "bit-vec 0.9.1", | |
| 416 | + | ] | |
| 417 | + | ||
| 418 | + | [[package]] | |
| 419 | + | name = "bit-vec" | |
| 420 | + | version = "0.8.0" | |
| 421 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 422 | + | checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" | |
| 423 | + | ||
| 424 | + | [[package]] | |
| 425 | + | name = "bit-vec" | |
| 426 | + | version = "0.9.1" | |
| 427 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 428 | + | checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" | |
| 429 | + | dependencies = [ | |
| 430 | + | "serde", | |
| 431 | + | ] | |
| 432 | + | ||
| 433 | + | [[package]] | |
| 434 | + | name = "bitflags" | |
| 435 | + | version = "2.13.0" | |
| 436 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 437 | + | checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" | |
| 438 | + | ||
| 439 | + | [[package]] | |
| 440 | + | name = "blake2" | |
| 441 | + | version = "0.10.6" | |
| 442 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 443 | + | checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" | |
| 444 | + | dependencies = [ | |
| 445 | + | "digest 0.10.7", | |
| 446 | + | ] | |
| 447 | + | ||
| 448 | + | [[package]] | |
| 449 | + | name = "blake2" | |
| 450 | + | version = "0.11.0-rc.6" | |
| 451 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 452 | + | checksum = "061f1a09225e328e1ffbb378d2d49923c0ca5fee19fb5ac1cc9c1e9d52b93690" | |
| 453 | + | dependencies = [ | |
| 454 | + | "digest 0.11.3", | |
| 455 | + | ] | |
| 456 | + | ||
| 457 | + | [[package]] | |
| 458 | + | name = "block-buffer" | |
| 459 | + | version = "0.10.4" | |
| 460 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 461 | + | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" | |
| 462 | + | dependencies = [ | |
| 463 | + | "generic-array 0.14.7", | |
| 464 | + | ] | |
| 465 | + | ||
| 466 | + | [[package]] | |
| 467 | + | name = "block-buffer" | |
| 468 | + | version = "0.12.0" | |
| 469 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 470 | + | checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" | |
| 471 | + | dependencies = [ | |
| 472 | + | "hybrid-array", | |
| 473 | + | "zeroize", | |
| 474 | + | ] | |
| 475 | + | ||
| 476 | + | [[package]] | |
| 477 | + | name = "block-padding" | |
| 478 | + | version = "0.4.2" | |
| 479 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 480 | + | checksum = "710f1dd022ef4e93f8a438b4ba958de7f64308434fa6a87104481645cc30068b" | |
| 481 | + | dependencies = [ | |
| 482 | + | "hybrid-array", | |
| 483 | + | ] | |
| 484 | + | ||
| 485 | + | [[package]] | |
| 486 | + | name = "blowfish" | |
| 487 | + | version = "0.10.0" | |
| 488 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 489 | + | checksum = "62ce3946557b35e71d1bbe07ec385073ce9eda05043f95de134eb578fcf1a298" | |
| 490 | + | dependencies = [ | |
| 491 | + | "byteorder", | |
| 492 | + | "cipher", | |
| 493 | + | ] | |
| 494 | + | ||
| 495 | + | [[package]] | |
| 496 | + | name = "bstr" | |
| 497 | + | version = "1.12.1" | |
| 498 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 499 | + | checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" | |
| 500 | + | dependencies = [ | |
| 501 | + | "memchr", | |
| 502 | + | "regex-automata", | |
| 503 | + | "serde", | |
| 504 | + | ] | |
| 505 | + | ||
| 506 | + | [[package]] | |
| 507 | + | name = "bumpalo" | |
| 508 | + | version = "3.20.3" | |
| 509 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 510 | + | checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" | |
| 511 | + | ||
| 512 | + | [[package]] | |
| 513 | + | name = "by_address" | |
| 514 | + | version = "1.2.1" | |
| 515 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 516 | + | checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" | |
| 517 | + | ||
| 518 | + | [[package]] | |
| 519 | + | name = "byteorder" | |
| 520 | + | version = "1.5.0" | |
| 521 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 522 | + | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" | |
| 523 | + | ||
| 524 | + | [[package]] | |
| 525 | + | name = "bytes" | |
| 526 | + | version = "1.11.1" | |
| 527 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 528 | + | checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" | |
| 529 | + | ||
| 530 | + | [[package]] | |
| 531 | + | name = "cbc" | |
| 532 | + | version = "0.2.1" | |
| 533 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 534 | + | checksum = "ce2dc9ee5f88d11e0beb842c88b33c8a5cf0d1329c4b19494af42b07dbfe8896" | |
| 535 | + | dependencies = [ | |
| 536 | + | "cipher", | |
| 537 | + | ] | |
| 538 | + | ||
| 539 | + | [[package]] | |
| 540 | + | name = "cc" | |
| 541 | + | version = "1.2.63" | |
| 542 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 543 | + | checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" | |
| 544 | + | dependencies = [ | |
| 545 | + | "find-msvc-tools", | |
| 546 | + | "jobserver", | |
| 547 | + | "libc", | |
| 548 | + | "shlex", | |
| 549 | + | ] | |
| 550 | + | ||
| 551 | + | [[package]] | |
| 552 | + | name = "cfg-if" | |
| 553 | + | version = "1.0.4" | |
| 554 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 555 | + | checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" | |
| 556 | + | ||
| 557 | + | [[package]] | |
| 558 | + | name = "cfg_aliases" | |
| 559 | + | version = "0.2.1" | |
| 560 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 561 | + | checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" | |
| 562 | + | ||
| 563 | + | [[package]] | |
| 564 | + | name = "chacha20" | |
| 565 | + | version = "0.10.0" | |
| 566 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 567 | + | checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" | |
| 568 | + | dependencies = [ | |
| 569 | + | "cfg-if", | |
| 570 | + | "cipher", | |
| 571 | + | "cpufeatures 0.3.0", | |
| 572 | + | "rand_core 0.10.1", | |
| 573 | + | "zeroize", | |
| 574 | + | ] | |
| 575 | + | ||
| 576 | + | [[package]] | |
| 577 | + | name = "chrono" | |
| 578 | + | version = "0.4.45" | |
| 579 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 580 | + | checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" | |
| 581 | + | dependencies = [ | |
| 582 | + | "iana-time-zone", | |
| 583 | + | "js-sys", | |
| 584 | + | "num-traits", | |
| 585 | + | "wasm-bindgen", | |
| 586 | + | "windows-link", | |
| 587 | + | ] | |
| 588 | + | ||
| 589 | + | [[package]] | |
| 590 | + | name = "cipher" | |
| 591 | + | version = "0.5.2" | |
| 592 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 593 | + | checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" | |
| 594 | + | dependencies = [ | |
| 595 | + | "block-buffer 0.12.0", | |
| 596 | + | "crypto-common 0.2.2", | |
| 597 | + | "inout", | |
| 598 | + | "zeroize", | |
| 599 | + | ] | |
| 600 | + | ||
| 601 | + | [[package]] | |
| 602 | + | name = "clap" | |
| 603 | + | version = "4.6.1" | |
| 604 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 605 | + | checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" | |
| 606 | + | dependencies = [ | |
| 607 | + | "clap_builder", | |
| 608 | + | "clap_derive", | |
| 609 | + | ] | |
| 610 | + | ||
| 611 | + | [[package]] | |
| 612 | + | name = "clap_builder" | |
| 613 | + | version = "4.6.0" | |
| 614 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 615 | + | checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" | |
| 616 | + | dependencies = [ | |
| 617 | + | "anstream", | |
| 618 | + | "anstyle", | |
| 619 | + | "clap_lex", | |
| 620 | + | "strsim", | |
| 621 | + | ] | |
| 622 | + | ||
| 623 | + | [[package]] | |
| 624 | + | name = "clap_derive" | |
| 625 | + | version = "4.6.1" | |
| 626 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 627 | + | checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" | |
| 628 | + | dependencies = [ | |
| 629 | + | "heck", | |
| 630 | + | "proc-macro2", | |
| 631 | + | "quote", | |
| 632 | + | "syn", | |
| 633 | + | ] | |
| 634 | + | ||
| 635 | + | [[package]] | |
| 636 | + | name = "clap_lex" | |
| 637 | + | version = "1.1.0" | |
| 638 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 639 | + | checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" | |
| 640 | + | ||
| 641 | + | [[package]] | |
| 642 | + | name = "clru" | |
| 643 | + | version = "0.6.3" | |
| 644 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 645 | + | checksum = "197fd99cb113a8d5d9b6376f3aa817f32c1078f2343b714fff7d2ca44fdf67d5" | |
| 646 | + | dependencies = [ | |
| 647 | + | "hashbrown 0.16.1", | |
| 648 | + | ] | |
| 649 | + | ||
| 650 | + | [[package]] | |
| 651 | + | name = "cmake" | |
| 652 | + | version = "0.1.58" | |
| 653 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 654 | + | checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" | |
| 655 | + | dependencies = [ | |
| 656 | + | "cc", | |
| 657 | + | ] | |
| 658 | + | ||
| 659 | + | [[package]] | |
| 660 | + | name = "cmov" | |
| 661 | + | version = "0.5.4" | |
| 662 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 663 | + | checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" | |
| 664 | + | ||
| 665 | + | [[package]] | |
| 666 | + | name = "colorchoice" | |
| 667 | + | version = "1.0.5" | |
| 668 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 669 | + | checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" | |
| 670 | + | ||
| 671 | + | [[package]] | |
| 672 | + | name = "const-oid" | |
| 673 | + | version = "0.10.2" | |
| 674 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 675 | + | checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" | |
| 676 | + | ||
| 677 | + | [[package]] | |
| 678 | + | name = "cookie" | |
| 679 | + | version = "0.18.1" | |
| 680 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 681 | + | checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" | |
| 682 | + | dependencies = [ | |
| 683 | + | "percent-encoding", | |
| 684 | + | "time", | |
| 685 | + | "version_check", | |
| 686 | + | ] | |
| 687 | + | ||
| 688 | + | [[package]] | |
| 689 | + | name = "core-foundation-sys" | |
| 690 | + | version = "0.8.7" | |
| 691 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 692 | + | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" | |
| 693 | + | ||
| 694 | + | [[package]] | |
| 695 | + | name = "cpubits" | |
| 696 | + | version = "0.1.1" | |
| 697 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 698 | + | checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" | |
| 699 | + | ||
| 700 | + | [[package]] | |
| 701 | + | name = "cpufeatures" | |
| 702 | + | version = "0.2.17" | |
| 703 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 704 | + | checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" | |
| 705 | + | dependencies = [ | |
| 706 | + | "libc", | |
| 707 | + | ] | |
| 708 | + | ||
| 709 | + | [[package]] | |
| 710 | + | name = "cpufeatures" | |
| 711 | + | version = "0.3.0" | |
| 712 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 713 | + | checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" | |
| 714 | + | dependencies = [ | |
| 715 | + | "libc", | |
| 716 | + | ] | |
| 717 | + | ||
| 718 | + | [[package]] | |
| 719 | + | name = "crc32fast" | |
| 720 | + | version = "1.5.0" | |
| 721 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 722 | + | checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" | |
| 723 | + | dependencies = [ | |
| 724 | + | "cfg-if", | |
| 725 | + | ] | |
| 726 | + | ||
| 727 | + | [[package]] | |
| 728 | + | name = "crossbeam-channel" | |
| 729 | + | version = "0.5.15" | |
| 730 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 731 | + | checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" | |
| 732 | + | dependencies = [ | |
| 733 | + | "crossbeam-utils", | |
| 734 | + | ] | |
| 735 | + | ||
| 736 | + | [[package]] | |
| 737 | + | name = "crossbeam-utils" | |
| 738 | + | version = "0.8.21" | |
| 739 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 740 | + | checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" | |
| 741 | + | ||
| 742 | + | [[package]] | |
| 743 | + | name = "crypto-bigint" | |
| 744 | + | version = "0.7.3" | |
| 745 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 746 | + | checksum = "42a0d26b245348befa0c121944541476763dcc46ede886c88f9d12e1697d27c3" | |
| 747 | + | dependencies = [ | |
| 748 | + | "cpubits", | |
| 749 | + | "ctutils", | |
| 750 | + | "getrandom 0.4.2", | |
| 751 | + | "hybrid-array", | |
| 752 | + | "num-traits", | |
| 753 | + | "rand_core 0.10.1", | |
| 754 | + | "serdect", | |
| 755 | + | "subtle", | |
| 756 | + | "zeroize", | |
| 757 | + | ] | |
| 758 | + | ||
| 759 | + | [[package]] | |
| 760 | + | name = "crypto-common" | |
| 761 | + | version = "0.1.7" | |
| 762 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 763 | + | checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" | |
| 764 | + | dependencies = [ | |
| 765 | + | "generic-array 0.14.7", | |
| 766 | + | "typenum", | |
| 767 | + | ] | |
| 768 | + | ||
| 769 | + | [[package]] | |
| 770 | + | name = "crypto-common" | |
| 771 | + | version = "0.2.2" | |
| 772 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 773 | + | checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" | |
| 774 | + | dependencies = [ | |
| 775 | + | "getrandom 0.4.2", | |
| 776 | + | "hybrid-array", | |
| 777 | + | "rand_core 0.10.1", | |
| 778 | + | ] | |
| 779 | + | ||
| 780 | + | [[package]] | |
| 781 | + | name = "crypto-primes" | |
| 782 | + | version = "0.7.0" | |
| 783 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 784 | + | checksum = "21f41f23de7d24cdbda7f0c4d9c0351f99a4ceb258ef30e5c1927af8987ffe5a" | |
| 785 | + | dependencies = [ | |
| 786 | + | "crypto-bigint", | |
| 787 | + | "libm", | |
| 788 | + | "rand_core 0.10.1", | |
| 789 | + | ] | |
| 790 | + | ||
| 791 | + | [[package]] | |
| 792 | + | name = "ctr" | |
| 793 | + | version = "0.10.1" | |
| 794 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 795 | + | checksum = "baaca1c4b237092596f64d571e9db6ce4109c4ef9742e27590f1709594461f21" | |
| 796 | + | dependencies = [ | |
| 797 | + | "cipher", | |
| 798 | + | ] | |
| 799 | + | ||
| 800 | + | [[package]] | |
| 801 | + | name = "ctutils" | |
| 802 | + | version = "0.4.2" | |
| 803 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 804 | + | checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" | |
| 805 | + | dependencies = [ | |
| 806 | + | "cmov", | |
| 807 | + | "subtle", | |
| 808 | + | ] | |
| 809 | + | ||
| 810 | + | [[package]] | |
| 811 | + | name = "curve25519-dalek" | |
| 812 | + | version = "5.0.0-rc.0" | |
| 813 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 814 | + | checksum = "4f359e08ca85e7bd759e1fd933ff2bccd81864c60a8fba0e259c7f822b0924bf" | |
| 815 | + | dependencies = [ | |
| 816 | + | "cfg-if", | |
| 817 | + | "cpufeatures 0.3.0", | |
| 818 | + | "curve25519-dalek-derive", | |
| 819 | + | "digest 0.11.3", | |
| 820 | + | "fiat-crypto", | |
| 821 | + | "rustc_version", | |
| 822 | + | "subtle", | |
| 823 | + | "zeroize", | |
| 824 | + | ] | |
| 825 | + | ||
| 826 | + | [[package]] | |
| 827 | + | name = "curve25519-dalek-derive" | |
| 828 | + | version = "0.1.1" | |
| 829 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 830 | + | checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" | |
| 831 | + | dependencies = [ | |
| 832 | + | "proc-macro2", | |
| 833 | + | "quote", | |
| 834 | + | "syn", | |
| 835 | + | ] | |
| 836 | + | ||
| 837 | + | [[package]] | |
| 838 | + | name = "dashmap" | |
| 839 | + | version = "6.2.1" | |
| 840 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 841 | + | checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c" | |
| 842 | + | dependencies = [ | |
| 843 | + | "cfg-if", | |
| 844 | + | "crossbeam-utils", | |
| 845 | + | "hashbrown 0.14.5", | |
| 846 | + | "lock_api", | |
| 847 | + | "once_cell", | |
| 848 | + | "parking_lot_core", | |
| 849 | + | ] | |
| 850 | + | ||
| 851 | + | [[package]] | |
| 852 | + | name = "data-encoding" | |
| 853 | + | version = "2.11.0" | |
| 854 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 855 | + | checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" | |
| 856 | + | ||
| 857 | + | [[package]] | |
| 858 | + | name = "deadpool" | |
| 859 | + | version = "0.13.0" | |
| 860 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 861 | + | checksum = "883466cb8db62725aee5f4a6011e8a5d42912b42632df32aad57fc91127c6e04" | |
| 862 | + | dependencies = [ | |
| 863 | + | "deadpool-runtime", | |
| 864 | + | "num_cpus", | |
| 865 | + | "tokio", | |
| 866 | + | ] | |
| 867 | + | ||
| 868 | + | [[package]] | |
| 869 | + | name = "deadpool-runtime" | |
| 870 | + | version = "0.3.1" | |
| 871 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 872 | + | checksum = "2657f61fb1dd8bf37a8d51093cc7cee4e77125b22f7753f49b289f831bec2bae" | |
| 873 | + | dependencies = [ | |
| 874 | + | "tokio", | |
| 875 | + | ] | |
| 876 | + | ||
| 877 | + | [[package]] | |
| 878 | + | name = "delegate" | |
| 879 | + | version = "0.13.5" | |
| 880 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 881 | + | checksum = "780eb241654bf097afb00fc5f054a09b687dad862e485fdcf8399bb056565370" | |
| 882 | + | dependencies = [ | |
| 883 | + | "proc-macro2", | |
| 884 | + | "quote", | |
| 885 | + | "syn", | |
| 886 | + | ] | |
| 887 | + | ||
| 888 | + | [[package]] | |
| 889 | + | name = "der" | |
| 890 | + | version = "0.8.0" | |
| 891 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 892 | + | checksum = "71fd89660b2dc699704064e59e9dba0147b903e85319429e131620d022be411b" | |
| 893 | + | dependencies = [ | |
| 894 | + | "const-oid", | |
| 895 | + | "pem-rfc7468", | |
| 896 | + | "zeroize", | |
| 897 | + | ] | |
| 898 | + | ||
| 899 | + | [[package]] | |
| 900 | + | name = "deranged" | |
| 901 | + | version = "0.5.8" | |
| 902 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 903 | + | checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" | |
| 904 | + | dependencies = [ | |
| 905 | + | "powerfmt", | |
| 906 | + | "serde_core", | |
| 907 | + | ] | |
| 908 | + | ||
| 909 | + | [[package]] | |
| 910 | + | name = "des" | |
| 911 | + | version = "0.9.0" | |
| 912 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 913 | + | checksum = "916a94e407b54f9034d71dd748234cd1e516ced6284009906ae246f177eafe5a" | |
| 914 | + | dependencies = [ | |
| 915 | + | "cipher", | |
| 916 | + | ] | |
| 917 | + | ||
| 918 | + | [[package]] | |
| 919 | + | name = "digest" | |
| 920 | + | version = "0.10.7" | |
| 921 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 922 | + | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" | |
| 923 | + | dependencies = [ | |
| 924 | + | "block-buffer 0.10.4", | |
| 925 | + | "crypto-common 0.1.7", | |
| 926 | + | "subtle", | |
| 927 | + | ] | |
| 928 | + | ||
| 929 | + | [[package]] | |
| 930 | + | name = "digest" | |
| 931 | + | version = "0.11.3" | |
| 932 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 933 | + | checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" | |
| 934 | + | dependencies = [ | |
| 935 | + | "block-buffer 0.12.0", | |
| 936 | + | "const-oid", | |
| 937 | + | "crypto-common 0.2.2", | |
| 938 | + | "ctutils", | |
| 939 | + | ] | |
| 940 | + | ||
| 941 | + | [[package]] | |
| 942 | + | name = "displaydoc" | |
| 943 | + | version = "0.2.6" | |
| 944 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 945 | + | checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" | |
| 946 | + | dependencies = [ | |
| 947 | + | "proc-macro2", | |
| 948 | + | "quote", | |
| 949 | + | "syn", | |
| 950 | + | ] | |
| 951 | + | ||
| 952 | + | [[package]] | |
| 953 | + | name = "dunce" | |
| 954 | + | version = "1.0.5" | |
| 955 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 956 | + | checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" | |
| 957 | + | ||
| 958 | + | [[package]] | |
| 959 | + | name = "ecdsa" | |
| 960 | + | version = "0.17.0-rc.18" | |
| 961 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 962 | + | checksum = "54fb064faabbee66e1fc8e5c5a9458d4269dc2d8b638fe86a425adb2510d1a96" | |
| 963 | + | dependencies = [ | |
| 964 | + | "der", | |
| 965 | + | "digest 0.11.3", | |
| 966 | + | "elliptic-curve", | |
| 967 | + | "rfc6979", | |
| 968 | + | "signature", | |
| 969 | + | "spki", | |
| 970 | + | "zeroize", | |
| 971 | + | ] | |
| 972 | + | ||
| 973 | + | [[package]] | |
| 974 | + | name = "ed25519" | |
| 975 | + | version = "3.0.0" | |
| 976 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 977 | + | checksum = "29fcf32e6c73d1079f83ab4d782de2d81620346a5f38c6237a86a22f8368980a" | |
| 978 | + | dependencies = [ | |
| 979 | + | "pkcs8", | |
| 980 | + | "signature", | |
| 981 | + | ] | |
| 982 | + | ||
| 983 | + | [[package]] | |
| 984 | + | name = "ed25519-dalek" | |
| 985 | + | version = "3.0.0-rc.0" | |
| 986 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 987 | + | checksum = "b011170fe4f04665565b4110afef66774fe9ffff278f3eb5b81cc73d26e27d60" | |
| 988 | + | dependencies = [ | |
| 989 | + | "curve25519-dalek", | |
| 990 | + | "ed25519", | |
| 991 | + | "rand_core 0.10.1", | |
| 992 | + | "serde", | |
| 993 | + | "sha2", | |
| 994 | + | "signature", | |
| 995 | + | "subtle", | |
| 996 | + | "zeroize", | |
| 997 | + | ] | |
| 998 | + | ||
| 999 | + | [[package]] | |
| 1000 | + | name = "elliptic-curve" | |
| 1001 | + | version = "0.14.0-rc.33" | |
| 1002 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1003 | + | checksum = "102d3643d30dd8b559613c5cced68317199597fffb278cdc88daa2ef7fafc935" | |
| 1004 | + | dependencies = [ | |
| 1005 | + | "base16ct", | |
| 1006 | + | "crypto-bigint", | |
| 1007 | + | "crypto-common 0.2.2", | |
| 1008 | + | "digest 0.11.3", | |
| 1009 | + | "ff", | |
| 1010 | + | "group", | |
| 1011 | + | "hkdf", | |
| 1012 | + | "hybrid-array", | |
| 1013 | + | "once_cell", | |
| 1014 | + | "pem-rfc7468", | |
| 1015 | + | "pkcs8", | |
| 1016 | + | "rand_core 0.10.1", | |
| 1017 | + | "sec1", | |
| 1018 | + | "subtle", | |
| 1019 | + | "zeroize", | |
| 1020 | + | ] | |
| 1021 | + | ||
| 1022 | + | [[package]] | |
| 1023 | + | name = "encoding_rs" | |
| 1024 | + | version = "0.8.35" | |
| 1025 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1026 | + | checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" | |
| 1027 | + | dependencies = [ | |
| 1028 | + | "cfg-if", | |
| 1029 | + | ] | |
| 1030 | + | ||
| 1031 | + | [[package]] | |
| 1032 | + | name = "enum_dispatch" | |
| 1033 | + | version = "0.3.13" | |
| 1034 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1035 | + | checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd" | |
| 1036 | + | dependencies = [ | |
| 1037 | + | "once_cell", | |
| 1038 | + | "proc-macro2", | |
| 1039 | + | "quote", | |
| 1040 | + | "syn", | |
| 1041 | + | ] | |
| 1042 | + | ||
| 1043 | + | [[package]] | |
| 1044 | + | name = "equivalent" | |
| 1045 | + | version = "1.0.2" | |
| 1046 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1047 | + | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" | |
| 1048 | + | ||
| 1049 | + | [[package]] | |
| 1050 | + | name = "errno" | |
| 1051 | + | version = "0.3.14" | |
| 1052 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1053 | + | checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" | |
| 1054 | + | dependencies = [ | |
| 1055 | + | "libc", | |
| 1056 | + | "windows-sys", | |
| 1057 | + | ] | |
| 1058 | + | ||
| 1059 | + | [[package]] | |
| 1060 | + | name = "fallible-iterator" | |
| 1061 | + | version = "0.3.0" | |
| 1062 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1063 | + | checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" | |
| 1064 | + | ||
| 1065 | + | [[package]] | |
| 1066 | + | name = "fallible-streaming-iterator" | |
| 1067 | + | version = "0.1.9" | |
| 1068 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1069 | + | checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" | |
| 1070 | + | ||
| 1071 | + | [[package]] | |
| 1072 | + | name = "fancy-regex" | |
| 1073 | + | version = "0.16.2" | |
| 1074 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1075 | + | checksum = "998b056554fbe42e03ae0e152895cd1a7e1002aec800fdc6635d20270260c46f" | |
| 1076 | + | dependencies = [ | |
| 1077 | + | "bit-set 0.8.0", | |
| 1078 | + | "regex-automata", | |
| 1079 | + | "regex-syntax", | |
| 1080 | + | ] | |
| 1081 | + | ||
| 1082 | + | [[package]] | |
| 1083 | + | name = "faster-hex" | |
| 1084 | + | version = "0.10.0" | |
| 1085 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1086 | + | checksum = "7223ae2d2f179b803433d9c830478527e92b8117eab39460edae7f1614d9fb73" | |
| 1087 | + | dependencies = [ | |
| 1088 | + | "heapless", | |
| 1089 | + | "serde", | |
| 1090 | + | ] | |
| 1091 | + | ||
| 1092 | + | [[package]] | |
| 1093 | + | name = "fastrand" | |
| 1094 | + | version = "2.4.1" | |
| 1095 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1096 | + | checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" | |
| 1097 | + | ||
| 1098 | + | [[package]] | |
| 1099 | + | name = "ff" | |
| 1100 | + | version = "0.14.0" | |
| 1101 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1102 | + | checksum = "a1f686ab92a9fb0eaf188f6c6c87b89490baa6fdb0db4544ba4dc47f7942489f" | |
| 1103 | + | dependencies = [ | |
| 1104 | + | "rand_core 0.10.1", | |
| 1105 | + | "subtle", | |
| 1106 | + | ] | |
| 1107 | + | ||
| 1108 | + | [[package]] | |
| 1109 | + | name = "fiat-crypto" | |
| 1110 | + | version = "0.3.0" | |
| 1111 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1112 | + | checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" | |
| 1113 | + | ||
| 1114 | + | [[package]] | |
| 1115 | + | name = "filetime" | |
| 1116 | + | version = "0.2.29" | |
| 1117 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1118 | + | checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" | |
| 1119 | + | dependencies = [ | |
| 1120 | + | "cfg-if", | |
| 1121 | + | "libc", | |
| 1122 | + | ] | |
| 1123 | + | ||
| 1124 | + | [[package]] | |
| 1125 | + | name = "find-msvc-tools" | |
| 1126 | + | version = "0.1.9" | |
| 1127 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1128 | + | checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" | |
| 1129 | + | ||
| 1130 | + | [[package]] | |
| 1131 | + | name = "flate2" | |
| 1132 | + | version = "1.1.9" | |
| 1133 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1134 | + | checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" | |
| 1135 | + | dependencies = [ | |
| 1136 | + | "crc32fast", | |
| 1137 | + | "miniz_oxide", | |
| 1138 | + | ] | |
| 1139 | + | ||
| 1140 | + | [[package]] | |
| 1141 | + | name = "fnv" | |
| 1142 | + | version = "1.0.7" | |
| 1143 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1144 | + | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" | |
| 1145 | + | ||
| 1146 | + | [[package]] | |
| 1147 | + | name = "foldhash" | |
| 1148 | + | version = "0.1.5" | |
| 1149 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1150 | + | checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" | |
| 1151 | + | ||
| 1152 | + | [[package]] | |
| 1153 | + | name = "foldhash" | |
| 1154 | + | version = "0.2.0" | |
| 1155 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1156 | + | checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" | |
| 1157 | + | ||
| 1158 | + | [[package]] | |
| 1159 | + | name = "form_urlencoded" | |
| 1160 | + | version = "1.2.2" | |
| 1161 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1162 | + | checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" | |
| 1163 | + | dependencies = [ | |
| 1164 | + | "percent-encoding", | |
| 1165 | + | ] | |
| 1166 | + | ||
| 1167 | + | [[package]] | |
| 1168 | + | name = "fs_extra" | |
| 1169 | + | version = "1.3.0" | |
| 1170 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1171 | + | checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" | |
| 1172 | + | ||
| 1173 | + | [[package]] | |
| 1174 | + | name = "futures" | |
| 1175 | + | version = "0.3.32" | |
| 1176 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1177 | + | checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" | |
| 1178 | + | dependencies = [ | |
| 1179 | + | "futures-channel", | |
| 1180 | + | "futures-core", | |
| 1181 | + | "futures-executor", | |
| 1182 | + | "futures-io", | |
| 1183 | + | "futures-sink", | |
| 1184 | + | "futures-task", | |
| 1185 | + | "futures-util", | |
| 1186 | + | ] | |
| 1187 | + | ||
| 1188 | + | [[package]] | |
| 1189 | + | name = "futures-channel" | |
| 1190 | + | version = "0.3.32" | |
| 1191 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1192 | + | checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" | |
| 1193 | + | dependencies = [ | |
| 1194 | + | "futures-core", | |
| 1195 | + | "futures-sink", | |
| 1196 | + | ] | |
| 1197 | + | ||
| 1198 | + | [[package]] | |
| 1199 | + | name = "futures-core" | |
| 1200 | + | version = "0.3.32" | |
| 1201 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1202 | + | checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" | |
| 1203 | + | ||
| 1204 | + | [[package]] | |
| 1205 | + | name = "futures-executor" | |
| 1206 | + | version = "0.3.32" | |
| 1207 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1208 | + | checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" | |
| 1209 | + | dependencies = [ | |
| 1210 | + | "futures-core", | |
| 1211 | + | "futures-task", | |
| 1212 | + | "futures-util", | |
| 1213 | + | ] | |
| 1214 | + | ||
| 1215 | + | [[package]] | |
| 1216 | + | name = "futures-io" | |
| 1217 | + | version = "0.3.32" | |
| 1218 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1219 | + | checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" | |
| 1220 | + | ||
| 1221 | + | [[package]] | |
| 1222 | + | name = "futures-macro" | |
| 1223 | + | version = "0.3.32" | |
| 1224 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1225 | + | checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" | |
| 1226 | + | dependencies = [ | |
| 1227 | + | "proc-macro2", | |
| 1228 | + | "quote", | |
| 1229 | + | "syn", | |
| 1230 | + | ] | |
| 1231 | + | ||
| 1232 | + | [[package]] | |
| 1233 | + | name = "futures-sink" | |
| 1234 | + | version = "0.3.32" | |
| 1235 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1236 | + | checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" | |
| 1237 | + | ||
| 1238 | + | [[package]] | |
| 1239 | + | name = "futures-task" | |
| 1240 | + | version = "0.3.32" | |
| 1241 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1242 | + | checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" | |
| 1243 | + | ||
| 1244 | + | [[package]] | |
| 1245 | + | name = "futures-util" | |
| 1246 | + | version = "0.3.32" | |
| 1247 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1248 | + | checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" | |
| 1249 | + | dependencies = [ | |
| 1250 | + | "futures-channel", | |
| 1251 | + | "futures-core", | |
| 1252 | + | "futures-io", | |
| 1253 | + | "futures-macro", | |
| 1254 | + | "futures-sink", | |
| 1255 | + | "futures-task", | |
| 1256 | + | "memchr", | |
| 1257 | + | "pin-project-lite", | |
| 1258 | + | "slab", | |
| 1259 | + | ] | |
| 1260 | + | ||
| 1261 | + | [[package]] | |
| 1262 | + | name = "generic-array" | |
| 1263 | + | version = "0.14.7" | |
| 1264 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1265 | + | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" | |
| 1266 | + | dependencies = [ | |
| 1267 | + | "typenum", | |
| 1268 | + | "version_check", | |
| 1269 | + | ] | |
| 1270 | + | ||
| 1271 | + | [[package]] | |
| 1272 | + | name = "generic-array" | |
| 1273 | + | version = "1.4.3" | |
| 1274 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1275 | + | checksum = "c2e55f16dcf0e9c00efbe2e655ffe45fc98e7066b52bc92f8a79e64060a79351" | |
| 1276 | + | dependencies = [ | |
| 1277 | + | "generic-array 0.14.7", | |
| 1278 | + | "rustversion", | |
| 1279 | + | "typenum", | |
| 1280 | + | ] | |
| 1281 | + | ||
| 1282 | + | [[package]] | |
| 1283 | + | name = "getrandom" | |
| 1284 | + | version = "0.2.17" | |
| 1285 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1286 | + | checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" | |
| 1287 | + | dependencies = [ | |
| 1288 | + | "cfg-if", | |
| 1289 | + | "libc", | |
| 1290 | + | "wasi", | |
| 1291 | + | ] | |
| 1292 | + | ||
| 1293 | + | [[package]] | |
| 1294 | + | name = "getrandom" | |
| 1295 | + | version = "0.3.4" | |
| 1296 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1297 | + | checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" | |
| 1298 | + | dependencies = [ | |
| 1299 | + | "cfg-if", | |
| 1300 | + | "libc", | |
| 1301 | + | "r-efi 5.3.0", | |
| 1302 | + | "wasip2", | |
| 1303 | + | ] | |
| 1304 | + | ||
| 1305 | + | [[package]] | |
| 1306 | + | name = "getrandom" | |
| 1307 | + | version = "0.4.2" | |
| 1308 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1309 | + | checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" | |
| 1310 | + | dependencies = [ | |
| 1311 | + | "cfg-if", | |
| 1312 | + | "js-sys", | |
| 1313 | + | "libc", | |
| 1314 | + | "r-efi 6.0.0", | |
| 1315 | + | "rand_core 0.10.1", | |
| 1316 | + | "wasip2", | |
| 1317 | + | "wasip3", | |
| 1318 | + | "wasm-bindgen", | |
| 1319 | + | ] | |
| 1320 | + | ||
| 1321 | + | [[package]] | |
| 1322 | + | name = "ghash" | |
| 1323 | + | version = "0.6.0" | |
| 1324 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1325 | + | checksum = "2eecf2d5dc9b66b732b97707a0210906b1d30523eb773193ab777c0c84b3e8d5" | |
| 1326 | + | dependencies = [ | |
| 1327 | + | "polyval", | |
| 1328 | + | ] | |
| 1329 | + | ||
| 1330 | + | [[package]] | |
| 1331 | + | name = "gitserver-core" | |
| 1332 | + | version = "0.0.3" | |
| 1333 | + | dependencies = [ | |
| 1334 | + | "bytes", | |
| 1335 | + | "gix", | |
| 1336 | + | "gix-pack", | |
| 1337 | + | "miniz_oxide", | |
| 1338 | + | "serde", | |
| 1339 | + | "sha1 0.10.6", | |
| 1340 | + | "tempfile", | |
| 1341 | + | "thiserror", | |
| 1342 | + | "tokio", | |
| 1343 | + | "tokio-stream", | |
| 1344 | + | "tokio-util", | |
| 1345 | + | "tracing", | |
| 1346 | + | ] | |
| 1347 | + | ||
| 1348 | + | [[package]] | |
| 1349 | + | name = "gix" | |
| 1350 | + | version = "0.84.0" | |
| 1351 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1352 | + | checksum = "ae54ae0ebd1a5a3c3f8d95dd3b5ca6e63f4fed9bfd585e13801a97d7bde8f9ce" | |
| 1353 | + | dependencies = [ | |
| 1354 | + | "gix-actor", | |
| 1355 | + | "gix-attributes", | |
| 1356 | + | "gix-command", | |
| 1357 | + | "gix-commitgraph", | |
| 1358 | + | "gix-config", | |
| 1359 | + | "gix-date", | |
| 1360 | + | "gix-diff", | |
| 1361 | + | "gix-discover", | |
| 1362 | + | "gix-error", | |
| 1363 | + | "gix-features", | |
| 1364 | + | "gix-filter", | |
| 1365 | + | "gix-fs", | |
| 1366 | + | "gix-glob", | |
| 1367 | + | "gix-hash", | |
| 1368 | + | "gix-hashtable", | |
| 1369 | + | "gix-ignore", | |
| 1370 | + | "gix-index", | |
| 1371 | + | "gix-lock", | |
| 1372 | + | "gix-object", | |
| 1373 | + | "gix-odb", | |
| 1374 | + | "gix-pack", | |
| 1375 | + | "gix-path", | |
| 1376 | + | "gix-pathspec", | |
| 1377 | + | "gix-protocol", | |
| 1378 | + | "gix-ref", | |
| 1379 | + | "gix-refspec", | |
| 1380 | + | "gix-revision", | |
| 1381 | + | "gix-revwalk", | |
| 1382 | + | "gix-sec", | |
| 1383 | + | "gix-shallow", | |
| 1384 | + | "gix-submodule", | |
| 1385 | + | "gix-tempfile", | |
| 1386 | + | "gix-trace", | |
| 1387 | + | "gix-traverse", | |
| 1388 | + | "gix-url", | |
| 1389 | + | "gix-utils", | |
| 1390 | + | "gix-validate", | |
| 1391 | + | "gix-worktree", | |
| 1392 | + | "gix-worktree-stream", | |
| 1393 | + | "nonempty", | |
| 1394 | + | "smallvec", | |
| 1395 | + | "thiserror", | |
| 1396 | + | ] | |
| 1397 | + | ||
| 1398 | + | [[package]] | |
| 1399 | + | name = "gix-actor" | |
| 1400 | + | version = "0.41.1" | |
| 1401 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1402 | + | checksum = "8bc998b8f746dda8565450d08a63b792ced9165d8c27a1ed3f02799ec6a7820f" | |
| 1403 | + | dependencies = [ | |
| 1404 | + | "bstr", | |
| 1405 | + | "gix-date", | |
| 1406 | + | "gix-error", | |
| 1407 | + | ] | |
| 1408 | + | ||
| 1409 | + | [[package]] | |
| 1410 | + | name = "gix-attributes" | |
| 1411 | + | version = "0.33.1" | |
| 1412 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1413 | + | checksum = "8d43f12e246d3bf7ec624c8fc15ac4a4b62b7c4c6f586cb82be6c90bf84c9d02" | |
| 1414 | + | dependencies = [ | |
| 1415 | + | "bstr", | |
| 1416 | + | "gix-glob", | |
| 1417 | + | "gix-path", | |
| 1418 | + | "gix-quote", | |
| 1419 | + | "gix-trace", | |
| 1420 | + | "kstring", | |
| 1421 | + | "smallvec", | |
| 1422 | + | "thiserror", | |
| 1423 | + | "unicode-bom", | |
| 1424 | + | ] | |
| 1425 | + | ||
| 1426 | + | [[package]] | |
| 1427 | + | name = "gix-bitmap" | |
| 1428 | + | version = "0.3.2" | |
| 1429 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1430 | + | checksum = "52ebef0c26ad305747649e727bbcd56a7b7910754eb7cea88f6dff6f93c51283" | |
| 1431 | + | dependencies = [ | |
| 1432 | + | "gix-error", | |
| 1433 | + | ] | |
| 1434 | + | ||
| 1435 | + | [[package]] | |
| 1436 | + | name = "gix-chunk" | |
| 1437 | + | version = "0.7.2" | |
| 1438 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1439 | + | checksum = "9faee47943b638e58ddd5e275a4906ad3e4b6c8584f1d41bd18ab9032ec52afb" | |
| 1440 | + | dependencies = [ | |
| 1441 | + | "gix-error", | |
| 1442 | + | ] | |
| 1443 | + | ||
| 1444 | + | [[package]] | |
| 1445 | + | name = "gix-command" | |
| 1446 | + | version = "0.9.1" | |
| 1447 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1448 | + | checksum = "00706d4fef135ef4b01680d5218c6ee40cda8baf697b864296cbc887d19118f6" | |
| 1449 | + | dependencies = [ | |
| 1450 | + | "bstr", | |
| 1451 | + | "gix-path", | |
| 1452 | + | "gix-quote", | |
| 1453 | + | "gix-trace", | |
| 1454 | + | "shell-words", | |
| 1455 | + | ] | |
| 1456 | + | ||
| 1457 | + | [[package]] | |
| 1458 | + | name = "gix-commitgraph" | |
| 1459 | + | version = "0.37.1" | |
| 1460 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1461 | + | checksum = "7f675d0df484a7f6a47e64bd6f311af489d947c0323b0564f36d14f3d7762abb" | |
| 1462 | + | dependencies = [ | |
| 1463 | + | "bstr", | |
| 1464 | + | "gix-chunk", | |
| 1465 | + | "gix-error", | |
| 1466 | + | "gix-hash", | |
| 1467 | + | "memmap2", | |
| 1468 | + | "nonempty", | |
| 1469 | + | ] | |
| 1470 | + | ||
| 1471 | + | [[package]] | |
| 1472 | + | name = "gix-config" | |
| 1473 | + | version = "0.57.0" | |
| 1474 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1475 | + | checksum = "4f2372d4b49ca28431e7d150cab9d25edc1890f0184bd57eb0e917c7799e63de" | |
| 1476 | + | dependencies = [ | |
| 1477 | + | "bstr", | |
| 1478 | + | "gix-config-value", | |
| 1479 | + | "gix-features", | |
| 1480 | + | "gix-glob", | |
| 1481 | + | "gix-path", | |
| 1482 | + | "gix-ref", | |
| 1483 | + | "gix-sec", | |
| 1484 | + | "smallvec", | |
| 1485 | + | "thiserror", | |
| 1486 | + | "unicode-bom", | |
| 1487 | + | ] | |
| 1488 | + | ||
| 1489 | + | [[package]] | |
| 1490 | + | name = "gix-config-value" | |
| 1491 | + | version = "0.18.1" | |
| 1492 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1493 | + | checksum = "ed42168329552f6c2e5df09665c104199d45d84bedb53683738a49b57fe1baab" | |
| 1494 | + | dependencies = [ | |
| 1495 | + | "bitflags", | |
| 1496 | + | "bstr", | |
| 1497 | + | "gix-path", | |
| 1498 | + | "libc", | |
| 1499 | + | "thiserror", | |
| 1500 | + | ] | |
| 1501 | + | ||
| 1502 | + | [[package]] | |
| 1503 | + | name = "gix-date" | |
| 1504 | + | version = "0.15.4" | |
| 1505 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1506 | + | checksum = "a3ecab64a98bbac9f8e02990a9ea5e3c974a7d49b95f2bd70ad94ad22fa6b48c" | |
| 1507 | + | dependencies = [ | |
| 1508 | + | "bstr", | |
| 1509 | + | "gix-error", | |
| 1510 | + | "itoa", | |
| 1511 | + | "jiff", | |
| 1512 | + | ] | |
| 1513 | + | ||
| 1514 | + | [[package]] | |
| 1515 | + | name = "gix-diff" | |
| 1516 | + | version = "0.64.0" | |
| 1517 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1518 | + | checksum = "3b6d9528f32d94cef2edf39a1ac01fe5a0fc44ddbb18d9e44099936047c3302b" | |
| 1519 | + | dependencies = [ | |
| 1520 | + | "bstr", | |
| 1521 | + | "gix-command", | |
| 1522 | + | "gix-filter", | |
| 1523 | + | "gix-fs", | |
| 1524 | + | "gix-hash", | |
| 1525 | + | "gix-imara-diff", | |
| 1526 | + | "gix-object", | |
| 1527 | + | "gix-path", | |
| 1528 | + | "gix-tempfile", | |
| 1529 | + | "gix-trace", | |
| 1530 | + | "gix-traverse", | |
| 1531 | + | "gix-worktree", | |
| 1532 | + | "thiserror", | |
| 1533 | + | ] | |
| 1534 | + | ||
| 1535 | + | [[package]] | |
| 1536 | + | name = "gix-discover" | |
| 1537 | + | version = "0.52.0" | |
| 1538 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1539 | + | checksum = "77bacdd12b7879d2178a80c58c2f319995e4654e1a7a23e3181e5c8a12b824f7" | |
| 1540 | + | dependencies = [ | |
| 1541 | + | "bstr", | |
| 1542 | + | "dunce", | |
| 1543 | + | "gix-fs", | |
| 1544 | + | "gix-path", | |
| 1545 | + | "gix-ref", | |
| 1546 | + | "gix-sec", | |
| 1547 | + | "thiserror", | |
| 1548 | + | ] | |
| 1549 | + | ||
| 1550 | + | [[package]] | |
| 1551 | + | name = "gix-error" | |
| 1552 | + | version = "0.2.4" | |
| 1553 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1554 | + | checksum = "e57831e199be480af90dcd7e459abed8a174c09ec9a6e2cc8f7ca6c54598b06b" | |
| 1555 | + | dependencies = [ | |
| 1556 | + | "bstr", | |
| 1557 | + | ] | |
| 1558 | + | ||
| 1559 | + | [[package]] | |
| 1560 | + | name = "gix-features" | |
| 1561 | + | version = "0.48.1" | |
| 1562 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1563 | + | checksum = "1849ae154d38bc403185be14fa871e38e3c93ee606875d94e207fdb9fba52dbc" | |
| 1564 | + | dependencies = [ | |
| 1565 | + | "bytes", | |
| 1566 | + | "crc32fast", | |
| 1567 | + | "crossbeam-channel", | |
| 1568 | + | "gix-path", | |
| 1569 | + | "gix-trace", | |
| 1570 | + | "gix-utils", | |
| 1571 | + | "libc", | |
| 1572 | + | "once_cell", | |
| 1573 | + | "parking_lot", | |
| 1574 | + | "prodash", | |
| 1575 | + | "thiserror", | |
| 1576 | + | "walkdir", | |
| 1577 | + | "zlib-rs", | |
| 1578 | + | ] | |
| 1579 | + | ||
| 1580 | + | [[package]] | |
| 1581 | + | name = "gix-filter" | |
| 1582 | + | version = "0.31.0" | |
| 1583 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1584 | + | checksum = "ecf74b7d16f6694ce4a3049074c41be0c7987105743674f1671807bd6dce09fa" | |
| 1585 | + | dependencies = [ | |
| 1586 | + | "bstr", | |
| 1587 | + | "encoding_rs", | |
| 1588 | + | "gix-attributes", | |
| 1589 | + | "gix-command", | |
| 1590 | + | "gix-hash", | |
| 1591 | + | "gix-object", | |
| 1592 | + | "gix-packetline", | |
| 1593 | + | "gix-path", | |
| 1594 | + | "gix-quote", | |
| 1595 | + | "gix-trace", | |
| 1596 | + | "gix-utils", | |
| 1597 | + | "smallvec", | |
| 1598 | + | "thiserror", | |
| 1599 | + | ] | |
| 1600 | + | ||
| 1601 | + | [[package]] | |
| 1602 | + | name = "gix-fs" | |
| 1603 | + | version = "0.21.2" | |
| 1604 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1605 | + | checksum = "6cdff46db8798e47e2f727d84b9379aac5add3dd3d9d0b07bb4d7d5d640771fe" | |
| 1606 | + | dependencies = [ | |
| 1607 | + | "bstr", | |
| 1608 | + | "fastrand", | |
| 1609 | + | "gix-features", | |
| 1610 | + | "gix-path", | |
| 1611 | + | "gix-utils", | |
| 1612 | + | "thiserror", | |
| 1613 | + | ] | |
| 1614 | + | ||
| 1615 | + | [[package]] | |
| 1616 | + | name = "gix-glob" | |
| 1617 | + | version = "0.26.1" | |
| 1618 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1619 | + | checksum = "d1fcb8ef5b16bcf874abe9b68d8abb3c0493c876d367ab824151f30a0f3f3756" | |
| 1620 | + | dependencies = [ | |
| 1621 | + | "bitflags", | |
| 1622 | + | "bstr", | |
| 1623 | + | "gix-features", | |
| 1624 | + | "gix-path", | |
| 1625 | + | ] | |
| 1626 | + | ||
| 1627 | + | [[package]] | |
| 1628 | + | name = "gix-hash" | |
| 1629 | + | version = "0.25.1" | |
| 1630 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1631 | + | checksum = "cb0926d3819c837750b4e03c7754901e73f68b8c9b690753a6372a1bed4eedce" | |
| 1632 | + | dependencies = [ | |
| 1633 | + | "faster-hex", | |
| 1634 | + | "gix-features", | |
| 1635 | + | "sha1-checked", | |
| 1636 | + | "thiserror", | |
| 1637 | + | ] | |
| 1638 | + | ||
| 1639 | + | [[package]] | |
| 1640 | + | name = "gix-hashtable" | |
| 1641 | + | version = "0.15.1" | |
| 1642 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1643 | + | checksum = "b0e30b93eea8718baf7d8153fcb938e2926175bbf18097c09f1c01b6f0be0563" | |
| 1644 | + | dependencies = [ | |
| 1645 | + | "gix-hash", | |
| 1646 | + | "hashbrown 0.17.1", | |
| 1647 | + | "parking_lot", | |
| 1648 | + | ] | |
| 1649 | + | ||
| 1650 | + | [[package]] | |
| 1651 | + | name = "gix-ignore" | |
| 1652 | + | version = "0.21.1" | |
| 1653 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1654 | + | checksum = "d491bab9bf2c9f341dc754f425c31d5d3f63aca615312167b82e1deeaca97d8d" | |
| 1655 | + | dependencies = [ | |
| 1656 | + | "bstr", | |
| 1657 | + | "gix-glob", | |
| 1658 | + | "gix-path", | |
| 1659 | + | "gix-trace", | |
| 1660 | + | "unicode-bom", | |
| 1661 | + | ] | |
| 1662 | + | ||
| 1663 | + | [[package]] | |
| 1664 | + | name = "gix-imara-diff" | |
| 1665 | + | version = "0.2.2" | |
| 1666 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1667 | + | checksum = "19753d40da53d0ec41604750eeb969097a90fb2d7f7992730d904541c04e2c19" | |
| 1668 | + | dependencies = [ | |
| 1669 | + | "bstr", | |
| 1670 | + | "hashbrown 0.17.1", | |
| 1671 | + | ] | |
| 1672 | + | ||
| 1673 | + | [[package]] | |
| 1674 | + | name = "gix-index" | |
| 1675 | + | version = "0.52.0" | |
| 1676 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1677 | + | checksum = "4e6b28cc592dc753adb58302bb14a64e412ee591a3bec77aa4df87bff74fa80d" | |
| 1678 | + | dependencies = [ | |
| 1679 | + | "bitflags", | |
| 1680 | + | "bstr", | |
| 1681 | + | "filetime", | |
| 1682 | + | "fnv", | |
| 1683 | + | "gix-bitmap", | |
| 1684 | + | "gix-features", | |
| 1685 | + | "gix-fs", | |
| 1686 | + | "gix-hash", | |
| 1687 | + | "gix-lock", | |
| 1688 | + | "gix-object", | |
| 1689 | + | "gix-traverse", | |
| 1690 | + | "gix-utils", | |
| 1691 | + | "gix-validate", | |
| 1692 | + | "hashbrown 0.17.1", | |
| 1693 | + | "itoa", | |
| 1694 | + | "libc", | |
| 1695 | + | "memmap2", | |
| 1696 | + | "rustix", | |
| 1697 | + | "smallvec", | |
| 1698 | + | "thiserror", | |
| 1699 | + | ] | |
| 1700 | + | ||
| 1701 | + | [[package]] | |
| 1702 | + | name = "gix-lock" | |
| 1703 | + | version = "23.0.1" | |
| 1704 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1705 | + | checksum = "65c9dedd9e90b0d47624d2ed241d394e09294118364e87b9b7e5f1fe755f3c2c" | |
| 1706 | + | dependencies = [ | |
| 1707 | + | "gix-tempfile", | |
| 1708 | + | "gix-utils", | |
| 1709 | + | "thiserror", | |
| 1710 | + | ] | |
| 1711 | + | ||
| 1712 | + | [[package]] | |
| 1713 | + | name = "gix-object" | |
| 1714 | + | version = "0.61.0" | |
| 1715 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1716 | + | checksum = "d5cd857e29429c7213bdef3f5aef83f8cc124774fe8ae0d27b1607d218d6d525" | |
| 1717 | + | dependencies = [ | |
| 1718 | + | "bstr", | |
| 1719 | + | "gix-actor", | |
| 1720 | + | "gix-date", | |
| 1721 | + | "gix-features", | |
| 1722 | + | "gix-hash", | |
| 1723 | + | "gix-hashtable", | |
| 1724 | + | "gix-utils", | |
| 1725 | + | "gix-validate", | |
| 1726 | + | "itoa", | |
| 1727 | + | "smallvec", | |
| 1728 | + | "thiserror", | |
| 1729 | + | ] | |
| 1730 | + | ||
| 1731 | + | [[package]] | |
| 1732 | + | name = "gix-odb" | |
| 1733 | + | version = "0.81.0" | |
| 1734 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1735 | + | checksum = "7d004c32858b1556f2d7874405edb3c97dc78fc09beaa87d57bb077ee2858a7d" | |
| 1736 | + | dependencies = [ | |
| 1737 | + | "arc-swap", | |
| 1738 | + | "gix-features", | |
| 1739 | + | "gix-fs", | |
| 1740 | + | "gix-hash", | |
| 1741 | + | "gix-hashtable", | |
| 1742 | + | "gix-object", | |
| 1743 | + | "gix-pack", | |
| 1744 | + | "gix-path", | |
| 1745 | + | "gix-quote", | |
| 1746 | + | "memmap2", | |
| 1747 | + | "parking_lot", | |
| 1748 | + | "tempfile", | |
| 1749 | + | "thiserror", | |
| 1750 | + | ] | |
| 1751 | + | ||
| 1752 | + | [[package]] | |
| 1753 | + | name = "gix-pack" | |
| 1754 | + | version = "0.71.0" | |
| 1755 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1756 | + | checksum = "e43626f2a27d1033674ec1a196b845614231e6bbd949d5e21c133045ff56b174" | |
| 1757 | + | dependencies = [ | |
| 1758 | + | "clru", | |
| 1759 | + | "gix-chunk", | |
| 1760 | + | "gix-diff", | |
| 1761 | + | "gix-error", | |
| 1762 | + | "gix-features", | |
| 1763 | + | "gix-hash", | |
| 1764 | + | "gix-hashtable", | |
| 1765 | + | "gix-object", | |
| 1766 | + | "gix-path", | |
| 1767 | + | "gix-tempfile", | |
| 1768 | + | "gix-traverse", | |
| 1769 | + | "memmap2", | |
| 1770 | + | "parking_lot", | |
| 1771 | + | "smallvec", | |
| 1772 | + | "thiserror", | |
| 1773 | + | "uluru", | |
| 1774 | + | ] | |
| 1775 | + | ||
| 1776 | + | [[package]] | |
| 1777 | + | name = "gix-packetline" | |
| 1778 | + | version = "0.21.4" | |
| 1779 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1780 | + | checksum = "bb18337ba2830bb43367d1af43819c8c78f31337f079fc76d0f1f1750a173126" | |
| 1781 | + | dependencies = [ | |
| 1782 | + | "bstr", | |
| 1783 | + | "faster-hex", | |
| 1784 | + | "gix-trace", | |
| 1785 | + | "thiserror", | |
| 1786 | + | ] | |
| 1787 | + | ||
| 1788 | + | [[package]] | |
| 1789 | + | name = "gix-path" | |
| 1790 | + | version = "0.12.1" | |
| 1791 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1792 | + | checksum = "afa6ac14cd14939ea94a496ce7460daa6511c09f5b84757e9cfc6f9c8d0f93a6" | |
| 1793 | + | dependencies = [ | |
| 1794 | + | "bstr", | |
| 1795 | + | "gix-trace", | |
| 1796 | + | "gix-validate", | |
| 1797 | + | "thiserror", | |
| 1798 | + | ] | |
| 1799 | + | ||
| 1800 | + | [[package]] | |
| 1801 | + | name = "gix-pathspec" | |
| 1802 | + | version = "0.18.1" | |
| 1803 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1804 | + | checksum = "3050783b41ee11511e1e8fb35623df81806194f4030395f14f48ea37c2798c9f" | |
| 1805 | + | dependencies = [ | |
| 1806 | + | "bitflags", | |
| 1807 | + | "bstr", | |
| 1808 | + | "gix-attributes", | |
| 1809 | + | "gix-config-value", | |
| 1810 | + | "gix-glob", | |
| 1811 | + | "gix-path", | |
| 1812 | + | "thiserror", | |
| 1813 | + | ] | |
| 1814 | + | ||
| 1815 | + | [[package]] | |
| 1816 | + | name = "gix-protocol" | |
| 1817 | + | version = "0.62.0" | |
| 1818 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1819 | + | checksum = "51dea3acb390707ab868f1f9584f18449eb95d869deffae96768e47d303595ee" | |
| 1820 | + | dependencies = [ | |
| 1821 | + | "bstr", | |
| 1822 | + | "gix-date", | |
| 1823 | + | "gix-features", | |
| 1824 | + | "gix-hash", | |
| 1825 | + | "gix-ref", | |
| 1826 | + | "gix-shallow", | |
| 1827 | + | "gix-transport", | |
| 1828 | + | "gix-utils", | |
| 1829 | + | "maybe-async", | |
| 1830 | + | "nonempty", | |
| 1831 | + | "thiserror", | |
| 1832 | + | ] | |
| 1833 | + | ||
| 1834 | + | [[package]] | |
| 1835 | + | name = "gix-quote" | |
| 1836 | + | version = "0.7.2" | |
| 1837 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1838 | + | checksum = "a6e541fc33cc2b783b7979040d445a0c86a2eca747c8faea4ca84230d06ae6ef" | |
| 1839 | + | dependencies = [ | |
| 1840 | + | "bstr", | |
| 1841 | + | "gix-error", | |
| 1842 | + | "gix-utils", | |
| 1843 | + | ] | |
| 1844 | + | ||
| 1845 | + | [[package]] | |
| 1846 | + | name = "gix-ref" | |
| 1847 | + | version = "0.64.0" | |
| 1848 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1849 | + | checksum = "4c04f64c37eb7e6feb73c7060f8dc6f381cc5de5d53249bfd450bc48a86b2e8b" | |
| 1850 | + | dependencies = [ | |
| 1851 | + | "gix-actor", | |
| 1852 | + | "gix-features", | |
| 1853 | + | "gix-fs", | |
| 1854 | + | "gix-hash", | |
| 1855 | + | "gix-lock", | |
| 1856 | + | "gix-object", | |
| 1857 | + | "gix-path", | |
| 1858 | + | "gix-tempfile", | |
| 1859 | + | "gix-utils", | |
| 1860 | + | "gix-validate", | |
| 1861 | + | "memmap2", | |
| 1862 | + | "thiserror", | |
| 1863 | + | ] | |
| 1864 | + | ||
| 1865 | + | [[package]] | |
| 1866 | + | name = "gix-refspec" | |
| 1867 | + | version = "0.42.0" | |
| 1868 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1869 | + | checksum = "b216ae06ec74b5f24ad0142026a997fb0a935b7410eaf9c1616fc3f0e6c5a6d3" | |
| 1870 | + | dependencies = [ | |
| 1871 | + | "bstr", | |
| 1872 | + | "gix-error", | |
| 1873 | + | "gix-glob", | |
| 1874 | + | "gix-hash", | |
| 1875 | + | "gix-revision", | |
| 1876 | + | "gix-validate", | |
| 1877 | + | "smallvec", | |
| 1878 | + | "thiserror", | |
| 1879 | + | ] | |
| 1880 | + | ||
| 1881 | + | [[package]] | |
| 1882 | + | name = "gix-revision" | |
| 1883 | + | version = "0.46.0" | |
| 1884 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1885 | + | checksum = "0b47c88884dd3c1a19a39da19d10211fcdea2809aadc86869b6e824a1774340f" | |
| 1886 | + | dependencies = [ | |
| 1887 | + | "bitflags", | |
| 1888 | + | "bstr", | |
| 1889 | + | "gix-commitgraph", | |
| 1890 | + | "gix-date", | |
| 1891 | + | "gix-error", | |
| 1892 | + | "gix-hash", | |
| 1893 | + | "gix-hashtable", | |
| 1894 | + | "gix-object", | |
| 1895 | + | "gix-revwalk", | |
| 1896 | + | "gix-trace", | |
| 1897 | + | "nonempty", | |
| 1898 | + | ] | |
| 1899 | + | ||
| 1900 | + | [[package]] | |
| 1901 | + | name = "gix-revwalk" | |
| 1902 | + | version = "0.32.0" | |
| 1903 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1904 | + | checksum = "85f5756abffe0917827aac683b13684ed99875bc398fa1f9b8f479b0681ef9e6" | |
| 1905 | + | dependencies = [ | |
| 1906 | + | "gix-commitgraph", | |
| 1907 | + | "gix-date", | |
| 1908 | + | "gix-error", | |
| 1909 | + | "gix-hash", | |
| 1910 | + | "gix-hashtable", | |
| 1911 | + | "gix-object", | |
| 1912 | + | "smallvec", | |
| 1913 | + | "thiserror", | |
| 1914 | + | ] | |
| 1915 | + | ||
| 1916 | + | [[package]] | |
| 1917 | + | name = "gix-sec" | |
| 1918 | + | version = "0.14.1" | |
| 1919 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1920 | + | checksum = "ab8519976e4c7e486270740a5400369f37940779b80bd1377d94cfa1125d01b3" | |
| 1921 | + | dependencies = [ | |
| 1922 | + | "bitflags", | |
| 1923 | + | "gix-path", | |
| 1924 | + | "libc", | |
| 1925 | + | "windows-sys", | |
| 1926 | + | ] | |
| 1927 | + | ||
| 1928 | + | [[package]] | |
| 1929 | + | name = "gix-shallow" | |
| 1930 | + | version = "0.12.1" | |
| 1931 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1932 | + | checksum = "a292fc2fe548c5dfa575479d16b445b0ddf1dd2f56f1fec6aed386f82553cd97" | |
| 1933 | + | dependencies = [ | |
| 1934 | + | "bstr", | |
| 1935 | + | "gix-hash", | |
| 1936 | + | "gix-lock", | |
| 1937 | + | "nonempty", | |
| 1938 | + | "thiserror", | |
| 1939 | + | ] | |
| 1940 | + | ||
| 1941 | + | [[package]] | |
| 1942 | + | name = "gix-submodule" | |
| 1943 | + | version = "0.31.0" | |
| 1944 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1945 | + | checksum = "3059890ef054066c22a94bfc6a3eaba0d806aedcd630a0bc9e5783fd88884781" | |
| 1946 | + | dependencies = [ | |
| 1947 | + | "bstr", | |
| 1948 | + | "gix-config", | |
| 1949 | + | "gix-path", | |
| 1950 | + | "gix-pathspec", | |
| 1951 | + | "gix-refspec", | |
| 1952 | + | "gix-url", | |
| 1953 | + | "thiserror", | |
| 1954 | + | ] | |
| 1955 | + | ||
| 1956 | + | [[package]] | |
| 1957 | + | name = "gix-tempfile" | |
| 1958 | + | version = "23.0.1" | |
| 1959 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1960 | + | checksum = "27850097e1ff9515f46a0dad0f5f9c9d020e972727772dabab9450690c4adb22" | |
| 1961 | + | dependencies = [ | |
| 1962 | + | "dashmap", | |
| 1963 | + | "gix-fs", | |
| 1964 | + | "libc", | |
| 1965 | + | "parking_lot", | |
| 1966 | + | "tempfile", | |
| 1967 | + | ] | |
| 1968 | + | ||
| 1969 | + | [[package]] | |
| 1970 | + | name = "gix-trace" | |
| 1971 | + | version = "0.1.20" | |
| 1972 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1973 | + | checksum = "44dc45eae785c0eb14173e0f152e6e224dcf4d45b6a6999a3aed22af541ad678" | |
| 1974 | + | ||
| 1975 | + | [[package]] | |
| 1976 | + | name = "gix-transport" | |
| 1977 | + | version = "0.57.1" | |
| 1978 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1979 | + | checksum = "7cd0e34995b1aab0fa8dff2af8db726a0bfad3e119c89302604463264046e7ff" | |
| 1980 | + | dependencies = [ | |
| 1981 | + | "bstr", | |
| 1982 | + | "gix-command", | |
| 1983 | + | "gix-features", | |
| 1984 | + | "gix-packetline", | |
| 1985 | + | "gix-quote", | |
| 1986 | + | "gix-sec", | |
| 1987 | + | "gix-url", | |
| 1988 | + | "thiserror", | |
| 1989 | + | ] | |
| 1990 | + | ||
| 1991 | + | [[package]] | |
| 1992 | + | name = "gix-traverse" | |
| 1993 | + | version = "0.58.0" | |
| 1994 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1995 | + | checksum = "e8de590ecc86a3b2870665f2288324fa9f7f8672c7fc2d4e020fdd81cd1f7aed" | |
| 1996 | + | dependencies = [ | |
| 1997 | + | "bitflags", | |
| 1998 | + | "gix-commitgraph", | |
| 1999 | + | "gix-date", | |
| 2000 | + | "gix-hash", | |
| 2001 | + | "gix-hashtable", | |
| 2002 | + | "gix-object", | |
| 2003 | + | "gix-revwalk", | |
| 2004 | + | "smallvec", | |
| 2005 | + | "thiserror", | |
| 2006 | + | ] | |
| 2007 | + | ||
| 2008 | + | [[package]] | |
| 2009 | + | name = "gix-url" | |
| 2010 | + | version = "0.36.1" | |
| 2011 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2012 | + | checksum = "65bb01ec69d55e82ccb7a19e264501ead4e6aac38463a8cebfdd81e22bb67ab2" | |
| 2013 | + | dependencies = [ | |
| 2014 | + | "bstr", | |
| 2015 | + | "gix-path", | |
| 2016 | + | "percent-encoding", | |
| 2017 | + | "thiserror", | |
| 2018 | + | ] | |
| 2019 | + | ||
| 2020 | + | [[package]] | |
| 2021 | + | name = "gix-utils" | |
| 2022 | + | version = "0.3.3" | |
| 2023 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2024 | + | checksum = "66c50966184123caf580ffa64e28031a878597f1c7fceb8fe19566c38eb1b771" | |
| 2025 | + | dependencies = [ | |
| 2026 | + | "fastrand", | |
| 2027 | + | "unicode-normalization", | |
| 2028 | + | ] | |
| 2029 | + | ||
| 2030 | + | [[package]] | |
| 2031 | + | name = "gix-validate" | |
| 2032 | + | version = "0.11.2" | |
| 2033 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2034 | + | checksum = "7bc6fc771c4063ba7cd2f47b91fb6076251c6a823b64b7fe7b8874b0fe4afae3" | |
| 2035 | + | dependencies = [ | |
| 2036 | + | "bstr", | |
| 2037 | + | ] | |
| 2038 | + | ||
| 2039 | + | [[package]] | |
| 2040 | + | name = "gix-worktree" | |
| 2041 | + | version = "0.53.0" | |
| 2042 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2043 | + | checksum = "cef414ed275e8407cd5d53d301e83be19700b0dd3f859d2434417b58f454a2d1" | |
| 2044 | + | dependencies = [ | |
| 2045 | + | "bstr", | |
| 2046 | + | "gix-attributes", | |
| 2047 | + | "gix-fs", | |
| 2048 | + | "gix-glob", | |
| 2049 | + | "gix-hash", | |
| 2050 | + | "gix-ignore", | |
| 2051 | + | "gix-index", | |
| 2052 | + | "gix-object", | |
| 2053 | + | "gix-path", | |
| 2054 | + | "gix-validate", | |
| 2055 | + | ] | |
| 2056 | + | ||
| 2057 | + | [[package]] | |
| 2058 | + | name = "gix-worktree-stream" | |
| 2059 | + | version = "0.33.0" | |
| 2060 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2061 | + | checksum = "d25e9ed30100c63f7590bc581c225e53f731a53e06aa79a245739c07f7dcc557" | |
| 2062 | + | dependencies = [ | |
| 2063 | + | "gix-attributes", | |
| 2064 | + | "gix-error", | |
| 2065 | + | "gix-features", | |
| 2066 | + | "gix-filter", | |
| 2067 | + | "gix-fs", | |
| 2068 | + | "gix-hash", | |
| 2069 | + | "gix-object", | |
| 2070 | + | "gix-path", | |
| 2071 | + | "gix-traverse", | |
| 2072 | + | "parking_lot", | |
| 2073 | + | ] | |
| 2074 | + | ||
| 2075 | + | [[package]] | |
| 2076 | + | name = "group" | |
| 2077 | + | version = "0.14.0" | |
| 2078 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2079 | + | checksum = "7fd1a1c7a5206c5b7a3f5a0d7ccd3ff85d0c8f5133d62a02680255b0004af5f4" | |
| 2080 | + | dependencies = [ | |
| 2081 | + | "ff", | |
| 2082 | + | "rand_core 0.10.1", | |
| 2083 | + | "subtle", | |
| 2084 | + | ] | |
| 2085 | + | ||
| 2086 | + | [[package]] | |
| 2087 | + | name = "hash32" | |
| 2088 | + | version = "0.3.1" | |
| 2089 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2090 | + | checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" | |
| 2091 | + | dependencies = [ | |
| 2092 | + | "byteorder", | |
| 2093 | + | ] | |
| 2094 | + | ||
| 2095 | + | [[package]] | |
| 2096 | + | name = "hashbrown" | |
| 2097 | + | version = "0.14.5" | |
| 2098 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2099 | + | checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" | |
| 2100 | + | ||
| 2101 | + | [[package]] | |
| 2102 | + | name = "hashbrown" | |
| 2103 | + | version = "0.15.5" | |
| 2104 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2105 | + | checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" | |
| 2106 | + | dependencies = [ | |
| 2107 | + | "foldhash 0.1.5", | |
| 2108 | + | ] | |
| 2109 | + | ||
| 2110 | + | [[package]] | |
| 2111 | + | name = "hashbrown" | |
| 2112 | + | version = "0.16.1" | |
| 2113 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2114 | + | checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" | |
| 2115 | + | dependencies = [ | |
| 2116 | + | "allocator-api2", | |
| 2117 | + | "equivalent", | |
| 2118 | + | "foldhash 0.2.0", | |
| 2119 | + | ] | |
| 2120 | + | ||
| 2121 | + | [[package]] | |
| 2122 | + | name = "hashbrown" | |
| 2123 | + | version = "0.17.1" | |
| 2124 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2125 | + | checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" | |
| 2126 | + | dependencies = [ | |
| 2127 | + | "allocator-api2", | |
| 2128 | + | "equivalent", | |
| 2129 | + | "foldhash 0.2.0", | |
| 2130 | + | ] | |
| 2131 | + | ||
| 2132 | + | [[package]] | |
| 2133 | + | name = "hashlink" | |
| 2134 | + | version = "0.11.1" | |
| 2135 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2136 | + | checksum = "824e001ac4f3012dd16a264bec811403a67ca9deb6c102fc5049b32c4574b35f" | |
| 2137 | + | dependencies = [ | |
| 2138 | + | "hashbrown 0.16.1", | |
| 2139 | + | ] | |
| 2140 | + | ||
| 2141 | + | [[package]] | |
| 2142 | + | name = "heapless" | |
| 2143 | + | version = "0.8.0" | |
| 2144 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2145 | + | checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" | |
| 2146 | + | dependencies = [ | |
| 2147 | + | "hash32", | |
| 2148 | + | "stable_deref_trait", | |
| 2149 | + | ] | |
| 2150 | + | ||
| 2151 | + | [[package]] | |
| 2152 | + | name = "heck" | |
| 2153 | + | version = "0.5.0" | |
| 2154 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2155 | + | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" | |
| 2156 | + | ||
| 2157 | + | [[package]] | |
| 2158 | + | name = "hermit-abi" | |
| 2159 | + | version = "0.5.2" | |
| 2160 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2161 | + | checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" | |
| 2162 | + | ||
| 2163 | + | [[package]] | |
| 2164 | + | name = "hex" | |
| 2165 | + | version = "0.4.3" | |
| 2166 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2167 | + | checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" | |
| 2168 | + | ||
| 2169 | + | [[package]] | |
| 2170 | + | name = "hex-literal" | |
| 2171 | + | version = "1.1.0" | |
| 2172 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2173 | + | checksum = "e712f64ec3850b98572bffac52e2c6f282b29fe6c5fa6d42334b30be438d95c1" | |
| 2174 | + | ||
| 2175 | + | [[package]] | |
| 2176 | + | name = "hkdf" | |
| 2177 | + | version = "0.13.0" | |
| 2178 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2179 | + | checksum = "4aaa26c720c68b866f2c96ef5c1264b3e6f473fe5d4ce61cd44bbe913e553018" | |
| 2180 | + | dependencies = [ | |
| 2181 | + | "hmac", | |
| 2182 | + | ] | |
| 2183 | + | ||
| 2184 | + | [[package]] | |
| 2185 | + | name = "hmac" | |
| 2186 | + | version = "0.13.0" | |
| 2187 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2188 | + | checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" | |
| 2189 | + | dependencies = [ | |
| 2190 | + | "digest 0.11.3", | |
| 2191 | + | ] | |
| 2192 | + | ||
| 2193 | + | [[package]] | |
| 2194 | + | name = "http" | |
| 2195 | + | version = "1.4.2" | |
| 2196 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2197 | + | checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" | |
| 2198 | + | dependencies = [ | |
| 2199 | + | "bytes", | |
| 2200 | + | "itoa", | |
| 2201 | + | ] | |
| 2202 | + | ||
| 2203 | + | [[package]] | |
| 2204 | + | name = "http-body" | |
| 2205 | + | version = "1.0.1" | |
| 2206 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2207 | + | checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" | |
| 2208 | + | dependencies = [ | |
| 2209 | + | "bytes", | |
| 2210 | + | "http", | |
| 2211 | + | ] | |
| 2212 | + | ||
| 2213 | + | [[package]] | |
| 2214 | + | name = "http-body-util" | |
| 2215 | + | version = "0.1.3" | |
| 2216 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2217 | + | checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" | |
| 2218 | + | dependencies = [ | |
| 2219 | + | "bytes", | |
| 2220 | + | "futures-core", | |
| 2221 | + | "http", | |
| 2222 | + | "http-body", | |
| 2223 | + | "pin-project-lite", | |
| 2224 | + | ] | |
| 2225 | + | ||
| 2226 | + | [[package]] | |
| 2227 | + | name = "http-range-header" | |
| 2228 | + | version = "0.4.2" | |
| 2229 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2230 | + | checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" | |
| 2231 | + | ||
| 2232 | + | [[package]] | |
| 2233 | + | name = "httparse" | |
| 2234 | + | version = "1.10.1" | |
| 2235 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2236 | + | checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" | |
| 2237 | + | ||
| 2238 | + | [[package]] | |
| 2239 | + | name = "httpdate" | |
| 2240 | + | version = "1.0.3" | |
| 2241 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2242 | + | checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" | |
| 2243 | + | ||
| 2244 | + | [[package]] | |
| 2245 | + | name = "hybrid-array" | |
| 2246 | + | version = "0.4.12" | |
| 2247 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2248 | + | checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" | |
| 2249 | + | dependencies = [ | |
| 2250 | + | "ctutils", | |
| 2251 | + | "subtle", | |
| 2252 | + | "typenum", | |
| 2253 | + | "zeroize", | |
| 2254 | + | ] | |
| 2255 | + | ||
| 2256 | + | [[package]] | |
| 2257 | + | name = "hyper" | |
| 2258 | + | version = "1.10.1" | |
| 2259 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2260 | + | checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" | |
| 2261 | + | dependencies = [ | |
| 2262 | + | "atomic-waker", | |
| 2263 | + | "bytes", | |
| 2264 | + | "futures-channel", | |
| 2265 | + | "futures-core", | |
| 2266 | + | "http", | |
| 2267 | + | "http-body", | |
| 2268 | + | "httparse", | |
| 2269 | + | "httpdate", | |
| 2270 | + | "itoa", | |
| 2271 | + | "pin-project-lite", | |
| 2272 | + | "smallvec", | |
| 2273 | + | "tokio", | |
| 2274 | + | ] | |
| 2275 | + | ||
| 2276 | + | [[package]] | |
| 2277 | + | name = "hyper-util" | |
| 2278 | + | version = "0.1.20" | |
| 2279 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2280 | + | checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" | |
| 2281 | + | dependencies = [ | |
| 2282 | + | "bytes", | |
| 2283 | + | "http", | |
| 2284 | + | "http-body", | |
| 2285 | + | "hyper", | |
| 2286 | + | "pin-project-lite", | |
| 2287 | + | "tokio", | |
| 2288 | + | "tower-service", | |
| 2289 | + | ] | |
| 2290 | + | ||
| 2291 | + | [[package]] | |
| 2292 | + | name = "iana-time-zone" | |
| 2293 | + | version = "0.1.65" | |
| 2294 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2295 | + | checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" | |
| 2296 | + | dependencies = [ | |
| 2297 | + | "android_system_properties", | |
| 2298 | + | "core-foundation-sys", | |
| 2299 | + | "iana-time-zone-haiku", | |
| 2300 | + | "js-sys", | |
| 2301 | + | "log", | |
| 2302 | + | "wasm-bindgen", | |
| 2303 | + | "windows-core", | |
| 2304 | + | ] | |
| 2305 | + | ||
| 2306 | + | [[package]] | |
| 2307 | + | name = "iana-time-zone-haiku" | |
| 2308 | + | version = "0.1.2" | |
| 2309 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2310 | + | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" | |
| 2311 | + | dependencies = [ | |
| 2312 | + | "cc", | |
| 2313 | + | ] | |
| 2314 | + | ||
| 2315 | + | [[package]] | |
| 2316 | + | name = "icu_collections" | |
| 2317 | + | version = "2.2.0" | |
| 2318 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2319 | + | checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" | |
| 2320 | + | dependencies = [ | |
| 2321 | + | "displaydoc", | |
| 2322 | + | "potential_utf", | |
| 2323 | + | "utf8_iter", | |
| 2324 | + | "yoke", | |
| 2325 | + | "zerofrom", | |
| 2326 | + | "zerovec", | |
| 2327 | + | ] | |
| 2328 | + | ||
| 2329 | + | [[package]] | |
| 2330 | + | name = "icu_locale_core" | |
| 2331 | + | version = "2.2.0" | |
| 2332 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2333 | + | checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" | |
| 2334 | + | dependencies = [ | |
| 2335 | + | "displaydoc", | |
| 2336 | + | "litemap", | |
| 2337 | + | "tinystr", | |
| 2338 | + | "writeable", | |
| 2339 | + | "zerovec", | |
| 2340 | + | ] | |
| 2341 | + | ||
| 2342 | + | [[package]] | |
| 2343 | + | name = "icu_normalizer" | |
| 2344 | + | version = "2.2.0" | |
| 2345 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2346 | + | checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" | |
| 2347 | + | dependencies = [ | |
| 2348 | + | "icu_collections", | |
| 2349 | + | "icu_normalizer_data", | |
| 2350 | + | "icu_properties", | |
| 2351 | + | "icu_provider", | |
| 2352 | + | "smallvec", | |
| 2353 | + | "zerovec", | |
| 2354 | + | ] | |
| 2355 | + | ||
| 2356 | + | [[package]] | |
| 2357 | + | name = "icu_normalizer_data" | |
| 2358 | + | version = "2.2.0" | |
| 2359 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2360 | + | checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" | |
| 2361 | + | ||
| 2362 | + | [[package]] | |
| 2363 | + | name = "icu_properties" | |
| 2364 | + | version = "2.2.0" | |
| 2365 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2366 | + | checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" | |
| 2367 | + | dependencies = [ | |
| 2368 | + | "icu_collections", | |
| 2369 | + | "icu_locale_core", | |
| 2370 | + | "icu_properties_data", | |
| 2371 | + | "icu_provider", | |
| 2372 | + | "zerotrie", | |
| 2373 | + | "zerovec", | |
| 2374 | + | ] | |
| 2375 | + | ||
| 2376 | + | [[package]] | |
| 2377 | + | name = "icu_properties_data" | |
| 2378 | + | version = "2.2.0" | |
| 2379 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2380 | + | checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" | |
| 2381 | + | ||
| 2382 | + | [[package]] | |
| 2383 | + | name = "icu_provider" | |
| 2384 | + | version = "2.2.0" | |
| 2385 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2386 | + | checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" | |
| 2387 | + | dependencies = [ | |
| 2388 | + | "displaydoc", | |
| 2389 | + | "icu_locale_core", | |
| 2390 | + | "writeable", | |
| 2391 | + | "yoke", | |
| 2392 | + | "zerofrom", | |
| 2393 | + | "zerotrie", | |
| 2394 | + | "zerovec", | |
| 2395 | + | ] | |
| 2396 | + | ||
| 2397 | + | [[package]] | |
| 2398 | + | name = "id-arena" | |
| 2399 | + | version = "2.3.0" | |
| 2400 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2401 | + | checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" | |
| 2402 | + | ||
| 2403 | + | [[package]] | |
| 2404 | + | name = "idna" | |
| 2405 | + | version = "1.1.0" | |
| 2406 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2407 | + | checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" | |
| 2408 | + | dependencies = [ | |
| 2409 | + | "idna_adapter", | |
| 2410 | + | "smallvec", | |
| 2411 | + | "utf8_iter", | |
| 2412 | + | ] | |
| 2413 | + | ||
| 2414 | + | [[package]] | |
| 2415 | + | name = "idna_adapter" | |
| 2416 | + | version = "1.2.2" | |
| 2417 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2418 | + | checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" | |
| 2419 | + | dependencies = [ | |
| 2420 | + | "icu_normalizer", | |
| 2421 | + | "icu_properties", | |
| 2422 | + | ] | |
| 2423 | + | ||
| 2424 | + | [[package]] | |
| 2425 | + | name = "index_vec" | |
| 2426 | + | version = "0.1.4" | |
| 2427 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2428 | + | checksum = "44faf5bb8861a9c72e20d3fb0fdbd59233e43056e2b80475ab0aacdc2e781355" | |
| 2429 | + | ||
| 2430 | + | [[package]] | |
| 2431 | + | name = "indexmap" | |
| 2432 | + | version = "2.14.0" | |
| 2433 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2434 | + | checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" | |
| 2435 | + | dependencies = [ | |
| 2436 | + | "equivalent", | |
| 2437 | + | "hashbrown 0.17.1", | |
| 2438 | + | "serde", | |
| 2439 | + | "serde_core", | |
| 2440 | + | ] | |
| 2441 | + | ||
| 2442 | + | [[package]] | |
| 2443 | + | name = "inout" | |
| 2444 | + | version = "0.2.2" | |
| 2445 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2446 | + | checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" | |
| 2447 | + | dependencies = [ | |
| 2448 | + | "block-padding", | |
| 2449 | + | "hybrid-array", | |
| 2450 | + | ] | |
| 2451 | + | ||
| 2452 | + | [[package]] | |
| 2453 | + | name = "internal-russh-num-bigint" | |
| 2454 | + | version = "0.5.0" | |
| 2455 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2456 | + | checksum = "ae8e22120c32fb4d19ec55fba35015f57095cd95a2e3b732e44457f5915b2ee8" | |
| 2457 | + | dependencies = [ | |
| 2458 | + | "num-integer", | |
| 2459 | + | "num-traits", | |
| 2460 | + | "rand", | |
| 2461 | + | "rand_core 0.10.1", | |
| 2462 | + | ] | |
| 2463 | + | ||
| 2464 | + | [[package]] | |
| 2465 | + | name = "inventory" | |
| 2466 | + | version = "0.3.24" | |
| 2467 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2468 | + | checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b" | |
| 2469 | + | dependencies = [ | |
| 2470 | + | "rustversion", | |
| 2471 | + | ] | |
| 2472 | + | ||
| 2473 | + | [[package]] | |
| 2474 | + | name = "is_terminal_polyfill" | |
| 2475 | + | version = "1.70.2" | |
| 2476 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2477 | + | checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" | |
| 2478 | + | ||
| 2479 | + | [[package]] | |
| 2480 | + | name = "itoa" | |
| 2481 | + | version = "1.0.18" | |
| 2482 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2483 | + | checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" | |
| 2484 | + | ||
| 2485 | + | [[package]] | |
| 2486 | + | name = "jiff" | |
| 2487 | + | version = "0.2.28" | |
| 2488 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2489 | + | checksum = "4603d3033e49e2b0e31229fcab20a5d40089c607d975cd9c80551dc69eed9102" | |
| 2490 | + | dependencies = [ | |
| 2491 | + | "jiff-static", | |
| 2492 | + | "jiff-tzdb-platform", | |
| 2493 | + | "log", | |
| 2494 | + | "portable-atomic", | |
| 2495 | + | "portable-atomic-util", | |
| 2496 | + | "serde_core", | |
| 2497 | + | "windows-link", | |
| 2498 | + | ] | |
| 2499 | + | ||
| 2500 | + | [[package]] | |
| 2501 | + | name = "jiff-static" | |
| 2502 | + | version = "0.2.28" | |
| 2503 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2504 | + | checksum = "782d32378dddf207193ac91cefb848ad41abb58195c95168e1291227a0832b47" | |
| 2505 | + | dependencies = [ | |
| 2506 | + | "proc-macro2", | |
| 2507 | + | "quote", | |
| 2508 | + | "syn", | |
| 2509 | + | ] | |
| 2510 | + | ||
| 2511 | + | [[package]] | |
| 2512 | + | name = "jiff-tzdb" | |
| 2513 | + | version = "0.1.6" | |
| 2514 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2515 | + | checksum = "c900ef84826f1338a557697dc8fc601df9ca9af4ac137c7fb61d4c6f2dfd3076" | |
| 2516 | + | ||
| 2517 | + | [[package]] | |
| 2518 | + | name = "jiff-tzdb-platform" | |
| 2519 | + | version = "0.1.3" | |
| 2520 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2521 | + | checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" | |
| 2522 | + | dependencies = [ | |
| 2523 | + | "jiff-tzdb", | |
| 2524 | + | ] | |
| 2525 | + | ||
| 2526 | + | [[package]] | |
| 2527 | + | name = "jobserver" | |
| 2528 | + | version = "0.1.34" | |
| 2529 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2530 | + | checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" | |
| 2531 | + | dependencies = [ | |
| 2532 | + | "getrandom 0.3.4", | |
| 2533 | + | "libc", | |
| 2534 | + | ] | |
| 2535 | + | ||
| 2536 | + | [[package]] | |
| 2537 | + | name = "js-sys" | |
| 2538 | + | version = "0.3.100" | |
| 2539 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2540 | + | checksum = "f2025f20d7a4fa7785846e7b63d10a76d3f1cee98ee5cb79ea59703f95e42162" | |
| 2541 | + | dependencies = [ | |
| 2542 | + | "cfg-if", | |
| 2543 | + | "futures-util", | |
| 2544 | + | "wasm-bindgen", | |
| 2545 | + | ] | |
| 2546 | + | ||
| 2547 | + | [[package]] | |
| 2548 | + | name = "keccak" | |
| 2549 | + | version = "0.2.0" | |
| 2550 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2551 | + | checksum = "9e24a010dd405bd7ed803e5253182815b41bf2e6a80cc3bfc066658e03a198aa" | |
| 2552 | + | dependencies = [ | |
| 2553 | + | "cfg-if", | |
| 2554 | + | "cpufeatures 0.3.0", | |
| 2555 | + | ] | |
| 2556 | + | ||
| 2557 | + | [[package]] | |
| 2558 | + | name = "kem" | |
| 2559 | + | version = "0.3.0" | |
| 2560 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2561 | + | checksum = "01737161ba802849cfd486b5bd209d38ba4943494c249a8126005170c7621edd" | |
| 2562 | + | dependencies = [ | |
| 2563 | + | "crypto-common 0.2.2", | |
| 2564 | + | "rand_core 0.10.1", | |
| 2565 | + | ] | |
| 2566 | + | ||
| 2567 | + | [[package]] | |
| 2568 | + | name = "kstring" | |
| 2569 | + | version = "2.0.2" | |
| 2570 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2571 | + | checksum = "558bf9508a558512042d3095138b1f7b8fe90c5467d94f9f1da28b3731c5dbd1" | |
| 2572 | + | dependencies = [ | |
| 2573 | + | "static_assertions", | |
| 2574 | + | ] | |
| 2575 | + | ||
| 2576 | + | [[package]] | |
| 2577 | + | name = "lazy_static" | |
| 2578 | + | version = "1.5.0" | |
| 2579 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2580 | + | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" | |
| 2581 | + | ||
| 2582 | + | [[package]] | |
| 2583 | + | name = "leb128fmt" | |
| 2584 | + | version = "0.1.0" | |
| 2585 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2586 | + | checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" | |
| 2587 | + | ||
| 2588 | + | [[package]] | |
| 2589 | + | name = "libc" | |
| 2590 | + | version = "0.2.186" | |
| 2591 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2592 | + | checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" | |
| 2593 | + | ||
| 2594 | + | [[package]] | |
| 2595 | + | name = "libm" | |
| 2596 | + | version = "0.2.16" | |
| 2597 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2598 | + | checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" | |
| 2599 | + | ||
| 2600 | + | [[package]] | |
| 2601 | + | name = "libsqlite3-sys" | |
| 2602 | + | version = "0.37.0" | |
| 2603 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2604 | + | checksum = "b1f111c8c41e7c61a49cd34e44c7619462967221a6443b0ec299e0ac30cfb9b1" | |
| 2605 | + | dependencies = [ | |
| 2606 | + | "cc", | |
| 2607 | + | "pkg-config", | |
| 2608 | + | "vcpkg", | |
| 2609 | + | ] | |
| 2610 | + | ||
| 2611 | + | [[package]] | |
| 2612 | + | name = "linked-hash-map" | |
| 2613 | + | version = "0.5.6" | |
| 2614 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2615 | + | checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" | |
| 2616 | + | ||
| 2617 | + | [[package]] | |
| 2618 | + | name = "linux-raw-sys" | |
| 2619 | + | version = "0.12.1" | |
| 2620 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2621 | + | checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" | |
| 2622 | + | ||
| 2623 | + | [[package]] | |
| 2624 | + | name = "litemap" | |
| 2625 | + | version = "0.8.2" | |
| 2626 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2627 | + | checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" | |
| 2628 | + | ||
| 2629 | + | [[package]] | |
| 2630 | + | name = "lock_api" | |
| 2631 | + | version = "0.4.14" | |
| 2632 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2633 | + | checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" | |
| 2634 | + | dependencies = [ | |
| 2635 | + | "scopeguard", | |
| 2636 | + | ] | |
| 2637 | + | ||
| 2638 | + | [[package]] | |
| 2639 | + | name = "log" | |
| 2640 | + | version = "0.4.32" | |
| 2641 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2642 | + | checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" | |
| 2643 | + | ||
| 2644 | + | [[package]] | |
| 2645 | + | name = "matchers" | |
| 2646 | + | version = "0.2.0" | |
| 2647 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2648 | + | checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" | |
| 2649 | + | dependencies = [ | |
| 2650 | + | "regex-automata", | |
| 2651 | + | ] | |
| 2652 | + | ||
| 2653 | + | [[package]] | |
| 2654 | + | name = "matchit" | |
| 2655 | + | version = "0.8.4" | |
| 2656 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2657 | + | checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" | |
| 2658 | + | ||
| 2659 | + | [[package]] | |
| 2660 | + | name = "maud" | |
| 2661 | + | version = "0.27.0" | |
| 2662 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2663 | + | checksum = "8156733e27020ea5c684db5beac5d1d611e1272ab17901a49466294b84fc217e" | |
| 2664 | + | dependencies = [ | |
| 2665 | + | "axum-core", | |
| 2666 | + | "http", | |
| 2667 | + | "itoa", | |
| 2668 | + | "maud_macros", | |
| 2669 | + | ] | |
| 2670 | + | ||
| 2671 | + | [[package]] | |
| 2672 | + | name = "maud_macros" | |
| 2673 | + | version = "0.27.0" | |
| 2674 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2675 | + | checksum = "7261b00f3952f617899bc012e3dbd56e4f0110a038175929fa5d18e5a19913ca" | |
| 2676 | + | dependencies = [ | |
| 2677 | + | "proc-macro2", | |
| 2678 | + | "proc-macro2-diagnostics", | |
| 2679 | + | "quote", | |
| 2680 | + | "syn", | |
| 2681 | + | ] | |
| 2682 | + | ||
| 2683 | + | [[package]] | |
| 2684 | + | name = "maybe-async" | |
| 2685 | + | version = "0.2.11" | |
| 2686 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2687 | + | checksum = "746873a384ad60adc5db74471dfaba74bd278afbdcfd81db93fafcdfc8b5ca0c" | |
| 2688 | + | dependencies = [ | |
| 2689 | + | "proc-macro2", | |
| 2690 | + | "quote", | |
| 2691 | + | "syn", | |
| 2692 | + | ] | |
| 2693 | + | ||
| 2694 | + | [[package]] | |
| 2695 | + | name = "md5" | |
| 2696 | + | version = "0.8.0" | |
| 2697 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2698 | + | checksum = "ae960838283323069879657ca3de837e9f7bbb4c7bf6ea7f1b290d5e9476d2e0" | |
| 2699 | + | ||
| 2700 | + | [[package]] | |
| 2701 | + | name = "memchr" | |
| 2702 | + | version = "2.8.1" | |
| 2703 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2704 | + | checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" | |
| 2705 | + | ||
| 2706 | + | [[package]] | |
| 2707 | + | name = "memmap2" | |
| 2708 | + | version = "0.9.10" | |
| 2709 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2710 | + | checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" | |
| 2711 | + | dependencies = [ | |
| 2712 | + | "libc", | |
| 2713 | + | ] | |
| 2714 | + | ||
| 2715 | + | [[package]] | |
| 2716 | + | name = "mime" | |
| 2717 | + | version = "0.3.17" | |
| 2718 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2719 | + | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" | |
| 2720 | + | ||
| 2721 | + | [[package]] | |
| 2722 | + | name = "mime_guess" | |
| 2723 | + | version = "2.0.5" | |
| 2724 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2725 | + | checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" | |
| 2726 | + | dependencies = [ | |
| 2727 | + | "mime", | |
| 2728 | + | "unicase", | |
| 2729 | + | ] | |
| 2730 | + | ||
| 2731 | + | [[package]] | |
| 2732 | + | name = "miniz_oxide" | |
| 2733 | + | version = "0.8.9" | |
| 2734 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2735 | + | checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" | |
| 2736 | + | dependencies = [ | |
| 2737 | + | "adler2", | |
| 2738 | + | "simd-adler32", | |
| 2739 | + | ] | |
| 2740 | + | ||
| 2741 | + | [[package]] | |
| 2742 | + | name = "mio" | |
| 2743 | + | version = "1.2.1" | |
| 2744 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2745 | + | checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" | |
| 2746 | + | dependencies = [ | |
| 2747 | + | "libc", | |
| 2748 | + | "wasi", | |
| 2749 | + | "windows-sys", | |
| 2750 | + | ] | |
| 2751 | + | ||
| 2752 | + | [[package]] | |
| 2753 | + | name = "ml-kem" | |
| 2754 | + | version = "0.3.2" | |
| 2755 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2756 | + | checksum = "5e15f3e5b957493873e396a66914e83e616b6afe335cdef7efe5c6e1216aba66" | |
| 2757 | + | dependencies = [ | |
| 2758 | + | "hybrid-array", | |
| 2759 | + | "kem", | |
| 2760 | + | "module-lattice", | |
| 2761 | + | "pkcs8", | |
| 2762 | + | "rand_core 0.10.1", | |
| 2763 | + | "sha3", | |
| 2764 | + | ] | |
| 2765 | + | ||
| 2766 | + | [[package]] | |
| 2767 | + | name = "module-lattice" | |
| 2768 | + | version = "0.2.3" | |
| 2769 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2770 | + | checksum = "0c61b87c9683ab7cb1c6871d261ad5479b6b10ceb52c4352aaca3b5d35a8febe" | |
| 2771 | + | dependencies = [ | |
| 2772 | + | "ctutils", | |
| 2773 | + | "hybrid-array", | |
| 2774 | + | "num-traits", | |
| 2775 | + | ] | |
| 2776 | + | ||
| 2777 | + | [[package]] | |
| 2778 | + | name = "nix" | |
| 2779 | + | version = "0.31.3" | |
| 2780 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2781 | + | checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" | |
| 2782 | + | dependencies = [ | |
| 2783 | + | "bitflags", | |
| 2784 | + | "cfg-if", | |
| 2785 | + | "cfg_aliases", | |
| 2786 | + | "libc", | |
| 2787 | + | ] | |
| 2788 | + | ||
| 2789 | + | [[package]] | |
| 2790 | + | name = "nonempty" | |
| 2791 | + | version = "0.12.0" | |
| 2792 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2793 | + | checksum = "9737e026353e5cd0736f98eddae28665118eb6f6600902a7f50db585621fecb6" | |
| 2794 | + | ||
| 2795 | + | [[package]] | |
| 2796 | + | name = "nu-ansi-term" | |
| 2797 | + | version = "0.50.3" | |
| 2798 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2799 | + | checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" | |
| 2800 | + | dependencies = [ | |
| 2801 | + | "windows-sys", | |
| 2802 | + | ] | |
| 2803 | + | ||
| 2804 | + | [[package]] | |
| 2805 | + | name = "num-bigint" | |
| 2806 | + | version = "0.4.6" | |
| 2807 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2808 | + | checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" | |
| 2809 | + | dependencies = [ | |
| 2810 | + | "num-integer", | |
| 2811 | + | "num-traits", | |
| 2812 | + | ] | |
| 2813 | + | ||
| 2814 | + | [[package]] | |
| 2815 | + | name = "num-conv" | |
| 2816 | + | version = "0.2.2" | |
| 2817 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2818 | + | checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" | |
| 2819 | + | ||
| 2820 | + | [[package]] | |
| 2821 | + | name = "num-integer" | |
| 2822 | + | version = "0.1.46" | |
| 2823 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2824 | + | checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" | |
| 2825 | + | dependencies = [ | |
| 2826 | + | "num-traits", | |
| 2827 | + | ] | |
| 2828 | + | ||
| 2829 | + | [[package]] | |
| 2830 | + | name = "num-traits" | |
| 2831 | + | version = "0.2.19" | |
| 2832 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2833 | + | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" | |
| 2834 | + | dependencies = [ | |
| 2835 | + | "autocfg", | |
| 2836 | + | ] | |
| 2837 | + | ||
| 2838 | + | [[package]] | |
| 2839 | + | name = "num_cpus" | |
| 2840 | + | version = "1.17.0" | |
| 2841 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2842 | + | checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" | |
| 2843 | + | dependencies = [ | |
| 2844 | + | "hermit-abi", | |
| 2845 | + | "libc", | |
| 2846 | + | ] | |
| 2847 | + | ||
| 2848 | + | [[package]] | |
| 2849 | + | name = "once_cell" | |
| 2850 | + | version = "1.21.4" | |
| 2851 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2852 | + | checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" | |
| 2853 | + | ||
| 2854 | + | [[package]] | |
| 2855 | + | name = "once_cell_polyfill" | |
| 2856 | + | version = "1.70.2" | |
| 2857 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2858 | + | checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" | |
| 2859 | + | ||
| 2860 | + | [[package]] | |
| 2861 | + | name = "p256" | |
| 2862 | + | version = "0.14.0-rc.10" | |
| 2863 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2864 | + | checksum = "41adc63effe99d48837a8cc0e6d7a77e32ae6a07f6000df466178dbc2193093e" | |
| 2865 | + | dependencies = [ | |
| 2866 | + | "ecdsa", | |
| 2867 | + | "elliptic-curve", | |
| 2868 | + | "primefield", | |
| 2869 | + | "primeorder", | |
| 2870 | + | "sha2", | |
| 2871 | + | ] | |
| 2872 | + | ||
| 2873 | + | [[package]] | |
| 2874 | + | name = "p384" | |
| 2875 | + | version = "0.14.0-rc.10" | |
| 2876 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2877 | + | checksum = "9bd5333afa5ae0347f39e6a0f2c9c155da431583fd71fe5555bd0521b4ccaf02" | |
| 2878 | + | dependencies = [ | |
| 2879 | + | "ecdsa", | |
| 2880 | + | "elliptic-curve", | |
| 2881 | + | "fiat-crypto", | |
| 2882 | + | "primefield", | |
| 2883 | + | "primeorder", | |
| 2884 | + | "sha2", | |
| 2885 | + | ] | |
| 2886 | + | ||
| 2887 | + | [[package]] | |
| 2888 | + | name = "p521" | |
| 2889 | + | version = "0.14.0-rc.10" | |
| 2890 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2891 | + | checksum = "a3a5297f53dc16d35909060ba3032cff7867e8809f01e273ff325579d5f0ceae" | |
| 2892 | + | dependencies = [ | |
| 2893 | + | "base16ct", | |
| 2894 | + | "ecdsa", | |
| 2895 | + | "elliptic-curve", | |
| 2896 | + | "primefield", | |
| 2897 | + | "primeorder", | |
| 2898 | + | "sha2", | |
| 2899 | + | ] | |
| 2900 | + | ||
| 2901 | + | [[package]] | |
| 2902 | + | name = "pageant" | |
| 2903 | + | version = "0.2.1" | |
| 2904 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2905 | + | checksum = "4f3a5ae18f65a85c67a77d18d42d3606c07948e3c17c1e5f74852b26589e88a5" | |
| 2906 | + | dependencies = [ | |
| 2907 | + | "base16ct", | |
| 2908 | + | "byteorder", | |
| 2909 | + | "bytes", | |
| 2910 | + | "delegate", | |
| 2911 | + | "futures", | |
| 2912 | + | "log", | |
| 2913 | + | "rand", | |
| 2914 | + | "sha2", | |
| 2915 | + | "thiserror", | |
| 2916 | + | "tokio", | |
| 2917 | + | "windows", | |
| 2918 | + | "windows-strings", | |
| 2919 | + | ] | |
| 2920 | + | ||
| 2921 | + | [[package]] | |
| 2922 | + | name = "parking_lot" | |
| 2923 | + | version = "0.12.5" | |
| 2924 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2925 | + | checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" | |
| 2926 | + | dependencies = [ | |
| 2927 | + | "lock_api", | |
| 2928 | + | "parking_lot_core", | |
| 2929 | + | ] | |
| 2930 | + | ||
| 2931 | + | [[package]] | |
| 2932 | + | name = "parking_lot_core" | |
| 2933 | + | version = "0.9.12" | |
| 2934 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2935 | + | checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" | |
| 2936 | + | dependencies = [ | |
| 2937 | + | "cfg-if", | |
| 2938 | + | "libc", | |
| 2939 | + | "redox_syscall", | |
| 2940 | + | "smallvec", | |
| 2941 | + | "windows-link", | |
| 2942 | + | ] | |
| 2943 | + | ||
| 2944 | + | [[package]] | |
| 2945 | + | name = "password-hash" | |
| 2946 | + | version = "0.5.0" | |
| 2947 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2948 | + | checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" | |
| 2949 | + | dependencies = [ | |
| 2950 | + | "base64ct", | |
| 2951 | + | "rand_core 0.6.4", | |
| 2952 | + | "subtle", | |
| 2953 | + | ] | |
| 2954 | + | ||
| 2955 | + | [[package]] | |
| 2956 | + | name = "password-hash" | |
| 2957 | + | version = "0.6.1" | |
| 2958 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2959 | + | checksum = "aab41826031698d6ffcd9cff78ef56ef998e39dc7e5067cdfebe373842d4723b" | |
| 2960 | + | dependencies = [ | |
| 2961 | + | "phc", | |
| 2962 | + | ] | |
| 2963 | + | ||
| 2964 | + | [[package]] | |
| 2965 | + | name = "pbkdf2" | |
| 2966 | + | version = "0.13.0" | |
| 2967 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2968 | + | checksum = "112d82ceb8c5bf524d9af484d4e4970c9fd5a0cc15ba14ad93dccd28873b0629" | |
| 2969 | + | dependencies = [ | |
| 2970 | + | "digest 0.11.3", | |
| 2971 | + | "hmac", | |
| 2972 | + | ] | |
| 2973 | + | ||
| 2974 | + | [[package]] | |
| 2975 | + | name = "pem-rfc7468" | |
| 2976 | + | version = "1.0.0" | |
| 2977 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2978 | + | checksum = "a6305423e0e7738146434843d1694d621cce767262b2a86910beab705e4493d9" | |
| 2979 | + | dependencies = [ | |
| 2980 | + | "base64ct", | |
| 2981 | + | ] | |
| 2982 | + | ||
| 2983 | + | [[package]] | |
| 2984 | + | name = "percent-encoding" | |
| 2985 | + | version = "2.3.2" | |
| 2986 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2987 | + | checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" | |
| 2988 | + | ||
| 2989 | + | [[package]] | |
| 2990 | + | name = "phc" | |
| 2991 | + | version = "0.6.1" | |
| 2992 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2993 | + | checksum = "44dc769b75f93afdddd8c7fa12d685292ddeff1e66f7f0f3a234cf1818afe892" | |
| 2994 | + | dependencies = [ | |
| 2995 | + | "base64ct", | |
| 2996 | + | "ctutils", | |
| 2997 | + | ] | |
| 2998 | + | ||
| 2999 | + | [[package]] | |
| 3000 | + | name = "pin-project-lite" | |
| 3001 | + | version = "0.2.17" | |
| 3002 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3003 | + | checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" | |
| 3004 | + | ||
| 3005 | + | [[package]] | |
| 3006 | + | name = "pkcs1" | |
| 3007 | + | version = "0.8.0-rc.4" | |
| 3008 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3009 | + | checksum = "986d2e952779af96ea048f160fd9194e1751b4faea78bcf3ceb456efe008088e" | |
| 3010 | + | dependencies = [ | |
| 3011 | + | "der", | |
| 3012 | + | "spki", | |
| 3013 | + | ] | |
| 3014 | + | ||
| 3015 | + | [[package]] | |
| 3016 | + | name = "pkcs5" | |
| 3017 | + | version = "0.8.0" | |
| 3018 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3019 | + | checksum = "279a91971a1d8eb1260a30938eae3be9cb67b472dffecb222fbbbe2fd2dc1453" | |
| 3020 | + | dependencies = [ | |
| 3021 | + | "aes", | |
| 3022 | + | "cbc", | |
| 3023 | + | "der", | |
| 3024 | + | "pbkdf2", | |
| 3025 | + | "rand_core 0.10.1", | |
| 3026 | + | "scrypt", | |
| 3027 | + | "sha2", | |
| 3028 | + | "spki", | |
| 3029 | + | ] | |
| 3030 | + | ||
| 3031 | + | [[package]] | |
| 3032 | + | name = "pkcs8" | |
| 3033 | + | version = "0.11.0" | |
| 3034 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3035 | + | checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" | |
| 3036 | + | dependencies = [ | |
| 3037 | + | "der", | |
| 3038 | + | "pkcs5", | |
| 3039 | + | "rand_core 0.10.1", | |
| 3040 | + | "spki", | |
| 3041 | + | ] | |
| 3042 | + | ||
| 3043 | + | [[package]] | |
| 3044 | + | name = "pkg-config" | |
| 3045 | + | version = "0.3.33" | |
| 3046 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3047 | + | checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" | |
| 3048 | + | ||
| 3049 | + | [[package]] | |
| 3050 | + | name = "plist" | |
| 3051 | + | version = "1.9.0" | |
| 3052 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3053 | + | checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1" | |
| 3054 | + | dependencies = [ | |
| 3055 | + | "base64", | |
| 3056 | + | "indexmap", | |
| 3057 | + | "quick-xml", | |
| 3058 | + | "serde", | |
| 3059 | + | "time", | |
| 3060 | + | ] | |
| 3061 | + | ||
| 3062 | + | [[package]] | |
| 3063 | + | name = "pluralizer" | |
| 3064 | + | version = "0.5.0" | |
| 3065 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3066 | + | checksum = "4b3eba432a00a1f6c16f39147847a870e94e2e9b992759b503e330efec778cbe" | |
| 3067 | + | dependencies = [ | |
| 3068 | + | "once_cell", | |
| 3069 | + | "regex", | |
| 3070 | + | ] | |
| 3071 | + | ||
| 3072 | + | [[package]] | |
| 3073 | + | name = "poly1305" | |
| 3074 | + | version = "0.9.0" | |
| 3075 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3076 | + | checksum = "a00baa632505d05512f48a963e16051c54fda9a95cc9acea1a4e3c90991c4a2e" | |
| 3077 | + | dependencies = [ | |
| 3078 | + | "cpufeatures 0.3.0", | |
| 3079 | + | "universal-hash", | |
| 3080 | + | "zeroize", | |
| 3081 | + | ] | |
| 3082 | + | ||
| 3083 | + | [[package]] | |
| 3084 | + | name = "polyval" | |
| 3085 | + | version = "0.7.1" | |
| 3086 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3087 | + | checksum = "7dfc63250416fea14f5749b90725916a6c903f599d51cb635aa7a52bfd03eede" | |
| 3088 | + | dependencies = [ | |
| 3089 | + | "cpubits", | |
| 3090 | + | "cpufeatures 0.3.0", | |
| 3091 | + | "universal-hash", | |
| 3092 | + | ] | |
| 3093 | + | ||
| 3094 | + | [[package]] | |
| 3095 | + | name = "portable-atomic" | |
| 3096 | + | version = "1.13.1" | |
| 3097 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3098 | + | checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" | |
| 3099 | + | ||
| 3100 | + | [[package]] | |
| 3101 | + | name = "portable-atomic-util" | |
| 3102 | + | version = "0.2.7" | |
| 3103 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3104 | + | checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" | |
| 3105 | + | dependencies = [ | |
| 3106 | + | "portable-atomic", | |
| 3107 | + | ] | |
| 3108 | + | ||
| 3109 | + | [[package]] | |
| 3110 | + | name = "potential_utf" | |
| 3111 | + | version = "0.1.5" | |
| 3112 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3113 | + | checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" | |
| 3114 | + | dependencies = [ | |
| 3115 | + | "zerovec", | |
| 3116 | + | ] | |
| 3117 | + | ||
| 3118 | + | [[package]] | |
| 3119 | + | name = "powerfmt" | |
| 3120 | + | version = "0.2.0" | |
| 3121 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3122 | + | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" | |
| 3123 | + | ||
| 3124 | + | [[package]] | |
| 3125 | + | name = "prettyplease" | |
| 3126 | + | version = "0.2.37" | |
| 3127 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3128 | + | checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" | |
| 3129 | + | dependencies = [ | |
| 3130 | + | "proc-macro2", | |
| 3131 | + | "syn", | |
| 3132 | + | ] | |
| 3133 | + | ||
| 3134 | + | [[package]] | |
| 3135 | + | name = "primefield" | |
| 3136 | + | version = "0.14.0-rc.10" | |
| 3137 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3138 | + | checksum = "f845ec3240cd5ed5e1e31cf3ff633a5bf47c698dc4092ba9e767415b3d393406" | |
| 3139 | + | dependencies = [ | |
| 3140 | + | "crypto-bigint", | |
| 3141 | + | "crypto-common 0.2.2", | |
| 3142 | + | "ff", | |
| 3143 | + | "rand_core 0.10.1", | |
| 3144 | + | "subtle", | |
| 3145 | + | "zeroize", | |
| 3146 | + | ] | |
| 3147 | + | ||
| 3148 | + | [[package]] | |
| 3149 | + | name = "primeorder" | |
| 3150 | + | version = "0.14.0-rc.10" | |
| 3151 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3152 | + | checksum = "7d2793f22b9b6fd11ef3ac1d59bf003c2573593e4968702341605c2748fd90bf" | |
| 3153 | + | dependencies = [ | |
| 3154 | + | "elliptic-curve", | |
| 3155 | + | ] | |
| 3156 | + | ||
| 3157 | + | [[package]] | |
| 3158 | + | name = "proc-macro2" | |
| 3159 | + | version = "1.0.106" | |
| 3160 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3161 | + | checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" | |
| 3162 | + | dependencies = [ | |
| 3163 | + | "unicode-ident", | |
| 3164 | + | ] | |
| 3165 | + | ||
| 3166 | + | [[package]] | |
| 3167 | + | name = "proc-macro2-diagnostics" | |
| 3168 | + | version = "0.10.1" | |
| 3169 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3170 | + | checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" | |
| 3171 | + | dependencies = [ | |
| 3172 | + | "proc-macro2", | |
| 3173 | + | "quote", | |
| 3174 | + | "syn", | |
| 3175 | + | "version_check", | |
| 3176 | + | ] | |
| 3177 | + | ||
| 3178 | + | [[package]] | |
| 3179 | + | name = "prodash" | |
| 3180 | + | version = "31.0.0" | |
| 3181 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3182 | + | checksum = "962200e2d7d551451297d9fdce85138374019ada198e30ea9ede38034e27604c" | |
| 3183 | + | dependencies = [ | |
| 3184 | + | "parking_lot", | |
| 3185 | + | ] | |
| 3186 | + | ||
| 3187 | + | [[package]] | |
| 3188 | + | name = "quick-xml" | |
| 3189 | + | version = "0.39.4" | |
| 3190 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3191 | + | checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" | |
| 3192 | + | dependencies = [ | |
| 3193 | + | "memchr", | |
| 3194 | + | ] | |
| 3195 | + | ||
| 3196 | + | [[package]] | |
| 3197 | + | name = "quote" | |
| 3198 | + | version = "1.0.45" | |
| 3199 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3200 | + | checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" | |
| 3201 | + | dependencies = [ | |
| 3202 | + | "proc-macro2", | |
| 3203 | + | ] | |
| 3204 | + | ||
| 3205 | + | [[package]] | |
| 3206 | + | name = "r-efi" | |
| 3207 | + | version = "5.3.0" | |
| 3208 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3209 | + | checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" | |
| 3210 | + | ||
| 3211 | + | [[package]] | |
| 3212 | + | name = "r-efi" | |
| 3213 | + | version = "6.0.0" | |
| 3214 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3215 | + | checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" | |
| 3216 | + | ||
| 3217 | + | [[package]] | |
| 3218 | + | name = "rand" | |
| 3219 | + | version = "0.10.1" | |
| 3220 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3221 | + | checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" | |
| 3222 | + | dependencies = [ | |
| 3223 | + | "chacha20", | |
| 3224 | + | "getrandom 0.4.2", | |
| 3225 | + | "rand_core 0.10.1", | |
| 3226 | + | ] | |
| 3227 | + | ||
| 3228 | + | [[package]] | |
| 3229 | + | name = "rand_core" | |
| 3230 | + | version = "0.6.4" | |
| 3231 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3232 | + | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" | |
| 3233 | + | dependencies = [ | |
| 3234 | + | "getrandom 0.2.17", | |
| 3235 | + | ] | |
| 3236 | + | ||
| 3237 | + | [[package]] | |
| 3238 | + | name = "rand_core" | |
| 3239 | + | version = "0.10.1" | |
| 3240 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3241 | + | checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" | |
| 3242 | + | ||
| 3243 | + | [[package]] | |
| 3244 | + | name = "redox_syscall" | |
| 3245 | + | version = "0.5.18" | |
| 3246 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3247 | + | checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" | |
| 3248 | + | dependencies = [ | |
| 3249 | + | "bitflags", | |
| 3250 | + | ] | |
| 3251 | + | ||
| 3252 | + | [[package]] | |
| 3253 | + | name = "regex" | |
| 3254 | + | version = "1.12.4" | |
| 3255 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3256 | + | checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" | |
| 3257 | + | dependencies = [ | |
| 3258 | + | "aho-corasick", | |
| 3259 | + | "memchr", | |
| 3260 | + | "regex-automata", | |
| 3261 | + | "regex-syntax", | |
| 3262 | + | ] | |
| 3263 | + | ||
| 3264 | + | [[package]] | |
| 3265 | + | name = "regex-automata" | |
| 3266 | + | version = "0.4.14" | |
| 3267 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3268 | + | checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" | |
| 3269 | + | dependencies = [ | |
| 3270 | + | "aho-corasick", | |
| 3271 | + | "memchr", | |
| 3272 | + | "regex-syntax", | |
| 3273 | + | ] | |
| 3274 | + | ||
| 3275 | + | [[package]] | |
| 3276 | + | name = "regex-syntax" | |
| 3277 | + | version = "0.8.11" | |
| 3278 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3279 | + | checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" | |
| 3280 | + | ||
| 3281 | + | [[package]] | |
| 3282 | + | name = "rfc6979" | |
| 3283 | + | version = "0.5.0" | |
| 3284 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3285 | + | checksum = "5236ce872cac07e0fb3969b0cbf468c7d2f37d432f1b627dcb7b8d34563fb0c3" | |
| 3286 | + | dependencies = [ | |
| 3287 | + | "hmac", | |
| 3288 | + | "subtle", | |
| 3289 | + | ] | |
| 3290 | + | ||
| 3291 | + | [[package]] | |
| 3292 | + | name = "rsa" | |
| 3293 | + | version = "0.10.0-rc.18" | |
| 3294 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3295 | + | checksum = "30b2aa4ba0d89f73d1e332df05be0eeab8840351c36ca5654341dfdb57bb3caf" | |
| 3296 | + | dependencies = [ | |
| 3297 | + | "const-oid", | |
| 3298 | + | "crypto-bigint", | |
| 3299 | + | "crypto-primes", | |
| 3300 | + | "digest 0.11.3", | |
| 3301 | + | "pkcs1", | |
| 3302 | + | "pkcs8", | |
| 3303 | + | "rand_core 0.10.1", | |
| 3304 | + | "sha2", | |
| 3305 | + | "signature", | |
| 3306 | + | "spki", | |
| 3307 | + | "zeroize", | |
| 3308 | + | ] | |
| 3309 | + | ||
| 3310 | + | [[package]] | |
| 3311 | + | name = "rsqlite-vfs" | |
| 3312 | + | version = "0.1.1" | |
| 3313 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3314 | + | checksum = "c51c9ae4df8a7fba42103df5c621fa3c37eccf3a3c650879e90fc48b11cc192c" | |
| 3315 | + | dependencies = [ | |
| 3316 | + | "hashbrown 0.16.1", | |
| 3317 | + | "thiserror", | |
| 3318 | + | ] | |
| 3319 | + | ||
| 3320 | + | [[package]] | |
| 3321 | + | name = "rusqlite" | |
| 3322 | + | version = "0.39.0" | |
| 3323 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3324 | + | checksum = "a0d2b0146dd9661bf67bb107c0bb2a55064d556eeb3fc314151b957f313bcd4e" | |
| 3325 | + | dependencies = [ | |
| 3326 | + | "bitflags", | |
| 3327 | + | "fallible-iterator", | |
| 3328 | + | "fallible-streaming-iterator", | |
| 3329 | + | "hashlink", | |
| 3330 | + | "libsqlite3-sys", | |
| 3331 | + | "smallvec", | |
| 3332 | + | "sqlite-wasm-rs", | |
| 3333 | + | ] | |
| 3334 | + | ||
| 3335 | + | [[package]] | |
| 3336 | + | name = "russh" | |
| 3337 | + | version = "0.61.2" | |
| 3338 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3339 | + | checksum = "bbf893f64684e58da8a68d56a5e84d1cf0440226274c515770fe267707a7d0b0" | |
| 3340 | + | dependencies = [ | |
| 3341 | + | "aes", | |
| 3342 | + | "aws-lc-rs", | |
| 3343 | + | "bitflags", | |
| 3344 | + | "block-padding", | |
| 3345 | + | "byteorder", | |
| 3346 | + | "bytes", | |
| 3347 | + | "cbc", | |
| 3348 | + | "cipher", | |
| 3349 | + | "crypto-bigint", | |
| 3350 | + | "ctr", | |
| 3351 | + | "curve25519-dalek", | |
| 3352 | + | "data-encoding", | |
| 3353 | + | "delegate", | |
| 3354 | + | "der", | |
| 3355 | + | "digest 0.11.3", | |
| 3356 | + | "ecdsa", | |
| 3357 | + | "ed25519-dalek", | |
| 3358 | + | "elliptic-curve", | |
| 3359 | + | "enum_dispatch", | |
| 3360 | + | "flate2", | |
| 3361 | + | "futures", | |
| 3362 | + | "generic-array 1.4.3", | |
| 3363 | + | "getrandom 0.4.2", | |
| 3364 | + | "ghash", | |
| 3365 | + | "hex-literal", | |
| 3366 | + | "hmac", | |
| 3367 | + | "inout", | |
| 3368 | + | "internal-russh-num-bigint", | |
| 3369 | + | "keccak", | |
| 3370 | + | "log", | |
| 3371 | + | "md5", | |
| 3372 | + | "ml-kem", | |
| 3373 | + | "module-lattice", | |
| 3374 | + | "num-bigint", | |
| 3375 | + | "p256", | |
| 3376 | + | "p384", | |
| 3377 | + | "p521", | |
| 3378 | + | "pageant", | |
| 3379 | + | "pbkdf2", | |
| 3380 | + | "pkcs1", | |
| 3381 | + | "pkcs5", | |
| 3382 | + | "pkcs8", | |
| 3383 | + | "polyval", | |
| 3384 | + | "rand", | |
| 3385 | + | "rand_core 0.10.1", | |
| 3386 | + | "rsa", | |
| 3387 | + | "russh-cryptovec", | |
| 3388 | + | "russh-util", | |
| 3389 | + | "salsa20", | |
| 3390 | + | "scrypt", | |
| 3391 | + | "sec1", | |
| 3392 | + | "sha1 0.11.0", | |
| 3393 | + | "sha2", | |
| 3394 | + | "sha3", | |
| 3395 | + | "signature", | |
| 3396 | + | "spki", | |
| 3397 | + | "ssh-encoding", | |
| 3398 | + | "ssh-key", | |
| 3399 | + | "subtle", | |
| 3400 | + | "thiserror", | |
| 3401 | + | "tokio", | |
| 3402 | + | "typenum", | |
| 3403 | + | "universal-hash", | |
| 3404 | + | "zeroize", | |
| 3405 | + | ] | |
| 3406 | + | ||
| 3407 | + | [[package]] | |
| 3408 | + | name = "russh-cryptovec" | |
| 3409 | + | version = "0.61.0" | |
| 3410 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3411 | + | checksum = "443f6bbcfacb34a1aab2b12b99bf08e0c63abdc5a0db261901365df9d57fff51" | |
| 3412 | + | dependencies = [ | |
| 3413 | + | "log", | |
| 3414 | + | "nix", | |
| 3415 | + | "ssh-encoding", | |
| 3416 | + | "windows-sys", | |
| 3417 | + | ] | |
| 3418 | + | ||
| 3419 | + | [[package]] | |
| 3420 | + | name = "russh-util" | |
| 3421 | + | version = "0.52.0" | |
| 3422 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3423 | + | checksum = "668424a5dde0bcb45b55ba7de8476b93831b4aa2fa6947e145f3b053e22c60b6" | |
| 3424 | + | dependencies = [ | |
| 3425 | + | "chrono", | |
| 3426 | + | "tokio", | |
| 3427 | + | "wasm-bindgen", | |
| 3428 | + | "wasm-bindgen-futures", | |
| 3429 | + | ] | |
| 3430 | + | ||
| 3431 | + | [[package]] | |
| 3432 | + | name = "rustc_version" | |
| 3433 | + | version = "0.4.1" | |
| 3434 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3435 | + | checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" | |
| 3436 | + | dependencies = [ | |
| 3437 | + | "semver", | |
| 3438 | + | ] | |
| 3439 | + | ||
| 3440 | + | [[package]] | |
| 3441 | + | name = "rustix" | |
| 3442 | + | version = "1.1.4" | |
| 3443 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3444 | + | checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" | |
| 3445 | + | dependencies = [ | |
| 3446 | + | "bitflags", | |
| 3447 | + | "errno", | |
| 3448 | + | "libc", | |
| 3449 | + | "linux-raw-sys", | |
| 3450 | + | "windows-sys", | |
| 3451 | + | ] | |
| 3452 | + | ||
| 3453 | + | [[package]] | |
| 3454 | + | name = "rustversion" | |
| 3455 | + | version = "1.0.22" | |
| 3456 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3457 | + | checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" | |
| 3458 | + | ||
| 3459 | + | [[package]] | |
| 3460 | + | name = "ryu" | |
| 3461 | + | version = "1.0.23" | |
| 3462 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3463 | + | checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" | |
| 3464 | + | ||
| 3465 | + | [[package]] | |
| 3466 | + | name = "salsa20" | |
| 3467 | + | version = "0.11.0" | |
| 3468 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3469 | + | checksum = "2f874456e72520ff1375a06c588eaf074b0f01f9e9e1aada45bd9b7954a6e42c" | |
| 3470 | + | dependencies = [ | |
| 3471 | + | "cfg-if", | |
| 3472 | + | "cipher", | |
| 3473 | + | ] | |
| 3474 | + | ||
| 3475 | + | [[package]] | |
| 3476 | + | name = "same-file" | |
| 3477 | + | version = "1.0.6" | |
| 3478 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3479 | + | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" | |
| 3480 | + | dependencies = [ | |
| 3481 | + | "winapi-util", | |
| 3482 | + | ] | |
| 3483 | + | ||
| 3484 | + | [[package]] | |
| 3485 | + | name = "scopeguard" | |
| 3486 | + | version = "1.2.0" | |
| 3487 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3488 | + | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" | |
| 3489 | + | ||
| 3490 | + | [[package]] | |
| 3491 | + | name = "scrypt" | |
| 3492 | + | version = "0.12.0" | |
| 3493 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3494 | + | checksum = "d87af57419b594aa23fa95f09f0e06d80d84ba01c26148c43844cad6ff4485f0" | |
| 3495 | + | dependencies = [ | |
| 3496 | + | "cfg-if", | |
| 3497 | + | "pbkdf2", | |
| 3498 | + | "salsa20", | |
| 3499 | + | "sha2", | |
| 3500 | + | ] | |
| 3501 | + | ||
| 3502 | + | [[package]] | |
| 3503 | + | name = "sec1" | |
| 3504 | + | version = "0.8.1" | |
| 3505 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3506 | + | checksum = "d56d437c2f19203ce5f7122e507831de96f3d2d4d3be5af44a0b0a09d8a80e4d" | |
| 3507 | + | dependencies = [ | |
| 3508 | + | "base16ct", | |
| 3509 | + | "ctutils", | |
| 3510 | + | "der", | |
| 3511 | + | "hybrid-array", | |
| 3512 | + | "subtle", | |
| 3513 | + | "zeroize", | |
| 3514 | + | ] | |
| 3515 | + | ||
| 3516 | + | [[package]] | |
| 3517 | + | name = "semver" | |
| 3518 | + | version = "1.0.28" | |
| 3519 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3520 | + | checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" | |
| 3521 | + | ||
| 3522 | + | [[package]] | |
| 3523 | + | name = "serde" | |
| 3524 | + | version = "1.0.228" | |
| 3525 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3526 | + | checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" | |
| 3527 | + | dependencies = [ | |
| 3528 | + | "serde_core", | |
| 3529 | + | "serde_derive", | |
| 3530 | + | ] | |
| 3531 | + | ||
| 3532 | + | [[package]] | |
| 3533 | + | name = "serde_core" | |
| 3534 | + | version = "1.0.228" | |
| 3535 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3536 | + | checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" | |
| 3537 | + | dependencies = [ | |
| 3538 | + | "serde_derive", | |
| 3539 | + | ] | |
| 3540 | + | ||
| 3541 | + | [[package]] | |
| 3542 | + | name = "serde_derive" | |
| 3543 | + | version = "1.0.228" | |
| 3544 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3545 | + | checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" | |
| 3546 | + | dependencies = [ | |
| 3547 | + | "proc-macro2", | |
| 3548 | + | "quote", | |
| 3549 | + | "syn", | |
| 3550 | + | ] | |
| 3551 | + | ||
| 3552 | + | [[package]] | |
| 3553 | + | name = "serde_json" | |
| 3554 | + | version = "1.0.150" | |
| 3555 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3556 | + | checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" | |
| 3557 | + | dependencies = [ | |
| 3558 | + | "itoa", | |
| 3559 | + | "memchr", | |
| 3560 | + | "serde", | |
| 3561 | + | "serde_core", | |
| 3562 | + | "zmij", | |
| 3563 | + | ] | |
| 3564 | + | ||
| 3565 | + | [[package]] | |
| 3566 | + | name = "serde_path_to_error" | |
| 3567 | + | version = "0.1.20" | |
| 3568 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3569 | + | checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" | |
| 3570 | + | dependencies = [ | |
| 3571 | + | "itoa", | |
| 3572 | + | "serde", | |
| 3573 | + | "serde_core", | |
| 3574 | + | ] | |
| 3575 | + | ||
| 3576 | + | [[package]] | |
| 3577 | + | name = "serde_spanned" | |
| 3578 | + | version = "0.6.9" | |
| 3579 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3580 | + | checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" | |
| 3581 | + | dependencies = [ | |
| 3582 | + | "serde", | |
| 3583 | + | ] | |
| 3584 | + | ||
| 3585 | + | [[package]] | |
| 3586 | + | name = "serde_urlencoded" | |
| 3587 | + | version = "0.7.1" | |
| 3588 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3589 | + | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" | |
| 3590 | + | dependencies = [ | |
| 3591 | + | "form_urlencoded", | |
| 3592 | + | "itoa", | |
| 3593 | + | "ryu", | |
| 3594 | + | "serde", | |
| 3595 | + | ] | |
| 3596 | + | ||
| 3597 | + | [[package]] | |
| 3598 | + | name = "serdect" | |
| 3599 | + | version = "0.4.3" | |
| 3600 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3601 | + | checksum = "66cf8fedced2fcf12406bcb34223dffb92eaf34908ede12fed414c82b7f00b3e" | |
| 3602 | + | dependencies = [ | |
| 3603 | + | "base16ct", | |
| 3604 | + | "serde", | |
| 3605 | + | ] | |
| 3606 | + | ||
| 3607 | + | [[package]] | |
| 3608 | + | name = "sha1" | |
| 3609 | + | version = "0.10.6" | |
| 3610 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3611 | + | checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" | |
| 3612 | + | dependencies = [ | |
| 3613 | + | "cfg-if", | |
| 3614 | + | "cpufeatures 0.2.17", | |
| 3615 | + | "digest 0.10.7", | |
| 3616 | + | ] | |
| 3617 | + | ||
| 3618 | + | [[package]] | |
| 3619 | + | name = "sha1" | |
| 3620 | + | version = "0.11.0" | |
| 3621 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3622 | + | checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" | |
| 3623 | + | dependencies = [ | |
| 3624 | + | "cfg-if", | |
| 3625 | + | "cpufeatures 0.3.0", | |
| 3626 | + | "digest 0.11.3", | |
| 3627 | + | ] | |
| 3628 | + | ||
| 3629 | + | [[package]] | |
| 3630 | + | name = "sha1-checked" | |
| 3631 | + | version = "0.10.0" | |
| 3632 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3633 | + | checksum = "89f599ac0c323ebb1c6082821a54962b839832b03984598375bff3975b804423" | |
| 3634 | + | dependencies = [ | |
| 3635 | + | "digest 0.10.7", | |
| 3636 | + | "sha1 0.10.6", | |
| 3637 | + | ] | |
| 3638 | + | ||
| 3639 | + | [[package]] | |
| 3640 | + | name = "sha2" | |
| 3641 | + | version = "0.11.0" | |
| 3642 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3643 | + | checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" | |
| 3644 | + | dependencies = [ | |
| 3645 | + | "cfg-if", | |
| 3646 | + | "cpufeatures 0.3.0", | |
| 3647 | + | "digest 0.11.3", | |
| 3648 | + | ] | |
| 3649 | + | ||
| 3650 | + | [[package]] | |
| 3651 | + | name = "sha3" | |
| 3652 | + | version = "0.11.0" | |
| 3653 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3654 | + | checksum = "be176f1a57ce4e3d31c1a166222d9768de5954f811601fb7ca06fc8203905ce1" | |
| 3655 | + | dependencies = [ | |
| 3656 | + | "digest 0.11.3", | |
| 3657 | + | "keccak", | |
| 3658 | + | ] | |
| 3659 | + | ||
| 3660 | + | [[package]] | |
| 3661 | + | name = "sharded-slab" | |
| 3662 | + | version = "0.1.7" | |
| 3663 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3664 | + | checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" | |
| 3665 | + | dependencies = [ | |
| 3666 | + | "lazy_static", | |
| 3667 | + | ] | |
| 3668 | + | ||
| 3669 | + | [[package]] | |
| 3670 | + | name = "shell-words" | |
| 3671 | + | version = "1.1.1" | |
| 3672 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3673 | + | checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" | |
| 3674 | + | ||
| 3675 | + | [[package]] | |
| 3676 | + | name = "shlex" | |
| 3677 | + | version = "2.0.1" | |
| 3678 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3679 | + | checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" | |
| 3680 | + | ||
| 3681 | + | [[package]] | |
| 3682 | + | name = "signal-hook-registry" | |
| 3683 | + | version = "1.4.8" | |
| 3684 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3685 | + | checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" | |
| 3686 | + | dependencies = [ | |
| 3687 | + | "errno", | |
| 3688 | + | "libc", | |
| 3689 | + | ] | |
| 3690 | + | ||
| 3691 | + | [[package]] | |
| 3692 | + | name = "signature" | |
| 3693 | + | version = "3.0.0" | |
| 3694 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3695 | + | checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" | |
| 3696 | + | dependencies = [ | |
| 3697 | + | "digest 0.11.3", | |
| 3698 | + | "rand_core 0.10.1", | |
| 3699 | + | ] | |
| 3700 | + | ||
| 3701 | + | [[package]] | |
| 3702 | + | name = "simd-adler32" | |
| 3703 | + | version = "0.3.9" | |
| 3704 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3705 | + | checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" | |
| 3706 | + | ||
| 3707 | + | [[package]] | |
| 3708 | + | name = "similar" | |
| 3709 | + | version = "2.7.0" | |
| 3710 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3711 | + | checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" | |
| 3712 | + | ||
| 3713 | + | [[package]] | |
| 3714 | + | name = "slab" | |
| 3715 | + | version = "0.4.12" | |
| 3716 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3717 | + | checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" | |
| 3718 | + | ||
| 3719 | + | [[package]] | |
| 3720 | + | name = "smallvec" | |
| 3721 | + | version = "1.15.1" | |
| 3722 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3723 | + | checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" | |
| 3724 | + | ||
| 3725 | + | [[package]] | |
| 3726 | + | name = "socket2" | |
| 3727 | + | version = "0.6.4" | |
| 3728 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3729 | + | checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" | |
| 3730 | + | dependencies = [ | |
| 3731 | + | "libc", | |
| 3732 | + | "windows-sys", | |
| 3733 | + | ] | |
| 3734 | + | ||
| 3735 | + | [[package]] | |
| 3736 | + | name = "spki" | |
| 3737 | + | version = "0.8.0" | |
| 3738 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3739 | + | checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f" | |
| 3740 | + | dependencies = [ | |
| 3741 | + | "base64ct", | |
| 3742 | + | "der", | |
| 3743 | + | ] | |
| 3744 | + | ||
| 3745 | + | [[package]] | |
| 3746 | + | name = "sqlite-wasm-rs" | |
| 3747 | + | version = "0.5.5" | |
| 3748 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3749 | + | checksum = "dc3efc0da82635d7e1ced0053bbbfa8c7ab9645d0bf36ceb4f7127bb85315d75" | |
| 3750 | + | dependencies = [ | |
| 3751 | + | "cc", | |
| 3752 | + | "js-sys", | |
| 3753 | + | "rsqlite-vfs", | |
| 3754 | + | "wasm-bindgen", | |
| 3755 | + | ] | |
| 3756 | + | ||
| 3757 | + | [[package]] | |
| 3758 | + | name = "ssh-cipher" | |
| 3759 | + | version = "0.3.0-rc.9" | |
| 3760 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3761 | + | checksum = "10db6f219196a8528f9ec904d9d45cdad692d65b0e57e72be4dedd1c5fddce36" | |
| 3762 | + | dependencies = [ | |
| 3763 | + | "aead", | |
| 3764 | + | "aes", | |
| 3765 | + | "aes-gcm", | |
| 3766 | + | "cbc", | |
| 3767 | + | "chacha20", | |
| 3768 | + | "cipher", | |
| 3769 | + | "ctr", | |
| 3770 | + | "ctutils", | |
| 3771 | + | "des", | |
| 3772 | + | "poly1305", | |
| 3773 | + | "ssh-encoding", | |
| 3774 | + | "zeroize", | |
| 3775 | + | ] | |
| 3776 | + | ||
| 3777 | + | [[package]] | |
| 3778 | + | name = "ssh-encoding" | |
| 3779 | + | version = "0.3.0-rc.9" | |
| 3780 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3781 | + | checksum = "7abf34aa716da5d5b4c496936d042ea282ab392092cd68a72ef6a8863ff8c96a" | |
| 3782 | + | dependencies = [ | |
| 3783 | + | "base64ct", | |
| 3784 | + | "bytes", | |
| 3785 | + | "crypto-bigint", | |
| 3786 | + | "ctutils", | |
| 3787 | + | "digest 0.11.3", | |
| 3788 | + | "pem-rfc7468", | |
| 3789 | + | "zeroize", | |
| 3790 | + | ] | |
| 3791 | + | ||
| 3792 | + | [[package]] | |
| 3793 | + | name = "ssh-key" | |
| 3794 | + | version = "0.7.0-rc.10" | |
| 3795 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3796 | + | checksum = "45735ce3dea95690e4a9e414c4cfde7f79835063c3dcd35881df85a84118e74b" | |
| 3797 | + | dependencies = [ | |
| 3798 | + | "argon2 0.6.0-rc.8", | |
| 3799 | + | "bcrypt-pbkdf", | |
| 3800 | + | "ctutils", | |
| 3801 | + | "ed25519-dalek", | |
| 3802 | + | "hex", | |
| 3803 | + | "hmac", | |
| 3804 | + | "p256", | |
| 3805 | + | "p384", | |
| 3806 | + | "p521", | |
| 3807 | + | "rand_core 0.10.1", | |
| 3808 | + | "rsa", | |
| 3809 | + | "sec1", | |
| 3810 | + | "sha1 0.11.0", | |
| 3811 | + | "sha2", | |
| 3812 | + | "signature", | |
| 3813 | + | "ssh-cipher", | |
| 3814 | + | "ssh-encoding", | |
| 3815 | + | "zeroize", | |
| 3816 | + | ] | |
| 3817 | + | ||
| 3818 | + | [[package]] | |
| 3819 | + | name = "stable_deref_trait" | |
| 3820 | + | version = "1.2.1" | |
| 3821 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3822 | + | checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" | |
| 3823 | + | ||
| 3824 | + | [[package]] | |
| 3825 | + | name = "static_assertions" | |
| 3826 | + | version = "1.1.0" | |
| 3827 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3828 | + | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" | |
| 3829 | + | ||
| 3830 | + | [[package]] | |
| 3831 | + | name = "strsim" | |
| 3832 | + | version = "0.11.1" | |
| 3833 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3834 | + | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" | |
| 3835 | + | ||
| 3836 | + | [[package]] | |
| 3837 | + | name = "subtle" | |
| 3838 | + | version = "2.6.1" | |
| 3839 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3840 | + | checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" | |
| 3841 | + | ||
| 3842 | + | [[package]] | |
| 3843 | + | name = "syn" | |
| 3844 | + | version = "2.0.117" | |
| 3845 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3846 | + | checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" | |
| 3847 | + | dependencies = [ | |
| 3848 | + | "proc-macro2", | |
| 3849 | + | "quote", | |
| 3850 | + | "unicode-ident", | |
| 3851 | + | ] | |
| 3852 | + | ||
| 3853 | + | [[package]] | |
| 3854 | + | name = "sync_wrapper" | |
| 3855 | + | version = "1.0.2" | |
| 3856 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3857 | + | checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" | |
| 3858 | + | ||
| 3859 | + | [[package]] | |
| 3860 | + | name = "synstructure" | |
| 3861 | + | version = "0.13.2" | |
| 3862 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3863 | + | checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" | |
| 3864 | + | dependencies = [ | |
| 3865 | + | "proc-macro2", | |
| 3866 | + | "quote", | |
| 3867 | + | "syn", | |
| 3868 | + | ] | |
| 3869 | + | ||
| 3870 | + | [[package]] | |
| 3871 | + | name = "syntect" | |
| 3872 | + | version = "5.3.0" | |
| 3873 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3874 | + | checksum = "656b45c05d95a5704399aeef6bd0ddec7b2b3531b7c9e900abbf7c4d2190c925" | |
| 3875 | + | dependencies = [ | |
| 3876 | + | "bincode", | |
| 3877 | + | "fancy-regex", | |
| 3878 | + | "flate2", | |
| 3879 | + | "fnv", | |
| 3880 | + | "once_cell", | |
| 3881 | + | "plist", | |
| 3882 | + | "regex-syntax", | |
| 3883 | + | "serde", | |
| 3884 | + | "serde_derive", | |
| 3885 | + | "serde_json", | |
| 3886 | + | "thiserror", | |
| 3887 | + | "walkdir", | |
| 3888 | + | "yaml-rust", | |
| 3889 | + | ] | |
| 3890 | + | ||
| 3891 | + | [[package]] | |
| 3892 | + | name = "tempfile" | |
| 3893 | + | version = "3.27.0" | |
| 3894 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3895 | + | checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" | |
| 3896 | + | dependencies = [ | |
| 3897 | + | "fastrand", | |
| 3898 | + | "getrandom 0.4.2", | |
| 3899 | + | "once_cell", | |
| 3900 | + | "rustix", | |
| 3901 | + | "windows-sys", | |
| 3902 | + | ] | |
| 3903 | + | ||
| 3904 | + | [[package]] | |
| 3905 | + | name = "thiserror" | |
| 3906 | + | version = "2.0.18" | |
| 3907 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3908 | + | checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" | |
| 3909 | + | dependencies = [ | |
| 3910 | + | "thiserror-impl", | |
| 3911 | + | ] | |
| 3912 | + | ||
| 3913 | + | [[package]] | |
| 3914 | + | name = "thiserror-impl" | |
| 3915 | + | version = "2.0.18" | |
| 3916 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3917 | + | checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" | |
| 3918 | + | dependencies = [ | |
| 3919 | + | "proc-macro2", | |
| 3920 | + | "quote", | |
| 3921 | + | "syn", | |
| 3922 | + | ] | |
| 3923 | + | ||
| 3924 | + | [[package]] | |
| 3925 | + | name = "thread_local" | |
| 3926 | + | version = "1.1.9" | |
| 3927 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3928 | + | checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" | |
| 3929 | + | dependencies = [ | |
| 3930 | + | "cfg-if", | |
| 3931 | + | ] | |
| 3932 | + | ||
| 3933 | + | [[package]] | |
| 3934 | + | name = "time" | |
| 3935 | + | version = "0.3.47" | |
| 3936 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3937 | + | checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" | |
| 3938 | + | dependencies = [ | |
| 3939 | + | "deranged", | |
| 3940 | + | "itoa", | |
| 3941 | + | "num-conv", | |
| 3942 | + | "powerfmt", | |
| 3943 | + | "serde_core", | |
| 3944 | + | "time-core", | |
| 3945 | + | "time-macros", | |
| 3946 | + | ] | |
| 3947 | + | ||
| 3948 | + | [[package]] | |
| 3949 | + | name = "time-core" | |
| 3950 | + | version = "0.1.8" | |
| 3951 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3952 | + | checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" | |
| 3953 | + | ||
| 3954 | + | [[package]] | |
| 3955 | + | name = "time-macros" | |
| 3956 | + | version = "0.2.27" | |
| 3957 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3958 | + | checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" | |
| 3959 | + | dependencies = [ | |
| 3960 | + | "num-conv", | |
| 3961 | + | "time-core", | |
| 3962 | + | ] | |
| 3963 | + | ||
| 3964 | + | [[package]] | |
| 3965 | + | name = "tinystr" | |
| 3966 | + | version = "0.8.3" | |
| 3967 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3968 | + | checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" | |
| 3969 | + | dependencies = [ | |
| 3970 | + | "displaydoc", | |
| 3971 | + | "zerovec", | |
| 3972 | + | ] | |
| 3973 | + | ||
| 3974 | + | [[package]] | |
| 3975 | + | name = "tinyvec" | |
| 3976 | + | version = "1.11.0" | |
| 3977 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3978 | + | checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" | |
| 3979 | + | dependencies = [ | |
| 3980 | + | "tinyvec_macros", | |
| 3981 | + | ] | |
| 3982 | + | ||
| 3983 | + | [[package]] | |
| 3984 | + | name = "tinyvec_macros" | |
| 3985 | + | version = "0.1.1" | |
| 3986 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3987 | + | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" | |
| 3988 | + | ||
| 3989 | + | [[package]] | |
| 3990 | + | name = "toasty" | |
| 3991 | + | version = "0.7.0" | |
| 3992 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3993 | + | checksum = "17bd9e42ba3c1aa195101d150ba281f3d426439e0d2b5991b4c7c6290204348f" | |
| 3994 | + | dependencies = [ | |
| 3995 | + | "async-trait", | |
| 3996 | + | "bit-set 0.10.0", | |
| 3997 | + | "by_address", | |
| 3998 | + | "deadpool", | |
| 3999 | + | "hashbrown 0.17.1", | |
| 4000 | + | "index_vec", | |
| 4001 | + | "indexmap", | |
| 4002 | + | "inventory", | |
| 4003 | + | "toasty-core", | |
| 4004 | + | "toasty-driver-sqlite", | |
| 4005 | + | "toasty-macros", | |
| 4006 | + | "tokio", | |
| 4007 | + | "tokio-stream", | |
| 4008 | + | "tracing", | |
| 4009 | + | "url", | |
| 4010 | + | "uuid", | |
| 4011 | + | ] | |
| 4012 | + | ||
| 4013 | + | [[package]] | |
| 4014 | + | name = "toasty-core" | |
| 4015 | + | version = "0.7.0" | |
| 4016 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4017 | + | checksum = "7f06ecf2a9091a814d7cfa7edc39c24a791460431f55a0ee19c1b2cb4a15b98c" | |
| 4018 | + | dependencies = [ | |
| 4019 | + | "async-trait", | |
| 4020 | + | "bit-set 0.10.0", | |
| 4021 | + | "hashbrown 0.17.1", | |
| 4022 | + | "heck", | |
| 4023 | + | "indexmap", | |
| 4024 | + | "pluralizer", | |
| 4025 | + | "tokio-stream", | |
| 4026 | + | "uuid", | |
| 4027 | + | ] | |
| 4028 | + | ||
| 4029 | + | [[package]] | |
| 4030 | + | name = "toasty-driver-sqlite" | |
| 4031 | + | version = "0.7.0" | |
| 4032 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4033 | + | checksum = "fd09e84ccfbd608949bc501c30e1c62334ddf07eb1c30976b2c470dfd19aef3e" | |
| 4034 | + | dependencies = [ | |
| 4035 | + | "async-trait", | |
| 4036 | + | "rusqlite", | |
| 4037 | + | "serde", | |
| 4038 | + | "serde_json", | |
| 4039 | + | "toasty-core", | |
| 4040 | + | "toasty-sql", | |
| 4041 | + | "tracing", | |
| 4042 | + | "url", | |
| 4043 | + | "uuid", | |
| 4044 | + | ] | |
| 4045 | + | ||
| 4046 | + | [[package]] | |
| 4047 | + | name = "toasty-macros" | |
| 4048 | + | version = "0.7.0" | |
| 4049 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4050 | + | checksum = "1d27469a13c35b53f3080ca24e4585f5cd2d1ba4dd71432125aa376759439131" | |
| 4051 | + | dependencies = [ | |
| 4052 | + | "hashbrown 0.17.1", | |
| 4053 | + | "heck", | |
| 4054 | + | "pluralizer", | |
| 4055 | + | "proc-macro2", | |
| 4056 | + | "quote", | |
| 4057 | + | "syn", | |
| 4058 | + | "toasty-core", | |
| 4059 | + | ] | |
| 4060 | + | ||
| 4061 | + | [[package]] | |
| 4062 | + | name = "toasty-sql" | |
| 4063 | + | version = "0.7.0" | |
| 4064 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4065 | + | checksum = "2848a892303d9d63485a8f16d9c1b692b145c09940d2cdeefaf0f837e1bf6a49" | |
| 4066 | + | dependencies = [ | |
| 4067 | + | "serde_json", | |
| 4068 | + | "toasty-core", | |
| 4069 | + | ] | |
| 4070 | + | ||
| 4071 | + | [[package]] | |
| 4072 | + | name = "tokio" | |
| 4073 | + | version = "1.52.3" | |
| 4074 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4075 | + | checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" | |
| 4076 | + | dependencies = [ | |
| 4077 | + | "bytes", | |
| 4078 | + | "libc", | |
| 4079 | + | "mio", | |
| 4080 | + | "parking_lot", | |
| 4081 | + | "pin-project-lite", | |
| 4082 | + | "signal-hook-registry", | |
| 4083 | + | "socket2", | |
| 4084 | + | "tokio-macros", | |
| 4085 | + | "windows-sys", | |
| 4086 | + | ] | |
| 4087 | + | ||
| 4088 | + | [[package]] | |
| 4089 | + | name = "tokio-macros" | |
| 4090 | + | version = "2.7.0" | |
| 4091 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4092 | + | checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" | |
| 4093 | + | dependencies = [ | |
| 4094 | + | "proc-macro2", | |
| 4095 | + | "quote", | |
| 4096 | + | "syn", | |
| 4097 | + | ] | |
| 4098 | + | ||
| 4099 | + | [[package]] | |
| 4100 | + | name = "tokio-stream" | |
| 4101 | + | version = "0.1.18" | |
| 4102 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4103 | + | checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" | |
| 4104 | + | dependencies = [ | |
| 4105 | + | "futures-core", | |
| 4106 | + | "pin-project-lite", | |
| 4107 | + | "tokio", | |
| 4108 | + | ] | |
| 4109 | + | ||
| 4110 | + | [[package]] | |
| 4111 | + | name = "tokio-util" | |
| 4112 | + | version = "0.7.18" | |
| 4113 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4114 | + | checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" | |
| 4115 | + | dependencies = [ | |
| 4116 | + | "bytes", | |
| 4117 | + | "futures-core", | |
| 4118 | + | "futures-sink", | |
| 4119 | + | "pin-project-lite", | |
| 4120 | + | "tokio", | |
| 4121 | + | ] | |
| 4122 | + | ||
| 4123 | + | [[package]] | |
| 4124 | + | name = "toml" | |
| 4125 | + | version = "0.8.23" | |
| 4126 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4127 | + | checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" | |
| 4128 | + | dependencies = [ | |
| 4129 | + | "serde", | |
| 4130 | + | "serde_spanned", | |
| 4131 | + | "toml_datetime", | |
| 4132 | + | "toml_edit", | |
| 4133 | + | ] | |
| 4134 | + | ||
| 4135 | + | [[package]] | |
| 4136 | + | name = "toml_datetime" | |
| 4137 | + | version = "0.6.11" | |
| 4138 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4139 | + | checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" | |
| 4140 | + | dependencies = [ | |
| 4141 | + | "serde", | |
| 4142 | + | ] | |
| 4143 | + | ||
| 4144 | + | [[package]] | |
| 4145 | + | name = "toml_edit" | |
| 4146 | + | version = "0.22.27" | |
| 4147 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4148 | + | checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" | |
| 4149 | + | dependencies = [ | |
| 4150 | + | "indexmap", | |
| 4151 | + | "serde", | |
| 4152 | + | "serde_spanned", | |
| 4153 | + | "toml_datetime", | |
| 4154 | + | "toml_write", | |
| 4155 | + | "winnow", | |
| 4156 | + | ] | |
| 4157 | + | ||
| 4158 | + | [[package]] | |
| 4159 | + | name = "toml_write" | |
| 4160 | + | version = "0.1.2" | |
| 4161 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4162 | + | checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" | |
| 4163 | + | ||
| 4164 | + | [[package]] | |
| 4165 | + | name = "tower" | |
| 4166 | + | version = "0.5.3" | |
| 4167 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4168 | + | checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" | |
| 4169 | + | dependencies = [ | |
| 4170 | + | "futures-core", | |
| 4171 | + | "futures-util", | |
| 4172 | + | "pin-project-lite", | |
| 4173 | + | "sync_wrapper", | |
| 4174 | + | "tokio", | |
| 4175 | + | "tower-layer", | |
| 4176 | + | "tower-service", | |
| 4177 | + | "tracing", | |
| 4178 | + | ] | |
| 4179 | + | ||
| 4180 | + | [[package]] | |
| 4181 | + | name = "tower-http" | |
| 4182 | + | version = "0.6.11" | |
| 4183 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4184 | + | checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" | |
| 4185 | + | dependencies = [ | |
| 4186 | + | "bitflags", | |
| 4187 | + | "bytes", | |
| 4188 | + | "futures-core", | |
| 4189 | + | "futures-util", | |
| 4190 | + | "http", | |
| 4191 | + | "http-body", | |
| 4192 | + | "http-body-util", | |
| 4193 | + | "http-range-header", | |
| 4194 | + | "httpdate", | |
| 4195 | + | "mime", | |
| 4196 | + | "mime_guess", | |
| 4197 | + | "percent-encoding", | |
| 4198 | + | "pin-project-lite", | |
| 4199 | + | "tokio", | |
| 4200 | + | "tokio-util", | |
| 4201 | + | "tower-layer", | |
| 4202 | + | "tower-service", | |
| 4203 | + | "tracing", | |
| 4204 | + | ] | |
| 4205 | + | ||
| 4206 | + | [[package]] | |
| 4207 | + | name = "tower-layer" | |
| 4208 | + | version = "0.3.3" | |
| 4209 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4210 | + | checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" | |
| 4211 | + | ||
| 4212 | + | [[package]] | |
| 4213 | + | name = "tower-service" | |
| 4214 | + | version = "0.3.3" | |
| 4215 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4216 | + | checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" | |
| 4217 | + | ||
| 4218 | + | [[package]] | |
| 4219 | + | name = "tracing" | |
| 4220 | + | version = "0.1.44" | |
| 4221 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4222 | + | checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" | |
| 4223 | + | dependencies = [ | |
| 4224 | + | "log", | |
| 4225 | + | "pin-project-lite", | |
| 4226 | + | "tracing-attributes", | |
| 4227 | + | "tracing-core", | |
| 4228 | + | ] | |
| 4229 | + | ||
| 4230 | + | [[package]] | |
| 4231 | + | name = "tracing-attributes" | |
| 4232 | + | version = "0.1.31" | |
| 4233 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4234 | + | checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" | |
| 4235 | + | dependencies = [ | |
| 4236 | + | "proc-macro2", | |
| 4237 | + | "quote", | |
| 4238 | + | "syn", | |
| 4239 | + | ] | |
| 4240 | + | ||
| 4241 | + | [[package]] | |
| 4242 | + | name = "tracing-core" | |
| 4243 | + | version = "0.1.36" | |
| 4244 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4245 | + | checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" | |
| 4246 | + | dependencies = [ | |
| 4247 | + | "once_cell", | |
| 4248 | + | "valuable", | |
| 4249 | + | ] | |
| 4250 | + | ||
| 4251 | + | [[package]] | |
| 4252 | + | name = "tracing-log" | |
| 4253 | + | version = "0.2.0" | |
| 4254 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4255 | + | checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" | |
| 4256 | + | dependencies = [ | |
| 4257 | + | "log", | |
| 4258 | + | "once_cell", | |
| 4259 | + | "tracing-core", | |
| 4260 | + | ] | |
| 4261 | + | ||
| 4262 | + | [[package]] | |
| 4263 | + | name = "tracing-subscriber" | |
| 4264 | + | version = "0.3.23" | |
| 4265 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4266 | + | checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" | |
| 4267 | + | dependencies = [ | |
| 4268 | + | "matchers", | |
| 4269 | + | "nu-ansi-term", | |
| 4270 | + | "once_cell", | |
| 4271 | + | "regex-automata", | |
| 4272 | + | "sharded-slab", | |
| 4273 | + | "smallvec", | |
| 4274 | + | "thread_local", | |
| 4275 | + | "tracing", | |
| 4276 | + | "tracing-core", | |
| 4277 | + | "tracing-log", | |
| 4278 | + | ] | |
| 4279 | + | ||
| 4280 | + | [[package]] | |
| 4281 | + | name = "typenum" | |
| 4282 | + | version = "1.20.1" | |
| 4283 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4284 | + | checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" | |
| 4285 | + | ||
| 4286 | + | [[package]] | |
| 4287 | + | name = "uluru" | |
| 4288 | + | version = "3.1.0" | |
| 4289 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4290 | + | checksum = "7c8a2469e56e6e5095c82ccd3afb98dad95f7af7929aab6d8ba8d6e0f73657da" | |
| 4291 | + | dependencies = [ | |
| 4292 | + | "arrayvec", | |
| 4293 | + | ] | |
| 4294 | + | ||
| 4295 | + | [[package]] | |
| 4296 | + | name = "unicase" | |
| 4297 | + | version = "2.9.0" | |
| 4298 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4299 | + | checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" | |
| 4300 | + | ||
| 4301 | + | [[package]] | |
| 4302 | + | name = "unicode-bom" | |
| 4303 | + | version = "2.0.3" | |
| 4304 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4305 | + | checksum = "7eec5d1121208364f6793f7d2e222bf75a915c19557537745b195b253dd64217" | |
| 4306 | + | ||
| 4307 | + | [[package]] | |
| 4308 | + | name = "unicode-ident" | |
| 4309 | + | version = "1.0.24" | |
| 4310 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4311 | + | checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" | |
| 4312 | + | ||
| 4313 | + | [[package]] | |
| 4314 | + | name = "unicode-normalization" | |
| 4315 | + | version = "0.1.25" | |
| 4316 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4317 | + | checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" | |
| 4318 | + | dependencies = [ | |
| 4319 | + | "tinyvec", | |
| 4320 | + | ] | |
| 4321 | + | ||
| 4322 | + | [[package]] | |
| 4323 | + | name = "unicode-xid" | |
| 4324 | + | version = "0.2.6" | |
| 4325 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4326 | + | checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" | |
| 4327 | + | ||
| 4328 | + | [[package]] | |
| 4329 | + | name = "universal-hash" | |
| 4330 | + | version = "0.6.1" | |
| 4331 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4332 | + | checksum = "f4987bdc12753382e0bec4a65c50738ffaabc998b9cdd1f952fb5f39b0048a96" | |
| 4333 | + | dependencies = [ | |
| 4334 | + | "crypto-common 0.2.2", | |
| 4335 | + | "ctutils", | |
| 4336 | + | ] | |
| 4337 | + | ||
| 4338 | + | [[package]] | |
| 4339 | + | name = "untrusted" | |
| 4340 | + | version = "0.7.1" | |
| 4341 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4342 | + | checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" | |
| 4343 | + | ||
| 4344 | + | [[package]] | |
| 4345 | + | name = "url" | |
| 4346 | + | version = "2.5.8" | |
| 4347 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4348 | + | checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" | |
| 4349 | + | dependencies = [ | |
| 4350 | + | "form_urlencoded", | |
| 4351 | + | "idna", | |
| 4352 | + | "percent-encoding", | |
| 4353 | + | "serde", | |
| 4354 | + | ] | |
| 4355 | + | ||
| 4356 | + | [[package]] | |
| 4357 | + | name = "utf8_iter" | |
| 4358 | + | version = "1.0.4" | |
| 4359 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4360 | + | checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" | |
| 4361 | + | ||
| 4362 | + | [[package]] | |
| 4363 | + | name = "utf8parse" | |
| 4364 | + | version = "0.2.2" | |
| 4365 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4366 | + | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" | |
| 4367 | + | ||
| 4368 | + | [[package]] | |
| 4369 | + | name = "uuid" | |
| 4370 | + | version = "1.23.3" | |
| 4371 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4372 | + | checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" | |
| 4373 | + | dependencies = [ | |
| 4374 | + | "getrandom 0.4.2", | |
| 4375 | + | "js-sys", | |
| 4376 | + | "rand", | |
| 4377 | + | "wasm-bindgen", | |
| 4378 | + | ] | |
| 4379 | + | ||
| 4380 | + | [[package]] | |
| 4381 | + | name = "valuable" | |
| 4382 | + | version = "0.1.1" | |
| 4383 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4384 | + | checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" | |
| 4385 | + | ||
| 4386 | + | [[package]] | |
| 4387 | + | name = "vcpkg" | |
| 4388 | + | version = "0.2.15" | |
| 4389 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4390 | + | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" | |
| 4391 | + | ||
| 4392 | + | [[package]] | |
| 4393 | + | name = "version_check" | |
| 4394 | + | version = "0.9.5" | |
| 4395 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4396 | + | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" | |
| 4397 | + | ||
| 4398 | + | [[package]] | |
| 4399 | + | name = "walkdir" | |
| 4400 | + | version = "2.5.0" | |
| 4401 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4402 | + | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" | |
| 4403 | + | dependencies = [ | |
| 4404 | + | "same-file", | |
| 4405 | + | "winapi-util", | |
| 4406 | + | ] | |
| 4407 | + | ||
| 4408 | + | [[package]] | |
| 4409 | + | name = "wasi" | |
| 4410 | + | version = "0.11.1+wasi-snapshot-preview1" | |
| 4411 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4412 | + | checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" | |
| 4413 | + | ||
| 4414 | + | [[package]] | |
| 4415 | + | name = "wasip2" | |
| 4416 | + | version = "1.0.3+wasi-0.2.9" | |
| 4417 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4418 | + | checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" | |
| 4419 | + | dependencies = [ | |
| 4420 | + | "wit-bindgen 0.57.1", | |
| 4421 | + | ] | |
| 4422 | + | ||
| 4423 | + | [[package]] | |
| 4424 | + | name = "wasip3" | |
| 4425 | + | version = "0.4.0+wasi-0.3.0-rc-2026-01-06" | |
| 4426 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4427 | + | checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" | |
| 4428 | + | dependencies = [ | |
| 4429 | + | "wit-bindgen 0.51.0", | |
| 4430 | + | ] | |
| 4431 | + | ||
| 4432 | + | [[package]] | |
| 4433 | + | name = "wasm-bindgen" | |
| 4434 | + | version = "0.2.123" | |
| 4435 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4436 | + | checksum = "a254a4b10c19a76f09a27640e7ffbf9bc30bf67e16a3bf28aaefa4920fe81563" | |
| 4437 | + | dependencies = [ | |
| 4438 | + | "cfg-if", | |
| 4439 | + | "once_cell", | |
| 4440 | + | "rustversion", | |
| 4441 | + | "wasm-bindgen-macro", | |
| 4442 | + | "wasm-bindgen-shared", | |
| 4443 | + | ] | |
| 4444 | + | ||
| 4445 | + | [[package]] | |
| 4446 | + | name = "wasm-bindgen-futures" | |
| 4447 | + | version = "0.4.73" | |
| 4448 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4449 | + | checksum = "54568702fabf5d4849ce2b90fadfa64168a097eaf4b351ce9df8b687a0086aaf" | |
| 4450 | + | dependencies = [ | |
| 4451 | + | "js-sys", | |
| 4452 | + | "wasm-bindgen", | |
| 4453 | + | ] | |
| 4454 | + | ||
| 4455 | + | [[package]] | |
| 4456 | + | name = "wasm-bindgen-macro" | |
| 4457 | + | version = "0.2.123" | |
| 4458 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4459 | + | checksum = "24a40fc75b0ec6f3746ceb10d36f53a93dcd68a93b11b6445983945d79eba0dc" | |
| 4460 | + | dependencies = [ | |
| 4461 | + | "quote", | |
| 4462 | + | "wasm-bindgen-macro-support", | |
| 4463 | + | ] | |
| 4464 | + | ||
| 4465 | + | [[package]] | |
| 4466 | + | name = "wasm-bindgen-macro-support" | |
| 4467 | + | version = "0.2.123" | |
| 4468 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4469 | + | checksum = "908f34bd9b9ce3d4caf07b72dfab63d61504d156856c6bd3cd87fa350cf3985b" | |
| 4470 | + | dependencies = [ | |
| 4471 | + | "bumpalo", | |
| 4472 | + | "proc-macro2", | |
| 4473 | + | "quote", | |
| 4474 | + | "syn", | |
| 4475 | + | "wasm-bindgen-shared", | |
| 4476 | + | ] | |
| 4477 | + | ||
| 4478 | + | [[package]] | |
| 4479 | + | name = "wasm-bindgen-shared" | |
| 4480 | + | version = "0.2.123" | |
| 4481 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4482 | + | checksum = "7acbf7616c27b194bbb550bf77ed0c2c3e5b7fd1260a93082b95fb7f47959b92" | |
| 4483 | + | dependencies = [ | |
| 4484 | + | "unicode-ident", | |
| 4485 | + | ] | |
| 4486 | + | ||
| 4487 | + | [[package]] | |
| 4488 | + | name = "wasm-encoder" | |
| 4489 | + | version = "0.244.0" | |
| 4490 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4491 | + | checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" | |
| 4492 | + | dependencies = [ | |
| 4493 | + | "leb128fmt", | |
| 4494 | + | "wasmparser", | |
| 4495 | + | ] | |
| 4496 | + | ||
| 4497 | + | [[package]] | |
| 4498 | + | name = "wasm-metadata" | |
| 4499 | + | version = "0.244.0" | |
| 4500 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4501 | + | checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" | |
| 4502 | + | dependencies = [ | |
| 4503 | + | "anyhow", | |
| 4504 | + | "indexmap", | |
| 4505 | + | "wasm-encoder", | |
| 4506 | + | "wasmparser", | |
| 4507 | + | ] | |
| 4508 | + | ||
| 4509 | + | [[package]] | |
| 4510 | + | name = "wasmparser" | |
| 4511 | + | version = "0.244.0" | |
| 4512 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4513 | + | checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" | |
| 4514 | + | dependencies = [ | |
| 4515 | + | "bitflags", | |
| 4516 | + | "hashbrown 0.15.5", | |
| 4517 | + | "indexmap", | |
| 4518 | + | "semver", | |
| 4519 | + | ] | |
| 4520 | + | ||
| 4521 | + | [[package]] | |
| 4522 | + | name = "winapi-util" | |
| 4523 | + | version = "0.1.11" | |
| 4524 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4525 | + | checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" | |
| 4526 | + | dependencies = [ | |
| 4527 | + | "windows-sys", | |
| 4528 | + | ] | |
| 4529 | + | ||
| 4530 | + | [[package]] | |
| 4531 | + | name = "windows" | |
| 4532 | + | version = "0.62.2" | |
| 4533 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4534 | + | checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" | |
| 4535 | + | dependencies = [ | |
| 4536 | + | "windows-collections", | |
| 4537 | + | "windows-core", | |
| 4538 | + | "windows-future", | |
| 4539 | + | "windows-numerics", | |
| 4540 | + | ] | |
| 4541 | + | ||
| 4542 | + | [[package]] | |
| 4543 | + | name = "windows-collections" | |
| 4544 | + | version = "0.3.2" | |
| 4545 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4546 | + | checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" | |
| 4547 | + | dependencies = [ | |
| 4548 | + | "windows-core", | |
| 4549 | + | ] | |
| 4550 | + | ||
| 4551 | + | [[package]] | |
| 4552 | + | name = "windows-core" | |
| 4553 | + | version = "0.62.2" | |
| 4554 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4555 | + | checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" | |
| 4556 | + | dependencies = [ | |
| 4557 | + | "windows-implement", | |
| 4558 | + | "windows-interface", | |
| 4559 | + | "windows-link", | |
| 4560 | + | "windows-result", | |
| 4561 | + | "windows-strings", | |
| 4562 | + | ] | |
| 4563 | + | ||
| 4564 | + | [[package]] | |
| 4565 | + | name = "windows-future" | |
| 4566 | + | version = "0.3.2" | |
| 4567 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4568 | + | checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" | |
| 4569 | + | dependencies = [ | |
| 4570 | + | "windows-core", | |
| 4571 | + | "windows-link", | |
| 4572 | + | "windows-threading", | |
| 4573 | + | ] | |
| 4574 | + | ||
| 4575 | + | [[package]] | |
| 4576 | + | name = "windows-implement" | |
| 4577 | + | version = "0.60.2" | |
| 4578 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4579 | + | checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" | |
| 4580 | + | dependencies = [ | |
| 4581 | + | "proc-macro2", | |
| 4582 | + | "quote", | |
| 4583 | + | "syn", | |
| 4584 | + | ] | |
| 4585 | + | ||
| 4586 | + | [[package]] | |
| 4587 | + | name = "windows-interface" | |
| 4588 | + | version = "0.59.3" | |
| 4589 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4590 | + | checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" | |
| 4591 | + | dependencies = [ | |
| 4592 | + | "proc-macro2", | |
| 4593 | + | "quote", | |
| 4594 | + | "syn", | |
| 4595 | + | ] | |
| 4596 | + | ||
| 4597 | + | [[package]] | |
| 4598 | + | name = "windows-link" | |
| 4599 | + | version = "0.2.1" | |
| 4600 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4601 | + | checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" | |
| 4602 | + | ||
| 4603 | + | [[package]] | |
| 4604 | + | name = "windows-numerics" | |
| 4605 | + | version = "0.3.1" | |
| 4606 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4607 | + | checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" | |
| 4608 | + | dependencies = [ | |
| 4609 | + | "windows-core", | |
| 4610 | + | "windows-link", | |
| 4611 | + | ] | |
| 4612 | + | ||
| 4613 | + | [[package]] | |
| 4614 | + | name = "windows-result" | |
| 4615 | + | version = "0.4.1" | |
| 4616 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4617 | + | checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" | |
| 4618 | + | dependencies = [ | |
| 4619 | + | "windows-link", | |
| 4620 | + | ] | |
| 4621 | + | ||
| 4622 | + | [[package]] | |
| 4623 | + | name = "windows-strings" | |
| 4624 | + | version = "0.5.1" | |
| 4625 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4626 | + | checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" | |
| 4627 | + | dependencies = [ | |
| 4628 | + | "windows-link", | |
| 4629 | + | ] | |
| 4630 | + | ||
| 4631 | + | [[package]] | |
| 4632 | + | name = "windows-sys" | |
| 4633 | + | version = "0.61.2" | |
| 4634 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4635 | + | checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" | |
| 4636 | + | dependencies = [ | |
| 4637 | + | "windows-link", | |
| 4638 | + | ] | |
| 4639 | + | ||
| 4640 | + | [[package]] | |
| 4641 | + | name = "windows-threading" | |
| 4642 | + | version = "0.2.1" | |
| 4643 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4644 | + | checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" | |
| 4645 | + | dependencies = [ | |
| 4646 | + | "windows-link", | |
| 4647 | + | ] | |
| 4648 | + | ||
| 4649 | + | [[package]] | |
| 4650 | + | name = "winnow" | |
| 4651 | + | version = "0.7.15" | |
| 4652 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4653 | + | checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" | |
| 4654 | + | dependencies = [ | |
| 4655 | + | "memchr", | |
| 4656 | + | ] | |
| 4657 | + | ||
| 4658 | + | [[package]] | |
| 4659 | + | name = "wit-bindgen" | |
| 4660 | + | version = "0.51.0" | |
| 4661 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4662 | + | checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" | |
| 4663 | + | dependencies = [ | |
| 4664 | + | "wit-bindgen-rust-macro", | |
| 4665 | + | ] | |
| 4666 | + | ||
| 4667 | + | [[package]] | |
| 4668 | + | name = "wit-bindgen" | |
| 4669 | + | version = "0.57.1" | |
| 4670 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4671 | + | checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" | |
| 4672 | + | ||
| 4673 | + | [[package]] | |
| 4674 | + | name = "wit-bindgen-core" | |
| 4675 | + | version = "0.51.0" | |
| 4676 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4677 | + | checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" | |
| 4678 | + | dependencies = [ | |
| 4679 | + | "anyhow", | |
| 4680 | + | "heck", | |
| 4681 | + | "wit-parser", | |
| 4682 | + | ] | |
| 4683 | + | ||
| 4684 | + | [[package]] | |
| 4685 | + | name = "wit-bindgen-rust" | |
| 4686 | + | version = "0.51.0" | |
| 4687 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4688 | + | checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" | |
| 4689 | + | dependencies = [ | |
| 4690 | + | "anyhow", | |
| 4691 | + | "heck", | |
| 4692 | + | "indexmap", | |
| 4693 | + | "prettyplease", | |
| 4694 | + | "syn", | |
| 4695 | + | "wasm-metadata", | |
| 4696 | + | "wit-bindgen-core", | |
| 4697 | + | "wit-component", | |
| 4698 | + | ] | |
| 4699 | + | ||
| 4700 | + | [[package]] | |
| 4701 | + | name = "wit-bindgen-rust-macro" | |
| 4702 | + | version = "0.51.0" | |
| 4703 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4704 | + | checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" | |
| 4705 | + | dependencies = [ | |
| 4706 | + | "anyhow", | |
| 4707 | + | "prettyplease", | |
| 4708 | + | "proc-macro2", | |
| 4709 | + | "quote", | |
| 4710 | + | "syn", | |
| 4711 | + | "wit-bindgen-core", | |
| 4712 | + | "wit-bindgen-rust", | |
| 4713 | + | ] | |
| 4714 | + | ||
| 4715 | + | [[package]] | |
| 4716 | + | name = "wit-component" | |
| 4717 | + | version = "0.244.0" | |
| 4718 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4719 | + | checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" | |
| 4720 | + | dependencies = [ | |
| 4721 | + | "anyhow", | |
| 4722 | + | "bitflags", | |
| 4723 | + | "indexmap", | |
| 4724 | + | "log", | |
| 4725 | + | "serde", | |
| 4726 | + | "serde_derive", | |
| 4727 | + | "serde_json", | |
| 4728 | + | "wasm-encoder", | |
| 4729 | + | "wasm-metadata", | |
| 4730 | + | "wasmparser", | |
| 4731 | + | "wit-parser", | |
| 4732 | + | ] | |
| 4733 | + | ||
| 4734 | + | [[package]] | |
| 4735 | + | name = "wit-parser" | |
| 4736 | + | version = "0.244.0" | |
| 4737 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4738 | + | checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" | |
| 4739 | + | dependencies = [ | |
| 4740 | + | "anyhow", | |
| 4741 | + | "id-arena", | |
| 4742 | + | "indexmap", | |
| 4743 | + | "log", | |
| 4744 | + | "semver", | |
| 4745 | + | "serde", | |
| 4746 | + | "serde_derive", | |
| 4747 | + | "serde_json", | |
| 4748 | + | "unicode-xid", | |
| 4749 | + | "wasmparser", | |
| 4750 | + | ] | |
| 4751 | + | ||
| 4752 | + | [[package]] | |
| 4753 | + | name = "writeable" | |
| 4754 | + | version = "0.6.3" | |
| 4755 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4756 | + | checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" | |
| 4757 | + | ||
| 4758 | + | [[package]] | |
| 4759 | + | name = "yaml-rust" | |
| 4760 | + | version = "0.4.5" | |
| 4761 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4762 | + | checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" | |
| 4763 | + | dependencies = [ | |
| 4764 | + | "linked-hash-map", | |
| 4765 | + | ] | |
| 4766 | + | ||
| 4767 | + | [[package]] | |
| 4768 | + | name = "yoke" | |
| 4769 | + | version = "0.8.3" | |
| 4770 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4771 | + | checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" | |
| 4772 | + | dependencies = [ | |
| 4773 | + | "stable_deref_trait", | |
| 4774 | + | "yoke-derive", | |
| 4775 | + | "zerofrom", | |
| 4776 | + | ] | |
| 4777 | + | ||
| 4778 | + | [[package]] | |
| 4779 | + | name = "yoke-derive" | |
| 4780 | + | version = "0.8.2" | |
| 4781 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4782 | + | checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" | |
| 4783 | + | dependencies = [ | |
| 4784 | + | "proc-macro2", | |
| 4785 | + | "quote", | |
| 4786 | + | "syn", | |
| 4787 | + | "synstructure", | |
| 4788 | + | ] | |
| 4789 | + | ||
| 4790 | + | [[package]] | |
| 4791 | + | name = "zerofrom" | |
| 4792 | + | version = "0.1.8" | |
| 4793 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4794 | + | checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" | |
| 4795 | + | dependencies = [ | |
| 4796 | + | "zerofrom-derive", | |
| 4797 | + | ] | |
| 4798 | + | ||
| 4799 | + | [[package]] | |
| 4800 | + | name = "zerofrom-derive" | |
| 4801 | + | version = "0.1.7" | |
| 4802 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4803 | + | checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" | |
| 4804 | + | dependencies = [ | |
| 4805 | + | "proc-macro2", | |
| 4806 | + | "quote", | |
| 4807 | + | "syn", | |
| 4808 | + | "synstructure", | |
| 4809 | + | ] | |
| 4810 | + | ||
| 4811 | + | [[package]] | |
| 4812 | + | name = "zeroize" | |
| 4813 | + | version = "1.8.2" | |
| 4814 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4815 | + | checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" | |
| 4816 | + | ||
| 4817 | + | [[package]] | |
| 4818 | + | name = "zerotrie" | |
| 4819 | + | version = "0.2.4" | |
| 4820 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4821 | + | checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" | |
| 4822 | + | dependencies = [ | |
| 4823 | + | "displaydoc", | |
| 4824 | + | "yoke", | |
| 4825 | + | "zerofrom", | |
| 4826 | + | ] | |
| 4827 | + | ||
| 4828 | + | [[package]] | |
| 4829 | + | name = "zerovec" | |
| 4830 | + | version = "0.11.6" | |
| 4831 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4832 | + | checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" | |
| 4833 | + | dependencies = [ | |
| 4834 | + | "yoke", | |
| 4835 | + | "zerofrom", | |
| 4836 | + | "zerovec-derive", | |
| 4837 | + | ] | |
| 4838 | + | ||
| 4839 | + | [[package]] | |
| 4840 | + | name = "zerovec-derive" | |
| 4841 | + | version = "0.11.3" | |
| 4842 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4843 | + | checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" | |
| 4844 | + | dependencies = [ | |
| 4845 | + | "proc-macro2", | |
| 4846 | + | "quote", | |
| 4847 | + | "syn", | |
| 4848 | + | ] | |
| 4849 | + | ||
| 4850 | + | [[package]] | |
| 4851 | + | name = "zlib-rs" | |
| 4852 | + | version = "0.6.3" | |
| 4853 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4854 | + | checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513" | |
| 4855 | + | ||
| 4856 | + | [[package]] | |
| 4857 | + | name = "zmij" | |
| 4858 | + | version = "1.0.21" | |
| 4859 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4860 | + | checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" |
addedCargo.toml+51 −0
| 1 | + | [workspace] | |
| 2 | + | resolver = "2" | |
| 3 | + | members = [ | |
| 4 | + | "crates/*", | |
| 5 | + | "vendor/gitserver-core", | |
| 6 | + | ] | |
| 7 | + | ||
| 8 | + | [workspace.package] | |
| 9 | + | version = "0.0.0" | |
| 10 | + | edition = "2024" | |
| 11 | + | license = "MIT OR Apache-2.0" | |
| 12 | + | repository = "https://github.com/richardscollin/anvil" | |
| 13 | + | rust-version = "1.95" | |
| 14 | + | ||
| 15 | + | [workspace.dependencies] | |
| 16 | + | # Internal crates | |
| 17 | + | anvil-core = { path = "crates/anvil-core" } | |
| 18 | + | anvil-git = { path = "crates/anvil-git" } | |
| 19 | + | anvil-web = { path = "crates/anvil-web" } | |
| 20 | + | anvil-ssh = { path = "crates/anvil-ssh" } | |
| 21 | + | ||
| 22 | + | anyhow = "1" | |
| 23 | + | argon2 = { version = "0.5", features = ["std"] } | |
| 24 | + | axum = "0.8" | |
| 25 | + | axum-extra = { version = "0.10", features = ["cookie"] } | |
| 26 | + | base64 = "0.22" | |
| 27 | + | clap = { version = "4", features = ["derive"] } | |
| 28 | + | gitserver-core = { path = "vendor/gitserver-core" } | |
| 29 | + | gix = { version = "0.84", default-features = false, features = ["sha1", "max-performance-safe", "blob-diff", "revision"] } | |
| 30 | + | gix-pack = { version = "0.71", features = ["sha1"] } | |
| 31 | + | maud = { version = "0.27", features = ["axum"] } | |
| 32 | + | rand = "0.10" | |
| 33 | + | serde = { version = "1", features = ["derive"] } | |
| 34 | + | similar = "2" | |
| 35 | + | syntect = { version = "5", default-features = false, features = ["default-fancy"] } | |
| 36 | + | thiserror = "2" | |
| 37 | + | time = { version = "0.3", features = ["serde", "formatting"] } | |
| 38 | + | toasty = { version = "0.7", features = ["sqlite"] } | |
| 39 | + | tokio = { version = "1", features = ["full"] } | |
| 40 | + | tokio-util = { version = "0.7", features = ["io"] } | |
| 41 | + | toml = "0.8" | |
| 42 | + | tower = "0.5" | |
| 43 | + | tower-http = { version = "0.6", features = ["trace", "fs"] } | |
| 44 | + | tracing = "0.1" | |
| 45 | + | tracing-subscriber = { version = "0.3", features = ["env-filter"] } | |
| 46 | + | ||
| 47 | + | # ssh | |
| 48 | + | russh = "0.61" | |
| 49 | + | # ssh-key parsing/fingerprinting, shared by anvil-core (storage) and anvil-ssh | |
| 50 | + | # (auth). Pinned to match russh's transitive ssh-key so fingerprints agree. | |
| 51 | + | ssh-key = "0.7.0-rc.10" |
addedREADME.md+41 −0
| 1 | + | # anvil | |
| 2 | + | ||
| 3 | + | A minimal self-hosted git forge in Rust, built on [gix (gitoxide)](https://github.com/GitoxideLabs/gitoxide). | |
| 4 | + | Modeled on Gitea's *basic* feature set: repository hosting, web code browsing, | |
| 5 | + | the git smart protocols (HTTP and SSH), issues, pull requests, users/orgs with | |
| 6 | + | access control, and webhooks. No wiki, no CI/CD. | |
| 7 | + | ||
| 8 | + | ## Status | |
| 9 | + | ||
| 10 | + | Early development. Milestone 1 (workspace scaffold + storage) is in place: | |
| 11 | + | `anvild` can initialize its database and create users and bare repositories on | |
| 12 | + | disk. The git wire protocol, web UI, and SSH transport follow. | |
| 13 | + | ||
| 14 | + | ## Architecture | |
| 15 | + | ||
| 16 | + | A Cargo workspace. The keystone is **`anvil-git`**: gix is a *client* library | |
| 17 | + | with no server-side protocol, so anvil supplies its own transport-agnostic | |
| 18 | + | `upload-pack`/`receive-pack` (smart-HTTP and SSH share one implementation). | |
| 19 | + | ||
| 20 | + | | Crate | Responsibility | | |
| 21 | + | |--------------|----------------| | |
| 22 | + | | `anvil-core` | Domain model, SQLite persistence, on-disk bare-repo storage | | |
| 23 | + | | `anvil-git` | Server-side git wire protocol on gix (upstream-candidate) | | |
| 24 | + | | `anvil-web` | axum HTTP server: web UI + smart-HTTP git endpoints | | |
| 25 | + | | `anvil-ssh` | git-over-SSH (russh) | | |
| 26 | + | | `anvil` | `anvild` daemon + admin CLI | | |
| 27 | + | ||
| 28 | + | ## Quick start | |
| 29 | + | ||
| 30 | + | ```sh | |
| 31 | + | cargo run -- migrate # create data/ + database | |
| 32 | + | cargo run -- user create alice --password secret # create a user | |
| 33 | + | cargo run -- repo create alice/hello # create a bare repo on disk | |
| 34 | + | cargo run -- serve # start the server | |
| 35 | + | ``` | |
| 36 | + | ||
| 37 | + | Configuration is optional; see [`anvil.example.toml`](anvil.example.toml). | |
| 38 | + | ||
| 39 | + | ## License | |
| 40 | + | ||
| 41 | + | MIT OR Apache-2.0. |
addedTODO.md+6 −0
| 1 | + | - [ ] don't expose a users email on their profile page | |
| 2 | + | - [ ] implement github action style CI feature | |
| 3 | + | - I want to get this to a sufficient state that we could bootstrap this app using this CI and automate deployments if we wished | |
| 4 | + | - probably might want to have other isolated anvil workers or something running for CI jobs | |
| 5 | + | - [ ] implement github pages style hosting feature | |
| 6 | + | - [ ] security audit |
addedanvil.example.toml+19 −0
| 1 | + | # anvil configuration. Copy to `anvil.toml` and edit. All fields are optional; | |
| 2 | + | # omitted fields fall back to the defaults shown here. | |
| 3 | + | ||
| 4 | + | # Root directory for all server state (database + repositories). | |
| 5 | + | data_dir = "data" | |
| 6 | + | ||
| 7 | + | [http] | |
| 8 | + | listen = "127.0.0.1:3000" | |
| 9 | + | base_url = "http://localhost:3000" | |
| 10 | + | ||
| 11 | + | [ssh] | |
| 12 | + | enabled = false | |
| 13 | + | # Internal bind address. Under Docker, set host = "0.0.0.0" and forward the port. | |
| 14 | + | listen = "127.0.0.1:2222" | |
| 15 | + | # What to show users in SSH clone URLs. Set clone_port to the externally | |
| 16 | + | # forwarded port if it differs from the internal bind (e.g. Docker -p 2200:2222). | |
| 17 | + | clone_host = "localhost" | |
| 18 | + | clone_port = 2222 | |
| 19 | + | clone_user = "git" |
addedcrates/anvil-core/Cargo.toml+22 −0
| 1 | + | [package] | |
| 2 | + | name = "anvil-core" | |
| 3 | + | version.workspace = true | |
| 4 | + | edition.workspace = true | |
| 5 | + | license.workspace = true | |
| 6 | + | repository.workspace = true | |
| 7 | + | rust-version.workspace = true | |
| 8 | + | description = "Domain model, persistence, and on-disk repository storage for the anvil git forge." | |
| 9 | + | ||
| 10 | + | [dependencies] | |
| 11 | + | gix.workspace = true | |
| 12 | + | toasty.workspace = true | |
| 13 | + | argon2.workspace = true | |
| 14 | + | ssh-key.workspace = true | |
| 15 | + | serde.workspace = true | |
| 16 | + | toml.workspace = true | |
| 17 | + | thiserror.workspace = true | |
| 18 | + | tracing.workspace = true | |
| 19 | + | ||
| 20 | + | [dev-dependencies] | |
| 21 | + | tokio = { workspace = true } | |
| 22 | + | tempfile = "3" |
addedcrates/anvil-core/src/access.rs+23 −0
| 1 | + | //! Authorization rules for repositories. | |
| 2 | + | //! | |
| 3 | + | //! The model is deliberately simple, matching a personal / small-team forge: | |
| 4 | + | //! a repo is owned by one user; private repos are visible only to their owner; | |
| 5 | + | //! writes (push) and settings changes are owner-only. Admins may do anything. | |
| 6 | + | ||
| 7 | + | use crate::models::{ | |
| 8 | + | Repository, | |
| 9 | + | User, | |
| 10 | + | }; | |
| 11 | + | ||
| 12 | + | /// Whether `viewer` may read (browse / clone) `repo`. | |
| 13 | + | pub fn can_read(repo: &Repository, viewer: Option<&User>) -> bool { | |
| 14 | + | !repo.is_private || can_write(repo, viewer) | |
| 15 | + | } | |
| 16 | + | ||
| 17 | + | /// Whether `viewer` may write (push) to / administer `repo`. | |
| 18 | + | pub fn can_write(repo: &Repository, viewer: Option<&User>) -> bool { | |
| 19 | + | match viewer { | |
| 20 | + | Some(user) => user.id == repo.owner_id || user.is_admin, | |
| 21 | + | None => false, | |
| 22 | + | } | |
| 23 | + | } |
addedcrates/anvil-core/src/config.rs+145 −0
| 1 | + | //! Server configuration: loaded from a TOML file with sensible defaults. | |
| 2 | + | ||
| 3 | + | use std::path::{ | |
| 4 | + | Path, | |
| 5 | + | PathBuf, | |
| 6 | + | }; | |
| 7 | + | ||
| 8 | + | use serde::{ | |
| 9 | + | Deserialize, | |
| 10 | + | Serialize, | |
| 11 | + | }; | |
| 12 | + | ||
| 13 | + | use crate::error::{ | |
| 14 | + | Error, | |
| 15 | + | Result, | |
| 16 | + | }; | |
| 17 | + | ||
| 18 | + | /// Top-level anvil configuration. | |
| 19 | + | /// | |
| 20 | + | /// Load with [`Config::load`] (from a TOML file) or [`Config::default`]. | |
| 21 | + | #[derive(Debug, Clone, Serialize, Deserialize)] | |
| 22 | + | #[serde(default)] | |
| 23 | + | pub struct Config { | |
| 24 | + | /// Root directory holding all server state (database + repositories). | |
| 25 | + | pub data_dir: PathBuf, | |
| 26 | + | /// HTTP server settings. | |
| 27 | + | pub http: HttpConfig, | |
| 28 | + | /// SSH server settings. | |
| 29 | + | pub ssh: SshConfig, | |
| 30 | + | } | |
| 31 | + | ||
| 32 | + | #[derive(Debug, Clone, Serialize, Deserialize)] | |
| 33 | + | #[serde(default)] | |
| 34 | + | pub struct HttpConfig { | |
| 35 | + | /// Address the HTTP server binds to, e.g. `127.0.0.1:3000`. | |
| 36 | + | pub listen: String, | |
| 37 | + | /// Externally visible base URL, used when constructing clone URLs. | |
| 38 | + | pub base_url: String, | |
| 39 | + | } | |
| 40 | + | ||
| 41 | + | #[derive(Debug, Clone, Serialize, Deserialize)] | |
| 42 | + | #[serde(default)] | |
| 43 | + | pub struct SshConfig { | |
| 44 | + | /// Whether the SSH git transport is enabled. | |
| 45 | + | pub enabled: bool, | |
| 46 | + | /// Address the SSH server binds to internally, e.g. `0.0.0.0:2222`. Under | |
| 47 | + | /// Docker this is the in-container bind, which may differ from the | |
| 48 | + | /// externally forwarded port — see the `clone_*` fields below. | |
| 49 | + | pub listen: String, | |
| 50 | + | /// Hostname shown in SSH clone URLs (what users actually connect to). | |
| 51 | + | pub clone_host: String, | |
| 52 | + | /// Port shown in SSH clone URLs. Set this to the *externally forwarded* | |
| 53 | + | /// port when it differs from the internal bind (e.g. Docker `-p 2200:2222`). | |
| 54 | + | pub clone_port: u16, | |
| 55 | + | /// Username shown in SSH clone URLs (conventionally `git`). | |
| 56 | + | pub clone_user: String, | |
| 57 | + | } | |
| 58 | + | ||
| 59 | + | impl Default for Config { | |
| 60 | + | fn default() -> Self { | |
| 61 | + | Self { | |
| 62 | + | data_dir: PathBuf::from("data"), | |
| 63 | + | http: HttpConfig::default(), | |
| 64 | + | ssh: SshConfig::default(), | |
| 65 | + | } | |
| 66 | + | } | |
| 67 | + | } | |
| 68 | + | ||
| 69 | + | impl Default for HttpConfig { | |
| 70 | + | fn default() -> Self { | |
| 71 | + | Self { | |
| 72 | + | listen: "127.0.0.1:3000".to_string(), | |
| 73 | + | base_url: "http://localhost:3000".to_string(), | |
| 74 | + | } | |
| 75 | + | } | |
| 76 | + | } | |
| 77 | + | ||
| 78 | + | impl Default for SshConfig { | |
| 79 | + | fn default() -> Self { | |
| 80 | + | Self { | |
| 81 | + | enabled: false, | |
| 82 | + | listen: "127.0.0.1:2222".to_string(), | |
| 83 | + | clone_host: "localhost".to_string(), | |
| 84 | + | clone_port: 2222, | |
| 85 | + | clone_user: "git".to_string(), | |
| 86 | + | } | |
| 87 | + | } | |
| 88 | + | } | |
| 89 | + | ||
| 90 | + | impl Config { | |
| 91 | + | /// Load configuration from a TOML file. Missing fields fall back to defaults. | |
| 92 | + | pub fn load(path: impl AsRef<Path>) -> Result<Self> { | |
| 93 | + | let path = path.as_ref(); | |
| 94 | + | let text = std::fs::read_to_string(path) | |
| 95 | + | .map_err(|e| Error::Config(format!("reading {}: {e}", path.display())))?; | |
| 96 | + | toml::from_str(&text).map_err(|e| Error::Config(format!("parsing {}: {e}", path.display()))) | |
| 97 | + | } | |
| 98 | + | ||
| 99 | + | /// Load from `path` if it exists, otherwise return defaults. | |
| 100 | + | pub fn load_or_default(path: impl AsRef<Path>) -> Result<Self> { | |
| 101 | + | let path = path.as_ref(); | |
| 102 | + | if path.exists() { | |
| 103 | + | Self::load(path) | |
| 104 | + | } else { | |
| 105 | + | Ok(Self::default()) | |
| 106 | + | } | |
| 107 | + | } | |
| 108 | + | ||
| 109 | + | /// Filesystem path to the SQLite database file. | |
| 110 | + | pub fn database_path(&self) -> PathBuf { | |
| 111 | + | self.data_dir.join("anvil.db") | |
| 112 | + | } | |
| 113 | + | ||
| 114 | + | /// Root directory under which bare repositories are stored. | |
| 115 | + | pub fn repositories_dir(&self) -> PathBuf { | |
| 116 | + | self.data_dir.join("repositories") | |
| 117 | + | } | |
| 118 | + | ||
| 119 | + | /// The HTTP clone URL for `<owner>/<name>`, e.g. | |
| 120 | + | /// `http://localhost:3000/alice/hello.git`. | |
| 121 | + | pub fn http_clone_url(&self, owner: &str, name: &str) -> String { | |
| 122 | + | format!( | |
| 123 | + | "{}/{owner}/{name}.git", | |
| 124 | + | self.http.base_url.trim_end_matches('/') | |
| 125 | + | ) | |
| 126 | + | } | |
| 127 | + | ||
| 128 | + | /// The SSH clone URL for `<owner>/<name>`, using the externally advertised | |
| 129 | + | /// host/port/user (which may differ from the internal bind under Docker). | |
| 130 | + | /// The port is omitted when it is the SSH default (22). | |
| 131 | + | pub fn ssh_clone_url(&self, owner: &str, name: &str) -> String { | |
| 132 | + | let ssh = &self.ssh; | |
| 133 | + | if ssh.clone_port == 22 { | |
| 134 | + | format!( | |
| 135 | + | "ssh://{}@{}/{owner}/{name}.git", | |
| 136 | + | ssh.clone_user, ssh.clone_host | |
| 137 | + | ) | |
| 138 | + | } else { | |
| 139 | + | format!( | |
| 140 | + | "ssh://{}@{}:{}/{owner}/{name}.git", | |
| 141 | + | ssh.clone_user, ssh.clone_host, ssh.clone_port | |
| 142 | + | ) | |
| 143 | + | } | |
| 144 | + | } | |
| 145 | + | } |
addedcrates/anvil-core/src/db.rs+36 −0
| 1 | + | //! Database connection and schema setup via Toasty. | |
| 2 | + | ||
| 3 | + | use std::path::Path; | |
| 4 | + | ||
| 5 | + | use crate::error::Result; | |
| 6 | + | use crate::models::{ | |
| 7 | + | Repository, | |
| 8 | + | Session, | |
| 9 | + | SshKey, | |
| 10 | + | User, | |
| 11 | + | }; | |
| 12 | + | ||
| 13 | + | /// Open (creating if necessary) the SQLite database at `path`, creating the | |
| 14 | + | /// schema on first run. | |
| 15 | + | /// | |
| 16 | + | /// Note: Toasty 0.7's `push_schema` emits plain `CREATE TABLE` (no | |
| 17 | + | /// `IF NOT EXISTS`) for model tables, so it is *not* idempotent — calling it on | |
| 18 | + | /// an existing database errors. We therefore only push the schema when the | |
| 19 | + | /// database file does not yet exist. Skipping it on an existing database issues | |
| 20 | + | /// no DDL, so data is never touched. Schema evolution will move to Toasty's | |
| 21 | + | /// migration system when models start changing. | |
| 22 | + | pub async fn connect(path: impl AsRef<Path>) -> Result<toasty::Db> { | |
| 23 | + | let path = path.as_ref(); | |
| 24 | + | let fresh = !path.exists(); | |
| 25 | + | let url = format!("sqlite:{}", path.display()); | |
| 26 | + | ||
| 27 | + | let db = toasty::Db::builder() | |
| 28 | + | .models(toasty::models!(User, Repository, SshKey, Session)) | |
| 29 | + | .connect(&url) | |
| 30 | + | .await?; | |
| 31 | + | ||
| 32 | + | if fresh { | |
| 33 | + | db.push_schema().await?; | |
| 34 | + | } | |
| 35 | + | Ok(db) | |
| 36 | + | } |
addedcrates/anvil-core/src/error.rs+32 −0
| 1 | + | //! Crate-wide error type. | |
| 2 | + | ||
| 3 | + | /// Errors produced by the core domain and storage layers. | |
| 4 | + | #[derive(Debug, thiserror::Error)] | |
| 5 | + | pub enum Error { | |
| 6 | + | #[error("database error: {0}")] | |
| 7 | + | Db(#[from] toasty::Error), | |
| 8 | + | ||
| 9 | + | #[error("repository storage error: {0}")] | |
| 10 | + | Storage(String), | |
| 11 | + | ||
| 12 | + | #[error("password hashing error: {0}")] | |
| 13 | + | Password(String), | |
| 14 | + | ||
| 15 | + | #[error("config error: {0}")] | |
| 16 | + | Config(String), | |
| 17 | + | ||
| 18 | + | #[error("not found: {0}")] | |
| 19 | + | NotFound(String), | |
| 20 | + | ||
| 21 | + | #[error("already exists: {0}")] | |
| 22 | + | AlreadyExists(String), | |
| 23 | + | ||
| 24 | + | #[error("invalid input: {0}")] | |
| 25 | + | Invalid(String), | |
| 26 | + | ||
| 27 | + | #[error(transparent)] | |
| 28 | + | Io(#[from] std::io::Error), | |
| 29 | + | } | |
| 30 | + | ||
| 31 | + | /// Convenience alias used throughout the crate. | |
| 32 | + | pub type Result<T> = std::result::Result<T, Error>; |
addedcrates/anvil-core/src/lib.rs+60 −0
| 1 | + | //! Core domain model, persistence, and on-disk repository storage for anvil. | |
| 2 | + | //! | |
| 3 | + | //! This crate is transport-agnostic: it knows about users, repositories, the | |
| 4 | + | //! SQLite database, and bare git repositories on disk, but nothing about HTTP, | |
| 5 | + | //! SSH, or the git wire protocol. Higher layers (`anvil-web`, `anvil-ssh`, | |
| 6 | + | //! `anvil-git`) build on top of it. | |
| 7 | + | ||
| 8 | + | pub mod access; | |
| 9 | + | pub mod config; | |
| 10 | + | pub mod db; | |
| 11 | + | pub mod error; | |
| 12 | + | pub mod models; | |
| 13 | + | pub mod repos; | |
| 14 | + | pub mod sessions; | |
| 15 | + | pub mod ssh_keys; | |
| 16 | + | pub mod storage; | |
| 17 | + | pub mod users; | |
| 18 | + | ||
| 19 | + | pub use config::Config; | |
| 20 | + | pub use error::{ | |
| 21 | + | Error, | |
| 22 | + | Result, | |
| 23 | + | }; | |
| 24 | + | pub use models::{ | |
| 25 | + | Repository, | |
| 26 | + | Session, | |
| 27 | + | SshKey, | |
| 28 | + | User, | |
| 29 | + | }; | |
| 30 | + | ||
| 31 | + | /// Current Unix time in seconds, for `created_at` columns. | |
| 32 | + | pub(crate) fn now() -> i64 { | |
| 33 | + | std::time::SystemTime::now() | |
| 34 | + | .duration_since(std::time::UNIX_EPOCH) | |
| 35 | + | .map(|d| d.as_secs() as i64) | |
| 36 | + | .unwrap_or(0) | |
| 37 | + | } | |
| 38 | + | ||
| 39 | + | /// Shared application state: configuration plus a database handle. | |
| 40 | + | /// | |
| 41 | + | /// Cloneable and cheap to pass around — `toasty::Db` is internally reference | |
| 42 | + | /// counted and backed by a connection pool. | |
| 43 | + | #[derive(Clone)] | |
| 44 | + | pub struct App { | |
| 45 | + | pub config: Config, | |
| 46 | + | pub db: toasty::Db, | |
| 47 | + | } | |
| 48 | + | ||
| 49 | + | impl App { | |
| 50 | + | /// Initialize application state from a config: ensure the data directories | |
| 51 | + | /// exist, then open the database and create the schema. | |
| 52 | + | pub async fn bootstrap(config: Config) -> Result<Self> { | |
| 53 | + | std::fs::create_dir_all(&config.data_dir)?; | |
| 54 | + | std::fs::create_dir_all(config.repositories_dir())?; | |
| 55 | + | ||
| 56 | + | let db = db::connect(config.database_path()).await?; | |
| 57 | + | ||
| 58 | + | Ok(Self { config, db }) | |
| 59 | + | } | |
| 60 | + | } |
addedcrates/anvil-core/src/models.rs+67 −0
| 1 | + | //! Persisted domain types, defined as Toasty models. Tables are created from | |
| 2 | + | //! these definitions via [`crate::db`]'s `push_schema`. | |
| 3 | + | //! | |
| 4 | + | //! Foreign keys are kept as plain scalar fields (`owner_id`, `user_id`) and | |
| 5 | + | //! queried explicitly, rather than declaring Toasty relations — simpler and a | |
| 6 | + | //! good fit for our small schema. | |
| 7 | + | ||
| 8 | + | /// A registered user account. | |
| 9 | + | #[derive(Debug, toasty::Model)] | |
| 10 | + | pub struct User { | |
| 11 | + | #[key] | |
| 12 | + | #[auto] | |
| 13 | + | pub id: i64, | |
| 14 | + | #[unique] | |
| 15 | + | pub username: String, | |
| 16 | + | pub email: String, | |
| 17 | + | /// Argon2 PHC-format password hash. | |
| 18 | + | pub password_hash: String, | |
| 19 | + | pub is_admin: bool, | |
| 20 | + | /// Unix timestamp (seconds) of account creation. | |
| 21 | + | pub created_at: i64, | |
| 22 | + | } | |
| 23 | + | ||
| 24 | + | /// A hosted repository, owned by a [`User`]. | |
| 25 | + | #[derive(Debug, toasty::Model)] | |
| 26 | + | pub struct Repository { | |
| 27 | + | #[key] | |
| 28 | + | #[auto] | |
| 29 | + | pub id: i64, | |
| 30 | + | #[index] | |
| 31 | + | pub owner_id: i64, | |
| 32 | + | pub name: String, | |
| 33 | + | pub description: String, | |
| 34 | + | pub is_private: bool, | |
| 35 | + | /// Short name of the default branch, e.g. `main`. | |
| 36 | + | pub default_branch: String, | |
| 37 | + | pub created_at: i64, | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | /// A web login session, keyed by an opaque random token stored in a cookie. | |
| 41 | + | #[derive(Debug, toasty::Model)] | |
| 42 | + | pub struct Session { | |
| 43 | + | #[key] | |
| 44 | + | pub token: String, | |
| 45 | + | #[index] | |
| 46 | + | pub user_id: i64, | |
| 47 | + | pub created_at: i64, | |
| 48 | + | /// Unix timestamp (seconds) after which the session is invalid. | |
| 49 | + | pub expires_at: i64, | |
| 50 | + | } | |
| 51 | + | ||
| 52 | + | /// A registered SSH public key, used to authenticate git-over-SSH connections. | |
| 53 | + | #[derive(Debug, toasty::Model)] | |
| 54 | + | pub struct SshKey { | |
| 55 | + | #[key] | |
| 56 | + | #[auto] | |
| 57 | + | pub id: i64, | |
| 58 | + | #[index] | |
| 59 | + | pub user_id: i64, | |
| 60 | + | pub title: String, | |
| 61 | + | /// Canonical SHA256 fingerprint, e.g. `SHA256:…`. | |
| 62 | + | #[unique] | |
| 63 | + | pub fingerprint: String, | |
| 64 | + | /// Normalized OpenSSH public-key line. | |
| 65 | + | pub content: String, | |
| 66 | + | pub created_at: i64, | |
| 67 | + | } |
addedcrates/anvil-core/src/repos.rs+156 −0
| 1 | + | //! Repository records: creation (DB row + on-disk bare repo) and lookup. | |
| 2 | + | ||
| 3 | + | use std::path::Path; | |
| 4 | + | ||
| 5 | + | use crate::error::{ | |
| 6 | + | Error, | |
| 7 | + | Result, | |
| 8 | + | }; | |
| 9 | + | use crate::models::{ | |
| 10 | + | Repository, | |
| 11 | + | User, | |
| 12 | + | }; | |
| 13 | + | use crate::storage; | |
| 14 | + | ||
| 15 | + | /// A repository joined with its owner's username, for listing pages. | |
| 16 | + | pub struct RepoWithOwner { | |
| 17 | + | pub owner_id: i64, | |
| 18 | + | pub owner: String, | |
| 19 | + | pub name: String, | |
| 20 | + | pub description: String, | |
| 21 | + | pub is_private: bool, | |
| 22 | + | pub default_branch: String, | |
| 23 | + | } | |
| 24 | + | ||
| 25 | + | /// Create a repository owned by `owner`: insert the database row and initialize | |
| 26 | + | /// the bare repository on disk. If the on-disk init fails, the row is rolled | |
| 27 | + | /// back (deleted) so no orphan remains. | |
| 28 | + | pub async fn create( | |
| 29 | + | db: &toasty::Db, | |
| 30 | + | repositories_dir: &Path, | |
| 31 | + | owner: &User, | |
| 32 | + | name: &str, | |
| 33 | + | description: &str, | |
| 34 | + | is_private: bool, | |
| 35 | + | ) -> Result<Repository> { | |
| 36 | + | validate_name(name)?; | |
| 37 | + | if find(db, owner.id, name).await?.is_some() { | |
| 38 | + | return Err(Error::AlreadyExists(format!( | |
| 39 | + | "repository {}/{name}", | |
| 40 | + | owner.username | |
| 41 | + | ))); | |
| 42 | + | } | |
| 43 | + | ||
| 44 | + | let mut conn = db.clone(); | |
| 45 | + | let repo = toasty::create!(Repository { | |
| 46 | + | owner_id: owner.id, | |
| 47 | + | name: name, | |
| 48 | + | description: description, | |
| 49 | + | is_private: is_private, | |
| 50 | + | default_branch: "main", | |
| 51 | + | created_at: crate::now(), | |
| 52 | + | }) | |
| 53 | + | .exec(&mut conn) | |
| 54 | + | .await?; | |
| 55 | + | ||
| 56 | + | // Create the bare repo on disk; on failure, remove the row we just wrote. | |
| 57 | + | if let Err(e) = storage::create_bare( | |
| 58 | + | repositories_dir, | |
| 59 | + | &owner.username, | |
| 60 | + | name, | |
| 61 | + | &repo.default_branch, | |
| 62 | + | ) { | |
| 63 | + | let _ = repo.delete().exec(&mut conn).await; | |
| 64 | + | return Err(e); | |
| 65 | + | } | |
| 66 | + | Ok(repo) | |
| 67 | + | } | |
| 68 | + | ||
| 69 | + | /// Find a repository by its id. | |
| 70 | + | pub async fn find_by_id(db: &toasty::Db, id: i64) -> Result<Option<Repository>> { | |
| 71 | + | let mut db = db.clone(); | |
| 72 | + | let repo = Repository::filter(Repository::fields().id().eq(id)) | |
| 73 | + | .first() | |
| 74 | + | .exec(&mut db) | |
| 75 | + | .await?; | |
| 76 | + | Ok(repo) | |
| 77 | + | } | |
| 78 | + | ||
| 79 | + | /// Update a repository's description and visibility. | |
| 80 | + | pub async fn update_settings( | |
| 81 | + | db: &toasty::Db, | |
| 82 | + | repo_id: i64, | |
| 83 | + | description: &str, | |
| 84 | + | is_private: bool, | |
| 85 | + | ) -> Result<()> { | |
| 86 | + | let mut conn = db.clone(); | |
| 87 | + | let Some(mut repo) = Repository::filter(Repository::fields().id().eq(repo_id)) | |
| 88 | + | .first() | |
| 89 | + | .exec(&mut conn) | |
| 90 | + | .await? | |
| 91 | + | else { | |
| 92 | + | return Err(Error::NotFound(format!("repository id {repo_id}"))); | |
| 93 | + | }; | |
| 94 | + | let mut conn = db.clone(); | |
| 95 | + | repo.update() | |
| 96 | + | .description(description) | |
| 97 | + | .is_private(is_private) | |
| 98 | + | .exec(&mut conn) | |
| 99 | + | .await?; | |
| 100 | + | Ok(()) | |
| 101 | + | } | |
| 102 | + | ||
| 103 | + | /// Find a repository by owner id and name. | |
| 104 | + | pub async fn find(db: &toasty::Db, owner_id: i64, name: &str) -> Result<Option<Repository>> { | |
| 105 | + | let mut db = db.clone(); | |
| 106 | + | let repo = Repository::filter(Repository::fields().owner_id().eq(owner_id)) | |
| 107 | + | .filter(Repository::fields().name().eq(name)) | |
| 108 | + | .first() | |
| 109 | + | .exec(&mut db) | |
| 110 | + | .await?; | |
| 111 | + | Ok(repo) | |
| 112 | + | } | |
| 113 | + | ||
| 114 | + | /// List all repositories owned by `owner_id`. | |
| 115 | + | pub async fn list_by_owner(db: &toasty::Db, owner_id: i64) -> Result<Vec<Repository>> { | |
| 116 | + | let mut db = db.clone(); | |
| 117 | + | let repos = Repository::filter(Repository::fields().owner_id().eq(owner_id)) | |
| 118 | + | .exec(&mut db) | |
| 119 | + | .await?; | |
| 120 | + | Ok(repos) | |
| 121 | + | } | |
| 122 | + | ||
| 123 | + | /// List all repositories with their owner usernames, ordered by owner then name. | |
| 124 | + | pub async fn list_all_with_owner(db: &toasty::Db) -> Result<Vec<RepoWithOwner>> { | |
| 125 | + | let mut conn = db.clone(); | |
| 126 | + | let users = User::all().exec(&mut conn).await?; | |
| 127 | + | let owner_name: std::collections::HashMap<i64, String> = | |
| 128 | + | users.into_iter().map(|u| (u.id, u.username)).collect(); | |
| 129 | + | ||
| 130 | + | let mut conn = db.clone(); | |
| 131 | + | let repos = Repository::all().exec(&mut conn).await?; | |
| 132 | + | let mut out: Vec<RepoWithOwner> = repos | |
| 133 | + | .into_iter() | |
| 134 | + | .map(|r| RepoWithOwner { | |
| 135 | + | owner: owner_name.get(&r.owner_id).cloned().unwrap_or_default(), | |
| 136 | + | owner_id: r.owner_id, | |
| 137 | + | name: r.name, | |
| 138 | + | description: r.description, | |
| 139 | + | is_private: r.is_private, | |
| 140 | + | default_branch: r.default_branch, | |
| 141 | + | }) | |
| 142 | + | .collect(); | |
| 143 | + | out.sort_by(|a, b| (&a.owner, &a.name).cmp(&(&b.owner, &b.name))); | |
| 144 | + | Ok(out) | |
| 145 | + | } | |
| 146 | + | ||
| 147 | + | /// Reject names that are empty or contain path separators / traversal. | |
| 148 | + | fn validate_name(name: &str) -> Result<()> { | |
| 149 | + | if name.is_empty() { | |
| 150 | + | return Err(Error::Invalid("repository name must not be empty".into())); | |
| 151 | + | } | |
| 152 | + | if name.contains('/') || name.contains('\\') || name.contains("..") { | |
| 153 | + | return Err(Error::Invalid(format!("invalid repository name: {name:?}"))); | |
| 154 | + | } | |
| 155 | + | Ok(()) | |
| 156 | + | } |
addedcrates/anvil-core/src/sessions.rs+79 −0
| 1 | + | //! Web login sessions: opaque random tokens stored server-side and referenced | |
| 2 | + | //! by a cookie. | |
| 3 | + | ||
| 4 | + | use argon2::password_hash::rand_core::{ | |
| 5 | + | OsRng, | |
| 6 | + | RngCore, | |
| 7 | + | }; | |
| 8 | + | ||
| 9 | + | use crate::error::Result; | |
| 10 | + | use crate::models::{ | |
| 11 | + | Session, | |
| 12 | + | User, | |
| 13 | + | }; | |
| 14 | + | ||
| 15 | + | /// Default session lifetime: 30 days. | |
| 16 | + | const SESSION_TTL_SECS: i64 = 60 * 60 * 24 * 30; | |
| 17 | + | ||
| 18 | + | /// Generate a 256-bit random session token, hex-encoded. | |
| 19 | + | fn generate_token() -> String { | |
| 20 | + | let mut bytes = [0u8; 32]; | |
| 21 | + | let mut rng = OsRng; | |
| 22 | + | rng.fill_bytes(&mut bytes); | |
| 23 | + | bytes.iter().map(|b| format!("{b:02x}")).collect() | |
| 24 | + | } | |
| 25 | + | ||
| 26 | + | /// Create a new session for `user_id`, returning it (the `token` goes in a cookie). | |
| 27 | + | pub async fn create(db: &toasty::Db, user_id: i64) -> Result<Session> { | |
| 28 | + | let mut conn = db.clone(); | |
| 29 | + | let now = crate::now(); | |
| 30 | + | let session = toasty::create!(Session { | |
| 31 | + | token: generate_token(), | |
| 32 | + | user_id: user_id, | |
| 33 | + | created_at: now, | |
| 34 | + | expires_at: now + SESSION_TTL_SECS, | |
| 35 | + | }) | |
| 36 | + | .exec(&mut conn) | |
| 37 | + | .await?; | |
| 38 | + | Ok(session) | |
| 39 | + | } | |
| 40 | + | ||
| 41 | + | /// Resolve a session token to its user, or `None` if missing/expired. | |
| 42 | + | /// Expired sessions are deleted as a side effect. | |
| 43 | + | pub async fn lookup_user(db: &toasty::Db, token: &str) -> Result<Option<User>> { | |
| 44 | + | let mut conn = db.clone(); | |
| 45 | + | let Some(session) = Session::filter(Session::fields().token().eq(token)) | |
| 46 | + | .first() | |
| 47 | + | .exec(&mut conn) | |
| 48 | + | .await? | |
| 49 | + | else { | |
| 50 | + | return Ok(None); | |
| 51 | + | }; | |
| 52 | + | ||
| 53 | + | if session.expires_at < crate::now() { | |
| 54 | + | let mut conn = db.clone(); | |
| 55 | + | let _ = session.delete().exec(&mut conn).await; | |
| 56 | + | return Ok(None); | |
| 57 | + | } | |
| 58 | + | ||
| 59 | + | let mut conn = db.clone(); | |
| 60 | + | let user = User::filter(User::fields().id().eq(session.user_id)) | |
| 61 | + | .first() | |
| 62 | + | .exec(&mut conn) | |
| 63 | + | .await?; | |
| 64 | + | Ok(user) | |
| 65 | + | } | |
| 66 | + | ||
| 67 | + | /// Delete a session (logout). No-op if the token is unknown. | |
| 68 | + | pub async fn delete(db: &toasty::Db, token: &str) -> Result<()> { | |
| 69 | + | let mut conn = db.clone(); | |
| 70 | + | if let Some(session) = Session::filter(Session::fields().token().eq(token)) | |
| 71 | + | .first() | |
| 72 | + | .exec(&mut conn) | |
| 73 | + | .await? | |
| 74 | + | { | |
| 75 | + | let mut conn = db.clone(); | |
| 76 | + | session.delete().exec(&mut conn).await?; | |
| 77 | + | } | |
| 78 | + | Ok(()) | |
| 79 | + | } |
addedcrates/anvil-core/src/ssh_keys.rs+98 −0
| 1 | + | //! SSH public keys: parsing, registration, and lookup for git-over-SSH | |
| 2 | + | //! authentication. | |
| 3 | + | ||
| 4 | + | use ssh_key::{ | |
| 5 | + | HashAlg, | |
| 6 | + | PublicKey, | |
| 7 | + | }; | |
| 8 | + | ||
| 9 | + | use crate::error::{ | |
| 10 | + | Error, | |
| 11 | + | Result, | |
| 12 | + | }; | |
| 13 | + | use crate::models::SshKey; | |
| 14 | + | ||
| 15 | + | /// Parse an OpenSSH public-key line (`ssh-ed25519 AAAA… comment`) into its | |
| 16 | + | /// canonical SHA256 fingerprint and normalized key line, for registration. | |
| 17 | + | pub fn parse_public_key(openssh: &str) -> Result<(String, String)> { | |
| 18 | + | let key = PublicKey::from_openssh(openssh.trim()) | |
| 19 | + | .map_err(|e| Error::Invalid(format!("invalid ssh public key: {e}")))?; | |
| 20 | + | let fingerprint = key.fingerprint(HashAlg::Sha256).to_string(); | |
| 21 | + | let normalized = key | |
| 22 | + | .to_openssh() | |
| 23 | + | .map_err(|e| Error::Invalid(format!("encoding ssh public key: {e}")))?; | |
| 24 | + | Ok((fingerprint, normalized)) | |
| 25 | + | } | |
| 26 | + | ||
| 27 | + | /// Register an SSH public key for a user. | |
| 28 | + | /// | |
| 29 | + | /// `fingerprint` must be the canonical SHA256 fingerprint and `content` the | |
| 30 | + | /// normalized OpenSSH key line. Returns [`Error::AlreadyExists`] if the | |
| 31 | + | /// fingerprint is already registered. | |
| 32 | + | pub async fn add( | |
| 33 | + | db: &toasty::Db, | |
| 34 | + | user_id: i64, | |
| 35 | + | title: &str, | |
| 36 | + | fingerprint: &str, | |
| 37 | + | content: &str, | |
| 38 | + | ) -> Result<SshKey> { | |
| 39 | + | if find_by_fingerprint(db, fingerprint).await?.is_some() { | |
| 40 | + | return Err(Error::AlreadyExists(format!("ssh key {fingerprint}"))); | |
| 41 | + | } | |
| 42 | + | let mut db = db.clone(); | |
| 43 | + | let key = toasty::create!(SshKey { | |
| 44 | + | user_id: user_id, | |
| 45 | + | title: title, | |
| 46 | + | fingerprint: fingerprint, | |
| 47 | + | content: content, | |
| 48 | + | created_at: crate::now(), | |
| 49 | + | }) | |
| 50 | + | .exec(&mut db) | |
| 51 | + | .await?; | |
| 52 | + | Ok(key) | |
| 53 | + | } | |
| 54 | + | ||
| 55 | + | /// Look up a key by its fingerprint. | |
| 56 | + | pub async fn find_by_fingerprint(db: &toasty::Db, fingerprint: &str) -> Result<Option<SshKey>> { | |
| 57 | + | let mut db = db.clone(); | |
| 58 | + | let key = SshKey::filter(SshKey::fields().fingerprint().eq(fingerprint)) | |
| 59 | + | .first() | |
| 60 | + | .exec(&mut db) | |
| 61 | + | .await?; | |
| 62 | + | Ok(key) | |
| 63 | + | } | |
| 64 | + | ||
| 65 | + | /// Find the user id that owns the key with this fingerprint, if any. | |
| 66 | + | pub async fn find_user_id_by_fingerprint( | |
| 67 | + | db: &toasty::Db, | |
| 68 | + | fingerprint: &str, | |
| 69 | + | ) -> Result<Option<i64>> { | |
| 70 | + | Ok(find_by_fingerprint(db, fingerprint) | |
| 71 | + | .await? | |
| 72 | + | .map(|k| k.user_id)) | |
| 73 | + | } | |
| 74 | + | ||
| 75 | + | /// Delete one of `user_id`'s keys by id. No-op if the key is missing or owned | |
| 76 | + | /// by someone else. | |
| 77 | + | pub async fn delete(db: &toasty::Db, id: i64, user_id: i64) -> Result<()> { | |
| 78 | + | let mut conn = db.clone(); | |
| 79 | + | if let Some(key) = SshKey::filter(SshKey::fields().id().eq(id)) | |
| 80 | + | .first() | |
| 81 | + | .exec(&mut conn) | |
| 82 | + | .await? | |
| 83 | + | && key.user_id == user_id | |
| 84 | + | { | |
| 85 | + | let mut conn = db.clone(); | |
| 86 | + | key.delete().exec(&mut conn).await?; | |
| 87 | + | } | |
| 88 | + | Ok(()) | |
| 89 | + | } | |
| 90 | + | ||
| 91 | + | /// List a user's registered SSH keys. | |
| 92 | + | pub async fn list_by_user(db: &toasty::Db, user_id: i64) -> Result<Vec<SshKey>> { | |
| 93 | + | let mut db = db.clone(); | |
| 94 | + | let keys = SshKey::filter(SshKey::fields().user_id().eq(user_id)) | |
| 95 | + | .exec(&mut db) | |
| 96 | + | .await?; | |
| 97 | + | Ok(keys) | |
| 98 | + | } |
addedcrates/anvil-core/src/storage.rs+87 −0
| 1 | + | //! On-disk repository storage. | |
| 2 | + | //! | |
| 3 | + | //! Repositories are bare git repositories laid out as | |
| 4 | + | //! `<repositories_dir>/<owner>/<name>.git`. This module owns the mapping | |
| 5 | + | //! between forge identities and filesystem paths, plus creation/opening via | |
| 6 | + | //! `gix`. No database access happens here. | |
| 7 | + | ||
| 8 | + | use std::path::{ | |
| 9 | + | Path, | |
| 10 | + | PathBuf, | |
| 11 | + | }; | |
| 12 | + | ||
| 13 | + | use crate::error::{ | |
| 14 | + | Error, | |
| 15 | + | Result, | |
| 16 | + | }; | |
| 17 | + | ||
| 18 | + | /// Compute the on-disk path of a bare repository. | |
| 19 | + | pub fn repo_path(repositories_dir: &Path, owner: &str, name: &str) -> PathBuf { | |
| 20 | + | repositories_dir.join(owner).join(format!("{name}.git")) | |
| 21 | + | } | |
| 22 | + | ||
| 23 | + | /// Create a new bare repository on disk, returning the opened handle. | |
| 24 | + | /// | |
| 25 | + | /// `HEAD` is pointed at `refs/heads/<default_branch>` so the on-disk repository | |
| 26 | + | /// agrees with the forge's recorded default branch (gix otherwise defaults to | |
| 27 | + | /// whatever `init.defaultBranch` resolves to, often `master`). | |
| 28 | + | /// | |
| 29 | + | /// Fails if a repository already exists at the target path. The owner | |
| 30 | + | /// directory is created as needed. | |
| 31 | + | pub fn create_bare( | |
| 32 | + | repositories_dir: &Path, | |
| 33 | + | owner: &str, | |
| 34 | + | name: &str, | |
| 35 | + | default_branch: &str, | |
| 36 | + | ) -> Result<gix::Repository> { | |
| 37 | + | let path = repo_path(repositories_dir, owner, name); | |
| 38 | + | if path.exists() { | |
| 39 | + | return Err(Error::AlreadyExists(format!( | |
| 40 | + | "repository on disk at {}", | |
| 41 | + | path.display() | |
| 42 | + | ))); | |
| 43 | + | } | |
| 44 | + | if let Some(parent) = path.parent() { | |
| 45 | + | std::fs::create_dir_all(parent)?; | |
| 46 | + | } | |
| 47 | + | let repo = gix::init_bare(&path) | |
| 48 | + | .map_err(|e| Error::Storage(format!("init bare {}: {e}", path.display())))?; | |
| 49 | + | set_head_branch(&repo, default_branch)?; | |
| 50 | + | Ok(repo) | |
| 51 | + | } | |
| 52 | + | ||
| 53 | + | /// Point `HEAD` at `refs/heads/<branch>` as a symbolic reference. | |
| 54 | + | fn set_head_branch(repo: &gix::Repository, branch: &str) -> Result<()> { | |
| 55 | + | use gix::refs::Target; | |
| 56 | + | use gix::refs::transaction::{ | |
| 57 | + | Change, | |
| 58 | + | LogChange, | |
| 59 | + | PreviousValue, | |
| 60 | + | RefEdit, | |
| 61 | + | }; | |
| 62 | + | ||
| 63 | + | let target_name: gix::refs::FullName = format!("refs/heads/{branch}") | |
| 64 | + | .try_into() | |
| 65 | + | .map_err(|e| Error::Storage(format!("invalid branch name {branch:?}: {e}")))?; | |
| 66 | + | let head_name: gix::refs::FullName = "HEAD" | |
| 67 | + | .try_into() | |
| 68 | + | .map_err(|e| Error::Storage(format!("HEAD ref name: {e}")))?; | |
| 69 | + | ||
| 70 | + | repo.edit_reference(RefEdit { | |
| 71 | + | change: Change::Update { | |
| 72 | + | log: LogChange::default(), | |
| 73 | + | expected: PreviousValue::Any, | |
| 74 | + | new: Target::Symbolic(target_name), | |
| 75 | + | }, | |
| 76 | + | name: head_name, | |
| 77 | + | deref: false, | |
| 78 | + | }) | |
| 79 | + | .map_err(|e| Error::Storage(format!("set HEAD to {branch}: {e}")))?; | |
| 80 | + | Ok(()) | |
| 81 | + | } | |
| 82 | + | ||
| 83 | + | /// Open an existing bare repository. | |
| 84 | + | pub fn open(repositories_dir: &Path, owner: &str, name: &str) -> Result<gix::Repository> { | |
| 85 | + | let path = repo_path(repositories_dir, owner, name); | |
| 86 | + | gix::open(&path).map_err(|e| Error::Storage(format!("open {}: {e}", path.display()))) | |
| 87 | + | } |
addedcrates/anvil-core/src/users.rs+112 −0
| 1 | + | //! User accounts: creation, lookup, and password hashing. | |
| 2 | + | ||
| 3 | + | use argon2::Argon2; | |
| 4 | + | use argon2::password_hash::rand_core::OsRng; | |
| 5 | + | use argon2::password_hash::{ | |
| 6 | + | PasswordHash, | |
| 7 | + | PasswordHasher, | |
| 8 | + | PasswordVerifier, | |
| 9 | + | SaltString, | |
| 10 | + | }; | |
| 11 | + | ||
| 12 | + | use crate::error::{ | |
| 13 | + | Error, | |
| 14 | + | Result, | |
| 15 | + | }; | |
| 16 | + | use crate::models::User; | |
| 17 | + | ||
| 18 | + | /// Usernames reserved for system use — they collide with (or could be confused | |
| 19 | + | /// for) top-level routes such as the `/-/…` namespace. | |
| 20 | + | const RESERVED_USERNAMES: &[&str] = &[ | |
| 21 | + | "-", | |
| 22 | + | "new", | |
| 23 | + | "settings", | |
| 24 | + | "login", | |
| 25 | + | "logout", | |
| 26 | + | "static", | |
| 27 | + | "healthz", | |
| 28 | + | "admin", | |
| 29 | + | "api", | |
| 30 | + | "about", | |
| 31 | + | "help", | |
| 32 | + | "assets", | |
| 33 | + | "favicon.ico", | |
| 34 | + | "robots.txt", | |
| 35 | + | ]; | |
| 36 | + | ||
| 37 | + | /// Hash a plaintext password into a PHC-format Argon2 string. | |
| 38 | + | pub fn hash_password(password: &str) -> Result<String> { | |
| 39 | + | let salt = SaltString::generate(&mut OsRng); | |
| 40 | + | Argon2::default() | |
| 41 | + | .hash_password(password.as_bytes(), &salt) | |
| 42 | + | .map(|h| h.to_string()) | |
| 43 | + | .map_err(|e| Error::Password(e.to_string())) | |
| 44 | + | } | |
| 45 | + | ||
| 46 | + | /// Verify a plaintext password against a stored PHC hash. | |
| 47 | + | pub fn verify_password(hash: &str, password: &str) -> Result<bool> { | |
| 48 | + | let parsed = PasswordHash::new(hash).map_err(|e| Error::Password(e.to_string()))?; | |
| 49 | + | Ok(Argon2::default() | |
| 50 | + | .verify_password(password.as_bytes(), &parsed) | |
| 51 | + | .is_ok()) | |
| 52 | + | } | |
| 53 | + | ||
| 54 | + | /// Create a new user, hashing `password` before storage. | |
| 55 | + | /// | |
| 56 | + | /// Returns [`Error::AlreadyExists`] if the username is taken. | |
| 57 | + | pub async fn create( | |
| 58 | + | db: &toasty::Db, | |
| 59 | + | username: &str, | |
| 60 | + | email: &str, | |
| 61 | + | password: &str, | |
| 62 | + | is_admin: bool, | |
| 63 | + | ) -> Result<User> { | |
| 64 | + | if username.trim().is_empty() { | |
| 65 | + | return Err(Error::Invalid("username must not be empty".into())); | |
| 66 | + | } | |
| 67 | + | // Usernames live in the URL root namespace (e.g. `/<username>`) and on disk | |
| 68 | + | // under `repositories/<username>/`. Reject path-unsafe characters and names | |
| 69 | + | // reserved for system routes (the `/-/…` prefix is reserved structurally, | |
| 70 | + | // but we keep a denylist as defense-in-depth). | |
| 71 | + | if username.contains('/') || username.contains('\\') || username.contains("..") { | |
| 72 | + | return Err(Error::Invalid(format!("invalid username: {username:?}"))); | |
| 73 | + | } | |
| 74 | + | if RESERVED_USERNAMES.contains(&username.to_ascii_lowercase().as_str()) { | |
| 75 | + | return Err(Error::Invalid(format!("username '{username}' is reserved"))); | |
| 76 | + | } | |
| 77 | + | if find_by_username(db, username).await?.is_some() { | |
| 78 | + | return Err(Error::AlreadyExists(format!("user {username}"))); | |
| 79 | + | } | |
| 80 | + | ||
| 81 | + | let mut db = db.clone(); | |
| 82 | + | let user = toasty::create!(User { | |
| 83 | + | username: username, | |
| 84 | + | email: email, | |
| 85 | + | password_hash: hash_password(password)?, | |
| 86 | + | is_admin: is_admin, | |
| 87 | + | created_at: crate::now(), | |
| 88 | + | }) | |
| 89 | + | .exec(&mut db) | |
| 90 | + | .await?; | |
| 91 | + | Ok(user) | |
| 92 | + | } | |
| 93 | + | ||
| 94 | + | /// Look up a user by id. | |
| 95 | + | pub async fn find_by_id(db: &toasty::Db, id: i64) -> Result<Option<User>> { | |
| 96 | + | let mut db = db.clone(); | |
| 97 | + | let user = User::filter(User::fields().id().eq(id)) | |
| 98 | + | .first() | |
| 99 | + | .exec(&mut db) | |
| 100 | + | .await?; | |
| 101 | + | Ok(user) | |
| 102 | + | } | |
| 103 | + | ||
| 104 | + | /// Look up a user by exact username. | |
| 105 | + | pub async fn find_by_username(db: &toasty::Db, username: &str) -> Result<Option<User>> { | |
| 106 | + | let mut db = db.clone(); | |
| 107 | + | let user = User::filter(User::fields().username().eq(username)) | |
| 108 | + | .first() | |
| 109 | + | .exec(&mut db) | |
| 110 | + | .await?; | |
| 111 | + | Ok(user) | |
| 112 | + | } |
addedcrates/anvil-git/Cargo.toml+16 −0
| 1 | + | [package] | |
| 2 | + | name = "anvil-git" | |
| 3 | + | version.workspace = true | |
| 4 | + | edition.workspace = true | |
| 5 | + | license.workspace = true | |
| 6 | + | repository.workspace = true | |
| 7 | + | rust-version.workspace = true | |
| 8 | + | description = "Server-side git wire protocol (smart-HTTP / SSH) on top of gix. Transport-agnostic; upstream-candidate." | |
| 9 | + | ||
| 10 | + | [dependencies] | |
| 11 | + | anvil-core.workspace = true | |
| 12 | + | gix.workspace = true | |
| 13 | + | gitserver-core.workspace = true | |
| 14 | + | tokio.workspace = true | |
| 15 | + | thiserror.workspace = true | |
| 16 | + | tracing.workspace = true |
addedcrates/anvil-git/src/browse.rs+300 −0
| 1 | + | //! Read-only repository browsing via gix: refs, trees, and blobs. | |
| 2 | + | //! | |
| 3 | + | //! These helpers back the web UI. Each opens the bare repo by path; that is | |
| 4 | + | //! cheap enough at our scale and keeps the API stateless. | |
| 5 | + | ||
| 6 | + | use std::collections::{ | |
| 7 | + | BTreeMap, | |
| 8 | + | BTreeSet, | |
| 9 | + | }; | |
| 10 | + | use std::fmt::Display; | |
| 11 | + | use std::path::Path; | |
| 12 | + | ||
| 13 | + | use crate::error::{ | |
| 14 | + | Error, | |
| 15 | + | Result, | |
| 16 | + | }; | |
| 17 | + | ||
| 18 | + | /// Summary of a repository's refs and state, for the repo landing page. | |
| 19 | + | pub struct Overview { | |
| 20 | + | /// Short name of the default branch (from `HEAD`), if resolvable. | |
| 21 | + | pub default_branch: Option<String>, | |
| 22 | + | pub branches: Vec<String>, | |
| 23 | + | pub tags: Vec<String>, | |
| 24 | + | /// True if the repository has no commits yet (unborn `HEAD`). | |
| 25 | + | pub is_empty: bool, | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | /// One entry in a tree listing. | |
| 29 | + | pub struct TreeEntry { | |
| 30 | + | pub name: String, | |
| 31 | + | pub is_dir: bool, | |
| 32 | + | pub oid: String, | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | /// Map any gix error into our error type without a forest of `From` impls. | |
| 36 | + | fn read(e: impl Display) -> Error { | |
| 37 | + | Error::Read(e.to_string()) | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | /// Summarize a repository's default branch, branches, and tags. | |
| 41 | + | pub fn overview(repo_path: &Path) -> Result<Overview> { | |
| 42 | + | let repo = gix::open(repo_path).map_err(read)?; | |
| 43 | + | let default_branch = repo | |
| 44 | + | .head_name() | |
| 45 | + | .ok() | |
| 46 | + | .flatten() | |
| 47 | + | .map(|n| n.shorten().to_string()); | |
| 48 | + | let is_empty = repo.head_id().is_err(); | |
| 49 | + | ||
| 50 | + | let refs = repo.references().map_err(read)?; | |
| 51 | + | let mut branches = Vec::new(); | |
| 52 | + | for r in refs.local_branches().map_err(read)?.flatten() { | |
| 53 | + | branches.push(r.name().shorten().to_string()); | |
| 54 | + | } | |
| 55 | + | let mut tags = Vec::new(); | |
| 56 | + | for r in refs.tags().map_err(read)?.flatten() { | |
| 57 | + | tags.push(r.name().shorten().to_string()); | |
| 58 | + | } | |
| 59 | + | branches.sort(); | |
| 60 | + | tags.sort(); | |
| 61 | + | ||
| 62 | + | Ok(Overview { | |
| 63 | + | default_branch, | |
| 64 | + | branches, | |
| 65 | + | tags, | |
| 66 | + | is_empty, | |
| 67 | + | }) | |
| 68 | + | } | |
| 69 | + | ||
| 70 | + | /// List the entries of the tree at `path` (empty for root) for revision `rev` | |
| 71 | + | /// (a branch, tag, or commit-ish). Directories sort before files. Returns an | |
| 72 | + | /// empty list if `path` is not a directory. | |
| 73 | + | pub fn list_tree(repo_path: &Path, rev: &str, path: &str) -> Result<Vec<TreeEntry>> { | |
| 74 | + | let repo = gix::open(repo_path).map_err(read)?; | |
| 75 | + | let root = repo | |
| 76 | + | .rev_parse_single(rev) | |
| 77 | + | .map_err(read)? | |
| 78 | + | .object() | |
| 79 | + | .map_err(read)? | |
| 80 | + | .peel_to_commit() | |
| 81 | + | .map_err(read)? | |
| 82 | + | .tree() | |
| 83 | + | .map_err(read)?; | |
| 84 | + | ||
| 85 | + | let tree = if path.is_empty() { | |
| 86 | + | root | |
| 87 | + | } else { | |
| 88 | + | match root.lookup_entry_by_path(path).map_err(read)? { | |
| 89 | + | Some(entry) if entry.mode().is_tree() => { | |
| 90 | + | entry.object().map_err(read)?.peel_to_tree().map_err(read)? | |
| 91 | + | } | |
| 92 | + | _ => return Ok(Vec::new()), | |
| 93 | + | } | |
| 94 | + | }; | |
| 95 | + | ||
| 96 | + | let mut entries = Vec::new(); | |
| 97 | + | for e in tree.iter() { | |
| 98 | + | let e = e.map_err(read)?; | |
| 99 | + | entries.push(TreeEntry { | |
| 100 | + | name: e.filename().to_string(), | |
| 101 | + | is_dir: e.mode().is_tree(), | |
| 102 | + | oid: e.oid().to_string(), | |
| 103 | + | }); | |
| 104 | + | } | |
| 105 | + | entries.sort_by(|a, b| (!a.is_dir, &a.name).cmp(&(!b.is_dir, &b.name))); | |
| 106 | + | Ok(entries) | |
| 107 | + | } | |
| 108 | + | ||
| 109 | + | /// Metadata about a single commit. | |
| 110 | + | pub struct CommitInfo { | |
| 111 | + | pub id: String, | |
| 112 | + | pub short: String, | |
| 113 | + | pub summary: String, | |
| 114 | + | pub author: String, | |
| 115 | + | /// Commit time in Unix seconds. | |
| 116 | + | pub time: i64, | |
| 117 | + | } | |
| 118 | + | ||
| 119 | + | /// How a file changed between two commits. | |
| 120 | + | #[derive(Clone, Copy, PartialEq, Eq)] | |
| 121 | + | pub enum ChangeKind { | |
| 122 | + | Added, | |
| 123 | + | Deleted, | |
| 124 | + | Modified, | |
| 125 | + | } | |
| 126 | + | ||
| 127 | + | /// A single file's change in a commit, with old/new contents for diffing. | |
| 128 | + | pub struct FileChange { | |
| 129 | + | pub path: String, | |
| 130 | + | pub kind: ChangeKind, | |
| 131 | + | pub old: Option<Vec<u8>>, | |
| 132 | + | pub new: Option<Vec<u8>>, | |
| 133 | + | } | |
| 134 | + | ||
| 135 | + | /// A commit plus its diff against its first parent. | |
| 136 | + | pub struct CommitDetail { | |
| 137 | + | pub info: CommitInfo, | |
| 138 | + | pub parent: Option<String>, | |
| 139 | + | pub message: String, | |
| 140 | + | pub changes: Vec<FileChange>, | |
| 141 | + | } | |
| 142 | + | ||
| 143 | + | /// Walk commit history starting at `rev`, newest first, up to `limit` commits. | |
| 144 | + | pub fn commit_log(repo_path: &Path, rev: &str, limit: usize) -> Result<Vec<CommitInfo>> { | |
| 145 | + | let repo = gix::open(repo_path).map_err(read)?; | |
| 146 | + | let start = repo | |
| 147 | + | .rev_parse_single(rev) | |
| 148 | + | .map_err(read)? | |
| 149 | + | .object() | |
| 150 | + | .map_err(read)? | |
| 151 | + | .peel_to_commit() | |
| 152 | + | .map_err(read)? | |
| 153 | + | .id; | |
| 154 | + | ||
| 155 | + | let mut out = Vec::new(); | |
| 156 | + | for info in repo.rev_walk(Some(start)).all().map_err(read)?.take(limit) { | |
| 157 | + | let info = info.map_err(read)?; | |
| 158 | + | let commit = repo | |
| 159 | + | .find_object(info.id) | |
| 160 | + | .map_err(read)? | |
| 161 | + | .try_into_commit() | |
| 162 | + | .map_err(read)?; | |
| 163 | + | out.push(commit_info(&commit)?); | |
| 164 | + | } | |
| 165 | + | Ok(out) | |
| 166 | + | } | |
| 167 | + | ||
| 168 | + | /// Load a commit and compute its file-level diff against its first parent | |
| 169 | + | /// (against the empty tree for a root commit). | |
| 170 | + | pub fn commit_detail(repo_path: &Path, rev: &str) -> Result<CommitDetail> { | |
| 171 | + | let repo = gix::open(repo_path).map_err(read)?; | |
| 172 | + | let commit = repo | |
| 173 | + | .rev_parse_single(rev) | |
| 174 | + | .map_err(read)? | |
| 175 | + | .object() | |
| 176 | + | .map_err(read)? | |
| 177 | + | .peel_to_commit() | |
| 178 | + | .map_err(read)?; | |
| 179 | + | ||
| 180 | + | let info = commit_info(&commit)?; | |
| 181 | + | let message = commit.message_raw().map_err(read)?.to_string(); | |
| 182 | + | let parent = commit.parent_ids().next().map(|p| p.detach()); | |
| 183 | + | ||
| 184 | + | let mut new_map = BTreeMap::new(); | |
| 185 | + | flatten_tree(&commit.tree().map_err(read)?, "", &mut new_map)?; | |
| 186 | + | let mut old_map = BTreeMap::new(); | |
| 187 | + | if let Some(pid) = parent { | |
| 188 | + | let parent_commit = repo | |
| 189 | + | .find_object(pid) | |
| 190 | + | .map_err(read)? | |
| 191 | + | .try_into_commit() | |
| 192 | + | .map_err(read)?; | |
| 193 | + | flatten_tree(&parent_commit.tree().map_err(read)?, "", &mut old_map)?; | |
| 194 | + | } | |
| 195 | + | ||
| 196 | + | let blob = |oid: gix::ObjectId| -> Result<Vec<u8>> { | |
| 197 | + | Ok(repo.find_object(oid).map_err(read)?.data.clone()) | |
| 198 | + | }; | |
| 199 | + | ||
| 200 | + | let mut paths: BTreeSet<&String> = new_map.keys().collect(); | |
| 201 | + | paths.extend(old_map.keys()); | |
| 202 | + | let mut changes = Vec::new(); | |
| 203 | + | for path in paths { | |
| 204 | + | match (old_map.get(path), new_map.get(path)) { | |
| 205 | + | (None, Some(n)) => changes.push(FileChange { | |
| 206 | + | path: path.clone(), | |
| 207 | + | kind: ChangeKind::Added, | |
| 208 | + | old: None, | |
| 209 | + | new: Some(blob(*n)?), | |
| 210 | + | }), | |
| 211 | + | (Some(o), None) => changes.push(FileChange { | |
| 212 | + | path: path.clone(), | |
| 213 | + | kind: ChangeKind::Deleted, | |
| 214 | + | old: Some(blob(*o)?), | |
| 215 | + | new: None, | |
| 216 | + | }), | |
| 217 | + | (Some(o), Some(n)) if o != n => changes.push(FileChange { | |
| 218 | + | path: path.clone(), | |
| 219 | + | kind: ChangeKind::Modified, | |
| 220 | + | old: Some(blob(*o)?), | |
| 221 | + | new: Some(blob(*n)?), | |
| 222 | + | }), | |
| 223 | + | _ => {} | |
| 224 | + | } | |
| 225 | + | } | |
| 226 | + | ||
| 227 | + | Ok(CommitDetail { | |
| 228 | + | info, | |
| 229 | + | parent: parent.map(|p| p.to_string()), | |
| 230 | + | message, | |
| 231 | + | changes, | |
| 232 | + | }) | |
| 233 | + | } | |
| 234 | + | ||
| 235 | + | fn commit_info(commit: &gix::Commit) -> Result<CommitInfo> { | |
| 236 | + | let id = commit.id.to_string(); | |
| 237 | + | let raw = commit.message_raw().map_err(read)?; | |
| 238 | + | let summary = raw | |
| 239 | + | .to_string() | |
| 240 | + | .lines() | |
| 241 | + | .next() | |
| 242 | + | .unwrap_or_default() | |
| 243 | + | .to_string(); | |
| 244 | + | let author = commit.author().map_err(read)?.name.to_string(); | |
| 245 | + | let time = commit.time().map_err(read)?.seconds; | |
| 246 | + | Ok(CommitInfo { | |
| 247 | + | short: id[..id.len().min(8)].to_string(), | |
| 248 | + | id, | |
| 249 | + | summary, | |
| 250 | + | author, | |
| 251 | + | time, | |
| 252 | + | }) | |
| 253 | + | } | |
| 254 | + | ||
| 255 | + | /// Recursively collect blob paths → object id from a tree. | |
| 256 | + | fn flatten_tree( | |
| 257 | + | tree: &gix::Tree, | |
| 258 | + | prefix: &str, | |
| 259 | + | out: &mut BTreeMap<String, gix::ObjectId>, | |
| 260 | + | ) -> Result<()> { | |
| 261 | + | for e in tree.iter() { | |
| 262 | + | let e = e.map_err(read)?; | |
| 263 | + | let name = e.filename().to_string(); | |
| 264 | + | let path = if prefix.is_empty() { | |
| 265 | + | name | |
| 266 | + | } else { | |
| 267 | + | format!("{prefix}/{name}") | |
| 268 | + | }; | |
| 269 | + | if e.mode().is_tree() { | |
| 270 | + | let sub = e.object().map_err(read)?.peel_to_tree().map_err(read)?; | |
| 271 | + | flatten_tree(&sub, &path, out)?; | |
| 272 | + | } else if e.mode().is_blob() { | |
| 273 | + | out.insert(path, e.oid().to_owned()); | |
| 274 | + | } | |
| 275 | + | } | |
| 276 | + | Ok(()) | |
| 277 | + | } | |
| 278 | + | ||
| 279 | + | /// Read the raw bytes of the blob at `path` for revision `rev`. Returns `None` | |
| 280 | + | /// if the path does not exist or is not a blob. | |
| 281 | + | pub fn read_blob(repo_path: &Path, rev: &str, path: &str) -> Result<Option<Vec<u8>>> { | |
| 282 | + | let repo = gix::open(repo_path).map_err(read)?; | |
| 283 | + | let tree = repo | |
| 284 | + | .rev_parse_single(rev) | |
| 285 | + | .map_err(read)? | |
| 286 | + | .object() | |
| 287 | + | .map_err(read)? | |
| 288 | + | .peel_to_commit() | |
| 289 | + | .map_err(read)? | |
| 290 | + | .tree() | |
| 291 | + | .map_err(read)?; | |
| 292 | + | ||
| 293 | + | match tree.lookup_entry_by_path(path).map_err(read)? { | |
| 294 | + | Some(entry) if entry.mode().is_blob() => { | |
| 295 | + | let object = repo.find_object(entry.oid().to_owned()).map_err(read)?; | |
| 296 | + | Ok(Some(object.data.clone())) | |
| 297 | + | } | |
| 298 | + | _ => Ok(None), | |
| 299 | + | } | |
| 300 | + | } |
addedcrates/anvil-git/src/error.rs+18 −0
| 1 | + | //! Errors from the git wire-protocol layer. | |
| 2 | + | ||
| 3 | + | /// Errors produced while serving the git smart protocol. | |
| 4 | + | #[derive(Debug, thiserror::Error)] | |
| 5 | + | pub enum Error { | |
| 6 | + | /// An error from the underlying protocol engine (`gitserver-core`). | |
| 7 | + | #[error(transparent)] | |
| 8 | + | Core(#[from] gitserver_core::error::Error), | |
| 9 | + | ||
| 10 | + | #[error(transparent)] | |
| 11 | + | Io(#[from] std::io::Error), | |
| 12 | + | ||
| 13 | + | /// An error reading repository contents via gix (browse layer). | |
| 14 | + | #[error("git read error: {0}")] | |
| 15 | + | Read(String), | |
| 16 | + | } | |
| 17 | + | ||
| 18 | + | pub type Result<T> = std::result::Result<T, Error>; |
addedcrates/anvil-git/src/lib.rs+28 −0
| 1 | + | //! Server-side git wire protocol for anvil. | |
| 2 | + | //! | |
| 3 | + | //! `gix` provides object/ref/pack *plumbing* but no server-side protocol: it | |
| 4 | + | //! ships no `upload-pack`/`receive-pack` and its `gix-transport`/`gix-protocol` | |
| 5 | + | //! crates are client-only. This crate fills that gap. | |
| 6 | + | //! | |
| 7 | + | //! ## Design | |
| 8 | + | //! - **Transport-agnostic.** [`smart_http`] takes a repo path plus request bytes | |
| 9 | + | //! and returns response bytes / a packfile stream, with no HTTP dependency, so | |
| 10 | + | //! the same engine serves smart-HTTP (`anvil-web`) and, later, SSH | |
| 11 | + | //! (`anvil-ssh`). | |
| 12 | + | //! - **Bootstrapped on [`gitserver_core`]** (vendored, gix-native pkt-line / | |
| 13 | + | //! protocol-v2 / upload-pack / receive-pack), behind this crate's interface so | |
| 14 | + | //! it can be replaced incrementally with an upstream-shaped implementation. | |
| 15 | + | ||
| 16 | + | pub mod browse; | |
| 17 | + | pub mod error; | |
| 18 | + | pub mod smart_http; | |
| 19 | + | pub mod ssh; | |
| 20 | + | ||
| 21 | + | pub use error::{ | |
| 22 | + | Error, | |
| 23 | + | Result, | |
| 24 | + | }; | |
| 25 | + | pub use smart_http::{ | |
| 26 | + | Service, | |
| 27 | + | UploadPack, | |
| 28 | + | }; |
addedcrates/anvil-git/src/smart_http.rs+155 −0
| 1 | + | //! Git smart-HTTP protocol, transport-agnostic. | |
| 2 | + | //! | |
| 3 | + | //! These functions take a bare-repository path plus request bytes and return | |
| 4 | + | //! response bytes (or a packfile stream), with no dependency on the HTTP | |
| 5 | + | //! framework. `anvil-web` adapts them to axum; a future `anvil-ssh` can reuse | |
| 6 | + | //! the same engine over an SSH channel. | |
| 7 | + | //! | |
| 8 | + | //! The protocol engine is [`gitserver_core`]; this module orchestrates it into | |
| 9 | + | //! the request/response shapes a transport needs. We target git **protocol v2** | |
| 10 | + | //! for fetch (the modern default) with a v0 fallback, and the classic v0 | |
| 11 | + | //! advertisement for `receive-pack` (push). | |
| 12 | + | ||
| 13 | + | use std::path::Path; | |
| 14 | + | use std::pin::Pin; | |
| 15 | + | ||
| 16 | + | use gitserver_core::backend::GitBackend; | |
| 17 | + | use gitserver_core::{ | |
| 18 | + | pack, | |
| 19 | + | pktline, | |
| 20 | + | protocol_v2, | |
| 21 | + | }; | |
| 22 | + | use tokio::io::AsyncRead; | |
| 23 | + | ||
| 24 | + | use crate::error::Result; | |
| 25 | + | ||
| 26 | + | /// The two git wire services. | |
| 27 | + | #[derive(Debug, Copy, Clone, Eq, PartialEq)] | |
| 28 | + | pub enum Service { | |
| 29 | + | /// `git-upload-pack` — serves fetch/clone. | |
| 30 | + | UploadPack, | |
| 31 | + | /// `git-receive-pack` — accepts push. | |
| 32 | + | ReceivePack, | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | impl Service { | |
| 36 | + | pub fn as_str(self) -> &'static str { | |
| 37 | + | match self { | |
| 38 | + | Service::UploadPack => "git-upload-pack", | |
| 39 | + | Service::ReceivePack => "git-receive-pack", | |
| 40 | + | } | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | /// Parse the `?service=` query value used by `GET /info/refs`. | |
| 44 | + | pub fn from_query(value: &str) -> Option<Self> { | |
| 45 | + | match value { | |
| 46 | + | "git-upload-pack" => Some(Service::UploadPack), | |
| 47 | + | "git-receive-pack" => Some(Service::ReceivePack), | |
| 48 | + | _ => None, | |
| 49 | + | } | |
| 50 | + | } | |
| 51 | + | ||
| 52 | + | /// The `Content-Type` for this service's `info/refs` advertisement. | |
| 53 | + | pub fn advertisement_content_type(self) -> String { | |
| 54 | + | format!("application/x-{}-advertisement", self.as_str()) | |
| 55 | + | } | |
| 56 | + | ||
| 57 | + | /// The `Content-Type` for this service's RPC result. | |
| 58 | + | pub fn result_content_type(self) -> String { | |
| 59 | + | format!("application/x-{}-result", self.as_str()) | |
| 60 | + | } | |
| 61 | + | } | |
| 62 | + | ||
| 63 | + | /// A boxed packfile stream produced by `upload-pack`. | |
| 64 | + | pub type PackStream = Pin<Box<dyn AsyncRead + Send>>; | |
| 65 | + | ||
| 66 | + | /// Outcome of a `git-upload-pack` RPC: either a fully buffered response (an | |
| 67 | + | /// `ls-refs` reply or an acknowledgments-only negotiation round) or a streamed | |
| 68 | + | /// packfile (prefixed with any acknowledgments/shallow info). | |
| 69 | + | pub enum UploadPack { | |
| 70 | + | Buffered(Vec<u8>), | |
| 71 | + | Pack(PackStream), | |
| 72 | + | } | |
| 73 | + | ||
| 74 | + | /// Build the `GET /info/refs` advertisement body for `service`. | |
| 75 | + | /// | |
| 76 | + | /// `protocol_v2` should be true only when the client requested v2 | |
| 77 | + | /// (`Git-Protocol: version=2`) for `upload-pack`; `receive-pack` always uses the | |
| 78 | + | /// v0-style advertisement. | |
| 79 | + | pub fn advertise(repo_path: &Path, service: Service, protocol_v2: bool) -> Result<Vec<u8>> { | |
| 80 | + | let body = match service { | |
| 81 | + | Service::UploadPack if protocol_v2 => protocol_v2::advertise_capabilities(), | |
| 82 | + | Service::UploadPack => GitBackend::new(repo_path.to_path_buf()).advertise_refs()?, | |
| 83 | + | Service::ReceivePack => { | |
| 84 | + | let mut body = pktline::encode_comment("service=git-receive-pack"); | |
| 85 | + | body.extend_from_slice(pktline::flush()); | |
| 86 | + | body.extend_from_slice( | |
| 87 | + | &GitBackend::new(repo_path.to_path_buf()).advertise_receive_refs()?, | |
| 88 | + | ); | |
| 89 | + | body | |
| 90 | + | } | |
| 91 | + | }; | |
| 92 | + | Ok(body) | |
| 93 | + | } | |
| 94 | + | ||
| 95 | + | /// Handle a `POST /git-upload-pack` request using protocol v2. | |
| 96 | + | pub async fn upload_pack_v2(repo_path: &Path, request: &[u8]) -> Result<UploadPack> { | |
| 97 | + | let backend = GitBackend::new(repo_path.to_path_buf()); | |
| 98 | + | match protocol_v2::parse_command_request(request)? { | |
| 99 | + | protocol_v2::Command::LsRefs(req) => { | |
| 100 | + | Ok(UploadPack::Buffered(protocol_v2::ls_refs(repo_path, &req)?)) | |
| 101 | + | } | |
| 102 | + | protocol_v2::Command::Fetch(mut req) => { | |
| 103 | + | let shallow = protocol_v2::apply_shallow_boundaries(repo_path, &mut req)?; | |
| 104 | + | let is_shallow_negotiation = req.upload_request.shallow.depth.is_some(); | |
| 105 | + | ||
| 106 | + | // Negotiation round (no packfile yet): reply with acknowledgments. | |
| 107 | + | if !req.upload_request.done && !is_shallow_negotiation { | |
| 108 | + | let common = protocol_v2::common_haves(repo_path, &req)?; | |
| 109 | + | let mut body = protocol_v2::encode_fetch_acknowledgments(&common); | |
| 110 | + | body.extend_from_slice(&protocol_v2::encode_shallow_info(&shallow)); | |
| 111 | + | return Ok(UploadPack::Buffered(body)); | |
| 112 | + | } | |
| 113 | + | ||
| 114 | + | let reader = backend.upload_pack(&req.upload_request).await?; | |
| 115 | + | ||
| 116 | + | let mut prefix = if is_shallow_negotiation | |
| 117 | + | && !req.upload_request.haves.is_empty() | |
| 118 | + | && !req.upload_request.done | |
| 119 | + | { | |
| 120 | + | protocol_v2::encode_fetch_ready_and_acknowledgments(&protocol_v2::common_haves( | |
| 121 | + | repo_path, &req, | |
| 122 | + | )?) | |
| 123 | + | } else { | |
| 124 | + | Vec::new() | |
| 125 | + | }; | |
| 126 | + | prefix.extend_from_slice(&protocol_v2::encode_shallow_info(&shallow)); | |
| 127 | + | prefix.extend_from_slice(&pktline::encode(b"packfile\n")); | |
| 128 | + | ||
| 129 | + | let stream = protocol_v2::PrefixThenReader::new( | |
| 130 | + | prefix, | |
| 131 | + | protocol_v2::PackSectionReader::new(reader), | |
| 132 | + | ); | |
| 133 | + | Ok(UploadPack::Pack(Box::pin(stream))) | |
| 134 | + | } | |
| 135 | + | } | |
| 136 | + | } | |
| 137 | + | ||
| 138 | + | /// Handle a `POST /git-upload-pack` request using protocol v0/v1 (fallback for | |
| 139 | + | /// clients that did not request v2). Returns the raw packfile stream. | |
| 140 | + | pub async fn upload_pack_v0(repo_path: &Path, request: &[u8]) -> Result<PackStream> { | |
| 141 | + | let backend = GitBackend::new(repo_path.to_path_buf()); | |
| 142 | + | let parsed = pack::UploadPackRequest::parse(request)?; | |
| 143 | + | let reader = backend.upload_pack(&parsed).await?; | |
| 144 | + | Ok(Box::pin(reader)) | |
| 145 | + | } | |
| 146 | + | ||
| 147 | + | /// Handle a `POST /git-receive-pack` request (push). Returns the report-status | |
| 148 | + | /// response body. | |
| 149 | + | pub async fn receive_pack<R>(repo_path: &Path, request: R) -> Result<Vec<u8>> | |
| 150 | + | where | |
| 151 | + | R: AsyncRead + Unpin + Send + 'static, | |
| 152 | + | { | |
| 153 | + | let backend = GitBackend::new(repo_path.to_path_buf()); | |
| 154 | + | Ok(backend.receive_pack(request).await?) | |
| 155 | + | } |
addedcrates/anvil-git/src/ssh.rs+228 −0
| 1 | + | //! Git smart protocol over a bidirectional stream (SSH transport / `git://`). | |
| 2 | + | //! | |
| 3 | + | //! Unlike smart-HTTP — which is a sequence of independent request/response pairs | |
| 4 | + | //! — the SSH transport is one duplex stream: the server writes the ref | |
| 5 | + | //! advertisement, then reads the client's request(s) and writes the | |
| 6 | + | //! response/packfile on the same connection. SSH also omits the HTTP-only | |
| 7 | + | //! `# service=…` announcement line. | |
| 8 | + | //! | |
| 9 | + | //! This module reuses the same [`crate::smart_http`] engine; it only adds the | |
| 10 | + | //! stream framing (reading pkt-line requests and stripping the HTTP prefix). | |
| 11 | + | ||
| 12 | + | use std::path::Path; | |
| 13 | + | ||
| 14 | + | use gitserver_core::backend::GitBackend; | |
| 15 | + | use tokio::io::{ | |
| 16 | + | AsyncRead, | |
| 17 | + | AsyncReadExt, | |
| 18 | + | AsyncWrite, | |
| 19 | + | AsyncWriteExt, | |
| 20 | + | }; | |
| 21 | + | ||
| 22 | + | use crate::error::Result; | |
| 23 | + | use crate::smart_http::{ | |
| 24 | + | self, | |
| 25 | + | Service, | |
| 26 | + | UploadPack, | |
| 27 | + | }; | |
| 28 | + | ||
| 29 | + | /// Parse an SSH `exec` command such as `git-upload-pack '/alice/hello.git'`, | |
| 30 | + | /// returning the service and the (leading-slash-trimmed) repository path. | |
| 31 | + | pub fn parse_command(command: &str) -> Option<(Service, String)> { | |
| 32 | + | let command = command.trim(); | |
| 33 | + | for (prefix, service) in [ | |
| 34 | + | ("git-upload-pack", Service::UploadPack), | |
| 35 | + | ("git-receive-pack", Service::ReceivePack), | |
| 36 | + | ] { | |
| 37 | + | if let Some(rest) = command.strip_prefix(prefix) { | |
| 38 | + | let path = rest | |
| 39 | + | .trim() | |
| 40 | + | .trim_matches('\'') | |
| 41 | + | .trim_matches('"') | |
| 42 | + | .trim_start_matches('/'); | |
| 43 | + | return Some((service, path.to_string())); | |
| 44 | + | } | |
| 45 | + | } | |
| 46 | + | None | |
| 47 | + | } | |
| 48 | + | ||
| 49 | + | /// Serve one git request over `stream` for the bare repo at `repo_path`. | |
| 50 | + | /// | |
| 51 | + | /// Takes ownership of the stream: `receive-pack` splits it so the protocol | |
| 52 | + | /// engine can own the read half. | |
| 53 | + | pub async fn serve<S>( | |
| 54 | + | repo_path: &Path, | |
| 55 | + | service: Service, | |
| 56 | + | protocol_v2: bool, | |
| 57 | + | mut stream: S, | |
| 58 | + | ) -> Result<()> | |
| 59 | + | where | |
| 60 | + | S: AsyncRead + AsyncWrite + Unpin + Send + 'static, | |
| 61 | + | { | |
| 62 | + | match service { | |
| 63 | + | Service::UploadPack => upload_pack(repo_path, protocol_v2, &mut stream).await, | |
| 64 | + | Service::ReceivePack => receive_pack(repo_path, stream).await, | |
| 65 | + | } | |
| 66 | + | } | |
| 67 | + | ||
| 68 | + | async fn upload_pack<S>(repo_path: &Path, protocol_v2: bool, stream: &mut S) -> Result<()> | |
| 69 | + | where | |
| 70 | + | S: AsyncRead + AsyncWrite + Unpin + Send, | |
| 71 | + | { | |
| 72 | + | let advertisement = smart_http::advertise(repo_path, Service::UploadPack, protocol_v2)?; | |
| 73 | + | stream | |
| 74 | + | .write_all(strip_http_service_prefix(&advertisement)) | |
| 75 | + | .await?; | |
| 76 | + | stream.flush().await?; | |
| 77 | + | ||
| 78 | + | if protocol_v2 { | |
| 79 | + | // v2: a sequence of commands (ls-refs, fetch), each terminated by flush. | |
| 80 | + | loop { | |
| 81 | + | let command = read_until_flush(stream).await?; | |
| 82 | + | if command.is_empty() { | |
| 83 | + | break; // client closed the connection | |
| 84 | + | } | |
| 85 | + | match smart_http::upload_pack_v2(repo_path, &command).await? { | |
| 86 | + | UploadPack::Buffered(body) => { | |
| 87 | + | stream.write_all(&body).await?; | |
| 88 | + | stream.flush().await?; | |
| 89 | + | } | |
| 90 | + | UploadPack::Pack(mut pack) => { | |
| 91 | + | tokio::io::copy(&mut pack, stream).await?; | |
| 92 | + | stream.flush().await?; | |
| 93 | + | break; | |
| 94 | + | } | |
| 95 | + | } | |
| 96 | + | } | |
| 97 | + | } else { | |
| 98 | + | // v0/v1: a single want/have request terminated by `done`. | |
| 99 | + | let request = read_until_done(stream).await?; | |
| 100 | + | if !request.is_empty() { | |
| 101 | + | let mut pack = smart_http::upload_pack_v0(repo_path, &request).await?; | |
| 102 | + | tokio::io::copy(&mut pack, stream).await?; | |
| 103 | + | stream.flush().await?; | |
| 104 | + | } | |
| 105 | + | } | |
| 106 | + | Ok(()) | |
| 107 | + | } | |
| 108 | + | ||
| 109 | + | async fn receive_pack<S>(repo_path: &Path, stream: S) -> Result<()> | |
| 110 | + | where | |
| 111 | + | S: AsyncRead + AsyncWrite + Unpin + Send + 'static, | |
| 112 | + | { | |
| 113 | + | let (read_half, mut write_half) = tokio::io::split(stream); | |
| 114 | + | let backend = GitBackend::new(repo_path.to_path_buf()); | |
| 115 | + | ||
| 116 | + | // Advertise refs (no HTTP `# service` prefix on the SSH transport). | |
| 117 | + | let advertisement = backend.advertise_receive_refs()?; | |
| 118 | + | write_half.write_all(&advertisement).await?; | |
| 119 | + | write_half.flush().await?; | |
| 120 | + | ||
| 121 | + | // The engine reads exactly the commands + packfile (not to EOF), so this | |
| 122 | + | // does not deadlock waiting for the client to half-close. | |
| 123 | + | let report = backend.receive_pack(read_half).await?; | |
| 124 | + | write_half.write_all(&report).await?; | |
| 125 | + | write_half.flush().await?; | |
| 126 | + | Ok(()) | |
| 127 | + | } | |
| 128 | + | ||
| 129 | + | /// The HTTP advertisements begin with a `# service=…` pkt-line followed by a | |
| 130 | + | /// flush; the SSH/`git://` transports omit it. Return the bytes after the first | |
| 131 | + | /// flush packet (the service comment never contains `0000`). | |
| 132 | + | fn strip_http_service_prefix(advertisement: &[u8]) -> &[u8] { | |
| 133 | + | match advertisement.windows(4).position(|w| w == b"0000") { | |
| 134 | + | Some(pos) => &advertisement[pos + 4..], | |
| 135 | + | None => advertisement, | |
| 136 | + | } | |
| 137 | + | } | |
| 138 | + | ||
| 139 | + | /// Read raw pkt-lines until (and including) a flush packet. Returns the exact | |
| 140 | + | /// bytes read, suitable for the v2 command parser. An empty result means EOF | |
| 141 | + | /// before any data (the client closed the connection). | |
| 142 | + | async fn read_until_flush<S>(stream: &mut S) -> Result<Vec<u8>> | |
| 143 | + | where | |
| 144 | + | S: AsyncRead + Unpin, | |
| 145 | + | { | |
| 146 | + | let mut buf = Vec::new(); | |
| 147 | + | loop { | |
| 148 | + | match read_pkt(stream).await? { | |
| 149 | + | None => break, | |
| 150 | + | Some(Pkt::Flush) => { | |
| 151 | + | buf.extend_from_slice(b"0000"); | |
| 152 | + | break; | |
| 153 | + | } | |
| 154 | + | Some(Pkt::Delim) => buf.extend_from_slice(b"0001"), | |
| 155 | + | Some(Pkt::ResponseEnd) => buf.extend_from_slice(b"0002"), | |
| 156 | + | Some(Pkt::Line(len, payload)) => { | |
| 157 | + | buf.extend_from_slice(format!("{len:04x}").as_bytes()); | |
| 158 | + | buf.extend_from_slice(&payload); | |
| 159 | + | } | |
| 160 | + | } | |
| 161 | + | } | |
| 162 | + | Ok(buf) | |
| 163 | + | } | |
| 164 | + | ||
| 165 | + | /// Read raw pkt-lines until a `done` line (v0/v1 negotiation end) or EOF. | |
| 166 | + | async fn read_until_done<S>(stream: &mut S) -> Result<Vec<u8>> | |
| 167 | + | where | |
| 168 | + | S: AsyncRead + Unpin, | |
| 169 | + | { | |
| 170 | + | let mut buf = Vec::new(); | |
| 171 | + | loop { | |
| 172 | + | match read_pkt(stream).await? { | |
| 173 | + | None => break, | |
| 174 | + | Some(Pkt::Flush) => buf.extend_from_slice(b"0000"), | |
| 175 | + | Some(Pkt::Delim) => buf.extend_from_slice(b"0001"), | |
| 176 | + | Some(Pkt::ResponseEnd) => buf.extend_from_slice(b"0002"), | |
| 177 | + | Some(Pkt::Line(len, payload)) => { | |
| 178 | + | buf.extend_from_slice(format!("{len:04x}").as_bytes()); | |
| 179 | + | buf.extend_from_slice(&payload); | |
| 180 | + | if payload.trim_ascii_end() == b"done" { | |
| 181 | + | break; | |
| 182 | + | } | |
| 183 | + | } | |
| 184 | + | } | |
| 185 | + | } | |
| 186 | + | Ok(buf) | |
| 187 | + | } | |
| 188 | + | ||
| 189 | + | enum Pkt { | |
| 190 | + | Flush, | |
| 191 | + | Delim, | |
| 192 | + | ResponseEnd, | |
| 193 | + | Line(usize, Vec<u8>), | |
| 194 | + | } | |
| 195 | + | ||
| 196 | + | /// Read a single pkt-line. Returns `None` on a clean EOF at a packet boundary. | |
| 197 | + | async fn read_pkt<S>(stream: &mut S) -> Result<Option<Pkt>> | |
| 198 | + | where | |
| 199 | + | S: AsyncRead + Unpin, | |
| 200 | + | { | |
| 201 | + | let mut header = [0u8; 4]; | |
| 202 | + | match stream.read_exact(&mut header).await { | |
| 203 | + | Ok(_) => {} | |
| 204 | + | Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => return Ok(None), | |
| 205 | + | Err(e) => return Err(e.into()), | |
| 206 | + | } | |
| 207 | + | let len = usize::from_str_radix( | |
| 208 | + | std::str::from_utf8(&header).map_err(|_| proto_err("non-utf8 pkt-line length"))?, | |
| 209 | + | 16, | |
| 210 | + | ) | |
| 211 | + | .map_err(|_| proto_err("invalid pkt-line length"))?; | |
| 212 | + | ||
| 213 | + | match len { | |
| 214 | + | 0 => Ok(Some(Pkt::Flush)), | |
| 215 | + | 1 => Ok(Some(Pkt::Delim)), | |
| 216 | + | 2 => Ok(Some(Pkt::ResponseEnd)), | |
| 217 | + | 3 => Err(proto_err("invalid pkt-line length 0003")), | |
| 218 | + | _ => { | |
| 219 | + | let mut payload = vec![0u8; len - 4]; | |
| 220 | + | stream.read_exact(&mut payload).await?; | |
| 221 | + | Ok(Some(Pkt::Line(len, payload))) | |
| 222 | + | } | |
| 223 | + | } | |
| 224 | + | } | |
| 225 | + | ||
| 226 | + | fn proto_err(message: &'static str) -> crate::error::Error { | |
| 227 | + | std::io::Error::new(std::io::ErrorKind::InvalidData, message).into() | |
| 228 | + | } |
addedcrates/anvil-ssh/Cargo.toml+16 −0
| 1 | + | [package] | |
| 2 | + | name = "anvil-ssh" | |
| 3 | + | version.workspace = true | |
| 4 | + | edition.workspace = true | |
| 5 | + | license.workspace = true | |
| 6 | + | repository.workspace = true | |
| 7 | + | rust-version.workspace = true | |
| 8 | + | description = "git-over-SSH transport for anvil (russh), delegating to anvil-git. Added in milestone 6." | |
| 9 | + | ||
| 10 | + | [dependencies] | |
| 11 | + | anvil-core.workspace = true | |
| 12 | + | anvil-git.workspace = true | |
| 13 | + | russh.workspace = true | |
| 14 | + | tokio.workspace = true | |
| 15 | + | tracing.workspace = true | |
| 16 | + | rand.workspace = true |
addedcrates/anvil-ssh/src/lib.rs+275 −0
| 1 | + | //! git-over-SSH transport for anvil, built on `russh` (pure Rust — no OpenSSH). | |
| 2 | + | //! | |
| 3 | + | //! Authenticates by public key, then handles `git-upload-pack` / | |
| 4 | + | //! `git-receive-pack` `exec` requests by running the transport-agnostic engine | |
| 5 | + | //! in [`anvil_git::ssh`] over the channel's byte stream. | |
| 6 | + | //! | |
| 7 | + | //! The SSH bind address is configurable (`[ssh] listen` in the config). | |
| 8 | + | ||
| 9 | + | use std::net::SocketAddr; | |
| 10 | + | use std::path::{ | |
| 11 | + | Path, | |
| 12 | + | PathBuf, | |
| 13 | + | }; | |
| 14 | + | use std::sync::Arc; | |
| 15 | + | use std::time::Duration; | |
| 16 | + | ||
| 17 | + | use anvil_core::{ | |
| 18 | + | App, | |
| 19 | + | Error as CoreError, | |
| 20 | + | Result as CoreResult, | |
| 21 | + | access, | |
| 22 | + | repos, | |
| 23 | + | users, | |
| 24 | + | }; | |
| 25 | + | use anvil_git::ssh as git_ssh; | |
| 26 | + | use russh::keys::ssh_key::{ | |
| 27 | + | HashAlg, | |
| 28 | + | LineEnding, | |
| 29 | + | PublicKey, | |
| 30 | + | }; | |
| 31 | + | use russh::keys::{ | |
| 32 | + | Algorithm, | |
| 33 | + | PrivateKey, | |
| 34 | + | }; | |
| 35 | + | use russh::server::{ | |
| 36 | + | self, | |
| 37 | + | Auth, | |
| 38 | + | Handler, | |
| 39 | + | Msg, | |
| 40 | + | Server as _, | |
| 41 | + | Session, | |
| 42 | + | }; | |
| 43 | + | use russh::{ | |
| 44 | + | Channel, | |
| 45 | + | ChannelId, | |
| 46 | + | }; | |
| 47 | + | use tokio::net::TcpListener; | |
| 48 | + | ||
| 49 | + | /// Bind to the configured SSH address and serve git over SSH until shutdown. | |
| 50 | + | pub async fn serve(app: App) -> CoreResult<()> { | |
| 51 | + | let listen = app.config.ssh.listen.clone(); | |
| 52 | + | let host_key = load_or_create_host_key(&app.config.data_dir)?; | |
| 53 | + | ||
| 54 | + | let config = Arc::new(server::Config { | |
| 55 | + | inactivity_timeout: Some(Duration::from_secs(3600)), | |
| 56 | + | auth_rejection_time: Duration::from_secs(1), | |
| 57 | + | keys: vec![host_key], | |
| 58 | + | ..Default::default() | |
| 59 | + | }); | |
| 60 | + | ||
| 61 | + | let listener = TcpListener::bind(&listen).await?; | |
| 62 | + | tracing::info!("anvil ssh server listening on {listen}"); | |
| 63 | + | ||
| 64 | + | let mut server = GitSshServer { app }; | |
| 65 | + | server | |
| 66 | + | .run_on_socket(config, &listener) | |
| 67 | + | .await | |
| 68 | + | .map_err(|e| CoreError::Storage(format!("ssh server: {e}")))?; | |
| 69 | + | Ok(()) | |
| 70 | + | } | |
| 71 | + | ||
| 72 | + | /// Load the persistent ed25519 host key, generating and saving it on first run | |
| 73 | + | /// so the server identity is stable across restarts. | |
| 74 | + | fn load_or_create_host_key(data_dir: &Path) -> CoreResult<PrivateKey> { | |
| 75 | + | let path = data_dir.join("ssh_host_ed25519_key"); | |
| 76 | + | if path.exists() { | |
| 77 | + | let pem = std::fs::read_to_string(&path)?; | |
| 78 | + | return PrivateKey::from_openssh(&pem).map_err(|e| { | |
| 79 | + | CoreError::Config(format!("reading ssh host key {}: {e}", path.display())) | |
| 80 | + | }); | |
| 81 | + | } | |
| 82 | + | ||
| 83 | + | let key = PrivateKey::random(&mut rand::rng(), Algorithm::Ed25519) | |
| 84 | + | .map_err(|e| CoreError::Config(format!("generating ssh host key: {e}")))?; | |
| 85 | + | let pem = key | |
| 86 | + | .to_openssh(LineEnding::LF) | |
| 87 | + | .map_err(|e| CoreError::Config(format!("encoding ssh host key: {e}")))?; | |
| 88 | + | std::fs::write(&path, pem.as_bytes())?; | |
| 89 | + | #[cfg(unix)] | |
| 90 | + | { | |
| 91 | + | use std::os::unix::fs::PermissionsExt; | |
| 92 | + | let _ = std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600)); | |
| 93 | + | } | |
| 94 | + | tracing::info!("generated ssh host key at {}", path.display()); | |
| 95 | + | Ok(key) | |
| 96 | + | } | |
| 97 | + | ||
| 98 | + | struct GitSshServer { | |
| 99 | + | app: App, | |
| 100 | + | } | |
| 101 | + | ||
| 102 | + | impl server::Server for GitSshServer { | |
| 103 | + | type Handler = GitSshSession; | |
| 104 | + | ||
| 105 | + | fn new_client(&mut self, _peer: Option<SocketAddr>) -> GitSshSession { | |
| 106 | + | GitSshSession { | |
| 107 | + | app: self.app.clone(), | |
| 108 | + | channel: None, | |
| 109 | + | protocol_v2: false, | |
| 110 | + | authed_user: None, | |
| 111 | + | } | |
| 112 | + | } | |
| 113 | + | } | |
| 114 | + | ||
| 115 | + | struct GitSshSession { | |
| 116 | + | app: App, | |
| 117 | + | /// The session channel, taken in `channel_open_session` and consumed by the | |
| 118 | + | /// git protocol task in `exec_request`. | |
| 119 | + | channel: Option<Channel<Msg>>, | |
| 120 | + | /// Set if the client requested git protocol v2 via the `GIT_PROTOCOL` env. | |
| 121 | + | protocol_v2: bool, | |
| 122 | + | /// The user id authenticated by public key, set in `auth_publickey`. | |
| 123 | + | authed_user: Option<i64>, | |
| 124 | + | } | |
| 125 | + | ||
| 126 | + | impl Handler for GitSshSession { | |
| 127 | + | type Error = russh::Error; | |
| 128 | + | ||
| 129 | + | async fn auth_publickey(&mut self, _user: &str, key: &PublicKey) -> Result<Auth, Self::Error> { | |
| 130 | + | // Authenticate by matching the (signature-verified) key's fingerprint to | |
| 131 | + | // a registered user. Unknown keys are rejected. Per-repo authorization | |
| 132 | + | // is a later milestone; for now any authenticated user has full access. | |
| 133 | + | let fingerprint = key.fingerprint(HashAlg::Sha256).to_string(); | |
| 134 | + | match anvil_core::ssh_keys::find_user_id_by_fingerprint(&self.app.db, &fingerprint).await { | |
| 135 | + | Ok(Some(user_id)) => { | |
| 136 | + | self.authed_user = Some(user_id); | |
| 137 | + | tracing::info!("ssh auth: accepted key {fingerprint} (user {user_id})"); | |
| 138 | + | Ok(Auth::Accept) | |
| 139 | + | } | |
| 140 | + | Ok(None) => { | |
| 141 | + | tracing::info!("ssh auth: rejected unknown key {fingerprint}"); | |
| 142 | + | Ok(Auth::reject()) | |
| 143 | + | } | |
| 144 | + | Err(e) => { | |
| 145 | + | tracing::error!("ssh auth: key lookup failed: {e}"); | |
| 146 | + | Ok(Auth::reject()) | |
| 147 | + | } | |
| 148 | + | } | |
| 149 | + | } | |
| 150 | + | ||
| 151 | + | async fn channel_open_session( | |
| 152 | + | &mut self, | |
| 153 | + | channel: Channel<Msg>, | |
| 154 | + | _session: &mut Session, | |
| 155 | + | ) -> Result<bool, Self::Error> { | |
| 156 | + | self.channel = Some(channel); | |
| 157 | + | Ok(true) | |
| 158 | + | } | |
| 159 | + | ||
| 160 | + | async fn env_request( | |
| 161 | + | &mut self, | |
| 162 | + | _channel: ChannelId, | |
| 163 | + | name: &str, | |
| 164 | + | value: &str, | |
| 165 | + | _session: &mut Session, | |
| 166 | + | ) -> Result<(), Self::Error> { | |
| 167 | + | if name == "GIT_PROTOCOL" && value.split(':').any(|v| v.trim() == "version=2") { | |
| 168 | + | self.protocol_v2 = true; | |
| 169 | + | } | |
| 170 | + | Ok(()) | |
| 171 | + | } | |
| 172 | + | ||
| 173 | + | async fn exec_request( | |
| 174 | + | &mut self, | |
| 175 | + | channel_id: ChannelId, | |
| 176 | + | data: &[u8], | |
| 177 | + | session: &mut Session, | |
| 178 | + | ) -> Result<(), Self::Error> { | |
| 179 | + | let command = String::from_utf8_lossy(data); | |
| 180 | + | let Some((service, rel)) = git_ssh::parse_command(&command) else { | |
| 181 | + | return fail(session, channel_id, "unsupported command"); | |
| 182 | + | }; | |
| 183 | + | let need_write = service == anvil_git::Service::ReceivePack; | |
| 184 | + | let path = match authorize_repo(&self.app, self.authed_user, &rel, need_write).await { | |
| 185 | + | Ok(path) => path, | |
| 186 | + | Err(message) => return fail(session, channel_id, message), | |
| 187 | + | }; | |
| 188 | + | let Some(channel) = self.channel.take() else { | |
| 189 | + | return fail(session, channel_id, "no session channel"); | |
| 190 | + | }; | |
| 191 | + | ||
| 192 | + | tracing::info!( | |
| 193 | + | "ssh {} on {rel} (user {:?})", | |
| 194 | + | service.as_str(), | |
| 195 | + | self.authed_user | |
| 196 | + | ); | |
| 197 | + | ||
| 198 | + | let protocol_v2 = self.protocol_v2; | |
| 199 | + | let handle = session.handle(); | |
| 200 | + | session.channel_success(channel_id)?; | |
| 201 | + | ||
| 202 | + | tokio::spawn(async move { | |
| 203 | + | let stream = channel.into_stream(); | |
| 204 | + | let code = match git_ssh::serve(&path, service, protocol_v2, stream).await { | |
| 205 | + | Ok(()) => 0, | |
| 206 | + | Err(e) => { | |
| 207 | + | tracing::error!("git ssh {}: {e}", service.as_str()); | |
| 208 | + | 1 | |
| 209 | + | } | |
| 210 | + | }; | |
| 211 | + | let _ = handle.exit_status_request(channel_id, code).await; | |
| 212 | + | let _ = handle.eof(channel_id).await; | |
| 213 | + | let _ = handle.close(channel_id).await; | |
| 214 | + | }); | |
| 215 | + | Ok(()) | |
| 216 | + | } | |
| 217 | + | } | |
| 218 | + | ||
| 219 | + | /// Write a one-line error to the channel and close it with a failure status. | |
| 220 | + | fn fail(session: &mut Session, channel_id: ChannelId, message: &str) -> Result<(), russh::Error> { | |
| 221 | + | let _ = session.data(channel_id, format!("{message}\n").into_bytes()); | |
| 222 | + | session.exit_status_request(channel_id, 1)?; | |
| 223 | + | session.close(channel_id)?; | |
| 224 | + | Ok(()) | |
| 225 | + | } | |
| 226 | + | ||
| 227 | + | /// Resolve an SSH repo path (`owner/name[.git]`) to an existing bare repo and | |
| 228 | + | /// enforce access for the authenticated user. Returns the on-disk path or a | |
| 229 | + | /// short error message. Rejects traversal. | |
| 230 | + | async fn authorize_repo( | |
| 231 | + | app: &App, | |
| 232 | + | authed_user: Option<i64>, | |
| 233 | + | rel: &str, | |
| 234 | + | need_write: bool, | |
| 235 | + | ) -> Result<PathBuf, &'static str> { | |
| 236 | + | let (owner, repo) = rel | |
| 237 | + | .trim_start_matches('/') | |
| 238 | + | .split_once('/') | |
| 239 | + | .ok_or("invalid repository path")?; | |
| 240 | + | let name = repo.strip_suffix(".git").unwrap_or(repo); | |
| 241 | + | let bad = |s: &str| s.is_empty() || s.contains("..") || s.contains('\\') || s.contains('/'); | |
| 242 | + | if bad(owner) || bad(name) { | |
| 243 | + | return Err("invalid repository path"); | |
| 244 | + | } | |
| 245 | + | ||
| 246 | + | let owner_user = users::find_by_username(&app.db, owner) | |
| 247 | + | .await | |
| 248 | + | .ok() | |
| 249 | + | .flatten() | |
| 250 | + | .ok_or("repository not found")?; | |
| 251 | + | let repo = repos::find(&app.db, owner_user.id, name) | |
| 252 | + | .await | |
| 253 | + | .ok() | |
| 254 | + | .flatten() | |
| 255 | + | .ok_or("repository not found")?; | |
| 256 | + | ||
| 257 | + | let viewer = match authed_user { | |
| 258 | + | Some(id) => users::find_by_id(&app.db, id).await.ok().flatten(), | |
| 259 | + | None => None, | |
| 260 | + | }; | |
| 261 | + | let allowed = if need_write { | |
| 262 | + | access::can_write(&repo, viewer.as_ref()) | |
| 263 | + | } else { | |
| 264 | + | access::can_read(&repo, viewer.as_ref()) | |
| 265 | + | }; | |
| 266 | + | if !allowed { | |
| 267 | + | return Err("access denied"); | |
| 268 | + | } | |
| 269 | + | ||
| 270 | + | let path = anvil_core::storage::repo_path(&app.config.repositories_dir(), owner, name); | |
| 271 | + | if !path.exists() { | |
| 272 | + | return Err("repository not found"); | |
| 273 | + | } | |
| 274 | + | Ok(path) | |
| 275 | + | } |
addedcrates/anvil-web/Cargo.toml+24 −0
| 1 | + | [package] | |
| 2 | + | name = "anvil-web" | |
| 3 | + | version.workspace = true | |
| 4 | + | edition.workspace = true | |
| 5 | + | license.workspace = true | |
| 6 | + | repository.workspace = true | |
| 7 | + | rust-version.workspace = true | |
| 8 | + | description = "HTTP server for anvil: web UI and smart-HTTP git endpoints (axum)." | |
| 9 | + | ||
| 10 | + | [dependencies] | |
| 11 | + | anvil-core.workspace = true | |
| 12 | + | anvil-git.workspace = true | |
| 13 | + | axum.workspace = true | |
| 14 | + | axum-extra.workspace = true | |
| 15 | + | tokio.workspace = true | |
| 16 | + | tokio-util.workspace = true | |
| 17 | + | tower-http.workspace = true | |
| 18 | + | tracing.workspace = true | |
| 19 | + | serde.workspace = true | |
| 20 | + | base64.workspace = true | |
| 21 | + | maud.workspace = true | |
| 22 | + | similar.workspace = true | |
| 23 | + | syntect.workspace = true | |
| 24 | + | time.workspace = true |
addedcrates/anvil-web/assets/htmx.min.js+1 −0
| 1 | + | var htmx=function(){"use strict";const Q={onLoad:null,process:null,on:null,off:null,trigger:null,ajax:null,find:null,findAll:null,closest:null,values:function(e,t){const n=cn(e,t||"post");return n.values},remove:null,addClass:null,removeClass:null,toggleClass:null,takeClass:null,swap:null,defineExtension:null,removeExtension:null,logAll:null,logNone:null,logger:null,config:{historyEnabled:true,historyCacheSize:10,refreshOnHistoryMiss:false,defaultSwapStyle:"innerHTML",defaultSwapDelay:0,defaultSettleDelay:20,includeIndicatorStyles:true,indicatorClass:"htmx-indicator",requestClass:"htmx-request",addedClass:"htmx-added",settlingClass:"htmx-settling",swappingClass:"htmx-swapping",allowEval:true,allowScriptTags:true,inlineScriptNonce:"",inlineStyleNonce:"",attributesToSettle:["class","style","width","height"],withCredentials:false,timeout:0,wsReconnectDelay:"full-jitter",wsBinaryType:"blob",disableSelector:"[hx-disable], [data-hx-disable]",scrollBehavior:"instant",defaultFocusScroll:false,getCacheBusterParam:false,globalViewTransitions:false,methodsThatUseUrlParams:["get","delete"],selfRequestsOnly:true,ignoreTitle:false,scrollIntoViewOnBoost:true,triggerSpecsCache:null,disableInheritance:false,responseHandling:[{code:"204",swap:false},{code:"[23]..",swap:true},{code:"[45]..",swap:false,error:true}],allowNestedOobSwaps:true},parseInterval:null,_:null,version:"2.0.4"};Q.onLoad=j;Q.process=kt;Q.on=ye;Q.off=be;Q.trigger=he;Q.ajax=Rn;Q.find=u;Q.findAll=x;Q.closest=g;Q.remove=z;Q.addClass=K;Q.removeClass=G;Q.toggleClass=W;Q.takeClass=Z;Q.swap=$e;Q.defineExtension=Fn;Q.removeExtension=Bn;Q.logAll=V;Q.logNone=_;Q.parseInterval=d;Q._=e;const n={addTriggerHandler:St,bodyContains:le,canAccessLocalStorage:B,findThisElement:Se,filterValues:hn,swap:$e,hasAttribute:s,getAttributeValue:te,getClosestAttributeValue:re,getClosestMatch:o,getExpressionVars:En,getHeaders:fn,getInputValues:cn,getInternalData:ie,getSwapSpecification:gn,getTriggerSpecs:st,getTarget:Ee,makeFragment:P,mergeObjects:ce,makeSettleInfo:xn,oobSwap:He,querySelectorExt:ae,settleImmediately:Kt,shouldCancel:ht,triggerEvent:he,triggerErrorEvent:fe,withExtensions:Ft};const r=["get","post","put","delete","patch"];const H=r.map(function(e){return"[hx-"+e+"], [data-hx-"+e+"]"}).join(", ");function d(e){if(e==undefined){return undefined}let t=NaN;if(e.slice(-2)=="ms"){t=parseFloat(e.slice(0,-2))}else if(e.slice(-1)=="s"){t=parseFloat(e.slice(0,-1))*1e3}else if(e.slice(-1)=="m"){t=parseFloat(e.slice(0,-1))*1e3*60}else{t=parseFloat(e)}return isNaN(t)?undefined:t}function ee(e,t){return e instanceof Element&&e.getAttribute(t)}function s(e,t){return!!e.hasAttribute&&(e.hasAttribute(t)||e.hasAttribute("data-"+t))}function te(e,t){return ee(e,t)||ee(e,"data-"+t)}function c(e){const t=e.parentElement;if(!t&&e.parentNode instanceof ShadowRoot)return e.parentNode;return t}function ne(){return document}function m(e,t){return e.getRootNode?e.getRootNode({composed:t}):ne()}function o(e,t){while(e&&!t(e)){e=c(e)}return e||null}function i(e,t,n){const r=te(t,n);const o=te(t,"hx-disinherit");var i=te(t,"hx-inherit");if(e!==t){if(Q.config.disableInheritance){if(i&&(i==="*"||i.split(" ").indexOf(n)>=0)){return r}else{return null}}if(o&&(o==="*"||o.split(" ").indexOf(n)>=0)){return"unset"}}return r}function re(t,n){let r=null;o(t,function(e){return!!(r=i(t,ue(e),n))});if(r!=="unset"){return r}}function h(e,t){const n=e instanceof Element&&(e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.webkitMatchesSelector||e.oMatchesSelector);return!!n&&n.call(e,t)}function T(e){const t=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i;const n=t.exec(e);if(n){return n[1].toLowerCase()}else{return""}}function q(e){const t=new DOMParser;return t.parseFromString(e,"text/html")}function L(e,t){while(t.childNodes.length>0){e.append(t.childNodes[0])}}function A(e){const t=ne().createElement("script");se(e.attributes,function(e){t.setAttribute(e.name,e.value)});t.textContent=e.textContent;t.async=false;if(Q.config.inlineScriptNonce){t.nonce=Q.config.inlineScriptNonce}return t}function N(e){return e.matches("script")&&(e.type==="text/javascript"||e.type==="module"||e.type==="")}function I(e){Array.from(e.querySelectorAll("script")).forEach(e=>{if(N(e)){const t=A(e);const n=e.parentNode;try{n.insertBefore(t,e)}catch(e){O(e)}finally{e.remove()}}})}function P(e){const t=e.replace(/<head(\s[^>]*)?>[\s\S]*?<\/head>/i,"");const n=T(t);let r;if(n==="html"){r=new DocumentFragment;const i=q(e);L(r,i.body);r.title=i.title}else if(n==="body"){r=new DocumentFragment;const i=q(t);L(r,i.body);r.title=i.title}else{const i=q('<body><template class="internal-htmx-wrapper">'+t+"</template></body>");r=i.querySelector("template").content;r.title=i.title;var o=r.querySelector("title");if(o&&o.parentNode===r){o.remove();r.title=o.innerText}}if(r){if(Q.config.allowScriptTags){I(r)}else{r.querySelectorAll("script").forEach(e=>e.remove())}}return r}function oe(e){if(e){e()}}function t(e,t){return Object.prototype.toString.call(e)==="[object "+t+"]"}function k(e){return typeof e==="function"}function D(e){return t(e,"Object")}function ie(e){const t="htmx-internal-data";let n=e[t];if(!n){n=e[t]={}}return n}function M(t){const n=[];if(t){for(let e=0;e<t.length;e++){n.push(t[e])}}return n}function se(t,n){if(t){for(let e=0;e<t.length;e++){n(t[e])}}}function X(e){const t=e.getBoundingClientRect();const n=t.top;const r=t.bottom;return n<window.innerHeight&&r>=0}function le(e){return e.getRootNode({composed:true})===document}function F(e){return e.trim().split(/\s+/)}function ce(e,t){for(const n in t){if(t.hasOwnProperty(n)){e[n]=t[n]}}return e}function S(e){try{return JSON.parse(e)}catch(e){O(e);return null}}function B(){const e="htmx:localStorageTest";try{localStorage.setItem(e,e);localStorage.removeItem(e);return true}catch(e){return false}}function U(t){try{const e=new URL(t);if(e){t=e.pathname+e.search}if(!/^\/$/.test(t)){t=t.replace(/\/+$/,"")}return t}catch(e){return t}}function e(e){return vn(ne().body,function(){return eval(e)})}function j(t){const e=Q.on("htmx:load",function(e){t(e.detail.elt)});return e}function V(){Q.logger=function(e,t,n){if(console){console.log(t,e,n)}}}function _(){Q.logger=null}function u(e,t){if(typeof e!=="string"){return e.querySelector(t)}else{return u(ne(),e)}}function x(e,t){if(typeof e!=="string"){return e.querySelectorAll(t)}else{return x(ne(),e)}}function E(){return window}function z(e,t){e=y(e);if(t){E().setTimeout(function(){z(e);e=null},t)}else{c(e).removeChild(e)}}function ue(e){return e instanceof Element?e:null}function $(e){return e instanceof HTMLElement?e:null}function J(e){return typeof e==="string"?e:null}function f(e){return e instanceof Element||e instanceof Document||e instanceof DocumentFragment?e:null}function K(e,t,n){e=ue(y(e));if(!e){return}if(n){E().setTimeout(function(){K(e,t);e=null},n)}else{e.classList&&e.classList.add(t)}}function G(e,t,n){let r=ue(y(e));if(!r){return}if(n){E().setTimeout(function(){G(r,t);r=null},n)}else{if(r.classList){r.classList.remove(t);if(r.classList.length===0){r.removeAttribute("class")}}}}function W(e,t){e=y(e);e.classList.toggle(t)}function Z(e,t){e=y(e);se(e.parentElement.children,function(e){G(e,t)});K(ue(e),t)}function g(e,t){e=ue(y(e));if(e&&e.closest){return e.closest(t)}else{do{if(e==null||h(e,t)){return e}}while(e=e&&ue(c(e)));return null}}function l(e,t){return e.substring(0,t.length)===t}function Y(e,t){return e.substring(e.length-t.length)===t}function ge(e){const t=e.trim();if(l(t,"<")&&Y(t,"/>")){return t.substring(1,t.length-2)}else{return t}}function p(t,r,n){if(r.indexOf("global ")===0){return p(t,r.slice(7),true)}t=y(t);const o=[];{let t=0;let n=0;for(let e=0;e<r.length;e++){const l=r[e];if(l===","&&t===0){o.push(r.substring(n,e));n=e+1;continue}if(l==="<"){t++}else if(l==="/"&&e<r.length-1&&r[e+1]===">"){t--}}if(n<r.length){o.push(r.substring(n))}}const i=[];const s=[];while(o.length>0){const r=ge(o.shift());let e;if(r.indexOf("closest ")===0){e=g(ue(t),ge(r.substr(8)))}else if(r.indexOf("find ")===0){e=u(f(t),ge(r.substr(5)))}else if(r==="next"||r==="nextElementSibling"){e=ue(t).nextElementSibling}else if(r.indexOf("next ")===0){e=pe(t,ge(r.substr(5)),!!n)}else if(r==="previous"||r==="previousElementSibling"){e=ue(t).previousElementSibling}else if(r.indexOf("previous ")===0){e=me(t,ge(r.substr(9)),!!n)}else if(r==="document"){e=document}else if(r==="window"){e=window}else if(r==="body"){e=document.body}else if(r==="root"){e=m(t,!!n)}else if(r==="host"){e=t.getRootNode().host}else{s.push(r)}if(e){i.push(e)}}if(s.length>0){const e=s.join(",");const c=f(m(t,!!n));i.push(...M(c.querySelectorAll(e)))}return i}var pe=function(t,e,n){const r=f(m(t,n)).querySelectorAll(e);for(let e=0;e<r.length;e++){const o=r[e];if(o.compareDocumentPosition(t)===Node.DOCUMENT_POSITION_PRECEDING){return o}}};var me=function(t,e,n){const r=f(m(t,n)).querySelectorAll(e);for(let e=r.length-1;e>=0;e--){const o=r[e];if(o.compareDocumentPosition(t)===Node.DOCUMENT_POSITION_FOLLOWING){return o}}};function ae(e,t){if(typeof e!=="string"){return p(e,t)[0]}else{return p(ne().body,e)[0]}}function y(e,t){if(typeof e==="string"){return u(f(t)||document,e)}else{return e}}function xe(e,t,n,r){if(k(t)){return{target:ne().body,event:J(e),listener:t,options:n}}else{return{target:y(e),event:J(t),listener:n,options:r}}}function ye(t,n,r,o){Vn(function(){const e=xe(t,n,r,o);e.target.addEventListener(e.event,e.listener,e.options)});const e=k(n);return e?n:r}function be(t,n,r){Vn(function(){const e=xe(t,n,r);e.target.removeEventListener(e.event,e.listener)});return k(n)?n:r}const ve=ne().createElement("output");function we(e,t){const n=re(e,t);if(n){if(n==="this"){return[Se(e,t)]}else{const r=p(e,n);if(r.length===0){O('The selector "'+n+'" on '+t+" returned no matches!");return[ve]}else{return r}}}}function Se(e,t){return ue(o(e,function(e){return te(ue(e),t)!=null}))}function Ee(e){const t=re(e,"hx-target");if(t){if(t==="this"){return Se(e,"hx-target")}else{return ae(e,t)}}else{const n=ie(e);if(n.boosted){return ne().body}else{return e}}}function Ce(t){const n=Q.config.attributesToSettle;for(let e=0;e<n.length;e++){if(t===n[e]){return true}}return false}function Oe(t,n){se(t.attributes,function(e){if(!n.hasAttribute(e.name)&&Ce(e.name)){t.removeAttribute(e.name)}});se(n.attributes,function(e){if(Ce(e.name)){t.setAttribute(e.name,e.value)}})}function Re(t,e){const n=Un(e);for(let e=0;e<n.length;e++){const r=n[e];try{if(r.isInlineSwap(t)){return true}}catch(e){O(e)}}return t==="outerHTML"}function He(e,o,i,t){t=t||ne();let n="#"+ee(o,"id");let s="outerHTML";if(e==="true"){}else if(e.indexOf(":")>0){s=e.substring(0,e.indexOf(":"));n=e.substring(e.indexOf(":")+1)}else{s=e}o.removeAttribute("hx-swap-oob");o.removeAttribute("data-hx-swap-oob");const r=p(t,n,false);if(r){se(r,function(e){let t;const n=o.cloneNode(true);t=ne().createDocumentFragment();t.appendChild(n);if(!Re(s,e)){t=f(n)}const r={shouldSwap:true,target:e,fragment:t};if(!he(e,"htmx:oobBeforeSwap",r))return;e=r.target;if(r.shouldSwap){qe(t);_e(s,e,e,t,i);Te()}se(i.elts,function(e){he(e,"htmx:oobAfterSwap",r)})});o.parentNode.removeChild(o)}else{o.parentNode.removeChild(o);fe(ne().body,"htmx:oobErrorNoTarget",{content:o})}return e}function Te(){const e=u("#--htmx-preserve-pantry--");if(e){for(const t of[...e.children]){const n=u("#"+t.id);n.parentNode.moveBefore(t,n);n.remove()}e.remove()}}function qe(e){se(x(e,"[hx-preserve], [data-hx-preserve]"),function(e){const t=te(e,"id");const n=ne().getElementById(t);if(n!=null){if(e.moveBefore){let e=u("#--htmx-preserve-pantry--");if(e==null){ne().body.insertAdjacentHTML("afterend","<div id='--htmx-preserve-pantry--'></div>");e=u("#--htmx-preserve-pantry--")}e.moveBefore(n,null)}else{e.parentNode.replaceChild(n,e)}}})}function Le(l,e,c){se(e.querySelectorAll("[id]"),function(t){const n=ee(t,"id");if(n&&n.length>0){const r=n.replace("'","\\'");const o=t.tagName.replace(":","\\:");const e=f(l);const i=e&&e.querySelector(o+"[id='"+r+"']");if(i&&i!==e){const s=t.cloneNode();Oe(t,i);c.tasks.push(function(){Oe(t,s)})}}})}function Ae(e){return function(){G(e,Q.config.addedClass);kt(ue(e));Ne(f(e));he(e,"htmx:load")}}function Ne(e){const t="[autofocus]";const n=$(h(e,t)?e:e.querySelector(t));if(n!=null){n.focus()}}function a(e,t,n,r){Le(e,n,r);while(n.childNodes.length>0){const o=n.firstChild;K(ue(o),Q.config.addedClass);e.insertBefore(o,t);if(o.nodeType!==Node.TEXT_NODE&&o.nodeType!==Node.COMMENT_NODE){r.tasks.push(Ae(o))}}}function Ie(e,t){let n=0;while(n<e.length){t=(t<<5)-t+e.charCodeAt(n++)|0}return t}function Pe(t){let n=0;if(t.attributes){for(let e=0;e<t.attributes.length;e++){const r=t.attributes[e];if(r.value){n=Ie(r.name,n);n=Ie(r.value,n)}}}return n}function ke(t){const n=ie(t);if(n.onHandlers){for(let e=0;e<n.onHandlers.length;e++){const r=n.onHandlers[e];be(t,r.event,r.listener)}delete n.onHandlers}}function De(e){const t=ie(e);if(t.timeout){clearTimeout(t.timeout)}if(t.listenerInfos){se(t.listenerInfos,function(e){if(e.on){be(e.on,e.trigger,e.listener)}})}ke(e);se(Object.keys(t),function(e){if(e!=="firstInitCompleted")delete t[e]})}function b(e){he(e,"htmx:beforeCleanupElement");De(e);if(e.children){se(e.children,function(e){b(e)})}}function Me(t,e,n){if(t instanceof Element&&t.tagName==="BODY"){return Ve(t,e,n)}let r;const o=t.previousSibling;const i=c(t);if(!i){return}a(i,t,e,n);if(o==null){r=i.firstChild}else{r=o.nextSibling}n.elts=n.elts.filter(function(e){return e!==t});while(r&&r!==t){if(r instanceof Element){n.elts.push(r)}r=r.nextSibling}b(t);if(t instanceof Element){t.remove()}else{t.parentNode.removeChild(t)}}function Xe(e,t,n){return a(e,e.firstChild,t,n)}function Fe(e,t,n){return a(c(e),e,t,n)}function Be(e,t,n){return a(e,null,t,n)}function Ue(e,t,n){return a(c(e),e.nextSibling,t,n)}function je(e){b(e);const t=c(e);if(t){return t.removeChild(e)}}function Ve(e,t,n){const r=e.firstChild;a(e,r,t,n);if(r){while(r.nextSibling){b(r.nextSibling);e.removeChild(r.nextSibling)}b(r);e.removeChild(r)}}function _e(t,e,n,r,o){switch(t){case"none":return;case"outerHTML":Me(n,r,o);return;case"afterbegin":Xe(n,r,o);return;case"beforebegin":Fe(n,r,o);return;case"beforeend":Be(n,r,o);return;case"afterend":Ue(n,r,o);return;case"delete":je(n);return;default:var i=Un(e);for(let e=0;e<i.length;e++){const s=i[e];try{const l=s.handleSwap(t,n,r,o);if(l){if(Array.isArray(l)){for(let e=0;e<l.length;e++){const c=l[e];if(c.nodeType!==Node.TEXT_NODE&&c.nodeType!==Node.COMMENT_NODE){o.tasks.push(Ae(c))}}}return}}catch(e){O(e)}}if(t==="innerHTML"){Ve(n,r,o)}else{_e(Q.config.defaultSwapStyle,e,n,r,o)}}}function ze(e,n,r){var t=x(e,"[hx-swap-oob], [data-hx-swap-oob]");se(t,function(e){if(Q.config.allowNestedOobSwaps||e.parentElement===null){const t=te(e,"hx-swap-oob");if(t!=null){He(t,e,n,r)}}else{e.removeAttribute("hx-swap-oob");e.removeAttribute("data-hx-swap-oob")}});return t.length>0}function $e(e,t,r,o){if(!o){o={}}e=y(e);const i=o.contextElement?m(o.contextElement,false):ne();const n=document.activeElement;let s={};try{s={elt:n,start:n?n.selectionStart:null,end:n?n.selectionEnd:null}}catch(e){}const l=xn(e);if(r.swapStyle==="textContent"){e.textContent=t}else{let n=P(t);l.title=n.title;if(o.selectOOB){const u=o.selectOOB.split(",");for(let t=0;t<u.length;t++){const a=u[t].split(":",2);let e=a[0].trim();if(e.indexOf("#")===0){e=e.substring(1)}const f=a[1]||"true";const h=n.querySelector("#"+e);if(h){He(f,h,l,i)}}}ze(n,l,i);se(x(n,"template"),function(e){if(e.content&&ze(e.content,l,i)){e.remove()}});if(o.select){const d=ne().createDocumentFragment();se(n.querySelectorAll(o.select),function(e){d.appendChild(e)});n=d}qe(n);_e(r.swapStyle,o.contextElement,e,n,l);Te()}if(s.elt&&!le(s.elt)&&ee(s.elt,"id")){const g=document.getElementById(ee(s.elt,"id"));const p={preventScroll:r.focusScroll!==undefined?!r.focusScroll:!Q.config.defaultFocusScroll};if(g){if(s.start&&g.setSelectionRange){try{g.setSelectionRange(s.start,s.end)}catch(e){}}g.focus(p)}}e.classList.remove(Q.config.swappingClass);se(l.elts,function(e){if(e.classList){e.classList.add(Q.config.settlingClass)}he(e,"htmx:afterSwap",o.eventInfo)});if(o.afterSwapCallback){o.afterSwapCallback()}if(!r.ignoreTitle){kn(l.title)}const c=function(){se(l.tasks,function(e){e.call()});se(l.elts,function(e){if(e.classList){e.classList.remove(Q.config.settlingClass)}he(e,"htmx:afterSettle",o.eventInfo)});if(o.anchor){const e=ue(y("#"+o.anchor));if(e){e.scrollIntoView({block:"start",behavior:"auto"})}}yn(l.elts,r);if(o.afterSettleCallback){o.afterSettleCallback()}};if(r.settleDelay>0){E().setTimeout(c,r.settleDelay)}else{c()}}function Je(e,t,n){const r=e.getResponseHeader(t);if(r.indexOf("{")===0){const o=S(r);for(const i in o){if(o.hasOwnProperty(i)){let e=o[i];if(D(e)){n=e.target!==undefined?e.target:n}else{e={value:e}}he(n,i,e)}}}else{const s=r.split(",");for(let e=0;e<s.length;e++){he(n,s[e].trim(),[])}}}const Ke=/\s/;const v=/[\s,]/;const Ge=/[_$a-zA-Z]/;const We=/[_$a-zA-Z0-9]/;const Ze=['"',"'","/"];const w=/[^\s]/;const Ye=/[{(]/;const Qe=/[})]/;function et(e){const t=[];let n=0;while(n<e.length){if(Ge.exec(e.charAt(n))){var r=n;while(We.exec(e.charAt(n+1))){n++}t.push(e.substring(r,n+1))}else if(Ze.indexOf(e.charAt(n))!==-1){const o=e.charAt(n);var r=n;n++;while(n<e.length&&e.charAt(n)!==o){if(e.charAt(n)==="\\"){n++}n++}t.push(e.substring(r,n+1))}else{const i=e.charAt(n);t.push(i)}n++}return t}function tt(e,t,n){return Ge.exec(e.charAt(0))&&e!=="true"&&e!=="false"&&e!=="this"&&e!==n&&t!=="."}function nt(r,o,i){if(o[0]==="["){o.shift();let e=1;let t=" return (function("+i+"){ return (";let n=null;while(o.length>0){const s=o[0];if(s==="]"){e--;if(e===0){if(n===null){t=t+"true"}o.shift();t+=")})";try{const l=vn(r,function(){return Function(t)()},function(){return true});l.source=t;return l}catch(e){fe(ne().body,"htmx:syntax:error",{error:e,source:t});return null}}}else if(s==="["){e++}if(tt(s,n,i)){t+="(("+i+"."+s+") ? ("+i+"."+s+") : (window."+s+"))"}else{t=t+s}n=o.shift()}}}function C(e,t){let n="";while(e.length>0&&!t.test(e[0])){n+=e.shift()}return n}function rt(e){let t;if(e.length>0&&Ye.test(e[0])){e.shift();t=C(e,Qe).trim();e.shift()}else{t=C(e,v)}return t}const ot="input, textarea, select";function it(e,t,n){const r=[];const o=et(t);do{C(o,w);const l=o.length;const c=C(o,/[,\[\s]/);if(c!==""){if(c==="every"){const u={trigger:"every"};C(o,w);u.pollInterval=d(C(o,/[,\[\s]/));C(o,w);var i=nt(e,o,"event");if(i){u.eventFilter=i}r.push(u)}else{const a={trigger:c};var i=nt(e,o,"event");if(i){a.eventFilter=i}C(o,w);while(o.length>0&&o[0]!==","){const f=o.shift();if(f==="changed"){a.changed=true}else if(f==="once"){a.once=true}else if(f==="consume"){a.consume=true}else if(f==="delay"&&o[0]===":"){o.shift();a.delay=d(C(o,v))}else if(f==="from"&&o[0]===":"){o.shift();if(Ye.test(o[0])){var s=rt(o)}else{var s=C(o,v);if(s==="closest"||s==="find"||s==="next"||s==="previous"){o.shift();const h=rt(o);if(h.length>0){s+=" "+h}}}a.from=s}else if(f==="target"&&o[0]===":"){o.shift();a.target=rt(o)}else if(f==="throttle"&&o[0]===":"){o.shift();a.throttle=d(C(o,v))}else if(f==="queue"&&o[0]===":"){o.shift();a.queue=C(o,v)}else if(f==="root"&&o[0]===":"){o.shift();a[f]=rt(o)}else if(f==="threshold"&&o[0]===":"){o.shift();a[f]=C(o,v)}else{fe(e,"htmx:syntax:error",{token:o.shift()})}C(o,w)}r.push(a)}}if(o.length===l){fe(e,"htmx:syntax:error",{token:o.shift()})}C(o,w)}while(o[0]===","&&o.shift());if(n){n[t]=r}return r}function st(e){const t=te(e,"hx-trigger");let n=[];if(t){const r=Q.config.triggerSpecsCache;n=r&&r[t]||it(e,t,r)}if(n.length>0){return n}else if(h(e,"form")){return[{trigger:"submit"}]}else if(h(e,'input[type="button"], input[type="submit"]')){return[{trigger:"click"}]}else if(h(e,ot)){return[{trigger:"change"}]}else{return[{trigger:"click"}]}}function lt(e){ie(e).cancelled=true}function ct(e,t,n){const r=ie(e);r.timeout=E().setTimeout(function(){if(le(e)&&r.cancelled!==true){if(!gt(n,e,Mt("hx:poll:trigger",{triggerSpec:n,target:e}))){t(e)}ct(e,t,n)}},n.pollInterval)}function ut(e){return location.hostname===e.hostname&&ee(e,"href")&&ee(e,"href").indexOf("#")!==0}function at(e){return g(e,Q.config.disableSelector)}function ft(t,n,e){if(t instanceof HTMLAnchorElement&&ut(t)&&(t.target===""||t.target==="_self")||t.tagName==="FORM"&&String(ee(t,"method")).toLowerCase()!=="dialog"){n.boosted=true;let r,o;if(t.tagName==="A"){r="get";o=ee(t,"href")}else{const i=ee(t,"method");r=i?i.toLowerCase():"get";o=ee(t,"action");if(o==null||o===""){o=ne().location.href}if(r==="get"&&o.includes("?")){o=o.replace(/\?[^#]+/,"")}}e.forEach(function(e){pt(t,function(e,t){const n=ue(e);if(at(n)){b(n);return}de(r,o,n,t)},n,e,true)})}}function ht(e,t){const n=ue(t);if(!n){return false}if(e.type==="submit"||e.type==="click"){if(n.tagName==="FORM"){return true}if(h(n,'input[type="submit"], button')&&(h(n,"[form]")||g(n,"form")!==null)){return true}if(n instanceof HTMLAnchorElement&&n.href&&(n.getAttribute("href")==="#"||n.getAttribute("href").indexOf("#")!==0)){return true}}return false}function dt(e,t){return ie(e).boosted&&e instanceof HTMLAnchorElement&&t.type==="click"&&(t.ctrlKey||t.metaKey)}function gt(e,t,n){const r=e.eventFilter;if(r){try{return r.call(t,n)!==true}catch(e){const o=r.source;fe(ne().body,"htmx:eventFilter:error",{error:e,source:o});return true}}return false}function pt(l,c,e,u,a){const f=ie(l);let t;if(u.from){t=p(l,u.from)}else{t=[l]}if(u.changed){if(!("lastValue"in f)){f.lastValue=new WeakMap}t.forEach(function(e){if(!f.lastValue.has(u)){f.lastValue.set(u,new WeakMap)}f.lastValue.get(u).set(e,e.value)})}se(t,function(i){const s=function(e){if(!le(l)){i.removeEventListener(u.trigger,s);return}if(dt(l,e)){return}if(a||ht(e,l)){e.preventDefault()}if(gt(u,l,e)){return}const t=ie(e);t.triggerSpec=u;if(t.handledFor==null){t.handledFor=[]}if(t.handledFor.indexOf(l)<0){t.handledFor.push(l);if(u.consume){e.stopPropagation()}if(u.target&&e.target){if(!h(ue(e.target),u.target)){return}}if(u.once){if(f.triggeredOnce){return}else{f.triggeredOnce=true}}if(u.changed){const n=event.target;const r=n.value;const o=f.lastValue.get(u);if(o.has(n)&&o.get(n)===r){return}o.set(n,r)}if(f.delayed){clearTimeout(f.delayed)}if(f.throttle){return}if(u.throttle>0){if(!f.throttle){he(l,"htmx:trigger");c(l,e);f.throttle=E().setTimeout(function(){f.throttle=null},u.throttle)}}else if(u.delay>0){f.delayed=E().setTimeout(function(){he(l,"htmx:trigger");c(l,e)},u.delay)}else{he(l,"htmx:trigger");c(l,e)}}};if(e.listenerInfos==null){e.listenerInfos=[]}e.listenerInfos.push({trigger:u.trigger,listener:s,on:i});i.addEventListener(u.trigger,s)})}let mt=false;let xt=null;function yt(){if(!xt){xt=function(){mt=true};window.addEventListener("scroll",xt);window.addEventListener("resize",xt);setInterval(function(){if(mt){mt=false;se(ne().querySelectorAll("[hx-trigger*='revealed'],[data-hx-trigger*='revealed']"),function(e){bt(e)})}},200)}}function bt(e){if(!s(e,"data-hx-revealed")&&X(e)){e.setAttribute("data-hx-revealed","true");const t=ie(e);if(t.initHash){he(e,"revealed")}else{e.addEventListener("htmx:afterProcessNode",function(){he(e,"revealed")},{once:true})}}}function vt(e,t,n,r){const o=function(){if(!n.loaded){n.loaded=true;he(e,"htmx:trigger");t(e)}};if(r>0){E().setTimeout(o,r)}else{o()}}function wt(t,n,e){let i=false;se(r,function(r){if(s(t,"hx-"+r)){const o=te(t,"hx-"+r);i=true;n.path=o;n.verb=r;e.forEach(function(e){St(t,e,n,function(e,t){const n=ue(e);if(g(n,Q.config.disableSelector)){b(n);return}de(r,o,n,t)})})}});return i}function St(r,e,t,n){if(e.trigger==="revealed"){yt();pt(r,n,t,e);bt(ue(r))}else if(e.trigger==="intersect"){const o={};if(e.root){o.root=ae(r,e.root)}if(e.threshold){o.threshold=parseFloat(e.threshold)}const i=new IntersectionObserver(function(t){for(let e=0;e<t.length;e++){const n=t[e];if(n.isIntersecting){he(r,"intersect");break}}},o);i.observe(ue(r));pt(ue(r),n,t,e)}else if(!t.firstInitCompleted&&e.trigger==="load"){if(!gt(e,r,Mt("load",{elt:r}))){vt(ue(r),n,t,e.delay)}}else if(e.pollInterval>0){t.polling=true;ct(ue(r),n,e)}else{pt(r,n,t,e)}}function Et(e){const t=ue(e);if(!t){return false}const n=t.attributes;for(let e=0;e<n.length;e++){const r=n[e].name;if(l(r,"hx-on:")||l(r,"data-hx-on:")||l(r,"hx-on-")||l(r,"data-hx-on-")){return true}}return false}const Ct=(new XPathEvaluator).createExpression('.//*[@*[ starts-with(name(), "hx-on:") or starts-with(name(), "data-hx-on:") or'+' starts-with(name(), "hx-on-") or starts-with(name(), "data-hx-on-") ]]');function Ot(e,t){if(Et(e)){t.push(ue(e))}const n=Ct.evaluate(e);let r=null;while(r=n.iterateNext())t.push(ue(r))}function Rt(e){const t=[];if(e instanceof DocumentFragment){for(const n of e.childNodes){Ot(n,t)}}else{Ot(e,t)}return t}function Ht(e){if(e.querySelectorAll){const n=", [hx-boost] a, [data-hx-boost] a, a[hx-boost], a[data-hx-boost]";const r=[];for(const i in Mn){const s=Mn[i];if(s.getSelectors){var t=s.getSelectors();if(t){r.push(t)}}}const o=e.querySelectorAll(H+n+", form, [type='submit'],"+" [hx-ext], [data-hx-ext], [hx-trigger], [data-hx-trigger]"+r.flat().map(e=>", "+e).join(""));return o}else{return[]}}function Tt(e){const t=g(ue(e.target),"button, input[type='submit']");const n=Lt(e);if(n){n.lastButtonClicked=t}}function qt(e){const t=Lt(e);if(t){t.lastButtonClicked=null}}function Lt(e){const t=g(ue(e.target),"button, input[type='submit']");if(!t){return}const n=y("#"+ee(t,"form"),t.getRootNode())||g(t,"form");if(!n){return}return ie(n)}function At(e){e.addEventListener("click",Tt);e.addEventListener("focusin",Tt);e.addEventListener("focusout",qt)}function Nt(t,e,n){const r=ie(t);if(!Array.isArray(r.onHandlers)){r.onHandlers=[]}let o;const i=function(e){vn(t,function(){if(at(t)){return}if(!o){o=new Function("event",n)}o.call(t,e)})};t.addEventListener(e,i);r.onHandlers.push({event:e,listener:i})}function It(t){ke(t);for(let e=0;e<t.attributes.length;e++){const n=t.attributes[e].name;const r=t.attributes[e].value;if(l(n,"hx-on")||l(n,"data-hx-on")){const o=n.indexOf("-on")+3;const i=n.slice(o,o+1);if(i==="-"||i===":"){let e=n.slice(o+1);if(l(e,":")){e="htmx"+e}else if(l(e,"-")){e="htmx:"+e.slice(1)}else if(l(e,"htmx-")){e="htmx:"+e.slice(5)}Nt(t,e,r)}}}}function Pt(t){if(g(t,Q.config.disableSelector)){b(t);return}const n=ie(t);const e=Pe(t);if(n.initHash!==e){De(t);n.initHash=e;he(t,"htmx:beforeProcessNode");const r=st(t);const o=wt(t,n,r);if(!o){if(re(t,"hx-boost")==="true"){ft(t,n,r)}else if(s(t,"hx-trigger")){r.forEach(function(e){St(t,e,n,function(){})})}}if(t.tagName==="FORM"||ee(t,"type")==="submit"&&s(t,"form")){At(t)}n.firstInitCompleted=true;he(t,"htmx:afterProcessNode")}}function kt(e){e=y(e);if(g(e,Q.config.disableSelector)){b(e);return}Pt(e);se(Ht(e),function(e){Pt(e)});se(Rt(e),It)}function Dt(e){return e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase()}function Mt(e,t){let n;if(window.CustomEvent&&typeof window.CustomEvent==="function"){n=new CustomEvent(e,{bubbles:true,cancelable:true,composed:true,detail:t})}else{n=ne().createEvent("CustomEvent");n.initCustomEvent(e,true,true,t)}return n}function fe(e,t,n){he(e,t,ce({error:t},n))}function Xt(e){return e==="htmx:afterProcessNode"}function Ft(e,t){se(Un(e),function(e){try{t(e)}catch(e){O(e)}})}function O(e){if(console.error){console.error(e)}else if(console.log){console.log("ERROR: ",e)}}function he(e,t,n){e=y(e);if(n==null){n={}}n.elt=e;const r=Mt(t,n);if(Q.logger&&!Xt(t)){Q.logger(e,t,n)}if(n.error){O(n.error);he(e,"htmx:error",{errorInfo:n})}let o=e.dispatchEvent(r);const i=Dt(t);if(o&&i!==t){const s=Mt(i,r.detail);o=o&&e.dispatchEvent(s)}Ft(ue(e),function(e){o=o&&(e.onEvent(t,r)!==false&&!r.defaultPrevented)});return o}let Bt=location.pathname+location.search;function Ut(){const e=ne().querySelector("[hx-history-elt],[data-hx-history-elt]");return e||ne().body}function jt(t,e){if(!B()){return}const n=_t(e);const r=ne().title;const o=window.scrollY;if(Q.config.historyCacheSize<=0){localStorage.removeItem("htmx-history-cache");return}t=U(t);const i=S(localStorage.getItem("htmx-history-cache"))||[];for(let e=0;e<i.length;e++){if(i[e].url===t){i.splice(e,1);break}}const s={url:t,content:n,title:r,scroll:o};he(ne().body,"htmx:historyItemCreated",{item:s,cache:i});i.push(s);while(i.length>Q.config.historyCacheSize){i.shift()}while(i.length>0){try{localStorage.setItem("htmx-history-cache",JSON.stringify(i));break}catch(e){fe(ne().body,"htmx:historyCacheError",{cause:e,cache:i});i.shift()}}}function Vt(t){if(!B()){return null}t=U(t);const n=S(localStorage.getItem("htmx-history-cache"))||[];for(let e=0;e<n.length;e++){if(n[e].url===t){return n[e]}}return null}function _t(e){const t=Q.config.requestClass;const n=e.cloneNode(true);se(x(n,"."+t),function(e){G(e,t)});se(x(n,"[data-disabled-by-htmx]"),function(e){e.removeAttribute("disabled")});return n.innerHTML}function zt(){const e=Ut();const t=Bt||location.pathname+location.search;let n;try{n=ne().querySelector('[hx-history="false" i],[data-hx-history="false" i]')}catch(e){n=ne().querySelector('[hx-history="false"],[data-hx-history="false"]')}if(!n){he(ne().body,"htmx:beforeHistorySave",{path:t,historyElt:e});jt(t,e)}if(Q.config.historyEnabled)history.replaceState({htmx:true},ne().title,window.location.href)}function $t(e){if(Q.config.getCacheBusterParam){e=e.replace(/org\.htmx\.cache-buster=[^&]*&?/,"");if(Y(e,"&")||Y(e,"?")){e=e.slice(0,-1)}}if(Q.config.historyEnabled){history.pushState({htmx:true},"",e)}Bt=e}function Jt(e){if(Q.config.historyEnabled)history.replaceState({htmx:true},"",e);Bt=e}function Kt(e){se(e,function(e){e.call(undefined)})}function Gt(o){const e=new XMLHttpRequest;const i={path:o,xhr:e};he(ne().body,"htmx:historyCacheMiss",i);e.open("GET",o,true);e.setRequestHeader("HX-Request","true");e.setRequestHeader("HX-History-Restore-Request","true");e.setRequestHeader("HX-Current-URL",ne().location.href);e.onload=function(){if(this.status>=200&&this.status<400){he(ne().body,"htmx:historyCacheMissLoad",i);const e=P(this.response);const t=e.querySelector("[hx-history-elt],[data-hx-history-elt]")||e;const n=Ut();const r=xn(n);kn(e.title);qe(e);Ve(n,t,r);Te();Kt(r.tasks);Bt=o;he(ne().body,"htmx:historyRestore",{path:o,cacheMiss:true,serverResponse:this.response})}else{fe(ne().body,"htmx:historyCacheMissLoadError",i)}};e.send()}function Wt(e){zt();e=e||location.pathname+location.search;const t=Vt(e);if(t){const n=P(t.content);const r=Ut();const o=xn(r);kn(t.title);qe(n);Ve(r,n,o);Te();Kt(o.tasks);E().setTimeout(function(){window.scrollTo(0,t.scroll)},0);Bt=e;he(ne().body,"htmx:historyRestore",{path:e,item:t})}else{if(Q.config.refreshOnHistoryMiss){window.location.reload(true)}else{Gt(e)}}}function Zt(e){let t=we(e,"hx-indicator");if(t==null){t=[e]}se(t,function(e){const t=ie(e);t.requestCount=(t.requestCount||0)+1;e.classList.add.call(e.classList,Q.config.requestClass)});return t}function Yt(e){let t=we(e,"hx-disabled-elt");if(t==null){t=[]}se(t,function(e){const t=ie(e);t.requestCount=(t.requestCount||0)+1;e.setAttribute("disabled","");e.setAttribute("data-disabled-by-htmx","")});return t}function Qt(e,t){se(e.concat(t),function(e){const t=ie(e);t.requestCount=(t.requestCount||1)-1});se(e,function(e){const t=ie(e);if(t.requestCount===0){e.classList.remove.call(e.classList,Q.config.requestClass)}});se(t,function(e){const t=ie(e);if(t.requestCount===0){e.removeAttribute("disabled");e.removeAttribute("data-disabled-by-htmx")}})}function en(t,n){for(let e=0;e<t.length;e++){const r=t[e];if(r.isSameNode(n)){return true}}return false}function tn(e){const t=e;if(t.name===""||t.name==null||t.disabled||g(t,"fieldset[disabled]")){return false}if(t.type==="button"||t.type==="submit"||t.tagName==="image"||t.tagName==="reset"||t.tagName==="file"){return false}if(t.type==="checkbox"||t.type==="radio"){return t.checked}return true}function nn(t,e,n){if(t!=null&&e!=null){if(Array.isArray(e)){e.forEach(function(e){n.append(t,e)})}else{n.append(t,e)}}}function rn(t,n,r){if(t!=null&&n!=null){let e=r.getAll(t);if(Array.isArray(n)){e=e.filter(e=>n.indexOf(e)<0)}else{e=e.filter(e=>e!==n)}r.delete(t);se(e,e=>r.append(t,e))}}function on(t,n,r,o,i){if(o==null||en(t,o)){return}else{t.push(o)}if(tn(o)){const s=ee(o,"name");let e=o.value;if(o instanceof HTMLSelectElement&&o.multiple){e=M(o.querySelectorAll("option:checked")).map(function(e){return e.value})}if(o instanceof HTMLInputElement&&o.files){e=M(o.files)}nn(s,e,n);if(i){sn(o,r)}}if(o instanceof HTMLFormElement){se(o.elements,function(e){if(t.indexOf(e)>=0){rn(e.name,e.value,n)}else{t.push(e)}if(i){sn(e,r)}});new FormData(o).forEach(function(e,t){if(e instanceof File&&e.name===""){return}nn(t,e,n)})}}function sn(e,t){const n=e;if(n.willValidate){he(n,"htmx:validation:validate");if(!n.checkValidity()){t.push({elt:n,message:n.validationMessage,validity:n.validity});he(n,"htmx:validation:failed",{message:n.validationMessage,validity:n.validity})}}}function ln(n,e){for(const t of e.keys()){n.delete(t)}e.forEach(function(e,t){n.append(t,e)});return n}function cn(e,t){const n=[];const r=new FormData;const o=new FormData;const i=[];const s=ie(e);if(s.lastButtonClicked&&!le(s.lastButtonClicked)){s.lastButtonClicked=null}let l=e instanceof HTMLFormElement&&e.noValidate!==true||te(e,"hx-validate")==="true";if(s.lastButtonClicked){l=l&&s.lastButtonClicked.formNoValidate!==true}if(t!=="get"){on(n,o,i,g(e,"form"),l)}on(n,r,i,e,l);if(s.lastButtonClicked||e.tagName==="BUTTON"||e.tagName==="INPUT"&&ee(e,"type")==="submit"){const u=s.lastButtonClicked||e;const a=ee(u,"name");nn(a,u.value,o)}const c=we(e,"hx-include");se(c,function(e){on(n,r,i,ue(e),l);if(!h(e,"form")){se(f(e).querySelectorAll(ot),function(e){on(n,r,i,e,l)})}});ln(r,o);return{errors:i,formData:r,values:An(r)}}function un(e,t,n){if(e!==""){e+="&"}if(String(n)==="[object Object]"){n=JSON.stringify(n)}const r=encodeURIComponent(n);e+=encodeURIComponent(t)+"="+r;return e}function an(e){e=qn(e);let n="";e.forEach(function(e,t){n=un(n,t,e)});return n}function fn(e,t,n){const r={"HX-Request":"true","HX-Trigger":ee(e,"id"),"HX-Trigger-Name":ee(e,"name"),"HX-Target":te(t,"id"),"HX-Current-URL":ne().location.href};bn(e,"hx-headers",false,r);if(n!==undefined){r["HX-Prompt"]=n}if(ie(e).boosted){r["HX-Boosted"]="true"}return r}function hn(n,e){const t=re(e,"hx-params");if(t){if(t==="none"){return new FormData}else if(t==="*"){return n}else if(t.indexOf("not ")===0){se(t.slice(4).split(","),function(e){e=e.trim();n.delete(e)});return n}else{const r=new FormData;se(t.split(","),function(t){t=t.trim();if(n.has(t)){n.getAll(t).forEach(function(e){r.append(t,e)})}});return r}}else{return n}}function dn(e){return!!ee(e,"href")&&ee(e,"href").indexOf("#")>=0}function gn(e,t){const n=t||re(e,"hx-swap");const r={swapStyle:ie(e).boosted?"innerHTML":Q.config.defaultSwapStyle,swapDelay:Q.config.defaultSwapDelay,settleDelay:Q.config.defaultSettleDelay};if(Q.config.scrollIntoViewOnBoost&&ie(e).boosted&&!dn(e)){r.show="top"}if(n){const s=F(n);if(s.length>0){for(let e=0;e<s.length;e++){const l=s[e];if(l.indexOf("swap:")===0){r.swapDelay=d(l.slice(5))}else if(l.indexOf("settle:")===0){r.settleDelay=d(l.slice(7))}else if(l.indexOf("transition:")===0){r.transition=l.slice(11)==="true"}else if(l.indexOf("ignoreTitle:")===0){r.ignoreTitle=l.slice(12)==="true"}else if(l.indexOf("scroll:")===0){const c=l.slice(7);var o=c.split(":");const u=o.pop();var i=o.length>0?o.join(":"):null;r.scroll=u;r.scrollTarget=i}else if(l.indexOf("show:")===0){const a=l.slice(5);var o=a.split(":");const f=o.pop();var i=o.length>0?o.join(":"):null;r.show=f;r.showTarget=i}else if(l.indexOf("focus-scroll:")===0){const h=l.slice("focus-scroll:".length);r.focusScroll=h=="true"}else if(e==0){r.swapStyle=l}else{O("Unknown modifier in hx-swap: "+l)}}}}return r}function pn(e){return re(e,"hx-encoding")==="multipart/form-data"||h(e,"form")&&ee(e,"enctype")==="multipart/form-data"}function mn(t,n,r){let o=null;Ft(n,function(e){if(o==null){o=e.encodeParameters(t,r,n)}});if(o!=null){return o}else{if(pn(n)){return ln(new FormData,qn(r))}else{return an(r)}}}function xn(e){return{tasks:[],elts:[e]}}function yn(e,t){const n=e[0];const r=e[e.length-1];if(t.scroll){var o=null;if(t.scrollTarget){o=ue(ae(n,t.scrollTarget))}if(t.scroll==="top"&&(n||o)){o=o||n;o.scrollTop=0}if(t.scroll==="bottom"&&(r||o)){o=o||r;o.scrollTop=o.scrollHeight}}if(t.show){var o=null;if(t.showTarget){let e=t.showTarget;if(t.showTarget==="window"){e="body"}o=ue(ae(n,e))}if(t.show==="top"&&(n||o)){o=o||n;o.scrollIntoView({block:"start",behavior:Q.config.scrollBehavior})}if(t.show==="bottom"&&(r||o)){o=o||r;o.scrollIntoView({block:"end",behavior:Q.config.scrollBehavior})}}}function bn(r,e,o,i){if(i==null){i={}}if(r==null){return i}const s=te(r,e);if(s){let e=s.trim();let t=o;if(e==="unset"){return null}if(e.indexOf("javascript:")===0){e=e.slice(11);t=true}else if(e.indexOf("js:")===0){e=e.slice(3);t=true}if(e.indexOf("{")!==0){e="{"+e+"}"}let n;if(t){n=vn(r,function(){return Function("return ("+e+")")()},{})}else{n=S(e)}for(const l in n){if(n.hasOwnProperty(l)){if(i[l]==null){i[l]=n[l]}}}}return bn(ue(c(r)),e,o,i)}function vn(e,t,n){if(Q.config.allowEval){return t()}else{fe(e,"htmx:evalDisallowedError");return n}}function wn(e,t){return bn(e,"hx-vars",true,t)}function Sn(e,t){return bn(e,"hx-vals",false,t)}function En(e){return ce(wn(e),Sn(e))}function Cn(t,n,r){if(r!==null){try{t.setRequestHeader(n,r)}catch(e){t.setRequestHeader(n,encodeURIComponent(r));t.setRequestHeader(n+"-URI-AutoEncoded","true")}}}function On(t){if(t.responseURL&&typeof URL!=="undefined"){try{const e=new URL(t.responseURL);return e.pathname+e.search}catch(e){fe(ne().body,"htmx:badResponseUrl",{url:t.responseURL})}}}function R(e,t){return t.test(e.getAllResponseHeaders())}function Rn(t,n,r){t=t.toLowerCase();if(r){if(r instanceof Element||typeof r==="string"){return de(t,n,null,null,{targetOverride:y(r)||ve,returnPromise:true})}else{let e=y(r.target);if(r.target&&!e||r.source&&!e&&!y(r.source)){e=ve}return de(t,n,y(r.source),r.event,{handler:r.handler,headers:r.headers,values:r.values,targetOverride:e,swapOverride:r.swap,select:r.select,returnPromise:true})}}else{return de(t,n,null,null,{returnPromise:true})}}function Hn(e){const t=[];while(e){t.push(e);e=e.parentElement}return t}function Tn(e,t,n){let r;let o;if(typeof URL==="function"){o=new URL(t,document.location.href);const i=document.location.origin;r=i===o.origin}else{o=t;r=l(t,document.location.origin)}if(Q.config.selfRequestsOnly){if(!r){return false}}return he(e,"htmx:validateUrl",ce({url:o,sameHost:r},n))}function qn(e){if(e instanceof FormData)return e;const t=new FormData;for(const n in e){if(e.hasOwnProperty(n)){if(e[n]&&typeof e[n].forEach==="function"){e[n].forEach(function(e){t.append(n,e)})}else if(typeof e[n]==="object"&&!(e[n]instanceof Blob)){t.append(n,JSON.stringify(e[n]))}else{t.append(n,e[n])}}}return t}function Ln(r,o,e){return new Proxy(e,{get:function(t,e){if(typeof e==="number")return t[e];if(e==="length")return t.length;if(e==="push"){return function(e){t.push(e);r.append(o,e)}}if(typeof t[e]==="function"){return function(){t[e].apply(t,arguments);r.delete(o);t.forEach(function(e){r.append(o,e)})}}if(t[e]&&t[e].length===1){return t[e][0]}else{return t[e]}},set:function(e,t,n){e[t]=n;r.delete(o);e.forEach(function(e){r.append(o,e)});return true}})}function An(o){return new Proxy(o,{get:function(e,t){if(typeof t==="symbol"){const r=Reflect.get(e,t);if(typeof r==="function"){return function(){return r.apply(o,arguments)}}else{return r}}if(t==="toJSON"){return()=>Object.fromEntries(o)}if(t in e){if(typeof e[t]==="function"){return function(){return o[t].apply(o,arguments)}}else{return e[t]}}const n=o.getAll(t);if(n.length===0){return undefined}else if(n.length===1){return n[0]}else{return Ln(e,t,n)}},set:function(t,n,e){if(typeof n!=="string"){return false}t.delete(n);if(e&&typeof e.forEach==="function"){e.forEach(function(e){t.append(n,e)})}else if(typeof e==="object"&&!(e instanceof Blob)){t.append(n,JSON.stringify(e))}else{t.append(n,e)}return true},deleteProperty:function(e,t){if(typeof t==="string"){e.delete(t)}return true},ownKeys:function(e){return Reflect.ownKeys(Object.fromEntries(e))},getOwnPropertyDescriptor:function(e,t){return Reflect.getOwnPropertyDescriptor(Object.fromEntries(e),t)}})}function de(t,n,r,o,i,D){let s=null;let l=null;i=i!=null?i:{};if(i.returnPromise&&typeof Promise!=="undefined"){var e=new Promise(function(e,t){s=e;l=t})}if(r==null){r=ne().body}const M=i.handler||Dn;const X=i.select||null;if(!le(r)){oe(s);return e}const c=i.targetOverride||ue(Ee(r));if(c==null||c==ve){fe(r,"htmx:targetError",{target:te(r,"hx-target")});oe(l);return e}let u=ie(r);const a=u.lastButtonClicked;if(a){const L=ee(a,"formaction");if(L!=null){n=L}const A=ee(a,"formmethod");if(A!=null){if(A.toLowerCase()!=="dialog"){t=A}}}const f=re(r,"hx-confirm");if(D===undefined){const K=function(e){return de(t,n,r,o,i,!!e)};const G={target:c,elt:r,path:n,verb:t,triggeringEvent:o,etc:i,issueRequest:K,question:f};if(he(r,"htmx:confirm",G)===false){oe(s);return e}}let h=r;let d=re(r,"hx-sync");let g=null;let F=false;if(d){const N=d.split(":");const I=N[0].trim();if(I==="this"){h=Se(r,"hx-sync")}else{h=ue(ae(r,I))}d=(N[1]||"drop").trim();u=ie(h);if(d==="drop"&&u.xhr&&u.abortable!==true){oe(s);return e}else if(d==="abort"){if(u.xhr){oe(s);return e}else{F=true}}else if(d==="replace"){he(h,"htmx:abort")}else if(d.indexOf("queue")===0){const W=d.split(" ");g=(W[1]||"last").trim()}}if(u.xhr){if(u.abortable){he(h,"htmx:abort")}else{if(g==null){if(o){const P=ie(o);if(P&&P.triggerSpec&&P.triggerSpec.queue){g=P.triggerSpec.queue}}if(g==null){g="last"}}if(u.queuedRequests==null){u.queuedRequests=[]}if(g==="first"&&u.queuedRequests.length===0){u.queuedRequests.push(function(){de(t,n,r,o,i)})}else if(g==="all"){u.queuedRequests.push(function(){de(t,n,r,o,i)})}else if(g==="last"){u.queuedRequests=[];u.queuedRequests.push(function(){de(t,n,r,o,i)})}oe(s);return e}}const p=new XMLHttpRequest;u.xhr=p;u.abortable=F;const m=function(){u.xhr=null;u.abortable=false;if(u.queuedRequests!=null&&u.queuedRequests.length>0){const e=u.queuedRequests.shift();e()}};const B=re(r,"hx-prompt");if(B){var x=prompt(B);if(x===null||!he(r,"htmx:prompt",{prompt:x,target:c})){oe(s);m();return e}}if(f&&!D){if(!confirm(f)){oe(s);m();return e}}let y=fn(r,c,x);if(t!=="get"&&!pn(r)){y["Content-Type"]="application/x-www-form-urlencoded"}if(i.headers){y=ce(y,i.headers)}const U=cn(r,t);let b=U.errors;const j=U.formData;if(i.values){ln(j,qn(i.values))}const V=qn(En(r));const v=ln(j,V);let w=hn(v,r);if(Q.config.getCacheBusterParam&&t==="get"){w.set("org.htmx.cache-buster",ee(c,"id")||"true")}if(n==null||n===""){n=ne().location.href}const S=bn(r,"hx-request");const _=ie(r).boosted;let E=Q.config.methodsThatUseUrlParams.indexOf(t)>=0;const C={boosted:_,useUrlParams:E,formData:w,parameters:An(w),unfilteredFormData:v,unfilteredParameters:An(v),headers:y,target:c,verb:t,errors:b,withCredentials:i.credentials||S.credentials||Q.config.withCredentials,timeout:i.timeout||S.timeout||Q.config.timeout,path:n,triggeringEvent:o};if(!he(r,"htmx:configRequest",C)){oe(s);m();return e}n=C.path;t=C.verb;y=C.headers;w=qn(C.parameters);b=C.errors;E=C.useUrlParams;if(b&&b.length>0){he(r,"htmx:validation:halted",C);oe(s);m();return e}const z=n.split("#");const $=z[0];const O=z[1];let R=n;if(E){R=$;const Z=!w.keys().next().done;if(Z){if(R.indexOf("?")<0){R+="?"}else{R+="&"}R+=an(w);if(O){R+="#"+O}}}if(!Tn(r,R,C)){fe(r,"htmx:invalidPath",C);oe(l);return e}p.open(t.toUpperCase(),R,true);p.overrideMimeType("text/html");p.withCredentials=C.withCredentials;p.timeout=C.timeout;if(S.noHeaders){}else{for(const k in y){if(y.hasOwnProperty(k)){const Y=y[k];Cn(p,k,Y)}}}const H={xhr:p,target:c,requestConfig:C,etc:i,boosted:_,select:X,pathInfo:{requestPath:n,finalRequestPath:R,responsePath:null,anchor:O}};p.onload=function(){try{const t=Hn(r);H.pathInfo.responsePath=On(p);M(r,H);if(H.keepIndicators!==true){Qt(T,q)}he(r,"htmx:afterRequest",H);he(r,"htmx:afterOnLoad",H);if(!le(r)){let e=null;while(t.length>0&&e==null){const n=t.shift();if(le(n)){e=n}}if(e){he(e,"htmx:afterRequest",H);he(e,"htmx:afterOnLoad",H)}}oe(s);m()}catch(e){fe(r,"htmx:onLoadError",ce({error:e},H));throw e}};p.onerror=function(){Qt(T,q);fe(r,"htmx:afterRequest",H);fe(r,"htmx:sendError",H);oe(l);m()};p.onabort=function(){Qt(T,q);fe(r,"htmx:afterRequest",H);fe(r,"htmx:sendAbort",H);oe(l);m()};p.ontimeout=function(){Qt(T,q);fe(r,"htmx:afterRequest",H);fe(r,"htmx:timeout",H);oe(l);m()};if(!he(r,"htmx:beforeRequest",H)){oe(s);m();return e}var T=Zt(r);var q=Yt(r);se(["loadstart","loadend","progress","abort"],function(t){se([p,p.upload],function(e){e.addEventListener(t,function(e){he(r,"htmx:xhr:"+t,{lengthComputable:e.lengthComputable,loaded:e.loaded,total:e.total})})})});he(r,"htmx:beforeSend",H);const J=E?null:mn(p,r,w);p.send(J);return e}function Nn(e,t){const n=t.xhr;let r=null;let o=null;if(R(n,/HX-Push:/i)){r=n.getResponseHeader("HX-Push");o="push"}else if(R(n,/HX-Push-Url:/i)){r=n.getResponseHeader("HX-Push-Url");o="push"}else if(R(n,/HX-Replace-Url:/i)){r=n.getResponseHeader("HX-Replace-Url");o="replace"}if(r){if(r==="false"){return{}}else{return{type:o,path:r}}}const i=t.pathInfo.finalRequestPath;const s=t.pathInfo.responsePath;const l=re(e,"hx-push-url");const c=re(e,"hx-replace-url");const u=ie(e).boosted;let a=null;let f=null;if(l){a="push";f=l}else if(c){a="replace";f=c}else if(u){a="push";f=s||i}if(f){if(f==="false"){return{}}if(f==="true"){f=s||i}if(t.pathInfo.anchor&&f.indexOf("#")===-1){f=f+"#"+t.pathInfo.anchor}return{type:a,path:f}}else{return{}}}function In(e,t){var n=new RegExp(e.code);return n.test(t.toString(10))}function Pn(e){for(var t=0;t<Q.config.responseHandling.length;t++){var n=Q.config.responseHandling[t];if(In(n,e.status)){return n}}return{swap:false}}function kn(e){if(e){const t=u("title");if(t){t.innerHTML=e}else{window.document.title=e}}}function Dn(o,i){const s=i.xhr;let l=i.target;const e=i.etc;const c=i.select;if(!he(o,"htmx:beforeOnLoad",i))return;if(R(s,/HX-Trigger:/i)){Je(s,"HX-Trigger",o)}if(R(s,/HX-Location:/i)){zt();let e=s.getResponseHeader("HX-Location");var t;if(e.indexOf("{")===0){t=S(e);e=t.path;delete t.path}Rn("get",e,t).then(function(){$t(e)});return}const n=R(s,/HX-Refresh:/i)&&s.getResponseHeader("HX-Refresh")==="true";if(R(s,/HX-Redirect:/i)){i.keepIndicators=true;location.href=s.getResponseHeader("HX-Redirect");n&&location.reload();return}if(n){i.keepIndicators=true;location.reload();return}if(R(s,/HX-Retarget:/i)){if(s.getResponseHeader("HX-Retarget")==="this"){i.target=o}else{i.target=ue(ae(o,s.getResponseHeader("HX-Retarget")))}}const u=Nn(o,i);const r=Pn(s);const a=r.swap;let f=!!r.error;let h=Q.config.ignoreTitle||r.ignoreTitle;let d=r.select;if(r.target){i.target=ue(ae(o,r.target))}var g=e.swapOverride;if(g==null&&r.swapOverride){g=r.swapOverride}if(R(s,/HX-Retarget:/i)){if(s.getResponseHeader("HX-Retarget")==="this"){i.target=o}else{i.target=ue(ae(o,s.getResponseHeader("HX-Retarget")))}}if(R(s,/HX-Reswap:/i)){g=s.getResponseHeader("HX-Reswap")}var p=s.response;var m=ce({shouldSwap:a,serverResponse:p,isError:f,ignoreTitle:h,selectOverride:d,swapOverride:g},i);if(r.event&&!he(l,r.event,m))return;if(!he(l,"htmx:beforeSwap",m))return;l=m.target;p=m.serverResponse;f=m.isError;h=m.ignoreTitle;d=m.selectOverride;g=m.swapOverride;i.target=l;i.failed=f;i.successful=!f;if(m.shouldSwap){if(s.status===286){lt(o)}Ft(o,function(e){p=e.transformResponse(p,s,o)});if(u.type){zt()}var x=gn(o,g);if(!x.hasOwnProperty("ignoreTitle")){x.ignoreTitle=h}l.classList.add(Q.config.swappingClass);let n=null;let r=null;if(c){d=c}if(R(s,/HX-Reselect:/i)){d=s.getResponseHeader("HX-Reselect")}const y=re(o,"hx-select-oob");const b=re(o,"hx-select");let e=function(){try{if(u.type){he(ne().body,"htmx:beforeHistoryUpdate",ce({history:u},i));if(u.type==="push"){$t(u.path);he(ne().body,"htmx:pushedIntoHistory",{path:u.path})}else{Jt(u.path);he(ne().body,"htmx:replacedInHistory",{path:u.path})}}$e(l,p,x,{select:d||b,selectOOB:y,eventInfo:i,anchor:i.pathInfo.anchor,contextElement:o,afterSwapCallback:function(){if(R(s,/HX-Trigger-After-Swap:/i)){let e=o;if(!le(o)){e=ne().body}Je(s,"HX-Trigger-After-Swap",e)}},afterSettleCallback:function(){if(R(s,/HX-Trigger-After-Settle:/i)){let e=o;if(!le(o)){e=ne().body}Je(s,"HX-Trigger-After-Settle",e)}oe(n)}})}catch(e){fe(o,"htmx:swapError",i);oe(r);throw e}};let t=Q.config.globalViewTransitions;if(x.hasOwnProperty("transition")){t=x.transition}if(t&&he(o,"htmx:beforeTransition",i)&&typeof Promise!=="undefined"&&document.startViewTransition){const v=new Promise(function(e,t){n=e;r=t});const w=e;e=function(){document.startViewTransition(function(){w();return v})}}if(x.swapDelay>0){E().setTimeout(e,x.swapDelay)}else{e()}}if(f){fe(o,"htmx:responseError",ce({error:"Response Status Error Code "+s.status+" from "+i.pathInfo.requestPath},i))}}const Mn={};function Xn(){return{init:function(e){return null},getSelectors:function(){return null},onEvent:function(e,t){return true},transformResponse:function(e,t,n){return e},isInlineSwap:function(e){return false},handleSwap:function(e,t,n,r){return false},encodeParameters:function(e,t,n){return null}}}function Fn(e,t){if(t.init){t.init(n)}Mn[e]=ce(Xn(),t)}function Bn(e){delete Mn[e]}function Un(e,n,r){if(n==undefined){n=[]}if(e==undefined){return n}if(r==undefined){r=[]}const t=te(e,"hx-ext");if(t){se(t.split(","),function(e){e=e.replace(/ /g,"");if(e.slice(0,7)=="ignore:"){r.push(e.slice(7));return}if(r.indexOf(e)<0){const t=Mn[e];if(t&&n.indexOf(t)<0){n.push(t)}}})}return Un(ue(c(e)),n,r)}var jn=false;ne().addEventListener("DOMContentLoaded",function(){jn=true});function Vn(e){if(jn||ne().readyState==="complete"){e()}else{ne().addEventListener("DOMContentLoaded",e)}}function _n(){if(Q.config.includeIndicatorStyles!==false){const e=Q.config.inlineStyleNonce?` nonce="${Q.config.inlineStyleNonce}"`:"";ne().head.insertAdjacentHTML("beforeend","<style"+e+"> ."+Q.config.indicatorClass+"{opacity:0} ."+Q.config.requestClass+" ."+Q.config.indicatorClass+"{opacity:1; transition: opacity 200ms ease-in;} ."+Q.config.requestClass+"."+Q.config.indicatorClass+"{opacity:1; transition: opacity 200ms ease-in;} </style>")}}function zn(){const e=ne().querySelector('meta[name="htmx-config"]');if(e){return S(e.content)}else{return null}}function $n(){const e=zn();if(e){Q.config=ce(Q.config,e)}}Vn(function(){$n();_n();let e=ne().body;kt(e);const t=ne().querySelectorAll("[hx-trigger='restored'],[data-hx-trigger='restored']");e.addEventListener("htmx:abort",function(e){const t=e.target;const n=ie(t);if(n&&n.xhr){n.xhr.abort()}});const n=window.onpopstate?window.onpopstate.bind(window):null;window.onpopstate=function(e){if(e.state&&e.state.htmx){Wt();se(t,function(e){he(e,"htmx:restored",{document:ne(),triggerEvent:he})})}else{if(n){n(e)}}};E().setTimeout(function(){he(e,"htmx:load",{});e=null},0)});return Q}(); |
addedcrates/anvil-web/src/auth.rs+156 −0
| 1 | + | //! Web authentication: cookie sessions, login/logout, the `CurrentUser` | |
| 2 | + | //! extractor, and HTTP Basic auth for git push. | |
| 3 | + | ||
| 4 | + | use std::convert::Infallible; | |
| 5 | + | ||
| 6 | + | use anvil_core::{ | |
| 7 | + | App, | |
| 8 | + | User, | |
| 9 | + | sessions, | |
| 10 | + | users, | |
| 11 | + | }; | |
| 12 | + | use axum::{ | |
| 13 | + | Form, | |
| 14 | + | extract::{ | |
| 15 | + | FromRequestParts, | |
| 16 | + | State, | |
| 17 | + | }, | |
| 18 | + | http::request::Parts, | |
| 19 | + | response::{ | |
| 20 | + | IntoResponse, | |
| 21 | + | Redirect, | |
| 22 | + | Response, | |
| 23 | + | }, | |
| 24 | + | }; | |
| 25 | + | use axum_extra::extract::cookie::{ | |
| 26 | + | Cookie, | |
| 27 | + | CookieJar, | |
| 28 | + | SameSite, | |
| 29 | + | }; | |
| 30 | + | use maud::{ | |
| 31 | + | Markup, | |
| 32 | + | html, | |
| 33 | + | }; | |
| 34 | + | ||
| 35 | + | use crate::ui::layout; | |
| 36 | + | ||
| 37 | + | const SESSION_COOKIE: &str = "anvil_session"; | |
| 38 | + | ||
| 39 | + | /// Extractor yielding the logged-in user, if any, from the session cookie. | |
| 40 | + | /// Never fails — absence of a valid session simply yields `None`. | |
| 41 | + | pub struct CurrentUser(pub Option<User>); | |
| 42 | + | ||
| 43 | + | impl FromRequestParts<App> for CurrentUser { | |
| 44 | + | type Rejection = Infallible; | |
| 45 | + | ||
| 46 | + | async fn from_request_parts(parts: &mut Parts, app: &App) -> Result<Self, Infallible> { | |
| 47 | + | let jar = CookieJar::from_headers(&parts.headers); | |
| 48 | + | let user = match jar.get(SESSION_COOKIE) { | |
| 49 | + | Some(cookie) => sessions::lookup_user(&app.db, cookie.value()) | |
| 50 | + | .await | |
| 51 | + | .ok() | |
| 52 | + | .flatten(), | |
| 53 | + | None => None, | |
| 54 | + | }; | |
| 55 | + | Ok(CurrentUser(user)) | |
| 56 | + | } | |
| 57 | + | } | |
| 58 | + | ||
| 59 | + | #[derive(serde::Deserialize)] | |
| 60 | + | pub struct LoginForm { | |
| 61 | + | username: String, | |
| 62 | + | password: String, | |
| 63 | + | } | |
| 64 | + | ||
| 65 | + | /// `GET /login` — show the login form (or bounce home if already signed in). | |
| 66 | + | pub async fn login_form(CurrentUser(user): CurrentUser) -> Response { | |
| 67 | + | if user.is_some() { | |
| 68 | + | return Redirect::to("/").into_response(); | |
| 69 | + | } | |
| 70 | + | login_page(None).into_response() | |
| 71 | + | } | |
| 72 | + | ||
| 73 | + | /// `POST /login` — verify credentials, create a session, set the cookie. | |
| 74 | + | pub async fn login_submit( | |
| 75 | + | State(app): State<App>, | |
| 76 | + | jar: CookieJar, | |
| 77 | + | Form(form): Form<LoginForm>, | |
| 78 | + | ) -> Response { | |
| 79 | + | let ok = match users::find_by_username(&app.db, &form.username).await { | |
| 80 | + | Ok(Some(user)) => users::verify_password(&user.password_hash, &form.password) | |
| 81 | + | .unwrap_or(false) | |
| 82 | + | .then_some(user), | |
| 83 | + | _ => None, | |
| 84 | + | }; | |
| 85 | + | ||
| 86 | + | let Some(user) = ok else { | |
| 87 | + | return ( | |
| 88 | + | axum::http::StatusCode::UNAUTHORIZED, | |
| 89 | + | login_page(Some("Invalid username or password.")), | |
| 90 | + | ) | |
| 91 | + | .into_response(); | |
| 92 | + | }; | |
| 93 | + | ||
| 94 | + | match sessions::create(&app.db, user.id).await { | |
| 95 | + | Ok(session) => { | |
| 96 | + | let cookie = Cookie::build((SESSION_COOKIE, session.token)) | |
| 97 | + | .path("/") | |
| 98 | + | .http_only(true) | |
| 99 | + | .same_site(SameSite::Lax) | |
| 100 | + | .build(); | |
| 101 | + | (jar.add(cookie), Redirect::to("/")).into_response() | |
| 102 | + | } | |
| 103 | + | Err(e) => { | |
| 104 | + | tracing::error!("session create failed: {e}"); | |
| 105 | + | ( | |
| 106 | + | axum::http::StatusCode::INTERNAL_SERVER_ERROR, | |
| 107 | + | login_page(Some("Could not start a session.")), | |
| 108 | + | ) | |
| 109 | + | .into_response() | |
| 110 | + | } | |
| 111 | + | } | |
| 112 | + | } | |
| 113 | + | ||
| 114 | + | /// `POST /logout` — destroy the session and clear the cookie. | |
| 115 | + | pub async fn logout(State(app): State<App>, jar: CookieJar) -> Response { | |
| 116 | + | if let Some(cookie) = jar.get(SESSION_COOKIE) { | |
| 117 | + | let _ = sessions::delete(&app.db, cookie.value()).await; | |
| 118 | + | } | |
| 119 | + | (jar.remove(Cookie::from(SESSION_COOKIE)), Redirect::to("/")).into_response() | |
| 120 | + | } | |
| 121 | + | ||
| 122 | + | fn login_page(error: Option<&str>) -> Markup { | |
| 123 | + | layout( | |
| 124 | + | "Sign in", | |
| 125 | + | None, | |
| 126 | + | html! { | |
| 127 | + | h1 { "Sign in" } | |
| 128 | + | @if let Some(error) = error { | |
| 129 | + | p style="color:#cf222e" { (error) } | |
| 130 | + | } | |
| 131 | + | form method="post" action="/-/login" style="max-width:320px" { | |
| 132 | + | p { label { "Username" br; input name="username" autofocus; } } | |
| 133 | + | p { label { "Password" br; input name="password" type="password"; } } | |
| 134 | + | button type="submit" { "Sign in" } | |
| 135 | + | } | |
| 136 | + | }, | |
| 137 | + | ) | |
| 138 | + | } | |
| 139 | + | ||
| 140 | + | /// Verify HTTP Basic credentials from the `Authorization` header against a user. | |
| 141 | + | /// Returns the authenticated user, or `None` if absent/invalid. | |
| 142 | + | pub async fn basic_auth_user(app: &App, authorization: Option<&str>) -> Option<User> { | |
| 143 | + | use base64::Engine; | |
| 144 | + | ||
| 145 | + | let encoded = authorization?.strip_prefix("Basic ")?; | |
| 146 | + | let decoded = base64::engine::general_purpose::STANDARD | |
| 147 | + | .decode(encoded.trim()) | |
| 148 | + | .ok()?; | |
| 149 | + | let creds = String::from_utf8(decoded).ok()?; | |
| 150 | + | let (username, password) = creds.split_once(':')?; | |
| 151 | + | ||
| 152 | + | let user = users::find_by_username(&app.db, username).await.ok()??; | |
| 153 | + | users::verify_password(&user.password_hash, password) | |
| 154 | + | .unwrap_or(false) | |
| 155 | + | .then_some(user) | |
| 156 | + | } |
addedcrates/anvil-web/src/git_http.rs+221 −0
| 1 | + | //! Smart-HTTP git endpoints: `info/refs`, `git-upload-pack` (clone/fetch), and | |
| 2 | + | //! `git-receive-pack` (push). These adapt the transport-agnostic protocol layer | |
| 3 | + | //! in [`anvil_git::smart_http`] to axum. | |
| 4 | + | //! | |
| 5 | + | //! Routes are mounted under `/{owner}/{repo}/…` where `{repo}` is the URL form | |
| 6 | + | //! including the `.git` suffix (e.g. `/alice/hello.git/info/refs`). | |
| 7 | + | //! | |
| 8 | + | //! Access control: public repos may be cloned anonymously; private repos and all | |
| 9 | + | //! pushes require HTTP Basic auth, enforced via [`anvil_core::access`]. | |
| 10 | + | ||
| 11 | + | use std::collections::HashMap; | |
| 12 | + | use std::path::PathBuf; | |
| 13 | + | ||
| 14 | + | use anvil_core::{ | |
| 15 | + | App, | |
| 16 | + | Repository, | |
| 17 | + | access, | |
| 18 | + | repos, | |
| 19 | + | users, | |
| 20 | + | }; | |
| 21 | + | use anvil_git::smart_http::{ | |
| 22 | + | self, | |
| 23 | + | Service, | |
| 24 | + | UploadPack, | |
| 25 | + | }; | |
| 26 | + | use axum::{ | |
| 27 | + | Router, | |
| 28 | + | body::{ | |
| 29 | + | Body, | |
| 30 | + | Bytes, | |
| 31 | + | }, | |
| 32 | + | extract::{ | |
| 33 | + | Path, | |
| 34 | + | Query, | |
| 35 | + | State, | |
| 36 | + | }, | |
| 37 | + | http::{ | |
| 38 | + | HeaderMap, | |
| 39 | + | StatusCode, | |
| 40 | + | header, | |
| 41 | + | }, | |
| 42 | + | response::{ | |
| 43 | + | IntoResponse, | |
| 44 | + | Response, | |
| 45 | + | }, | |
| 46 | + | routing::{ | |
| 47 | + | get, | |
| 48 | + | post, | |
| 49 | + | }, | |
| 50 | + | }; | |
| 51 | + | use tokio_util::io::ReaderStream; | |
| 52 | + | ||
| 53 | + | /// Mount the smart-HTTP git routes onto `router`. | |
| 54 | + | pub fn routes(router: Router<App>) -> Router<App> { | |
| 55 | + | router | |
| 56 | + | .route("/{owner}/{repo}/info/refs", get(info_refs)) | |
| 57 | + | .route("/{owner}/{repo}/git-upload-pack", post(upload_pack)) | |
| 58 | + | .route("/{owner}/{repo}/git-receive-pack", post(receive_pack)) | |
| 59 | + | } | |
| 60 | + | ||
| 61 | + | /// Resolve `<owner>/<repo>` (repo may carry a `.git` suffix) to its on-disk path | |
| 62 | + | /// and metadata row, rejecting traversal and missing repos. | |
| 63 | + | async fn load_repo(app: &App, owner: &str, repo: &str) -> Result<(PathBuf, Repository), Response> { | |
| 64 | + | let name = repo.strip_suffix(".git").unwrap_or(repo); | |
| 65 | + | let bad = |s: &str| s.is_empty() || s.contains('/') || s.contains('\\') || s.contains(".."); | |
| 66 | + | if bad(owner) || bad(name) { | |
| 67 | + | return Err((StatusCode::BAD_REQUEST, "invalid repository path").into_response()); | |
| 68 | + | } | |
| 69 | + | let not_found = || (StatusCode::NOT_FOUND, "repository not found").into_response(); | |
| 70 | + | let owner_user = users::find_by_username(&app.db, owner) | |
| 71 | + | .await | |
| 72 | + | .map_err(internal)? | |
| 73 | + | .ok_or_else(not_found)?; | |
| 74 | + | let meta = repos::find(&app.db, owner_user.id, name) | |
| 75 | + | .await | |
| 76 | + | .map_err(internal)? | |
| 77 | + | .ok_or_else(not_found)?; | |
| 78 | + | let path = anvil_core::storage::repo_path(&app.config.repositories_dir(), owner, name); | |
| 79 | + | if !path.exists() { | |
| 80 | + | return Err(not_found()); | |
| 81 | + | } | |
| 82 | + | Ok((path, meta)) | |
| 83 | + | } | |
| 84 | + | ||
| 85 | + | /// Enforce access for a git request: anonymous reads are allowed for public | |
| 86 | + | /// repos; private reads and all writes require valid Basic credentials. On | |
| 87 | + | /// failure, returns a `401` with a `WWW-Authenticate` challenge so the git | |
| 88 | + | /// client prompts for credentials. | |
| 89 | + | async fn authorize( | |
| 90 | + | app: &App, | |
| 91 | + | headers: &HeaderMap, | |
| 92 | + | repo: &Repository, | |
| 93 | + | need_write: bool, | |
| 94 | + | ) -> Result<(), Response> { | |
| 95 | + | let authorization = headers | |
| 96 | + | .get(header::AUTHORIZATION) | |
| 97 | + | .and_then(|v| v.to_str().ok()); | |
| 98 | + | let user = crate::auth::basic_auth_user(app, authorization).await; | |
| 99 | + | let allowed = if need_write { | |
| 100 | + | access::can_write(repo, user.as_ref()) | |
| 101 | + | } else { | |
| 102 | + | access::can_read(repo, user.as_ref()) | |
| 103 | + | }; | |
| 104 | + | if allowed { | |
| 105 | + | Ok(()) | |
| 106 | + | } else { | |
| 107 | + | Err(Response::builder() | |
| 108 | + | .status(StatusCode::UNAUTHORIZED) | |
| 109 | + | .header(header::WWW_AUTHENTICATE, "Basic realm=\"anvil\"") | |
| 110 | + | .body(Body::from("authentication required")) | |
| 111 | + | .unwrap()) | |
| 112 | + | } | |
| 113 | + | } | |
| 114 | + | ||
| 115 | + | /// True if the client requested git protocol v2 via the `Git-Protocol` header. | |
| 116 | + | fn wants_v2(headers: &HeaderMap) -> bool { | |
| 117 | + | headers | |
| 118 | + | .get("git-protocol") | |
| 119 | + | .and_then(|v| v.to_str().ok()) | |
| 120 | + | .map(|v| v.split(':').any(|item| item.trim() == "version=2")) | |
| 121 | + | .unwrap_or(false) | |
| 122 | + | } | |
| 123 | + | ||
| 124 | + | fn internal(err: impl std::fmt::Display) -> Response { | |
| 125 | + | tracing::error!("git smart-http error: {err}"); | |
| 126 | + | (StatusCode::INTERNAL_SERVER_ERROR, "internal server error").into_response() | |
| 127 | + | } | |
| 128 | + | ||
| 129 | + | fn rpc_response(content_type: String, body: Body) -> Response { | |
| 130 | + | Response::builder() | |
| 131 | + | .status(StatusCode::OK) | |
| 132 | + | .header(header::CONTENT_TYPE, content_type) | |
| 133 | + | .header(header::CACHE_CONTROL, "no-cache") | |
| 134 | + | .body(body) | |
| 135 | + | .unwrap() | |
| 136 | + | } | |
| 137 | + | ||
| 138 | + | /// `GET /{owner}/{repo}/info/refs?service=…` — ref advertisement. | |
| 139 | + | async fn info_refs( | |
| 140 | + | State(app): State<App>, | |
| 141 | + | Path((owner, repo)): Path<(String, String)>, | |
| 142 | + | Query(query): Query<HashMap<String, String>>, | |
| 143 | + | headers: HeaderMap, | |
| 144 | + | ) -> Response { | |
| 145 | + | let (path, meta) = match load_repo(&app, &owner, &repo).await { | |
| 146 | + | Ok(v) => v, | |
| 147 | + | Err(resp) => return resp, | |
| 148 | + | }; | |
| 149 | + | let Some(service) = query.get("service").and_then(|s| Service::from_query(s)) else { | |
| 150 | + | return (StatusCode::BAD_REQUEST, "missing or unsupported service").into_response(); | |
| 151 | + | }; | |
| 152 | + | let need_write = service == Service::ReceivePack; | |
| 153 | + | if let Err(resp) = authorize(&app, &headers, &meta, need_write).await { | |
| 154 | + | return resp; | |
| 155 | + | } | |
| 156 | + | ||
| 157 | + | let v2 = service == Service::UploadPack && wants_v2(&headers); | |
| 158 | + | match smart_http::advertise(&path, service, v2) { | |
| 159 | + | Ok(body) => Response::builder() | |
| 160 | + | .status(StatusCode::OK) | |
| 161 | + | .header(header::CONTENT_TYPE, service.advertisement_content_type()) | |
| 162 | + | .header(header::CACHE_CONTROL, "no-cache") | |
| 163 | + | .body(Body::from(body)) | |
| 164 | + | .unwrap(), | |
| 165 | + | Err(e) => internal(e), | |
| 166 | + | } | |
| 167 | + | } | |
| 168 | + | ||
| 169 | + | /// `POST /{owner}/{repo}/git-upload-pack` — clone/fetch (read access). | |
| 170 | + | async fn upload_pack( | |
| 171 | + | State(app): State<App>, | |
| 172 | + | Path((owner, repo)): Path<(String, String)>, | |
| 173 | + | headers: HeaderMap, | |
| 174 | + | body: Bytes, | |
| 175 | + | ) -> Response { | |
| 176 | + | let (path, meta) = match load_repo(&app, &owner, &repo).await { | |
| 177 | + | Ok(v) => v, | |
| 178 | + | Err(resp) => return resp, | |
| 179 | + | }; | |
| 180 | + | if let Err(resp) = authorize(&app, &headers, &meta, false).await { | |
| 181 | + | return resp; | |
| 182 | + | } | |
| 183 | + | let content_type = Service::UploadPack.result_content_type(); | |
| 184 | + | ||
| 185 | + | if wants_v2(&headers) { | |
| 186 | + | match smart_http::upload_pack_v2(&path, &body).await { | |
| 187 | + | Ok(UploadPack::Buffered(b)) => rpc_response(content_type, Body::from(b)), | |
| 188 | + | Ok(UploadPack::Pack(reader)) => { | |
| 189 | + | rpc_response(content_type, Body::from_stream(ReaderStream::new(reader))) | |
| 190 | + | } | |
| 191 | + | Err(e) => internal(e), | |
| 192 | + | } | |
| 193 | + | } else { | |
| 194 | + | match smart_http::upload_pack_v0(&path, &body).await { | |
| 195 | + | Ok(reader) => rpc_response(content_type, Body::from_stream(ReaderStream::new(reader))), | |
| 196 | + | Err(e) => internal(e), | |
| 197 | + | } | |
| 198 | + | } | |
| 199 | + | } | |
| 200 | + | ||
| 201 | + | /// `POST /{owner}/{repo}/git-receive-pack` — push (write access). | |
| 202 | + | async fn receive_pack( | |
| 203 | + | State(app): State<App>, | |
| 204 | + | Path((owner, repo)): Path<(String, String)>, | |
| 205 | + | headers: HeaderMap, | |
| 206 | + | body: Bytes, | |
| 207 | + | ) -> Response { | |
| 208 | + | let (path, meta) = match load_repo(&app, &owner, &repo).await { | |
| 209 | + | Ok(v) => v, | |
| 210 | + | Err(resp) => return resp, | |
| 211 | + | }; | |
| 212 | + | if let Err(resp) = authorize(&app, &headers, &meta, true).await { | |
| 213 | + | return resp; | |
| 214 | + | } | |
| 215 | + | ||
| 216 | + | let reader = std::io::Cursor::new(body.to_vec()); | |
| 217 | + | match smart_http::receive_pack(&path, reader).await { | |
| 218 | + | Ok(b) => rpc_response(Service::ReceivePack.result_content_type(), Body::from(b)), | |
| 219 | + | Err(e) => internal(e), | |
| 220 | + | } | |
| 221 | + | } |
addedcrates/anvil-web/src/lib.rs+51 −0
| 1 | + | //! HTTP server for anvil: the web UI and the smart-HTTP git endpoints | |
| 2 | + | //! (`/<owner>/<repo>.git/info/refs`, `/git-upload-pack`, `/git-receive-pack`), | |
| 3 | + | //! plus cookie-session auth. | |
| 4 | + | ||
| 5 | + | // Handlers return `Result<_, Response>` — the idiomatic axum pattern where the | |
| 6 | + | // error arm is a ready-made HTTP response (404/500/redirect). `Response` is | |
| 7 | + | // intrinsically large, so `result_large_err` fires across the crate; the | |
| 8 | + | // pattern is intentional and the responses are never hot-path allocated en masse. | |
| 9 | + | #![allow(clippy::result_large_err)] | |
| 10 | + | ||
| 11 | + | use anvil_core::{ | |
| 12 | + | App, | |
| 13 | + | Result, | |
| 14 | + | }; | |
| 15 | + | use axum::{ | |
| 16 | + | Router, | |
| 17 | + | routing::{ | |
| 18 | + | get, | |
| 19 | + | post, | |
| 20 | + | }, | |
| 21 | + | }; | |
| 22 | + | ||
| 23 | + | pub mod auth; | |
| 24 | + | pub mod git_http; | |
| 25 | + | pub mod ui; | |
| 26 | + | ||
| 27 | + | /// Build the application router. | |
| 28 | + | pub fn router(app: App) -> Router { | |
| 29 | + | let mut router = Router::new() | |
| 30 | + | .route("/-/healthz", get(healthz)) | |
| 31 | + | .route("/-/login", get(auth::login_form).post(auth::login_submit)) | |
| 32 | + | .route("/-/logout", post(auth::logout)); | |
| 33 | + | router = ui::routes(router); // web UI, including `/` | |
| 34 | + | router = git_http::routes(router); // smart-HTTP git endpoints | |
| 35 | + | router.with_state(app) | |
| 36 | + | } | |
| 37 | + | ||
| 38 | + | /// Bind to the configured HTTP address and serve until shutdown. | |
| 39 | + | pub async fn serve(app: App) -> Result<()> { | |
| 40 | + | let listen = app.config.http.listen.clone(); | |
| 41 | + | let router = router(app); | |
| 42 | + | ||
| 43 | + | let listener = tokio::net::TcpListener::bind(&listen).await?; | |
| 44 | + | tracing::info!("anvil web server listening on http://{listen}"); | |
| 45 | + | axum::serve(listener, router).await?; | |
| 46 | + | Ok(()) | |
| 47 | + | } | |
| 48 | + | ||
| 49 | + | async fn healthz() -> &'static str { | |
| 50 | + | "ok" | |
| 51 | + | } |
addedcrates/anvil-web/src/ui.rs+1055 −0
| 1 | + | //! Server-rendered web UI (Maud): repo list, repo overview, tree browsing, and | |
| 2 | + | //! blob viewing. Pages are plain SSR and work without JavaScript; htmx-based | |
| 3 | + | //! progressive enhancement is a follow-up. | |
| 4 | + | ||
| 5 | + | use std::path::PathBuf; | |
| 6 | + | use std::sync::OnceLock; | |
| 7 | + | ||
| 8 | + | use anvil_core::{ | |
| 9 | + | App, | |
| 10 | + | Repository, | |
| 11 | + | SshKey, | |
| 12 | + | User, | |
| 13 | + | access, | |
| 14 | + | repos, | |
| 15 | + | ssh_keys, | |
| 16 | + | users, | |
| 17 | + | }; | |
| 18 | + | use anvil_git::browse::{ | |
| 19 | + | self, | |
| 20 | + | ChangeKind, | |
| 21 | + | FileChange, | |
| 22 | + | }; | |
| 23 | + | use axum::{ | |
| 24 | + | Form, | |
| 25 | + | Router, | |
| 26 | + | extract::{ | |
| 27 | + | Path, | |
| 28 | + | State, | |
| 29 | + | }, | |
| 30 | + | http::{ | |
| 31 | + | StatusCode, | |
| 32 | + | header, | |
| 33 | + | }, | |
| 34 | + | response::{ | |
| 35 | + | IntoResponse, | |
| 36 | + | Redirect, | |
| 37 | + | Response, | |
| 38 | + | }, | |
| 39 | + | routing::{ | |
| 40 | + | get, | |
| 41 | + | post, | |
| 42 | + | }, | |
| 43 | + | }; | |
| 44 | + | use maud::{ | |
| 45 | + | DOCTYPE, | |
| 46 | + | Markup, | |
| 47 | + | PreEscaped, | |
| 48 | + | html, | |
| 49 | + | }; | |
| 50 | + | use similar::{ | |
| 51 | + | ChangeTag, | |
| 52 | + | TextDiff, | |
| 53 | + | }; | |
| 54 | + | use syntect::easy::HighlightLines; | |
| 55 | + | use syntect::highlighting::{ | |
| 56 | + | Theme, | |
| 57 | + | ThemeSet, | |
| 58 | + | }; | |
| 59 | + | use syntect::html::{ | |
| 60 | + | IncludeBackground, | |
| 61 | + | styled_line_to_highlighted_html, | |
| 62 | + | }; | |
| 63 | + | use syntect::parsing::SyntaxSet; | |
| 64 | + | use time::OffsetDateTime; | |
| 65 | + | ||
| 66 | + | use crate::auth::CurrentUser; | |
| 67 | + | ||
| 68 | + | const STYLE: &str = r#" | |
| 69 | + | :root { --fg:#1f2328; --muted:#656d76; --bg:#fff; --border:#d0d7de; --accent:#0969da; --code-bg:#f6f8fa; } | |
| 70 | + | * { box-sizing:border-box; } | |
| 71 | + | body { margin:0; font:14px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif; color:var(--fg); background:var(--bg); } | |
| 72 | + | a { color:var(--accent); text-decoration:none; } a:hover { text-decoration:underline; } | |
| 73 | + | header.top { border-bottom:1px solid var(--border); padding:12px 0; background:var(--code-bg); } | |
| 74 | + | .container { max-width:980px; margin:0 auto; padding:0 16px; } | |
| 75 | + | header.top .container { display:flex; align-items:center; gap:12px; } | |
| 76 | + | .brand { font-weight:700; font-size:16px; color:var(--fg); } | |
| 77 | + | main { padding:24px 0; } | |
| 78 | + | h1,h2 { font-weight:600; } h1 { font-size:20px; } h2 { font-size:15px; margin:20px 0 8px; } | |
| 79 | + | .muted { color:var(--muted); } | |
| 80 | + | .repo-list { list-style:none; padding:0; margin:0; } | |
| 81 | + | .repo-list li { padding:12px 0; border-bottom:1px solid var(--border); } | |
| 82 | + | .repo-list .name { font-size:16px; font-weight:600; } | |
| 83 | + | .box { border:1px solid var(--border); border-radius:6px; overflow:hidden; } | |
| 84 | + | .box .row { display:flex; justify-content:space-between; padding:8px 16px; border-top:1px solid var(--border); } | |
| 85 | + | .box .row:first-child { border-top:0; } | |
| 86 | + | .box .row a.entry { display:flex; gap:8px; align-items:center; } | |
| 87 | + | .icon { width:16px; color:var(--muted); } | |
| 88 | + | table.code { border-collapse:collapse; width:100%; font:12px/1.45 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; } | |
| 89 | + | table.code td { padding:0 10px; vertical-align:top; white-space:pre; } | |
| 90 | + | table.code td.ln { text-align:right; color:var(--muted); user-select:none; width:1%; border-right:1px solid var(--border); background:var(--code-bg); } | |
| 91 | + | .clone { border:1px solid var(--border); border-radius:6px; padding:12px 16px; margin:16px 0; } | |
| 92 | + | .clone-head { display:flex; align-items:center; gap:12px; margin-bottom:8px; } | |
| 93 | + | .clone-tabs { display:flex; gap:4px; margin-left:auto; } | |
| 94 | + | .clone-tab { font-size:12px; padding:2px 10px; border:1px solid var(--border); border-radius:2em; background:var(--bg); color:var(--muted); cursor:pointer; } | |
| 95 | + | .clone-tab.active { background:var(--accent); color:#fff; border-color:var(--accent); } | |
| 96 | + | .clone-cmd { display:flex; align-items:center; gap:8px; background:var(--code-bg); border:1px solid var(--border); border-radius:6px; padding:6px 10px; } | |
| 97 | + | .clone-cmd code { flex:1; font:12px ui-monospace,monospace; user-select:all; overflow-x:auto; white-space:nowrap; } | |
| 98 | + | .copy-btn { display:inline-flex; align-items:center; background:none; border:0; color:var(--muted); cursor:pointer; padding:2px; } | |
| 99 | + | .copy-btn:hover { color:var(--fg); } | |
| 100 | + | .copied-msg { display:none; color:#1a7f37; font-size:12px; } | |
| 101 | + | .clone.copied .copied-msg { display:inline; } | |
| 102 | + | .clone.copied .copy-btn { color:#1a7f37; } | |
| 103 | + | .crumbs { margin:12px 0; font:13px ui-monospace,monospace; } | |
| 104 | + | .pill { display:inline-block; background:var(--code-bg); border:1px solid var(--border); border-radius:2em; padding:1px 8px; font-size:12px; color:var(--muted); } | |
| 105 | + | .linkbtn { background:none; border:0; color:var(--accent); cursor:pointer; font:inherit; padding:0; } | |
| 106 | + | .linkbtn:hover { text-decoration:underline; } | |
| 107 | + | .btn { display:inline-block; background:var(--accent); color:#fff; border:1px solid var(--accent); border-radius:6px; padding:5px 12px; font-size:13px; cursor:pointer; } | |
| 108 | + | .btn:hover { text-decoration:none; opacity:.92; } | |
| 109 | + | form.stack p { margin:10px 0; } form.stack label { font-size:13px; color:var(--muted); } | |
| 110 | + | form.stack input[type=text], form.stack textarea { width:100%; max-width:480px; padding:6px 8px; border:1px solid var(--border); border-radius:6px; font:inherit; } | |
| 111 | + | form.stack .check { display:flex; gap:8px; align-items:flex-start; max-width:480px; } | |
| 112 | + | .commit-list { list-style:none; padding:0; margin:0; } | |
| 113 | + | .commit-list li { padding:8px 0; border-top:1px solid var(--border); display:flex; gap:12px; align-items:baseline; } | |
| 114 | + | .commit-list li:first-child { border-top:0; } | |
| 115 | + | .sha { font:12px ui-monospace,monospace; color:var(--muted); } | |
| 116 | + | .file-diff { margin:16px 0; } | |
| 117 | + | .file-diff .head { background:var(--code-bg); border:1px solid var(--border); border-bottom:0; border-radius:6px 6px 0 0; padding:6px 12px; font:12px ui-monospace,monospace; } | |
| 118 | + | table.diff { border:1px solid var(--border); border-radius:0 0 6px 6px; } | |
| 119 | + | table.diff td.sign { width:1%; text-align:center; color:var(--muted); user-select:none; } | |
| 120 | + | table.diff tr.ins { background:#e6ffec; } table.diff tr.ins td.sign { color:#1a7f37; } | |
| 121 | + | table.diff tr.del { background:#ffebe9; } table.diff tr.del td.sign { color:#cf222e; } | |
| 122 | + | .badge { font-size:11px; border-radius:3px; padding:1px 6px; } | |
| 123 | + | .badge.add { background:#dafbe1; color:#1a7f37; } .badge.del { background:#ffebe9; color:#cf222e; } .badge.mod { background:#fff8c5; color:#7d4e00; } | |
| 124 | + | footer { color:var(--muted); font-size:12px; padding:24px 0; border-top:1px solid var(--border); margin-top:32px; } | |
| 125 | + | "#; | |
| 126 | + | ||
| 127 | + | /// Clipboard icon for the clone "copy" button. | |
| 128 | + | const CLIPBOARD_SVG: &str = r#"<svg viewBox="0 0 16 16" width="15" height="15" fill="currentColor" aria-hidden="true"><path d="M10 1.5H6a.5.5 0 0 0-.5.5v1H4A1.5 1.5 0 0 0 2.5 4.5v9A1.5 1.5 0 0 0 4 15h8a1.5 1.5 0 0 0 1.5-1.5v-9A1.5 1.5 0 0 0 12 3h-1.5V2a.5.5 0 0 0-.5-.5zm-3.5 1h3v1h-3v-1zM4 4.5h8v9H4v-9z"/></svg>"#; | |
| 129 | + | ||
| 130 | + | /// Delegated handlers for the clone widget: protocol toggle + copy-to-clipboard. | |
| 131 | + | /// Registered once on `document`, so it survives htmx body swaps. | |
| 132 | + | const CLONE_JS: &str = r#" | |
| 133 | + | (function(){ | |
| 134 | + | function copyText(t){ | |
| 135 | + | if (navigator.clipboard && navigator.clipboard.writeText) return navigator.clipboard.writeText(t); | |
| 136 | + | var ta=document.createElement('textarea'); ta.value=t; ta.style.position='fixed'; ta.style.opacity='0'; | |
| 137 | + | document.body.appendChild(ta); ta.focus(); ta.select(); | |
| 138 | + | try{document.execCommand('copy')}catch(e){} | |
| 139 | + | document.body.removeChild(ta); return Promise.resolve(); | |
| 140 | + | } | |
| 141 | + | document.addEventListener('click', function(e){ | |
| 142 | + | var tab=e.target.closest('.clone-tab'); | |
| 143 | + | if(tab){ | |
| 144 | + | var box=tab.closest('.clone'), cmd=box.dataset[tab.dataset.proto]; | |
| 145 | + | if(cmd){ box.querySelector('.clone-cmd code').textContent=cmd; } | |
| 146 | + | box.querySelectorAll('.clone-tab').forEach(function(t){ t.classList.toggle('active', t===tab); }); | |
| 147 | + | return; | |
| 148 | + | } | |
| 149 | + | var copy=e.target.closest('.copy-btn'); | |
| 150 | + | if(copy){ | |
| 151 | + | var box=copy.closest('.clone'); | |
| 152 | + | copyText(box.querySelector('.clone-cmd code').textContent).then(function(){ | |
| 153 | + | box.classList.add('copied'); | |
| 154 | + | setTimeout(function(){ box.classList.remove('copied'); }, 1300); | |
| 155 | + | }); | |
| 156 | + | } | |
| 157 | + | }); | |
| 158 | + | })(); | |
| 159 | + | "#; | |
| 160 | + | ||
| 161 | + | /// Mount the web UI routes. | |
| 162 | + | pub fn routes(router: Router<App>) -> Router<App> { | |
| 163 | + | router | |
| 164 | + | .route("/", get(home)) | |
| 165 | + | .route("/-/settings", get(account_settings)) | |
| 166 | + | .route("/-/settings/keys", post(add_ssh_key)) | |
| 167 | + | .route("/-/settings/keys/{id}/delete", post(delete_ssh_key)) | |
| 168 | + | .route("/-/new", get(new_repo_form).post(new_repo_submit)) | |
| 169 | + | .route("/{username}", get(user_profile)) | |
| 170 | + | .route( | |
| 171 | + | "/{owner}/{repo}/settings", | |
| 172 | + | get(repo_settings).post(repo_settings_submit), | |
| 173 | + | ) | |
| 174 | + | .route("/{owner}/{repo}", get(repo_index)) | |
| 175 | + | .route("/{owner}/{repo}/tree/{rev}", get(tree_root)) | |
| 176 | + | .route("/{owner}/{repo}/tree/{rev}/{*path}", get(tree_path)) | |
| 177 | + | .route("/{owner}/{repo}/blob/{rev}/{*path}", get(blob)) | |
| 178 | + | .route("/{owner}/{repo}/commits/{rev}", get(commits)) | |
| 179 | + | .route("/{owner}/{repo}/commit/{id}", get(commit)) | |
| 180 | + | .route("/-/static/htmx.min.js", get(htmx_js)) | |
| 181 | + | } | |
| 182 | + | ||
| 183 | + | /// Serve the vendored htmx script (embedded in the binary). | |
| 184 | + | async fn htmx_js() -> Response { | |
| 185 | + | ( | |
| 186 | + | [( | |
| 187 | + | header::CONTENT_TYPE, | |
| 188 | + | "application/javascript; charset=utf-8", | |
| 189 | + | )], | |
| 190 | + | include_str!("../assets/htmx.min.js"), | |
| 191 | + | ) | |
| 192 | + | .into_response() | |
| 193 | + | } | |
| 194 | + | ||
| 195 | + | pub(crate) fn layout(title: &str, user: Option<&User>, body: Markup) -> Markup { | |
| 196 | + | html! { | |
| 197 | + | (DOCTYPE) | |
| 198 | + | html lang="en" { | |
| 199 | + | head { | |
| 200 | + | meta charset="utf-8"; | |
| 201 | + | meta name="viewport" content="width=device-width, initial-scale=1"; | |
| 202 | + | title { (title) " · anvil" } | |
| 203 | + | style { (PreEscaped(STYLE)) } | |
| 204 | + | } | |
| 205 | + | body hx-boost="true" { | |
| 206 | + | header.top { div.container { | |
| 207 | + | a.brand href="/" { "anvil" } | |
| 208 | + | span style="margin-left:auto" { | |
| 209 | + | @match user { | |
| 210 | + | Some(u) => { | |
| 211 | + | a href="/-/settings" { (u.username) } | |
| 212 | + | " · " | |
| 213 | + | form method="post" action="/-/logout" style="display:inline" { | |
| 214 | + | button.linkbtn type="submit" { "sign out" } | |
| 215 | + | } | |
| 216 | + | } | |
| 217 | + | None => { a href="/-/login" { "sign in" } } | |
| 218 | + | } | |
| 219 | + | } | |
| 220 | + | } } | |
| 221 | + | main { div.container { (body) } } | |
| 222 | + | footer { div.container { "anvil — a minimal git forge" } } | |
| 223 | + | script src="/-/static/htmx.min.js" {} | |
| 224 | + | script { (PreEscaped(CLONE_JS)) } | |
| 225 | + | } | |
| 226 | + | } | |
| 227 | + | } | |
| 228 | + | } | |
| 229 | + | ||
| 230 | + | fn not_found(message: &str) -> Response { | |
| 231 | + | ( | |
| 232 | + | StatusCode::NOT_FOUND, | |
| 233 | + | layout( | |
| 234 | + | "Not found", | |
| 235 | + | None, | |
| 236 | + | html! { h1 { "Not found" } p.muted { (message) } }, | |
| 237 | + | ), | |
| 238 | + | ) | |
| 239 | + | .into_response() | |
| 240 | + | } | |
| 241 | + | ||
| 242 | + | fn server_error(err: impl std::fmt::Display) -> Response { | |
| 243 | + | tracing::error!("ui error: {err}"); | |
| 244 | + | ( | |
| 245 | + | StatusCode::INTERNAL_SERVER_ERROR, | |
| 246 | + | layout("Error", None, html! { h1 { "Something went wrong" } }), | |
| 247 | + | ) | |
| 248 | + | .into_response() | |
| 249 | + | } | |
| 250 | + | ||
| 251 | + | /// Resolve `<owner>/<repo>` to its on-disk path and metadata row, enforcing read | |
| 252 | + | /// access for `viewer`. Private repos 404 for non-owners (no existence leak). | |
| 253 | + | async fn resolve_repo( | |
| 254 | + | app: &App, | |
| 255 | + | viewer: Option<&User>, | |
| 256 | + | owner: &str, | |
| 257 | + | name: &str, | |
| 258 | + | ) -> Result<(PathBuf, Repository), Response> { | |
| 259 | + | let owner_user = users::find_by_username(&app.db, owner) | |
| 260 | + | .await | |
| 261 | + | .map_err(server_error)? | |
| 262 | + | .ok_or_else(|| not_found("no such user"))?; | |
| 263 | + | let repo = repos::find(&app.db, owner_user.id, name) | |
| 264 | + | .await | |
| 265 | + | .map_err(server_error)? | |
| 266 | + | .ok_or_else(|| not_found("no such repository"))?; | |
| 267 | + | if !access::can_read(&repo, viewer) { | |
| 268 | + | return Err(not_found("no such repository")); | |
| 269 | + | } | |
| 270 | + | let path = anvil_core::storage::repo_path(&app.config.repositories_dir(), owner, name); | |
| 271 | + | if !path.exists() { | |
| 272 | + | return Err(not_found("repository not found on disk")); | |
| 273 | + | } | |
| 274 | + | Ok((path, repo)) | |
| 275 | + | } | |
| 276 | + | ||
| 277 | + | /// `GET /` — list repositories visible to the current user. | |
| 278 | + | async fn home(State(app): State<App>, CurrentUser(user): CurrentUser) -> Result<Markup, Response> { | |
| 279 | + | let all = repos::list_all_with_owner(&app.db) | |
| 280 | + | .await | |
| 281 | + | .map_err(server_error)?; | |
| 282 | + | let repos: Vec<_> = all | |
| 283 | + | .into_iter() | |
| 284 | + | .filter(|r| { | |
| 285 | + | !r.is_private | |
| 286 | + | || user | |
| 287 | + | .as_ref() | |
| 288 | + | .is_some_and(|u| u.id == r.owner_id || u.is_admin) | |
| 289 | + | }) | |
| 290 | + | .collect(); | |
| 291 | + | Ok(layout( | |
| 292 | + | "Repositories", | |
| 293 | + | user.as_ref(), | |
| 294 | + | html! { | |
| 295 | + | div style="display:flex;align-items:center" { | |
| 296 | + | h1 style="margin-right:auto" { "Repositories" } | |
| 297 | + | @if user.is_some() { a.btn href="/-/new" { "New repository" } } | |
| 298 | + | } | |
| 299 | + | @if repos.is_empty() { | |
| 300 | + | p.muted { | |
| 301 | + | "No repositories yet. " | |
| 302 | + | @if user.is_some() { a href="/-/new" { "Create one" } "." } | |
| 303 | + | @else { "Sign in to create one." } | |
| 304 | + | } | |
| 305 | + | } @else { | |
| 306 | + | ul.repo-list { | |
| 307 | + | @for r in &repos { | |
| 308 | + | li { | |
| 309 | + | div.name { | |
| 310 | + | a href=(format!("/{}", r.owner)) { (r.owner) } | |
| 311 | + | "/" | |
| 312 | + | a href=(format!("/{}/{}", r.owner, r.name)) { (r.name) } | |
| 313 | + | @if r.is_private { " " span.pill { "private" } } | |
| 314 | + | } | |
| 315 | + | @if !r.description.is_empty() { div.muted { (r.description) } } | |
| 316 | + | } | |
| 317 | + | } | |
| 318 | + | } | |
| 319 | + | } | |
| 320 | + | }, | |
| 321 | + | )) | |
| 322 | + | } | |
| 323 | + | ||
| 324 | + | /// `GET /{username}` — a user's profile: their repositories (public to all; | |
| 325 | + | /// private only to themselves or an admin). | |
| 326 | + | async fn user_profile( | |
| 327 | + | State(app): State<App>, | |
| 328 | + | CurrentUser(viewer): CurrentUser, | |
| 329 | + | Path(username): Path<String>, | |
| 330 | + | ) -> Result<Markup, Response> { | |
| 331 | + | let owner = users::find_by_username(&app.db, &username) | |
| 332 | + | .await | |
| 333 | + | .map_err(server_error)? | |
| 334 | + | .ok_or_else(|| not_found("no such user"))?; | |
| 335 | + | let visible: Vec<_> = repos::list_by_owner(&app.db, owner.id) | |
| 336 | + | .await | |
| 337 | + | .map_err(server_error)? | |
| 338 | + | .into_iter() | |
| 339 | + | .filter(|r| access::can_read(r, viewer.as_ref())) | |
| 340 | + | .collect(); | |
| 341 | + | let is_self = viewer.as_ref().is_some_and(|u| u.id == owner.id); | |
| 342 | + | ||
| 343 | + | Ok(layout( | |
| 344 | + | &owner.username, | |
| 345 | + | viewer.as_ref(), | |
| 346 | + | html! { | |
| 347 | + | div style="display:flex;align-items:center" { | |
| 348 | + | h1 style="margin-right:auto" { (owner.username) } | |
| 349 | + | @if is_self { a.btn href="/-/new" { "New repository" } } | |
| 350 | + | } | |
| 351 | + | @if !owner.email.is_empty() { p.muted { (owner.email) } } | |
| 352 | + | h2 { "Repositories" } | |
| 353 | + | @if visible.is_empty() { | |
| 354 | + | p.muted { "No repositories." } | |
| 355 | + | } @else { | |
| 356 | + | ul.repo-list { | |
| 357 | + | @for r in &visible { | |
| 358 | + | li { | |
| 359 | + | div.name { | |
| 360 | + | a href=(format!("/{}/{}", owner.username, r.name)) { (r.name) } | |
| 361 | + | @if r.is_private { " " span.pill { "private" } } | |
| 362 | + | } | |
| 363 | + | @if !r.description.is_empty() { div.muted { (r.description) } } | |
| 364 | + | } | |
| 365 | + | } | |
| 366 | + | } | |
| 367 | + | } | |
| 368 | + | }, | |
| 369 | + | )) | |
| 370 | + | } | |
| 371 | + | ||
| 372 | + | #[derive(serde::Deserialize)] | |
| 373 | + | struct AddKeyForm { | |
| 374 | + | #[serde(default)] | |
| 375 | + | title: String, | |
| 376 | + | key: String, | |
| 377 | + | } | |
| 378 | + | ||
| 379 | + | /// `GET /settings` — account settings: profile + SSH keys. | |
| 380 | + | async fn account_settings(State(app): State<App>, CurrentUser(user): CurrentUser) -> Response { | |
| 381 | + | let Some(user) = user else { | |
| 382 | + | return Redirect::to("/-/login").into_response(); | |
| 383 | + | }; | |
| 384 | + | let keys = match ssh_keys::list_by_user(&app.db, user.id).await { | |
| 385 | + | Ok(keys) => keys, | |
| 386 | + | Err(e) => return server_error(e), | |
| 387 | + | }; | |
| 388 | + | account_page(&user, &keys, None).into_response() | |
| 389 | + | } | |
| 390 | + | ||
| 391 | + | /// `POST /settings/keys` — register an SSH public key for the current user. | |
| 392 | + | async fn add_ssh_key( | |
| 393 | + | State(app): State<App>, | |
| 394 | + | CurrentUser(user): CurrentUser, | |
| 395 | + | Form(form): Form<AddKeyForm>, | |
| 396 | + | ) -> Response { | |
| 397 | + | let Some(user) = user else { | |
| 398 | + | return Redirect::to("/-/login").into_response(); | |
| 399 | + | }; | |
| 400 | + | let result = match ssh_keys::parse_public_key(&form.key) { | |
| 401 | + | Ok((fingerprint, content)) => { | |
| 402 | + | ssh_keys::add(&app.db, user.id, &form.title, &fingerprint, &content) | |
| 403 | + | .await | |
| 404 | + | .map(|_| ()) | |
| 405 | + | } | |
| 406 | + | Err(e) => Err(e), | |
| 407 | + | }; | |
| 408 | + | match result { | |
| 409 | + | Ok(()) => Redirect::to("/-/settings").into_response(), | |
| 410 | + | Err(e) => { | |
| 411 | + | let keys = ssh_keys::list_by_user(&app.db, user.id) | |
| 412 | + | .await | |
| 413 | + | .unwrap_or_default(); | |
| 414 | + | ( | |
| 415 | + | StatusCode::BAD_REQUEST, | |
| 416 | + | account_page(&user, &keys, Some(&e.to_string())), | |
| 417 | + | ) | |
| 418 | + | .into_response() | |
| 419 | + | } | |
| 420 | + | } | |
| 421 | + | } | |
| 422 | + | ||
| 423 | + | /// `POST /settings/keys/{id}/delete` — remove one of the current user's keys. | |
| 424 | + | async fn delete_ssh_key( | |
| 425 | + | State(app): State<App>, | |
| 426 | + | CurrentUser(user): CurrentUser, | |
| 427 | + | Path(id): Path<i64>, | |
| 428 | + | ) -> Response { | |
| 429 | + | let Some(user) = user else { | |
| 430 | + | return Redirect::to("/-/login").into_response(); | |
| 431 | + | }; | |
| 432 | + | if let Err(e) = ssh_keys::delete(&app.db, id, user.id).await { | |
| 433 | + | return server_error(e); | |
| 434 | + | } | |
| 435 | + | Redirect::to("/-/settings").into_response() | |
| 436 | + | } | |
| 437 | + | ||
| 438 | + | fn account_page(user: &User, keys: &[SshKey], error: Option<&str>) -> Markup { | |
| 439 | + | layout( | |
| 440 | + | "Account settings", | |
| 441 | + | Some(user), | |
| 442 | + | html! { | |
| 443 | + | h1 { "Account settings" } | |
| 444 | + | p.muted { | |
| 445 | + | "Signed in as " strong { (user.username) } | |
| 446 | + | @if !user.email.is_empty() { " · " (user.email) } | |
| 447 | + | } | |
| 448 | + | ||
| 449 | + | h2 { "SSH keys" } | |
| 450 | + | p.muted { "Add a public key to clone and push over SSH." } | |
| 451 | + | @if let Some(error) = error { p style="color:#cf222e" { (error) } } | |
| 452 | + | @if keys.is_empty() { | |
| 453 | + | p.muted { "No SSH keys yet." } | |
| 454 | + | } @else { | |
| 455 | + | div.box { | |
| 456 | + | @for k in keys { | |
| 457 | + | div.row { | |
| 458 | + | div { | |
| 459 | + | @if !k.title.is_empty() { strong { (k.title) } " " } | |
| 460 | + | span.sha { (k.fingerprint) } | |
| 461 | + | div.muted style="font-size:12px" { "added " (fmt_time(k.created_at)) } | |
| 462 | + | } | |
| 463 | + | form method="post" action=(format!("/-/settings/keys/{}/delete", k.id)) { | |
| 464 | + | button.linkbtn type="submit" { "delete" } | |
| 465 | + | } | |
| 466 | + | } | |
| 467 | + | } | |
| 468 | + | } | |
| 469 | + | } | |
| 470 | + | ||
| 471 | + | form.stack method="post" action="/-/settings/keys" style="margin-top:16px" { | |
| 472 | + | p { label { "Title" br; input type="text" name="title" placeholder="laptop"; } } | |
| 473 | + | p { label { "Public key" br; textarea name="key" rows="4" placeholder="ssh-ed25519 AAAA…" {} } } | |
| 474 | + | p { button.btn type="submit" { "Add SSH key" } } | |
| 475 | + | } | |
| 476 | + | }, | |
| 477 | + | ) | |
| 478 | + | } | |
| 479 | + | ||
| 480 | + | fn forbidden() -> Response { | |
| 481 | + | ( | |
| 482 | + | StatusCode::FORBIDDEN, | |
| 483 | + | layout( | |
| 484 | + | "Forbidden", | |
| 485 | + | None, | |
| 486 | + | html! { h1 { "Forbidden" } p.muted { "You don't have access to this." } }, | |
| 487 | + | ), | |
| 488 | + | ) | |
| 489 | + | .into_response() | |
| 490 | + | } | |
| 491 | + | ||
| 492 | + | #[derive(serde::Deserialize)] | |
| 493 | + | struct NewRepoForm { | |
| 494 | + | name: String, | |
| 495 | + | #[serde(default)] | |
| 496 | + | description: String, | |
| 497 | + | private: Option<String>, | |
| 498 | + | } | |
| 499 | + | ||
| 500 | + | #[derive(serde::Deserialize)] | |
| 501 | + | struct SettingsForm { | |
| 502 | + | #[serde(default)] | |
| 503 | + | description: String, | |
| 504 | + | private: Option<String>, | |
| 505 | + | } | |
| 506 | + | ||
| 507 | + | /// `GET /new` — new-repository form (requires login). | |
| 508 | + | async fn new_repo_form(CurrentUser(user): CurrentUser) -> Response { | |
| 509 | + | let Some(user) = user else { | |
| 510 | + | return Redirect::to("/-/login").into_response(); | |
| 511 | + | }; | |
| 512 | + | new_repo_page(&user, None, "", "", false).into_response() | |
| 513 | + | } | |
| 514 | + | ||
| 515 | + | /// `POST /new` — create a repository owned by the current user. | |
| 516 | + | async fn new_repo_submit( | |
| 517 | + | State(app): State<App>, | |
| 518 | + | CurrentUser(user): CurrentUser, | |
| 519 | + | Form(form): Form<NewRepoForm>, | |
| 520 | + | ) -> Response { | |
| 521 | + | let Some(user) = user else { | |
| 522 | + | return Redirect::to("/-/login").into_response(); | |
| 523 | + | }; | |
| 524 | + | let private = form.private.is_some(); | |
| 525 | + | match repos::create( | |
| 526 | + | &app.db, | |
| 527 | + | &app.config.repositories_dir(), | |
| 528 | + | &user, | |
| 529 | + | &form.name, | |
| 530 | + | &form.description, | |
| 531 | + | private, | |
| 532 | + | ) | |
| 533 | + | .await | |
| 534 | + | { | |
| 535 | + | Ok(repo) => Redirect::to(&format!("/{}/{}", user.username, repo.name)).into_response(), | |
| 536 | + | Err(e) => ( | |
| 537 | + | StatusCode::BAD_REQUEST, | |
| 538 | + | new_repo_page( | |
| 539 | + | &user, | |
| 540 | + | Some(&e.to_string()), | |
| 541 | + | &form.name, | |
| 542 | + | &form.description, | |
| 543 | + | private, | |
| 544 | + | ), | |
| 545 | + | ) | |
| 546 | + | .into_response(), | |
| 547 | + | } | |
| 548 | + | } | |
| 549 | + | ||
| 550 | + | fn new_repo_page( | |
| 551 | + | user: &User, | |
| 552 | + | error: Option<&str>, | |
| 553 | + | name: &str, | |
| 554 | + | description: &str, | |
| 555 | + | private: bool, | |
| 556 | + | ) -> Markup { | |
| 557 | + | layout( | |
| 558 | + | "New repository", | |
| 559 | + | Some(user), | |
| 560 | + | html! { | |
| 561 | + | h1 { "New repository" } | |
| 562 | + | @if let Some(error) = error { p style="color:#cf222e" { (error) } } | |
| 563 | + | form.stack method="post" action="/-/new" { | |
| 564 | + | p { label { "Name" br; input type="text" name="name" value=(name) placeholder="my-project" autofocus; } } | |
| 565 | + | p { label { "Description" br; input type="text" name="description" value=(description); } } | |
| 566 | + | p { label.check { input type="checkbox" name="private" value="on" checked[private]; span { "Private — only you can see and push to it" } } } | |
| 567 | + | p { button.btn type="submit" { "Create repository" } } | |
| 568 | + | } | |
| 569 | + | p.muted { "It will be created at " code { (user.username) "/" (if name.is_empty() { "<name>" } else { name }) } "." } | |
| 570 | + | }, | |
| 571 | + | ) | |
| 572 | + | } | |
| 573 | + | ||
| 574 | + | /// Load a repo for an owner-only settings action, enforcing write access. | |
| 575 | + | async fn resolve_for_settings( | |
| 576 | + | app: &App, | |
| 577 | + | viewer: Option<&User>, | |
| 578 | + | owner: &str, | |
| 579 | + | name: &str, | |
| 580 | + | ) -> Result<Repository, Response> { | |
| 581 | + | let owner_user = users::find_by_username(&app.db, owner) | |
| 582 | + | .await | |
| 583 | + | .map_err(server_error)? | |
| 584 | + | .ok_or_else(|| not_found("no such repository"))?; | |
| 585 | + | let repo = repos::find(&app.db, owner_user.id, name) | |
| 586 | + | .await | |
| 587 | + | .map_err(server_error)? | |
| 588 | + | .ok_or_else(|| not_found("no such repository"))?; | |
| 589 | + | if !access::can_read(&repo, viewer) { | |
| 590 | + | return Err(not_found("no such repository")); | |
| 591 | + | } | |
| 592 | + | if !access::can_write(&repo, viewer) { | |
| 593 | + | return Err(forbidden()); | |
| 594 | + | } | |
| 595 | + | Ok(repo) | |
| 596 | + | } | |
| 597 | + | ||
| 598 | + | /// `GET /{owner}/{repo}/settings` — owner-only repository settings. | |
| 599 | + | async fn repo_settings( | |
| 600 | + | State(app): State<App>, | |
| 601 | + | CurrentUser(user): CurrentUser, | |
| 602 | + | Path((owner, repo)): Path<(String, String)>, | |
| 603 | + | ) -> Response { | |
| 604 | + | let meta = match resolve_for_settings(&app, user.as_ref(), &owner, &repo).await { | |
| 605 | + | Ok(m) => m, | |
| 606 | + | Err(resp) => return resp, | |
| 607 | + | }; | |
| 608 | + | settings_page(user.as_ref(), &owner, &repo, &meta, None).into_response() | |
| 609 | + | } | |
| 610 | + | ||
| 611 | + | /// `POST /{owner}/{repo}/settings` — update description / visibility. | |
| 612 | + | async fn repo_settings_submit( | |
| 613 | + | State(app): State<App>, | |
| 614 | + | CurrentUser(user): CurrentUser, | |
| 615 | + | Path((owner, repo)): Path<(String, String)>, | |
| 616 | + | Form(form): Form<SettingsForm>, | |
| 617 | + | ) -> Response { | |
| 618 | + | let meta = match resolve_for_settings(&app, user.as_ref(), &owner, &repo).await { | |
| 619 | + | Ok(m) => m, | |
| 620 | + | Err(resp) => return resp, | |
| 621 | + | }; | |
| 622 | + | if let Err(e) = | |
| 623 | + | repos::update_settings(&app.db, meta.id, &form.description, form.private.is_some()).await | |
| 624 | + | { | |
| 625 | + | return server_error(e); | |
| 626 | + | } | |
| 627 | + | Redirect::to(&format!("/{owner}/{repo}")).into_response() | |
| 628 | + | } | |
| 629 | + | ||
| 630 | + | fn settings_page( | |
| 631 | + | user: Option<&User>, | |
| 632 | + | owner: &str, | |
| 633 | + | repo: &str, | |
| 634 | + | meta: &Repository, | |
| 635 | + | error: Option<&str>, | |
| 636 | + | ) -> Markup { | |
| 637 | + | layout( | |
| 638 | + | &format!("{owner}/{repo}: settings"), | |
| 639 | + | user, | |
| 640 | + | html! { | |
| 641 | + | h1 { a href=(format!("/{owner}/{repo}")) { (owner) "/" (repo) } " · settings" } | |
| 642 | + | @if let Some(error) = error { p style="color:#cf222e" { (error) } } | |
| 643 | + | form.stack method="post" action=(format!("/{owner}/{repo}/settings")) { | |
| 644 | + | p { label { "Description" br; input type="text" name="description" value=(meta.description); } } | |
| 645 | + | p { label.check { input type="checkbox" name="private" value="on" checked[meta.is_private]; span { "Private — only you can see and push to it" } } } | |
| 646 | + | p { button.btn type="submit" { "Save changes" } } | |
| 647 | + | } | |
| 648 | + | }, | |
| 649 | + | ) | |
| 650 | + | } | |
| 651 | + | ||
| 652 | + | fn clone_box(app: &App, owner: &str, name: &str) -> Markup { | |
| 653 | + | let http = app.config.http_clone_url(owner, name); | |
| 654 | + | let ssh = app | |
| 655 | + | .config | |
| 656 | + | .ssh | |
| 657 | + | .enabled | |
| 658 | + | .then(|| app.config.ssh_clone_url(owner, name)); | |
| 659 | + | html! { | |
| 660 | + | div.clone data-http=(format!("git clone {http}")) data-ssh=[ssh.as_ref().map(|s| format!("git clone {s}"))] { | |
| 661 | + | div.clone-head { | |
| 662 | + | span.muted { "Clone" } | |
| 663 | + | div.clone-tabs { | |
| 664 | + | button.clone-tab.active type="button" data-proto="http" { "HTTP" } | |
| 665 | + | @if ssh.is_some() { | |
| 666 | + | button.clone-tab type="button" data-proto="ssh" { "SSH" } | |
| 667 | + | } | |
| 668 | + | } | |
| 669 | + | } | |
| 670 | + | div.clone-cmd { | |
| 671 | + | code { "git clone " (http) } | |
| 672 | + | button.copy-btn type="button" title="Copy to clipboard" aria-label="Copy" { | |
| 673 | + | (PreEscaped(CLIPBOARD_SVG)) | |
| 674 | + | } | |
| 675 | + | span.copied-msg { "Copied!" } | |
| 676 | + | } | |
| 677 | + | } | |
| 678 | + | } | |
| 679 | + | } | |
| 680 | + | ||
| 681 | + | /// `GET /{owner}/{repo}` — repository overview with the root tree. | |
| 682 | + | async fn repo_index( | |
| 683 | + | State(app): State<App>, | |
| 684 | + | CurrentUser(user): CurrentUser, | |
| 685 | + | Path((owner, repo)): Path<(String, String)>, | |
| 686 | + | ) -> Result<Markup, Response> { | |
| 687 | + | let (path, meta) = resolve_repo(&app, user.as_ref(), &owner, &repo).await?; | |
| 688 | + | let overview = browse::overview(&path).map_err(server_error)?; | |
| 689 | + | ||
| 690 | + | let can_write = access::can_write(&meta, user.as_ref()); | |
| 691 | + | let header = html! { | |
| 692 | + | div style="display:flex;align-items:center;gap:8px" { | |
| 693 | + | h1 style="margin-right:auto" { | |
| 694 | + | a href="/" { "anvil" } " / " | |
| 695 | + | a href=(format!("/{owner}")) { (owner) } " / " (repo) | |
| 696 | + | @if meta.is_private { " " span.pill { "private" } } | |
| 697 | + | } | |
| 698 | + | @if can_write { | |
| 699 | + | a.btn href=(format!("/{owner}/{repo}/settings")) { "Settings" } | |
| 700 | + | } | |
| 701 | + | } | |
| 702 | + | @if !meta.description.is_empty() { p.muted { (meta.description) } } | |
| 703 | + | p { | |
| 704 | + | span.pill { (overview.branches.len()) " branches" } | |
| 705 | + | " " | |
| 706 | + | span.pill { (overview.tags.len()) " tags" } | |
| 707 | + | } | |
| 708 | + | (clone_box(&app, &owner, &repo)) | |
| 709 | + | }; | |
| 710 | + | ||
| 711 | + | if overview.is_empty { | |
| 712 | + | return Ok(layout( | |
| 713 | + | &format!("{owner}/{repo}"), | |
| 714 | + | user.as_ref(), | |
| 715 | + | html! { | |
| 716 | + | (header) | |
| 717 | + | p.muted { "This repository is empty. Push to it to get started." } | |
| 718 | + | }, | |
| 719 | + | )); | |
| 720 | + | } | |
| 721 | + | ||
| 722 | + | let rev = overview | |
| 723 | + | .default_branch | |
| 724 | + | .clone() | |
| 725 | + | .unwrap_or_else(|| "HEAD".to_string()); | |
| 726 | + | let entries = browse::list_tree(&path, &rev, "").map_err(server_error)?; | |
| 727 | + | ||
| 728 | + | Ok(layout( | |
| 729 | + | &format!("{owner}/{repo}"), | |
| 730 | + | user.as_ref(), | |
| 731 | + | html! { | |
| 732 | + | (header) | |
| 733 | + | p.muted { | |
| 734 | + | "Branch: " (rev) " · " | |
| 735 | + | a href=(format!("/{owner}/{repo}/commits/{rev}")) { "commits" } | |
| 736 | + | } | |
| 737 | + | (tree_table(&owner, &repo, &rev, "", &entries)) | |
| 738 | + | }, | |
| 739 | + | )) | |
| 740 | + | } | |
| 741 | + | ||
| 742 | + | async fn tree_root( | |
| 743 | + | State(app): State<App>, | |
| 744 | + | user: CurrentUser, | |
| 745 | + | Path((owner, repo, rev)): Path<(String, String, String)>, | |
| 746 | + | ) -> Result<Markup, Response> { | |
| 747 | + | render_tree(&app, user, &owner, &repo, &rev, "").await | |
| 748 | + | } | |
| 749 | + | ||
| 750 | + | async fn tree_path( | |
| 751 | + | State(app): State<App>, | |
| 752 | + | user: CurrentUser, | |
| 753 | + | Path((owner, repo, rev, path)): Path<(String, String, String, String)>, | |
| 754 | + | ) -> Result<Markup, Response> { | |
| 755 | + | render_tree(&app, user, &owner, &repo, &rev, &path).await | |
| 756 | + | } | |
| 757 | + | ||
| 758 | + | async fn render_tree( | |
| 759 | + | app: &App, | |
| 760 | + | CurrentUser(user): CurrentUser, | |
| 761 | + | owner: &str, | |
| 762 | + | repo: &str, | |
| 763 | + | rev: &str, | |
| 764 | + | path: &str, | |
| 765 | + | ) -> Result<Markup, Response> { | |
| 766 | + | let (repo_path, _) = resolve_repo(app, user.as_ref(), owner, repo).await?; | |
| 767 | + | let entries = browse::list_tree(&repo_path, rev, path).map_err(server_error)?; | |
| 768 | + | Ok(layout( | |
| 769 | + | &format!("{owner}/{repo}: {path}"), | |
| 770 | + | user.as_ref(), | |
| 771 | + | html! { | |
| 772 | + | h1 { a href=(format!("/{owner}/{repo}")) { (owner) "/" (repo) } } | |
| 773 | + | (breadcrumbs(owner, repo, rev, path, false)) | |
| 774 | + | (tree_table(owner, repo, rev, path, &entries)) | |
| 775 | + | }, | |
| 776 | + | )) | |
| 777 | + | } | |
| 778 | + | ||
| 779 | + | /// `GET /{owner}/{repo}/blob/{rev}/{*path}` — view a file. | |
| 780 | + | async fn blob( | |
| 781 | + | State(app): State<App>, | |
| 782 | + | CurrentUser(user): CurrentUser, | |
| 783 | + | Path((owner, repo, rev, path)): Path<(String, String, String, String)>, | |
| 784 | + | ) -> Result<Markup, Response> { | |
| 785 | + | let (repo_path, _) = resolve_repo(&app, user.as_ref(), &owner, &repo).await?; | |
| 786 | + | let bytes = browse::read_blob(&repo_path, &rev, &path) | |
| 787 | + | .map_err(server_error)? | |
| 788 | + | .ok_or_else(|| not_found("file not found"))?; | |
| 789 | + | ||
| 790 | + | let body = if is_binary(&bytes) { | |
| 791 | + | html! { p.muted { "Binary file (" (bytes.len()) " bytes)" } } | |
| 792 | + | } else { | |
| 793 | + | let text = String::from_utf8_lossy(&bytes); | |
| 794 | + | let lines = highlight(&path, &text); | |
| 795 | + | html! { | |
| 796 | + | table.code { | |
| 797 | + | @for (i, line) in lines.iter().enumerate() { | |
| 798 | + | tr { | |
| 799 | + | td.ln { (i + 1) } | |
| 800 | + | td { (PreEscaped(line)) } | |
| 801 | + | } | |
| 802 | + | } | |
| 803 | + | } | |
| 804 | + | } | |
| 805 | + | }; | |
| 806 | + | ||
| 807 | + | Ok(layout( | |
| 808 | + | &format!("{owner}/{repo}: {path}"), | |
| 809 | + | user.as_ref(), | |
| 810 | + | html! { | |
| 811 | + | h1 { a href=(format!("/{owner}/{repo}")) { (owner) "/" (repo) } } | |
| 812 | + | (breadcrumbs(&owner, &repo, &rev, &path, true)) | |
| 813 | + | div.box style="overflow-x:auto" { (body) } | |
| 814 | + | }, | |
| 815 | + | )) | |
| 816 | + | } | |
| 817 | + | ||
| 818 | + | /// Render a tree listing as a box of rows; directories link to `tree`, files to `blob`. | |
| 819 | + | fn tree_table( | |
| 820 | + | owner: &str, | |
| 821 | + | repo: &str, | |
| 822 | + | rev: &str, | |
| 823 | + | path: &str, | |
| 824 | + | entries: &[browse::TreeEntry], | |
| 825 | + | ) -> Markup { | |
| 826 | + | let join = |name: &str| { | |
| 827 | + | if path.is_empty() { | |
| 828 | + | name.to_string() | |
| 829 | + | } else { | |
| 830 | + | format!("{path}/{name}") | |
| 831 | + | } | |
| 832 | + | }; | |
| 833 | + | html! { | |
| 834 | + | div.box { | |
| 835 | + | @if !path.is_empty() { | |
| 836 | + | div.row { | |
| 837 | + | a.entry href=(parent_link(owner, repo, rev, path)) { span.icon { ".." } "up" } | |
| 838 | + | } | |
| 839 | + | } | |
| 840 | + | @for e in entries { | |
| 841 | + | @let child = join(&e.name); | |
| 842 | + | @let kind = if e.is_dir { "tree" } else { "blob" }; | |
| 843 | + | div.row { | |
| 844 | + | a.entry href=(format!("/{owner}/{repo}/{kind}/{rev}/{child}")) { | |
| 845 | + | span.icon { (if e.is_dir { "[ ]" } else { "·" }) } | |
| 846 | + | (e.name) @if e.is_dir { "/" } | |
| 847 | + | } | |
| 848 | + | } | |
| 849 | + | } | |
| 850 | + | } | |
| 851 | + | } | |
| 852 | + | } | |
| 853 | + | ||
| 854 | + | fn parent_link(owner: &str, repo: &str, rev: &str, path: &str) -> String { | |
| 855 | + | match path.rsplit_once('/') { | |
| 856 | + | Some((parent, _)) => format!("/{owner}/{repo}/tree/{rev}/{parent}"), | |
| 857 | + | None => format!("/{owner}/{repo}/tree/{rev}"), | |
| 858 | + | } | |
| 859 | + | } | |
| 860 | + | ||
| 861 | + | /// Path breadcrumbs. `is_blob` marks the final component as a file. | |
| 862 | + | fn breadcrumbs(owner: &str, repo: &str, rev: &str, path: &str, is_blob: bool) -> Markup { | |
| 863 | + | // Precompute (label, cumulative_path) for each path component. | |
| 864 | + | let mut crumbs: Vec<(String, String)> = Vec::new(); | |
| 865 | + | let mut acc = String::new(); | |
| 866 | + | for part in path.split('/').filter(|p| !p.is_empty()) { | |
| 867 | + | if !acc.is_empty() { | |
| 868 | + | acc.push('/'); | |
| 869 | + | } | |
| 870 | + | acc.push_str(part); | |
| 871 | + | crumbs.push((part.to_string(), acc.clone())); | |
| 872 | + | } | |
| 873 | + | let last = crumbs.len(); | |
| 874 | + | html! { | |
| 875 | + | div.crumbs { | |
| 876 | + | a href=(format!("/{owner}/{repo}/tree/{rev}")) { (rev) } | |
| 877 | + | @for (i, (label, cum)) in crumbs.iter().enumerate() { | |
| 878 | + | " / " | |
| 879 | + | @if i + 1 == last && is_blob { | |
| 880 | + | span { (label) } | |
| 881 | + | } @else { | |
| 882 | + | a href=(format!("/{owner}/{repo}/tree/{rev}/{cum}")) { (label) } | |
| 883 | + | } | |
| 884 | + | } | |
| 885 | + | } | |
| 886 | + | } | |
| 887 | + | } | |
| 888 | + | ||
| 889 | + | /// `GET /{owner}/{repo}/commits/{rev}` — commit history. | |
| 890 | + | async fn commits( | |
| 891 | + | State(app): State<App>, | |
| 892 | + | CurrentUser(user): CurrentUser, | |
| 893 | + | Path((owner, repo, rev)): Path<(String, String, String)>, | |
| 894 | + | ) -> Result<Markup, Response> { | |
| 895 | + | let (path, _) = resolve_repo(&app, user.as_ref(), &owner, &repo).await?; | |
| 896 | + | let log = browse::commit_log(&path, &rev, 100).map_err(server_error)?; | |
| 897 | + | Ok(layout( | |
| 898 | + | &format!("{owner}/{repo}: commits"), | |
| 899 | + | user.as_ref(), | |
| 900 | + | html! { | |
| 901 | + | h1 { a href=(format!("/{owner}/{repo}")) { (owner) "/" (repo) } " · commits" } | |
| 902 | + | ul.commit-list { | |
| 903 | + | @for c in &log { | |
| 904 | + | li { | |
| 905 | + | a.sha href=(format!("/{owner}/{repo}/commit/{}", c.id)) { (c.short) } | |
| 906 | + | span { (c.summary) } | |
| 907 | + | span.muted style="margin-left:auto" { (c.author) " · " (fmt_time(c.time)) } | |
| 908 | + | } | |
| 909 | + | } | |
| 910 | + | } | |
| 911 | + | }, | |
| 912 | + | )) | |
| 913 | + | } | |
| 914 | + | ||
| 915 | + | /// `GET /{owner}/{repo}/commit/{id}` — a commit with its diff. | |
| 916 | + | async fn commit( | |
| 917 | + | State(app): State<App>, | |
| 918 | + | CurrentUser(user): CurrentUser, | |
| 919 | + | Path((owner, repo, id)): Path<(String, String, String)>, | |
| 920 | + | ) -> Result<Markup, Response> { | |
| 921 | + | let (path, _) = resolve_repo(&app, user.as_ref(), &owner, &repo).await?; | |
| 922 | + | let detail = browse::commit_detail(&path, &id).map_err(server_error)?; | |
| 923 | + | Ok(layout( | |
| 924 | + | &format!("{owner}/{repo}: {}", detail.info.short), | |
| 925 | + | user.as_ref(), | |
| 926 | + | html! { | |
| 927 | + | h1 { a href=(format!("/{owner}/{repo}")) { (owner) "/" (repo) } " · " span.sha { (detail.info.short) } } | |
| 928 | + | p { (detail.info.summary) } | |
| 929 | + | p.muted { | |
| 930 | + | (detail.info.author) " · " (fmt_time(detail.info.time)) " · " | |
| 931 | + | span.sha { (detail.info.id) } | |
| 932 | + | @if let Some(parent) = &detail.parent { | |
| 933 | + | " · parent " a.sha href=(format!("/{owner}/{repo}/commit/{parent}")) { (&parent[..parent.len().min(8)]) } | |
| 934 | + | } | |
| 935 | + | } | |
| 936 | + | @if detail.changes.is_empty() { | |
| 937 | + | p.muted { "No file changes." } | |
| 938 | + | } | |
| 939 | + | @for change in &detail.changes { | |
| 940 | + | (render_file_diff(change)) | |
| 941 | + | } | |
| 942 | + | }, | |
| 943 | + | )) | |
| 944 | + | } | |
| 945 | + | ||
| 946 | + | /// Render one file's diff (added/deleted/modified) as a unified line diff. | |
| 947 | + | fn render_file_diff(change: &FileChange) -> Markup { | |
| 948 | + | let (badge_cls, badge) = match change.kind { | |
| 949 | + | ChangeKind::Added => ("add", "added"), | |
| 950 | + | ChangeKind::Deleted => ("del", "deleted"), | |
| 951 | + | ChangeKind::Modified => ("mod", "modified"), | |
| 952 | + | }; | |
| 953 | + | let binary = change.old.as_deref().is_some_and(is_binary) | |
| 954 | + | || change.new.as_deref().is_some_and(is_binary); | |
| 955 | + | html! { | |
| 956 | + | div.file-diff { | |
| 957 | + | div.head { | |
| 958 | + | span class=(format!("badge {badge_cls}")) { (badge) } | |
| 959 | + | " " (change.path) | |
| 960 | + | } | |
| 961 | + | @if binary { | |
| 962 | + | div.box { div.row { span.muted { "Binary file" } } } | |
| 963 | + | } @else { | |
| 964 | + | @let old = change.old.as_deref().map(|b| String::from_utf8_lossy(b).into_owned()).unwrap_or_default(); | |
| 965 | + | @let new = change.new.as_deref().map(|b| String::from_utf8_lossy(b).into_owned()).unwrap_or_default(); | |
| 966 | + | (unified_diff(&old, &new)) | |
| 967 | + | } | |
| 968 | + | } | |
| 969 | + | } | |
| 970 | + | } | |
| 971 | + | ||
| 972 | + | fn unified_diff(old: &str, new: &str) -> Markup { | |
| 973 | + | let diff = TextDiff::from_lines(old, new); | |
| 974 | + | html! { | |
| 975 | + | table.code.diff { | |
| 976 | + | @for change in diff.iter_all_changes() { | |
| 977 | + | @let (sign, cls) = match change.tag() { | |
| 978 | + | ChangeTag::Delete => ("-", "del"), | |
| 979 | + | ChangeTag::Insert => ("+", "ins"), | |
| 980 | + | ChangeTag::Equal => (" ", ""), | |
| 981 | + | }; | |
| 982 | + | tr class=(cls) { | |
| 983 | + | td.sign { (sign) } | |
| 984 | + | td { (change.value().trim_end_matches('\n')) } | |
| 985 | + | } | |
| 986 | + | } | |
| 987 | + | } | |
| 988 | + | } | |
| 989 | + | } | |
| 990 | + | ||
| 991 | + | /// Lazily-loaded syntax set and theme (pure-Rust fancy-regex backend). | |
| 992 | + | fn highlighter() -> &'static (SyntaxSet, Theme) { | |
| 993 | + | static HL: OnceLock<(SyntaxSet, Theme)> = OnceLock::new(); | |
| 994 | + | HL.get_or_init(|| { | |
| 995 | + | let syntaxes = SyntaxSet::load_defaults_newlines(); | |
| 996 | + | let themes = ThemeSet::load_defaults(); | |
| 997 | + | let theme = themes | |
| 998 | + | .themes | |
| 999 | + | .get("InspiredGitHub") | |
| 1000 | + | .or_else(|| themes.themes.values().next()) | |
| 1001 | + | .cloned() | |
| 1002 | + | .expect("at least one default theme"); | |
| 1003 | + | (syntaxes, theme) | |
| 1004 | + | }) | |
| 1005 | + | } | |
| 1006 | + | ||
| 1007 | + | /// Syntax-highlight `text` (chosen by file extension), returning per-line HTML. | |
| 1008 | + | /// Falls back to escaped plain text for large files or on any failure. | |
| 1009 | + | fn highlight(path: &str, text: &str) -> Vec<String> { | |
| 1010 | + | if text.len() > 512 * 1024 { | |
| 1011 | + | return text.lines().map(escape).collect(); | |
| 1012 | + | } | |
| 1013 | + | let (syntaxes, theme) = highlighter(); | |
| 1014 | + | let syntax = std::path::Path::new(path) | |
| 1015 | + | .extension() | |
| 1016 | + | .and_then(|e| e.to_str()) | |
| 1017 | + | .and_then(|ext| syntaxes.find_syntax_by_extension(ext)) | |
| 1018 | + | .or_else(|| syntaxes.find_syntax_by_first_line(text.lines().next().unwrap_or(""))) | |
| 1019 | + | .unwrap_or_else(|| syntaxes.find_syntax_plain_text()); | |
| 1020 | + | ||
| 1021 | + | let mut h = HighlightLines::new(syntax, theme); | |
| 1022 | + | text.lines() | |
| 1023 | + | .map(|line| match h.highlight_line(line, syntaxes) { | |
| 1024 | + | Ok(ranges) => styled_line_to_highlighted_html(&ranges, IncludeBackground::No) | |
| 1025 | + | .unwrap_or_else(|_| escape(line)), | |
| 1026 | + | Err(_) => escape(line), | |
| 1027 | + | }) | |
| 1028 | + | .collect() | |
| 1029 | + | } | |
| 1030 | + | ||
| 1031 | + | fn escape(s: &str) -> String { | |
| 1032 | + | s.replace('&', "&") | |
| 1033 | + | .replace('<', "<") | |
| 1034 | + | .replace('>', ">") | |
| 1035 | + | } | |
| 1036 | + | ||
| 1037 | + | /// Format a Unix timestamp as `YYYY-MM-DD HH:MM UTC`. | |
| 1038 | + | fn fmt_time(secs: i64) -> String { | |
| 1039 | + | match OffsetDateTime::from_unix_timestamp(secs) { | |
| 1040 | + | Ok(t) => format!( | |
| 1041 | + | "{:04}-{:02}-{:02} {:02}:{:02} UTC", | |
| 1042 | + | t.year(), | |
| 1043 | + | u8::from(t.month()), | |
| 1044 | + | t.day(), | |
| 1045 | + | t.hour(), | |
| 1046 | + | t.minute() | |
| 1047 | + | ), | |
| 1048 | + | Err(_) => secs.to_string(), | |
| 1049 | + | } | |
| 1050 | + | } | |
| 1051 | + | ||
| 1052 | + | /// Heuristic: treat content with a NUL in the first 8 KiB as binary. | |
| 1053 | + | fn is_binary(bytes: &[u8]) -> bool { | |
| 1054 | + | bytes.iter().take(8192).any(|&b| b == 0) | |
| 1055 | + | } |
addedcrates/anvil/Cargo.toml+23 −0
| 1 | + | [package] | |
| 2 | + | name = "anvil" | |
| 3 | + | version.workspace = true | |
| 4 | + | edition.workspace = true | |
| 5 | + | license.workspace = true | |
| 6 | + | repository.workspace = true | |
| 7 | + | rust-version.workspace = true | |
| 8 | + | description = "anvil git forge daemon and admin CLI." | |
| 9 | + | default-run = "anvild" | |
| 10 | + | ||
| 11 | + | [[bin]] | |
| 12 | + | name = "anvild" | |
| 13 | + | path = "src/main.rs" | |
| 14 | + | ||
| 15 | + | [dependencies] | |
| 16 | + | anvil-core.workspace = true | |
| 17 | + | anvil-web.workspace = true | |
| 18 | + | anvil-ssh.workspace = true | |
| 19 | + | tokio.workspace = true | |
| 20 | + | clap.workspace = true | |
| 21 | + | anyhow.workspace = true | |
| 22 | + | tracing.workspace = true | |
| 23 | + | tracing-subscriber.workspace = true |
addedcrates/anvil/src/main.rs+214 −0
| 1 | + | //! `anvild` — the anvil git forge daemon and admin CLI. | |
| 2 | + | ||
| 3 | + | use anvil_core::{ | |
| 4 | + | App, | |
| 5 | + | Config, | |
| 6 | + | repos, | |
| 7 | + | ssh_keys, | |
| 8 | + | users, | |
| 9 | + | }; | |
| 10 | + | use anyhow::{ | |
| 11 | + | Context, | |
| 12 | + | Result, | |
| 13 | + | }; | |
| 14 | + | use clap::{ | |
| 15 | + | Parser, | |
| 16 | + | Subcommand, | |
| 17 | + | }; | |
| 18 | + | ||
| 19 | + | #[derive(Parser)] | |
| 20 | + | #[command(name = "anvild", version, about = "anvil git forge")] | |
| 21 | + | struct Cli { | |
| 22 | + | /// Path to the configuration file (TOML). Defaults are used if absent. | |
| 23 | + | #[arg(long, short, default_value = "anvil.toml", global = true)] | |
| 24 | + | config: String, | |
| 25 | + | ||
| 26 | + | /// Override the data directory from config. | |
| 27 | + | #[arg(long, global = true)] | |
| 28 | + | data_dir: Option<String>, | |
| 29 | + | ||
| 30 | + | #[command(subcommand)] | |
| 31 | + | command: Option<Command>, | |
| 32 | + | } | |
| 33 | + | ||
| 34 | + | #[derive(Subcommand)] | |
| 35 | + | enum Command { | |
| 36 | + | /// Run the server (default). | |
| 37 | + | Serve, | |
| 38 | + | /// Apply database migrations and exit. | |
| 39 | + | Migrate, | |
| 40 | + | /// Manage users. | |
| 41 | + | User { | |
| 42 | + | #[command(subcommand)] | |
| 43 | + | command: UserCommand, | |
| 44 | + | }, | |
| 45 | + | /// Manage repositories. | |
| 46 | + | Repo { | |
| 47 | + | #[command(subcommand)] | |
| 48 | + | command: RepoCommand, | |
| 49 | + | }, | |
| 50 | + | } | |
| 51 | + | ||
| 52 | + | #[derive(Subcommand)] | |
| 53 | + | enum UserCommand { | |
| 54 | + | /// Create a new user. | |
| 55 | + | Create { | |
| 56 | + | username: String, | |
| 57 | + | #[arg(long, default_value = "")] | |
| 58 | + | email: String, | |
| 59 | + | #[arg(long)] | |
| 60 | + | password: String, | |
| 61 | + | #[arg(long)] | |
| 62 | + | admin: bool, | |
| 63 | + | }, | |
| 64 | + | /// Register an SSH public key for a user (for git-over-SSH access). | |
| 65 | + | AddKey { | |
| 66 | + | username: String, | |
| 67 | + | /// The OpenSSH public key line. Mutually exclusive with --key-file. | |
| 68 | + | #[arg(long)] | |
| 69 | + | key: Option<String>, | |
| 70 | + | /// Path to a `.pub` file (e.g. ~/.ssh/id_ed25519.pub). | |
| 71 | + | #[arg(long)] | |
| 72 | + | key_file: Option<String>, | |
| 73 | + | #[arg(long, default_value = "")] | |
| 74 | + | title: String, | |
| 75 | + | }, | |
| 76 | + | } | |
| 77 | + | ||
| 78 | + | #[derive(Subcommand)] | |
| 79 | + | enum RepoCommand { | |
| 80 | + | /// Create a repository, given as `owner/name`. | |
| 81 | + | Create { | |
| 82 | + | /// Repository in `owner/name` form. | |
| 83 | + | path: String, | |
| 84 | + | #[arg(long, default_value = "")] | |
| 85 | + | description: String, | |
| 86 | + | #[arg(long)] | |
| 87 | + | private: bool, | |
| 88 | + | }, | |
| 89 | + | } | |
| 90 | + | ||
| 91 | + | #[tokio::main] | |
| 92 | + | async fn main() -> Result<()> { | |
| 93 | + | tracing_subscriber::fmt() | |
| 94 | + | .with_env_filter( | |
| 95 | + | tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()), | |
| 96 | + | ) | |
| 97 | + | .init(); | |
| 98 | + | ||
| 99 | + | let cli = Cli::parse(); | |
| 100 | + | ||
| 101 | + | let mut config = Config::load_or_default(&cli.config) | |
| 102 | + | .with_context(|| format!("loading config from {}", cli.config))?; | |
| 103 | + | if let Some(dir) = &cli.data_dir { | |
| 104 | + | config.data_dir = dir.into(); | |
| 105 | + | } | |
| 106 | + | ||
| 107 | + | match cli.command.unwrap_or(Command::Serve) { | |
| 108 | + | Command::Serve => serve(config).await, | |
| 109 | + | Command::Migrate => migrate(config).await, | |
| 110 | + | Command::User { command } => user(config, command).await, | |
| 111 | + | Command::Repo { command } => repo(config, command).await, | |
| 112 | + | } | |
| 113 | + | } | |
| 114 | + | ||
| 115 | + | async fn serve(config: Config) -> Result<()> { | |
| 116 | + | let app = App::bootstrap(config).await?; | |
| 117 | + | if app.config.ssh.enabled { | |
| 118 | + | // Run the HTTP and SSH servers concurrently; if either exits, stop. | |
| 119 | + | tokio::try_join!(anvil_web::serve(app.clone()), anvil_ssh::serve(app))?; | |
| 120 | + | } else { | |
| 121 | + | anvil_web::serve(app).await?; | |
| 122 | + | } | |
| 123 | + | Ok(()) | |
| 124 | + | } | |
| 125 | + | ||
| 126 | + | async fn migrate(config: Config) -> Result<()> { | |
| 127 | + | App::bootstrap(config).await?; | |
| 128 | + | println!("migrations applied"); | |
| 129 | + | Ok(()) | |
| 130 | + | } | |
| 131 | + | ||
| 132 | + | async fn user(config: Config, command: UserCommand) -> Result<()> { | |
| 133 | + | let app = App::bootstrap(config).await?; | |
| 134 | + | match command { | |
| 135 | + | UserCommand::Create { | |
| 136 | + | username, | |
| 137 | + | email, | |
| 138 | + | password, | |
| 139 | + | admin, | |
| 140 | + | } => { | |
| 141 | + | let user = users::create(&app.db, &username, &email, &password, admin).await?; | |
| 142 | + | println!( | |
| 143 | + | "created user {} (id {}){}", | |
| 144 | + | user.username, | |
| 145 | + | user.id, | |
| 146 | + | if user.is_admin { " [admin]" } else { "" } | |
| 147 | + | ); | |
| 148 | + | } | |
| 149 | + | UserCommand::AddKey { | |
| 150 | + | username, | |
| 151 | + | key, | |
| 152 | + | key_file, | |
| 153 | + | title, | |
| 154 | + | } => { | |
| 155 | + | let user = users::find_by_username(&app.db, &username) | |
| 156 | + | .await? | |
| 157 | + | .with_context(|| format!("no such user: {username}"))?; | |
| 158 | + | let openssh = match (key, key_file) { | |
| 159 | + | (Some(k), None) => k, | |
| 160 | + | (None, Some(path)) => std::fs::read_to_string(&path) | |
| 161 | + | .with_context(|| format!("reading key file {path}"))?, | |
| 162 | + | (Some(_), Some(_)) => anyhow::bail!("pass only one of --key / --key-file"), | |
| 163 | + | (None, None) => anyhow::bail!("pass --key or --key-file"), | |
| 164 | + | }; | |
| 165 | + | let (fingerprint, content) = ssh_keys::parse_public_key(&openssh)?; | |
| 166 | + | let saved = ssh_keys::add(&app.db, user.id, &title, &fingerprint, &content).await?; | |
| 167 | + | println!( | |
| 168 | + | "added ssh key for {} ({})", | |
| 169 | + | user.username, saved.fingerprint | |
| 170 | + | ); | |
| 171 | + | } | |
| 172 | + | } | |
| 173 | + | Ok(()) | |
| 174 | + | } | |
| 175 | + | ||
| 176 | + | async fn repo(config: Config, command: RepoCommand) -> Result<()> { | |
| 177 | + | let app = App::bootstrap(config).await?; | |
| 178 | + | match command { | |
| 179 | + | RepoCommand::Create { | |
| 180 | + | path, | |
| 181 | + | description, | |
| 182 | + | private, | |
| 183 | + | } => { | |
| 184 | + | let (owner_name, name) = path | |
| 185 | + | .split_once('/') | |
| 186 | + | .context("repository path must be in `owner/name` form")?; | |
| 187 | + | let owner = users::find_by_username(&app.db, owner_name) | |
| 188 | + | .await? | |
| 189 | + | .with_context(|| format!("no such user: {owner_name}"))?; | |
| 190 | + | let repo = repos::create( | |
| 191 | + | &app.db, | |
| 192 | + | &app.config.repositories_dir(), | |
| 193 | + | &owner, | |
| 194 | + | name, | |
| 195 | + | &description, | |
| 196 | + | private, | |
| 197 | + | ) | |
| 198 | + | .await?; | |
| 199 | + | println!( | |
| 200 | + | "created repository {}/{} (id {}) at {}", | |
| 201 | + | owner.username, | |
| 202 | + | repo.name, | |
| 203 | + | repo.id, | |
| 204 | + | anvil_core::storage::repo_path( | |
| 205 | + | &app.config.repositories_dir(), | |
| 206 | + | &owner.username, | |
| 207 | + | name | |
| 208 | + | ) | |
| 209 | + | .display() | |
| 210 | + | ); | |
| 211 | + | } | |
| 212 | + | } | |
| 213 | + | Ok(()) | |
| 214 | + | } |
addedrustfmt.toml+7 −0
| 1 | + | # https://github.com/rust-lang/rustfmt/blob/master/Configurations.md | |
| 2 | + | ||
| 3 | + | # using nightly rustfmt options | |
| 4 | + | # use: cargo +nightly fmt | |
| 5 | + | ||
| 6 | + | group_imports = "StdExternalCrate" | |
| 7 | + | imports_layout = "Vertical" |
addedvendor/gitserver-core/Cargo.toml+25 −0
| 1 | + | [package] | |
| 2 | + | name = "gitserver-core" | |
| 3 | + | description = "Core Git protocol operations (vendored into anvil from WJQSERVER/gitserver, bumped to gix 0.84)." | |
| 4 | + | version = "0.0.3" | |
| 5 | + | edition = "2024" | |
| 6 | + | license = "MPL-2.0" | |
| 7 | + | repository = "https://github.com/WJQSERVER/gitserver" | |
| 8 | + | ||
| 9 | + | [dependencies] | |
| 10 | + | # gix / gix-pack come from the anvil workspace (bumped to 0.84 / 0.71 here). | |
| 11 | + | gix = { workspace = true } | |
| 12 | + | gix-pack = { workspace = true } | |
| 13 | + | tokio = { workspace = true } | |
| 14 | + | tracing = { workspace = true } | |
| 15 | + | serde = { workspace = true } | |
| 16 | + | thiserror = { workspace = true } | |
| 17 | + | miniz_oxide = "0.8" | |
| 18 | + | sha1 = "0.10" | |
| 19 | + | tokio-stream = "0.1" | |
| 20 | + | tokio-util = { version = "0.7", features = ["io", "io-util"] } | |
| 21 | + | bytes = "1" | |
| 22 | + | ||
| 23 | + | [dev-dependencies] | |
| 24 | + | tempfile = "3" | |
| 25 | + | tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } |
addedvendor/gitserver-core/LICENSE+373 −0
| 1 | + | Mozilla Public License Version 2.0 | |
| 2 | + | ================================== | |
| 3 | + | ||
| 4 | + | 1. Definitions | |
| 5 | + | -------------- | |
| 6 | + | ||
| 7 | + | 1.1. "Contributor" | |
| 8 | + | means each individual or legal entity that creates, contributes to | |
| 9 | + | the creation of, or owns Covered Software. | |
| 10 | + | ||
| 11 | + | 1.2. "Contributor Version" | |
| 12 | + | means the combination of the Contributions of others (if any) used | |
| 13 | + | by a Contributor and that particular Contributor's Contribution. | |
| 14 | + | ||
| 15 | + | 1.3. "Contribution" | |
| 16 | + | means Covered Software of a particular Contributor. | |
| 17 | + | ||
| 18 | + | 1.4. "Covered Software" | |
| 19 | + | means Source Code Form to which the initial Contributor has attached | |
| 20 | + | the notice in Exhibit A, the Executable Form of such Source Code | |
| 21 | + | Form, and Modifications of such Source Code Form, in each case | |
| 22 | + | including portions thereof. | |
| 23 | + | ||
| 24 | + | 1.5. "Incompatible With Secondary Licenses" | |
| 25 | + | means | |
| 26 | + | ||
| 27 | + | (a) that the initial Contributor has attached the notice described | |
| 28 | + | in Exhibit B to the Covered Software; or | |
| 29 | + | ||
| 30 | + | (b) that the Covered Software was made available under the terms of | |
| 31 | + | version 1.1 or earlier of the License, but not also under the | |
| 32 | + | terms of a Secondary License. | |
| 33 | + | ||
| 34 | + | 1.6. "Executable Form" | |
| 35 | + | means any form of the work other than Source Code Form. | |
| 36 | + | ||
| 37 | + | 1.7. "Larger Work" | |
| 38 | + | means a work that combines Covered Software with other material, in | |
| 39 | + | a separate file or files, that is not Covered Software. | |
| 40 | + | ||
| 41 | + | 1.8. "License" | |
| 42 | + | means this document. | |
| 43 | + | ||
| 44 | + | 1.9. "Licensable" | |
| 45 | + | means having the right to grant, to the maximum extent possible, | |
| 46 | + | whether at the time of the initial grant or subsequently, any and | |
| 47 | + | all of the rights conveyed by this License. | |
| 48 | + | ||
| 49 | + | 1.10. "Modifications" | |
| 50 | + | means any of the following: | |
| 51 | + | ||
| 52 | + | (a) any file in Source Code Form that results from an addition to, | |
| 53 | + | deletion from, or modification of the contents of Covered | |
| 54 | + | Software; or | |
| 55 | + | ||
| 56 | + | (b) any new file in Source Code Form that contains any Covered | |
| 57 | + | Software. | |
| 58 | + | ||
| 59 | + | 1.11. "Patent Claims" of a Contributor | |
| 60 | + | means any patent claim(s), including without limitation, method, | |
| 61 | + | process, and apparatus claims, in any patent Licensable by such | |
| 62 | + | Contributor that would be infringed, but for the grant of the | |
| 63 | + | License, by the making, using, selling, offering for sale, having | |
| 64 | + | made, import, or transfer of either its Contributions or its | |
| 65 | + | Contributor Version. | |
| 66 | + | ||
| 67 | + | 1.12. "Secondary License" | |
| 68 | + | means either the GNU General Public License, Version 2.0, the GNU | |
| 69 | + | Lesser General Public License, Version 2.1, the GNU Affero General | |
| 70 | + | Public License, Version 3.0, or any later versions of those | |
| 71 | + | licenses. | |
| 72 | + | ||
| 73 | + | 1.13. "Source Code Form" | |
| 74 | + | means the form of the work preferred for making modifications. | |
| 75 | + | ||
| 76 | + | 1.14. "You" (or "Your") | |
| 77 | + | means an individual or a legal entity exercising rights under this | |
| 78 | + | License. For legal entities, "You" includes any entity that | |
| 79 | + | controls, is controlled by, or is under common control with You. For | |
| 80 | + | purposes of this definition, "control" means (a) the power, direct | |
| 81 | + | or indirect, to cause the direction or management of such entity, | |
| 82 | + | whether by contract or otherwise, or (b) ownership of more than | |
| 83 | + | fifty percent (50%) of the outstanding shares or beneficial | |
| 84 | + | ownership of such entity. | |
| 85 | + | ||
| 86 | + | 2. License Grants and Conditions | |
| 87 | + | -------------------------------- | |
| 88 | + | ||
| 89 | + | 2.1. Grants | |
| 90 | + | ||
| 91 | + | Each Contributor hereby grants You a world-wide, royalty-free, | |
| 92 | + | non-exclusive license: | |
| 93 | + | ||
| 94 | + | (a) under intellectual property rights (other than patent or trademark) | |
| 95 | + | Licensable by such Contributor to use, reproduce, make available, | |
| 96 | + | modify, display, perform, distribute, and otherwise exploit its | |
| 97 | + | Contributions, either on an unmodified basis, with Modifications, or | |
| 98 | + | as part of a Larger Work; and | |
| 99 | + | ||
| 100 | + | (b) under Patent Claims of such Contributor to make, use, sell, offer | |
| 101 | + | for sale, have made, import, and otherwise transfer either its | |
| 102 | + | Contributions or its Contributor Version. | |
| 103 | + | ||
| 104 | + | 2.2. Effective Date | |
| 105 | + | ||
| 106 | + | The licenses granted in Section 2.1 with respect to any Contribution | |
| 107 | + | become effective for each Contribution on the date the Contributor first | |
| 108 | + | distributes such Contribution. | |
| 109 | + | ||
| 110 | + | 2.3. Limitations on Grant Scope | |
| 111 | + | ||
| 112 | + | The licenses granted in this Section 2 are the only rights granted under | |
| 113 | + | this License. No additional rights or licenses will be implied from the | |
| 114 | + | distribution or licensing of Covered Software under this License. | |
| 115 | + | Notwithstanding Section 2.1(b) above, no patent license is granted by a | |
| 116 | + | Contributor: | |
| 117 | + | ||
| 118 | + | (a) for any code that a Contributor has removed from Covered Software; | |
| 119 | + | or | |
| 120 | + | ||
| 121 | + | (b) for infringements caused by: (i) Your and any other third party's | |
| 122 | + | modifications of Covered Software, or (ii) the combination of its | |
| 123 | + | Contributions with other software (except as part of its Contributor | |
| 124 | + | Version); or | |
| 125 | + | ||
| 126 | + | (c) under Patent Claims infringed by Covered Software in the absence of | |
| 127 | + | its Contributions. | |
| 128 | + | ||
| 129 | + | This License does not grant any rights in the trademarks, service marks, | |
| 130 | + | or logos of any Contributor (except as may be necessary to comply with | |
| 131 | + | the notice requirements in Section 3.4). | |
| 132 | + | ||
| 133 | + | 2.4. Subsequent Licenses | |
| 134 | + | ||
| 135 | + | No Contributor makes additional grants as a result of Your choice to | |
| 136 | + | distribute the Covered Software under a subsequent version of this | |
| 137 | + | License (see Section 10.2) or under the terms of a Secondary License (if | |
| 138 | + | permitted under the terms of Section 3.3). | |
| 139 | + | ||
| 140 | + | 2.5. Representation | |
| 141 | + | ||
| 142 | + | Each Contributor represents that the Contributor believes its | |
| 143 | + | Contributions are its original creation(s) or it has sufficient rights | |
| 144 | + | to grant the rights to its Contributions conveyed by this License. | |
| 145 | + | ||
| 146 | + | 2.6. Fair Use | |
| 147 | + | ||
| 148 | + | This License is not intended to limit any rights You have under | |
| 149 | + | applicable copyright doctrines of fair use, fair dealing, or other | |
| 150 | + | equivalents. | |
| 151 | + | ||
| 152 | + | 2.7. Conditions | |
| 153 | + | ||
| 154 | + | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted | |
| 155 | + | in Section 2.1. | |
| 156 | + | ||
| 157 | + | 3. Responsibilities | |
| 158 | + | ------------------- | |
| 159 | + | ||
| 160 | + | 3.1. Distribution of Source Form | |
| 161 | + | ||
| 162 | + | All distribution of Covered Software in Source Code Form, including any | |
| 163 | + | Modifications that You create or to which You contribute, must be under | |
| 164 | + | the terms of this License. You must inform recipients that the Source | |
| 165 | + | Code Form of the Covered Software is governed by the terms of this | |
| 166 | + | License, and how they can obtain a copy of this License. You may not | |
| 167 | + | attempt to alter or restrict the recipients' rights in the Source Code | |
| 168 | + | Form. | |
| 169 | + | ||
| 170 | + | 3.2. Distribution of Executable Form | |
| 171 | + | ||
| 172 | + | If You distribute Covered Software in Executable Form then: | |
| 173 | + | ||
| 174 | + | (a) such Covered Software must also be made available in Source Code | |
| 175 | + | Form, as described in Section 3.1, and You must inform recipients of | |
| 176 | + | the Executable Form how they can obtain a copy of such Source Code | |
| 177 | + | Form by reasonable means in a timely manner, at a charge no more | |
| 178 | + | than the cost of distribution to the recipient; and | |
| 179 | + | ||
| 180 | + | (b) You may distribute such Executable Form under the terms of this | |
| 181 | + | License, or sublicense it under different terms, provided that the | |
| 182 | + | license for the Executable Form does not attempt to limit or alter | |
| 183 | + | the recipients' rights in the Source Code Form under this License. | |
| 184 | + | ||
| 185 | + | 3.3. Distribution of a Larger Work | |
| 186 | + | ||
| 187 | + | You may create and distribute a Larger Work under terms of Your choice, | |
| 188 | + | provided that You also comply with the requirements of this License for | |
| 189 | + | the Covered Software. If the Larger Work is a combination of Covered | |
| 190 | + | Software with a work governed by one or more Secondary Licenses, and the | |
| 191 | + | Covered Software is not Incompatible With Secondary Licenses, this | |
| 192 | + | License permits You to additionally distribute such Covered Software | |
| 193 | + | under the terms of such Secondary License(s), so that the recipient of | |
| 194 | + | the Larger Work may, at their option, further distribute the Covered | |
| 195 | + | Software under the terms of either this License or such Secondary | |
| 196 | + | License(s). | |
| 197 | + | ||
| 198 | + | 3.4. Notices | |
| 199 | + | ||
| 200 | + | You may not remove or alter the substance of any license notices | |
| 201 | + | (including copyright notices, patent notices, disclaimers of warranty, | |
| 202 | + | or limitations of liability) contained within the Source Code Form of | |
| 203 | + | the Covered Software, except that You may alter any license notices to | |
| 204 | + | the extent required to remedy known factual inaccuracies. | |
| 205 | + | ||
| 206 | + | 3.5. Application of Additional Terms | |
| 207 | + | ||
| 208 | + | You may choose to offer, and to charge a fee for, warranty, support, | |
| 209 | + | indemnity or liability obligations to one or more recipients of Covered | |
| 210 | + | Software. However, You may do so only on Your own behalf, and not on | |
| 211 | + | behalf of any Contributor. You must make it absolutely clear that any | |
| 212 | + | such warranty, support, indemnity, or liability obligation is offered by | |
| 213 | + | You alone, and You hereby agree to indemnify every Contributor for any | |
| 214 | + | liability incurred by such Contributor as a result of warranty, support, | |
| 215 | + | indemnity or liability terms You offer. You may include additional | |
| 216 | + | disclaimers of warranty and limitations of liability specific to any | |
| 217 | + | jurisdiction. | |
| 218 | + | ||
| 219 | + | 4. Inability to Comply Due to Statute or Regulation | |
| 220 | + | --------------------------------------------------- | |
| 221 | + | ||
| 222 | + | If it is impossible for You to comply with any of the terms of this | |
| 223 | + | License with respect to some or all of the Covered Software due to | |
| 224 | + | statute, judicial order, or regulation then You must: (a) comply with | |
| 225 | + | the terms of this License to the maximum extent possible; and (b) | |
| 226 | + | describe the limitations and the code they affect. Such description must | |
| 227 | + | be placed in a text file included with all distributions of the Covered | |
| 228 | + | Software under this License. Except to the extent prohibited by statute | |
| 229 | + | or regulation, such description must be sufficiently detailed for a | |
| 230 | + | recipient of ordinary skill to be able to understand it. | |
| 231 | + | ||
| 232 | + | 5. Termination | |
| 233 | + | -------------- | |
| 234 | + | ||
| 235 | + | 5.1. The rights granted under this License will terminate automatically | |
| 236 | + | if You fail to comply with any of its terms. However, if You become | |
| 237 | + | compliant, then the rights granted under this License from a particular | |
| 238 | + | Contributor are reinstated (a) provisionally, unless and until such | |
| 239 | + | Contributor explicitly and finally terminates Your grants, and (b) on an | |
| 240 | + | ongoing basis, if such Contributor fails to notify You of the | |
| 241 | + | non-compliance by some reasonable means prior to 60 days after You have | |
| 242 | + | come back into compliance. Moreover, Your grants from a particular | |
| 243 | + | Contributor are reinstated on an ongoing basis if such Contributor | |
| 244 | + | notifies You of the non-compliance by some reasonable means, this is the | |
| 245 | + | first time You have received notice of non-compliance with this License | |
| 246 | + | from such Contributor, and You become compliant prior to 30 days after | |
| 247 | + | Your receipt of the notice. | |
| 248 | + | ||
| 249 | + | 5.2. If You initiate litigation against any entity by asserting a patent | |
| 250 | + | infringement claim (excluding declaratory judgment actions, | |
| 251 | + | counter-claims, and cross-claims) alleging that a Contributor Version | |
| 252 | + | directly or indirectly infringes any patent, then the rights granted to | |
| 253 | + | You by any and all Contributors for the Covered Software under Section | |
| 254 | + | 2.1 of this License shall terminate. | |
| 255 | + | ||
| 256 | + | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all | |
| 257 | + | end user license agreements (excluding distributors and resellers) which | |
| 258 | + | have been validly granted by You or Your distributors under this License | |
| 259 | + | prior to termination shall survive termination. | |
| 260 | + | ||
| 261 | + | ************************************************************************ | |
| 262 | + | * * | |
| 263 | + | * 6. Disclaimer of Warranty * | |
| 264 | + | * ------------------------- * | |
| 265 | + | * * | |
| 266 | + | * Covered Software is provided under this License on an "as is" * | |
| 267 | + | * basis, without warranty of any kind, either expressed, implied, or * | |
| 268 | + | * statutory, including, without limitation, warranties that the * | |
| 269 | + | * Covered Software is free of defects, merchantable, fit for a * | |
| 270 | + | * particular purpose or non-infringing. The entire risk as to the * | |
| 271 | + | * quality and performance of the Covered Software is with You. * | |
| 272 | + | * Should any Covered Software prove defective in any respect, You * | |
| 273 | + | * (not any Contributor) assume the cost of any necessary servicing, * | |
| 274 | + | * repair, or correction. This disclaimer of warranty constitutes an * | |
| 275 | + | * essential part of this License. No use of any Covered Software is * | |
| 276 | + | * authorized under this License except under this disclaimer. * | |
| 277 | + | * * | |
| 278 | + | ************************************************************************ | |
| 279 | + | ||
| 280 | + | ************************************************************************ | |
| 281 | + | * * | |
| 282 | + | * 7. Limitation of Liability * | |
| 283 | + | * -------------------------- * | |
| 284 | + | * * | |
| 285 | + | * Under no circumstances and under no legal theory, whether tort * | |
| 286 | + | * (including negligence), contract, or otherwise, shall any * | |
| 287 | + | * Contributor, or anyone who distributes Covered Software as * | |
| 288 | + | * permitted above, be liable to You for any direct, indirect, * | |
| 289 | + | * special, incidental, or consequential damages of any character * | |
| 290 | + | * including, without limitation, damages for lost profits, loss of * | |
| 291 | + | * goodwill, work stoppage, computer failure or malfunction, or any * | |
| 292 | + | * and all other commercial damages or losses, even if such party * | |
| 293 | + | * shall have been informed of the possibility of such damages. This * | |
| 294 | + | * limitation of liability shall not apply to liability for death or * | |
| 295 | + | * personal injury resulting from such party's negligence to the * | |
| 296 | + | * extent applicable law prohibits such limitation. Some * | |
| 297 | + | * jurisdictions do not allow the exclusion or limitation of * | |
| 298 | + | * incidental or consequential damages, so this exclusion and * | |
| 299 | + | * limitation may not apply to You. * | |
| 300 | + | * * | |
| 301 | + | ************************************************************************ | |
| 302 | + | ||
| 303 | + | 8. Litigation | |
| 304 | + | ------------- | |
| 305 | + | ||
| 306 | + | Any litigation relating to this License may be brought only in the | |
| 307 | + | courts of a jurisdiction where the defendant maintains its principal | |
| 308 | + | place of business and such litigation shall be governed by laws of that | |
| 309 | + | jurisdiction, without reference to its conflict-of-law provisions. | |
| 310 | + | Nothing in this Section shall prevent a party's ability to bring | |
| 311 | + | cross-claims or counter-claims. | |
| 312 | + | ||
| 313 | + | 9. Miscellaneous | |
| 314 | + | ---------------- | |
| 315 | + | ||
| 316 | + | This License represents the complete agreement concerning the subject | |
| 317 | + | matter hereof. If any provision of this License is held to be | |
| 318 | + | unenforceable, such provision shall be reformed only to the extent | |
| 319 | + | necessary to make it enforceable. Any law or regulation which provides | |
| 320 | + | that the language of a contract shall be construed against the drafter | |
| 321 | + | shall not be used to construe this License against a Contributor. | |
| 322 | + | ||
| 323 | + | 10. Versions of the License | |
| 324 | + | --------------------------- | |
| 325 | + | ||
| 326 | + | 10.1. New Versions | |
| 327 | + | ||
| 328 | + | Mozilla Foundation is the license steward. Except as provided in Section | |
| 329 | + | 10.3, no one other than the license steward has the right to modify or | |
| 330 | + | publish new versions of this License. Each version will be given a | |
| 331 | + | distinguishing version number. | |
| 332 | + | ||
| 333 | + | 10.2. Effect of New Versions | |
| 334 | + | ||
| 335 | + | You may distribute the Covered Software under the terms of the version | |
| 336 | + | of the License under which You originally received the Covered Software, | |
| 337 | + | or under the terms of any subsequent version published by the license | |
| 338 | + | steward. | |
| 339 | + | ||
| 340 | + | 10.3. Modified Versions | |
| 341 | + | ||
| 342 | + | If you create software not governed by this License, and you want to | |
| 343 | + | create a new license for such software, you may create and use a | |
| 344 | + | modified version of this License if you rename the license and remove | |
| 345 | + | any references to the name of the license steward (except to note that | |
| 346 | + | such modified license differs from this License). | |
| 347 | + | ||
| 348 | + | 10.4. Distributing Source Code Form that is Incompatible With Secondary | |
| 349 | + | Licenses | |
| 350 | + | ||
| 351 | + | If You choose to distribute Source Code Form that is Incompatible With | |
| 352 | + | Secondary Licenses under the terms of this version of the License, the | |
| 353 | + | notice described in Exhibit B of this License must be attached. | |
| 354 | + | ||
| 355 | + | Exhibit A - Source Code Form License Notice | |
| 356 | + | ------------------------------------------- | |
| 357 | + | ||
| 358 | + | This Source Code Form is subject to the terms of the Mozilla Public | |
| 359 | + | License, v. 2.0. If a copy of the MPL was not distributed with this | |
| 360 | + | file, You can obtain one at https://mozilla.org/MPL/2.0/. | |
| 361 | + | ||
| 362 | + | If it is not possible or desirable to put the notice in a particular | |
| 363 | + | file, then You may include the notice in a location (such as a LICENSE | |
| 364 | + | file in a relevant directory) where a recipient would be likely to look | |
| 365 | + | for such a notice. | |
| 366 | + | ||
| 367 | + | You may add additional accurate notices of copyright ownership. | |
| 368 | + | ||
| 369 | + | Exhibit B - "Incompatible With Secondary Licenses" Notice | |
| 370 | + | --------------------------------------------------------- | |
| 371 | + | ||
| 372 | + | This Source Code Form is "Incompatible With Secondary Licenses", as | |
| 373 | + | defined by the Mozilla Public License, v. 2.0. |
addedvendor/gitserver-core/NOTICE+7 −0
| 1 | + | Copyright (c) 2026 WJQSERVER | |
| 2 | + | ||
| 3 | + | Repository licensing summary: | |
| 4 | + | - Primary repository license: MPL-2.0 (see `LICENSE`) | |
| 5 | + | - Upstream project: https://github.com/ggueret/git-server | |
| 6 | + | - Upstream MIT license text is preserved in `UPSTREAM-LICENSE` and in each crate's `license/UPSTREAM-LICENSE` | |
| 7 | + | - File-level notices and preserved upstream license files govern specific files where applicable |
addedvendor/gitserver-core/UPSTREAM-LICENSE+21 −0
| 1 | + | MIT License | |
| 2 | + | ||
| 3 | + | Copyright (c) 2026 Geoffrey Guéret | |
| 4 | + | ||
| 5 | + | Permission is hereby granted, free of charge, to any person obtaining a copy | |
| 6 | + | of this software and associated documentation files (the "Software"), to deal | |
| 7 | + | in the Software without restriction, including without limitation the rights | |
| 8 | + | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| 9 | + | copies of the Software, and to permit persons to whom the Software is | |
| 10 | + | furnished to do so, subject to the following conditions: | |
| 11 | + | ||
| 12 | + | The above copyright notice and this permission notice shall be included in all | |
| 13 | + | copies or substantial portions of the Software. | |
| 14 | + | ||
| 15 | + | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| 16 | + | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 17 | + | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| 18 | + | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 19 | + | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| 20 | + | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
| 21 | + | SOFTWARE. |
addedvendor/gitserver-core/VENDOR.md+27 −0
| 1 | + | # Vendored: gitserver-core | |
| 2 | + | ||
| 3 | + | This is a vendored, modified copy of the `gitserver-core` crate from | |
| 4 | + | [WJQSERVER/gitserver](https://github.com/WJQSERVER/gitserver), which implements | |
| 5 | + | the server side of the git wire protocol (pkt-line, protocol v2 `ls-refs` / | |
| 6 | + | `fetch` / `upload-pack`, and `receive-pack`) on top of `gix` — the piece gix | |
| 7 | + | itself does not provide. | |
| 8 | + | ||
| 9 | + | - **Upstream commit:** `3cec05b02eea3220c67782e4d9d0404d8b001aa3` (2026-04-27) | |
| 10 | + | - **License:** MPL-2.0 (see `LICENSE`); upstream MIT material noted in | |
| 11 | + | `UPSTREAM-LICENSE` / `NOTICE`. MPL is file-level copyleft: modifications to | |
| 12 | + | these files stay MPL, but anvil as a whole may carry its own license. | |
| 13 | + | ||
| 14 | + | ## Why vendored | |
| 15 | + | ||
| 16 | + | We vendor rather than depend on crates.io so we can (1) bump `gix` from the | |
| 17 | + | upstream-pinned 0.80 to the workspace version (0.84) and keep it in lockstep | |
| 18 | + | with the rest of anvil, and (2) modify the protocol code as `anvil-git` evolves. | |
| 19 | + | ||
| 20 | + | `anvil-git` wraps this crate behind its own interface so the dependency can be | |
| 21 | + | incrementally replaced with an upstream-shaped, gix-native implementation. | |
| 22 | + | ||
| 23 | + | ## Local modifications | |
| 24 | + | ||
| 25 | + | - `Cargo.toml`: standalone manifest using the anvil workspace's `gix` (0.84) and | |
| 26 | + | `gix-pack` (0.71) instead of upstream's pinned 0.80 / 0.67. | |
| 27 | + | - Source changes required by the gix version bump are recorded in git history. |
addedvendor/gitserver-core/src/backend.rs+251 −0
| 1 | + | use std::path::PathBuf; | |
| 2 | + | use std::pin::Pin; | |
| 3 | + | use std::sync::Arc; | |
| 4 | + | use std::sync::atomic::{ | |
| 5 | + | AtomicBool, | |
| 6 | + | Ordering, | |
| 7 | + | }; | |
| 8 | + | use std::task::{ | |
| 9 | + | Context, | |
| 10 | + | Poll, | |
| 11 | + | }; | |
| 12 | + | ||
| 13 | + | use tokio::io::AsyncRead; | |
| 14 | + | use tokio::time::{ | |
| 15 | + | Duration, | |
| 16 | + | Sleep, | |
| 17 | + | sleep, | |
| 18 | + | }; | |
| 19 | + | use tokio_util::io::SyncIoBridge; | |
| 20 | + | ||
| 21 | + | use crate::error::Result; | |
| 22 | + | use crate::pack::UploadPackRequest; | |
| 23 | + | ||
| 24 | + | pub const RECEIVE_PACK_TIMEOUT: Duration = Duration::from_secs(300); | |
| 25 | + | const RECEIVE_PACK_IDLE_TIMEOUT: Duration = Duration::from_secs(30); | |
| 26 | + | ||
| 27 | + | struct TimedAsyncRead<R> { | |
| 28 | + | inner: R, | |
| 29 | + | timeout: Duration, | |
| 30 | + | sleep: Option<Pin<Box<Sleep>>>, | |
| 31 | + | interrupt: Arc<AtomicBool>, | |
| 32 | + | } | |
| 33 | + | ||
| 34 | + | impl<R> TimedAsyncRead<R> { | |
| 35 | + | fn new(inner: R, timeout: Duration, interrupt: Arc<AtomicBool>) -> Self { | |
| 36 | + | Self { | |
| 37 | + | inner, | |
| 38 | + | timeout, | |
| 39 | + | sleep: None, | |
| 40 | + | interrupt, | |
| 41 | + | } | |
| 42 | + | } | |
| 43 | + | } | |
| 44 | + | ||
| 45 | + | impl<R> AsyncRead for TimedAsyncRead<R> | |
| 46 | + | where | |
| 47 | + | R: AsyncRead + Unpin, | |
| 48 | + | { | |
| 49 | + | fn poll_read( | |
| 50 | + | mut self: Pin<&mut Self>, | |
| 51 | + | cx: &mut Context<'_>, | |
| 52 | + | buf: &mut tokio::io::ReadBuf<'_>, | |
| 53 | + | ) -> Poll<std::io::Result<()>> { | |
| 54 | + | if self.sleep.is_none() { | |
| 55 | + | self.sleep = Some(Box::pin(sleep(self.timeout))); | |
| 56 | + | } | |
| 57 | + | ||
| 58 | + | let before = buf.filled().len(); | |
| 59 | + | match Pin::new(&mut self.inner).poll_read(cx, buf) { | |
| 60 | + | Poll::Ready(Ok(())) => { | |
| 61 | + | if buf.filled().len() > before { | |
| 62 | + | self.sleep = None; | |
| 63 | + | } | |
| 64 | + | Poll::Ready(Ok(())) | |
| 65 | + | } | |
| 66 | + | Poll::Ready(Err(err)) => Poll::Ready(Err(err)), | |
| 67 | + | Poll::Pending => { | |
| 68 | + | if self | |
| 69 | + | .sleep | |
| 70 | + | .as_mut() | |
| 71 | + | .expect("timeout sleep must exist") | |
| 72 | + | .as_mut() | |
| 73 | + | .poll(cx) | |
| 74 | + | .is_ready() | |
| 75 | + | { | |
| 76 | + | self.interrupt.store(true, Ordering::Relaxed); | |
| 77 | + | Poll::Ready(Err(std::io::Error::new( | |
| 78 | + | std::io::ErrorKind::TimedOut, | |
| 79 | + | "receive-pack read timed out", | |
| 80 | + | ))) | |
| 81 | + | } else { | |
| 82 | + | Poll::Pending | |
| 83 | + | } | |
| 84 | + | } | |
| 85 | + | } | |
| 86 | + | } | |
| 87 | + | } | |
| 88 | + | ||
| 89 | + | pub struct GitBackend { | |
| 90 | + | repo_path: PathBuf, | |
| 91 | + | } | |
| 92 | + | ||
| 93 | + | impl GitBackend { | |
| 94 | + | pub fn new(repo_path: PathBuf) -> Self { | |
| 95 | + | Self { repo_path } | |
| 96 | + | } | |
| 97 | + | ||
| 98 | + | pub fn advertise_refs(&self) -> Result<Vec<u8>> { | |
| 99 | + | crate::refs::advertise_refs(&self.repo_path) | |
| 100 | + | } | |
| 101 | + | ||
| 102 | + | pub fn advertise_receive_refs(&self) -> Result<Vec<u8>> { | |
| 103 | + | crate::receive_pack::advertise_receive_refs(&self.repo_path) | |
| 104 | + | } | |
| 105 | + | ||
| 106 | + | pub async fn upload_pack(&self, request: &UploadPackRequest) -> Result<impl AsyncRead + use<>> { | |
| 107 | + | crate::pack::generate_pack(&self.repo_path, request) | |
| 108 | + | } | |
| 109 | + | ||
| 110 | + | pub async fn receive_pack<R>(&self, request: R) -> Result<Vec<u8>> | |
| 111 | + | where | |
| 112 | + | R: AsyncRead + Unpin + Send + 'static, | |
| 113 | + | { | |
| 114 | + | self.receive_pack_with_timeout(request, RECEIVE_PACK_TIMEOUT) | |
| 115 | + | .await | |
| 116 | + | } | |
| 117 | + | ||
| 118 | + | async fn receive_pack_with_timeout<R>( | |
| 119 | + | &self, | |
| 120 | + | request: R, | |
| 121 | + | timeout_duration: Duration, | |
| 122 | + | ) -> Result<Vec<u8>> | |
| 123 | + | where | |
| 124 | + | R: AsyncRead + Unpin + Send + 'static, | |
| 125 | + | { | |
| 126 | + | let repo_path = self.repo_path.clone(); | |
| 127 | + | let interrupt = Arc::new(AtomicBool::new(false)); | |
| 128 | + | let watchdog_interrupt = interrupt.clone(); | |
| 129 | + | let watchdog = tokio::spawn(async move { | |
| 130 | + | sleep(timeout_duration).await; | |
| 131 | + | watchdog_interrupt.store(true, Ordering::Relaxed); | |
| 132 | + | }); | |
| 133 | + | ||
| 134 | + | let join = tokio::task::spawn_blocking(move || { | |
| 135 | + | let request = | |
| 136 | + | TimedAsyncRead::new(request, RECEIVE_PACK_IDLE_TIMEOUT, interrupt.clone()); | |
| 137 | + | let mut request = SyncIoBridge::new(request); | |
| 138 | + | crate::receive_pack::receive_pack_with_interrupt( | |
| 139 | + | &repo_path, | |
| 140 | + | &mut request, | |
| 141 | + | interrupt.as_ref(), | |
| 142 | + | ) | |
| 143 | + | }) | |
| 144 | + | .await | |
| 145 | + | .map_err(|e| crate::error::Error::Protocol(format!("receive-pack task panicked: {e}"))); | |
| 146 | + | ||
| 147 | + | watchdog.abort(); | |
| 148 | + | join? | |
| 149 | + | } | |
| 150 | + | } | |
| 151 | + | ||
| 152 | + | #[cfg(test)] | |
| 153 | + | mod tests { | |
| 154 | + | use std::process::Command; | |
| 155 | + | ||
| 156 | + | use tempfile::TempDir; | |
| 157 | + | ||
| 158 | + | use super::*; | |
| 159 | + | ||
| 160 | + | fn create_repo_with_commit(root: &std::path::Path) -> PathBuf { | |
| 161 | + | let repo_path = root.join("test.git"); | |
| 162 | + | let work_dir = root.join("work"); | |
| 163 | + | std::fs::create_dir(&work_dir).unwrap(); | |
| 164 | + | Command::new("git") | |
| 165 | + | .args(["init", "--bare", repo_path.to_str().unwrap()]) | |
| 166 | + | .output() | |
| 167 | + | .unwrap(); | |
| 168 | + | Command::new("git") | |
| 169 | + | .args(["symbolic-ref", "HEAD", "refs/heads/main"]) | |
| 170 | + | .current_dir(&repo_path) | |
| 171 | + | .output() | |
| 172 | + | .unwrap(); | |
| 173 | + | Command::new("git") | |
| 174 | + | .args([ | |
| 175 | + | "clone", | |
| 176 | + | repo_path.to_str().unwrap(), | |
| 177 | + | work_dir.to_str().unwrap(), | |
| 178 | + | ]) | |
| 179 | + | .output() | |
| 180 | + | .unwrap(); | |
| 181 | + | Command::new("git") | |
| 182 | + | .current_dir(&work_dir) | |
| 183 | + | .args(["commit", "--allow-empty", "-m", "init"]) | |
| 184 | + | .env("GIT_AUTHOR_NAME", "Test") | |
| 185 | + | .env("GIT_AUTHOR_EMAIL", "t@t.com") | |
| 186 | + | .env("GIT_COMMITTER_NAME", "Test") | |
| 187 | + | .env("GIT_COMMITTER_EMAIL", "t@t.com") | |
| 188 | + | .output() | |
| 189 | + | .unwrap(); | |
| 190 | + | Command::new("git") | |
| 191 | + | .current_dir(&work_dir) | |
| 192 | + | .args(["push", "origin", "main"]) | |
| 193 | + | .output() | |
| 194 | + | .unwrap(); | |
| 195 | + | repo_path | |
| 196 | + | } | |
| 197 | + | ||
| 198 | + | #[test] | |
| 199 | + | fn backend_advertise_refs() { | |
| 200 | + | let root = TempDir::new().unwrap(); | |
| 201 | + | let repo_path = create_repo_with_commit(root.path()); | |
| 202 | + | let backend = GitBackend::new(repo_path); | |
| 203 | + | let output = backend.advertise_refs().unwrap(); | |
| 204 | + | let output_str = String::from_utf8_lossy(&output); | |
| 205 | + | assert!(output_str.contains("refs/heads/main")); | |
| 206 | + | } | |
| 207 | + | ||
| 208 | + | #[tokio::test] | |
| 209 | + | async fn backend_upload_pack() { | |
| 210 | + | let root = TempDir::new().unwrap(); | |
| 211 | + | let repo_path = create_repo_with_commit(root.path()); | |
| 212 | + | let repo = gix::open(&repo_path).unwrap(); | |
| 213 | + | let head = repo.head_id().unwrap(); | |
| 214 | + | ||
| 215 | + | let backend = GitBackend::new(repo_path); | |
| 216 | + | let request = UploadPackRequest { | |
| 217 | + | wants: vec![head.detach()], | |
| 218 | + | haves: vec![], | |
| 219 | + | done: true, | |
| 220 | + | capabilities: Default::default(), | |
| 221 | + | shallow: Default::default(), | |
| 222 | + | object_ids: None, | |
| 223 | + | }; | |
| 224 | + | let reader = backend.upload_pack(&request).await.unwrap(); | |
| 225 | + | let mut buf = Vec::new(); | |
| 226 | + | tokio::io::AsyncReadExt::read_to_end(&mut tokio::io::BufReader::new(reader), &mut buf) | |
| 227 | + | .await | |
| 228 | + | .unwrap(); | |
| 229 | + | assert!(buf.windows(4).any(|w| w == b"PACK")); | |
| 230 | + | } | |
| 231 | + | ||
| 232 | + | #[tokio::test] | |
| 233 | + | async fn backend_receive_pack_times_out_on_stalled_reader() { | |
| 234 | + | let root = TempDir::new().unwrap(); | |
| 235 | + | let repo_path = create_repo_with_commit(root.path()); | |
| 236 | + | let backend = GitBackend::new(repo_path); | |
| 237 | + | let (reader, _writer) = tokio::io::duplex(1); | |
| 238 | + | ||
| 239 | + | let err = backend | |
| 240 | + | .receive_pack_with_timeout(reader, Duration::from_millis(50)) | |
| 241 | + | .await | |
| 242 | + | .unwrap_err(); | |
| 243 | + | ||
| 244 | + | match err { | |
| 245 | + | crate::error::Error::Io(inner) => { | |
| 246 | + | assert_eq!(inner.kind(), std::io::ErrorKind::TimedOut); | |
| 247 | + | } | |
| 248 | + | other => panic!("expected timeout io error, got {other}"), | |
| 249 | + | } | |
| 250 | + | } | |
| 251 | + | } |
addedvendor/gitserver-core/src/discovery.rs+305 −0
| 1 | + | use std::fs; | |
| 2 | + | use std::path::{ | |
| 3 | + | Path, | |
| 4 | + | PathBuf, | |
| 5 | + | }; | |
| 6 | + | ||
| 7 | + | use serde::Serialize; | |
| 8 | + | ||
| 9 | + | pub use crate::dynamic_registry::{ | |
| 10 | + | DynamicRepoRegistry, | |
| 11 | + | MutableRepoRegistry, | |
| 12 | + | RepoResolver, | |
| 13 | + | }; | |
| 14 | + | use crate::error::{ | |
| 15 | + | Error, | |
| 16 | + | Result, | |
| 17 | + | }; | |
| 18 | + | ||
| 19 | + | const DEFAULT_GIT_DESCRIPTION: &str = | |
| 20 | + | "Unnamed repository; edit this file 'description' to name the repository."; | |
| 21 | + | ||
| 22 | + | /// Information about a discovered bare git repository. | |
| 23 | + | #[derive(Debug, Clone, Serialize)] | |
| 24 | + | pub struct RepoInfo { | |
| 25 | + | pub name: String, | |
| 26 | + | pub relative_path: String, | |
| 27 | + | #[serde(skip)] | |
| 28 | + | pub absolute_path: PathBuf, | |
| 29 | + | #[serde(skip_serializing_if = "Option::is_none")] | |
| 30 | + | pub description: Option<String>, | |
| 31 | + | } | |
| 32 | + | ||
| 33 | + | /// A store of discovered repositories under a root directory. | |
| 34 | + | #[derive(Debug, Clone)] | |
| 35 | + | pub struct RepoStore { | |
| 36 | + | root: PathBuf, | |
| 37 | + | max_depth: u32, | |
| 38 | + | repos: Vec<RepoInfo>, | |
| 39 | + | } | |
| 40 | + | ||
| 41 | + | impl RepoStore { | |
| 42 | + | /// Scan `root` recursively up to `max_depth` levels for bare git repositories. | |
| 43 | + | /// | |
| 44 | + | /// `max_depth = 0` means only repositories directly inside `root`. | |
| 45 | + | /// `max_depth = 3` means up to 3 levels of subdirectories below `root`. | |
| 46 | + | pub fn discover(root: PathBuf, max_depth: u32) -> Result<Self> { | |
| 47 | + | let root = root.canonicalize()?; | |
| 48 | + | let mut repos = Vec::new(); | |
| 49 | + | ||
| 50 | + | // Walk starts at depth 0 (root itself). We scan children of root at depth 1, | |
| 51 | + | // and allow descending up to max_depth subdirectory levels below root. | |
| 52 | + | walk_dir(&root, &root, 0, max_depth, &mut repos)?; | |
| 53 | + | ||
| 54 | + | repos.sort_by(|a, b| a.relative_path.cmp(&b.relative_path)); | |
| 55 | + | ||
| 56 | + | Ok(Self { | |
| 57 | + | root, | |
| 58 | + | max_depth, | |
| 59 | + | repos, | |
| 60 | + | }) | |
| 61 | + | } | |
| 62 | + | ||
| 63 | + | /// Resolve a relative path to a `RepoInfo`. | |
| 64 | + | /// | |
| 65 | + | /// Uses `crate::path::resolve_repo_path` for validation, then matches by | |
| 66 | + | /// canonical absolute path. | |
| 67 | + | pub fn resolve(&self, relative: &str) -> Result<&RepoInfo> { | |
| 68 | + | let canonical = crate::path::resolve_repo_path(&self.root, relative)?; | |
| 69 | + | self.repos | |
| 70 | + | .iter() | |
| 71 | + | .find(|r| r.absolute_path == canonical) | |
| 72 | + | .ok_or_else(|| Error::RepoNotFound(relative.to_string())) | |
| 73 | + | } | |
| 74 | + | ||
| 75 | + | /// Returns all discovered repositories, sorted by relative path. | |
| 76 | + | pub fn list(&self) -> &[RepoInfo] { | |
| 77 | + | &self.repos | |
| 78 | + | } | |
| 79 | + | ||
| 80 | + | /// Returns the root directory used for discovery. | |
| 81 | + | pub fn root(&self) -> &Path { | |
| 82 | + | &self.root | |
| 83 | + | } | |
| 84 | + | ||
| 85 | + | /// Returns the configured maximum discovery depth. | |
| 86 | + | pub fn max_depth(&self) -> u32 { | |
| 87 | + | self.max_depth | |
| 88 | + | } | |
| 89 | + | ||
| 90 | + | /// Re-run repository discovery using the original root and max depth. | |
| 91 | + | pub fn refresh(&mut self) -> Result<()> { | |
| 92 | + | let refreshed = Self::discover(self.root.clone(), self.max_depth)?; | |
| 93 | + | self.repos = refreshed.repos; | |
| 94 | + | Ok(()) | |
| 95 | + | } | |
| 96 | + | } | |
| 97 | + | ||
| 98 | + | /// Recursively walk `dir`, recording bare repositories. | |
| 99 | + | /// | |
| 100 | + | /// `depth` is the current depth relative to `root` (root itself is depth 0). | |
| 101 | + | /// Entries *inside* root are at depth 1. We descend while `depth <= max_depth`. | |
| 102 | + | fn walk_dir( | |
| 103 | + | root: &Path, | |
| 104 | + | dir: &Path, | |
| 105 | + | depth: u32, | |
| 106 | + | max_depth: u32, | |
| 107 | + | repos: &mut Vec<RepoInfo>, | |
| 108 | + | ) -> Result<()> { | |
| 109 | + | let read = match fs::read_dir(dir) { | |
| 110 | + | Ok(r) => r, | |
| 111 | + | Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => return Ok(()), | |
| 112 | + | Err(e) => return Err(Error::Io(e)), | |
| 113 | + | }; | |
| 114 | + | ||
| 115 | + | for entry in read { | |
| 116 | + | let entry = entry?; | |
| 117 | + | let path = entry.path(); | |
| 118 | + | ||
| 119 | + | let metadata = match fs::metadata(&path) { | |
| 120 | + | Ok(m) => m, | |
| 121 | + | Err(_) => continue, | |
| 122 | + | }; | |
| 123 | + | ||
| 124 | + | if !metadata.is_dir() { | |
| 125 | + | continue; | |
| 126 | + | } | |
| 127 | + | ||
| 128 | + | // Try to open as a git repository. | |
| 129 | + | match gix::open(&path) { | |
| 130 | + | Ok(repo) if repo.is_bare() => { | |
| 131 | + | let absolute_path = path.canonicalize()?; | |
| 132 | + | let relative_path = absolute_path | |
| 133 | + | .strip_prefix(root) | |
| 134 | + | .expect("discovered path must be inside root") | |
| 135 | + | .to_string_lossy() | |
| 136 | + | .into_owned(); | |
| 137 | + | let name = absolute_path | |
| 138 | + | .file_name() | |
| 139 | + | .map(|n| n.to_string_lossy().into_owned()) | |
| 140 | + | .unwrap_or_else(|| relative_path.clone()); | |
| 141 | + | let description = read_description(&absolute_path); | |
| 142 | + | ||
| 143 | + | repos.push(RepoInfo { | |
| 144 | + | name, | |
| 145 | + | relative_path, | |
| 146 | + | absolute_path, | |
| 147 | + | description, | |
| 148 | + | }); | |
| 149 | + | // Do not descend into a repository directory. | |
| 150 | + | } | |
| 151 | + | _ => { | |
| 152 | + | // Not a bare repo (or open failed). Descend if within max_depth. | |
| 153 | + | if depth < max_depth { | |
| 154 | + | walk_dir(root, &path, depth + 1, max_depth, repos)?; | |
| 155 | + | } | |
| 156 | + | } | |
| 157 | + | } | |
| 158 | + | } | |
| 159 | + | ||
| 160 | + | Ok(()) | |
| 161 | + | } | |
| 162 | + | ||
| 163 | + | /// Read the `description` file from a bare repository directory. | |
| 164 | + | /// | |
| 165 | + | /// Returns `None` if the file is absent, unreadable, or contains the default | |
| 166 | + | /// git placeholder text. | |
| 167 | + | fn read_description(repo_path: &Path) -> Option<String> { | |
| 168 | + | let desc_path = repo_path.join("description"); | |
| 169 | + | let content = fs::read_to_string(&desc_path).ok()?; | |
| 170 | + | let trimmed = content.trim().to_string(); | |
| 171 | + | if trimmed.is_empty() || trimmed == DEFAULT_GIT_DESCRIPTION { | |
| 172 | + | None | |
| 173 | + | } else { | |
| 174 | + | Some(trimmed) | |
| 175 | + | } | |
| 176 | + | } | |
| 177 | + | ||
| 178 | + | #[cfg(test)] | |
| 179 | + | mod tests { | |
| 180 | + | use std::path::Path; | |
| 181 | + | use std::process::Command; | |
| 182 | + | ||
| 183 | + | use tempfile::TempDir; | |
| 184 | + | ||
| 185 | + | use super::*; | |
| 186 | + | ||
| 187 | + | fn create_bare_repo(path: &Path) { | |
| 188 | + | Command::new("git") | |
| 189 | + | .args(["init", "--bare", path.to_str().unwrap()]) | |
| 190 | + | .output() | |
| 191 | + | .expect("git init --bare failed"); | |
| 192 | + | } | |
| 193 | + | ||
| 194 | + | #[test] | |
| 195 | + | fn discover_finds_bare_repos() { | |
| 196 | + | let dir = TempDir::new().unwrap(); | |
| 197 | + | create_bare_repo(&dir.path().join("alpha.git")); | |
| 198 | + | create_bare_repo(&dir.path().join("beta.git")); | |
| 199 | + | ||
| 200 | + | let store = RepoStore::discover(dir.path().to_path_buf(), 0).unwrap(); | |
| 201 | + | assert_eq!(store.list().len(), 2); | |
| 202 | + | } | |
| 203 | + | ||
| 204 | + | #[test] | |
| 205 | + | fn discover_finds_nested_repos() { | |
| 206 | + | let dir = TempDir::new().unwrap(); | |
| 207 | + | let repo_path = dir.path().join("org").join("project.git"); | |
| 208 | + | std::fs::create_dir_all(&repo_path).unwrap(); | |
| 209 | + | create_bare_repo(&repo_path); | |
| 210 | + | ||
| 211 | + | let store = RepoStore::discover(dir.path().to_path_buf(), 1).unwrap(); | |
| 212 | + | assert_eq!(store.list().len(), 1); | |
| 213 | + | assert_eq!(store.list()[0].relative_path, "org/project.git"); | |
| 214 | + | } | |
| 215 | + | ||
| 216 | + | #[test] | |
| 217 | + | fn discover_respects_max_depth() { | |
| 218 | + | let dir = TempDir::new().unwrap(); | |
| 219 | + | let deep = dir.path().join("a").join("b").join("c").join("deep.git"); | |
| 220 | + | std::fs::create_dir_all(&deep).unwrap(); | |
| 221 | + | create_bare_repo(&deep); | |
| 222 | + | ||
| 223 | + | // max_depth 2 should not find it (it is 3 levels below root) | |
| 224 | + | let store_shallow = RepoStore::discover(dir.path().to_path_buf(), 2).unwrap(); | |
| 225 | + | assert_eq!(store_shallow.list().len(), 0); | |
| 226 | + | ||
| 227 | + | // max_depth 3 should find it | |
| 228 | + | let store_deep = RepoStore::discover(dir.path().to_path_buf(), 3).unwrap(); | |
| 229 | + | assert_eq!(store_deep.list().len(), 1); | |
| 230 | + | } | |
| 231 | + | ||
| 232 | + | #[test] | |
| 233 | + | fn discover_max_depth_zero_only_root_level() { | |
| 234 | + | let dir = TempDir::new().unwrap(); | |
| 235 | + | create_bare_repo(&dir.path().join("root-level.git")); | |
| 236 | + | let nested = dir.path().join("nested").join("deep.git"); | |
| 237 | + | std::fs::create_dir_all(&nested).unwrap(); | |
| 238 | + | create_bare_repo(&nested); | |
| 239 | + | ||
| 240 | + | let store = RepoStore::discover(dir.path().to_path_buf(), 0).unwrap(); | |
| 241 | + | assert_eq!(store.list().len(), 1); | |
| 242 | + | assert_eq!(store.list()[0].relative_path, "root-level.git"); | |
| 243 | + | } | |
| 244 | + | ||
| 245 | + | #[test] | |
| 246 | + | fn discover_ignores_non_bare_dirs() { | |
| 247 | + | let dir = TempDir::new().unwrap(); | |
| 248 | + | // A plain directory -- not a git repo | |
| 249 | + | std::fs::create_dir(dir.path().join("just-a-dir")).unwrap(); | |
| 250 | + | ||
| 251 | + | let store = RepoStore::discover(dir.path().to_path_buf(), 0).unwrap(); | |
| 252 | + | assert_eq!(store.list().len(), 0); | |
| 253 | + | } | |
| 254 | + | ||
| 255 | + | #[test] | |
| 256 | + | fn resolve_existing_repo() { | |
| 257 | + | let dir = TempDir::new().unwrap(); | |
| 258 | + | create_bare_repo(&dir.path().join("myrepo.git")); | |
| 259 | + | ||
| 260 | + | let store = RepoStore::discover(dir.path().to_path_buf(), 0).unwrap(); | |
| 261 | + | let info = store.resolve("myrepo.git").unwrap(); | |
| 262 | + | assert_eq!(info.relative_path, "myrepo.git"); | |
| 263 | + | assert_eq!(info.name, "myrepo.git"); | |
| 264 | + | } | |
| 265 | + | ||
| 266 | + | #[test] | |
| 267 | + | fn resolve_missing_repo() { | |
| 268 | + | let dir = TempDir::new().unwrap(); | |
| 269 | + | create_bare_repo(&dir.path().join("exists.git")); | |
| 270 | + | ||
| 271 | + | let store = RepoStore::discover(dir.path().to_path_buf(), 0).unwrap(); | |
| 272 | + | let err = store.resolve("nope.git").unwrap_err(); | |
| 273 | + | assert!(matches!(err, Error::RepoNotFound(_))); | |
| 274 | + | } | |
| 275 | + | ||
| 276 | + | #[test] | |
| 277 | + | fn reads_description_file() { | |
| 278 | + | let dir = TempDir::new().unwrap(); | |
| 279 | + | let repo_path = dir.path().join("described.git"); | |
| 280 | + | create_bare_repo(&repo_path); | |
| 281 | + | std::fs::write(repo_path.join("description"), "A test repository\n").unwrap(); | |
| 282 | + | ||
| 283 | + | let store = RepoStore::discover(dir.path().to_path_buf(), 0).unwrap(); | |
| 284 | + | assert_eq!(store.list().len(), 1); | |
| 285 | + | assert_eq!( | |
| 286 | + | store.list()[0].description.as_deref(), | |
| 287 | + | Some("A test repository") | |
| 288 | + | ); | |
| 289 | + | } | |
| 290 | + | ||
| 291 | + | #[test] | |
| 292 | + | fn refresh_picks_up_new_repositories() { | |
| 293 | + | let dir = TempDir::new().unwrap(); | |
| 294 | + | create_bare_repo(&dir.path().join("alpha.git")); | |
| 295 | + | ||
| 296 | + | let mut store = RepoStore::discover(dir.path().to_path_buf(), 0).unwrap(); | |
| 297 | + | assert_eq!(store.list().len(), 1); | |
| 298 | + | ||
| 299 | + | create_bare_repo(&dir.path().join("beta.git")); | |
| 300 | + | store.refresh().unwrap(); | |
| 301 | + | ||
| 302 | + | assert_eq!(store.list().len(), 2); | |
| 303 | + | assert!(store.list().iter().any(|repo| repo.name == "beta.git")); | |
| 304 | + | } | |
| 305 | + | } |
addedvendor/gitserver-core/src/dynamic_registry.rs+275 −0
| 1 | + | // This Source Code Form is subject to the terms of the Mozilla Public | |
| 2 | + | // License, v. 2.0. If a copy of the MPL was not distributed with this | |
| 3 | + | // file, You can obtain one at https://mozilla.org/MPL/2.0/. | |
| 4 | + | // | |
| 5 | + | // Copyright (c) 2026 WJQSERVER | |
| 6 | + | ||
| 7 | + | use std::path::{ | |
| 8 | + | Path, | |
| 9 | + | PathBuf, | |
| 10 | + | }; | |
| 11 | + | use std::sync::{ | |
| 12 | + | Arc, | |
| 13 | + | RwLock, | |
| 14 | + | }; | |
| 15 | + | ||
| 16 | + | use crate::discovery::{ | |
| 17 | + | RepoInfo, | |
| 18 | + | RepoStore, | |
| 19 | + | }; | |
| 20 | + | use crate::error::{ | |
| 21 | + | Error, | |
| 22 | + | Result, | |
| 23 | + | }; | |
| 24 | + | ||
| 25 | + | pub trait RepoResolver: Send + Sync { | |
| 26 | + | fn resolve(&self, relative: &str) -> Result<RepoInfo>; | |
| 27 | + | fn list(&self) -> Result<Vec<RepoInfo>>; | |
| 28 | + | } | |
| 29 | + | ||
| 30 | + | pub trait MutableRepoRegistry: RepoResolver { | |
| 31 | + | fn register(&self, repo: RepoInfo) -> Result<()>; | |
| 32 | + | fn unregister(&self, relative: &str) -> Result<()>; | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | #[derive(Clone, Default)] | |
| 36 | + | pub struct DynamicRepoRegistry { | |
| 37 | + | repos: Arc<RwLock<Vec<RepoInfo>>>, | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | impl DynamicRepoRegistry { | |
| 41 | + | pub fn new() -> Self { | |
| 42 | + | Self::default() | |
| 43 | + | } | |
| 44 | + | ||
| 45 | + | pub fn from_repos(repos: Vec<RepoInfo>) -> Result<Self> { | |
| 46 | + | let registry = Self::new(); | |
| 47 | + | for repo in repos { | |
| 48 | + | registry.register(repo)?; | |
| 49 | + | } | |
| 50 | + | Ok(registry) | |
| 51 | + | } | |
| 52 | + | } | |
| 53 | + | ||
| 54 | + | impl RepoResolver for RepoStore { | |
| 55 | + | fn resolve(&self, relative: &str) -> Result<RepoInfo> { | |
| 56 | + | RepoStore::resolve(self, relative).cloned() | |
| 57 | + | } | |
| 58 | + | ||
| 59 | + | fn list(&self) -> Result<Vec<RepoInfo>> { | |
| 60 | + | Ok(RepoStore::list(self).to_vec()) | |
| 61 | + | } | |
| 62 | + | } | |
| 63 | + | ||
| 64 | + | impl RepoResolver for DynamicRepoRegistry { | |
| 65 | + | fn resolve(&self, relative: &str) -> Result<RepoInfo> { | |
| 66 | + | let normalized = normalize_relative_repo_path(relative)?; | |
| 67 | + | self.repos | |
| 68 | + | .read() | |
| 69 | + | .expect("dynamic repo registry poisoned") | |
| 70 | + | .iter() | |
| 71 | + | .find(|repo| repo.relative_path == normalized) | |
| 72 | + | .cloned() | |
| 73 | + | .ok_or_else(|| Error::RepoNotFound(relative.to_string())) | |
| 74 | + | } | |
| 75 | + | ||
| 76 | + | fn list(&self) -> Result<Vec<RepoInfo>> { | |
| 77 | + | Ok(self | |
| 78 | + | .repos | |
| 79 | + | .read() | |
| 80 | + | .expect("dynamic repo registry poisoned") | |
| 81 | + | .clone()) | |
| 82 | + | } | |
| 83 | + | } | |
| 84 | + | ||
| 85 | + | impl MutableRepoRegistry for DynamicRepoRegistry { | |
| 86 | + | fn register(&self, repo: RepoInfo) -> Result<()> { | |
| 87 | + | let relative_path = normalize_relative_repo_path(&repo.relative_path)?; | |
| 88 | + | let repo_path = repo.absolute_path.canonicalize()?; | |
| 89 | + | let opened = gix::open(&repo_path)?; | |
| 90 | + | if !opened.is_bare() { | |
| 91 | + | return Err(Error::Protocol(format!( | |
| 92 | + | "registered path is not a bare repository: {}", | |
| 93 | + | repo_path.display() | |
| 94 | + | ))); | |
| 95 | + | } | |
| 96 | + | ||
| 97 | + | let mut repos = self.repos.write().expect("dynamic repo registry poisoned"); | |
| 98 | + | if repos | |
| 99 | + | .iter() | |
| 100 | + | .any(|existing| existing.relative_path == relative_path) | |
| 101 | + | { | |
| 102 | + | return Err(Error::Protocol(format!( | |
| 103 | + | "repository already registered: {}", | |
| 104 | + | relative_path | |
| 105 | + | ))); | |
| 106 | + | } | |
| 107 | + | ||
| 108 | + | let mut repo = repo; | |
| 109 | + | repo.relative_path = relative_path; | |
| 110 | + | repo.absolute_path = repo_path; | |
| 111 | + | repos.push(repo); | |
| 112 | + | repos.sort_by(|a, b| a.relative_path.cmp(&b.relative_path)); | |
| 113 | + | Ok(()) | |
| 114 | + | } | |
| 115 | + | ||
| 116 | + | fn unregister(&self, relative: &str) -> Result<()> { | |
| 117 | + | let normalized = normalize_relative_repo_path(relative)?; | |
| 118 | + | let mut repos = self.repos.write().expect("dynamic repo registry poisoned"); | |
| 119 | + | let original_len = repos.len(); | |
| 120 | + | repos.retain(|repo| repo.relative_path != normalized); | |
| 121 | + | if repos.len() == original_len { | |
| 122 | + | return Err(Error::RepoNotFound(relative.to_string())); | |
| 123 | + | } | |
| 124 | + | Ok(()) | |
| 125 | + | } | |
| 126 | + | } | |
| 127 | + | ||
| 128 | + | fn normalize_relative_repo_path(relative: &str) -> Result<String> { | |
| 129 | + | let path = Path::new(relative); | |
| 130 | + | if path.is_absolute() { | |
| 131 | + | return Err(Error::PathTraversal(relative.to_string().into())); | |
| 132 | + | } | |
| 133 | + | ||
| 134 | + | let mut normalized = PathBuf::new(); | |
| 135 | + | for component in path.components() { | |
| 136 | + | match component { | |
| 137 | + | std::path::Component::Normal(part) => normalized.push(part), | |
| 138 | + | std::path::Component::CurDir => {} | |
| 139 | + | _ => return Err(Error::PathTraversal(relative.to_string().into())), | |
| 140 | + | } | |
| 141 | + | } | |
| 142 | + | ||
| 143 | + | if normalized.as_os_str().is_empty() { | |
| 144 | + | return Err(Error::RepoNotFound(relative.to_string())); | |
| 145 | + | } | |
| 146 | + | ||
| 147 | + | Ok(normalized.to_string_lossy().into_owned()) | |
| 148 | + | } | |
| 149 | + | ||
| 150 | + | #[cfg(test)] | |
| 151 | + | mod tests { | |
| 152 | + | use std::path::Path; | |
| 153 | + | use std::process::Command; | |
| 154 | + | ||
| 155 | + | use tempfile::TempDir; | |
| 156 | + | ||
| 157 | + | use super::*; | |
| 158 | + | ||
| 159 | + | fn create_bare_repo(path: &Path) { | |
| 160 | + | Command::new("git") | |
| 161 | + | .args(["init", "--bare", path.to_str().unwrap()]) | |
| 162 | + | .output() | |
| 163 | + | .expect("git init --bare failed"); | |
| 164 | + | } | |
| 165 | + | ||
| 166 | + | #[test] | |
| 167 | + | fn dynamic_registry_registers_and_unregisters() { | |
| 168 | + | let dir = TempDir::new().unwrap(); | |
| 169 | + | let repo_path = dir.path().join("alpha.git"); | |
| 170 | + | create_bare_repo(&repo_path); | |
| 171 | + | ||
| 172 | + | let registry = DynamicRepoRegistry::new(); | |
| 173 | + | registry | |
| 174 | + | .register(RepoInfo { | |
| 175 | + | name: "alpha.git".into(), | |
| 176 | + | relative_path: "alpha.git".into(), | |
| 177 | + | absolute_path: repo_path.clone(), | |
| 178 | + | description: None, | |
| 179 | + | }) | |
| 180 | + | .unwrap(); | |
| 181 | + | ||
| 182 | + | assert_eq!(registry.list().unwrap().len(), 1); | |
| 183 | + | assert_eq!( | |
| 184 | + | registry.resolve("alpha.git").unwrap().absolute_path, | |
| 185 | + | repo_path.canonicalize().unwrap() | |
| 186 | + | ); | |
| 187 | + | ||
| 188 | + | registry.unregister("alpha.git").unwrap(); | |
| 189 | + | assert!(matches!( | |
| 190 | + | registry.resolve("alpha.git"), | |
| 191 | + | Err(Error::RepoNotFound(_)) | |
| 192 | + | )); | |
| 193 | + | } | |
| 194 | + | ||
| 195 | + | #[test] | |
| 196 | + | fn dynamic_registry_resolve_and_unregister_normalize_paths() { | |
| 197 | + | let dir = TempDir::new().unwrap(); | |
| 198 | + | let repo_path = dir.path().join("alpha.git"); | |
| 199 | + | create_bare_repo(&repo_path); | |
| 200 | + | ||
| 201 | + | let registry = DynamicRepoRegistry::new(); | |
| 202 | + | registry | |
| 203 | + | .register(RepoInfo { | |
| 204 | + | name: "alpha.git".into(), | |
| 205 | + | relative_path: "alpha.git".into(), | |
| 206 | + | absolute_path: repo_path, | |
| 207 | + | description: None, | |
| 208 | + | }) | |
| 209 | + | .unwrap(); | |
| 210 | + | ||
| 211 | + | assert!(registry.resolve("./alpha.git").is_ok()); | |
| 212 | + | registry.unregister("./alpha.git").unwrap(); | |
| 213 | + | assert!(matches!( | |
| 214 | + | registry.resolve("alpha.git"), | |
| 215 | + | Err(Error::RepoNotFound(_)) | |
| 216 | + | )); | |
| 217 | + | } | |
| 218 | + | ||
| 219 | + | #[test] | |
| 220 | + | fn dynamic_registry_rejects_duplicate_registration() { | |
| 221 | + | let dir = TempDir::new().unwrap(); | |
| 222 | + | let repo_path = dir.path().join("alpha.git"); | |
| 223 | + | create_bare_repo(&repo_path); | |
| 224 | + | ||
| 225 | + | let registry = DynamicRepoRegistry::new(); | |
| 226 | + | let repo = RepoInfo { | |
| 227 | + | name: "alpha.git".into(), | |
| 228 | + | relative_path: "alpha.git".into(), | |
| 229 | + | absolute_path: repo_path, | |
| 230 | + | description: None, | |
| 231 | + | }; | |
| 232 | + | ||
| 233 | + | registry.register(repo.clone()).unwrap(); | |
| 234 | + | let err = registry.register(repo).unwrap_err(); | |
| 235 | + | assert!(matches!(err, Error::Protocol(_))); | |
| 236 | + | } | |
| 237 | + | ||
| 238 | + | #[test] | |
| 239 | + | fn dynamic_registry_rejects_parent_relative_paths() { | |
| 240 | + | let dir = TempDir::new().unwrap(); | |
| 241 | + | let repo_path = dir.path().join("alpha.git"); | |
| 242 | + | create_bare_repo(&repo_path); | |
| 243 | + | ||
| 244 | + | let registry = DynamicRepoRegistry::new(); | |
| 245 | + | let err = registry | |
| 246 | + | .register(RepoInfo { | |
| 247 | + | name: "alpha.git".into(), | |
| 248 | + | relative_path: "./team/../alpha.git".into(), | |
| 249 | + | absolute_path: repo_path, | |
| 250 | + | description: None, | |
| 251 | + | }) | |
| 252 | + | .unwrap_err(); | |
| 253 | + | ||
| 254 | + | assert!(matches!(err, Error::PathTraversal(_))); | |
| 255 | + | } | |
| 256 | + | ||
| 257 | + | #[test] | |
| 258 | + | fn dynamic_registry_rejects_absolute_relative_path() { | |
| 259 | + | let dir = TempDir::new().unwrap(); | |
| 260 | + | let repo_path = dir.path().join("alpha.git"); | |
| 261 | + | create_bare_repo(&repo_path); | |
| 262 | + | ||
| 263 | + | let registry = DynamicRepoRegistry::new(); | |
| 264 | + | let err = registry | |
| 265 | + | .register(RepoInfo { | |
| 266 | + | name: "alpha.git".into(), | |
| 267 | + | relative_path: "/tmp/alpha.git".into(), | |
| 268 | + | absolute_path: repo_path, | |
| 269 | + | description: None, | |
| 270 | + | }) | |
| 271 | + | .unwrap_err(); | |
| 272 | + | ||
| 273 | + | assert!(matches!(err, Error::PathTraversal(_))); | |
| 274 | + | } | |
| 275 | + | } |
addedvendor/gitserver-core/src/error.rs+47 −0
| 1 | + | use std::path::PathBuf; | |
| 2 | + | ||
| 3 | + | #[derive(Debug, thiserror::Error)] | |
| 4 | + | pub enum Error { | |
| 5 | + | #[error("repository not found: {0}")] | |
| 6 | + | RepoNotFound(String), | |
| 7 | + | ||
| 8 | + | #[error("path traversal rejected: {0}")] | |
| 9 | + | PathTraversal(PathBuf), | |
| 10 | + | ||
| 11 | + | #[error("invalid repository at {0}: {1}")] | |
| 12 | + | InvalidRepo(PathBuf, String), | |
| 13 | + | ||
| 14 | + | #[error("protocol error: {0}")] | |
| 15 | + | Protocol(String), | |
| 16 | + | ||
| 17 | + | #[error("git operation failed: {0}")] | |
| 18 | + | Git(Box<gix::open::Error>), | |
| 19 | + | ||
| 20 | + | #[error("io error: {0}")] | |
| 21 | + | Io(#[from] std::io::Error), | |
| 22 | + | } | |
| 23 | + | ||
| 24 | + | impl From<gix::open::Error> for Error { | |
| 25 | + | fn from(e: gix::open::Error) -> Self { | |
| 26 | + | Error::Git(Box::new(e)) | |
| 27 | + | } | |
| 28 | + | } | |
| 29 | + | ||
| 30 | + | pub type Result<T> = std::result::Result<T, Error>; | |
| 31 | + | ||
| 32 | + | #[cfg(test)] | |
| 33 | + | mod tests { | |
| 34 | + | use super::*; | |
| 35 | + | ||
| 36 | + | #[test] | |
| 37 | + | fn error_display_repo_not_found() { | |
| 38 | + | let err = Error::RepoNotFound("foo/bar.git".into()); | |
| 39 | + | assert_eq!(err.to_string(), "repository not found: foo/bar.git"); | |
| 40 | + | } | |
| 41 | + | ||
| 42 | + | #[test] | |
| 43 | + | fn error_display_path_traversal() { | |
| 44 | + | let err = Error::PathTraversal(PathBuf::from("../etc/passwd")); | |
| 45 | + | assert_eq!(err.to_string(), "path traversal rejected: ../etc/passwd"); | |
| 46 | + | } | |
| 47 | + | } |
addedvendor/gitserver-core/src/lib.rs+10 −0
| 1 | + | pub mod backend; | |
| 2 | + | pub mod discovery; | |
| 3 | + | pub mod dynamic_registry; | |
| 4 | + | pub mod error; | |
| 5 | + | pub mod pack; | |
| 6 | + | pub mod path; | |
| 7 | + | pub mod pktline; | |
| 8 | + | pub mod protocol_v2; | |
| 9 | + | pub mod receive_pack; | |
| 10 | + | pub mod refs; |
addedvendor/gitserver-core/src/pack.rs+848 −0
| 1 | + | use std::collections::HashSet; | |
| 2 | + | use std::path::Path; | |
| 3 | + | ||
| 4 | + | use bytes::Bytes; | |
| 5 | + | use sha1::{ | |
| 6 | + | Digest, | |
| 7 | + | Sha1, | |
| 8 | + | }; | |
| 9 | + | use tokio::io::AsyncRead; | |
| 10 | + | use tokio_util::io::StreamReader; | |
| 11 | + | ||
| 12 | + | use crate::error::{ | |
| 13 | + | Error, | |
| 14 | + | Result, | |
| 15 | + | }; | |
| 16 | + | use crate::pktline; | |
| 17 | + | ||
| 18 | + | #[derive(Debug, Clone, Default)] | |
| 19 | + | pub struct UploadPackCapabilities { | |
| 20 | + | pub ofs_delta: bool, | |
| 21 | + | pub multi_ack: bool, | |
| 22 | + | pub multi_ack_detailed: bool, | |
| 23 | + | } | |
| 24 | + | ||
| 25 | + | #[derive(Debug, Clone, Default)] | |
| 26 | + | pub struct ShallowRequest { | |
| 27 | + | pub depth: Option<usize>, | |
| 28 | + | pub client_shallows: Vec<gix::ObjectId>, | |
| 29 | + | pub deepen_relative: bool, | |
| 30 | + | } | |
| 31 | + | ||
| 32 | + | /// A parsed upload-pack request from a Git client. | |
| 33 | + | pub struct UploadPackRequest { | |
| 34 | + | pub wants: Vec<gix::ObjectId>, | |
| 35 | + | pub haves: Vec<gix::ObjectId>, | |
| 36 | + | pub done: bool, | |
| 37 | + | pub capabilities: UploadPackCapabilities, | |
| 38 | + | pub shallow: ShallowRequest, | |
| 39 | + | pub object_ids: Option<Vec<gix::ObjectId>>, | |
| 40 | + | } | |
| 41 | + | ||
| 42 | + | impl UploadPackRequest { | |
| 43 | + | /// Parse a pkt-line encoded upload-pack request body. | |
| 44 | + | /// | |
| 45 | + | /// The body contains: | |
| 46 | + | /// - "want <oid> [capabilities]\n" lines | |
| 47 | + | /// - flush packet "0000" | |
| 48 | + | /// - "have <oid>\n" lines (optional) | |
| 49 | + | /// - "done\n" | |
| 50 | + | pub fn parse(body: &[u8]) -> Result<Self> { | |
| 51 | + | let mut wants = Vec::new(); | |
| 52 | + | let mut haves = Vec::new(); | |
| 53 | + | let mut done = false; | |
| 54 | + | let mut capabilities = UploadPackCapabilities::default(); | |
| 55 | + | let mut shallow = ShallowRequest::default(); | |
| 56 | + | let mut pos = 0; | |
| 57 | + | ||
| 58 | + | while pos < body.len() { | |
| 59 | + | // Check for flush packet | |
| 60 | + | if body[pos..].starts_with(b"0000") { | |
| 61 | + | pos += 4; | |
| 62 | + | continue; | |
| 63 | + | } | |
| 64 | + | ||
| 65 | + | // Read 4-byte hex length prefix | |
| 66 | + | if pos + 4 > body.len() { | |
| 67 | + | break; | |
| 68 | + | } | |
| 69 | + | let len_str = std::str::from_utf8(&body[pos..pos + 4]) | |
| 70 | + | .map_err(|_| Error::Protocol("invalid pkt-line length prefix".into()))?; | |
| 71 | + | let len = usize::from_str_radix(len_str, 16) | |
| 72 | + | .map_err(|_| Error::Protocol("invalid pkt-line length".into()))?; | |
| 73 | + | ||
| 74 | + | if len == 0 { | |
| 75 | + | // flush packet already handled above, but just in case | |
| 76 | + | pos += 4; | |
| 77 | + | continue; | |
| 78 | + | } | |
| 79 | + | ||
| 80 | + | if len < 4 || pos + len > body.len() { | |
| 81 | + | break; | |
| 82 | + | } | |
| 83 | + | ||
| 84 | + | let payload = &body[pos + 4..pos + len]; | |
| 85 | + | let line = std::str::from_utf8(payload) | |
| 86 | + | .map_err(|_| Error::Protocol("invalid UTF-8 in pkt-line".into()))?; | |
| 87 | + | let line = line.trim_end_matches('\n'); | |
| 88 | + | ||
| 89 | + | if line == "done" { | |
| 90 | + | done = true; | |
| 91 | + | } else if let Some(rest) = line.strip_prefix("deepen ") { | |
| 92 | + | let depth = rest | |
| 93 | + | .parse::<usize>() | |
| 94 | + | .map_err(|_| Error::Protocol(format!("invalid deepen value: {rest}")))?; | |
| 95 | + | shallow.depth = Some(depth); | |
| 96 | + | } else if line == "deepen-relative" { | |
| 97 | + | shallow.deepen_relative = true; | |
| 98 | + | } else if let Some(rest) = line.strip_prefix("shallow ") { | |
| 99 | + | let oid = gix::ObjectId::from_hex(rest.as_bytes()) | |
| 100 | + | .map_err(|_| Error::Protocol(format!("invalid OID in shallow: {rest}")))?; | |
| 101 | + | shallow.client_shallows.push(oid); | |
| 102 | + | } else if let Some(rest) = line.strip_prefix("want ") { | |
| 103 | + | let mut parts = rest.split_ascii_whitespace(); | |
| 104 | + | let oid_hex = parts | |
| 105 | + | .next() | |
| 106 | + | .ok_or_else(|| Error::Protocol("missing OID in want".into()))?; | |
| 107 | + | let oid = gix::ObjectId::from_hex(oid_hex.as_bytes()) | |
| 108 | + | .map_err(|_| Error::Protocol(format!("invalid OID in want: {oid_hex}")))?; | |
| 109 | + | if wants.is_empty() { | |
| 110 | + | for capability in parts { | |
| 111 | + | if capability == "ofs-delta" { | |
| 112 | + | capabilities.ofs_delta = true; | |
| 113 | + | } else if capability == "multi_ack" { | |
| 114 | + | capabilities.multi_ack = true; | |
| 115 | + | } else if capability == "multi_ack_detailed" { | |
| 116 | + | capabilities.multi_ack = true; | |
| 117 | + | capabilities.multi_ack_detailed = true; | |
| 118 | + | } | |
| 119 | + | } | |
| 120 | + | } | |
| 121 | + | wants.push(oid); | |
| 122 | + | } else if let Some(rest) = line.strip_prefix("have ") { | |
| 123 | + | let oid_hex = rest | |
| 124 | + | .split_ascii_whitespace() | |
| 125 | + | .next() | |
| 126 | + | .ok_or_else(|| Error::Protocol("missing OID in have".into()))?; | |
| 127 | + | let oid = gix::ObjectId::from_hex(oid_hex.as_bytes()) | |
| 128 | + | .map_err(|_| Error::Protocol(format!("invalid OID in have: {oid_hex}")))?; | |
| 129 | + | haves.push(oid); | |
| 130 | + | } | |
| 131 | + | ||
| 132 | + | pos += len; | |
| 133 | + | } | |
| 134 | + | ||
| 135 | + | Ok(Self { | |
| 136 | + | wants, | |
| 137 | + | haves, | |
| 138 | + | done, | |
| 139 | + | capabilities, | |
| 140 | + | shallow, | |
| 141 | + | object_ids: None, | |
| 142 | + | }) | |
| 143 | + | } | |
| 144 | + | } | |
| 145 | + | ||
| 146 | + | /// Encode the variable-length pack object header. | |
| 147 | + | /// | |
| 148 | + | /// Format: first byte = MSB continuation + 3-bit type + 4-bit size | |
| 149 | + | /// Subsequent bytes: 7-bit size chunks with MSB continuation | |
| 150 | + | fn encode_pack_object_header(obj_type: u8, size: usize) -> Vec<u8> { | |
| 151 | + | let mut header = Vec::new(); | |
| 152 | + | let mut byte = (obj_type << 4) | (size as u8 & 0x0f); | |
| 153 | + | let mut remaining = size >> 4; | |
| 154 | + | ||
| 155 | + | if remaining > 0 { | |
| 156 | + | byte |= 0x80; // set continuation bit | |
| 157 | + | header.push(byte); | |
| 158 | + | while remaining > 0 { | |
| 159 | + | byte = remaining as u8 & 0x7f; | |
| 160 | + | remaining >>= 7; | |
| 161 | + | if remaining > 0 { | |
| 162 | + | byte |= 0x80; | |
| 163 | + | } | |
| 164 | + | header.push(byte); | |
| 165 | + | } | |
| 166 | + | } else { | |
| 167 | + | header.push(byte); | |
| 168 | + | } | |
| 169 | + | ||
| 170 | + | header | |
| 171 | + | } | |
| 172 | + | ||
| 173 | + | fn encode_ofs_delta_base_distance(mut distance: u64) -> Vec<u8> { | |
| 174 | + | debug_assert!(distance > 0, "offset deltas must point backwards"); | |
| 175 | + | ||
| 176 | + | let mut buf = [0u8; 10]; | |
| 177 | + | let mut bytes_written = 1; | |
| 178 | + | buf[buf.len() - 1] = distance as u8 & 0x7f; | |
| 179 | + | ||
| 180 | + | for out in buf.iter_mut().rev().skip(1) { | |
| 181 | + | distance >>= 7; | |
| 182 | + | if distance == 0 { | |
| 183 | + | break; | |
| 184 | + | } | |
| 185 | + | distance -= 1; | |
| 186 | + | *out = 0x80 | (distance as u8 & 0x7f); | |
| 187 | + | bytes_written += 1; | |
| 188 | + | } | |
| 189 | + | ||
| 190 | + | buf[buf.len() - bytes_written..].to_vec() | |
| 191 | + | } | |
| 192 | + | ||
| 193 | + | fn encode_delta_size(mut size: usize, out: &mut Vec<u8>) { | |
| 194 | + | loop { | |
| 195 | + | let mut byte = (size & 0x7f) as u8; | |
| 196 | + | size >>= 7; | |
| 197 | + | if size > 0 { | |
| 198 | + | byte |= 0x80; | |
| 199 | + | } | |
| 200 | + | out.push(byte); | |
| 201 | + | if size == 0 { | |
| 202 | + | break; | |
| 203 | + | } | |
| 204 | + | } | |
| 205 | + | } | |
| 206 | + | ||
| 207 | + | fn encode_delta_copy_instruction(out: &mut Vec<u8>, offset: usize, size: usize) { | |
| 208 | + | debug_assert!(size > 0 && size <= 0x10000); | |
| 209 | + | ||
| 210 | + | let command_pos = out.len(); | |
| 211 | + | out.push(0x80); | |
| 212 | + | let mut command = 0x80; | |
| 213 | + | ||
| 214 | + | if offset & 0xff != 0 { | |
| 215 | + | command |= 0x01; | |
| 216 | + | out.push(offset as u8); | |
| 217 | + | } | |
| 218 | + | if (offset >> 8) & 0xff != 0 { | |
| 219 | + | command |= 0x02; | |
| 220 | + | out.push((offset >> 8) as u8); | |
| 221 | + | } | |
| 222 | + | if (offset >> 16) & 0xff != 0 { | |
| 223 | + | command |= 0x04; | |
| 224 | + | out.push((offset >> 16) as u8); | |
| 225 | + | } | |
| 226 | + | if (offset >> 24) & 0xff != 0 { | |
| 227 | + | command |= 0x08; | |
| 228 | + | out.push((offset >> 24) as u8); | |
| 229 | + | } | |
| 230 | + | ||
| 231 | + | if size != 0x10000 { | |
| 232 | + | if size & 0xff != 0 { | |
| 233 | + | command |= 0x10; | |
| 234 | + | out.push(size as u8); | |
| 235 | + | } | |
| 236 | + | if (size >> 8) & 0xff != 0 { | |
| 237 | + | command |= 0x20; | |
| 238 | + | out.push((size >> 8) as u8); | |
| 239 | + | } | |
| 240 | + | if (size >> 16) & 0xff != 0 { | |
| 241 | + | command |= 0x40; | |
| 242 | + | out.push((size >> 16) as u8); | |
| 243 | + | } | |
| 244 | + | } | |
| 245 | + | ||
| 246 | + | out[command_pos] = command; | |
| 247 | + | } | |
| 248 | + | ||
| 249 | + | fn encode_delta_copy(out: &mut Vec<u8>, mut offset: usize, mut size: usize) { | |
| 250 | + | while size > 0 { | |
| 251 | + | let chunk = size.min(0x10000); | |
| 252 | + | encode_delta_copy_instruction(out, offset, chunk); | |
| 253 | + | offset += chunk; | |
| 254 | + | size -= chunk; | |
| 255 | + | } | |
| 256 | + | } | |
| 257 | + | ||
| 258 | + | fn encode_delta_insert(out: &mut Vec<u8>, data: &[u8]) { | |
| 259 | + | for chunk in data.chunks(0x7f) { | |
| 260 | + | out.push(chunk.len() as u8); | |
| 261 | + | out.extend_from_slice(chunk); | |
| 262 | + | } | |
| 263 | + | } | |
| 264 | + | ||
| 265 | + | fn encode_blob_delta(base: &[u8], target: &[u8]) -> Option<Vec<u8>> { | |
| 266 | + | let mut prefix = 0; | |
| 267 | + | let max_prefix = base.len().min(target.len()); | |
| 268 | + | while prefix < max_prefix && base[prefix] == target[prefix] { | |
| 269 | + | prefix += 1; | |
| 270 | + | } | |
| 271 | + | ||
| 272 | + | let max_suffix = base | |
| 273 | + | .len() | |
| 274 | + | .saturating_sub(prefix) | |
| 275 | + | .min(target.len().saturating_sub(prefix)); | |
| 276 | + | let mut suffix = 0; | |
| 277 | + | while suffix < max_suffix && base[base.len() - 1 - suffix] == target[target.len() - 1 - suffix] | |
| 278 | + | { | |
| 279 | + | suffix += 1; | |
| 280 | + | } | |
| 281 | + | ||
| 282 | + | if prefix == 0 && suffix == 0 { | |
| 283 | + | return None; | |
| 284 | + | } | |
| 285 | + | ||
| 286 | + | let mut delta = Vec::new(); | |
| 287 | + | encode_delta_size(base.len(), &mut delta); | |
| 288 | + | encode_delta_size(target.len(), &mut delta); | |
| 289 | + | ||
| 290 | + | if prefix > 0 { | |
| 291 | + | encode_delta_copy(&mut delta, 0, prefix); | |
| 292 | + | } | |
| 293 | + | ||
| 294 | + | let insert_start = prefix; | |
| 295 | + | let insert_end = target.len() - suffix; | |
| 296 | + | encode_delta_insert(&mut delta, &target[insert_start..insert_end]); | |
| 297 | + | ||
| 298 | + | if suffix > 0 { | |
| 299 | + | encode_delta_copy(&mut delta, base.len() - suffix, suffix); | |
| 300 | + | } | |
| 301 | + | ||
| 302 | + | Some(delta) | |
| 303 | + | } | |
| 304 | + | ||
| 305 | + | fn build_base_entry(kind: gix::object::Kind, data: &[u8]) -> Vec<u8> { | |
| 306 | + | let type_num = object_type_number(kind); | |
| 307 | + | let obj_header = encode_pack_object_header(type_num, data.len()); | |
| 308 | + | let compressed = miniz_oxide::deflate::compress_to_vec_zlib(data, 6); | |
| 309 | + | ||
| 310 | + | let mut entry = Vec::with_capacity(obj_header.len() + compressed.len()); | |
| 311 | + | entry.extend_from_slice(&obj_header); | |
| 312 | + | entry.extend_from_slice(&compressed); | |
| 313 | + | entry | |
| 314 | + | } | |
| 315 | + | ||
| 316 | + | fn build_ofs_delta_entry( | |
| 317 | + | pack_offset: u64, | |
| 318 | + | base_pack_offset: u64, | |
| 319 | + | base_data: &[u8], | |
| 320 | + | target_data: &[u8], | |
| 321 | + | ) -> Option<Vec<u8>> { | |
| 322 | + | let delta = encode_blob_delta(base_data, target_data)?; | |
| 323 | + | let obj_header = encode_pack_object_header(6, delta.len()); | |
| 324 | + | let base_distance = encode_ofs_delta_base_distance(pack_offset - base_pack_offset); | |
| 325 | + | let compressed = miniz_oxide::deflate::compress_to_vec_zlib(&delta, 6); | |
| 326 | + | ||
| 327 | + | let mut entry = Vec::with_capacity(obj_header.len() + base_distance.len() + compressed.len()); | |
| 328 | + | entry.extend_from_slice(&obj_header); | |
| 329 | + | entry.extend_from_slice(&base_distance); | |
| 330 | + | entry.extend_from_slice(&compressed); | |
| 331 | + | Some(entry) | |
| 332 | + | } | |
| 333 | + | ||
| 334 | + | struct BlobDeltaBase { | |
| 335 | + | pack_offset: u64, | |
| 336 | + | data: Vec<u8>, | |
| 337 | + | } | |
| 338 | + | ||
| 339 | + | /// Map gix object kind to pack type number. | |
| 340 | + | fn object_type_number(kind: gix::object::Kind) -> u8 { | |
| 341 | + | match kind { | |
| 342 | + | gix::object::Kind::Commit => 1, | |
| 343 | + | gix::object::Kind::Tree => 2, | |
| 344 | + | gix::object::Kind::Blob => 3, | |
| 345 | + | gix::object::Kind::Tag => 4, | |
| 346 | + | } | |
| 347 | + | } | |
| 348 | + | ||
| 349 | + | /// Send raw bytes through the channel. | |
| 350 | + | fn send( | |
| 351 | + | tx: &tokio::sync::mpsc::Sender<std::result::Result<Bytes, std::io::Error>>, | |
| 352 | + | data: &[u8], | |
| 353 | + | ) -> std::result::Result<(), Box<dyn std::error::Error + Send + Sync>> { | |
| 354 | + | tx.blocking_send(Ok(Bytes::copy_from_slice(data))) | |
| 355 | + | .map_err(|_| "receiver dropped".into()) | |
| 356 | + | } | |
| 357 | + | ||
| 358 | + | /// Send pack data through the channel wrapped in side-band-64k framing | |
| 359 | + | /// (band 1 = pack data). | |
| 360 | + | /// | |
| 361 | + | /// Respects LARGE_PACKET_MAX: each pkt-line frame carries at most | |
| 362 | + | /// 65520 - 4 (prefix) - 1 (band byte) = 65515 bytes of payload. | |
| 363 | + | fn send_sideband( | |
| 364 | + | tx: &tokio::sync::mpsc::Sender<std::result::Result<Bytes, std::io::Error>>, | |
| 365 | + | data: &[u8], | |
| 366 | + | ) -> std::result::Result<(), Box<dyn std::error::Error + Send + Sync>> { | |
| 367 | + | const MAX_DATA_PER_FRAME: usize = 65515; | |
| 368 | + | ||
| 369 | + | for chunk in data.chunks(MAX_DATA_PER_FRAME) { | |
| 370 | + | let pkt_len = 4 + 1 + chunk.len(); | |
| 371 | + | let mut frame = Vec::with_capacity(pkt_len); | |
| 372 | + | frame.extend_from_slice(format!("{pkt_len:04x}").as_bytes()); | |
| 373 | + | frame.push(0x01); // band 1 = pack data | |
| 374 | + | frame.extend_from_slice(chunk); | |
| 375 | + | send(tx, &frame)?; | |
| 376 | + | } | |
| 377 | + | ||
| 378 | + | Ok(()) | |
| 379 | + | } | |
| 380 | + | ||
| 381 | + | fn encode_ack_line(oid: gix::ObjectId, suffix: Option<&str>) -> Vec<u8> { | |
| 382 | + | let mut line = format!("ACK {oid}"); | |
| 383 | + | if let Some(suffix) = suffix { | |
| 384 | + | line.push(' '); | |
| 385 | + | line.push_str(suffix); | |
| 386 | + | } | |
| 387 | + | line.push('\n'); | |
| 388 | + | pktline::encode(line.as_bytes()) | |
| 389 | + | } | |
| 390 | + | ||
| 391 | + | /// Recursively collect tree and blob OIDs reachable from `tree_oid`. | |
| 392 | + | /// | |
| 393 | + | /// Uses a single `find_object` call per object and parses raw tree | |
| 394 | + | /// bytes via `TreeRefIter` to avoid a second ODB lookup. | |
| 395 | + | fn collect_tree_oids( | |
| 396 | + | repo: &gix::Repository, | |
| 397 | + | tree_oid: gix::ObjectId, | |
| 398 | + | seen: &mut HashSet<gix::ObjectId>, | |
| 399 | + | oids: &mut Vec<gix::ObjectId>, | |
| 400 | + | ) -> std::result::Result<(), Box<dyn std::error::Error + Send + Sync>> { | |
| 401 | + | if !seen.insert(tree_oid) { | |
| 402 | + | return Ok(()); | |
| 403 | + | } | |
| 404 | + | ||
| 405 | + | let tree_obj = repo.find_object(tree_oid)?; | |
| 406 | + | let tree_data = tree_obj.data.to_vec(); | |
| 407 | + | oids.push(tree_oid); | |
| 408 | + | ||
| 409 | + | for entry_result in gix::objs::TreeRefIter::from_bytes(&tree_data, gix::hash::Kind::Sha1) { | |
| 410 | + | let entry = entry_result?; | |
| 411 | + | let entry_oid = entry.oid.to_owned(); | |
| 412 | + | let entry_mode = entry.mode; | |
| 413 | + | ||
| 414 | + | if entry_mode.is_tree() { | |
| 415 | + | collect_tree_oids(repo, entry_oid, seen, oids)?; | |
| 416 | + | } else if seen.insert(entry_oid) && !entry_mode.is_commit() { | |
| 417 | + | oids.push(entry_oid); | |
| 418 | + | } | |
| 419 | + | } | |
| 420 | + | ||
| 421 | + | Ok(()) | |
| 422 | + | } | |
| 423 | + | ||
| 424 | + | /// Walk commits from `wants` (excluding `haves`) and collect all | |
| 425 | + | /// reachable ObjectIds (commits, trees, blobs). | |
| 426 | + | /// | |
| 427 | + | /// Pass 1 of the two-pass streaming approach: only OIDs are stored, | |
| 428 | + | /// not object data. | |
| 429 | + | fn collect_all_oids( | |
| 430 | + | repo: &gix::Repository, | |
| 431 | + | wants: &[gix::ObjectId], | |
| 432 | + | haves: &[gix::ObjectId], | |
| 433 | + | ) -> std::result::Result<Vec<gix::ObjectId>, Box<dyn std::error::Error + Send + Sync>> { | |
| 434 | + | let have_set: HashSet<gix::ObjectId> = haves.iter().copied().collect(); | |
| 435 | + | let mut seen = HashSet::new(); | |
| 436 | + | let mut oids = Vec::new(); | |
| 437 | + | ||
| 438 | + | // Mark have objects as already seen so we skip them | |
| 439 | + | for have in haves { | |
| 440 | + | seen.insert(*have); | |
| 441 | + | } | |
| 442 | + | ||
| 443 | + | let walk = repo | |
| 444 | + | .rev_walk(wants.iter().copied()) | |
| 445 | + | .with_hidden(haves.iter().copied()) | |
| 446 | + | .all()?; | |
| 447 | + | ||
| 448 | + | for info_result in walk { | |
| 449 | + | let info = info_result?; | |
| 450 | + | let commit_oid = info.id; | |
| 451 | + | ||
| 452 | + | if have_set.contains(&commit_oid) || !seen.insert(commit_oid) { | |
| 453 | + | continue; | |
| 454 | + | } | |
| 455 | + | ||
| 456 | + | // Extract tree OID from raw commit bytes (single ODB read) | |
| 457 | + | let commit_obj = repo.find_object(commit_oid)?; | |
| 458 | + | let tree_oid = | |
| 459 | + | gix::objs::CommitRefIter::from_bytes(&commit_obj.data, gix::hash::Kind::Sha1) | |
| 460 | + | .tree_id()?; | |
| 461 | + | ||
| 462 | + | oids.push(commit_oid); | |
| 463 | + | ||
| 464 | + | collect_tree_oids(repo, tree_oid, &mut seen, &mut oids)?; | |
| 465 | + | } | |
| 466 | + | ||
| 467 | + | Ok(oids) | |
| 468 | + | } | |
| 469 | + | ||
| 470 | + | fn common_haves( | |
| 471 | + | repo: &gix::Repository, | |
| 472 | + | wants: &[gix::ObjectId], | |
| 473 | + | haves: &[gix::ObjectId], | |
| 474 | + | ) -> std::result::Result<Vec<gix::ObjectId>, Box<dyn std::error::Error + Send + Sync>> { | |
| 475 | + | let want_set: HashSet<gix::ObjectId> = | |
| 476 | + | collect_all_oids(repo, wants, &[])?.into_iter().collect(); | |
| 477 | + | ||
| 478 | + | Ok(haves | |
| 479 | + | .iter() | |
| 480 | + | .copied() | |
| 481 | + | .filter(|oid| want_set.contains(oid)) | |
| 482 | + | .collect()) | |
| 483 | + | } | |
| 484 | + | ||
| 485 | + | /// Generate the complete pack response for a Git upload-pack request. | |
| 486 | + | /// | |
| 487 | + | /// Returns an `AsyncRead` producing the side-band-64k framed response that | |
| 488 | + | /// can be streamed as the HTTP response body. | |
| 489 | + | pub fn generate_pack( | |
| 490 | + | repo_path: &Path, | |
| 491 | + | request: &UploadPackRequest, | |
| 492 | + | ) -> Result<impl AsyncRead + Send + Unpin + use<>> { | |
| 493 | + | let repo_path = repo_path.to_path_buf(); | |
| 494 | + | let wants: Vec<gix::ObjectId> = request.wants.clone(); | |
| 495 | + | let haves: Vec<gix::ObjectId> = request.haves.clone(); | |
| 496 | + | let object_ids = request.object_ids.clone(); | |
| 497 | + | let done = request.done; | |
| 498 | + | let ofs_delta = request.capabilities.ofs_delta; | |
| 499 | + | let multi_ack = request.capabilities.multi_ack; | |
| 500 | + | let multi_ack_detailed = request.capabilities.multi_ack_detailed; | |
| 501 | + | ||
| 502 | + | let (tx, rx) = tokio::sync::mpsc::channel::<std::result::Result<Bytes, std::io::Error>>(64); | |
| 503 | + | ||
| 504 | + | let handle = tokio::task::spawn_blocking(move || { | |
| 505 | + | if let Err(e) = generate_pack_sync( | |
| 506 | + | &repo_path, | |
| 507 | + | &wants, | |
| 508 | + | &haves, | |
| 509 | + | object_ids, | |
| 510 | + | GeneratePackOptions { | |
| 511 | + | done, | |
| 512 | + | ofs_delta, | |
| 513 | + | multi_ack, | |
| 514 | + | multi_ack_detailed, | |
| 515 | + | }, | |
| 516 | + | &tx, | |
| 517 | + | ) { | |
| 518 | + | let _ = tx.blocking_send(Err(std::io::Error::other(e.to_string()))); | |
| 519 | + | } | |
| 520 | + | }); | |
| 521 | + | ||
| 522 | + | // Log panics from the blocking task without blocking the stream | |
| 523 | + | tokio::spawn(async move { | |
| 524 | + | if let Err(e) = handle.await { | |
| 525 | + | tracing::error!("pack generation task panicked: {e}"); | |
| 526 | + | } | |
| 527 | + | }); | |
| 528 | + | ||
| 529 | + | let stream = tokio_stream::wrappers::ReceiverStream::new(rx); | |
| 530 | + | Ok(StreamReader::new(stream)) | |
| 531 | + | } | |
| 532 | + | ||
| 533 | + | /// Synchronous two-pass streaming pack generator. | |
| 534 | + | /// | |
| 535 | + | /// Pass 1: collect OIDs only (lightweight -- no object data retained). | |
| 536 | + | /// Pass 2: re-read each object, compress, and stream it through `tx`. | |
| 537 | + | struct GeneratePackOptions { | |
| 538 | + | done: bool, | |
| 539 | + | ofs_delta: bool, | |
| 540 | + | multi_ack: bool, | |
| 541 | + | multi_ack_detailed: bool, | |
| 542 | + | } | |
| 543 | + | ||
| 544 | + | fn generate_pack_sync( | |
| 545 | + | repo_path: &Path, | |
| 546 | + | wants: &[gix::ObjectId], | |
| 547 | + | haves: &[gix::ObjectId], | |
| 548 | + | object_ids: Option<Vec<gix::ObjectId>>, | |
| 549 | + | options: GeneratePackOptions, | |
| 550 | + | tx: &tokio::sync::mpsc::Sender<std::result::Result<Bytes, std::io::Error>>, | |
| 551 | + | ) -> std::result::Result<(), Box<dyn std::error::Error + Send + Sync>> { | |
| 552 | + | const MAX_DELTA_BASES: usize = 8; | |
| 553 | + | const MIN_DELTA_BLOB_SIZE: usize = 1024; | |
| 554 | + | ||
| 555 | + | let repo = gix::open(repo_path)?; | |
| 556 | + | ||
| 557 | + | let common = if !haves.is_empty() { | |
| 558 | + | common_haves(&repo, wants, haves)? | |
| 559 | + | } else { | |
| 560 | + | Vec::new() | |
| 561 | + | }; | |
| 562 | + | ||
| 563 | + | if options.multi_ack && !haves.is_empty() && !options.done { | |
| 564 | + | for oid in &common { | |
| 565 | + | let suffix = if options.multi_ack_detailed { | |
| 566 | + | "common" | |
| 567 | + | } else { | |
| 568 | + | "continue" | |
| 569 | + | }; | |
| 570 | + | send(tx, &encode_ack_line(*oid, Some(suffix)))?; | |
| 571 | + | } | |
| 572 | + | send(tx, &pktline::encode(b"NAK\n"))?; | |
| 573 | + | return Ok(()); | |
| 574 | + | } | |
| 575 | + | ||
| 576 | + | if options.multi_ack && !common.is_empty() { | |
| 577 | + | send(tx, &encode_ack_line(*common.last().unwrap(), None))?; | |
| 578 | + | } else { | |
| 579 | + | // NAK line | |
| 580 | + | send(tx, &pktline::encode(b"NAK\n"))?; | |
| 581 | + | } | |
| 582 | + | ||
| 583 | + | // Pass 1: collect OIDs only | |
| 584 | + | let oids = match object_ids { | |
| 585 | + | Some(oids) => oids, | |
| 586 | + | None => collect_all_oids(&repo, wants, haves)?, | |
| 587 | + | }; | |
| 588 | + | ||
| 589 | + | // Pass 2: stream each object | |
| 590 | + | let mut hasher = Sha1::new(); | |
| 591 | + | ||
| 592 | + | // Pack header | |
| 593 | + | let mut header = Vec::with_capacity(12); | |
| 594 | + | header.extend_from_slice(b"PACK"); | |
| 595 | + | header.extend_from_slice(&2u32.to_be_bytes()); | |
| 596 | + | header.extend_from_slice(&(oids.len() as u32).to_be_bytes()); | |
| 597 | + | hasher.update(&header); | |
| 598 | + | send_sideband(tx, &header)?; | |
| 599 | + | ||
| 600 | + | let mut pack_offset = header.len() as u64; | |
| 601 | + | let mut recent_blob_bases = Vec::<BlobDeltaBase>::new(); | |
| 602 | + | ||
| 603 | + | // Each object: read, compress, frame, send | |
| 604 | + | for oid in &oids { | |
| 605 | + | let obj = repo.find_object(*oid)?; | |
| 606 | + | let full_entry = build_base_entry(obj.kind, &obj.data); | |
| 607 | + | let mut used_delta = false; | |
| 608 | + | let entry = if options.ofs_delta | |
| 609 | + | && obj.kind == gix::object::Kind::Blob | |
| 610 | + | && obj.data.len() >= MIN_DELTA_BLOB_SIZE | |
| 611 | + | { | |
| 612 | + | recent_blob_bases | |
| 613 | + | .iter() | |
| 614 | + | .filter(|base| base.data.len() >= MIN_DELTA_BLOB_SIZE) | |
| 615 | + | .filter_map(|base| { | |
| 616 | + | build_ofs_delta_entry(pack_offset, base.pack_offset, &base.data, &obj.data) | |
| 617 | + | }) | |
| 618 | + | .min_by_key(Vec::len) | |
| 619 | + | .filter(|delta_entry| delta_entry.len() < full_entry.len()) | |
| 620 | + | .inspect(|_| { | |
| 621 | + | used_delta = true; | |
| 622 | + | }) | |
| 623 | + | .unwrap_or(full_entry) | |
| 624 | + | } else { | |
| 625 | + | full_entry | |
| 626 | + | }; | |
| 627 | + | ||
| 628 | + | hasher.update(&entry); | |
| 629 | + | send_sideband(tx, &entry)?; | |
| 630 | + | ||
| 631 | + | if obj.kind == gix::object::Kind::Blob | |
| 632 | + | && !used_delta | |
| 633 | + | && obj.data.len() >= MIN_DELTA_BLOB_SIZE | |
| 634 | + | { | |
| 635 | + | recent_blob_bases.push(BlobDeltaBase { | |
| 636 | + | pack_offset, | |
| 637 | + | data: obj.data.to_vec(), | |
| 638 | + | }); | |
| 639 | + | if recent_blob_bases.len() > MAX_DELTA_BASES { | |
| 640 | + | recent_blob_bases.remove(0); | |
| 641 | + | } | |
| 642 | + | } | |
| 643 | + | ||
| 644 | + | pack_offset += entry.len() as u64; | |
| 645 | + | } | |
| 646 | + | ||
| 647 | + | // SHA-1 checksum over raw pack bytes | |
| 648 | + | let checksum = hasher.finalize(); | |
| 649 | + | send_sideband(tx, &checksum)?; | |
| 650 | + | ||
| 651 | + | // Flush | |
| 652 | + | send(tx, b"0000")?; | |
| 653 | + | ||
| 654 | + | Ok(()) | |
| 655 | + | } | |
| 656 | + | ||
| 657 | + | #[cfg(test)] | |
| 658 | + | mod tests { | |
| 659 | + | use std::path::{ | |
| 660 | + | Path, | |
| 661 | + | PathBuf, | |
| 662 | + | }; | |
| 663 | + | use std::process::Command; | |
| 664 | + | ||
| 665 | + | use tempfile::TempDir; | |
| 666 | + | use tokio::io::AsyncReadExt; | |
| 667 | + | ||
| 668 | + | use super::*; | |
| 669 | + | ||
| 670 | + | fn make_pktline(data: &str) -> Vec<u8> { | |
| 671 | + | let len = data.len() + 4; | |
| 672 | + | format!("{len:04x}{data}").into_bytes() | |
| 673 | + | } | |
| 674 | + | ||
| 675 | + | /// Create a bare repo with a single commit on the `main` branch. | |
| 676 | + | fn create_repo_with_commit(root: &Path) -> PathBuf { | |
| 677 | + | let bare_path = root.join("test.git"); | |
| 678 | + | let clone_path = root.join("workdir"); | |
| 679 | + | ||
| 680 | + | let out = Command::new("git") | |
| 681 | + | .args(["init", "--bare", bare_path.to_str().unwrap()]) | |
| 682 | + | .output() | |
| 683 | + | .expect("git init --bare failed"); | |
| 684 | + | assert!(out.status.success(), "git init --bare failed: {:?}", out); | |
| 685 | + | ||
| 686 | + | let out = Command::new("git") | |
| 687 | + | .args(["symbolic-ref", "HEAD", "refs/heads/main"]) | |
| 688 | + | .current_dir(&bare_path) | |
| 689 | + | .output() | |
| 690 | + | .expect("git symbolic-ref failed"); | |
| 691 | + | assert!(out.status.success()); | |
| 692 | + | ||
| 693 | + | let out = Command::new("git") | |
| 694 | + | .args([ | |
| 695 | + | "clone", | |
| 696 | + | bare_path.to_str().unwrap(), | |
| 697 | + | clone_path.to_str().unwrap(), | |
| 698 | + | ]) | |
| 699 | + | .output() | |
| 700 | + | .expect("git clone failed"); | |
| 701 | + | assert!(out.status.success(), "git clone failed: {:?}", out); | |
| 702 | + | ||
| 703 | + | for (key, val) in [("user.name", "Test User"), ("user.email", "test@test.com")] { | |
| 704 | + | Command::new("git") | |
| 705 | + | .args(["config", key, val]) | |
| 706 | + | .current_dir(&clone_path) | |
| 707 | + | .output() | |
| 708 | + | .expect("git config failed"); | |
| 709 | + | } | |
| 710 | + | ||
| 711 | + | // Create a file and commit | |
| 712 | + | std::fs::write(clone_path.join("README.md"), "# Test\n").unwrap(); | |
| 713 | + | ||
| 714 | + | Command::new("git") | |
| 715 | + | .args(["add", "README.md"]) | |
| 716 | + | .current_dir(&clone_path) | |
| 717 | + | .output() | |
| 718 | + | .expect("git add failed"); | |
| 719 | + | ||
| 720 | + | let out = Command::new("git") | |
| 721 | + | .args(["commit", "-m", "initial commit"]) | |
| 722 | + | .current_dir(&clone_path) | |
| 723 | + | .env("GIT_AUTHOR_NAME", "Test User") | |
| 724 | + | .env("GIT_AUTHOR_EMAIL", "test@test.com") | |
| 725 | + | .env("GIT_COMMITTER_NAME", "Test User") | |
| 726 | + | .env("GIT_COMMITTER_EMAIL", "test@test.com") | |
| 727 | + | .output() | |
| 728 | + | .expect("git commit failed"); | |
| 729 | + | assert!(out.status.success(), "git commit failed: {:?}", out); | |
| 730 | + | ||
| 731 | + | let out = Command::new("git") | |
| 732 | + | .args(["push", "origin", "main"]) | |
| 733 | + | .current_dir(&clone_path) | |
| 734 | + | .output() | |
| 735 | + | .expect("git push failed"); | |
| 736 | + | assert!(out.status.success(), "git push failed: {:?}", out); | |
| 737 | + | ||
| 738 | + | bare_path | |
| 739 | + | } | |
| 740 | + | ||
| 741 | + | #[test] | |
| 742 | + | fn parse_simple_want() { | |
| 743 | + | let hash = "0000000000000000000000000000000000000001"; | |
| 744 | + | let mut body = make_pktline(&format!("want {hash}\n")); | |
| 745 | + | body.extend_from_slice(b"00000009done\n"); | |
| 746 | + | let req = UploadPackRequest::parse(&body).unwrap(); | |
| 747 | + | assert_eq!(req.wants.len(), 1); | |
| 748 | + | assert!(req.haves.is_empty()); | |
| 749 | + | assert!(req.done); | |
| 750 | + | assert!(!req.capabilities.ofs_delta); | |
| 751 | + | assert_eq!(req.shallow.depth, None); | |
| 752 | + | } | |
| 753 | + | ||
| 754 | + | #[test] | |
| 755 | + | fn parse_wants_and_haves() { | |
| 756 | + | let want = "0000000000000000000000000000000000000001"; | |
| 757 | + | let have = "0000000000000000000000000000000000000002"; | |
| 758 | + | let mut body = make_pktline(&format!("want {want}\n")); | |
| 759 | + | body.extend_from_slice(b"0000"); | |
| 760 | + | body.extend_from_slice(&make_pktline(&format!("have {have}\n"))); | |
| 761 | + | body.extend_from_slice(b"0009done\n"); | |
| 762 | + | let req = UploadPackRequest::parse(&body).unwrap(); | |
| 763 | + | assert_eq!(req.wants.len(), 1); | |
| 764 | + | assert_eq!(req.haves.len(), 1); | |
| 765 | + | assert!(req.done); | |
| 766 | + | assert!(!req.capabilities.ofs_delta); | |
| 767 | + | assert!(req.shallow.client_shallows.is_empty()); | |
| 768 | + | } | |
| 769 | + | ||
| 770 | + | #[test] | |
| 771 | + | fn parse_ofs_delta_capability() { | |
| 772 | + | let hash = "0000000000000000000000000000000000000001"; | |
| 773 | + | let mut body = make_pktline(&format!("want {hash} side-band-64k ofs-delta\n")); | |
| 774 | + | body.extend_from_slice(b"0009done\n"); | |
| 775 | + | let req = UploadPackRequest::parse(&body).unwrap(); | |
| 776 | + | assert!(req.capabilities.ofs_delta); | |
| 777 | + | } | |
| 778 | + | ||
| 779 | + | #[test] | |
| 780 | + | fn parse_multi_ack_capability() { | |
| 781 | + | let hash = "0000000000000000000000000000000000000001"; | |
| 782 | + | let mut body = make_pktline(&format!("want {hash} multi_ack side-band-64k\n")); | |
| 783 | + | body.extend_from_slice(b"0009done\n"); | |
| 784 | + | let req = UploadPackRequest::parse(&body).unwrap(); | |
| 785 | + | assert!(req.capabilities.multi_ack); | |
| 786 | + | } | |
| 787 | + | ||
| 788 | + | #[test] | |
| 789 | + | fn parse_multi_ack_detailed_capability() { | |
| 790 | + | let hash = "0000000000000000000000000000000000000001"; | |
| 791 | + | let mut body = make_pktline(&format!("want {hash} multi_ack_detailed side-band-64k\n")); | |
| 792 | + | body.extend_from_slice(b"0009done\n"); | |
| 793 | + | let req = UploadPackRequest::parse(&body).unwrap(); | |
| 794 | + | assert!(req.capabilities.multi_ack); | |
| 795 | + | assert!(req.capabilities.multi_ack_detailed); | |
| 796 | + | } | |
| 797 | + | ||
| 798 | + | #[test] | |
| 799 | + | fn parse_shallow_request() { | |
| 800 | + | let hash = "0000000000000000000000000000000000000001"; | |
| 801 | + | let mut body = make_pktline(&format!("want {hash}\n")); | |
| 802 | + | body.extend_from_slice(&make_pktline("deepen 2\n")); | |
| 803 | + | body.extend_from_slice(&make_pktline(&format!("shallow {hash}\n"))); | |
| 804 | + | body.extend_from_slice(&make_pktline("deepen-relative\n")); | |
| 805 | + | body.extend_from_slice(b"0009done\n"); | |
| 806 | + | let req = UploadPackRequest::parse(&body).unwrap(); | |
| 807 | + | assert_eq!(req.shallow.depth, Some(2)); | |
| 808 | + | assert_eq!( | |
| 809 | + | req.shallow.client_shallows, | |
| 810 | + | vec![gix::ObjectId::from_hex(hash.as_bytes()).unwrap()] | |
| 811 | + | ); | |
| 812 | + | assert!(req.shallow.deepen_relative); | |
| 813 | + | } | |
| 814 | + | ||
| 815 | + | #[tokio::test] | |
| 816 | + | async fn generate_pack_for_clone() { | |
| 817 | + | let dir = TempDir::new().unwrap(); | |
| 818 | + | let repo_path = create_repo_with_commit(dir.path()); | |
| 819 | + | ||
| 820 | + | // Get HEAD OID | |
| 821 | + | let repo = gix::open(&repo_path).unwrap(); | |
| 822 | + | let head_oid = repo.head_id().unwrap().detach(); | |
| 823 | + | drop(repo); | |
| 824 | + | ||
| 825 | + | let request = UploadPackRequest { | |
| 826 | + | wants: vec![head_oid], | |
| 827 | + | haves: vec![], | |
| 828 | + | done: true, | |
| 829 | + | capabilities: UploadPackCapabilities::default(), | |
| 830 | + | shallow: ShallowRequest::default(), | |
| 831 | + | object_ids: None, | |
| 832 | + | }; | |
| 833 | + | ||
| 834 | + | let mut reader = generate_pack(&repo_path, &request).unwrap(); | |
| 835 | + | let mut buf = Vec::new(); | |
| 836 | + | reader.read_to_end(&mut buf).await.unwrap(); | |
| 837 | + | ||
| 838 | + | let response = String::from_utf8_lossy(&buf); | |
| 839 | + | assert!( | |
| 840 | + | response.contains("NAK"), | |
| 841 | + | "response should contain NAK: {response:?}" | |
| 842 | + | ); | |
| 843 | + | ||
| 844 | + | // Find PACK signature in the binary response | |
| 845 | + | let pack_found = buf.windows(4).any(|window| window == b"PACK"); | |
| 846 | + | assert!(pack_found, "response should contain PACK signature"); | |
| 847 | + | } | |
| 848 | + | } |
addedvendor/gitserver-core/src/path.rs+103 −0
| 1 | + | use std::path::{ | |
| 2 | + | Component, | |
| 3 | + | Path, | |
| 4 | + | PathBuf, | |
| 5 | + | }; | |
| 6 | + | ||
| 7 | + | use crate::error::{ | |
| 8 | + | Error, | |
| 9 | + | Result, | |
| 10 | + | }; | |
| 11 | + | ||
| 12 | + | /// Normalize a path by resolving `.` and `..` components lexically, | |
| 13 | + | /// without touching the filesystem. | |
| 14 | + | fn normalize(path: &Path) -> PathBuf { | |
| 15 | + | let mut out = PathBuf::new(); | |
| 16 | + | for component in path.components() { | |
| 17 | + | match component { | |
| 18 | + | Component::ParentDir => { | |
| 19 | + | out.pop(); | |
| 20 | + | } | |
| 21 | + | Component::CurDir => {} | |
| 22 | + | c => out.push(c), | |
| 23 | + | } | |
| 24 | + | } | |
| 25 | + | out | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | /// Resolve a relative repo path against a root directory. | |
| 29 | + | /// Returns the canonical absolute path if it is within root. | |
| 30 | + | pub fn resolve_repo_path(root: &Path, relative: &str) -> Result<PathBuf> { | |
| 31 | + | let candidate = root.join(relative); | |
| 32 | + | ||
| 33 | + | let canonical_root = root | |
| 34 | + | .canonicalize() | |
| 35 | + | .map_err(|_| Error::RepoNotFound(relative.to_string()))?; | |
| 36 | + | ||
| 37 | + | // Lexically normalize the candidate to detect traversal before hitting the filesystem. | |
| 38 | + | let normalized = normalize(&canonical_root.join(relative)); | |
| 39 | + | if !normalized.starts_with(&canonical_root) { | |
| 40 | + | return Err(Error::PathTraversal(candidate)); | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | let canonical = candidate | |
| 44 | + | .canonicalize() | |
| 45 | + | .map_err(|_| Error::RepoNotFound(relative.to_string()))?; | |
| 46 | + | ||
| 47 | + | if !canonical.starts_with(&canonical_root) { | |
| 48 | + | return Err(Error::PathTraversal(candidate)); | |
| 49 | + | } | |
| 50 | + | ||
| 51 | + | Ok(canonical) | |
| 52 | + | } | |
| 53 | + | ||
| 54 | + | #[cfg(test)] | |
| 55 | + | mod tests { | |
| 56 | + | use tempfile::TempDir; | |
| 57 | + | ||
| 58 | + | use super::*; | |
| 59 | + | ||
| 60 | + | #[test] | |
| 61 | + | fn resolve_simple_path() { | |
| 62 | + | let root = TempDir::new().unwrap(); | |
| 63 | + | let repo_dir = root.path().join("myrepo.git"); | |
| 64 | + | std::fs::create_dir(&repo_dir).unwrap(); | |
| 65 | + | ||
| 66 | + | let resolved = resolve_repo_path(root.path(), "myrepo.git").unwrap(); | |
| 67 | + | assert_eq!(resolved, repo_dir.canonicalize().unwrap()); | |
| 68 | + | } | |
| 69 | + | ||
| 70 | + | #[test] | |
| 71 | + | fn resolve_nested_path() { | |
| 72 | + | let root = TempDir::new().unwrap(); | |
| 73 | + | let repo_dir = root.path().join("org/project.git"); | |
| 74 | + | std::fs::create_dir_all(&repo_dir).unwrap(); | |
| 75 | + | ||
| 76 | + | let resolved = resolve_repo_path(root.path(), "org/project.git").unwrap(); | |
| 77 | + | assert_eq!(resolved, repo_dir.canonicalize().unwrap()); | |
| 78 | + | } | |
| 79 | + | ||
| 80 | + | #[test] | |
| 81 | + | fn reject_traversal() { | |
| 82 | + | let root = TempDir::new().unwrap(); | |
| 83 | + | let err = resolve_repo_path(root.path(), "../etc/passwd").unwrap_err(); | |
| 84 | + | assert!(matches!(err, Error::PathTraversal(_))); | |
| 85 | + | } | |
| 86 | + | ||
| 87 | + | #[test] | |
| 88 | + | fn reject_traversal_in_middle() { | |
| 89 | + | let root = TempDir::new().unwrap(); | |
| 90 | + | let repo_dir = root.path().join("legit"); | |
| 91 | + | std::fs::create_dir(&repo_dir).unwrap(); | |
| 92 | + | ||
| 93 | + | let err = resolve_repo_path(root.path(), "legit/../../etc/passwd").unwrap_err(); | |
| 94 | + | assert!(matches!(err, Error::PathTraversal(_))); | |
| 95 | + | } | |
| 96 | + | ||
| 97 | + | #[test] | |
| 98 | + | fn reject_nonexistent_path() { | |
| 99 | + | let root = TempDir::new().unwrap(); | |
| 100 | + | let err = resolve_repo_path(root.path(), "nonexistent.git").unwrap_err(); | |
| 101 | + | assert!(matches!(err, Error::RepoNotFound(_))); | |
| 102 | + | } | |
| 103 | + | } |
addedvendor/gitserver-core/src/pktline.rs+58 −0
| 1 | + | //! pkt-line encoding as defined by Git protocol. | |
| 2 | + | //! Format: 4-hex-digit length prefix (includes the 4 bytes) + content. | |
| 3 | + | //! Flush packet: "0000" | |
| 4 | + | ||
| 5 | + | /// Encode a single pkt-line. | |
| 6 | + | pub fn encode(data: &[u8]) -> Vec<u8> { | |
| 7 | + | let len = data.len() + 4; | |
| 8 | + | let mut buf = format!("{len:04x}").into_bytes(); | |
| 9 | + | buf.extend_from_slice(data); | |
| 10 | + | buf | |
| 11 | + | } | |
| 12 | + | ||
| 13 | + | /// Encode a flush packet. | |
| 14 | + | pub fn flush() -> &'static [u8] { | |
| 15 | + | b"0000" | |
| 16 | + | } | |
| 17 | + | ||
| 18 | + | /// Encode a comment line (e.g. "# service=git-upload-pack\n"). | |
| 19 | + | pub fn encode_comment(comment: &str) -> Vec<u8> { | |
| 20 | + | encode(format!("# {comment}\n").as_bytes()) | |
| 21 | + | } | |
| 22 | + | ||
| 23 | + | #[cfg(test)] | |
| 24 | + | mod tests { | |
| 25 | + | use super::*; | |
| 26 | + | ||
| 27 | + | #[test] | |
| 28 | + | fn encode_simple_line() { | |
| 29 | + | let encoded = encode(b"hello\n"); | |
| 30 | + | assert_eq!(&encoded, b"000ahello\n"); | |
| 31 | + | } | |
| 32 | + | ||
| 33 | + | #[test] | |
| 34 | + | fn encode_empty_content() { | |
| 35 | + | let encoded = encode(b""); | |
| 36 | + | assert_eq!(&encoded, b"0004"); | |
| 37 | + | } | |
| 38 | + | ||
| 39 | + | #[test] | |
| 40 | + | fn encode_comment_line() { | |
| 41 | + | let encoded = encode_comment("service=git-upload-pack"); | |
| 42 | + | assert_eq!(&encoded, b"001e# service=git-upload-pack\n"); | |
| 43 | + | } | |
| 44 | + | ||
| 45 | + | #[test] | |
| 46 | + | fn flush_packet() { | |
| 47 | + | assert_eq!(flush(), b"0000"); | |
| 48 | + | } | |
| 49 | + | ||
| 50 | + | #[test] | |
| 51 | + | fn encode_ref_line() { | |
| 52 | + | let hash = "0000000000000000000000000000000000000000"; | |
| 53 | + | let line = format!("{hash} refs/heads/main\n"); | |
| 54 | + | let encoded = encode(line.as_bytes()); | |
| 55 | + | // 4 (prefix) + 40 (hash) + 1 (space) + 15 (refs/heads/main) + 1 (\n) = 61 = 0x3d | |
| 56 | + | assert_eq!(&encoded[..4], b"003d"); | |
| 57 | + | } | |
| 58 | + | } |
addedvendor/gitserver-core/src/protocol_v2.rs+752 −0
| 1 | + | // This Source Code Form is subject to the terms of the Mozilla Public | |
| 2 | + | // License, v. 2.0. If a copy of the MPL was not distributed with this | |
| 3 | + | // file, You can obtain one at https://mozilla.org/MPL/2.0/. | |
| 4 | + | // | |
| 5 | + | // Copyright (c) 2026 WJQSERVER | |
| 6 | + | ||
| 7 | + | use std::collections::BTreeSet; | |
| 8 | + | use std::collections::HashSet; | |
| 9 | + | use std::io::Cursor; | |
| 10 | + | use std::path::Path; | |
| 11 | + | ||
| 12 | + | use crate::error::{ | |
| 13 | + | Error, | |
| 14 | + | Result, | |
| 15 | + | }; | |
| 16 | + | use crate::pack::UploadPackRequest; | |
| 17 | + | use crate::pktline; | |
| 18 | + | ||
| 19 | + | const CAPABILITIES: &[&str] = &[ | |
| 20 | + | "ls-refs=unborn", | |
| 21 | + | "fetch=shallow wait-for-done", | |
| 22 | + | "object-format=sha1", | |
| 23 | + | ]; | |
| 24 | + | ||
| 25 | + | pub enum Command { | |
| 26 | + | LsRefs(LsRefsRequest), | |
| 27 | + | Fetch(FetchRequest), | |
| 28 | + | } | |
| 29 | + | ||
| 30 | + | #[derive(Default)] | |
| 31 | + | pub struct LsRefsRequest { | |
| 32 | + | pub peel: bool, | |
| 33 | + | pub symrefs: bool, | |
| 34 | + | pub unborn: bool, | |
| 35 | + | pub ref_prefixes: Vec<String>, | |
| 36 | + | } | |
| 37 | + | ||
| 38 | + | pub struct FetchRequest { | |
| 39 | + | pub upload_request: UploadPackRequest, | |
| 40 | + | } | |
| 41 | + | ||
| 42 | + | pub struct ShallowUpdate { | |
| 43 | + | pub shallow: Vec<gix::ObjectId>, | |
| 44 | + | pub unshallow: Vec<gix::ObjectId>, | |
| 45 | + | } | |
| 46 | + | ||
| 47 | + | pub fn advertise_capabilities() -> Vec<u8> { | |
| 48 | + | let mut out = Vec::new(); | |
| 49 | + | out.extend_from_slice(&pktline::encode_comment("service=git-upload-pack")); | |
| 50 | + | out.extend_from_slice(pktline::flush()); | |
| 51 | + | out.extend_from_slice(&pktline::encode(b"version 2\n")); | |
| 52 | + | for capability in CAPABILITIES { | |
| 53 | + | out.extend_from_slice(&pktline::encode(format!("{capability}\n").as_bytes())); | |
| 54 | + | } | |
| 55 | + | out.extend_from_slice(pktline::flush()); | |
| 56 | + | out | |
| 57 | + | } | |
| 58 | + | ||
| 59 | + | pub fn parse_command_request(body: &[u8]) -> Result<Command> { | |
| 60 | + | let lines = decode_pkt_lines(body)?; | |
| 61 | + | let mut iter = lines.into_iter(); | |
| 62 | + | ||
| 63 | + | let command = iter | |
| 64 | + | .next() | |
| 65 | + | .ok_or_else(|| Error::Protocol("missing protocol v2 command".into()))?; | |
| 66 | + | let command = command | |
| 67 | + | .strip_prefix("command=") | |
| 68 | + | .ok_or_else(|| Error::Protocol("invalid protocol v2 command line".into()))?; | |
| 69 | + | ||
| 70 | + | let mut args = Vec::new(); | |
| 71 | + | let mut saw_delim = false; | |
| 72 | + | for line in iter { | |
| 73 | + | if line.is_empty() { | |
| 74 | + | continue; | |
| 75 | + | } | |
| 76 | + | if line == "0001" { | |
| 77 | + | saw_delim = true; | |
| 78 | + | continue; | |
| 79 | + | } | |
| 80 | + | if saw_delim { | |
| 81 | + | args.push(line); | |
| 82 | + | } | |
| 83 | + | } | |
| 84 | + | ||
| 85 | + | match command { | |
| 86 | + | "ls-refs" => parse_ls_refs(args), | |
| 87 | + | "fetch" => parse_fetch(args), | |
| 88 | + | _ => Err(Error::Protocol(format!( | |
| 89 | + | "unsupported protocol v2 command: {command}" | |
| 90 | + | ))), | |
| 91 | + | } | |
| 92 | + | } | |
| 93 | + | ||
| 94 | + | pub fn ls_refs(repo_path: &Path, request: &LsRefsRequest) -> Result<Vec<u8>> { | |
| 95 | + | let repo = gix::open(repo_path)?; | |
| 96 | + | let mut refs = BTreeSet::new(); | |
| 97 | + | ||
| 98 | + | if let Ok(mut head) = repo.head() { | |
| 99 | + | if let Some(id) = head | |
| 100 | + | .try_peel_to_id() | |
| 101 | + | .map_err(|e| Error::Protocol(e.to_string()))? | |
| 102 | + | { | |
| 103 | + | let mut line = format!("{} HEAD", id.detach()); | |
| 104 | + | if request.symrefs | |
| 105 | + | && let Some(target) = head.referent_name() | |
| 106 | + | { | |
| 107 | + | line.push_str(&format!(" symref-target:{}", target.as_bstr())); | |
| 108 | + | } | |
| 109 | + | refs.insert(line); | |
| 110 | + | } else if request.unborn | |
| 111 | + | && let Some(target) = head.referent_name() | |
| 112 | + | { | |
| 113 | + | refs.insert(format!("unborn HEAD symref-target:{}", target.as_bstr())); | |
| 114 | + | } | |
| 115 | + | } | |
| 116 | + | ||
| 117 | + | if let Ok(platform) = repo.references() | |
| 118 | + | && let Ok(iter) = platform.all() | |
| 119 | + | { | |
| 120 | + | for mut reference in iter.flatten() { | |
| 121 | + | let name = reference.name().as_bstr().to_string(); | |
| 122 | + | if !request.ref_prefixes.is_empty() | |
| 123 | + | && !request | |
| 124 | + | .ref_prefixes | |
| 125 | + | .iter() | |
| 126 | + | .any(|prefix| name.starts_with(prefix)) | |
| 127 | + | { | |
| 128 | + | continue; | |
| 129 | + | } | |
| 130 | + | ||
| 131 | + | let mut line = match reference.try_id() { | |
| 132 | + | Some(id) => format!("{} {name}", id.detach()), | |
| 133 | + | None => match reference.peel_to_id() { | |
| 134 | + | Ok(id) => format!("{} {name}", id.detach()), | |
| 135 | + | Err(_) => continue, | |
| 136 | + | }, | |
| 137 | + | }; | |
| 138 | + | ||
| 139 | + | if request.symrefs | |
| 140 | + | && let Some(target) = reference.target().try_name() | |
| 141 | + | { | |
| 142 | + | line.push_str(&format!(" symref-target:{}", target.as_bstr())); | |
| 143 | + | } | |
| 144 | + | ||
| 145 | + | if request.peel | |
| 146 | + | && let Ok(peeled) = reference.peel_to_id() | |
| 147 | + | { | |
| 148 | + | line.push_str(&format!(" peeled:{}", peeled.detach())); | |
| 149 | + | } | |
| 150 | + | ||
| 151 | + | refs.insert(line); | |
| 152 | + | } | |
| 153 | + | } | |
| 154 | + | ||
| 155 | + | let mut out = Vec::new(); | |
| 156 | + | for line in refs { | |
| 157 | + | out.extend_from_slice(&pktline::encode(format!("{line}\n").as_bytes())); | |
| 158 | + | } | |
| 159 | + | out.extend_from_slice(pktline::flush()); | |
| 160 | + | Ok(out) | |
| 161 | + | } | |
| 162 | + | ||
| 163 | + | pub fn encode_fetch_pack_response(pack_bytes: &[u8]) -> Vec<u8> { | |
| 164 | + | let mut out = Vec::new(); | |
| 165 | + | out.extend_from_slice(&pktline::encode(b"packfile\n")); | |
| 166 | + | ||
| 167 | + | let mut pos = 0; | |
| 168 | + | while pos + 4 <= pack_bytes.len() { | |
| 169 | + | let len_str = match std::str::from_utf8(&pack_bytes[pos..pos + 4]) { | |
| 170 | + | Ok(v) => v, | |
| 171 | + | Err(_) => break, | |
| 172 | + | }; | |
| 173 | + | pos += 4; | |
| 174 | + | ||
| 175 | + | if len_str == "0000" { | |
| 176 | + | out.extend_from_slice(b"0000"); | |
| 177 | + | break; | |
| 178 | + | } | |
| 179 | + | ||
| 180 | + | let len = match usize::from_str_radix(len_str, 16) { | |
| 181 | + | Ok(v) if v >= 4 && pos + (v - 4) <= pack_bytes.len() => v, | |
| 182 | + | _ => break, | |
| 183 | + | }; | |
| 184 | + | ||
| 185 | + | let frame = &pack_bytes[pos - 4..pos + (len - 4)]; | |
| 186 | + | let payload = &pack_bytes[pos..pos + (len - 4)]; | |
| 187 | + | pos += len - 4; | |
| 188 | + | ||
| 189 | + | if payload.starts_with(&[0x01]) | |
| 190 | + | || payload.starts_with(&[0x02]) | |
| 191 | + | || payload.starts_with(&[0x03]) | |
| 192 | + | { | |
| 193 | + | out.extend_from_slice(frame); | |
| 194 | + | } | |
| 195 | + | } | |
| 196 | + | ||
| 197 | + | out | |
| 198 | + | } | |
| 199 | + | ||
| 200 | + | pub struct PrefixThenReader<R> { | |
| 201 | + | prefix: Cursor<Vec<u8>>, | |
| 202 | + | reader: R, | |
| 203 | + | } | |
| 204 | + | ||
| 205 | + | impl<R> PrefixThenReader<R> { | |
| 206 | + | pub fn new(prefix: Vec<u8>, reader: R) -> Self { | |
| 207 | + | Self { | |
| 208 | + | prefix: Cursor::new(prefix), | |
| 209 | + | reader, | |
| 210 | + | } | |
| 211 | + | } | |
| 212 | + | } | |
| 213 | + | ||
| 214 | + | pub struct PackSectionReader<R> { | |
| 215 | + | reader: R, | |
| 216 | + | buf: Vec<u8>, | |
| 217 | + | out: Cursor<Vec<u8>>, | |
| 218 | + | finished: bool, | |
| 219 | + | } | |
| 220 | + | ||
| 221 | + | impl<R> PackSectionReader<R> { | |
| 222 | + | pub fn new(reader: R) -> Self { | |
| 223 | + | Self { | |
| 224 | + | reader, | |
| 225 | + | buf: Vec::new(), | |
| 226 | + | out: Cursor::new(Vec::new()), | |
| 227 | + | finished: false, | |
| 228 | + | } | |
| 229 | + | } | |
| 230 | + | } | |
| 231 | + | ||
| 232 | + | impl<R: tokio::io::AsyncRead + Unpin> tokio::io::AsyncRead for PackSectionReader<R> { | |
| 233 | + | fn poll_read( | |
| 234 | + | mut self: std::pin::Pin<&mut Self>, | |
| 235 | + | cx: &mut std::task::Context<'_>, | |
| 236 | + | buf: &mut tokio::io::ReadBuf<'_>, | |
| 237 | + | ) -> std::task::Poll<std::io::Result<()>> { | |
| 238 | + | loop { | |
| 239 | + | if (self.out.position() as usize) < self.out.get_ref().len() { | |
| 240 | + | let remaining = &self.out.get_ref()[self.out.position() as usize..]; | |
| 241 | + | let to_copy = remaining.len().min(buf.remaining()); | |
| 242 | + | buf.put_slice(&remaining[..to_copy]); | |
| 243 | + | let next = self.out.position() + to_copy as u64; | |
| 244 | + | self.out.set_position(next); | |
| 245 | + | return std::task::Poll::Ready(Ok(())); | |
| 246 | + | } | |
| 247 | + | ||
| 248 | + | if self.finished { | |
| 249 | + | return std::task::Poll::Ready(Ok(())); | |
| 250 | + | } | |
| 251 | + | ||
| 252 | + | let mut frame_buf = [0u8; 8192]; | |
| 253 | + | let mut read_buf = tokio::io::ReadBuf::new(&mut frame_buf); | |
| 254 | + | match std::pin::Pin::new(&mut self.reader).poll_read(cx, &mut read_buf) { | |
| 255 | + | std::task::Poll::Pending => return std::task::Poll::Pending, | |
| 256 | + | std::task::Poll::Ready(Err(err)) => return std::task::Poll::Ready(Err(err)), | |
| 257 | + | std::task::Poll::Ready(Ok(())) => { | |
| 258 | + | let filled = read_buf.filled(); | |
| 259 | + | if filled.is_empty() { | |
| 260 | + | self.finished = true; | |
| 261 | + | return std::task::Poll::Ready(Ok(())); | |
| 262 | + | } | |
| 263 | + | self.buf.extend_from_slice(filled); | |
| 264 | + | } | |
| 265 | + | } | |
| 266 | + | ||
| 267 | + | let mut emitted = Vec::new(); | |
| 268 | + | loop { | |
| 269 | + | if self.buf.len() < 4 { | |
| 270 | + | break; | |
| 271 | + | } | |
| 272 | + | let len_str = match std::str::from_utf8(&self.buf[..4]) { | |
| 273 | + | Ok(v) => v, | |
| 274 | + | Err(_) => { | |
| 275 | + | self.finished = true; | |
| 276 | + | return std::task::Poll::Ready(Err(std::io::Error::other( | |
| 277 | + | "invalid pkt-line prefix in pack response", | |
| 278 | + | ))); | |
| 279 | + | } | |
| 280 | + | }; | |
| 281 | + | ||
| 282 | + | if len_str == "0000" { | |
| 283 | + | emitted.extend_from_slice(b"0000"); | |
| 284 | + | self.buf.drain(..4); | |
| 285 | + | self.finished = true; | |
| 286 | + | break; | |
| 287 | + | } | |
| 288 | + | ||
| 289 | + | let len = match usize::from_str_radix(len_str, 16) { | |
| 290 | + | Ok(v) if v >= 4 => v, | |
| 291 | + | _ => { | |
| 292 | + | self.finished = true; | |
| 293 | + | return std::task::Poll::Ready(Err(std::io::Error::other( | |
| 294 | + | "invalid pkt-line length in pack response", | |
| 295 | + | ))); | |
| 296 | + | } | |
| 297 | + | }; | |
| 298 | + | ||
| 299 | + | if self.buf.len() < len { | |
| 300 | + | break; | |
| 301 | + | } | |
| 302 | + | ||
| 303 | + | let frame = self.buf[..len].to_vec(); | |
| 304 | + | let payload = &frame[4..]; | |
| 305 | + | if payload.starts_with(&[0x01]) | |
| 306 | + | || payload.starts_with(&[0x02]) | |
| 307 | + | || payload.starts_with(&[0x03]) | |
| 308 | + | { | |
| 309 | + | emitted.extend_from_slice(&frame); | |
| 310 | + | } | |
| 311 | + | self.buf.drain(..len); | |
| 312 | + | } | |
| 313 | + | ||
| 314 | + | if !emitted.is_empty() { | |
| 315 | + | self.out = Cursor::new(emitted); | |
| 316 | + | self.out.set_position(0); | |
| 317 | + | } | |
| 318 | + | } | |
| 319 | + | } | |
| 320 | + | } | |
| 321 | + | ||
| 322 | + | impl<R: tokio::io::AsyncRead + Unpin> tokio::io::AsyncRead for PrefixThenReader<R> { | |
| 323 | + | fn poll_read( | |
| 324 | + | mut self: std::pin::Pin<&mut Self>, | |
| 325 | + | cx: &mut std::task::Context<'_>, | |
| 326 | + | buf: &mut tokio::io::ReadBuf<'_>, | |
| 327 | + | ) -> std::task::Poll<std::io::Result<()>> { | |
| 328 | + | if (self.prefix.position() as usize) < self.prefix.get_ref().len() { | |
| 329 | + | let remaining = &self.prefix.get_ref()[self.prefix.position() as usize..]; | |
| 330 | + | let to_copy = remaining.len().min(buf.remaining()); | |
| 331 | + | buf.put_slice(&remaining[..to_copy]); | |
| 332 | + | let next = self.prefix.position() + to_copy as u64; | |
| 333 | + | self.prefix.set_position(next); | |
| 334 | + | return std::task::Poll::Ready(Ok(())); | |
| 335 | + | } | |
| 336 | + | ||
| 337 | + | std::pin::Pin::new(&mut self.reader).poll_read(cx, buf) | |
| 338 | + | } | |
| 339 | + | } | |
| 340 | + | ||
| 341 | + | pub fn encode_fetch_ready_and_acknowledgments(common: &[gix::ObjectId]) -> Vec<u8> { | |
| 342 | + | let mut out = encode_fetch_acknowledgments(common); | |
| 343 | + | if !common.is_empty() { | |
| 344 | + | out.truncate(out.len() - 4); | |
| 345 | + | out.extend_from_slice(&pktline::encode(b"ready\n")); | |
| 346 | + | out.extend_from_slice(b"0001"); | |
| 347 | + | } | |
| 348 | + | out | |
| 349 | + | } | |
| 350 | + | ||
| 351 | + | pub fn encode_fetch_acknowledgments(common: &[gix::ObjectId]) -> Vec<u8> { | |
| 352 | + | let mut out = Vec::new(); | |
| 353 | + | out.extend_from_slice(&pktline::encode(b"acknowledgments\n")); | |
| 354 | + | ||
| 355 | + | if common.is_empty() { | |
| 356 | + | out.extend_from_slice(&pktline::encode(b"NAK\n")); | |
| 357 | + | } else { | |
| 358 | + | for oid in common { | |
| 359 | + | out.extend_from_slice(&pktline::encode(format!("ACK {oid}\n").as_bytes())); | |
| 360 | + | } | |
| 361 | + | } | |
| 362 | + | ||
| 363 | + | out.extend_from_slice(pktline::flush()); | |
| 364 | + | out | |
| 365 | + | } | |
| 366 | + | ||
| 367 | + | pub fn encode_shallow_info(update: &ShallowUpdate) -> Vec<u8> { | |
| 368 | + | let mut out = Vec::new(); | |
| 369 | + | if update.shallow.is_empty() && update.unshallow.is_empty() { | |
| 370 | + | return out; | |
| 371 | + | } | |
| 372 | + | ||
| 373 | + | out.extend_from_slice(&pktline::encode(b"shallow-info\n")); | |
| 374 | + | for oid in &update.shallow { | |
| 375 | + | out.extend_from_slice(&pktline::encode(format!("shallow {oid}\n").as_bytes())); | |
| 376 | + | } | |
| 377 | + | for oid in &update.unshallow { | |
| 378 | + | out.extend_from_slice(&pktline::encode(format!("unshallow {oid}\n").as_bytes())); | |
| 379 | + | } | |
| 380 | + | out.extend_from_slice(b"0001"); | |
| 381 | + | out | |
| 382 | + | } | |
| 383 | + | ||
| 384 | + | pub fn common_haves(repo_path: &Path, request: &FetchRequest) -> Result<Vec<gix::ObjectId>> { | |
| 385 | + | let repo = gix::open(repo_path)?; | |
| 386 | + | let want_set: HashSet<gix::ObjectId> = | |
| 387 | + | collect_want_closure(&repo, &request.upload_request.wants)? | |
| 388 | + | .into_iter() | |
| 389 | + | .collect(); | |
| 390 | + | ||
| 391 | + | Ok(request | |
| 392 | + | .upload_request | |
| 393 | + | .haves | |
| 394 | + | .iter() | |
| 395 | + | .copied() | |
| 396 | + | .filter(|oid| want_set.contains(oid)) | |
| 397 | + | .collect()) | |
| 398 | + | } | |
| 399 | + | ||
| 400 | + | pub fn apply_shallow_boundaries( | |
| 401 | + | repo_path: &Path, | |
| 402 | + | request: &mut FetchRequest, | |
| 403 | + | ) -> Result<ShallowUpdate> { | |
| 404 | + | let Some(depth) = request.upload_request.shallow.depth else { | |
| 405 | + | return Ok(ShallowUpdate { | |
| 406 | + | shallow: Vec::new(), | |
| 407 | + | unshallow: Vec::new(), | |
| 408 | + | }); | |
| 409 | + | }; | |
| 410 | + | ||
| 411 | + | let repo = gix::open(repo_path)?; | |
| 412 | + | let previous_shallows = request.upload_request.shallow.client_shallows.clone(); | |
| 413 | + | let state = collect_depth_limited_commits(&repo, &request.upload_request, depth)?; | |
| 414 | + | ||
| 415 | + | request.upload_request.object_ids = Some(state.included_objects.clone()); | |
| 416 | + | request | |
| 417 | + | .upload_request | |
| 418 | + | .haves | |
| 419 | + | .extend(previous_shallows.iter().copied()); | |
| 420 | + | ||
| 421 | + | let next_shallows: HashSet<_> = state.shallow_boundary.iter().copied().collect(); | |
| 422 | + | let prev_shallows: HashSet<_> = previous_shallows.iter().copied().collect(); | |
| 423 | + | ||
| 424 | + | Ok(ShallowUpdate { | |
| 425 | + | shallow: state | |
| 426 | + | .shallow_boundary | |
| 427 | + | .iter() | |
| 428 | + | .copied() | |
| 429 | + | .filter(|oid| !prev_shallows.contains(oid)) | |
| 430 | + | .collect(), | |
| 431 | + | unshallow: previous_shallows | |
| 432 | + | .into_iter() | |
| 433 | + | .filter(|oid| !next_shallows.contains(oid)) | |
| 434 | + | .collect(), | |
| 435 | + | }) | |
| 436 | + | } | |
| 437 | + | ||
| 438 | + | struct DepthState { | |
| 439 | + | included_objects: Vec<gix::ObjectId>, | |
| 440 | + | shallow_boundary: Vec<gix::ObjectId>, | |
| 441 | + | } | |
| 442 | + | ||
| 443 | + | fn parse_ls_refs(args: Vec<String>) -> Result<Command> { | |
| 444 | + | let mut request = LsRefsRequest::default(); | |
| 445 | + | ||
| 446 | + | for arg in args { | |
| 447 | + | match arg.as_str() { | |
| 448 | + | "peel" => request.peel = true, | |
| 449 | + | "symrefs" => request.symrefs = true, | |
| 450 | + | "unborn" => request.unborn = true, | |
| 451 | + | _ => { | |
| 452 | + | if let Some(prefix) = arg.strip_prefix("ref-prefix ") { | |
| 453 | + | request.ref_prefixes.push(prefix.to_owned()); | |
| 454 | + | } else { | |
| 455 | + | return Err(Error::Protocol(format!( | |
| 456 | + | "unsupported ls-refs argument: {arg}" | |
| 457 | + | ))); | |
| 458 | + | } | |
| 459 | + | } | |
| 460 | + | } | |
| 461 | + | } | |
| 462 | + | ||
| 463 | + | Ok(Command::LsRefs(request)) | |
| 464 | + | } | |
| 465 | + | ||
| 466 | + | fn parse_fetch(args: Vec<String>) -> Result<Command> { | |
| 467 | + | let mut wants = Vec::new(); | |
| 468 | + | let mut haves = Vec::new(); | |
| 469 | + | let mut done = false; | |
| 470 | + | let mut capabilities = crate::pack::UploadPackCapabilities::default(); | |
| 471 | + | let mut shallow = crate::pack::ShallowRequest::default(); | |
| 472 | + | ||
| 473 | + | for arg in args { | |
| 474 | + | if arg == "done" { | |
| 475 | + | done = true; | |
| 476 | + | } else if arg == "ofs-delta" { | |
| 477 | + | capabilities.ofs_delta = true; | |
| 478 | + | } else if arg == "deepen-relative" { | |
| 479 | + | shallow.deepen_relative = true; | |
| 480 | + | } else if arg == "thin-pack" | |
| 481 | + | || arg == "no-progress" | |
| 482 | + | || arg == "include-tag" | |
| 483 | + | || arg == "wait-for-done" | |
| 484 | + | { | |
| 485 | + | continue; | |
| 486 | + | } else if let Some(depth) = arg.strip_prefix("deepen ") { | |
| 487 | + | shallow.depth = Some( | |
| 488 | + | depth | |
| 489 | + | .parse::<usize>() | |
| 490 | + | .map_err(|_| Error::Protocol(format!("invalid deepen value: {depth}")))?, | |
| 491 | + | ); | |
| 492 | + | } else if let Some(oid_hex) = arg.strip_prefix("shallow ") { | |
| 493 | + | let oid = gix::ObjectId::from_hex(oid_hex.as_bytes()) | |
| 494 | + | .map_err(|_| Error::Protocol(format!("invalid OID in shallow: {oid_hex}")))?; | |
| 495 | + | shallow.client_shallows.push(oid); | |
| 496 | + | } else if let Some(oid_hex) = arg.strip_prefix("want ") { | |
| 497 | + | let oid = gix::ObjectId::from_hex(oid_hex.as_bytes()) | |
| 498 | + | .map_err(|_| Error::Protocol(format!("invalid OID in want: {oid_hex}")))?; | |
| 499 | + | wants.push(oid); | |
| 500 | + | } else if let Some(oid_hex) = arg.strip_prefix("have ") { | |
| 501 | + | let oid = gix::ObjectId::from_hex(oid_hex.as_bytes()) | |
| 502 | + | .map_err(|_| Error::Protocol(format!("invalid OID in have: {oid_hex}")))?; | |
| 503 | + | haves.push(oid); | |
| 504 | + | } else { | |
| 505 | + | return Err(Error::Protocol(format!( | |
| 506 | + | "unsupported fetch argument: {arg}" | |
| 507 | + | ))); | |
| 508 | + | } | |
| 509 | + | } | |
| 510 | + | ||
| 511 | + | Ok(Command::Fetch(FetchRequest { | |
| 512 | + | upload_request: UploadPackRequest { | |
| 513 | + | wants, | |
| 514 | + | haves, | |
| 515 | + | done, | |
| 516 | + | capabilities, | |
| 517 | + | shallow, | |
| 518 | + | object_ids: None, | |
| 519 | + | }, | |
| 520 | + | })) | |
| 521 | + | } | |
| 522 | + | ||
| 523 | + | fn decode_pkt_lines(body: &[u8]) -> Result<Vec<String>> { | |
| 524 | + | let mut pos = 0; | |
| 525 | + | let mut out = Vec::new(); | |
| 526 | + | ||
| 527 | + | while pos < body.len() { | |
| 528 | + | if pos + 4 > body.len() { | |
| 529 | + | return Err(Error::Protocol("truncated pkt-line prefix".into())); | |
| 530 | + | } | |
| 531 | + | ||
| 532 | + | let len_str = std::str::from_utf8(&body[pos..pos + 4]) | |
| 533 | + | .map_err(|_| Error::Protocol("invalid pkt-line length prefix".into()))?; | |
| 534 | + | pos += 4; | |
| 535 | + | ||
| 536 | + | if len_str == "0000" { | |
| 537 | + | break; | |
| 538 | + | } | |
| 539 | + | if len_str == "0001" { | |
| 540 | + | out.push("0001".to_string()); | |
| 541 | + | continue; | |
| 542 | + | } | |
| 543 | + | ||
| 544 | + | let len = usize::from_str_radix(len_str, 16) | |
| 545 | + | .map_err(|_| Error::Protocol("invalid pkt-line length".into()))?; | |
| 546 | + | if len < 4 || pos + (len - 4) > body.len() { | |
| 547 | + | return Err(Error::Protocol("invalid pkt-line frame length".into())); | |
| 548 | + | } | |
| 549 | + | ||
| 550 | + | let payload = &body[pos..pos + (len - 4)]; | |
| 551 | + | pos += len - 4; | |
| 552 | + | let line = std::str::from_utf8(payload) | |
| 553 | + | .map_err(|_| Error::Protocol("invalid UTF-8 in pkt-line".into()))?; | |
| 554 | + | out.push(line.trim_end_matches('\n').to_owned()); | |
| 555 | + | } | |
| 556 | + | ||
| 557 | + | Ok(out) | |
| 558 | + | } | |
| 559 | + | ||
| 560 | + | fn collect_want_closure( | |
| 561 | + | repo: &gix::Repository, | |
| 562 | + | wants: &[gix::ObjectId], | |
| 563 | + | ) -> Result<Vec<gix::ObjectId>> { | |
| 564 | + | let mut seen = HashSet::new(); | |
| 565 | + | let mut out = Vec::new(); | |
| 566 | + | ||
| 567 | + | let walk = repo | |
| 568 | + | .rev_walk(wants.iter().copied()) | |
| 569 | + | .all() | |
| 570 | + | .map_err(|e| Error::Protocol(e.to_string()))?; | |
| 571 | + | for info_result in walk { | |
| 572 | + | let info = info_result.map_err(|e| Error::Protocol(e.to_string()))?; | |
| 573 | + | let commit_oid = info.id; | |
| 574 | + | if !seen.insert(commit_oid) { | |
| 575 | + | continue; | |
| 576 | + | } | |
| 577 | + | out.push(commit_oid); | |
| 578 | + | } | |
| 579 | + | ||
| 580 | + | Ok(out) | |
| 581 | + | } | |
| 582 | + | ||
| 583 | + | fn collect_depth_limited_commits( | |
| 584 | + | repo: &gix::Repository, | |
| 585 | + | request: &crate::pack::UploadPackRequest, | |
| 586 | + | depth: usize, | |
| 587 | + | ) -> Result<DepthState> { | |
| 588 | + | use std::collections::{ | |
| 589 | + | HashSet, | |
| 590 | + | VecDeque, | |
| 591 | + | }; | |
| 592 | + | ||
| 593 | + | let mut queue = VecDeque::new(); | |
| 594 | + | let mut seen = HashSet::new(); | |
| 595 | + | let mut included_commits = Vec::new(); | |
| 596 | + | let mut included_objects = Vec::new(); | |
| 597 | + | let mut shallow_boundary = Vec::new(); | |
| 598 | + | ||
| 599 | + | let base_depth = if request.shallow.deepen_relative { | |
| 600 | + | 1usize | |
| 601 | + | } else { | |
| 602 | + | 0 | |
| 603 | + | }; | |
| 604 | + | let limit = base_depth + depth; | |
| 605 | + | ||
| 606 | + | for want in &request.wants { | |
| 607 | + | queue.push_back((*want, 1usize)); | |
| 608 | + | } | |
| 609 | + | ||
| 610 | + | while let Some((commit_oid, current_depth)) = queue.pop_front() { | |
| 611 | + | if !seen.insert(commit_oid) { | |
| 612 | + | continue; | |
| 613 | + | } | |
| 614 | + | included_commits.push(commit_oid); | |
| 615 | + | included_objects.push(commit_oid); | |
| 616 | + | ||
| 617 | + | let commit_obj = repo | |
| 618 | + | .find_object(commit_oid) | |
| 619 | + | .map_err(|e| Error::Protocol(e.to_string()))?; | |
| 620 | + | let tree_oid = | |
| 621 | + | gix::objs::CommitRefIter::from_bytes(&commit_obj.data, gix::hash::Kind::Sha1) | |
| 622 | + | .tree_id() | |
| 623 | + | .map_err(|e| Error::Protocol(e.to_string()))?; | |
| 624 | + | collect_tree_oids(repo, tree_oid, &mut seen, &mut included_objects)?; | |
| 625 | + | let parents: Vec<_> = | |
| 626 | + | gix::objs::CommitRefIter::from_bytes(&commit_obj.data, gix::hash::Kind::Sha1) | |
| 627 | + | .parent_ids() | |
| 628 | + | .collect(); | |
| 629 | + | ||
| 630 | + | if current_depth >= limit || parents.is_empty() { | |
| 631 | + | shallow_boundary.push(commit_oid); | |
| 632 | + | continue; | |
| 633 | + | } | |
| 634 | + | ||
| 635 | + | for parent in parents { | |
| 636 | + | queue.push_back((parent, current_depth + 1)); | |
| 637 | + | } | |
| 638 | + | } | |
| 639 | + | ||
| 640 | + | Ok(DepthState { | |
| 641 | + | included_objects, | |
| 642 | + | shallow_boundary, | |
| 643 | + | }) | |
| 644 | + | } | |
| 645 | + | ||
| 646 | + | fn collect_tree_oids( | |
| 647 | + | repo: &gix::Repository, | |
| 648 | + | root_tree_oid: gix::ObjectId, | |
| 649 | + | seen: &mut HashSet<gix::ObjectId>, | |
| 650 | + | oids: &mut Vec<gix::ObjectId>, | |
| 651 | + | ) -> Result<()> { | |
| 652 | + | let mut stack = vec![root_tree_oid]; | |
| 653 | + | ||
| 654 | + | while let Some(tree_oid) = stack.pop() { | |
| 655 | + | if !seen.insert(tree_oid) { | |
| 656 | + | continue; | |
| 657 | + | } | |
| 658 | + | ||
| 659 | + | let tree_obj = repo | |
| 660 | + | .find_object(tree_oid) | |
| 661 | + | .map_err(|e| Error::Protocol(e.to_string()))?; | |
| 662 | + | oids.push(tree_oid); | |
| 663 | + | ||
| 664 | + | for entry_result in | |
| 665 | + | gix::objs::TreeRefIter::from_bytes(&tree_obj.data, gix::hash::Kind::Sha1) | |
| 666 | + | { | |
| 667 | + | let entry = entry_result.map_err(|e| Error::Protocol(e.to_string()))?; | |
| 668 | + | let entry_oid = entry.oid.to_owned(); | |
| 669 | + | let entry_mode = entry.mode; | |
| 670 | + | ||
| 671 | + | if entry_mode.is_tree() { | |
| 672 | + | stack.push(entry_oid); | |
| 673 | + | } else if seen.insert(entry_oid) && !entry_mode.is_commit() { | |
| 674 | + | oids.push(entry_oid); | |
| 675 | + | } | |
| 676 | + | } | |
| 677 | + | } | |
| 678 | + | ||
| 679 | + | Ok(()) | |
| 680 | + | } | |
| 681 | + | ||
| 682 | + | #[cfg(test)] | |
| 683 | + | mod tests { | |
| 684 | + | use super::*; | |
| 685 | + | ||
| 686 | + | fn pkt(data: &str) -> Vec<u8> { | |
| 687 | + | pktline::encode(data.as_bytes()) | |
| 688 | + | } | |
| 689 | + | ||
| 690 | + | #[test] | |
| 691 | + | fn parse_ls_refs_command() { | |
| 692 | + | let mut body = Vec::new(); | |
| 693 | + | body.extend_from_slice(&pkt("command=ls-refs\n")); | |
| 694 | + | body.extend_from_slice(b"0001"); | |
| 695 | + | body.extend_from_slice(&pkt("peel\n")); | |
| 696 | + | body.extend_from_slice(&pkt("symrefs\n")); | |
| 697 | + | body.extend_from_slice(&pkt("ref-prefix refs/heads/\n")); | |
| 698 | + | body.extend_from_slice(b"0000"); | |
| 699 | + | ||
| 700 | + | let Command::LsRefs(req) = parse_command_request(&body).unwrap() else { | |
| 701 | + | panic!("expected ls-refs command"); | |
| 702 | + | }; | |
| 703 | + | assert!(req.peel); | |
| 704 | + | assert!(req.symrefs); | |
| 705 | + | assert_eq!(req.ref_prefixes, vec!["refs/heads/"]); | |
| 706 | + | } | |
| 707 | + | ||
| 708 | + | #[test] | |
| 709 | + | fn parse_fetch_command() { | |
| 710 | + | let mut body = Vec::new(); | |
| 711 | + | body.extend_from_slice(&pkt("command=fetch\n")); | |
| 712 | + | body.extend_from_slice(b"0001"); | |
| 713 | + | body.extend_from_slice(&pkt("ofs-delta\n")); | |
| 714 | + | body.extend_from_slice(&pkt("want 0000000000000000000000000000000000000001\n")); | |
| 715 | + | body.extend_from_slice(&pkt("done\n")); | |
| 716 | + | body.extend_from_slice(b"0000"); | |
| 717 | + | ||
| 718 | + | let Command::Fetch(req) = parse_command_request(&body).unwrap() else { | |
| 719 | + | panic!("expected fetch command"); | |
| 720 | + | }; | |
| 721 | + | assert_eq!(req.upload_request.wants.len(), 1); | |
| 722 | + | assert!(req.upload_request.done); | |
| 723 | + | assert!(req.upload_request.capabilities.ofs_delta); | |
| 724 | + | } | |
| 725 | + | ||
| 726 | + | #[test] | |
| 727 | + | fn ls_refs_returns_unborn_head() { | |
| 728 | + | let root = tempfile::TempDir::new().unwrap(); | |
| 729 | + | let repo_path = root.path().join("repo.git"); | |
| 730 | + | std::process::Command::new("git") | |
| 731 | + | .args(["init", "--bare", repo_path.to_str().unwrap()]) | |
| 732 | + | .output() | |
| 733 | + | .unwrap(); | |
| 734 | + | std::process::Command::new("git") | |
| 735 | + | .args(["symbolic-ref", "HEAD", "refs/heads/main"]) | |
| 736 | + | .current_dir(&repo_path) | |
| 737 | + | .output() | |
| 738 | + | .unwrap(); | |
| 739 | + | ||
| 740 | + | let out = ls_refs( | |
| 741 | + | &repo_path, | |
| 742 | + | &LsRefsRequest { | |
| 743 | + | unborn: true, | |
| 744 | + | symrefs: true, | |
| 745 | + | ..Default::default() | |
| 746 | + | }, | |
| 747 | + | ) | |
| 748 | + | .unwrap(); | |
| 749 | + | let text = String::from_utf8(out).unwrap(); | |
| 750 | + | assert!(text.contains("unborn HEAD symref-target:refs/heads/main")); | |
| 751 | + | } | |
| 752 | + | } |
addedvendor/gitserver-core/src/receive_pack.rs+562 −0
| 1 | + | // This Source Code Form is subject to the terms of the Mozilla Public | |
| 2 | + | // License, v. 2.0. If a copy of the MPL was not distributed with this | |
| 3 | + | // file, You can obtain one at https://mozilla.org/MPL/2.0/. | |
| 4 | + | // | |
| 5 | + | // Copyright (c) 2026 WJQSERVER | |
| 6 | + | ||
| 7 | + | use std::io::{ | |
| 8 | + | BufRead, | |
| 9 | + | BufReader, | |
| 10 | + | Read, | |
| 11 | + | }; | |
| 12 | + | use std::path::Path; | |
| 13 | + | use std::sync::atomic::AtomicBool; | |
| 14 | + | ||
| 15 | + | use gix::objs::bstr::BString; | |
| 16 | + | use gix::prelude::ObjectIdExt; | |
| 17 | + | use gix::progress::Discard; | |
| 18 | + | use gix::refs::Target; | |
| 19 | + | use gix::refs::transaction::{ | |
| 20 | + | Change, | |
| 21 | + | LogChange, | |
| 22 | + | PreviousValue, | |
| 23 | + | RefEdit, | |
| 24 | + | RefLog, | |
| 25 | + | }; | |
| 26 | + | ||
| 27 | + | use crate::error::{ | |
| 28 | + | Error, | |
| 29 | + | Result, | |
| 30 | + | }; | |
| 31 | + | use crate::pktline; | |
| 32 | + | ||
| 33 | + | const ZERO_ID: &str = "0000000000000000000000000000000000000000"; | |
| 34 | + | const CAPABILITIES: &str = concat!( | |
| 35 | + | "report-status report-status-v2 side-band-64k quiet ofs-delta object-format=sha1 agent=gitserver/", | |
| 36 | + | env!("CARGO_PKG_VERSION") | |
| 37 | + | ); | |
| 38 | + | ||
| 39 | + | pub fn advertise_receive_refs(repo_path: &Path) -> Result<Vec<u8>> { | |
| 40 | + | let repo = gix::open(repo_path)?; | |
| 41 | + | let mut out = Vec::new(); | |
| 42 | + | let head_name = repo.head_name().ok().flatten(); | |
| 43 | + | let mut refs: Vec<(String, gix::ObjectId)> = repo | |
| 44 | + | .references() | |
| 45 | + | .map_err(|e| Error::Protocol(format!("failed to open refs: {e}")))? | |
| 46 | + | .all() | |
| 47 | + | .map_err(|e| Error::Protocol(format!("failed to iterate refs: {e}")))? | |
| 48 | + | .flatten() | |
| 49 | + | .filter_map(|mut reference| { | |
| 50 | + | reference | |
| 51 | + | .peel_to_id() | |
| 52 | + | .ok() | |
| 53 | + | .map(|id| (reference.name().as_bstr().to_string(), id.detach())) | |
| 54 | + | }) | |
| 55 | + | .collect(); | |
| 56 | + | refs.sort_by(|a, b| a.0.cmp(&b.0)); | |
| 57 | + | ||
| 58 | + | if refs.is_empty() { | |
| 59 | + | out.extend_from_slice(&pktline::encode( | |
| 60 | + | format!("{ZERO_ID} capabilities^{{}}\0{CAPABILITIES}\n").as_bytes(), | |
| 61 | + | )); | |
| 62 | + | } else { | |
| 63 | + | let (first_name, first_id) = &refs[0]; | |
| 64 | + | let mut first = format!("{} {}\0{CAPABILITIES}", first_id, first_name); | |
| 65 | + | if head_name | |
| 66 | + | .as_ref() | |
| 67 | + | .is_some_and(|head| head.as_bstr() == first_name.as_str()) | |
| 68 | + | { | |
| 69 | + | first.push_str(&format!(" symref=HEAD:{first_name}")); | |
| 70 | + | } | |
| 71 | + | first.push('\n'); | |
| 72 | + | out.extend_from_slice(&pktline::encode(first.as_bytes())); | |
| 73 | + | ||
| 74 | + | for (name, id) in refs.into_iter().skip(1) { | |
| 75 | + | out.extend_from_slice(&pktline::encode(format!("{id} {name}\n").as_bytes())); | |
| 76 | + | } | |
| 77 | + | } | |
| 78 | + | ||
| 79 | + | out.extend_from_slice(pktline::flush()); | |
| 80 | + | Ok(out) | |
| 81 | + | } | |
| 82 | + | ||
| 83 | + | pub fn receive_pack<R: Read>(repo_path: &Path, request: R) -> Result<Vec<u8>> { | |
| 84 | + | let interrupt = AtomicBool::new(false); | |
| 85 | + | receive_pack_with_interrupt(repo_path, request, &interrupt) | |
| 86 | + | } | |
| 87 | + | ||
| 88 | + | pub fn receive_pack_with_interrupt<R: Read>( | |
| 89 | + | repo_path: &Path, | |
| 90 | + | request: R, | |
| 91 | + | interrupt: &AtomicBool, | |
| 92 | + | ) -> Result<Vec<u8>> { | |
| 93 | + | let repo = gix::open(repo_path)?; | |
| 94 | + | let mut parsed = parse_request(request, interrupt)?; | |
| 95 | + | let status = apply_commands(&repo, repo_path, &mut parsed, interrupt)?; | |
| 96 | + | Ok(encode_report_status(&parsed.capabilities, &status)) | |
| 97 | + | } | |
| 98 | + | ||
| 99 | + | #[derive(Default)] | |
| 100 | + | struct ReceivePackCapabilities { | |
| 101 | + | report_status: bool, | |
| 102 | + | report_status_v2: bool, | |
| 103 | + | } | |
| 104 | + | ||
| 105 | + | struct ReceivePackRequest<R> { | |
| 106 | + | commands: Vec<UpdateCommand>, | |
| 107 | + | pack: R, | |
| 108 | + | capabilities: ReceivePackCapabilities, | |
| 109 | + | } | |
| 110 | + | ||
| 111 | + | struct UpdateCommand { | |
| 112 | + | old_id: String, | |
| 113 | + | new_id: String, | |
| 114 | + | refname: String, | |
| 115 | + | } | |
| 116 | + | ||
| 117 | + | enum CommandStatus { | |
| 118 | + | Ok(String), | |
| 119 | + | Ng(String, String), | |
| 120 | + | } | |
| 121 | + | ||
| 122 | + | fn parse_request<R: Read>( | |
| 123 | + | request: R, | |
| 124 | + | interrupt: &AtomicBool, | |
| 125 | + | ) -> Result<ReceivePackRequest<BufReader<R>>> { | |
| 126 | + | let mut request = BufReader::new(request); | |
| 127 | + | let mut commands = Vec::new(); | |
| 128 | + | let mut capabilities = ReceivePackCapabilities::default(); | |
| 129 | + | ||
| 130 | + | loop { | |
| 131 | + | check_interrupt(interrupt)?; | |
| 132 | + | let mut prefix = [0u8; 4]; | |
| 133 | + | match request.read_exact(&mut prefix) { | |
| 134 | + | Ok(()) => {} | |
| 135 | + | Err(err) if err.kind() == std::io::ErrorKind::UnexpectedEof => break, | |
| 136 | + | Err(err) => return Err(Error::Io(err)), | |
| 137 | + | } | |
| 138 | + | ||
| 139 | + | let len_str = std::str::from_utf8(&prefix) | |
| 140 | + | .map_err(|_| Error::Protocol("invalid pkt-line length prefix".into()))?; | |
| 141 | + | ||
| 142 | + | if len_str == "0000" { | |
| 143 | + | break; | |
| 144 | + | } | |
| 145 | + | ||
| 146 | + | let len = usize::from_str_radix(len_str, 16) | |
| 147 | + | .map_err(|_| Error::Protocol("invalid pkt-line length".into()))?; | |
| 148 | + | if len < 4 { | |
| 149 | + | return Err(Error::Protocol("invalid pkt-line frame length".into())); | |
| 150 | + | } | |
| 151 | + | ||
| 152 | + | check_interrupt(interrupt)?; | |
| 153 | + | let mut payload = vec![0u8; len - 4]; | |
| 154 | + | request.read_exact(&mut payload)?; | |
| 155 | + | ||
| 156 | + | let (command_bytes, capability_bytes) = | |
| 157 | + | if let Some(nul) = payload.iter().position(|b| *b == 0) { | |
| 158 | + | (&payload[..nul], Some(&payload[nul + 1..])) | |
| 159 | + | } else { | |
| 160 | + | (&payload[..], None) | |
| 161 | + | }; | |
| 162 | + | ||
| 163 | + | if let Some(capability_bytes) = capability_bytes { | |
| 164 | + | let capabilities_line = std::str::from_utf8(capability_bytes) | |
| 165 | + | .map_err(|_| Error::Protocol("invalid UTF-8 in receive-pack capabilities".into()))? | |
| 166 | + | .trim_end_matches('\n'); | |
| 167 | + | for capability in capabilities_line.split_ascii_whitespace() { | |
| 168 | + | match capability { | |
| 169 | + | "report-status" => capabilities.report_status = true, | |
| 170 | + | "report-status-v2" => { | |
| 171 | + | capabilities.report_status = true; | |
| 172 | + | capabilities.report_status_v2 = true; | |
| 173 | + | } | |
| 174 | + | _ => {} | |
| 175 | + | } | |
| 176 | + | } | |
| 177 | + | } | |
| 178 | + | ||
| 179 | + | let line = std::str::from_utf8(command_bytes) | |
| 180 | + | .map_err(|_| Error::Protocol("invalid UTF-8 in update command".into()))? | |
| 181 | + | .trim_end_matches('\n'); | |
| 182 | + | let mut parts = line.split_ascii_whitespace(); | |
| 183 | + | let Some(old_id) = parts.next() else { continue }; | |
| 184 | + | let Some(new_id) = parts.next() else { continue }; | |
| 185 | + | let Some(refname) = parts.next() else { | |
| 186 | + | continue; | |
| 187 | + | }; | |
| 188 | + | ||
| 189 | + | commands.push(UpdateCommand { | |
| 190 | + | old_id: old_id.to_owned(), | |
| 191 | + | new_id: new_id.to_owned(), | |
| 192 | + | refname: refname.to_owned(), | |
| 193 | + | }); | |
| 194 | + | } | |
| 195 | + | ||
| 196 | + | Ok(ReceivePackRequest { | |
| 197 | + | commands, | |
| 198 | + | pack: request, | |
| 199 | + | capabilities, | |
| 200 | + | }) | |
| 201 | + | } | |
| 202 | + | ||
| 203 | + | fn apply_commands<R: BufRead>( | |
| 204 | + | repo: &gix::Repository, | |
| 205 | + | repo_path: &Path, | |
| 206 | + | request: &mut ReceivePackRequest<R>, | |
| 207 | + | interrupt: &AtomicBool, | |
| 208 | + | ) -> Result<Vec<CommandStatus>> { | |
| 209 | + | check_interrupt(interrupt)?; | |
| 210 | + | if request.pack.fill_buf().map(|buf: &[u8]| !buf.is_empty())? { | |
| 211 | + | write_pack(repo_path, &mut request.pack, interrupt)?; | |
| 212 | + | } | |
| 213 | + | ||
| 214 | + | let mut edits = Vec::with_capacity(request.commands.len()); | |
| 215 | + | for (index, command) in request.commands.iter().enumerate() { | |
| 216 | + | check_interrupt(interrupt)?; | |
| 217 | + | match validate_ref_update(repo, command, interrupt) { | |
| 218 | + | Ok(edit) => edits.push((command.refname.clone(), edit)), | |
| 219 | + | Err(err) => { | |
| 220 | + | return Ok(request | |
| 221 | + | .commands | |
| 222 | + | .iter() | |
| 223 | + | .enumerate() | |
| 224 | + | .map(|(cmd_index, cmd)| { | |
| 225 | + | if cmd_index == index { | |
| 226 | + | CommandStatus::Ng(cmd.refname.clone(), err.to_string()) | |
| 227 | + | } else { | |
| 228 | + | CommandStatus::Ng( | |
| 229 | + | cmd.refname.clone(), | |
| 230 | + | "transaction aborted due to another command failing validation" | |
| 231 | + | .into(), | |
| 232 | + | ) | |
| 233 | + | } | |
| 234 | + | }) | |
| 235 | + | .collect()); | |
| 236 | + | } | |
| 237 | + | } | |
| 238 | + | } | |
| 239 | + | ||
| 240 | + | check_interrupt(interrupt)?; | |
| 241 | + | match repo.edit_references(edits.into_iter().map(|(_, edit)| edit)) { | |
| 242 | + | Ok(_) => Ok(request | |
| 243 | + | .commands | |
| 244 | + | .iter() | |
| 245 | + | .map(|cmd| CommandStatus::Ok(cmd.refname.clone())) | |
| 246 | + | .collect()), | |
| 247 | + | Err(err) => Ok(request | |
| 248 | + | .commands | |
| 249 | + | .iter() | |
| 250 | + | .map(|cmd| CommandStatus::Ng(cmd.refname.clone(), format!("transaction failed: {err}"))) | |
| 251 | + | .collect()), | |
| 252 | + | } | |
| 253 | + | } | |
| 254 | + | ||
| 255 | + | fn write_pack<R: BufRead>(repo_path: &Path, pack: &mut R, interrupt: &AtomicBool) -> Result<()> { | |
| 256 | + | let mut progress = Discard; | |
| 257 | + | let outcome = gix_pack::Bundle::write_to_directory( | |
| 258 | + | pack, | |
| 259 | + | Some(repo_path.join("objects/pack").as_path()), | |
| 260 | + | &mut progress, | |
| 261 | + | interrupt, | |
| 262 | + | None::<&gix::Repository>, | |
| 263 | + | Default::default(), | |
| 264 | + | ); | |
| 265 | + | if interrupt.load(std::sync::atomic::Ordering::Relaxed) { | |
| 266 | + | return Err(Error::Io(std::io::Error::new( | |
| 267 | + | std::io::ErrorKind::TimedOut, | |
| 268 | + | "receive-pack timed out", | |
| 269 | + | ))); | |
| 270 | + | } | |
| 271 | + | let outcome = | |
| 272 | + | outcome.map_err(|e| Error::Protocol(format!("failed to write incoming pack: {e}")))?; | |
| 273 | + | ||
| 274 | + | if let Some(keep) = outcome.keep_path { | |
| 275 | + | let _ = std::fs::remove_file(keep); | |
| 276 | + | } | |
| 277 | + | Ok(()) | |
| 278 | + | } | |
| 279 | + | ||
| 280 | + | fn check_interrupt(interrupt: &AtomicBool) -> Result<()> { | |
| 281 | + | if interrupt.load(std::sync::atomic::Ordering::Relaxed) { | |
| 282 | + | Err(Error::Io(std::io::Error::new( | |
| 283 | + | std::io::ErrorKind::TimedOut, | |
| 284 | + | "receive-pack timed out", | |
| 285 | + | ))) | |
| 286 | + | } else { | |
| 287 | + | Ok(()) | |
| 288 | + | } | |
| 289 | + | } | |
| 290 | + | ||
| 291 | + | fn validate_ref_update( | |
| 292 | + | repo: &gix::Repository, | |
| 293 | + | command: &UpdateCommand, | |
| 294 | + | interrupt: &AtomicBool, | |
| 295 | + | ) -> Result<RefEdit> { | |
| 296 | + | if command.new_id == ZERO_ID { | |
| 297 | + | return Err(Error::Protocol(format!( | |
| 298 | + | "deletion prohibited for {}", | |
| 299 | + | command.refname | |
| 300 | + | ))); | |
| 301 | + | } | |
| 302 | + | ||
| 303 | + | let is_branch = command.refname.starts_with("refs/heads/"); | |
| 304 | + | let is_tag = command.refname.starts_with("refs/tags/"); | |
| 305 | + | let new_id = gix::ObjectId::from_hex(command.new_id.as_bytes()) | |
| 306 | + | .map_err(|_| Error::Protocol(format!("invalid new object id: {}", command.new_id)))?; | |
| 307 | + | let new_header = repo | |
| 308 | + | .find_header(new_id) | |
| 309 | + | .map_err(|e| Error::Protocol(format!("missing new object {}: {e}", command.new_id)))?; | |
| 310 | + | if is_branch && new_header.kind() != gix::objs::Kind::Commit { | |
| 311 | + | return Err(Error::Protocol(format!( | |
| 312 | + | "updates to {} must point to a commit", | |
| 313 | + | command.refname | |
| 314 | + | ))); | |
| 315 | + | } | |
| 316 | + | ||
| 317 | + | let name: gix::refs::FullName = command | |
| 318 | + | .refname | |
| 319 | + | .as_str() | |
| 320 | + | .try_into() | |
| 321 | + | .map_err(|e| Error::Protocol(format!("invalid ref name {}: {e}", command.refname)))?; | |
| 322 | + | ||
| 323 | + | let (expected, log_message) = if command.old_id == ZERO_ID { | |
| 324 | + | (PreviousValue::MustNotExist, BString::from("push create")) | |
| 325 | + | } else { | |
| 326 | + | if is_tag { | |
| 327 | + | return Err(Error::Protocol(format!( | |
| 328 | + | "updating existing tag {} is not allowed", | |
| 329 | + | command.refname | |
| 330 | + | ))); | |
| 331 | + | } | |
| 332 | + | ||
| 333 | + | let old_id = gix::ObjectId::from_hex(command.old_id.as_bytes()) | |
| 334 | + | .map_err(|_| Error::Protocol(format!("invalid old object id: {}", command.old_id)))?; | |
| 335 | + | if is_branch { | |
| 336 | + | ensure_fast_forward(repo, old_id, new_id, &command.refname, interrupt)?; | |
| 337 | + | } | |
| 338 | + | ( | |
| 339 | + | PreviousValue::MustExistAndMatch(Target::Object(old_id)), | |
| 340 | + | BString::from("push"), | |
| 341 | + | ) | |
| 342 | + | }; | |
| 343 | + | ||
| 344 | + | Ok(RefEdit { | |
| 345 | + | change: Change::Update { | |
| 346 | + | log: LogChange { | |
| 347 | + | mode: RefLog::AndReference, | |
| 348 | + | force_create_reflog: false, | |
| 349 | + | message: log_message, | |
| 350 | + | }, | |
| 351 | + | expected, | |
| 352 | + | new: Target::Object(new_id), | |
| 353 | + | }, | |
| 354 | + | name, | |
| 355 | + | deref: false, | |
| 356 | + | }) | |
| 357 | + | } | |
| 358 | + | ||
| 359 | + | fn ensure_fast_forward( | |
| 360 | + | repo: &gix::Repository, | |
| 361 | + | old_id: gix::ObjectId, | |
| 362 | + | new_id: gix::ObjectId, | |
| 363 | + | refname: &str, | |
| 364 | + | interrupt: &AtomicBool, | |
| 365 | + | ) -> Result<()> { | |
| 366 | + | check_interrupt(interrupt)?; | |
| 367 | + | if old_id == new_id { | |
| 368 | + | return Ok(()); | |
| 369 | + | } | |
| 370 | + | ||
| 371 | + | let old_commit_time = repo | |
| 372 | + | .find_object(old_id) | |
| 373 | + | .map_err(|e| Error::Protocol(format!("failed to inspect current tip for {refname}: {e}")))? | |
| 374 | + | .try_into_commit() | |
| 375 | + | .map_err(|_| Error::Protocol(format!("current tip of {refname} is not a commit")))? | |
| 376 | + | .committer() | |
| 377 | + | .map_err(|e| Error::Protocol(format!("failed to read commit metadata for {refname}: {e}")))? | |
| 378 | + | .seconds(); | |
| 379 | + | ||
| 380 | + | let ancestors = new_id | |
| 381 | + | .attach(repo) | |
| 382 | + | .ancestors() | |
| 383 | + | .sorting(gix::revision::walk::Sorting::ByCommitTimeCutoff { | |
| 384 | + | order: Default::default(), | |
| 385 | + | seconds: old_commit_time, | |
| 386 | + | }) | |
| 387 | + | .all() | |
| 388 | + | .map_err(|e| Error::Protocol(format!("failed to walk commits for {refname}: {e}")))?; | |
| 389 | + | ||
| 390 | + | for id in ancestors { | |
| 391 | + | check_interrupt(interrupt)?; | |
| 392 | + | if id.is_ok_and(|commit| commit.id == old_id) { | |
| 393 | + | return Ok(()); | |
| 394 | + | } | |
| 395 | + | } | |
| 396 | + | ||
| 397 | + | Err(Error::Protocol(format!( | |
| 398 | + | "non-fast-forward update to {refname} is not allowed" | |
| 399 | + | ))) | |
| 400 | + | } | |
| 401 | + | ||
| 402 | + | fn encode_report_status( | |
| 403 | + | capabilities: &ReceivePackCapabilities, | |
| 404 | + | statuses: &[CommandStatus], | |
| 405 | + | ) -> Vec<u8> { | |
| 406 | + | if !capabilities.report_status { | |
| 407 | + | return pktline::flush().to_vec(); | |
| 408 | + | } | |
| 409 | + | ||
| 410 | + | let mut status_lines = Vec::new(); | |
| 411 | + | status_lines.extend_from_slice(&pktline::encode(b"unpack ok\n")); | |
| 412 | + | ||
| 413 | + | for status in statuses { | |
| 414 | + | match status { | |
| 415 | + | CommandStatus::Ok(refname) => { | |
| 416 | + | status_lines | |
| 417 | + | .extend_from_slice(&pktline::encode(format!("ok {refname}\n").as_bytes())); | |
| 418 | + | } | |
| 419 | + | CommandStatus::Ng(refname, message) => { | |
| 420 | + | status_lines.extend_from_slice(&pktline::encode( | |
| 421 | + | format!("ng {refname} {message}\n").as_bytes(), | |
| 422 | + | )); | |
| 423 | + | } | |
| 424 | + | } | |
| 425 | + | } | |
| 426 | + | status_lines.extend_from_slice(pktline::flush()); | |
| 427 | + | ||
| 428 | + | if capabilities.report_status_v2 { | |
| 429 | + | let mut sideband = Vec::new(); | |
| 430 | + | const MAX_BAND_PAYLOAD: usize = 65519; | |
| 431 | + | for chunk in status_lines.chunks(MAX_BAND_PAYLOAD) { | |
| 432 | + | let len = 4 + 1 + chunk.len(); | |
| 433 | + | sideband.extend_from_slice(format!("{len:04x}").as_bytes()); | |
| 434 | + | sideband.push(0x01); | |
| 435 | + | sideband.extend_from_slice(chunk); | |
| 436 | + | } | |
| 437 | + | sideband.extend_from_slice(pktline::flush()); | |
| 438 | + | sideband | |
| 439 | + | } else { | |
| 440 | + | status_lines | |
| 441 | + | } | |
| 442 | + | } | |
| 443 | + | ||
| 444 | + | #[cfg(test)] | |
| 445 | + | mod tests { | |
| 446 | + | use std::process::Command; | |
| 447 | + | ||
| 448 | + | use tempfile::TempDir; | |
| 449 | + | ||
| 450 | + | use super::*; | |
| 451 | + | ||
| 452 | + | fn create_repo_with_commit(root: &std::path::Path) -> std::path::PathBuf { | |
| 453 | + | let repo_path = root.join("test.git"); | |
| 454 | + | let work_dir = root.join("work"); | |
| 455 | + | std::fs::create_dir(&work_dir).unwrap(); | |
| 456 | + | Command::new("git") | |
| 457 | + | .args(["init", "--bare", repo_path.to_str().unwrap()]) | |
| 458 | + | .output() | |
| 459 | + | .unwrap(); | |
| 460 | + | Command::new("git") | |
| 461 | + | .args(["symbolic-ref", "HEAD", "refs/heads/main"]) | |
| 462 | + | .current_dir(&repo_path) | |
| 463 | + | .output() | |
| 464 | + | .unwrap(); | |
| 465 | + | Command::new("git") | |
| 466 | + | .args([ | |
| 467 | + | "clone", | |
| 468 | + | repo_path.to_str().unwrap(), | |
| 469 | + | work_dir.to_str().unwrap(), | |
| 470 | + | ]) | |
| 471 | + | .output() | |
| 472 | + | .unwrap(); | |
| 473 | + | Command::new("git") | |
| 474 | + | .current_dir(&work_dir) | |
| 475 | + | .args(["commit", "--allow-empty", "-m", "init"]) | |
| 476 | + | .env("GIT_AUTHOR_NAME", "Test") | |
| 477 | + | .env("GIT_AUTHOR_EMAIL", "t@t.com") | |
| 478 | + | .env("GIT_COMMITTER_NAME", "Test") | |
| 479 | + | .env("GIT_COMMITTER_EMAIL", "t@t.com") | |
| 480 | + | .output() | |
| 481 | + | .unwrap(); | |
| 482 | + | Command::new("git") | |
| 483 | + | .current_dir(&work_dir) | |
| 484 | + | .args(["push", "origin", "main"]) | |
| 485 | + | .output() | |
| 486 | + | .unwrap(); | |
| 487 | + | repo_path | |
| 488 | + | } | |
| 489 | + | ||
| 490 | + | #[test] | |
| 491 | + | fn advertise_receive_pack_refs() { | |
| 492 | + | let root = TempDir::new().unwrap(); | |
| 493 | + | let repo_path = create_repo_with_commit(root.path()); | |
| 494 | + | let output = advertise_receive_refs(&repo_path).unwrap(); | |
| 495 | + | let output_str = String::from_utf8_lossy(&output); | |
| 496 | + | assert!(output_str.contains("refs/heads/main")); | |
| 497 | + | assert!(output_str.contains("report-status")); | |
| 498 | + | } | |
| 499 | + | ||
| 500 | + | #[test] | |
| 501 | + | fn parse_receive_pack_request_with_capabilities() { | |
| 502 | + | let payload = b"0000000000000000000000000000000000000000 1111111111111111111111111111111111111111 refs/heads/main\0 report-status-v2 side-band-64k\n"; | |
| 503 | + | let mut body = format!("{:04x}", payload.len() + 4).into_bytes(); | |
| 504 | + | body.extend_from_slice(payload); | |
| 505 | + | body.extend_from_slice(b"0000PACK"); | |
| 506 | + | ||
| 507 | + | let interrupt = AtomicBool::new(false); | |
| 508 | + | let parsed = parse_request(std::io::Cursor::new(&body), &interrupt).unwrap(); | |
| 509 | + | assert_eq!(parsed.commands.len(), 1); | |
| 510 | + | assert!(parsed.capabilities.report_status); | |
| 511 | + | assert!(parsed.capabilities.report_status_v2); | |
| 512 | + | let mut pack = String::new(); | |
| 513 | + | let mut reader = parsed.pack; | |
| 514 | + | reader.read_to_string(&mut pack).unwrap(); | |
| 515 | + | assert_eq!(pack.as_bytes(), b"PACK"); | |
| 516 | + | } | |
| 517 | + | ||
| 518 | + | #[test] | |
| 519 | + | fn branch_updates_require_commit_target() { | |
| 520 | + | let root = TempDir::new().unwrap(); | |
| 521 | + | let repo_path = create_repo_with_commit(root.path()); | |
| 522 | + | let repo = gix::open(repo_path).unwrap(); | |
| 523 | + | let tree_id = Command::new("git") | |
| 524 | + | .args(["rev-parse", "HEAD^{tree}"]) | |
| 525 | + | .current_dir(root.path().join("work")) | |
| 526 | + | .output() | |
| 527 | + | .unwrap(); | |
| 528 | + | let tree_id = String::from_utf8(tree_id.stdout) | |
| 529 | + | .unwrap() | |
| 530 | + | .trim() | |
| 531 | + | .to_string(); | |
| 532 | + | ||
| 533 | + | let err = validate_ref_update( | |
| 534 | + | &repo, | |
| 535 | + | &UpdateCommand { | |
| 536 | + | old_id: ZERO_ID.into(), | |
| 537 | + | new_id: tree_id, | |
| 538 | + | refname: "refs/heads/feature".into(), | |
| 539 | + | }, | |
| 540 | + | &AtomicBool::new(false), | |
| 541 | + | ) | |
| 542 | + | .unwrap_err(); | |
| 543 | + | ||
| 544 | + | assert!(err.to_string().contains("must point to a commit")); | |
| 545 | + | } | |
| 546 | + | ||
| 547 | + | #[test] | |
| 548 | + | fn ensure_fast_forward_respects_interrupt() { | |
| 549 | + | let root = TempDir::new().unwrap(); | |
| 550 | + | let repo_path = create_repo_with_commit(root.path()); | |
| 551 | + | let repo = gix::open(repo_path).unwrap(); | |
| 552 | + | let head = repo.head_id().unwrap().detach(); | |
| 553 | + | let interrupt = AtomicBool::new(true); | |
| 554 | + | ||
| 555 | + | let err = | |
| 556 | + | ensure_fast_forward(&repo, head, head, "refs/heads/main", &interrupt).unwrap_err(); | |
| 557 | + | match err { | |
| 558 | + | Error::Io(inner) => assert_eq!(inner.kind(), std::io::ErrorKind::TimedOut), | |
| 559 | + | other => panic!("expected timeout io error, got {other}"), | |
| 560 | + | } | |
| 561 | + | } | |
| 562 | + | } |
addedvendor/gitserver-core/src/refs.rs+223 −0
| 1 | + | use std::path::Path; | |
| 2 | + | ||
| 3 | + | use crate::error::Result; | |
| 4 | + | use crate::pktline; | |
| 5 | + | ||
| 6 | + | const CAPABILITIES: &str = "multi_ack multi_ack_detailed side-band-64k ofs-delta"; | |
| 7 | + | ||
| 8 | + | const ZERO_OID: &str = "0000000000000000000000000000000000000000"; | |
| 9 | + | ||
| 10 | + | /// Generate the complete pkt-line encoded ref advertisement response | |
| 11 | + | /// for the Git smart HTTP protocol (`git-upload-pack`). | |
| 12 | + | /// | |
| 13 | + | /// The output follows the format expected by `git clone` / `git fetch`: | |
| 14 | + | /// | |
| 15 | + | /// ```text | |
| 16 | + | /// 001e# service=git-upload-pack\n | |
| 17 | + | /// 0000 | |
| 18 | + | /// <first-ref>\0<capabilities>\n | |
| 19 | + | /// <ref-line>\n | |
| 20 | + | /// ... | |
| 21 | + | /// 0000 | |
| 22 | + | /// ``` | |
| 23 | + | pub fn advertise_refs(repo_path: &Path) -> Result<Vec<u8>> { | |
| 24 | + | let repo = gix::open(repo_path)?; | |
| 25 | + | ||
| 26 | + | let mut output = Vec::new(); | |
| 27 | + | ||
| 28 | + | // Service header + flush | |
| 29 | + | output.extend_from_slice(&pktline::encode_comment("service=git-upload-pack")); | |
| 30 | + | output.extend_from_slice(pktline::flush()); | |
| 31 | + | ||
| 32 | + | // Collect refs: (oid_hex, refname) | |
| 33 | + | let mut refs: Vec<(String, String)> = Vec::new(); | |
| 34 | + | ||
| 35 | + | // Try to resolve HEAD first | |
| 36 | + | if let Ok(id) = repo.head_id() { | |
| 37 | + | refs.push((id.to_string(), "HEAD".to_string())); | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | // Iterate all references | |
| 41 | + | if let Ok(platform) = repo.references() | |
| 42 | + | && let Ok(iter) = platform.all() | |
| 43 | + | { | |
| 44 | + | for mut r in iter.flatten() { | |
| 45 | + | let name = r.name().as_bstr().to_string(); | |
| 46 | + | if let Ok(id) = r.peel_to_id() { | |
| 47 | + | refs.push((id.to_string(), name)); | |
| 48 | + | } | |
| 49 | + | } | |
| 50 | + | } | |
| 51 | + | ||
| 52 | + | if refs.is_empty() { | |
| 53 | + | // No refs at all: advertise capabilities with zero OID | |
| 54 | + | let line = format!("{ZERO_OID} capabilities^{{}}\0{CAPABILITIES}\n"); | |
| 55 | + | output.extend_from_slice(&pktline::encode(line.as_bytes())); | |
| 56 | + | } else { | |
| 57 | + | // First ref line includes capabilities after NUL byte | |
| 58 | + | let (oid, name) = &refs[0]; | |
| 59 | + | let first_line = format!("{oid} {name}\0{CAPABILITIES}\n"); | |
| 60 | + | output.extend_from_slice(&pktline::encode(first_line.as_bytes())); | |
| 61 | + | ||
| 62 | + | // Subsequent ref lines | |
| 63 | + | for (oid, name) in &refs[1..] { | |
| 64 | + | let line = format!("{oid} {name}\n"); | |
| 65 | + | output.extend_from_slice(&pktline::encode(line.as_bytes())); | |
| 66 | + | } | |
| 67 | + | } | |
| 68 | + | ||
| 69 | + | // Final flush | |
| 70 | + | output.extend_from_slice(pktline::flush()); | |
| 71 | + | ||
| 72 | + | Ok(output) | |
| 73 | + | } | |
| 74 | + | ||
| 75 | + | #[cfg(test)] | |
| 76 | + | mod tests { | |
| 77 | + | use std::path::{ | |
| 78 | + | Path, | |
| 79 | + | PathBuf, | |
| 80 | + | }; | |
| 81 | + | use std::process::Command; | |
| 82 | + | ||
| 83 | + | use tempfile::TempDir; | |
| 84 | + | ||
| 85 | + | use super::*; | |
| 86 | + | ||
| 87 | + | /// Create a bare repo with a single commit on the `main` branch. | |
| 88 | + | /// | |
| 89 | + | /// Returns the path to the bare repository. | |
| 90 | + | fn create_repo_with_commit(root: &Path) -> PathBuf { | |
| 91 | + | let bare_path = root.join("test.git"); | |
| 92 | + | let clone_path = root.join("workdir"); | |
| 93 | + | ||
| 94 | + | // Create a bare repository | |
| 95 | + | let out = Command::new("git") | |
| 96 | + | .args(["init", "--bare", bare_path.to_str().unwrap()]) | |
| 97 | + | .output() | |
| 98 | + | .expect("git init --bare failed"); | |
| 99 | + | assert!(out.status.success(), "git init --bare failed: {:?}", out); | |
| 100 | + | ||
| 101 | + | // Set default branch to main in bare repo | |
| 102 | + | let out = Command::new("git") | |
| 103 | + | .args(["symbolic-ref", "HEAD", "refs/heads/main"]) | |
| 104 | + | .current_dir(&bare_path) | |
| 105 | + | .output() | |
| 106 | + | .expect("git symbolic-ref failed"); | |
| 107 | + | assert!(out.status.success()); | |
| 108 | + | ||
| 109 | + | // Clone the bare repo into a working directory | |
| 110 | + | let out = Command::new("git") | |
| 111 | + | .args([ | |
| 112 | + | "clone", | |
| 113 | + | bare_path.to_str().unwrap(), | |
| 114 | + | clone_path.to_str().unwrap(), | |
| 115 | + | ]) | |
| 116 | + | .output() | |
| 117 | + | .expect("git clone failed"); | |
| 118 | + | assert!(out.status.success(), "git clone failed: {:?}", out); | |
| 119 | + | ||
| 120 | + | // Configure user in the clone | |
| 121 | + | for (key, val) in [("user.name", "Test User"), ("user.email", "test@test.com")] { | |
| 122 | + | Command::new("git") | |
| 123 | + | .args(["config", key, val]) | |
| 124 | + | .current_dir(&clone_path) | |
| 125 | + | .output() | |
| 126 | + | .expect("git config failed"); | |
| 127 | + | } | |
| 128 | + | ||
| 129 | + | // Create a commit | |
| 130 | + | let out = Command::new("git") | |
| 131 | + | .args(["commit", "--allow-empty", "-m", "initial commit"]) | |
| 132 | + | .current_dir(&clone_path) | |
| 133 | + | .env("GIT_AUTHOR_NAME", "Test User") | |
| 134 | + | .env("GIT_AUTHOR_EMAIL", "test@test.com") | |
| 135 | + | .env("GIT_COMMITTER_NAME", "Test User") | |
| 136 | + | .env("GIT_COMMITTER_EMAIL", "test@test.com") | |
| 137 | + | .output() | |
| 138 | + | .expect("git commit failed"); | |
| 139 | + | assert!(out.status.success(), "git commit failed: {:?}", out); | |
| 140 | + | ||
| 141 | + | // Push to the bare repo | |
| 142 | + | let out = Command::new("git") | |
| 143 | + | .args(["push", "origin", "main"]) | |
| 144 | + | .current_dir(&clone_path) | |
| 145 | + | .output() | |
| 146 | + | .expect("git push failed"); | |
| 147 | + | assert!(out.status.success(), "git push failed: {:?}", out); | |
| 148 | + | ||
| 149 | + | bare_path | |
| 150 | + | } | |
| 151 | + | ||
| 152 | + | #[test] | |
| 153 | + | fn advertise_refs_starts_with_service_header() { | |
| 154 | + | let dir = TempDir::new().unwrap(); | |
| 155 | + | let repo_path = create_repo_with_commit(dir.path()); | |
| 156 | + | let output = advertise_refs(&repo_path).unwrap(); | |
| 157 | + | let output_str = String::from_utf8_lossy(&output); | |
| 158 | + | ||
| 159 | + | assert!( | |
| 160 | + | output_str.starts_with("001e# service=git-upload-pack\n0000"), | |
| 161 | + | "output does not start with service header: {output_str:?}" | |
| 162 | + | ); | |
| 163 | + | } | |
| 164 | + | ||
| 165 | + | #[test] | |
| 166 | + | fn advertise_refs_contains_capabilities() { | |
| 167 | + | let dir = TempDir::new().unwrap(); | |
| 168 | + | let repo_path = create_repo_with_commit(dir.path()); | |
| 169 | + | let output = advertise_refs(&repo_path).unwrap(); | |
| 170 | + | let output_str = String::from_utf8_lossy(&output); | |
| 171 | + | ||
| 172 | + | assert!( | |
| 173 | + | output_str.contains("multi_ack"), | |
| 174 | + | "output missing multi_ack: {output_str:?}" | |
| 175 | + | ); | |
| 176 | + | assert!( | |
| 177 | + | output_str.contains("multi_ack_detailed"), | |
| 178 | + | "output missing multi_ack_detailed: {output_str:?}" | |
| 179 | + | ); | |
| 180 | + | assert!( | |
| 181 | + | output_str.contains("side-band-64k"), | |
| 182 | + | "output missing side-band-64k: {output_str:?}" | |
| 183 | + | ); | |
| 184 | + | assert!( | |
| 185 | + | output_str.contains("ofs-delta"), | |
| 186 | + | "output missing ofs-delta: {output_str:?}" | |
| 187 | + | ); | |
| 188 | + | assert!( | |
| 189 | + | !output_str.contains("thin-pack"), | |
| 190 | + | "output should not advertise thin-pack: {output_str:?}" | |
| 191 | + | ); | |
| 192 | + | assert!( | |
| 193 | + | !output_str.contains("shallow"), | |
| 194 | + | "output should not advertise shallow: {output_str:?}" | |
| 195 | + | ); | |
| 196 | + | } | |
| 197 | + | ||
| 198 | + | #[test] | |
| 199 | + | fn advertise_refs_contains_main_branch() { | |
| 200 | + | let dir = TempDir::new().unwrap(); | |
| 201 | + | let repo_path = create_repo_with_commit(dir.path()); | |
| 202 | + | let output = advertise_refs(&repo_path).unwrap(); | |
| 203 | + | let output_str = String::from_utf8_lossy(&output); | |
| 204 | + | ||
| 205 | + | assert!( | |
| 206 | + | output_str.contains("refs/heads/main"), | |
| 207 | + | "output missing refs/heads/main: {output_str:?}" | |
| 208 | + | ); | |
| 209 | + | } | |
| 210 | + | ||
| 211 | + | #[test] | |
| 212 | + | fn advertise_refs_ends_with_flush() { | |
| 213 | + | let dir = TempDir::new().unwrap(); | |
| 214 | + | let repo_path = create_repo_with_commit(dir.path()); | |
| 215 | + | let output = advertise_refs(&repo_path).unwrap(); | |
| 216 | + | ||
| 217 | + | assert!( | |
| 218 | + | output.ends_with(b"0000"), | |
| 219 | + | "output does not end with flush packet: {:?}", | |
| 220 | + | String::from_utf8_lossy(&output) | |
| 221 | + | ); | |
| 222 | + | } | |
| 223 | + | } |