From 8a598a4d93005c0f8ec793333efd35f0041931f3 Mon Sep 17 00:00:00 2001 From: Seongmin Lee Date: Sat, 27 Jun 2026 02:54:24 +0900 Subject: [PATCH] gitmirror: introduce gitmirror Signed-off-by: Seongmin Lee --- Cargo.lock | 1490 ++++++++++++++++-- appview/models/pull.go | 12 + appview/pages/pages.go | 45 + appview/pulls/pull2.go | 280 ++++ buf.yaml | 1 + docker-compose.yml | 15 + gitmirror/Cargo.toml | 11 +- gitmirror/README.md | 3 + gitmirror/proto/gen/gitmirror.pb.go | 728 +++++++++ gitmirror/proto/gen/gitmirror_grpc.pb.go | 204 +++ gitmirror/proto/gitmirror/v1/gitmirror.proto | 58 + gitmirror/src/diff.rs | 572 +++++++ gitmirror/src/main.rs | 163 +- go.mod | 2 +- localinfra/Caddyfile | 10 + localinfra/gitmirror.Dockerfile | 29 + 16 files changed, 3510 insertions(+), 113 deletions(-) create mode 100644 appview/pulls/pull2.go create mode 100644 gitmirror/README.md create mode 100644 gitmirror/proto/gen/gitmirror.pb.go create mode 100644 gitmirror/proto/gen/gitmirror_grpc.pb.go create mode 100644 gitmirror/src/diff.rs create mode 100644 localinfra/gitmirror.Dockerfile diff --git a/Cargo.lock b/Cargo.lock index 78d158ce..051831ae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -148,6 +148,12 @@ dependencies = [ "rustversion", ] +[[package]] +name = "arrayvec" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f02882884d3e1bc524fb12c79f107f6ad0e1cfd498c536ffb494301740995dfe" + [[package]] name = "assert-json-diff" version = "2.0.2" @@ -309,6 +315,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.11.1" @@ -333,6 +345,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + [[package]] name = "bobbin" version = "0.0.1" @@ -647,6 +668,17 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "bstr" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cee35f73844aa3014bb606320a6c1f010249dbdf43342fe54b5a4f6a8ed4b79" +dependencies = [ + "memchr", + "regex-automata", + "serde_core", +] + [[package]] name = "bumpalo" version = "3.20.3" @@ -668,6 +700,12 @@ dependencies = [ "serde", ] +[[package]] +name = "bytesize" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49e78e506b9d7633710dab98996f22f95f3d0f488e8f1aa162830556ed9fc14d" + [[package]] name = "cast" version = "0.3.0" @@ -825,6 +863,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +[[package]] +name = "clru" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "197fd99cb113a8d5d9b6376f3aa817f32c1078f2343b714fff7d2ca44fdf67d5" +dependencies = [ + "hashbrown 0.16.1", +] + [[package]] name = "cobs" version = "0.3.0" @@ -937,6 +984,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.5.0" @@ -1047,6 +1103,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + [[package]] name = "darling" version = "0.23.0" @@ -1145,6 +1210,38 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" +[[package]] +name = "defmt" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6e524506490a1953d237cb87b1cfc1e46f88c18f10a22dfe0f507dc6bfc7f7f" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0a27770e9c8f719a79d8b638281f4d828f77d8fd61e0bd94451b9b85e576a0b" +dependencies = [ + "defmt-parser", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror 2.0.18", +] + [[package]] name = "der" version = "0.7.10" @@ -1199,12 +1296,22 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", + "block-buffer 0.10.4", "const-oid", - "crypto-common", + "crypto-common 0.1.6", "subtle", ] +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "crypto-common 0.2.2", +] + [[package]] name = "displaydoc" version = "0.2.6" @@ -1222,6 +1329,12 @@ version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "117240f60069e65410b3ae1bb213295bd828f707b5bec6596a1afc8793ce0cbc" +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "ecdsa" version = "0.16.9" @@ -1229,7 +1342,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ "der", - "digest", + "digest 0.10.7", "elliptic-curve", "rfc6979", "signature", @@ -1250,7 +1363,7 @@ checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ "base16ct", "crypto-bigint", - "digest", + "digest 0.10.7", "ff", "generic-array", "group", @@ -1316,6 +1429,16 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9afc2bd4d5a73106dd53d10d73d3401c2f32730ba2c0b93ddb888a8983680471" +[[package]] +name = "faster-hex" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7223ae2d2f179b803433d9c830478527e92b8117eab39460edae7f1614d9fb73" +dependencies = [ + "heapless 0.8.0", + "serde", +] + [[package]] name = "fastrand" version = "2.4.1" @@ -1332,6 +1455,16 @@ dependencies = [ "subtle", ] +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -1477,122 +1610,1050 @@ dependencies = [ ] [[package]] -name = "futures-macro" -version = "0.3.32" +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generator" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f04ae4152da20c76fe800fa48659201d5cf627c5149ca0b707b69d7eef6cf9" +dependencies = [ + "cc", + "cfg-if", + "libc", + "log", + "rustversion", + "windows-link", + "windows-result", +] + +[[package]] +name = "generic-array" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "gitmirror" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "gix", + "line-numbers", + "prost", + "rustc-hash", + "tempfile", + "tokio", + "tokio-stream", + "tonic", + "tonic-prost", + "tonic-prost-build", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "gix" +version = "0.84.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae54ae0ebd1a5a3c3f8d95dd3b5ca6e63f4fed9bfd585e13801a97d7bde8f9ce" +dependencies = [ + "gix-actor", + "gix-archive", + "gix-attributes", + "gix-blame", + "gix-command", + "gix-commitgraph", + "gix-config", + "gix-credentials", + "gix-date", + "gix-diff", + "gix-dir", + "gix-discover", + "gix-error", + "gix-features", + "gix-filter", + "gix-fs", + "gix-glob", + "gix-hash", + "gix-hashtable", + "gix-ignore", + "gix-index", + "gix-lock", + "gix-mailmap", + "gix-merge", + "gix-negotiate", + "gix-object", + "gix-odb", + "gix-pack", + "gix-path", + "gix-pathspec", + "gix-prompt", + "gix-protocol", + "gix-ref", + "gix-refspec", + "gix-revision", + "gix-revwalk", + "gix-sec", + "gix-shallow", + "gix-status", + "gix-submodule", + "gix-tempfile", + "gix-trace", + "gix-traverse", + "gix-url", + "gix-utils", + "gix-validate", + "gix-worktree", + "gix-worktree-state", + "gix-worktree-stream", + "nonempty", + "parking_lot", + "regex", + "signal-hook", + "smallvec", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-actor" +version = "0.41.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bc998b8f746dda8565450d08a63b792ced9165d8c27a1ed3f02799ec6a7820f" +dependencies = [ + "bstr", + "gix-date", + "gix-error", +] + +[[package]] +name = "gix-archive" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16909cacc78936ab96f6c3be08379d0a2e88bfa3a7527972d2ed75c7517ef31e" +dependencies = [ + "bstr", + "gix-date", + "gix-error", + "gix-object", + "gix-worktree-stream", +] + +[[package]] +name = "gix-attributes" +version = "0.33.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39b40888d0ed415c0744a6cdc61eebf0304c9d26ab726725b718443c322e5ba4" +dependencies = [ + "bstr", + "gix-glob", + "gix-path", + "gix-quote", + "gix-trace", + "kstring", + "smallvec", + "thiserror 2.0.18", + "unicode-bom", +] + +[[package]] +name = "gix-bitmap" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ebef0c26ad305747649e727bbcd56a7b7910754eb7cea88f6dff6f93c51283" +dependencies = [ + "gix-error", +] + +[[package]] +name = "gix-blame" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d39a0c14af94c2edaa5eefe06d5ef2cdea55316ae9a9321314288e3f55fa4c0" +dependencies = [ + "gix-commitgraph", + "gix-date", + "gix-diff", + "gix-error", + "gix-hash", + "gix-object", + "gix-revwalk", + "gix-trace", + "gix-traverse", + "gix-worktree", + "smallvec", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-chunk" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9faee47943b638e58ddd5e275a4906ad3e4b6c8584f1d41bd18ab9032ec52afb" +dependencies = [ + "gix-error", +] + +[[package]] +name = "gix-command" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00706d4fef135ef4b01680d5218c6ee40cda8baf697b864296cbc887d19118f6" +dependencies = [ + "bstr", + "gix-path", + "gix-quote", + "gix-trace", + "shell-words", +] + +[[package]] +name = "gix-commitgraph" +version = "0.37.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f675d0df484a7f6a47e64bd6f311af489d947c0323b0564f36d14f3d7762abb" +dependencies = [ + "bstr", + "gix-chunk", + "gix-error", + "gix-hash", + "memmap2", + "nonempty", +] + +[[package]] +name = "gix-config" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f2372d4b49ca28431e7d150cab9d25edc1890f0184bd57eb0e917c7799e63de" +dependencies = [ + "bstr", + "gix-config-value", + "gix-features", + "gix-glob", + "gix-path", + "gix-ref", + "gix-sec", + "smallvec", + "thiserror 2.0.18", + "unicode-bom", +] + +[[package]] +name = "gix-config-value" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed42168329552f6c2e5df09665c104199d45d84bedb53683738a49b57fe1baab" +dependencies = [ + "bitflags 2.11.1", + "bstr", + "gix-path", + "libc", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-credentials" +version = "0.38.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40cd22f0dd71988be12d6e78b1709de2370e1957c5f107ff31e56caeba3745d" +dependencies = [ + "bstr", + "gix-command", + "gix-config-value", + "gix-date", + "gix-path", + "gix-prompt", + "gix-sec", + "gix-trace", + "gix-url", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-date" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d63f9e28b59ddeb1a1eb9e5cf986a9222b5d484947445edbc20473939cc7fd0" +dependencies = [ + "bstr", + "gix-error", + "itoa", + "jiff", +] + +[[package]] +name = "gix-diff" +version = "0.64.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b6d9528f32d94cef2edf39a1ac01fe5a0fc44ddbb18d9e44099936047c3302b" +dependencies = [ + "bstr", + "gix-attributes", + "gix-command", + "gix-filter", + "gix-fs", + "gix-hash", + "gix-imara-diff", + "gix-index", + "gix-object", + "gix-path", + "gix-pathspec", + "gix-tempfile", + "gix-trace", + "gix-traverse", + "gix-worktree", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-dir" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21bb2a53a6fd917ec499ed0bfb5b6887de7a15bd79197dcea7c987938749a9f1" +dependencies = [ + "bstr", + "gix-discover", + "gix-fs", + "gix-ignore", + "gix-index", + "gix-object", + "gix-path", + "gix-pathspec", + "gix-trace", + "gix-utils", + "gix-worktree", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-discover" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77bacdd12b7879d2178a80c58c2f319995e4654e1a7a23e3181e5c8a12b824f7" +dependencies = [ + "bstr", + "dunce", + "gix-fs", + "gix-path", + "gix-ref", + "gix-sec", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-error" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57831e199be480af90dcd7e459abed8a174c09ec9a6e2cc8f7ca6c54598b06b" +dependencies = [ + "bstr", +] + +[[package]] +name = "gix-features" +version = "0.48.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1849ae154d38bc403185be14fa871e38e3c93ee606875d94e207fdb9fba52dbc" +dependencies = [ + "bytes", + "bytesize", + "crc32fast", + "crossbeam-channel", + "gix-path", + "gix-trace", + "gix-utils", + "libc", + "once_cell", + "parking_lot", + "prodash", + "thiserror 2.0.18", + "walkdir", + "zlib-rs", +] + +[[package]] +name = "gix-filter" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecf74b7d16f6694ce4a3049074c41be0c7987105743674f1671807bd6dce09fa" +dependencies = [ + "bstr", + "encoding_rs", + "gix-attributes", + "gix-command", + "gix-hash", + "gix-object", + "gix-packetline", + "gix-path", + "gix-quote", + "gix-trace", + "gix-utils", + "smallvec", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-fs" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cdff46db8798e47e2f727d84b9379aac5add3dd3d9d0b07bb4d7d5d640771fe" +dependencies = [ + "bstr", + "fastrand", + "gix-features", + "gix-path", + "gix-utils", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-glob" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1fcb8ef5b16bcf874abe9b68d8abb3c0493c876d367ab824151f30a0f3f3756" +dependencies = [ + "bitflags 2.11.1", + "bstr", + "gix-features", + "gix-path", +] + +[[package]] +name = "gix-hash" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0926d3819c837750b4e03c7754901e73f68b8c9b690753a6372a1bed4eedce" +dependencies = [ + "faster-hex", + "gix-features", + "sha1-checked", + "sha2 0.11.0", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-hashtable" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e261d54091f0d1c729bc83f54548c071bdec60a697de1e58e88bdfd7a99d24e" +dependencies = [ + "gix-hash", + "hashbrown 0.17.1", + "parking_lot", +] + +[[package]] +name = "gix-ignore" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d491bab9bf2c9f341dc754f425c31d5d3f63aca615312167b82e1deeaca97d8d" +dependencies = [ + "bstr", + "gix-glob", + "gix-path", + "gix-trace", + "unicode-bom", +] + +[[package]] +name = "gix-imara-diff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b305d85504de270ad3525d726a6b69cc59ee7b2269b014387651107ab9f0755b" +dependencies = [ + "bstr", + "hashbrown 0.17.1", +] + +[[package]] +name = "gix-index" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e6b28cc592dc753adb58302bb14a64e412ee591a3bec77aa4df87bff74fa80d" +dependencies = [ + "bitflags 2.11.1", + "bstr", + "filetime", + "fnv", + "gix-bitmap", + "gix-features", + "gix-fs", + "gix-hash", + "gix-lock", + "gix-object", + "gix-traverse", + "gix-utils", + "gix-validate", + "hashbrown 0.17.1", + "itoa", + "libc", + "memmap2", + "rustix", + "smallvec", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-lock" +version = "23.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c9dedd9e90b0d47624d2ed241d394e09294118364e87b9b7e5f1fe755f3c2c" +dependencies = [ + "gix-tempfile", + "gix-utils", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-mailmap" +version = "0.33.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "195fd20808055824531be2fd0d34136d900e5fbca3ffb0a3c07e8beeefb9c828" +dependencies = [ + "bstr", + "gix-actor", + "gix-date", + "gix-error", +] + +[[package]] +name = "gix-merge" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7543e1eceb25fbbd1a29459c794148d8bafc1c77555eb386617a2d99b5371971" +dependencies = [ + "bstr", + "gix-command", + "gix-diff", + "gix-filter", + "gix-fs", + "gix-hash", + "gix-imara-diff", + "gix-index", + "gix-object", + "gix-path", + "gix-quote", + "gix-revision", + "gix-revwalk", + "gix-tempfile", + "gix-trace", + "gix-worktree", + "nonempty", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-negotiate" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "890c936a215bae25818c076cb881cb2e54d2c66ba947ba58b8dd47cff921bf55" +dependencies = [ + "bitflags 2.11.1", + "gix-commitgraph", + "gix-date", + "gix-hash", + "gix-object", + "gix-revwalk", +] + +[[package]] +name = "gix-object" +version = "0.61.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5cd857e29429c7213bdef3f5aef83f8cc124774fe8ae0d27b1607d218d6d525" +dependencies = [ + "bstr", + "gix-actor", + "gix-date", + "gix-features", + "gix-hash", + "gix-hashtable", + "gix-utils", + "gix-validate", + "itoa", + "smallvec", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-odb" +version = "0.81.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d004c32858b1556f2d7874405edb3c97dc78fc09beaa87d57bb077ee2858a7d" +dependencies = [ + "arc-swap", + "gix-features", + "gix-fs", + "gix-hash", + "gix-hashtable", + "gix-object", + "gix-pack", + "gix-path", + "gix-quote", + "memmap2", + "parking_lot", + "tempfile", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-pack" +version = "0.71.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e43626f2a27d1033674ec1a196b845614231e6bbd949d5e21c133045ff56b174" +dependencies = [ + "clru", + "gix-chunk", + "gix-error", + "gix-features", + "gix-hash", + "gix-hashtable", + "gix-object", + "gix-path", + "memmap2", + "smallvec", + "thiserror 2.0.18", + "uluru", +] + +[[package]] +name = "gix-packetline" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b217dd0ee0c4021ecf169a4a519b1b4f80d15e3f3765f3dc466223dc0ac891d7" +dependencies = [ + "bstr", + "faster-hex", + "gix-trace", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-path" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afa6ac14cd14939ea94a496ce7460daa6511c09f5b84757e9cfc6f9c8d0f93a6" +dependencies = [ + "bstr", + "gix-trace", + "gix-validate", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-pathspec" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3050783b41ee11511e1e8fb35623df81806194f4030395f14f48ea37c2798c9f" +dependencies = [ + "bitflags 2.11.1", + "bstr", + "gix-attributes", + "gix-config-value", + "gix-glob", + "gix-path", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-prompt" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ee604d7746080ae7e1023bf47204bcc2c5f307bfbe2306a3c90b1bfd1a2c6d8" +dependencies = [ + "gix-command", + "gix-config-value", + "parking_lot", + "rustix", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-protocol" +version = "0.62.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51dea3acb390707ab868f1f9584f18449eb95d869deffae96768e47d303595ee" +dependencies = [ + "bstr", + "gix-date", + "gix-features", + "gix-hash", + "gix-ref", + "gix-shallow", + "gix-transport", + "gix-utils", + "maybe-async", + "nonempty", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-quote" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6e541fc33cc2b783b7979040d445a0c86a2eca747c8faea4ca84230d06ae6ef" +dependencies = [ + "bstr", + "gix-error", + "gix-utils", +] + +[[package]] +name = "gix-ref" +version = "0.64.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c04f64c37eb7e6feb73c7060f8dc6f381cc5de5d53249bfd450bc48a86b2e8b" +dependencies = [ + "gix-actor", + "gix-features", + "gix-fs", + "gix-hash", + "gix-lock", + "gix-object", + "gix-path", + "gix-tempfile", + "gix-utils", + "gix-validate", + "memmap2", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-refspec" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b216ae06ec74b5f24ad0142026a997fb0a935b7410eaf9c1616fc3f0e6c5a6d3" +dependencies = [ + "bstr", + "gix-error", + "gix-glob", + "gix-hash", + "gix-revision", + "gix-validate", + "smallvec", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-revision" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b47c88884dd3c1a19a39da19d10211fcdea2809aadc86869b6e824a1774340f" +dependencies = [ + "bitflags 2.11.1", + "bstr", + "gix-commitgraph", + "gix-date", + "gix-error", + "gix-hash", + "gix-hashtable", + "gix-object", + "gix-revwalk", + "gix-trace", + "nonempty", +] + +[[package]] +name = "gix-revwalk" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85f5756abffe0917827aac683b13684ed99875bc398fa1f9b8f479b0681ef9e6" +dependencies = [ + "gix-commitgraph", + "gix-date", + "gix-error", + "gix-hash", + "gix-hashtable", + "gix-object", + "smallvec", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-sec" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8519976e4c7e486270740a5400369f37940779b80bd1377d94cfa1125d01b3" +dependencies = [ + "bitflags 2.11.1", + "gix-path", + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "gix-shallow" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a292fc2fe548c5dfa575479d16b445b0ddf1dd2f56f1fec6aed386f82553cd97" +dependencies = [ + "bstr", + "gix-hash", + "gix-lock", + "nonempty", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-status" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22042e385d28a34275e029d98f4970285045be14b9073658ca897923f2ed8700" +dependencies = [ + "bstr", + "filetime", + "gix-diff", + "gix-dir", + "gix-features", + "gix-filter", + "gix-fs", + "gix-hash", + "gix-index", + "gix-object", + "gix-path", + "gix-pathspec", + "gix-worktree", + "portable-atomic", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-submodule" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3059890ef054066c22a94bfc6a3eaba0d806aedcd630a0bc9e5783fd88884781" +dependencies = [ + "bstr", + "gix-config", + "gix-path", + "gix-pathspec", + "gix-refspec", + "gix-url", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-tempfile" +version = "23.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +checksum = "6ef60812443484e67bf84e444cc71b4c78ae62deb822221774a4fa0c57fdb17f" dependencies = [ - "proc-macro2", - "quote", - "syn", + "dashmap", + "gix-fs", + "libc", + "parking_lot", + "signal-hook", + "signal-hook-registry", + "tempfile", ] [[package]] -name = "futures-sink" -version = "0.3.32" +name = "gix-trace" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" +checksum = "44dc45eae785c0eb14173e0f152e6e224dcf4d45b6a6999a3aed22af541ad678" [[package]] -name = "futures-task" -version = "0.3.32" +name = "gix-transport" +version = "0.57.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +checksum = "186874f7ad1fb2f9a2f2aa9c2dabc7f9dd087bef74c1a0eee2b4a9cf0248fcb3" +dependencies = [ + "bstr", + "gix-command", + "gix-features", + "gix-packetline", + "gix-quote", + "gix-sec", + "gix-url", + "thiserror 2.0.18", +] [[package]] -name = "futures-util" -version = "0.3.32" +name = "gix-traverse" +version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +checksum = "e8de590ecc86a3b2870665f2288324fa9f7f8672c7fc2d4e020fdd81cd1f7aed" dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "slab", + "bitflags 2.11.1", + "gix-commitgraph", + "gix-date", + "gix-hash", + "gix-hashtable", + "gix-object", + "gix-revwalk", + "smallvec", + "thiserror 2.0.18", ] [[package]] -name = "generator" -version = "0.8.8" +name = "gix-url" +version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52f04ae4152da20c76fe800fa48659201d5cf627c5149ca0b707b69d7eef6cf9" +checksum = "65bb01ec69d55e82ccb7a19e264501ead4e6aac38463a8cebfdd81e22bb67ab2" dependencies = [ - "cc", - "cfg-if", - "libc", - "log", - "rustversion", - "windows-link", - "windows-result", + "bstr", + "gix-path", + "percent-encoding", + "thiserror 2.0.18", ] [[package]] -name = "generic-array" -version = "0.14.9" +name = "gix-utils" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +checksum = "66c50966184123caf580ffa64e28031a878597f1c7fceb8fe19566c38eb1b771" dependencies = [ - "typenum", - "version_check", - "zeroize", + "bstr", + "fastrand", + "unicode-normalization", ] [[package]] -name = "getrandom" -version = "0.2.17" +name = "gix-validate" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +checksum = "7bc6fc771c4063ba7cd2f47b91fb6076251c6a823b64b7fe7b8874b0fe4afae3" dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi", - "wasm-bindgen", + "bstr", ] [[package]] -name = "getrandom" -version = "0.3.4" +name = "gix-worktree" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +checksum = "cef414ed275e8407cd5d53d301e83be19700b0dd3f859d2434417b58f454a2d1" dependencies = [ - "cfg-if", - "js-sys", - "libc", - "r-efi 5.3.0", - "wasip2", - "wasm-bindgen", + "bstr", + "gix-attributes", + "gix-fs", + "gix-glob", + "gix-hash", + "gix-ignore", + "gix-index", + "gix-object", + "gix-path", + "gix-validate", ] [[package]] -name = "getrandom" -version = "0.4.2" +name = "gix-worktree-state" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +checksum = "4bffae8b3ca258fdd50370cd51f06deb4c76a3b43db3868bc28dde45ffa77d69" dependencies = [ - "cfg-if", - "libc", - "r-efi 6.0.0", - "wasip2", - "wasip3", + "bstr", + "gix-features", + "gix-filter", + "gix-fs", + "gix-index", + "gix-object", + "gix-path", + "gix-worktree", + "io-close", + "thiserror 2.0.18", ] [[package]] -name = "gitmirror" -version = "0.1.0" +name = "gix-worktree-stream" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25e9ed30100c63f7590bc581c225e53f731a53e06aa79a245739c07f7dcc557" dependencies = [ - "prost", - "tokio", - "tonic", - "tonic-prost", - "tonic-prost-build", - "tracing", - "tracing-subscriber", + "gix-attributes", + "gix-error", + "gix-features", + "gix-filter", + "gix-fs", + "gix-hash", + "gix-object", + "gix-path", + "gix-traverse", + "parking_lot", ] [[package]] @@ -1645,6 +2706,15 @@ dependencies = [ "byteorder", ] +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + [[package]] name = "hashbrown" version = "0.14.5" @@ -1682,6 +2752,11 @@ name = "hashbrown" version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] [[package]] name = "heapless" @@ -1690,13 +2765,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" dependencies = [ "atomic-polyfill", - "hash32", + "hash32 0.2.1", "rustc_version", "serde", "spin 0.9.8", "stable_deref_trait", ] +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32 0.3.1", + "stable_deref_trait", +] + [[package]] name = "heck" version = "0.4.1" @@ -1727,7 +2812,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest", + "digest 0.10.7", ] [[package]] @@ -1781,6 +2866,21 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "human_format" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaec953f16e5bcf6b8a3cb3aa959b17e5577dbd2693e94554c462c08be22624b" + +[[package]] +name = "hybrid-array" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" version = "1.10.0" @@ -2017,6 +3117,16 @@ dependencies = [ "rustversion", ] +[[package]] +name = "io-close" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cadcf447f06744f8ce713d2d6239bb5bde2c357a452397a9ed90c625da390bc" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "ipld-core" version = "0.4.3" @@ -2152,12 +3262,54 @@ dependencies = [ "serde_path_to_error", "serde_repr", "serde_with", - "sha2", + "sha2 0.10.9", "syn", "thiserror 2.0.18", "unicode-segmentation", ] +[[package]] +name = "jiff" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34f877a98676d2fb664698d74cc6a51ce6c484ce8c770f05d0108ec9090aeb46" +dependencies = [ + "defmt", + "jiff-static", + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", + "windows-link", +] + +[[package]] +name = "jiff-static" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0666b5ab5ecaca213fc2a85b8c0083d9004e84ee2d5f9a7e0017aaf50986f25f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c900ef84826f1338a557697dc8fc601df9ca9af4ac137c7fb61d4c6f2dfd3076" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + [[package]] name = "jobserver" version = "0.1.34" @@ -2189,7 +3341,16 @@ dependencies = [ "cfg-if", "ecdsa", "elliptic-curve", - "sha2", + "sha2 0.10.9", +] + +[[package]] +name = "kstring" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "558bf9508a558512042d3095138b1f7b8fe90c5467d94f9f1da28b3731c5dbd1" +dependencies = [ + "static_assertions", ] [[package]] @@ -2226,6 +3387,12 @@ version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +[[package]] +name = "line-numbers" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24ab6409a2f9ffae158ae823a012a7d7d68386663a14bedc260ca66eacf65c2c" + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -2326,6 +3493,17 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" +[[package]] +name = "maybe-async" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "746873a384ad60adc5db74471dfaba74bd278afbdcfd81db93fafcdfc8b5ca0c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "measure_time" version = "0.9.0" @@ -2490,7 +3668,7 @@ version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" dependencies = [ - "bitflags", + "bitflags 2.11.1", "cfg-if", "cfg_aliases", "libc", @@ -2507,6 +3685,12 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nonempty" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9737e026353e5cd0736f98eddae28665118eb6f6600902a7f50db585621fecb6" + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -2640,7 +3824,7 @@ dependencies = [ "ecdsa", "elliptic-curve", "primeorder", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -2804,6 +3988,15 @@ version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + [[package]] name = "postcard" version = "1.1.3" @@ -2813,7 +4006,7 @@ dependencies = [ "cobs", "embedded-io 0.4.0", "embedded-io 0.6.1", - "heapless", + "heapless 0.7.17", "serde", ] @@ -2860,6 +4053,28 @@ dependencies = [ "elliptic-curve", ] +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -2882,6 +4097,17 @@ dependencies = [ "yansi", ] +[[package]] +name = "prodash" +version = "31.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "962200e2d7d551451297d9fdce85138374019ada198e30ea9ede38034e27604c" +dependencies = [ + "bytesize", + "human_format", + "parking_lot", +] + [[package]] name = "prost" version = "0.14.3" @@ -3008,7 +4234,7 @@ version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9f068eba8e7071c5f9511831b44f32c740d5adf574e990f946ddb53db2f314e" dependencies = [ - "bitflags", + "bitflags 2.11.1", "memchr", "unicase", ] @@ -3195,7 +4421,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.11.1", ] [[package]] @@ -3352,7 +4578,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags", + "bitflags 2.11.1", "errno", "libc", "linux-raw-sys", @@ -3493,7 +4719,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags", + "bitflags 2.11.1", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -3687,8 +4913,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha1-checked" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89f599ac0c323ebb1c6082821a54962b839832b03984598375bff3975b804423" +dependencies = [ + "digest 0.10.7", + "sha1", ] [[package]] @@ -3698,8 +4934,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", ] [[package]] @@ -3711,6 +4958,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shell-words" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" + [[package]] name = "shlex" version = "1.3.0" @@ -3737,6 +4990,16 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "signal-hook" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a0c28ca5908dbdbcd52e6fdaa00358ab88637f8ab33e1f188dd510eb44b53d" +dependencies = [ + "libc", + "signal-hook-registry", +] + [[package]] name = "signal-hook-registry" version = "1.4.8" @@ -3753,7 +5016,7 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "digest", + "digest 0.10.7", "rand_core 0.6.4", ] @@ -3896,7 +5159,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ - "bitflags", + "bitflags 2.11.1", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -4264,6 +5527,7 @@ dependencies = [ "futures-core", "pin-project-lite", "tokio", + "tokio-util", ] [[package]] @@ -4477,7 +5741,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ "async-compression", - "bitflags", + "bitflags 2.11.1", "bytes", "futures-core", "futures-util", @@ -4678,18 +5942,42 @@ dependencies = [ "syn", ] +[[package]] +name = "uluru" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c8a2469e56e6e5095c82ccd3afb98dad95f7af7929aab6d8ba8d6e0f73657da" +dependencies = [ + "arrayvec", +] + [[package]] name = "unicase" version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" +[[package]] +name = "unicode-bom" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eec5d1121208364f6793f7d2e222bf75a915c19557537745b195b253dd64217" + [[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-segmentation" version = "1.13.2" @@ -4930,7 +6218,7 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags", + "bitflags 2.11.1", "hashbrown 0.15.5", "indexmap", "semver", @@ -5339,7 +6627,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags", + "bitflags 2.11.1", "indexmap", "log", "serde", @@ -5484,6 +6772,12 @@ dependencies = [ "syn", ] +[[package]] +name = "zlib-rs" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "977347db8caa080403f6b6b7c1cda9479a8e869316f7e13a59b19076a40f94e3" + [[package]] name = "zmij" version = "1.0.21" diff --git a/appview/models/pull.go b/appview/models/pull.go index e35d4d7f..9ba6851f 100644 --- a/appview/models/pull.go +++ b/appview/models/pull.go @@ -87,6 +87,18 @@ type Pull struct { Repo *Repo } +func (p *Pull) SourceRepoDid() syntax.DID { + if p.PullSource != nil && p.PullSource.RepoDid != nil { + return *p.PullSource.RepoDid + } + return p.RepoDid +} + +// HEAD returns PR's current HEAD commit ID. +func (p *Pull) HEAD() string { + panic("unimplemented") +} + // NOTE: This method does not include patch blob in returned atproto record func (p Pull) AsRecord() tangled.RepoPull { mentions := make([]string, len(p.Mentions)) diff --git a/appview/pages/pages.go b/appview/pages/pages.go index b0ce4608..2c6e2da3 100644 --- a/appview/pages/pages.go +++ b/appview/pages/pages.go @@ -1427,6 +1427,51 @@ type RepoSinglePullParams struct { VouchSkips map[syntax.DID]bool } +type PullPageBaseParams struct { + BaseParams + Pull *models.Pull + + Backlinks []models.RichReferenceLink + Comments []models.Comment + Commits []types.Commit // all commits between .. + + LabelDefs map[string]*models.LabelDefinition + Reactions map[syntax.ATURI]map[models.ReactionKind]models.ReactionDisplayData + UserReacted map[syntax.ATURI]map[models.ReactionKind]bool + VouchRelationships map[syntax.DID]*models.VouchRelationship + VouchSkips map[syntax.DID]bool + + // diff, branch-delete-status, merge-check, resubmit-check, pipelines will be lazy-loaded. +} + +// /pulls/123/2/1a2b3c..d4e5f6 +type PullDiffParams struct { + PullPageBaseParams + Version int + BaseCommitId string + HeadCommitId string + + ErrorMsg string +} + +// /pulls/123/1..2/abcdef +type PullInterdiffParams struct { + PullPageBaseParams + Version1 int + Version2 int + ChangeId string // optional change-id filter + + ErrorMsg string +} + +func (p *Pages) PullDiff(w io.Writer, params PullDiffParams) error { + panic("unimplemented") +} + +func (p *Pages) PullInterdiff(w io.Writer, params PullInterdiffParams) error { + panic("unimplemented") +} + func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error { params.Active = "pulls" return p.executeRepo("repo/pulls/pull", w, params) diff --git a/appview/pulls/pull2.go b/appview/pulls/pull2.go new file mode 100644 index 00000000..bfc0a69e --- /dev/null +++ b/appview/pulls/pull2.go @@ -0,0 +1,280 @@ +package pulls + +import ( + "context" + "fmt" + "net/http" + "strconv" + + "github.com/bluesky-social/indigo/atproto/syntax" + "github.com/bluesky-social/indigo/lex/util" + indigoxrpc "github.com/bluesky-social/indigo/xrpc" + "github.com/go-chi/chi/v5" + "golang.org/x/sync/errgroup" + "tangled.org/core/api/tangled" + "tangled.org/core/appview/models" + "tangled.org/core/appview/pages" + "tangled.org/core/types" +) + +// NOTE: parsing object in middleware is bad pattern +// you will have to check if object exist in context "just in case" +// so it's better to make helper function that can read the url pattern instead. + + +// A -- B -- C +// (master) (pr/123/0) +// +// A -- B -- C +// \ (pr/123/0) +// `-- D <- B' <- C' +// (master) (pr/123/1) + +// 1. rebase B<-C to D +// 2. compare tree of C and D + +// PullInterDiff is router for /pulls/{pull}/{version}..{version}/{change} +// +// Examples: +// - /pulls/123/0..2/all +// - /pulls/123/0..2/nrpytyzw +func (s *Pulls) PullInterDiff(w http.ResponseWriter, r *http.Request) { + l := s.logger.With("handler", "PullRound") + ctx := r.Context() + + pull, ok := r.Context().Value("pull").(*models.Pull) + if !ok { + l.Error("failed to get pull") + s.pages.Error500(w) + return + } + + var ( + version1 = 0 + version2 = 0 + changeId = chi.URLParam(r, "*") + ) + if changeId == "all" { + changeId = "" + } + + // defer render + var params pages.PullInterdiffParams + params.Pull = pull + params.Version1 = version1 + params.Version2 = version2 + params.ChangeId = changeId + defer s.pages.PullInterdiff(w, params) + + // 1. resolve target branch -> (branch, commit) + xrpcc := &indigoxrpc.Client{Host: s.config.KnotMirror.Url} + branch, err := tangled.GitTempGetBranch(ctx, xrpcc, pull.TargetBranch, pull.RepoDid.String()) + if err != nil { + panic("unimplemented") + } + + base := branch.Hash + head1 := "" // pull.Versions[version1].Head + head2 := "" // pull.Versions[version2].Head + + // 1. log commits from base..head1 and base..head2 + var commits1, commits2 []types.Commit + g, gctx := errgroup.WithContext(ctx) + g.Go(func() error { + commits1, err = getTempListCommits(gctx, xrpcc, pull.SourceRepoDid(), base, head1) + return err + }) + g.Go(func() error { + commits2, err = getTempListCommits(gctx, xrpcc, pull.SourceRepoDid(), base, head2) + return err + }) + if err := g.Wait(); err != nil { + params.ErrorMsg = "something something" + panic("unimplemented") + } + + if changeId != "" { + // interdiff by change-id + var old, new *types.Commit + for _, commit := range commits1 { + if commit.ChangeId == changeId { + old = &commit + break + } + } + for _, commit := range commits2 { + if commit.ChangeId == changeId { + new = &commit + break + } + } + _, _ = old, new + panic("unimplemented") + } else { + // interdiff of two commit ranges + panic("unimplemented") + } +} + +// PullDiff is router for /pulls/{pull}/{version}/{commit}..{commit} +// +// Examples: +// - /pulls/123/latest +// - /pulls/123/2/head +// - /pulls/123/2/base..head +// - /pulls/123/2/a53ab251e..d8add468c +// - /pulls/123/2/d8add468c +func (s *Pulls) PullDiff(w http.ResponseWriter, r *http.Request) { + l := s.logger.With("handler", "PullRound") + ctx := r.Context() + + pull, ok := r.Context().Value("pull").(*models.Pull) + if !ok { + l.Error("failed to get pull") + s.pages.Error500(w) + return + } + + var err error + + var version int + var versionRaw = chi.URLParam(r, "version") + if versionRaw == "latest" { + version = pull.LastRoundNumber() + } else { + version, err = strconv.Atoi(versionRaw) + if err != nil { + // invalid version number. redirect + http.Redirect(w, r, + fmt.Sprintf("/%s/pulls/%d/latest", pull.Repo.RepoIdentifier(), pull.ID), + http.StatusSeeOther, + ) + return + } + } + + var range_ = chi.URLParam(r, "*") + base, head, err := parseRevRange(range_) + if err != nil { + http.Redirect(w, r, + fmt.Sprintf("/%s/pulls/%d/%s", pull.Repo.RepoIdentifier(), pull.ID, versionRaw), + http.StatusSeeOther, + ) + return + } + + // defer render + var params pages.PullDiffParams + params.Pull = pull + params.Version = version + defer s.pages.PullDiff(w, params) + + // 1. resolve target branch -> (branch, commit) + xrpcc := &indigoxrpc.Client{Host: s.config.KnotMirror.Url} + branch, err := tangled.GitTempGetBranch(ctx, xrpcc, pull.TargetBranch, pull.RepoDid.String()) + if err != nil { + l.Warn("Failed to resolve target branch", "branch", pull.TargetBranch, "err", err) + params.ErrorMsg = fmt.Sprintf("Failed to resolve target branch %q", pull.TargetBranch) + return + } + + if base == "base" { + base = branch.Hash + } + if head == "head" { + head = pull.HEAD() + } + + sourceRepoDid := pull.SourceRepoDid() + + // 2. list diverged commits using knotmirror (BASE..HEAD) -> ([]commit) + // - knotmirror needs on-demand fetch implementation for this + commits, err := getTempListCommits(ctx, xrpcc, sourceRepoDid, base, head) + if err != nil { + panic("unimplemented") + } + + // 3. list every commits in UI. They will be lazy-loaded + params.Commits = commits +} + +// htmx fragment. render diff between commits +func (s *Pulls) PullDiffFragment(w http.ResponseWriter, r *http.Request) { + // var ( + // base = r.URL.Query().Get("base") // base commit ID + // head = r.URL.Query().Get("head") // head commit ID + // unified = r.URL.Query().Get("view") == "unified" + // ) + + // 1. get commit object + // 2. get diff between parent..commit (knotmirror), parse that diff + // 3. fetch each file entries (& run syntax highlight) <- skip this part for stage 1. we will do this at stage 2. + // 4. render diff +} + +// htmx fragment. render interdiff between changes +func (s *Pulls) PullInterdiffFragment(w http.ResponseWriter, r *http.Request) { + // var ( + // base1 = r.URL.Query().Get("base1") // base1 commit ID + // base2 = r.URL.Query().Get("base2") // base2 commit ID + // head1 = r.URL.Query().Get("head1") // head1 commit ID + // head2 = r.URL.Query().Get("head2") // head1 commit ID + // unified = r.URL.Query().Get("view") == "unified" + // ) + + // 1. compute interdiff. (knotmirror) + // 2. return rich diff data. (knotmirror) + // 3. load old/new blobs & run syntax highlight + // 4. render diff +} + +// parseRevRange parses .. string. +// base and head will default to "base" and "head" when omitted. +func parseRevRange(range_ string) (base string, head string, err error) { + panic("unimplemented") +} + +// parseVersionRange parses .. string. +// Each versions will default to "base" and "latest" when omitted. +func parseVersionRange(range_ string) (base string, head string, err error) { + panic("unimplemented") +} + +func getTempListCommits(ctx context.Context, xrpcc util.LexClient, repo syntax.DID, base, head string) ([]types.Commit, error) { + panic("unimplemented") + // raw, err := tangled.GitTempListCommits(ctx, xrpcc, "", 1000, head, repo.String()) + // if err != nil { + // return nil, err + // } + // + // var xrpcResp types.RepoLogResponse + // if err := json.Unmarshal(raw, &xrpcResp); err != nil { + // return nil, fmt.Errorf("failed to decode XRPC response: %w", err) + // } + // + // return xrpcResp.Commits, nil +} + +// htmx fragment. render interdiff between commits +func (s *Pulls) PullInterDiffFragment(w http.ResponseWriter, r *http.Request) { + panic("unimplemented") +} + +// gitmirror +// - git.ListCommitsSinceMergeBase(repo, base, head) +// - git.Diff(repo, base, head, mode) +// - git.Interdiff(repo, + +// for interdiff, we want: from{start,end}, to{start,end} +// 1. squash from.start ~ from.end into one commit +// 2. rebase that commit to to.start.parent() +// 3. diff from_squashed.tree and to.end.tree + +// we want git log BASE..HEAD (only commits in HEAD) diverged=false +// and git diff BASE...HEAD (changes from HEAD since merge-base) absolute=false + +// commands.go:56 picks comparison type: +// - diff BASE..HEAD (COMPARISON_TYPE_ONLY_IN_HEAD) = direct +// - diff BASE...HEAD (COMPARISON_TYPE_INTERSECTION) = merge-base. Server resolves merge-base via g.MergeBase() first (diff.go:43) then diffs. +// we want second one. we should compute merge-base first. +// we can have diff --git a/buf.yaml b/buf.yaml index 3c20343f..dc143378 100644 --- a/buf.yaml +++ b/buf.yaml @@ -1,5 +1,6 @@ version: v2 modules: - path: spindle/agentproto + - path: gitmirror/proto deps: - buf.build/bufbuild/protovalidate diff --git a/docker-compose.yml b/docker-compose.yml index dc210d34..457fb2f7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -283,6 +283,20 @@ services: condition: service_started networks: [tngl] + gitmirror: + build: + context: . + dockerfile: localinfra/gitmirror.Dockerfile + restart: unless-stopped + environment: + RUST_LOG: gitmirror=debug + GITMIRROR_ADDR: 0.0.0.0:9000 + GITMIRROR_REPO_BASE: /data/repos + volumes: + - knotmirror-data:/data + - ./localinfra/certs/root.crt:/usr/local/share/ca-certificates/caddy.crt:ro + networks: [tngl] + zoekt-webserver: build: context: https://tangled.org/boltless.me/zoekt.git#tngl @@ -379,6 +393,7 @@ services: - spindle.tngl.boltless.dev - tngl.boltless.dev - mirror.tngl.boltless.dev + - gitmirror.tngl.boltless.dev - zoekt.tngl.boltless.dev volumes: diff --git a/gitmirror/Cargo.toml b/gitmirror/Cargo.toml index 0a72780a..908bb1ee 100644 --- a/gitmirror/Cargo.toml +++ b/gitmirror/Cargo.toml @@ -6,15 +6,24 @@ license.workspace = true rust-version.workspace = true [dependencies] +anyhow = { workspace = true } +clap = { workspace = true } tonic = { workspace = true } tonic-prost = { workspace = true } prost = { workspace = true } -tokio = { workspace = true, features = ["net"] } +tokio = { workspace = true, features = ["net", "rt-multi-thread", "sync"] } +tokio-stream = { workspace = true, features = ["sync"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } +line-numbers = "0.4.0" +rustc-hash = "2.1.2" +gix = { version = "0.84", features = ["parallel", "blob-diff", "merge", "sha1", "sha256", "revision", "tree-editor"] } [build-dependencies] tonic-prost-build = { workspace = true } +[dev-dependencies] +tempfile = "3" + [lints] workspace = true diff --git a/gitmirror/README.md b/gitmirror/README.md new file mode 100644 index 00000000..e07940c5 --- /dev/null +++ b/gitmirror/README.md @@ -0,0 +1,3 @@ +```sh +buf generate --template gitmirror/buf.gen.yaml --path gitmirror/proto/gitmirror/v1/gitmirror.proto +``` diff --git a/gitmirror/proto/gen/gitmirror.pb.go b/gitmirror/proto/gen/gitmirror.pb.go new file mode 100644 index 00000000..d9eaf289 --- /dev/null +++ b/gitmirror/proto/gen/gitmirror.pb.go @@ -0,0 +1,728 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: gitmirror/v1/gitmirror.proto + +package gitmirrorv1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type CommitsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // repo carries the DID as a string; parsing/validation is an impl detail. + Repo string `protobuf:"bytes,1,opt,name=repo,proto3" json:"repo,omitempty"` + Options *CommitsOptions `protobuf:"bytes,2,opt,name=options,proto3" json:"options,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CommitsRequest) Reset() { + *x = CommitsRequest{} + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CommitsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CommitsRequest) ProtoMessage() {} + +func (x *CommitsRequest) ProtoReflect() protoreflect.Message { + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CommitsRequest.ProtoReflect.Descriptor instead. +func (*CommitsRequest) Descriptor() ([]byte, []int) { + return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{0} +} + +func (x *CommitsRequest) GetRepo() string { + if x != nil { + return x.Repo + } + return "" +} + +func (x *CommitsRequest) GetOptions() *CommitsOptions { + if x != nil { + return x.Options + } + return nil +} + +type CommitsOptions struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CommitsOptions) Reset() { + *x = CommitsOptions{} + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CommitsOptions) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CommitsOptions) ProtoMessage() {} + +func (x *CommitsOptions) ProtoReflect() protoreflect.Message { + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CommitsOptions.ProtoReflect.Descriptor instead. +func (*CommitsOptions) Descriptor() ([]byte, []int) { + return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{1} +} + +type CommitsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // maps the Go []string return. + Commits []string `protobuf:"bytes,1,rep,name=commits,proto3" json:"commits,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CommitsResponse) Reset() { + *x = CommitsResponse{} + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CommitsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CommitsResponse) ProtoMessage() {} + +func (x *CommitsResponse) ProtoReflect() protoreflect.Message { + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CommitsResponse.ProtoReflect.Descriptor instead. +func (*CommitsResponse) Descriptor() ([]byte, []int) { + return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{2} +} + +func (x *CommitsResponse) GetCommits() []string { + if x != nil { + return x.Commits + } + return nil +} + +type DiffRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // repo carries the DID as a string; parsing/validation is an impl detail. + Repo string `protobuf:"bytes,1,opt,name=repo,proto3" json:"repo,omitempty"` + // base/head are commit-ish ref strings. + Base string `protobuf:"bytes,2,opt,name=base,proto3" json:"base,omitempty"` + Head string `protobuf:"bytes,3,opt,name=head,proto3" json:"head,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DiffRequest) Reset() { + *x = DiffRequest{} + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DiffRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DiffRequest) ProtoMessage() {} + +func (x *DiffRequest) ProtoReflect() protoreflect.Message { + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DiffRequest.ProtoReflect.Descriptor instead. +func (*DiffRequest) Descriptor() ([]byte, []int) { + return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{3} +} + +func (x *DiffRequest) GetRepo() string { + if x != nil { + return x.Repo + } + return "" +} + +func (x *DiffRequest) GetBase() string { + if x != nil { + return x.Base + } + return "" +} + +func (x *DiffRequest) GetHead() string { + if x != nil { + return x.Head + } + return "" +} + +type InterdiffRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // repo carries the DID as a string. + Repo string `protobuf:"bytes,1,opt,name=repo,proto3" json:"repo,omitempty"` + // Old patch range (from_base..from_head) and new patch range (to_base..to_head). + FromBase string `protobuf:"bytes,2,opt,name=from_base,json=fromBase,proto3" json:"from_base,omitempty"` + FromHead string `protobuf:"bytes,3,opt,name=from_head,json=fromHead,proto3" json:"from_head,omitempty"` + ToBase string `protobuf:"bytes,4,opt,name=to_base,json=toBase,proto3" json:"to_base,omitempty"` + ToHead string `protobuf:"bytes,5,opt,name=to_head,json=toHead,proto3" json:"to_head,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *InterdiffRequest) Reset() { + *x = InterdiffRequest{} + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *InterdiffRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*InterdiffRequest) ProtoMessage() {} + +func (x *InterdiffRequest) ProtoReflect() protoreflect.Message { + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use InterdiffRequest.ProtoReflect.Descriptor instead. +func (*InterdiffRequest) Descriptor() ([]byte, []int) { + return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{4} +} + +func (x *InterdiffRequest) GetRepo() string { + if x != nil { + return x.Repo + } + return "" +} + +func (x *InterdiffRequest) GetFromBase() string { + if x != nil { + return x.FromBase + } + return "" +} + +func (x *InterdiffRequest) GetFromHead() string { + if x != nil { + return x.FromHead + } + return "" +} + +func (x *InterdiffRequest) GetToBase() string { + if x != nil { + return x.ToBase + } + return "" +} + +func (x *InterdiffRequest) GetToHead() string { + if x != nil { + return x.ToHead + } + return "" +} + +type FileContent struct { + state protoimpl.MessageState `protogen:"open.v1"` + Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` + Oid string `protobuf:"bytes,2,opt,name=oid,proto3" json:"oid,omitempty"` + Size uint64 `protobuf:"varint,3,opt,name=size,proto3" json:"size,omitempty"` + IsBinary bool `protobuf:"varint,4,opt,name=is_binary,json=isBinary,proto3" json:"is_binary,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FileContent) Reset() { + *x = FileContent{} + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FileContent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FileContent) ProtoMessage() {} + +func (x *FileContent) ProtoReflect() protoreflect.Message { + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FileContent.ProtoReflect.Descriptor instead. +func (*FileContent) Descriptor() ([]byte, []int) { + return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{5} +} + +func (x *FileContent) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *FileContent) GetOid() string { + if x != nil { + return x.Oid + } + return "" +} + +func (x *FileContent) GetSize() uint64 { + if x != nil { + return x.Size + } + return 0 +} + +func (x *FileContent) GetIsBinary() bool { + if x != nil { + return x.IsBinary + } + return false +} + +// One aligned line pair; a side is absent (None) when unset. +type LinePair struct { + state protoimpl.MessageState `protogen:"open.v1"` + Lhs *uint32 `protobuf:"varint,1,opt,name=lhs,proto3,oneof" json:"lhs,omitempty"` + Rhs *uint32 `protobuf:"varint,2,opt,name=rhs,proto3,oneof" json:"rhs,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LinePair) Reset() { + *x = LinePair{} + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LinePair) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LinePair) ProtoMessage() {} + +func (x *LinePair) ProtoReflect() protoreflect.Message { + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LinePair.ProtoReflect.Descriptor instead. +func (*LinePair) Descriptor() ([]byte, []int) { + return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{6} +} + +func (x *LinePair) GetLhs() uint32 { + if x != nil && x.Lhs != nil { + return *x.Lhs + } + return 0 +} + +func (x *LinePair) GetRhs() uint32 { + if x != nil && x.Rhs != nil { + return *x.Rhs + } + return 0 +} + +type Hunk struct { + state protoimpl.MessageState `protogen:"open.v1"` + // LineNumber (u32) sets containing novel content per side. + NovelLhs []uint32 `protobuf:"varint,1,rep,packed,name=novel_lhs,json=novelLhs,proto3" json:"novel_lhs,omitempty"` + NovelRhs []uint32 `protobuf:"varint,2,rep,packed,name=novel_rhs,json=novelRhs,proto3" json:"novel_rhs,omitempty"` + Lines []*LinePair `protobuf:"bytes,3,rep,name=lines,proto3" json:"lines,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Hunk) Reset() { + *x = Hunk{} + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Hunk) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Hunk) ProtoMessage() {} + +func (x *Hunk) ProtoReflect() protoreflect.Message { + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Hunk.ProtoReflect.Descriptor instead. +func (*Hunk) Descriptor() ([]byte, []int) { + return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{7} +} + +func (x *Hunk) GetNovelLhs() []uint32 { + if x != nil { + return x.NovelLhs + } + return nil +} + +func (x *Hunk) GetNovelRhs() []uint32 { + if x != nil { + return x.NovelRhs + } + return nil +} + +func (x *Hunk) GetLines() []*LinePair { + if x != nil { + return x.Lines + } + return nil +} + +// Number of bytes per side when the files differ (Rust Option<(usize, usize)>). +type ByteChanges struct { + state protoimpl.MessageState `protogen:"open.v1"` + Lhs uint64 `protobuf:"varint,1,opt,name=lhs,proto3" json:"lhs,omitempty"` + Rhs uint64 `protobuf:"varint,2,opt,name=rhs,proto3" json:"rhs,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ByteChanges) Reset() { + *x = ByteChanges{} + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ByteChanges) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ByteChanges) ProtoMessage() {} + +func (x *ByteChanges) ProtoReflect() protoreflect.Message { + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ByteChanges.ProtoReflect.Descriptor instead. +func (*ByteChanges) Descriptor() ([]byte, []int) { + return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{8} +} + +func (x *ByteChanges) GetLhs() uint64 { + if x != nil { + return x.Lhs + } + return 0 +} + +func (x *ByteChanges) GetRhs() uint64 { + if x != nil { + return x.Rhs + } + return 0 +} + +type FileDiff struct { + state protoimpl.MessageState `protogen:"open.v1"` + LhsSrc *FileContent `protobuf:"bytes,1,opt,name=lhs_src,json=lhsSrc,proto3" json:"lhs_src,omitempty"` + RhsSrc *FileContent `protobuf:"bytes,2,opt,name=rhs_src,json=rhsSrc,proto3" json:"rhs_src,omitempty"` + Hunks []*Hunk `protobuf:"bytes,3,rep,name=hunks,proto3" json:"hunks,omitempty"` + HasByteChanges *ByteChanges `protobuf:"bytes,4,opt,name=has_byte_changes,json=hasByteChanges,proto3,oneof" json:"has_byte_changes,omitempty"` + HasSyntacticChanges bool `protobuf:"varint,5,opt,name=has_syntactic_changes,json=hasSyntacticChanges,proto3" json:"has_syntactic_changes,omitempty"` // TODO: lhs_positions / rhs_positions (MatchedPos) — AST, added later. + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FileDiff) Reset() { + *x = FileDiff{} + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FileDiff) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FileDiff) ProtoMessage() {} + +func (x *FileDiff) ProtoReflect() protoreflect.Message { + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FileDiff.ProtoReflect.Descriptor instead. +func (*FileDiff) Descriptor() ([]byte, []int) { + return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{9} +} + +func (x *FileDiff) GetLhsSrc() *FileContent { + if x != nil { + return x.LhsSrc + } + return nil +} + +func (x *FileDiff) GetRhsSrc() *FileContent { + if x != nil { + return x.RhsSrc + } + return nil +} + +func (x *FileDiff) GetHunks() []*Hunk { + if x != nil { + return x.Hunks + } + return nil +} + +func (x *FileDiff) GetHasByteChanges() *ByteChanges { + if x != nil { + return x.HasByteChanges + } + return nil +} + +func (x *FileDiff) GetHasSyntacticChanges() bool { + if x != nil { + return x.HasSyntacticChanges + } + return false +} + +var File_gitmirror_v1_gitmirror_proto protoreflect.FileDescriptor + +const file_gitmirror_v1_gitmirror_proto_rawDesc = "" + + "\n" + + "\x1cgitmirror/v1/gitmirror.proto\x12\fgitmirror.v1\"\\\n" + + "\x0eCommitsRequest\x12\x12\n" + + "\x04repo\x18\x01 \x01(\tR\x04repo\x126\n" + + "\aoptions\x18\x02 \x01(\v2\x1c.gitmirror.v1.CommitsOptionsR\aoptions\"\x10\n" + + "\x0eCommitsOptions\"+\n" + + "\x0fCommitsResponse\x12\x18\n" + + "\acommits\x18\x01 \x03(\tR\acommits\"I\n" + + "\vDiffRequest\x12\x12\n" + + "\x04repo\x18\x01 \x01(\tR\x04repo\x12\x12\n" + + "\x04base\x18\x02 \x01(\tR\x04base\x12\x12\n" + + "\x04head\x18\x03 \x01(\tR\x04head\"\x92\x01\n" + + "\x10InterdiffRequest\x12\x12\n" + + "\x04repo\x18\x01 \x01(\tR\x04repo\x12\x1b\n" + + "\tfrom_base\x18\x02 \x01(\tR\bfromBase\x12\x1b\n" + + "\tfrom_head\x18\x03 \x01(\tR\bfromHead\x12\x17\n" + + "\ato_base\x18\x04 \x01(\tR\x06toBase\x12\x17\n" + + "\ato_head\x18\x05 \x01(\tR\x06toHead\"d\n" + + "\vFileContent\x12\x12\n" + + "\x04path\x18\x01 \x01(\tR\x04path\x12\x10\n" + + "\x03oid\x18\x02 \x01(\tR\x03oid\x12\x12\n" + + "\x04size\x18\x03 \x01(\x04R\x04size\x12\x1b\n" + + "\tis_binary\x18\x04 \x01(\bR\bisBinary\"H\n" + + "\bLinePair\x12\x15\n" + + "\x03lhs\x18\x01 \x01(\rH\x00R\x03lhs\x88\x01\x01\x12\x15\n" + + "\x03rhs\x18\x02 \x01(\rH\x01R\x03rhs\x88\x01\x01B\x06\n" + + "\x04_lhsB\x06\n" + + "\x04_rhs\"n\n" + + "\x04Hunk\x12\x1b\n" + + "\tnovel_lhs\x18\x01 \x03(\rR\bnovelLhs\x12\x1b\n" + + "\tnovel_rhs\x18\x02 \x03(\rR\bnovelRhs\x12,\n" + + "\x05lines\x18\x03 \x03(\v2\x16.gitmirror.v1.LinePairR\x05lines\"1\n" + + "\vByteChanges\x12\x10\n" + + "\x03lhs\x18\x01 \x01(\x04R\x03lhs\x12\x10\n" + + "\x03rhs\x18\x02 \x01(\x04R\x03rhs\"\xaf\x02\n" + + "\bFileDiff\x122\n" + + "\alhs_src\x18\x01 \x01(\v2\x19.gitmirror.v1.FileContentR\x06lhsSrc\x122\n" + + "\arhs_src\x18\x02 \x01(\v2\x19.gitmirror.v1.FileContentR\x06rhsSrc\x12(\n" + + "\x05hunks\x18\x03 \x03(\v2\x12.gitmirror.v1.HunkR\x05hunks\x12H\n" + + "\x10has_byte_changes\x18\x04 \x01(\v2\x19.gitmirror.v1.ByteChangesH\x00R\x0ehasByteChanges\x88\x01\x01\x122\n" + + "\x15has_syntactic_changes\x18\x05 \x01(\bR\x13hasSyntacticChangesB\x13\n" + + "\x11_has_byte_changes2\xde\x01\n" + + "\x10GitMirrorService\x12F\n" + + "\aCommits\x12\x1c.gitmirror.v1.CommitsRequest\x1a\x1d.gitmirror.v1.CommitsResponse\x12;\n" + + "\x04Diff\x12\x19.gitmirror.v1.DiffRequest\x1a\x16.gitmirror.v1.FileDiff0\x01\x12E\n" + + "\tInterdiff\x12\x1e.gitmirror.v1.InterdiffRequest\x1a\x16.gitmirror.v1.FileDiff0\x01B2Z0tangled.org/core/gitmirror/proto/gen;gitmirrorv1b\x06proto3" + +var ( + file_gitmirror_v1_gitmirror_proto_rawDescOnce sync.Once + file_gitmirror_v1_gitmirror_proto_rawDescData []byte +) + +func file_gitmirror_v1_gitmirror_proto_rawDescGZIP() []byte { + file_gitmirror_v1_gitmirror_proto_rawDescOnce.Do(func() { + file_gitmirror_v1_gitmirror_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_gitmirror_v1_gitmirror_proto_rawDesc), len(file_gitmirror_v1_gitmirror_proto_rawDesc))) + }) + return file_gitmirror_v1_gitmirror_proto_rawDescData +} + +var file_gitmirror_v1_gitmirror_proto_msgTypes = make([]protoimpl.MessageInfo, 10) +var file_gitmirror_v1_gitmirror_proto_goTypes = []any{ + (*CommitsRequest)(nil), // 0: gitmirror.v1.CommitsRequest + (*CommitsOptions)(nil), // 1: gitmirror.v1.CommitsOptions + (*CommitsResponse)(nil), // 2: gitmirror.v1.CommitsResponse + (*DiffRequest)(nil), // 3: gitmirror.v1.DiffRequest + (*InterdiffRequest)(nil), // 4: gitmirror.v1.InterdiffRequest + (*FileContent)(nil), // 5: gitmirror.v1.FileContent + (*LinePair)(nil), // 6: gitmirror.v1.LinePair + (*Hunk)(nil), // 7: gitmirror.v1.Hunk + (*ByteChanges)(nil), // 8: gitmirror.v1.ByteChanges + (*FileDiff)(nil), // 9: gitmirror.v1.FileDiff +} +var file_gitmirror_v1_gitmirror_proto_depIdxs = []int32{ + 1, // 0: gitmirror.v1.CommitsRequest.options:type_name -> gitmirror.v1.CommitsOptions + 6, // 1: gitmirror.v1.Hunk.lines:type_name -> gitmirror.v1.LinePair + 5, // 2: gitmirror.v1.FileDiff.lhs_src:type_name -> gitmirror.v1.FileContent + 5, // 3: gitmirror.v1.FileDiff.rhs_src:type_name -> gitmirror.v1.FileContent + 7, // 4: gitmirror.v1.FileDiff.hunks:type_name -> gitmirror.v1.Hunk + 8, // 5: gitmirror.v1.FileDiff.has_byte_changes:type_name -> gitmirror.v1.ByteChanges + 0, // 6: gitmirror.v1.GitMirrorService.Commits:input_type -> gitmirror.v1.CommitsRequest + 3, // 7: gitmirror.v1.GitMirrorService.Diff:input_type -> gitmirror.v1.DiffRequest + 4, // 8: gitmirror.v1.GitMirrorService.Interdiff:input_type -> gitmirror.v1.InterdiffRequest + 2, // 9: gitmirror.v1.GitMirrorService.Commits:output_type -> gitmirror.v1.CommitsResponse + 9, // 10: gitmirror.v1.GitMirrorService.Diff:output_type -> gitmirror.v1.FileDiff + 9, // 11: gitmirror.v1.GitMirrorService.Interdiff:output_type -> gitmirror.v1.FileDiff + 9, // [9:12] is the sub-list for method output_type + 6, // [6:9] is the sub-list for method input_type + 6, // [6:6] is the sub-list for extension type_name + 6, // [6:6] is the sub-list for extension extendee + 0, // [0:6] is the sub-list for field type_name +} + +func init() { file_gitmirror_v1_gitmirror_proto_init() } +func file_gitmirror_v1_gitmirror_proto_init() { + if File_gitmirror_v1_gitmirror_proto != nil { + return + } + file_gitmirror_v1_gitmirror_proto_msgTypes[6].OneofWrappers = []any{} + file_gitmirror_v1_gitmirror_proto_msgTypes[9].OneofWrappers = []any{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_gitmirror_v1_gitmirror_proto_rawDesc), len(file_gitmirror_v1_gitmirror_proto_rawDesc)), + NumEnums: 0, + NumMessages: 10, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_gitmirror_v1_gitmirror_proto_goTypes, + DependencyIndexes: file_gitmirror_v1_gitmirror_proto_depIdxs, + MessageInfos: file_gitmirror_v1_gitmirror_proto_msgTypes, + }.Build() + File_gitmirror_v1_gitmirror_proto = out.File + file_gitmirror_v1_gitmirror_proto_goTypes = nil + file_gitmirror_v1_gitmirror_proto_depIdxs = nil +} diff --git a/gitmirror/proto/gen/gitmirror_grpc.pb.go b/gitmirror/proto/gen/gitmirror_grpc.pb.go new file mode 100644 index 00000000..ec5d5948 --- /dev/null +++ b/gitmirror/proto/gen/gitmirror_grpc.pb.go @@ -0,0 +1,204 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.0 +// - protoc (unknown) +// source: gitmirror/v1/gitmirror.proto + +package gitmirrorv1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + GitMirrorService_Commits_FullMethodName = "/gitmirror.v1.GitMirrorService/Commits" + GitMirrorService_Diff_FullMethodName = "/gitmirror.v1.GitMirrorService/Diff" + GitMirrorService_Interdiff_FullMethodName = "/gitmirror.v1.GitMirrorService/Interdiff" +) + +// GitMirrorServiceClient is the client API for GitMirrorService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type GitMirrorServiceClient interface { + Commits(ctx context.Context, in *CommitsRequest, opts ...grpc.CallOption) (*CommitsResponse, error) + Diff(ctx context.Context, in *DiffRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[FileDiff], error) + Interdiff(ctx context.Context, in *InterdiffRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[FileDiff], error) +} + +type gitMirrorServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewGitMirrorServiceClient(cc grpc.ClientConnInterface) GitMirrorServiceClient { + return &gitMirrorServiceClient{cc} +} + +func (c *gitMirrorServiceClient) Commits(ctx context.Context, in *CommitsRequest, opts ...grpc.CallOption) (*CommitsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(CommitsResponse) + err := c.cc.Invoke(ctx, GitMirrorService_Commits_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *gitMirrorServiceClient) Diff(ctx context.Context, in *DiffRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[FileDiff], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &GitMirrorService_ServiceDesc.Streams[0], GitMirrorService_Diff_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[DiffRequest, FileDiff]{ClientStream: stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type GitMirrorService_DiffClient = grpc.ServerStreamingClient[FileDiff] + +func (c *gitMirrorServiceClient) Interdiff(ctx context.Context, in *InterdiffRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[FileDiff], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &GitMirrorService_ServiceDesc.Streams[1], GitMirrorService_Interdiff_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[InterdiffRequest, FileDiff]{ClientStream: stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type GitMirrorService_InterdiffClient = grpc.ServerStreamingClient[FileDiff] + +// GitMirrorServiceServer is the server API for GitMirrorService service. +// All implementations must embed UnimplementedGitMirrorServiceServer +// for forward compatibility. +type GitMirrorServiceServer interface { + Commits(context.Context, *CommitsRequest) (*CommitsResponse, error) + Diff(*DiffRequest, grpc.ServerStreamingServer[FileDiff]) error + Interdiff(*InterdiffRequest, grpc.ServerStreamingServer[FileDiff]) error + mustEmbedUnimplementedGitMirrorServiceServer() +} + +// UnimplementedGitMirrorServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedGitMirrorServiceServer struct{} + +func (UnimplementedGitMirrorServiceServer) Commits(context.Context, *CommitsRequest) (*CommitsResponse, error) { + return nil, status.Error(codes.Unimplemented, "method Commits not implemented") +} +func (UnimplementedGitMirrorServiceServer) Diff(*DiffRequest, grpc.ServerStreamingServer[FileDiff]) error { + return status.Error(codes.Unimplemented, "method Diff not implemented") +} +func (UnimplementedGitMirrorServiceServer) Interdiff(*InterdiffRequest, grpc.ServerStreamingServer[FileDiff]) error { + return status.Error(codes.Unimplemented, "method Interdiff not implemented") +} +func (UnimplementedGitMirrorServiceServer) mustEmbedUnimplementedGitMirrorServiceServer() {} +func (UnimplementedGitMirrorServiceServer) testEmbeddedByValue() {} + +// UnsafeGitMirrorServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to GitMirrorServiceServer will +// result in compilation errors. +type UnsafeGitMirrorServiceServer interface { + mustEmbedUnimplementedGitMirrorServiceServer() +} + +func RegisterGitMirrorServiceServer(s grpc.ServiceRegistrar, srv GitMirrorServiceServer) { + // If the following call panics, it indicates UnimplementedGitMirrorServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&GitMirrorService_ServiceDesc, srv) +} + +func _GitMirrorService_Commits_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CommitsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GitMirrorServiceServer).Commits(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: GitMirrorService_Commits_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GitMirrorServiceServer).Commits(ctx, req.(*CommitsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GitMirrorService_Diff_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(DiffRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(GitMirrorServiceServer).Diff(m, &grpc.GenericServerStream[DiffRequest, FileDiff]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type GitMirrorService_DiffServer = grpc.ServerStreamingServer[FileDiff] + +func _GitMirrorService_Interdiff_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(InterdiffRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(GitMirrorServiceServer).Interdiff(m, &grpc.GenericServerStream[InterdiffRequest, FileDiff]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type GitMirrorService_InterdiffServer = grpc.ServerStreamingServer[FileDiff] + +// GitMirrorService_ServiceDesc is the grpc.ServiceDesc for GitMirrorService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var GitMirrorService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "gitmirror.v1.GitMirrorService", + HandlerType: (*GitMirrorServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Commits", + Handler: _GitMirrorService_Commits_Handler, + }, + }, + Streams: []grpc.StreamDesc{ + { + StreamName: "Diff", + Handler: _GitMirrorService_Diff_Handler, + ServerStreams: true, + }, + { + StreamName: "Interdiff", + Handler: _GitMirrorService_Interdiff_Handler, + ServerStreams: true, + }, + }, + Metadata: "gitmirror/v1/gitmirror.proto", +} diff --git a/gitmirror/proto/gitmirror/v1/gitmirror.proto b/gitmirror/proto/gitmirror/v1/gitmirror.proto index 4a1affc7..79973948 100644 --- a/gitmirror/proto/gitmirror/v1/gitmirror.proto +++ b/gitmirror/proto/gitmirror/v1/gitmirror.proto @@ -6,6 +6,8 @@ option go_package = "tangled.org/core/gitmirror/proto/gen;gitmirrorv1"; service GitMirrorService { rpc Commits(CommitsRequest) returns (CommitsResponse); + rpc Diff(DiffRequest) returns (stream FileDiff); + rpc Interdiff(InterdiffRequest) returns (stream FileDiff); } message CommitsRequest { @@ -23,3 +25,59 @@ message CommitsResponse { // maps the Go []string return. repeated string commits = 1; } + +message DiffRequest { + // repo carries the DID as a string; parsing/validation is an impl detail. + string repo = 1; + // base/head are commit-ish ref strings. + string base = 2; + string head = 3; +} + +message InterdiffRequest { + // repo carries the DID as a string. + string repo = 1; + // Old patch range (from_base..from_head) and new patch range (to_base..to_head). + string from_base = 2; + string from_head = 3; + string to_base = 4; + string to_head = 5; +} + +// Diff and Interdiff stream one FileDiff per changed file. + +message FileContent { + string path = 1; + string oid = 2; + uint64 size = 3; + bool is_binary = 4; + bool is_submodule = 5; +} + +// One aligned line pair; a side is absent (None) when unset. +message LinePair { + optional uint32 lhs = 1; + optional uint32 rhs = 2; +} + +message Hunk { + // LineNumber (u32) sets containing novel content per side. + repeated uint32 novel_lhs = 1; + repeated uint32 novel_rhs = 2; + repeated LinePair lines = 3; +} + +// Number of bytes per side when the files differ (Rust Option<(usize, usize)>). +message ByteChanges { + uint64 lhs = 1; + uint64 rhs = 2; +} + +message FileDiff { + FileContent lhs_src = 1; + FileContent rhs_src = 2; + repeated Hunk hunks = 3; + optional ByteChanges has_byte_changes = 4; + bool has_syntactic_changes = 5; + // TODO: lhs_positions / rhs_positions (MatchedPos) — AST, added later. +} diff --git a/gitmirror/src/diff.rs b/gitmirror/src/diff.rs new file mode 100644 index 00000000..d041f28e --- /dev/null +++ b/gitmirror/src/diff.rs @@ -0,0 +1,572 @@ +use gix::bstr::ByteSlice as _; +use gix::objs::tree::EntryKind; +use gix::{Commit, ObjectId}; +use line_numbers::{LineNumber, SingleLineSpan}; +use rustc_hash::FxHashSet; +use tracing::info; + +/// Blobs larger than this are treated as binary (reported by size only, not diffed), +/// overriding gix's `core.bigFileThreshold` default of 512 MiB. Scoped to diffing only. +const LARGE_FILE_THRESHOLD_BYTES: u64 = 1024 * 1024; + +#[derive(Debug)] +pub struct FileContent { + pub(crate) path: String, + pub(crate) oid: String, + pub(crate) size: usize, + pub(crate) is_binary: bool, + /// True when this side is a submodule (gitlink); `oid` is then a commit id pointing into + /// the submodule's repository, not a blob in this one. + pub(crate) is_submodule: bool, +} + +/// A hunk represents a series of modified lines that are displayed +/// together. +#[derive(Debug, Clone)] +pub struct Hunk { + /// The LHS line numbers that contain novel content. + pub(crate) novel_lhs: FxHashSet, + /// The RHS line numbers that contain novel content. + pub(crate) novel_rhs: FxHashSet, + /// Line pairs that contain modified lines. This does not include + /// padding, so at least one of the two lines has novel content. + pub(crate) lines: Vec<(Option, Option)>, +} + +/// A matched token (an atom, a delimiter, or a comment word). +#[derive(PartialEq, Eq, Debug, Clone)] +enum MatchKind { + // TBD +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct MatchedPos { + kind: MatchKind, + pos: SingleLineSpan, +} + +#[derive(Debug)] +pub struct Diff { + pub(crate) lhs_src: FileContent, + pub(crate) rhs_src: FileContent, + pub(crate) hunks: Vec, + + lhs_positions: Vec, + rhs_positions: Vec, + + /// If the two files do not have exactly the same bytes, the + /// number of bytes in each file. + pub(crate) has_byte_changes: Option<(usize, usize)>, + pub(crate) has_syntactic_changes: bool, +} + +/// Diff two trees, yielding one [`Diff`] per changed file (line-level [`Hunk`]s included). +/// +/// Returns an iterator of `anyhow::Result`: per-file diffing can fail independently, so +/// each item is fallible. The consumer drives it lazily and can stop at any point by dropping +/// the iterator (e.g. when the gRPC client hangs up). +pub fn diff<'repo>( + repo: &'repo gix::Repository, + old_tree: &gix::Tree<'_>, + new_tree: &gix::Tree<'_>, +) -> anyhow::Result> { + let changes = repo.diff_tree_to_tree(Some(old_tree), Some(new_tree), None)?; + let mut cache = repo.diff_resource_cache_for_tree_diff()?; + cache.filter.options.large_file_threshold_bytes = LARGE_FILE_THRESHOLD_BYTES; + Ok(DiffIter { + repo, + changes: changes.into_iter(), + cache, + }) +} + +/// Lazy iterator over per-file [`Diff`]s. Created by [`diff`]. +pub struct DiffIter<'repo> { + repo: &'repo gix::Repository, + changes: std::vec::IntoIter, + cache: gix::diff::blob::Platform, +} + +impl Iterator for DiffIter<'_> { + type Item = anyhow::Result; + + fn next(&mut self) -> Option { + loop { + let change = self.changes.next()?; + let (old, new) = change_sides(&change); + // Trees are recursed into by gix and never diffed directly; skip defensively. + if old.is_kind(EntryKind::Tree) || new.is_kind(EntryKind::Tree) { + continue; + } + // Submodules (gitlinks) can't go through the blob pipeline — their commit lives in + // another repo. Emit the commit oids directly for the caller to compare. + if old.is_kind(EntryKind::Commit) || new.is_kind(EntryKind::Commit) { + return Some(Ok(submodule_diff(self.repo, &change, old, new))); + } + let result = build_diff(self.repo, &change, &mut self.cache); + self.cache.clear_resource_cache_keep_allocation(); + return Some(result); + } + } +} + +/// One side (old or new) of a change. +enum Side { + Absent, + Present { oid: ObjectId, kind: EntryKind }, +} + +impl Side { + fn is_kind(&self, wanted: EntryKind) -> bool { + matches!(self, Side::Present { kind, .. } if *kind == wanted) + } +} + +/// Decompose a change into its `(old, new)` sides regardless of variant. +fn change_sides(change: &gix::object::tree::diff::ChangeDetached) -> (Side, Side) { + use gix::object::tree::diff::ChangeDetached as C; + let present = |oid, mode: gix::object::tree::EntryMode| Side::Present { + oid, + kind: mode.kind(), + }; + match change { + C::Addition { id, entry_mode, .. } => (Side::Absent, present(*id, *entry_mode)), + C::Deletion { id, entry_mode, .. } => (present(*id, *entry_mode), Side::Absent), + C::Modification { + previous_id, + previous_entry_mode, + id, + entry_mode, + .. + } => ( + present(*previous_id, *previous_entry_mode), + present(*id, *entry_mode), + ), + C::Rewrite { + source_id, + source_entry_mode, + id, + entry_mode, + .. + } => ( + present(*source_id, *source_entry_mode), + present(*id, *entry_mode), + ), + } +} + +/// Build a [`Diff`] for a submodule (gitlink) change: no hunks, both sides carry their commit +/// oid with `is_submodule` set. The caller compares the oids as submodule pointers. +fn submodule_diff( + repo: &gix::Repository, + change: &gix::object::tree::diff::ChangeDetached, + old: Side, + new: Side, +) -> Diff { + let path = change.location().to_string(); + let side = |s: Side| FileContent { + path: path.clone(), + oid: match s { + Side::Absent => ObjectId::null(repo.object_hash()).to_string(), + Side::Present { oid, .. } => oid.to_string(), + }, + size: 0, + is_binary: false, + is_submodule: true, + }; + Diff { + lhs_src: side(old), + rhs_src: side(new), + hunks: Vec::new(), + lhs_positions: Vec::new(), + rhs_positions: Vec::new(), + has_byte_changes: None, + has_syntactic_changes: false, + } +} + +/// Build the [`Diff`] for a single changed file. +fn build_diff( + repo: &gix::Repository, + change: &gix::object::tree::diff::ChangeDetached, + cache: &mut gix::diff::blob::Platform, +) -> anyhow::Result { + use gix::diff::blob::platform::prepare_diff::Operation; + use gix::diff::blob::platform::resource::Data; + use gix::diff::blob::{Diff as BlobDiff, InternedInput}; + use gix::prelude::TreeDiffChangeExt as _; + + let change = change.attach(repo, repo); + let blob = change.diff(cache)?; + let out = blob.resource_cache.prepare_diff()?; + + // Per-side file metadata, derived from the prepared resources. + let side = |res: &gix::diff::blob::platform::Resource<'_>| -> FileContent { + let (size, is_binary) = match res.data { + Data::Buffer { buf, .. } => (buf.len(), false), + Data::Binary { size } => (size as usize, true), + Data::Missing => (0, false), + }; + FileContent { + path: res.rela_path.to_string(), + oid: res.id.to_string(), + size, + is_binary, + is_submodule: false, + } + }; + let mut lhs_src = side(&out.old); + let mut rhs_src = side(&out.new); + + let has_byte_changes = if lhs_src.size == rhs_src.size && out.old.id == out.new.id { + None + } else { + Some((lhs_src.size, rhs_src.size)) + }; + + let hunks = match out.operation { + Operation::InternalDiff { algorithm } => { + let input = InternedInput::new(out.old.intern_source(), out.new.intern_source()); + let mut d = BlobDiff::compute(algorithm, &input); + d.postprocess_lines(&input); + d.hunks() + .map(|h| { + let before: Vec = h.before.clone().map(LineNumber).collect(); + let after: Vec = h.after.clone().map(LineNumber).collect(); + let n = before.len().max(after.len()); + let lines = (0..n) + .map(|i| (before.get(i).copied(), after.get(i).copied())) + .collect(); + Hunk { + novel_lhs: before.iter().copied().collect(), + novel_rhs: after.iter().copied().collect(), + lines, + } + }) + .collect() + } + Operation::SourceOrDestinationIsBinary => { + lhs_src.is_binary = true; + rhs_src.is_binary = true; + Vec::new() + } + Operation::ExternalCommand { .. } => unreachable!("we disabled that"), + }; + + Ok(Diff { + lhs_src, + rhs_src, + hunks, + lhs_positions: Vec::new(), + rhs_positions: Vec::new(), + has_byte_changes, + has_syntactic_changes: false, + }) +} + +/// Compute the two trees whose diff is the *interdiff* between an old patch +/// version (`from_base..from_head`) and a new one (`to_base..to_head`). +/// +/// We rebase the old patch onto the new base so both versions share `to_base`, +/// then the caller diffs the returned trees (step 4): +/// +/// ```text +/// from_base -- from_head (old patch) +/// \ +/// `-- to_base -- to_head (new patch) +/// ``` +/// +/// 1. Rebase `from` onto `to_base` via a 3-way tree merge. +/// Replay the patch diff `from_base.tree -> from_head.tree` on top of `to_base.tree`. +/// 2. Return `(rebased_from_tree, to_head.tree)`, both rooted at `to_base`. +/// +/// `from_base` / `to_base` default to the respective head commit's first parent. +/// +/// `repo` is borrowed so the returned trees can reference it; resolving a +/// `repo_did` to an on-disk repository is out of scope here. +pub fn prepare_interdiff<'repo>( + repo: &'repo gix::Repository, + from: (Option, ObjectId), + to: (Option, ObjectId), +) -> anyhow::Result<(gix::Tree<'repo>, gix::Tree<'repo>)> { + let (from_base_id, from_head_id) = from; + let (to_base_id, to_head_id) = to; + + let from_head = repo.find_commit(from_head_id)?; + let to_head = repo.find_commit(to_head_id)?; + + let from_base_id = from_base_id.unwrap_or_else(|| resolve_base(&from_head).unwrap()); + let to_base_id = to_base_id.unwrap_or_else(|| resolve_base(&to_head).unwrap()); + + let from_base = repo.find_commit(from_base_id)?; + let to_base = repo.find_commit(to_base_id)?; + + // Endpoint trees for the 3-way merge. + let from_base_tree = from_base.tree_id()?.detach(); + let from_head_tree = from_head.tree_id()?.detach(); + let to_base_tree = to_base.tree_id()?.detach(); + + // Rebase = replay (from_base_tree -> from_head_tree) onto to_base_tree: + // ancestor = from_base_tree, ours = to_base_tree, theirs = from_head_tree. + let options = repo.tree_merge_options()?; + let labels = gix::merge::blob::builtin_driver::text::Labels { + ancestor: Some("from.base".as_bytes().as_bstr()), + current: Some("to.base".as_bytes().as_bstr()), + other: Some("from.head".as_bytes().as_bstr()), + }; + let mut outcome = repo.merge_trees( + from_base_tree, + to_base_tree, + from_head_tree, + labels, + options, + )?; + + // Interdiff tolerates conflicts: the merged tree carries conflict markers + // (blob driver keeps them); surface them rather than failing. + if !outcome.conflicts.is_empty() { + info!( + conflicts = outcome.conflicts.len(), + "interdiff rebase produced conflicts; tree contains conflict markers" + ); + } + + let rebased_id = outcome.tree.write()?.detach(); + let rebased_tree = repo.find_tree(rebased_id)?; + let to_head_tree = to_head.tree()?; + Ok((rebased_tree, to_head_tree)) +} + +fn resolve_base(commit: &Commit) -> anyhow::Result { + commit + .parent_ids() + .next() + .map(|id| id.detach()) + .ok_or_else(|| anyhow::anyhow!("commit {0} has no parent to use as base", commit.id)) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// A fresh bare repo in a tempdir. The `TempDir` is returned so it outlives the repo. + fn repo() -> (tempfile::TempDir, gix::Repository) { + let dir = tempfile::tempdir().unwrap(); + let repo = gix::init_bare(dir.path()).unwrap(); + (dir, repo) + } + + /// Build a tree from `(path, content)` pairs and return its oid. + fn tree(repo: &gix::Repository, files: &[(&str, &[u8])]) -> ObjectId { + let empty = repo.empty_tree().id; + let mut editor = repo.edit_tree(empty).unwrap(); + for (path, content) in files { + let blob = repo.write_blob(content).unwrap().detach(); + editor.upsert(*path, EntryKind::Blob, blob).unwrap(); + } + editor.write().unwrap().detach() + } + + /// Build a tree with a single submodule (gitlink) entry at `path` pointing at `commit_oid`. + fn tree_with_gitlink(repo: &gix::Repository, path: &str, commit_oid: ObjectId) -> ObjectId { + let empty = repo.empty_tree().id; + let mut editor = repo.edit_tree(empty).unwrap(); + editor.upsert(path, EntryKind::Commit, commit_oid).unwrap(); + editor.write().unwrap().detach() + } + + /// Commit `tree_oid` with the given parents. Uses an explicit signature because a fresh + /// `init_bare` repo has no user config (config-reading `commit` would fail AuthorMissing). + fn commit(repo: &gix::Repository, tree_oid: ObjectId, parents: &[ObjectId]) -> ObjectId { + let sig = gix::actor::SignatureRef { + name: b"t".as_bstr(), + email: b"t@t".as_bstr(), + time: "0 +0000", + }; + repo.new_commit_as(sig, sig, "msg", tree_oid, parents.iter().copied()) + .unwrap() + .id + } + + /// Collect all diffs between two tree oids. + fn diffs(repo: &gix::Repository, old: ObjectId, new: ObjectId) -> Vec { + let old = repo.find_tree(old).unwrap(); + let new = repo.find_tree(new).unwrap(); + diff(repo, &old, &new) + .unwrap() + .collect::>>() + .unwrap() + } + + #[test] + fn modification() { + let (_dir, repo) = repo(); + let old = tree(&repo, &[("a", b"x\n")]); + let new = tree(&repo, &[("a", b"y\n")]); + + let got = diffs(&repo, old, new); + assert_eq!(got.len(), 1); + let d = &got[0]; + assert_eq!(d.lhs_src.path, "a"); + assert_eq!(d.rhs_src.path, "a"); + assert!(!d.lhs_src.is_binary && !d.rhs_src.is_binary); + assert_eq!(d.has_byte_changes, Some((2, 2))); + assert_eq!(d.hunks.len(), 1); + let h = &d.hunks[0]; + assert!(h.novel_lhs.contains(&LineNumber(0))); + assert!(h.novel_rhs.contains(&LineNumber(0))); + } + + #[test] + fn addition() { + let (_dir, repo) = repo(); + let old = tree(&repo, &[]); + let new = tree(&repo, &[("a", b"hello\n")]); + + let got = diffs(&repo, old, new); + assert_eq!(got.len(), 1); + let d = &got[0]; + assert_eq!(d.lhs_src.size, 0); // old side missing + assert_eq!(d.rhs_src.size, 6); + assert_eq!(d.rhs_src.path, "a"); + assert!(d.has_byte_changes.is_some()); + } + + #[test] + fn deletion() { + let (_dir, repo) = repo(); + let old = tree(&repo, &[("a", b"hello\n")]); + let new = tree(&repo, &[]); + + let got = diffs(&repo, old, new); + assert_eq!(got.len(), 1); + let d = &got[0]; + assert_eq!(d.lhs_src.size, 6); + assert_eq!(d.rhs_src.size, 0); // new side missing + assert_eq!(d.lhs_src.path, "a"); + } + + #[test] + fn binary() { + let (_dir, repo) = repo(); + let old = tree(&repo, &[("a", b"\x00\x01bin\n")]); + let new = tree(&repo, &[("a", b"\x00\x01BIN\n")]); + + let got = diffs(&repo, old, new); + assert_eq!(got.len(), 1); + let d = &got[0]; + assert!(d.lhs_src.is_binary && d.rhs_src.is_binary); + assert!(d.hunks.is_empty()); + } + + #[test] + fn multiple_files_and_laziness() { + let (_dir, repo) = repo(); + let old = tree(&repo, &[("a", b"1\n"), ("b", b"1\n")]); + let new = tree(&repo, &[("a", b"2\n"), ("b", b"2\n")]); + + let old_t = repo.find_tree(old).unwrap(); + let new_t = repo.find_tree(new).unwrap(); + + // Lazy: pulling one item yields exactly one without consuming the rest. + let mut it = diff(&repo, &old_t, &new_t).unwrap(); + assert!(it.next().is_some()); + + let all = diffs(&repo, old, new); + assert_eq!(all.len(), 2); + let mut paths: Vec<_> = all.iter().map(|d| d.rhs_src.path.clone()).collect(); + paths.sort(); + assert_eq!(paths, vec!["a".to_string(), "b".to_string()]); + } + + #[test] + fn directory_addition_yields_inner_blobs() { + let (_dir, repo) = repo(); + let old = tree(&repo, &[]); + // A whole new directory: gix emits a Tree change for `dir/` plus blob changes for the + // files inside. We must skip the tree and yield only the blobs (no error). + let new = tree(&repo, &[("dir/a", b"x\n"), ("dir/b", b"y\n")]); + + let got = diffs(&repo, old, new); + let mut paths: Vec<_> = got.iter().map(|d| d.rhs_src.path.clone()).collect(); + paths.sort(); + assert_eq!(paths, vec!["dir/a".to_string(), "dir/b".to_string()]); + } + + #[test] + fn interdiff_equal_patches_is_empty() { + let (_dir, repo) = repo(); + // Shared base content B; patch P turns a's "1\n" into "2\n". + let base = commit(&repo, tree(&repo, &[("a", b"1\n")]), &[]); + let patched = tree(&repo, &[("a", b"2\n")]); + // `from` and `to` both apply the same patch P on top of the same base. + let from_head = commit(&repo, patched, &[base]); + let to_head = commit(&repo, patched, &[base]); + + let (rebased, to_head_tree) = + prepare_interdiff(&repo, (Some(base), from_head), (Some(base), to_head)).unwrap(); + + // Same patch ⇒ interdiff (diff of rebased vs to_head) is empty. + let got = diff(&repo, &rebased, &to_head_tree) + .unwrap() + .collect::>>() + .unwrap(); + assert!(got.is_empty(), "expected empty interdiff, got {got:?}"); + } + + #[test] + fn interdiff_divergent_shows_delta() { + let (_dir, repo) = repo(); + let base = commit(&repo, tree(&repo, &[("a", b"1\n")]), &[]); + // `from` applies patch P (a: 1->2). + let from_head = commit(&repo, tree(&repo, &[("a", b"2\n")]), &[base]); + // `to` applies P plus a new file `c`. + let to_head = commit(&repo, tree(&repo, &[("a", b"2\n"), ("c", b"new\n")]), &[base]); + + let (rebased, to_head_tree) = + prepare_interdiff(&repo, (Some(base), from_head), (Some(base), to_head)).unwrap(); + + let got = diff(&repo, &rebased, &to_head_tree) + .unwrap() + .collect::>>() + .unwrap(); + assert_eq!(got.len(), 1); + assert_eq!(got[0].rhs_src.path, "c"); + } + + #[test] + fn submodule_modification() { + let (_dir, repo) = repo(); + // Two distinct commit oids for the gitlink to point at (need not be real submodules). + let a = commit(&repo, tree(&repo, &[("x", b"1\n")]), &[]); + let b = commit(&repo, tree(&repo, &[("x", b"2\n")]), &[]); + let old = tree_with_gitlink(&repo, "sub", a); + let new = tree_with_gitlink(&repo, "sub", b); + + let got = diffs(&repo, old, new); + assert_eq!(got.len(), 1); + let d = &got[0]; + assert!(d.lhs_src.is_submodule && d.rhs_src.is_submodule); + assert!(d.hunks.is_empty()); + assert_eq!(d.lhs_src.path, "sub"); + assert_eq!(d.lhs_src.oid, a.to_string()); + assert_eq!(d.rhs_src.oid, b.to_string()); + } + + #[test] + fn submodule_addition() { + let (_dir, repo) = repo(); + let b = commit(&repo, tree(&repo, &[("x", b"1\n")]), &[]); + let old = tree(&repo, &[]); + let new = tree_with_gitlink(&repo, "sub", b); + + let got = diffs(&repo, old, new); + assert_eq!(got.len(), 1); + let d = &got[0]; + assert!(d.rhs_src.is_submodule); + assert!(d.hunks.is_empty()); + assert_eq!(d.rhs_src.oid, b.to_string()); + // Absent old side reported as the null oid. + assert_eq!(d.lhs_src.oid, ObjectId::null(repo.object_hash()).to_string()); + } +} diff --git a/gitmirror/src/main.rs b/gitmirror/src/main.rs index 5e433741..a601e4fb 100644 --- a/gitmirror/src/main.rs +++ b/gitmirror/src/main.rs @@ -1,21 +1,59 @@ -use std::env; use std::net::SocketAddr; +use clap::Parser; use tonic::transport::Server; use tonic::{Request, Response, Status}; use tracing::info; +mod diff; + pub mod gitmirror { pub mod v1 { tonic::include_proto!("gitmirror.v1"); } } +use std::path::PathBuf; +use std::pin::Pin; + +use gix::bstr::ByteSlice as _; +use tokio::sync::mpsc; +use tokio_stream::Stream; +use tokio_stream::wrappers::ReceiverStream; + use gitmirror::v1::git_mirror_service_server::{GitMirrorService, GitMirrorServiceServer}; -use gitmirror::v1::{CommitsRequest, CommitsResponse}; +use gitmirror::v1::{ + ByteChanges, CommitsRequest, CommitsResponse, DiffRequest, FileContent, FileDiff, Hunk, + InterdiffRequest, LinePair, +}; -#[derive(Default)] -struct GitMirror {} +type FileDiffStream = Pin> + Send>>; + +#[derive(Parser)] +#[command(name = "gitmirror", about = "Git mirror gRPC service")] +struct Args { + /// Address to bind the gRPC server to. + #[arg(long, env = "GITMIRROR_ADDR", default_value = "127.0.0.1:9000")] + addr: SocketAddr, + + /// Base directory holding bare mirror repos, one per DID (/). + #[arg(long, env = "GITMIRROR_REPO_BASE", default_value = "repos")] + repo_base: PathBuf, +} + +struct GitMirror { + repo_base: PathBuf, +} + +impl GitMirror { + fn open_repo(&self, did: &str) -> Result { + if !did.starts_with("did:") { + return Err(Status::invalid_argument("repo must be a DID")); + } + gix::open(self.repo_base.join(did)) + .map_err(|e| Status::not_found(format!("repo not found: {e}"))) + } +} #[tonic::async_trait] impl GitMirrorService for GitMirror { @@ -25,27 +63,126 @@ impl GitMirrorService for GitMirror { ) -> Result, Status> { Err(Status::unimplemented("not implemented")) } + + type DiffStream = FileDiffStream; + + async fn diff( + &self, + request: Request, + ) -> Result, Status> { + let req = request.into_inner(); + let repo = self.open_repo(&req.repo)?; + // Resolve refs up-front so bad input surfaces as the RPC status, then move the + // owned repo + oids into a blocking task for the (synchronous) diff loop. + let old_oid = resolve_tree_oid(&repo, &req.base)?; + let new_oid = resolve_tree_oid(&repo, &req.head)?; + let safe = repo.into_sync(); + + let (tx, rx) = mpsc::channel::>(16); + tokio::task::spawn_blocking(move || { + let repo = safe.to_thread_local(); + let run = || -> anyhow::Result<()> { + let old = repo.find_tree(old_oid)?; + let new = repo.find_tree(new_oid)?; + for item in diff::diff(&repo, &old, &new)? { + let proto = to_proto(item?); + if tx.blocking_send(Ok(proto)).is_err() { + break; // client hung up + } + } + Ok(()) + }; + if let Err(e) = run() { + let _ = tx.blocking_send(Err(Status::internal(e.to_string()))); + } + }); + + Ok(Response::new(Box::pin(ReceiverStream::new(rx)))) + } + + type InterdiffStream = FileDiffStream; + + async fn interdiff( + &self, + _request: Request, + ) -> Result, Status> { + Err(Status::unimplemented("not implemented")) + } +} + +/// Resolve a commit-ish ref string (oid, short oid, branch/tag) to its tree oid. +fn resolve_tree_oid(repo: &gix::Repository, spec: &str) -> Result { + let tree = repo + .rev_parse_single(spec.as_bytes().as_bstr()) + .map_err(|e| Status::invalid_argument(format!("bad rev '{spec}': {e}")))? + .object() + .map_err(|e| Status::internal(e.to_string()))? + .peel_to_tree() + .map_err(|e| Status::internal(e.to_string()))?; + Ok(tree.id) +} + +fn to_proto(d: diff::Diff) -> FileDiff { + FileDiff { + lhs_src: Some(file_content(d.lhs_src)), + rhs_src: Some(file_content(d.rhs_src)), + hunks: d.hunks.into_iter().map(hunk).collect(), + has_byte_changes: d.has_byte_changes.map(|(lhs, rhs)| ByteChanges { + lhs: lhs as u64, + rhs: rhs as u64, + }), + has_syntactic_changes: d.has_syntactic_changes, + } +} + +fn file_content(f: diff::FileContent) -> FileContent { + FileContent { + path: f.path, + oid: f.oid, + size: f.size as u64, + is_binary: f.is_binary, + is_submodule: f.is_submodule, + } +} + +fn hunk(h: diff::Hunk) -> Hunk { + // Sort the novel line sets for deterministic output. + let sorted = |set: rustc_hash::FxHashSet| -> Vec { + let mut v: Vec = set.into_iter().map(|n| n.0).collect(); + v.sort_unstable(); + v + }; + Hunk { + novel_lhs: sorted(h.novel_lhs), + novel_rhs: sorted(h.novel_rhs), + lines: h + .lines + .into_iter() + .map(|(lhs, rhs)| LinePair { + lhs: lhs.map(|n| n.0), + rhs: rhs.map(|n| n.0), + }) + .collect(), + } } #[tokio::main] async fn main() -> Result<(), Box> { tracing_subscriber::fmt() .with_env_filter( - tracing_subscriber::EnvFilter::try_from_default_env() - .unwrap_or_else(|_| "info".into()), + tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()), ) .init(); - let addr: SocketAddr = env::var("GITMIRROR_ADDR") - .unwrap_or_else(|_| "127.0.0.1:9000".to_string()) - .parse()?; - - let service = GitMirror::default(); - info!(%addr, "gitmirror gRPC server listening"); + let args = Args::parse(); + let service = GitMirror { + repo_base: args.repo_base, + }; + info!(addr = %args.addr, "gitmirror gRPC server listening"); Server::builder() .add_service(GitMirrorServiceServer::new(service)) - .serve(addr) + .serve(args.addr) .await?; Ok(()) diff --git a/go.mod b/go.mod index f926dc1e..c691e8a2 100644 --- a/go.mod +++ b/go.mod @@ -81,6 +81,7 @@ require ( golang.org/x/sync v0.20.0 golang.org/x/sys v0.45.0 golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da + google.golang.org/grpc v1.80.0 google.golang.org/protobuf v1.36.11 gopkg.in/yaml.v3 v3.0.1 ) @@ -310,7 +311,6 @@ require ( golang.org/x/tools v0.44.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect - google.golang.org/grpc v1.80.0 // indirect gopkg.in/fsnotify.v1 v1.4.7 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect diff --git a/localinfra/Caddyfile b/localinfra/Caddyfile index 2422b451..f3bc320c 100644 --- a/localinfra/Caddyfile +++ b/localinfra/Caddyfile @@ -53,6 +53,16 @@ mirror.tngl.boltless.dev { reverse_proxy knotmirror:7000 } +gitmirror.tngl.boltless.dev { + tls internal + # gitmirror (tonic) serves h2c; Caddy defaults to HTTP/1.1 to the backend. + reverse_proxy gitmirror:9000 { + transport http { + versions h2c 2 + } + } +} + # zoekt (internal service) zoekt.tngl.boltless.dev { tls internal diff --git a/localinfra/gitmirror.Dockerfile b/localinfra/gitmirror.Dockerfile new file mode 100644 index 00000000..91341e05 --- /dev/null +++ b/localinfra/gitmirror.Dockerfile @@ -0,0 +1,29 @@ +# Development only. Not for production use. + +FROM rust:1.96-bookworm AS build + +RUN apt-get update && apt-get install -y --no-install-recommends protobuf-compiler \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /src +COPY . . + +# Cache mounts on the cargo registry + target dir. The binary is copied out within the same +# RUN because the target cache mount is unmounted afterwards (a later COPY --from could not +# see it). +RUN --mount=type=cache,target=/usr/local/cargo/registry \ + --mount=type=cache,target=/src/target \ + cargo build --release -p gitmirror \ + && cp /src/target/release/gitmirror /usr/local/bin/gitmirror + +FROM debian:bookworm-slim + +RUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates tini \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=build /usr/local/bin/gitmirror /usr/local/bin/gitmirror + +EXPOSE 9000 + +ENTRYPOINT ["/usr/bin/tini", "--"] +CMD ["sh", "-c", "if [ -f /usr/local/share/ca-certificates/caddy.crt ]; then update-ca-certificates; fi && exec /usr/local/bin/gitmirror"] -- 2.51.2