From dcb0fb74c5fbcbd810d5fea90093b2245bbf2d27 Mon Sep 17 00:00:00 2001
From: zhenyi <434836402@qq.com>
Date: Thu, 4 Jun 2026 13:05:38 +0800
Subject: [PATCH] feat(core): implement Git repository operations with gRPC
services
- Add advertise_refs functionality for Git protocol communication
- Implement archive service with TAR/ZIP format support and streaming
- Create blame service for Git file annotation with line tracking
- Add branch management including create, delete, rename and compare operations
- Implement merge checking with conflict detection and fast-forward handling
- Add cherry-pick functionality for applying commits between branches
- Integrate gix library for Git repository operations and object handling
- Add comprehensive test suite covering all Git operations
- Implement proper error handling and repository validation
- Add pagination support for large result sets
- Create protobuf definitions for all Git operations and data structures
- Add build system for gRPC code generation and dependency management
---
.gitignore | 1 +
.idea/.gitignore | 10 +
.idea/gitks.iml | 14 +
.idea/modules.xml | 8 +
.idea/vcs.xml | 6 +
Cargo.lock | 2961 ++++++++++
Cargo.toml | 33 +
LICENSE | 58 +
README.md | 7 +
archive/get_archive.rs | 45 +
archive/list_archive_entries.rs | 68 +
archive/mod.rs | 2 +
bare.rs | 112 +
blame/do_blame.rs | 152 +
blame/mod.rs | 1 +
blob/get_blob.rs | 70 +
blob/get_raw_blob.rs | 16 +
blob/mod.rs | 2 +
branch/compare_branch.rs | 54 +
branch/create_branch.rs | 36 +
branch/delete_branch.rs | 23 +
branch/get_branch.rs | 23 +
branch/list_branches.rs | 78 +
branch/mod.rs | 8 +
branch/rename_branch.rs | 25 +
branch/set_branch_upstream.rs | 36 +
branch/update_branch_target.rs | 39 +
build.rs | 51 +
commit/cherry_pick_commit.rs | 161 +
commit/compare_commits.rs | 94 +
commit/create_commit.rs | 375 ++
commit/get_commit.rs | 76 +
commit/get_commit_ancestors.rs | 28 +
commit/list_commits.rs | 86 +
commit/mod.rs | 7 +
commit/revert_commit.rs | 146 +
commit/types.rs | 1 +
diff/get_commit_diff.rs | 86 +
diff/get_diff.rs | 330 ++
diff/get_diff_stats.rs | 108 +
diff/get_patch.rs | 42 +
diff/mod.rs | 4 +
error.rs | 78 +
init.rs | 24 +
lib.rs | 17 +
merge/check_merge.rs | 111 +
merge/do_merge.rs | 173 +
merge/list_merge_conflicts.rs | 69 +
merge/mod.rs | 5 +
merge/rebase.rs | 192 +
merge/resolve_merge_conflicts.rs | 128 +
oid.rs | 53 +
pack/advertise_refs.rs | 57 +
pack/fsck.rs | 45 +
pack/index_pack.rs | 138 +
pack/list_packfiles.rs | 92 +
pack/mod.rs | 7 +
pack/pack_objects.rs | 160 +
pack/receive_pack.rs | 142 +
pack/upload_pack.rs | 147 +
paginate.rs | 46 +
pb/gitks.rs | 8923 ++++++++++++++++++++++++++++++
pb/mod.rs | 5 +
proto/archive.proto | 58 +
proto/blame.proto | 56 +
proto/branch.proto | 114 +
proto/commit.proto | 165 +
proto/diff.proto | 140 +
proto/merge.proto | 139 +
proto/oid.proto | 64 +
proto/pack.proto | 134 +
proto/repository.proto | 157 +
proto/tag.proto | 67 +
proto/tagger.proto | 51 +
proto/tree.proto | 118 +
refs/list_refs.rs | 25 +
refs/mod.rs | 1 +
tag/create_tag.rs | 46 +
tag/delete_tag.rs | 29 +
tag/get_tag.rs | 63 +
tag/list_tags.rs | 41 +
tag/mod.rs | 5 +
tag/verify_tag.rs | 35 +
tests/archive_test.rs | 169 +
tests/blame_test.rs | 132 +
tests/branch_test.rs | 200 +
tests/commit_test.rs | 469 ++
tests/common/mod.rs | 167 +
tests/diff_test.rs | 236 +
tests/integration.rs | 743 +++
tests/merge_test.rs | 300 +
tests/tag_test.rs | 146 +
tests/tree_test.rs | 176 +
tree/find_files.rs | 56 +
tree/get_file_metadata.rs | 38 +
tree/get_tree.rs | 43 +
tree/list_tree.rs | 87 +
tree/mod.rs | 4 +
98 files changed, 20569 insertions(+)
create mode 100644 .gitignore
create mode 100644 .idea/.gitignore
create mode 100644 .idea/gitks.iml
create mode 100644 .idea/modules.xml
create mode 100644 .idea/vcs.xml
create mode 100644 Cargo.lock
create mode 100644 Cargo.toml
create mode 100644 LICENSE
create mode 100644 README.md
create mode 100644 archive/get_archive.rs
create mode 100644 archive/list_archive_entries.rs
create mode 100644 archive/mod.rs
create mode 100644 bare.rs
create mode 100644 blame/do_blame.rs
create mode 100644 blame/mod.rs
create mode 100644 blob/get_blob.rs
create mode 100644 blob/get_raw_blob.rs
create mode 100644 blob/mod.rs
create mode 100644 branch/compare_branch.rs
create mode 100644 branch/create_branch.rs
create mode 100644 branch/delete_branch.rs
create mode 100644 branch/get_branch.rs
create mode 100644 branch/list_branches.rs
create mode 100644 branch/mod.rs
create mode 100644 branch/rename_branch.rs
create mode 100644 branch/set_branch_upstream.rs
create mode 100644 branch/update_branch_target.rs
create mode 100644 build.rs
create mode 100644 commit/cherry_pick_commit.rs
create mode 100644 commit/compare_commits.rs
create mode 100644 commit/create_commit.rs
create mode 100644 commit/get_commit.rs
create mode 100644 commit/get_commit_ancestors.rs
create mode 100644 commit/list_commits.rs
create mode 100644 commit/mod.rs
create mode 100644 commit/revert_commit.rs
create mode 100644 commit/types.rs
create mode 100644 diff/get_commit_diff.rs
create mode 100644 diff/get_diff.rs
create mode 100644 diff/get_diff_stats.rs
create mode 100644 diff/get_patch.rs
create mode 100644 diff/mod.rs
create mode 100644 error.rs
create mode 100644 init.rs
create mode 100644 lib.rs
create mode 100644 merge/check_merge.rs
create mode 100644 merge/do_merge.rs
create mode 100644 merge/list_merge_conflicts.rs
create mode 100644 merge/mod.rs
create mode 100644 merge/rebase.rs
create mode 100644 merge/resolve_merge_conflicts.rs
create mode 100644 oid.rs
create mode 100644 pack/advertise_refs.rs
create mode 100644 pack/fsck.rs
create mode 100644 pack/index_pack.rs
create mode 100644 pack/list_packfiles.rs
create mode 100644 pack/mod.rs
create mode 100644 pack/pack_objects.rs
create mode 100644 pack/receive_pack.rs
create mode 100644 pack/upload_pack.rs
create mode 100644 paginate.rs
create mode 100644 pb/gitks.rs
create mode 100644 pb/mod.rs
create mode 100644 proto/archive.proto
create mode 100644 proto/blame.proto
create mode 100644 proto/branch.proto
create mode 100644 proto/commit.proto
create mode 100644 proto/diff.proto
create mode 100644 proto/merge.proto
create mode 100644 proto/oid.proto
create mode 100644 proto/pack.proto
create mode 100644 proto/repository.proto
create mode 100644 proto/tag.proto
create mode 100644 proto/tagger.proto
create mode 100644 proto/tree.proto
create mode 100644 refs/list_refs.rs
create mode 100644 refs/mod.rs
create mode 100644 tag/create_tag.rs
create mode 100644 tag/delete_tag.rs
create mode 100644 tag/get_tag.rs
create mode 100644 tag/list_tags.rs
create mode 100644 tag/mod.rs
create mode 100644 tag/verify_tag.rs
create mode 100644 tests/archive_test.rs
create mode 100644 tests/blame_test.rs
create mode 100644 tests/branch_test.rs
create mode 100644 tests/commit_test.rs
create mode 100644 tests/common/mod.rs
create mode 100644 tests/diff_test.rs
create mode 100644 tests/integration.rs
create mode 100644 tests/merge_test.rs
create mode 100644 tests/tag_test.rs
create mode 100644 tests/tree_test.rs
create mode 100644 tree/find_files.rs
create mode 100644 tree/get_file_metadata.rs
create mode 100644 tree/get_tree.rs
create mode 100644 tree/list_tree.rs
create mode 100644 tree/mod.rs
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ea8c4bf
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+/target
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..f6906f2
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,10 @@
+# 默认忽略的文件
+/shelf/
+/workspace.xml
+# 基于编辑器的 HTTP 客户端请求
+/httpRequests/
+# 已忽略包含查询文件的默认文件夹
+/queries/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/gitks.iml b/.idea/gitks.iml
new file mode 100644
index 0000000..c8ef7c9
--- /dev/null
+++ b/.idea/gitks.iml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..e7d02c9
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..35eb1dd
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..daf51ac
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,2961 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "adler2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "allocator-api2"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
+
+[[package]]
+name = "anyhow"
+version = "1.0.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
+
+[[package]]
+name = "arc-swap"
+version = "1.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207"
+dependencies = [
+ "rustversion",
+]
+
+[[package]]
+name = "arrayvec"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
+
+[[package]]
+name = "async-stream"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476"
+dependencies = [
+ "async-stream-impl",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-stream-impl"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "async-trait"
+version = "0.1.89"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
+name = "autocfg"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
+
+[[package]]
+name = "axum"
+version = "0.7.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f"
+dependencies = [
+ "async-trait",
+ "axum-core",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "itoa",
+ "matchit",
+ "memchr",
+ "mime",
+ "percent-encoding",
+ "pin-project-lite",
+ "rustversion",
+ "serde",
+ "sync_wrapper",
+ "tower 0.5.3",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
+name = "axum-core"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199"
+dependencies = [
+ "async-trait",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "mime",
+ "pin-project-lite",
+ "rustversion",
+ "sync_wrapper",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "bitflags"
+version = "2.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84d7ced0ae9557296835c32bf1b1e02b44c746701f898460fb000d7eaa84f00a"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "block-buffer"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be"
+dependencies = [
+ "hybrid-array",
+]
+
+[[package]]
+name = "bstr"
+version = "1.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab"
+dependencies = [
+ "memchr",
+ "regex-automata",
+ "serde",
+]
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "bytes"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[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 = "cpufeatures"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "cpufeatures"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crc32fast"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crossbeam-channel"
+version = "0.5.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
+[[package]]
+name = "crypto-common"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
+dependencies = [
+ "hybrid-array",
+]
+
+[[package]]
+name = "dashmap"
+version = "6.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c"
+dependencies = [
+ "cfg-if",
+ "crossbeam-utils",
+ "hashbrown 0.14.5",
+ "lock_api",
+ "once_cell",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer 0.10.4",
+ "crypto-common 0.1.7",
+]
+
+[[package]]
+name = "digest"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
+dependencies = [
+ "block-buffer 0.12.0",
+ "crypto-common 0.2.2",
+]
+
+[[package]]
+name = "document-features"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61"
+dependencies = [
+ "litrs",
+]
+
+[[package]]
+name = "duct"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e66e9c0c03d094e1a0ba1be130b849034aa80c3a2ab8ee94316bc809f3fa684"
+dependencies = [
+ "libc",
+ "os_pipe",
+ "shared_child",
+ "shared_thread",
+]
+
+[[package]]
+name = "dunce"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
+
+[[package]]
+name = "either"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
+
+[[package]]
+name = "encoding_rs"
+version = "0.8.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "faster-hex"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7223ae2d2f179b803433d9c830478527e92b8117eab39460edae7f1614d9fb73"
+dependencies = [
+ "heapless",
+ "serde",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
+
+[[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 = "fixedbitset"
+version = "0.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
+
+[[package]]
+name = "flate2"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
+dependencies = [
+ "miniz_oxide",
+ "zlib-rs",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foldhash"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+
+[[package]]
+name = "foldhash"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
+
+[[package]]
+name = "futures-channel"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
+
+[[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-core",
+ "futures-task",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[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",
+ "wasip2",
+ "wasip3",
+]
+
+[[package]]
+name = "gitks"
+version = "1.0.0"
+dependencies = [
+ "duct",
+ "gix",
+ "gix-archive",
+ "prost",
+ "prost-types",
+ "serde",
+ "tempfile",
+ "thiserror",
+ "tokio",
+ "tokio-stream",
+ "tonic",
+ "tonic-build",
+ "tracing",
+]
+
+[[package]]
+name = "gix"
+version = "0.84.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae54ae0ebd1a5a3c3f8d95dd3b5ca6e63f4fed9bfd585e13801a97d7bde8f9ce"
+dependencies = [
+ "gix-actor",
+ "gix-attributes",
+ "gix-blame",
+ "gix-command",
+ "gix-commitgraph",
+ "gix-config",
+ "gix-credentials",
+ "gix-date",
+ "gix-diff",
+ "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-object",
+ "gix-odb",
+ "gix-pack",
+ "gix-path",
+ "gix-pathspec",
+ "gix-protocol",
+ "gix-ref",
+ "gix-refspec",
+ "gix-revision",
+ "gix-revwalk",
+ "gix-sec",
+ "gix-shallow",
+ "gix-submodule",
+ "gix-tempfile",
+ "gix-trace",
+ "gix-transport",
+ "gix-traverse",
+ "gix-url",
+ "gix-utils",
+ "gix-validate",
+ "gix-worktree",
+ "gix-worktree-stream",
+ "nonempty",
+ "serde",
+ "smallvec",
+ "thiserror",
+]
+
+[[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",
+ "serde",
+]
+
+[[package]]
+name = "gix-archive"
+version = "0.33.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16909cacc78936ab96f6c3be08379d0a2e88bfa3a7527972d2ed75c7517ef31e"
+dependencies = [
+ "bstr",
+ "document-features",
+ "flate2",
+ "gix-date",
+ "gix-error",
+ "gix-object",
+ "gix-path",
+ "gix-worktree-stream",
+ "rawzip",
+ "tar",
+]
+
+[[package]]
+name = "gix-attributes"
+version = "0.33.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d43f12e246d3bf7ec624c8fc15ac4a4b62b7c4c6f586cb82be6c90bf84c9d02"
+dependencies = [
+ "bstr",
+ "gix-glob",
+ "gix-path",
+ "gix-quote",
+ "gix-trace",
+ "kstring",
+ "serde",
+ "smallvec",
+ "thiserror",
+ "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",
+]
+
+[[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",
+ "serde",
+]
+
+[[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",
+ "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",
+ "bstr",
+ "gix-path",
+ "libc",
+ "thiserror",
+]
+
+[[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",
+ "serde",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-date"
+version = "0.15.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3ecab64a98bbac9f8e02990a9ea5e3c974a7d49b95f2bd70ad94ad22fa6b48c"
+dependencies = [
+ "bstr",
+ "gix-error",
+ "itoa",
+ "jiff",
+ "serde",
+]
+
+[[package]]
+name = "gix-diff"
+version = "0.64.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b6d9528f32d94cef2edf39a1ac01fe5a0fc44ddbb18d9e44099936047c3302b"
+dependencies = [
+ "bstr",
+ "gix-command",
+ "gix-filter",
+ "gix-fs",
+ "gix-hash",
+ "gix-imara-diff",
+ "gix-object",
+ "gix-path",
+ "gix-tempfile",
+ "gix-trace",
+ "gix-traverse",
+ "gix-worktree",
+ "thiserror",
+]
+
+[[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",
+]
+
+[[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",
+ "crc32fast",
+ "crossbeam-channel",
+ "gix-path",
+ "gix-trace",
+ "gix-utils",
+ "libc",
+ "once_cell",
+ "parking_lot",
+ "prodash",
+ "thiserror",
+ "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",
+]
+
+[[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",
+]
+
+[[package]]
+name = "gix-glob"
+version = "0.26.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1fcb8ef5b16bcf874abe9b68d8abb3c0493c876d367ab824151f30a0f3f3756"
+dependencies = [
+ "bitflags",
+ "bstr",
+ "gix-features",
+ "gix-path",
+ "serde",
+]
+
+[[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",
+ "serde",
+ "sha1-checked",
+ "sha2",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-hashtable"
+version = "0.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0e30b93eea8718baf7d8153fcb938e2926175bbf18097c09f1c01b6f0be0563"
+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",
+ "serde",
+ "unicode-bom",
+]
+
+[[package]]
+name = "gix-imara-diff"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19753d40da53d0ec41604750eeb969097a90fb2d7f7992730d904541c04e2c19"
+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",
+ "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",
+ "serde",
+ "smallvec",
+ "thiserror",
+]
+
+[[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",
+]
+
+[[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",
+ "serde",
+]
+
+[[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",
+]
+
+[[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",
+ "serde",
+ "smallvec",
+ "thiserror",
+]
+
+[[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",
+ "serde",
+ "tempfile",
+ "thiserror",
+]
+
+[[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",
+ "serde",
+ "smallvec",
+ "thiserror",
+ "uluru",
+]
+
+[[package]]
+name = "gix-packetline"
+version = "0.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb18337ba2830bb43367d1af43819c8c78f31337f079fc76d0f1f1750a173126"
+dependencies = [
+ "bstr",
+ "faster-hex",
+ "gix-trace",
+ "thiserror",
+]
+
+[[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",
+]
+
+[[package]]
+name = "gix-pathspec"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3050783b41ee11511e1e8fb35623df81806194f4030395f14f48ea37c2798c9f"
+dependencies = [
+ "bitflags",
+ "bstr",
+ "gix-attributes",
+ "gix-config-value",
+ "gix-glob",
+ "gix-path",
+ "thiserror",
+]
+
+[[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",
+]
+
+[[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",
+ "serde",
+ "thiserror",
+]
+
+[[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",
+ "serde",
+ "thiserror",
+]
+
+[[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",
+]
+
+[[package]]
+name = "gix-revision"
+version = "0.46.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b47c88884dd3c1a19a39da19d10211fcdea2809aadc86869b6e824a1774340f"
+dependencies = [
+ "bitflags",
+ "bstr",
+ "gix-commitgraph",
+ "gix-date",
+ "gix-error",
+ "gix-hash",
+ "gix-hashtable",
+ "gix-object",
+ "gix-revwalk",
+ "gix-trace",
+ "nonempty",
+ "serde",
+]
+
+[[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",
+]
+
+[[package]]
+name = "gix-sec"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab8519976e4c7e486270740a5400369f37940779b80bd1377d94cfa1125d01b3"
+dependencies = [
+ "bitflags",
+ "gix-path",
+ "libc",
+ "serde",
+ "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",
+ "serde",
+ "thiserror",
+]
+
+[[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",
+]
+
+[[package]]
+name = "gix-tempfile"
+version = "23.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "27850097e1ff9515f46a0dad0f5f9c9d020e972727772dabab9450690c4adb22"
+dependencies = [
+ "dashmap",
+ "gix-fs",
+ "libc",
+ "parking_lot",
+ "tempfile",
+]
+
+[[package]]
+name = "gix-trace"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44dc45eae785c0eb14173e0f152e6e224dcf4d45b6a6999a3aed22af541ad678"
+dependencies = [
+ "tracing",
+]
+
+[[package]]
+name = "gix-transport"
+version = "0.57.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cd0e34995b1aab0fa8dff2af8db726a0bfad3e119c89302604463264046e7ff"
+dependencies = [
+ "bstr",
+ "gix-command",
+ "gix-features",
+ "gix-packetline",
+ "gix-quote",
+ "gix-sec",
+ "gix-url",
+ "serde",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-traverse"
+version = "0.58.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8de590ecc86a3b2870665f2288324fa9f7f8672c7fc2d4e020fdd81cd1f7aed"
+dependencies = [
+ "bitflags",
+ "gix-commitgraph",
+ "gix-date",
+ "gix-hash",
+ "gix-hashtable",
+ "gix-object",
+ "gix-revwalk",
+ "smallvec",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-url"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "65bb01ec69d55e82ccb7a19e264501ead4e6aac38463a8cebfdd81e22bb67ab2"
+dependencies = [
+ "bstr",
+ "gix-path",
+ "percent-encoding",
+ "serde",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-utils"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "66c50966184123caf580ffa64e28031a878597f1c7fceb8fe19566c38eb1b771"
+dependencies = [
+ "fastrand",
+ "unicode-normalization",
+]
+
+[[package]]
+name = "gix-validate"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7bc6fc771c4063ba7cd2f47b91fb6076251c6a823b64b7fe7b8874b0fe4afae3"
+dependencies = [
+ "bstr",
+]
+
+[[package]]
+name = "gix-worktree"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cef414ed275e8407cd5d53d301e83be19700b0dd3f859d2434417b58f454a2d1"
+dependencies = [
+ "bstr",
+ "gix-attributes",
+ "gix-fs",
+ "gix-glob",
+ "gix-hash",
+ "gix-ignore",
+ "gix-index",
+ "gix-object",
+ "gix-path",
+ "gix-validate",
+ "serde",
+]
+
+[[package]]
+name = "gix-worktree-stream"
+version = "0.33.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d25e9ed30100c63f7590bc581c225e53f731a53e06aa79a245739c07f7dcc557"
+dependencies = [
+ "gix-attributes",
+ "gix-error",
+ "gix-features",
+ "gix-filter",
+ "gix-fs",
+ "gix-hash",
+ "gix-object",
+ "gix-path",
+ "gix-traverse",
+ "parking_lot",
+]
+
+[[package]]
+name = "h2"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "http",
+ "indexmap 2.14.0",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "hash32"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606"
+dependencies = [
+ "byteorder",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+
+[[package]]
+name = "hashbrown"
+version = "0.14.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
+
+[[package]]
+name = "hashbrown"
+version = "0.15.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
+dependencies = [
+ "foldhash 0.1.5",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
+dependencies = [
+ "allocator-api2",
+ "equivalent",
+ "foldhash 0.2.0",
+]
+
+[[package]]
+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"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad"
+dependencies = [
+ "hash32",
+ "stable_deref_trait",
+]
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "http"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0"
+dependencies = [
+ "bytes",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
+dependencies = [
+ "bytes",
+ "http",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+
+[[package]]
+name = "httpdate"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
+
+[[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.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "h2",
+ "http",
+ "http-body",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "smallvec",
+ "tokio",
+ "want",
+]
+
+[[package]]
+name = "hyper-timeout"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0"
+dependencies = [
+ "hyper",
+ "hyper-util",
+ "pin-project-lite",
+ "tokio",
+ "tower-service",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "http",
+ "http-body",
+ "hyper",
+ "libc",
+ "pin-project-lite",
+ "socket2 0.6.4",
+ "tokio",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "id-arena"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
+
+[[package]]
+name = "indexmap"
+version = "1.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
+dependencies = [
+ "autocfg",
+ "hashbrown 0.12.3",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.17.1",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "itertools"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
+[[package]]
+name = "jiff"
+version = "0.2.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4603d3033e49e2b0e31229fcab20a5d40089c607d975cd9c80551dc69eed9102"
+dependencies = [
+ "jiff-static",
+ "jiff-tzdb-platform",
+ "log",
+ "portable-atomic",
+ "portable-atomic-util",
+ "serde_core",
+ "windows-link",
+]
+
+[[package]]
+name = "jiff-static"
+version = "0.2.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "782d32378dddf207193ac91cefb848ad41abb58195c95168e1291227a0832b47"
+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 = "kstring"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "558bf9508a558512042d3095138b1f7b8fe90c5467d94f9f1da28b3731c5dbd1"
+dependencies = [
+ "serde",
+ "static_assertions",
+]
+
+[[package]]
+name = "leb128fmt"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
+
+[[package]]
+name = "libc"
+version = "0.2.186"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
+
+[[package]]
+name = "litrs"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
+
+[[package]]
+name = "lock_api"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
+dependencies = [
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "113b30b4cd05f7c06868fdb2854f66a7b9fece9a48425351cd532e810d74024f"
+
+[[package]]
+name = "matchit"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
+
+[[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 = "memchr"
+version = "2.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8"
+
+[[package]]
+name = "memmap2"
+version = "0.9.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
+dependencies = [
+ "adler2",
+ "simd-adler32",
+]
+
+[[package]]
+name = "mio"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda"
+dependencies = [
+ "libc",
+ "wasi",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "multimap"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084"
+
+[[package]]
+name = "nonempty"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9737e026353e5cd0736f98eddae28665118eb6f6600902a7f50db585621fecb6"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
+name = "os_pipe"
+version = "1.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "parking_lot"
+version = "0.12.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-link",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
+
+[[package]]
+name = "petgraph"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772"
+dependencies = [
+ "fixedbitset",
+ "indexmap 2.14.0",
+]
+
+[[package]]
+name = "pin-project"
+version = "1.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924"
+dependencies = [
+ "pin-project-internal",
+]
+
+[[package]]
+name = "pin-project-internal"
+version = "1.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+
+[[package]]
+name = "portable-atomic"
+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 = "ppv-lite86"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
+dependencies = [
+ "zerocopy",
+]
+
+[[package]]
+name = "prettyplease"
+version = "0.2.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
+dependencies = [
+ "proc-macro2",
+ "syn",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "prodash"
+version = "31.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "962200e2d7d551451297d9fdce85138374019ada198e30ea9ede38034e27604c"
+dependencies = [
+ "parking_lot",
+]
+
+[[package]]
+name = "prost"
+version = "0.13.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5"
+dependencies = [
+ "bytes",
+ "prost-derive",
+]
+
+[[package]]
+name = "prost-build"
+version = "0.13.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf"
+dependencies = [
+ "heck",
+ "itertools",
+ "log",
+ "multimap",
+ "once_cell",
+ "petgraph",
+ "prettyplease",
+ "prost",
+ "prost-types",
+ "regex",
+ "syn",
+ "tempfile",
+]
+
+[[package]]
+name = "prost-derive"
+version = "0.13.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d"
+dependencies = [
+ "anyhow",
+ "itertools",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "prost-types"
+version = "0.13.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16"
+dependencies = [
+ "prost",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r-efi"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
+
+[[package]]
+name = "rand"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
+dependencies = [
+ "libc",
+ "rand_chacha",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom 0.2.17",
+]
+
+[[package]]
+name = "rawzip"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d9575f44c8cf85bc843ad666dcdf20d05a7753772bef56eb2a5140282b32150"
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "regex"
+version = "1.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
+
+[[package]]
+name = "rustix"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "semver"
+version = "1.0.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.150"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "sha1"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
+dependencies = [
+ "cfg-if",
+ "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]]
+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]]
+name = "shared_child"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e362d9935bc50f019969e2f9ecd66786612daae13e8f277be7bfb66e8bed3f7"
+dependencies = [
+ "libc",
+ "sigchld",
+ "windows-sys 0.60.2",
+]
+
+[[package]]
+name = "shared_thread"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52b86057fcb5423f5018e331ac04623e32d6b5ce85e33300f92c79a1973928b0"
+
+[[package]]
+name = "shell-words"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77"
+
+[[package]]
+name = "sigchld"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47106eded3c154e70176fc83df9737335c94ce22f821c32d17ed1db1f83badb1"
+dependencies = [
+ "libc",
+ "os_pipe",
+ "signal-hook",
+]
+
+[[package]]
+name = "signal-hook"
+version = "0.3.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
+dependencies = [
+ "libc",
+ "signal-hook-registry",
+]
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
+dependencies = [
+ "errno",
+ "libc",
+]
+
+[[package]]
+name = "simd-adler32"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
+
+[[package]]
+name = "slab"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
+
+[[package]]
+name = "smallvec"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "socket2"
+version = "0.5.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
+dependencies = [
+ "libc",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "socket2"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
+
+[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+
+[[package]]
+name = "syn"
+version = "2.0.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "sync_wrapper"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
+
+[[package]]
+name = "tar"
+version = "0.4.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840"
+dependencies = [
+ "filetime",
+ "libc",
+ "xattr",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
+dependencies = [
+ "fastrand",
+ "getrandom 0.4.2",
+ "once_cell",
+ "rustix",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
+[[package]]
+name = "tokio"
+version = "1.52.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
+dependencies = [
+ "bytes",
+ "libc",
+ "mio",
+ "pin-project-lite",
+ "signal-hook-registry",
+ "socket2 0.6.4",
+ "tokio-macros",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tokio-stream"
+version = "0.1.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+ "tokio",
+ "tokio-util",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "tonic"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52"
+dependencies = [
+ "async-stream",
+ "async-trait",
+ "axum",
+ "base64",
+ "bytes",
+ "h2",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-timeout",
+ "hyper-util",
+ "percent-encoding",
+ "pin-project",
+ "prost",
+ "socket2 0.5.10",
+ "tokio",
+ "tokio-stream",
+ "tower 0.4.13",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tonic-build"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9557ce109ea773b399c9b9e5dca39294110b74f1f342cb347a80d1fce8c26a11"
+dependencies = [
+ "prettyplease",
+ "proc-macro2",
+ "prost-build",
+ "prost-types",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tower"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "indexmap 1.9.3",
+ "pin-project",
+ "pin-project-lite",
+ "rand",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tower"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "pin-project-lite",
+ "sync_wrapper",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
+name = "tower-layer"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
+
+[[package]]
+name = "tower-service"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
+
+[[package]]
+name = "tracing"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
+dependencies = [
+ "log",
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+
+[[package]]
+name = "typenum"
+version = "1.20.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
+
+[[package]]
+name = "uluru"
+version = "3.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c8a2469e56e6e5095c82ccd3afb98dad95f7af7929aab6d8ba8d6e0f73657da"
+dependencies = [
+ "arrayvec",
+]
+
+[[package]]
+name = "unicode-bom"
+version = "2.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7eec5d1121208364f6793f7d2e222bf75a915c19557537745b195b253dd64217"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.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-xid"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
+
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "walkdir"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
+[[package]]
+name = "want"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
+dependencies = [
+ "try-lock",
+]
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "wasip2"
+version = "1.0.3+wasi-0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6"
+dependencies = [
+ "wit-bindgen 0.57.1",
+]
+
+[[package]]
+name = "wasip3"
+version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
+dependencies = [
+ "wit-bindgen 0.51.0",
+]
+
+[[package]]
+name = "wasm-encoder"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
+dependencies = [
+ "leb128fmt",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasm-metadata"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
+dependencies = [
+ "anyhow",
+ "indexmap 2.14.0",
+ "wasm-encoder",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasmparser"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
+dependencies = [
+ "bitflags",
+ "hashbrown 0.15.5",
+ "indexmap 2.14.0",
+ "semver",
+]
+
+[[package]]
+name = "winapi-util"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
+dependencies = [
+ "windows-targets 0.53.5",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
+ "windows_i686_gnullvm 0.52.6",
+ "windows_i686_msvc 0.52.6",
+ "windows_x86_64_gnu 0.52.6",
+ "windows_x86_64_gnullvm 0.52.6",
+ "windows_x86_64_msvc 0.52.6",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.53.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
+dependencies = [
+ "windows-link",
+ "windows_aarch64_gnullvm 0.53.1",
+ "windows_aarch64_msvc 0.53.1",
+ "windows_i686_gnu 0.53.1",
+ "windows_i686_gnullvm 0.53.1",
+ "windows_i686_msvc 0.53.1",
+ "windows_x86_64_gnu 0.53.1",
+ "windows_x86_64_gnullvm 0.53.1",
+ "windows_x86_64_msvc 0.53.1",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
+
+[[package]]
+name = "wit-bindgen"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
+dependencies = [
+ "wit-bindgen-rust-macro",
+]
+
+[[package]]
+name = "wit-bindgen"
+version = "0.57.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
+
+[[package]]
+name = "wit-bindgen-core"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
+dependencies = [
+ "anyhow",
+ "heck",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-bindgen-rust"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
+dependencies = [
+ "anyhow",
+ "heck",
+ "indexmap 2.14.0",
+ "prettyplease",
+ "syn",
+ "wasm-metadata",
+ "wit-bindgen-core",
+ "wit-component",
+]
+
+[[package]]
+name = "wit-bindgen-rust-macro"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
+dependencies = [
+ "anyhow",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wit-bindgen-core",
+ "wit-bindgen-rust",
+]
+
+[[package]]
+name = "wit-component"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
+dependencies = [
+ "anyhow",
+ "bitflags",
+ "indexmap 2.14.0",
+ "log",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "wasm-encoder",
+ "wasm-metadata",
+ "wasmparser",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-parser"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
+dependencies = [
+ "anyhow",
+ "id-arena",
+ "indexmap 2.14.0",
+ "log",
+ "semver",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "unicode-xid",
+ "wasmparser",
+]
+
+[[package]]
+name = "xattr"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
+dependencies = [
+ "libc",
+ "rustix",
+]
+
+[[package]]
+name = "zerocopy"
+version = "0.8.50"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.50"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "zlib-rs"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513"
+
+[[package]]
+name = "zmij"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..b433ef4
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,33 @@
+[package]
+name = "gitks"
+version = "1.0.0"
+edition = "2024"
+authors = ["gitks contributors"]
+description = "A gRPC-accessible Git repository operations library for bare repositories"
+repository = ""
+readme = ""
+homepage = ""
+license = "PolyForm-Noncommercial-1.0.0"
+keywords = ["git", "grpc", "bare-repository", "gix"]
+categories = ["development-tools"]
+documentation = ""
+
+[lib]
+path = "lib.rs"
+name = "gitks"
+[dependencies]
+serde = { version = "1.0.228", features = ["derive"] }
+gix = { version = "0.84.0", default-features = false, features = ["serde", "blame", "sha256", "sha1", "tracing", "merge", "max-performance-safe", "revision"] }
+gix-archive = { version = "0.33.0", features = ["sha256","sha1","document-features"] }
+duct = { version = "1.1.1", features = [] }
+tracing = { version = "0.1.32", features = ["log"] }
+tokio = { version = "1", features = ["rt-multi-thread", "macros", "process", "io-util", "sync"] }
+tokio-stream = { version = "0.1.18", features = ["full"] }
+thiserror = { version = "2.0.18", features = [] }
+prost = "0.13"
+prost-types = "0.13"
+tonic = { version = "0.12", features = ["transport"] }
+tempfile = "3"
+
+[build-dependencies]
+tonic-build = "0.12"
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..c7728b1
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,58 @@
+PolyForm Noncommercial License 1.0.0
+
+Copyright (c) 2024 gitks contributors
+
+License: "Noncommercial" as defined below.
+
+"Noncommercial" means primarily intended for or directed towards the
+advantage or monetary gain of a business, commercial entity, or for-profit
+organization. A use is "Noncommercial" if it is not primarily intended for
+or directed towards commercial advantage or monetary compensation.
+
+1. Grant of Copyright License. Subject to the terms of this license,
+ Licensor grants you a worldwide, royalty-free, non-exclusive, limited
+ license to exercise the Licensed Rights in the Licensed Material for
+ Noncommercial purposes only.
+
+2. Grant of Patent License. Subject to the terms of this license, Licensor
+ grants you a worldwide, royalty-free, non-exclusive, limited license
+ under patent claims owned or controlled by Licensor that are embodied
+ in the Licensed Material as furnished by Licensor, to make, use, sell,
+ offer for sale, have made, and import the Licensed Material for
+ Noncommercial purposes only.
+
+3. Limitations. The license granted in Section 1 and Section 2 above is
+ expressly limited to Noncommercial purposes. You may not exercise the
+ Licensed Rights for the purpose of providing services to third parties,
+ including but not limited to:
+ (a) offering the Licensed Material as a hosted or managed service
+ where third parties access or use the Licensed Material;
+ (b) offering the Licensed Material as part of a product or service
+ that is sold, licensed, or otherwise provided for monetary gain;
+ (c) using the Licensed Material to provide consulting, support, or
+ other services for monetary gain.
+
+4. Acceptance. Any use of the Licensed Material in violation of this
+ license will automatically terminate your rights under this license
+ for the current and all future versions of the Licensed Material.
+
+5. Patents. If you institute patent litigation against any entity
+ (including a cross-claim or counterclaim in a lawsuit) alleging that
+ the Licensed Material constitutes direct or contributory patent
+ infringement, then any patent licenses granted to you under this
+ license for the Licensed Material shall terminate as of the date
+ such litigation is filed.
+
+6. Disclaimer of Warranty. THE LICENSED MATERIAL IS PROVIDED "AS IS" AND
+ WITHOUT ANY WARRANTY OF ANY KIND. LICENSOR DISCLAIMS ALL WARRANTIES,
+ EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED WARRANTIES
+ OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A
+ PARTICULAR PURPOSE.
+
+7. Limitation of Liability. IN NO EVENT WILL LICENSOR BE LIABLE TO YOU
+ FOR ANY DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL, OR
+ CONSEQUENTIAL DAMAGES ARISING OUT OF THESE TERMS OR IN CONNECTION
+ WITH THE USE OR INABILITY TO USE THE LICENSED MATERIAL, EVEN IF
+ LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+
+For the full license text, see: https://polyformproject.org/licenses/noncommercial/1.0.0
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..7cab0a9
--- /dev/null
+++ b/README.md
@@ -0,0 +1,7 @@
+# gitks
+A Git bare repository operation library based on gRPC.
+
+
+## License
+
+[PolyForm Noncommercial 1.0.0](LICENSE) — Free for noncommercial use. For commercial licenses, please contact us.
\ No newline at end of file
diff --git a/archive/get_archive.rs b/archive/get_archive.rs
new file mode 100644
index 0000000..14e2134
--- /dev/null
+++ b/archive/get_archive.rs
@@ -0,0 +1,45 @@
+use std::process::Command;
+
+use crate::bare::GitBare;
+use crate::error::{GitError, GitResult};
+use crate::pb::{ArchiveChunk, ArchiveRequest, archive_options, object_selector};
+
+impl GitBare {
+ pub fn get_archive(&self, request: ArchiveRequest) -> GitResult> {
+ let revision = match request.treeish.and_then(|s| s.selector) {
+ Some(object_selector::Selector::Oid(oid)) => oid.hex,
+ Some(object_selector::Selector::Revision(name)) => name.revision,
+ None => "HEAD".into(),
+ };
+ let options = request.options.unwrap_or_default();
+ let format = archive_options::Format::try_from(options.format)
+ .unwrap_or(archive_options::Format::ArchiveFormatTar);
+ let mut args = vec!["archive".to_string()];
+ args.push(match format {
+ archive_options::Format::ArchiveFormatZip => "--format=zip".into(),
+ _ => "--format=tar".into(),
+ });
+ if !options.prefix.is_empty() {
+ args.push(format!("--prefix={}", options.prefix));
+ }
+ args.push(revision);
+ if !options.pathspec.is_empty() {
+ args.push("--".into());
+ args.extend(options.pathspec);
+ }
+ let output = Command::new("git")
+ .arg("--git-dir")
+ .arg(&self.bare_dir)
+ .args(&args)
+ .output()?;
+ if !output.status.success() {
+ return Err(GitError::CommandFailed {
+ status_code: output.status.code(),
+ stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
+ });
+ }
+ Ok(vec![ArchiveChunk {
+ data: output.stdout,
+ }])
+ }
+}
diff --git a/archive/list_archive_entries.rs b/archive/list_archive_entries.rs
new file mode 100644
index 0000000..898fae1
--- /dev/null
+++ b/archive/list_archive_entries.rs
@@ -0,0 +1,68 @@
+use std::process::Command;
+
+use crate::bare::GitBare;
+use crate::error::{GitError, GitResult};
+use crate::pb::{
+ ArchiveEntry, ListArchiveEntriesRequest, ListArchiveEntriesResponse, ObjectType, PageInfo,
+ object_selector,
+};
+
+impl GitBare {
+ pub fn list_archive_entries(
+ &self,
+ request: ListArchiveEntriesRequest,
+ ) -> GitResult {
+ let revision = match request.treeish.and_then(|s| s.selector) {
+ Some(object_selector::Selector::Oid(oid)) => oid.hex,
+ Some(object_selector::Selector::Revision(name)) => name.revision,
+ None => "HEAD".into(),
+ };
+ let mut args = vec!["ls-tree".to_string(), "-r".into(), "-l".into(), revision];
+ if !request.pathspec.is_empty() {
+ args.push("--".into());
+ args.extend(request.pathspec);
+ }
+ let output = Command::new("git")
+ .arg("--git-dir")
+ .arg(&self.bare_dir)
+ .args(&args)
+ .output()?;
+ if !output.status.success() {
+ return Err(GitError::CommandFailed {
+ status_code: output.status.code(),
+ stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
+ });
+ }
+ let entries = String::from_utf8_lossy(&output.stdout)
+ .lines()
+ .filter_map(|line| {
+ let (meta, path) = line.split_once('\t')?;
+ let parts: Vec<&str> = meta.split_whitespace().collect();
+ let hex = parts.get(2)?.to_string();
+ Some(ArchiveEntry {
+ path: path.to_string(),
+ oid: Some(self.oid_to_pb(hex)),
+ mode: u32::from_str_radix(parts.first().copied().unwrap_or("0"), 8)
+ .unwrap_or(0),
+ size: parts.get(3).and_then(|s| s.parse().ok()).unwrap_or(0),
+ r#type: match parts.get(1).copied().unwrap_or_default() {
+ "tree" => ObjectType::Tree as i32,
+ "blob" => ObjectType::Blob as i32,
+ "commit" => ObjectType::Commit as i32,
+ "tag" => ObjectType::Tag as i32,
+ _ => ObjectType::Unspecified as i32,
+ },
+ })
+ })
+ .collect::>();
+ let total_count = entries.len() as u64;
+ Ok(ListArchiveEntriesResponse {
+ entries,
+ page_info: Some(PageInfo {
+ next_page_token: String::new(),
+ has_next_page: false,
+ total_count,
+ }),
+ })
+ }
+}
diff --git a/archive/mod.rs b/archive/mod.rs
new file mode 100644
index 0000000..ddd2882
--- /dev/null
+++ b/archive/mod.rs
@@ -0,0 +1,2 @@
+pub mod get_archive;
+pub mod list_archive_entries;
diff --git a/bare.rs b/bare.rs
new file mode 100644
index 0000000..edd5e73
--- /dev/null
+++ b/bare.rs
@@ -0,0 +1,112 @@
+use std::path::{Path, PathBuf};
+
+use crate::error::{GitError, GitResult};
+use crate::pb::RepositoryHeader;
+
+pub struct GitBare {
+ pub bare_dir: PathBuf,
+}
+
+impl GitBare {
+ pub fn gix_repo(&self) -> GitResult {
+ gix::open(&self.bare_dir)
+ .map_err(|e| GitError::Internal(format!("failed to open gix repository: {e}")))
+ }
+
+ pub fn from_repository_header(header: &RepositoryHeader) -> GitResult {
+ let storage_path = header.storage_path.trim();
+ let relative_path = header.relative_path.trim();
+ let storage_name = header.storage_name.trim();
+ let _ = storage_name; // reserved for future sharding logic
+
+ // Build base path: storage_path if given, else relative_path alone
+ let base = if !storage_path.is_empty() {
+ let p = Path::new(storage_path);
+ if !p.is_absolute() {
+ return Err(GitError::InvalidArgument(
+ "storage_path must be an absolute path".into(),
+ ));
+ }
+ PathBuf::from(p)
+ } else if !relative_path.is_empty() {
+ // relative_path alone is rejected unless absolute
+ return Err(GitError::InvalidArgument(
+ "relative_path requires storage_path to be set".into(),
+ ));
+ } else {
+ return Err(GitError::InvalidArgument("empty repository path".into()));
+ };
+
+ // Join relative_path if provided
+ let bare_dir = if !relative_path.is_empty() && !storage_path.is_empty() {
+ let candidate = base.join(relative_path);
+ // Canonicalize to resolve any `..` / symlinks, then check still under base
+ let canonical = candidate
+ .canonicalize()
+ .unwrap_or_else(|_| candidate.clone());
+ // Path traversal check: canonical resolved dir must start with base
+ let base_canon = base.canonicalize().unwrap_or_else(|_| base.clone());
+ if !canonical.starts_with(&base_canon) {
+ return Err(GitError::InvalidArgument(format!(
+ "path traversal detected: {relative_path} escapes storage root"
+ )));
+ }
+ canonical
+ } else if !storage_path.is_empty() {
+ base.canonicalize().unwrap_or(base)
+ } else {
+ return Err(GitError::InvalidArgument("empty repository path".into()));
+ };
+
+ // Validate bare_dir exists, is a directory, and is readable
+ if !bare_dir.exists() {
+ return Err(GitError::RepoNotFound);
+ }
+ if !bare_dir.is_dir() {
+ return Err(GitError::InvalidArgument(format!(
+ "not a directory: {}",
+ bare_dir.display()
+ )));
+ }
+
+ // Accept either bare repos (HEAD file) or non-bare (HEAD + .git)
+ let head_path = bare_dir.join("HEAD");
+ if !head_path.exists() {
+ // Maybe it's a non-bare repo
+ let git_dir = bare_dir.join(".git");
+ if git_dir.is_dir() && git_dir.join("HEAD").exists() {
+ return Ok(Self { bare_dir: git_dir });
+ }
+ return Err(GitError::NotBareRepository);
+ }
+
+ Ok(Self { bare_dir })
+ }
+
+ /// Detect the repository's object format (SHA-1 or SHA-256).
+ pub fn object_format(&self) -> crate::pb::ObjectFormat {
+ let repo = self.gix_repo().ok();
+ let kind = repo
+ .map(|r| r.object_hash())
+ .unwrap_or(gix::hash::Kind::Sha1);
+ match kind {
+ gix::hash::Kind::Sha1 => crate::pb::ObjectFormat::Sha1,
+ gix::hash::Kind::Sha256 => crate::pb::ObjectFormat::Sha256,
+ _ => crate::pb::ObjectFormat::Unspecified,
+ }
+ }
+
+ /// Convert a hex object id to a protobuf Oid.
+ ///
+ /// `Oid.value` is the binary hash bytes, while `Oid.hex` keeps the printable
+ /// lowercase representation for clients that prefer string IDs.
+ pub fn oid_to_pb(&self, hex: impl Into) -> crate::pb::Oid {
+ let hex = hex.into().to_lowercase();
+ let format = self.object_format();
+ crate::pb::Oid {
+ value: crate::oid::hex_to_bytes(&hex).unwrap_or_default(),
+ hex,
+ format: format as i32,
+ }
+ }
+}
diff --git a/blame/do_blame.rs b/blame/do_blame.rs
new file mode 100644
index 0000000..dc98a5c
--- /dev/null
+++ b/blame/do_blame.rs
@@ -0,0 +1,152 @@
+use crate::bare::GitBare;
+use crate::error::{GitError, GitResult};
+use crate::pb::{BlameHunk, BlameLine, BlameRequest, BlameResponse, PageInfo};
+
+impl GitBare {
+ pub fn blame(&self, request: BlameRequest) -> GitResult {
+ let revision = match request.revision.and_then(|s| s.selector) {
+ Some(crate::pb::object_selector::Selector::Oid(oid)) => oid.hex,
+ Some(crate::pb::object_selector::Selector::Revision(name)) => name.revision,
+ None => "HEAD".into(),
+ };
+ let mut args = vec![
+ "--git-dir".to_string(),
+ self.bare_dir.to_string_lossy().into_owned(),
+ "blame".to_string(),
+ "--porcelain".to_string(),
+ ];
+ if let Some(range) = &request.range {
+ args.push("-L".into());
+ args.push(format!("{},{}", range.start, range.end));
+ }
+ args.push(revision);
+ args.push("--".into());
+ args.push(request.path.clone());
+ let result = duct::cmd("git", &args)
+ .stdout_capture()
+ .stderr_capture()
+ .unchecked()
+ .run()?;
+ if !result.status.success() {
+ return Err(GitError::CommandFailed {
+ status_code: result.status.code(),
+ stderr: String::from_utf8_lossy(&result.stderr).into_owned(),
+ });
+ }
+ let output = String::from_utf8_lossy(&result.stdout);
+ let hunks = parse_porcelain_blame(&output, &request.path, self);
+ let total_count = hunks.len() as u64;
+ Ok(BlameResponse {
+ hunks,
+ page_info: Some(PageInfo {
+ next_page_token: String::new(),
+ has_next_page: false,
+ total_count,
+ }),
+ truncated: false,
+ })
+ }
+}
+
+fn parse_porcelain_blame(output: &str, path: &str, repo: &GitBare) -> Vec {
+ let mut hunks = Vec::new();
+ let mut current_hunk: Option = None;
+
+ for line in output.lines() {
+ if let Some(content) = line.strip_prefix('\t') {
+ if let Some(ref mut hunk) = current_hunk {
+ let next_line_no = hunk.final_start_line + hunk.lines.len() as u32;
+ hunk.lines.push(BlameLine {
+ final_line: next_line_no,
+ original_line: 0,
+ content: content.as_bytes().to_vec(),
+ });
+ }
+ continue;
+ }
+
+ let parts: Vec<&str> = line.splitn(4, ' ').collect();
+ if parts.is_empty() {
+ continue;
+ }
+
+ let token = parts[0];
+ if token.len() == 40 || token.len() == 64 {
+ let orig_line: u32 = parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(0);
+ let final_line: u32 = parts.get(2).and_then(|s| s.parse().ok()).unwrap_or(0);
+
+ if let Some(prev) = current_hunk.take() {
+ hunks.push(prev);
+ }
+
+ current_hunk = Some(BlameHunk {
+ commit: Some(crate::pb::Commit {
+ oid: Some(repo.oid_to_pb(token)),
+ abbreviated_oid: token.chars().take(7).collect(),
+ subject: String::new(),
+ message: String::new(),
+ ..Default::default()
+ }),
+ original_path: String::new(),
+ final_path: path.to_string(),
+ original_start_line: orig_line,
+ final_start_line: final_line,
+ line_count: 0,
+ boundary: false,
+ lines: Vec::new(),
+ });
+ } else if let Some(ref mut hunk) = current_hunk {
+ match token {
+ "author" => {
+ if let Some(commit) = hunk.commit.as_mut() {
+ let name = line.strip_prefix("author ").unwrap_or_default();
+ if let Some(sig) = commit.author.as_mut() {
+ if let Some(id) = sig.identity.as_mut() {
+ id.name = name.to_string();
+ }
+ } else {
+ commit.author = Some(crate::pb::Signature {
+ identity: Some(crate::pb::Identity {
+ name: name.to_string(),
+ email: String::new(),
+ }),
+ ..Default::default()
+ });
+ }
+ }
+ }
+ "author-mail" => {
+ if let Some(commit) = hunk.commit.as_mut() {
+ let email = line
+ .strip_prefix("author-mail ")
+ .unwrap_or_default()
+ .trim_matches(|c| c == '<' || c == '>')
+ .to_string();
+ if let Some(sig) = commit.author.as_mut()
+ && let Some(id) = sig.identity.as_mut()
+ {
+ id.email = email;
+ }
+ }
+ }
+ "filename" => {
+ hunk.original_path = line.strip_prefix("filename ").unwrap_or(path).to_string();
+ }
+ "boundary" => {
+ hunk.boundary = true;
+ }
+ _ => {}
+ }
+ }
+ }
+
+ if let Some(hunk) = current_hunk {
+ hunks.push(hunk);
+ }
+
+ for hunk in &mut hunks {
+ hunk.line_count = hunk.lines.len() as u32;
+ }
+
+ hunks
+}
diff --git a/blame/mod.rs b/blame/mod.rs
new file mode 100644
index 0000000..9225936
--- /dev/null
+++ b/blame/mod.rs
@@ -0,0 +1 @@
+pub mod do_blame;
diff --git a/blob/get_blob.rs b/blob/get_blob.rs
new file mode 100644
index 0000000..d2e39c5
--- /dev/null
+++ b/blob/get_blob.rs
@@ -0,0 +1,70 @@
+use gix::object::tree::EntryKind;
+
+use crate::bare::GitBare;
+use crate::error::{GitError, GitResult};
+use crate::pb::{Blob, GetBlobRequest, object_selector};
+
+impl GitBare {
+ pub fn get_blob(&self, request: GetBlobRequest) -> GitResult {
+ let repo = self.gix_repo()?;
+ let (blob, mode, path) = if let Some(oid) = request.oid.as_ref() {
+ let id = gix::hash::ObjectId::from_hex(oid.hex.as_bytes())
+ .map_err(|e| GitError::InvalidOid(e.to_string()))?;
+ (
+ repo.find_object(id)?
+ .try_into_blob()
+ .map_err(|e| GitError::Gix(e.to_string()))?,
+ 0,
+ request.path,
+ )
+ } else {
+ let revision = match request.revision.and_then(|s| s.selector) {
+ Some(object_selector::Selector::Oid(oid)) => oid.hex,
+ Some(object_selector::Selector::Revision(name)) => name.revision,
+ None => "HEAD".into(),
+ };
+ let tree = repo
+ .rev_parse_single(format!("{}^{{tree}}", revision).as_str())?
+ .object()?
+ .try_into_tree()
+ .map_err(|e| GitError::Gix(e.to_string()))?;
+ let entry = tree
+ .lookup_entry_by_path(&request.path)?
+ .ok_or_else(|| GitError::NotFound(request.path.clone()))?;
+ let mode = u32::from_str_radix(&format!("{:o}", entry.mode()), 8).unwrap_or(0);
+ if !matches!(
+ entry.mode().kind(),
+ EntryKind::Blob | EntryKind::BlobExecutable | EntryKind::Link
+ ) {
+ return Err(GitError::InvalidArgument(
+ "path does not point to a blob".into(),
+ ));
+ }
+ (
+ entry
+ .object()?
+ .try_into_blob()
+ .map_err(|e| GitError::Gix(e.to_string()))?,
+ mode,
+ request.path,
+ )
+ };
+ let original_size = blob.data.len() as i64;
+ let mut data = blob.data.clone();
+ let truncated = request.max_bytes > 0 && data.len() > request.max_bytes as usize;
+ if truncated {
+ data.truncate(request.max_bytes as usize);
+ }
+ let hex = blob.id.to_string();
+ Ok(Blob {
+ oid: Some(self.oid_to_pb(hex)),
+ path,
+ mode,
+ size: original_size,
+ binary: blob.data.contains(&0),
+ encoding: String::new(),
+ truncated,
+ data,
+ })
+ }
+}
diff --git a/blob/get_raw_blob.rs b/blob/get_raw_blob.rs
new file mode 100644
index 0000000..ca54496
--- /dev/null
+++ b/blob/get_raw_blob.rs
@@ -0,0 +1,16 @@
+use crate::bare::GitBare;
+use crate::error::GitResult;
+use crate::pb::{GetBlobRequest, GetRawBlobRequest, GetRawBlobResponse};
+
+impl GitBare {
+ pub fn get_raw_blob(&self, request: GetRawBlobRequest) -> GitResult> {
+ let blob = self.get_blob(GetBlobRequest {
+ repository: request.repository,
+ revision: request.revision,
+ path: request.path,
+ oid: request.oid,
+ max_bytes: 0,
+ })?;
+ Ok(vec![GetRawBlobResponse { data: blob.data }])
+ }
+}
diff --git a/blob/mod.rs b/blob/mod.rs
new file mode 100644
index 0000000..b47e4d9
--- /dev/null
+++ b/blob/mod.rs
@@ -0,0 +1,2 @@
+pub mod get_blob;
+pub mod get_raw_blob;
diff --git a/branch/compare_branch.rs b/branch/compare_branch.rs
new file mode 100644
index 0000000..e3ef994
--- /dev/null
+++ b/branch/compare_branch.rs
@@ -0,0 +1,54 @@
+use crate::bare::GitBare;
+use crate::error::{GitError, GitResult};
+use crate::pb::{CompareBranchRequest, CompareBranchResponse};
+
+impl GitBare {
+ pub fn compare_branch(
+ &self,
+ request: CompareBranchRequest,
+ ) -> GitResult {
+ let repo = self.gix_repo()?;
+ let source_ref = format!("refs/heads/{}", request.source_branch);
+ let target_ref = format!("refs/heads/{}", request.target_branch);
+ let source_id = repo.find_reference(source_ref.as_str())?.peel_to_id()?;
+ let target_id = repo.find_reference(target_ref.as_str())?.peel_to_id()?;
+ let source_hex = source_id.to_string();
+ let target_hex = target_id.to_string();
+ let merge_base = repo
+ .merge_base(source_id.detach(), target_id.detach())
+ .ok()
+ .map(|id| self.oid_to_pb(id.to_string()));
+ let result = duct::cmd(
+ "git",
+ [
+ "--git-dir",
+ self.bare_dir.to_string_lossy().as_ref(),
+ "rev-list",
+ "--left-right",
+ "--count",
+ &format!("{}...{}", source_hex, target_hex),
+ ],
+ )
+ .stdout_capture()
+ .stderr_capture()
+ .unchecked()
+ .run()?;
+ if !result.status.success() {
+ return Err(GitError::CommandFailed {
+ status_code: result.status.code(),
+ stderr: String::from_utf8_lossy(&result.stderr).into_owned(),
+ });
+ }
+ let output = String::from_utf8_lossy(&result.stdout);
+ let parts: Vec<&str> = output.split_whitespace().collect();
+ let ahead_by = parts.first().and_then(|s| s.parse().ok()).unwrap_or(0);
+ let behind_by = parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(0);
+ Ok(CompareBranchResponse {
+ ahead: ahead_by > 0,
+ behind: behind_by > 0,
+ ahead_by,
+ behind_by,
+ merge_base,
+ })
+ }
+}
diff --git a/branch/create_branch.rs b/branch/create_branch.rs
new file mode 100644
index 0000000..807a1cb
--- /dev/null
+++ b/branch/create_branch.rs
@@ -0,0 +1,36 @@
+use std::process::Command;
+
+use crate::bare::GitBare;
+use crate::error::{GitError, GitResult};
+use crate::pb::{Branch, CreateBranchRequest, GetBranchRequest, object_selector};
+
+impl GitBare {
+ pub fn create_branch(&self, request: CreateBranchRequest) -> GitResult {
+ let revision = match request.start_point.and_then(|s| s.selector) {
+ Some(object_selector::Selector::Oid(oid)) => oid.hex,
+ Some(object_selector::Selector::Revision(name)) => name.revision,
+ None => "HEAD".into(),
+ };
+ let mut args = vec!["branch".to_string()];
+ if request.force {
+ args.push("-f".into());
+ }
+ args.push(request.name.clone());
+ args.push(revision);
+ let output = Command::new("git")
+ .arg("--git-dir")
+ .arg(&self.bare_dir)
+ .args(&args)
+ .output()?;
+ if !output.status.success() {
+ return Err(GitError::CommandFailed {
+ status_code: output.status.code(),
+ stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
+ });
+ }
+ self.get_branch(GetBranchRequest {
+ repository: request.repository,
+ name: request.name,
+ })
+ }
+}
diff --git a/branch/delete_branch.rs b/branch/delete_branch.rs
new file mode 100644
index 0000000..6c55284
--- /dev/null
+++ b/branch/delete_branch.rs
@@ -0,0 +1,23 @@
+use std::process::Command;
+
+use crate::bare::GitBare;
+use crate::error::{GitError, GitResult};
+use crate::pb::DeleteBranchRequest;
+
+impl GitBare {
+ pub fn delete_branch(&self, request: DeleteBranchRequest) -> GitResult<()> {
+ let flag = if request.force { "-D" } else { "-d" };
+ let output = Command::new("git")
+ .arg("--git-dir")
+ .arg(&self.bare_dir)
+ .args(["branch", flag, &request.name])
+ .output()?;
+ if !output.status.success() {
+ return Err(GitError::CommandFailed {
+ status_code: output.status.code(),
+ stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
+ });
+ }
+ Ok(())
+ }
+}
diff --git a/branch/get_branch.rs b/branch/get_branch.rs
new file mode 100644
index 0000000..b737f4a
--- /dev/null
+++ b/branch/get_branch.rs
@@ -0,0 +1,23 @@
+use crate::bare::GitBare;
+use crate::error::GitResult;
+use crate::pb::{Branch, GetBranchRequest};
+
+impl GitBare {
+ pub fn get_branch(&self, request: GetBranchRequest) -> GitResult {
+ let repo = self.gix_repo()?;
+ let refname = format!("refs/heads/{}", request.name);
+ let mut r = repo.find_reference(refname.as_str())?;
+ let hex = r.peel_to_id()?.to_string();
+ Ok(Branch {
+ name: request.name,
+ full_ref: refname,
+ target_oid: Some(self.oid_to_pb(hex)),
+ commit: None,
+ upstream: None,
+ is_default: false,
+ is_head: false,
+ is_merged: false,
+ is_detached: false,
+ })
+ }
+}
diff --git a/branch/list_branches.rs b/branch/list_branches.rs
new file mode 100644
index 0000000..b72253a
--- /dev/null
+++ b/branch/list_branches.rs
@@ -0,0 +1,78 @@
+use crate::bare::GitBare;
+use crate::error::GitResult;
+use crate::paginate;
+use crate::pb::{Branch, ListBranchesRequest, ListBranchesResponse};
+
+impl GitBare {
+ pub fn list_branches(&self, request: ListBranchesRequest) -> GitResult {
+ let repo = self.gix_repo()?;
+
+ let merged_set = if request.merged_into_head || request.not_merged_into_head {
+ let flag = if request.merged_into_head {
+ "--merged"
+ } else {
+ "--no-merged"
+ };
+ let check = duct::cmd(
+ "git",
+ [
+ "--git-dir",
+ self.bare_dir.to_string_lossy().as_ref(),
+ "branch",
+ flag,
+ "HEAD",
+ ],
+ )
+ .stdout_capture()
+ .stderr_capture()
+ .unchecked()
+ .run();
+ match check {
+ Ok(out) => String::from_utf8_lossy(&out.stdout)
+ .lines()
+ .map(|l| l.trim().trim_start_matches("* ").to_string())
+ .collect::>(),
+ Err(_) => Vec::new(),
+ }
+ } else {
+ Vec::new()
+ };
+
+ let mut branches: Vec = Vec::new();
+ for r in repo.references()?.local_branches()? {
+ let mut r = r.map_err(|e| crate::error::GitError::Gix(e.to_string()))?;
+ let name = r.name().shorten().to_string();
+ if !request.pattern.is_empty() && !name.contains(&request.pattern) {
+ continue;
+ }
+ if request.merged_into_head && !merged_set.contains(&name) {
+ continue;
+ }
+ if request.not_merged_into_head && merged_set.contains(&name) {
+ continue;
+ }
+ let hex = r
+ .peel_to_id()
+ .ok()
+ .map(|id| id.to_string())
+ .unwrap_or_default();
+ branches.push(Branch {
+ name,
+ full_ref: r.name().to_string(),
+ target_oid: Some(self.oid_to_pb(hex)),
+ commit: None,
+ upstream: None,
+ is_default: false,
+ is_head: false,
+ is_merged: false,
+ is_detached: false,
+ });
+ }
+ paginate::apply_sort(&mut branches, request.sort_direction);
+ let (branches, page_info) = paginate::paginate(&branches, request.pagination.as_ref());
+ Ok(ListBranchesResponse {
+ branches,
+ page_info: Some(page_info),
+ })
+ }
+}
diff --git a/branch/mod.rs b/branch/mod.rs
new file mode 100644
index 0000000..c0c2e6e
--- /dev/null
+++ b/branch/mod.rs
@@ -0,0 +1,8 @@
+pub mod compare_branch;
+pub mod create_branch;
+pub mod delete_branch;
+pub mod get_branch;
+pub mod list_branches;
+pub mod rename_branch;
+pub mod set_branch_upstream;
+pub mod update_branch_target;
diff --git a/branch/rename_branch.rs b/branch/rename_branch.rs
new file mode 100644
index 0000000..010b213
--- /dev/null
+++ b/branch/rename_branch.rs
@@ -0,0 +1,25 @@
+use std::process::Command;
+
+use crate::bare::GitBare;
+use crate::error::{GitError, GitResult};
+use crate::pb::{Branch, GetBranchRequest, RenameBranchRequest};
+
+impl GitBare {
+ pub fn rename_branch(&self, request: RenameBranchRequest) -> GitResult {
+ let output = Command::new("git")
+ .arg("--git-dir")
+ .arg(&self.bare_dir)
+ .args(["branch", "-m", &request.old_name, &request.new_name])
+ .output()?;
+ if !output.status.success() {
+ return Err(GitError::CommandFailed {
+ status_code: output.status.code(),
+ stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
+ });
+ }
+ self.get_branch(GetBranchRequest {
+ repository: request.repository,
+ name: request.new_name,
+ })
+ }
+}
diff --git a/branch/set_branch_upstream.rs b/branch/set_branch_upstream.rs
new file mode 100644
index 0000000..21acedd
--- /dev/null
+++ b/branch/set_branch_upstream.rs
@@ -0,0 +1,36 @@
+use crate::bare::GitBare;
+use crate::error::{GitError, GitResult};
+use crate::pb::{Branch, GetBranchRequest, SetBranchUpstreamRequest};
+
+impl GitBare {
+ pub fn set_branch_upstream(&self, request: SetBranchUpstreamRequest) -> GitResult {
+ if let Some(upstream) = request.upstream {
+ let tracking = format!("{}/{}", upstream.remote_name, upstream.remote_branch_name);
+ let result = duct::cmd(
+ "git",
+ [
+ "--git-dir",
+ self.bare_dir.to_string_lossy().as_ref(),
+ "branch",
+ "--set-upstream-to",
+ &tracking,
+ &request.name,
+ ],
+ )
+ .stdout_capture()
+ .stderr_capture()
+ .unchecked()
+ .run()?;
+ if !result.status.success() {
+ return Err(GitError::CommandFailed {
+ status_code: result.status.code(),
+ stderr: String::from_utf8_lossy(&result.stderr).into_owned(),
+ });
+ }
+ }
+ self.get_branch(GetBranchRequest {
+ repository: request.repository,
+ name: request.name,
+ })
+ }
+}
diff --git a/branch/update_branch_target.rs b/branch/update_branch_target.rs
new file mode 100644
index 0000000..55675b1
--- /dev/null
+++ b/branch/update_branch_target.rs
@@ -0,0 +1,39 @@
+#![allow(clippy::collapsible_if)]
+use std::process::Command;
+
+use crate::bare::GitBare;
+use crate::error::{GitError, GitResult};
+use crate::pb::{Branch, GetBranchRequest, UpdateBranchTargetRequest};
+
+impl GitBare {
+ pub fn update_branch_target(&self, request: UpdateBranchTargetRequest) -> GitResult {
+ let new_oid = request
+ .new_oid
+ .as_ref()
+ .ok_or_else(|| GitError::InvalidArgument("new_oid is required".into()))?
+ .hex
+ .clone();
+ let refname = format!("refs/heads/{}", request.name);
+ let mut args = vec!["update-ref".to_string(), refname.clone(), new_oid];
+ if !request.force
+ && let Some(old) = request.expected_old_oid.as_ref()
+ {
+ args.push(old.hex.clone());
+ }
+ let output = Command::new("git")
+ .arg("--git-dir")
+ .arg(&self.bare_dir)
+ .args(&args)
+ .output()?;
+ if !output.status.success() {
+ return Err(GitError::CommandFailed {
+ status_code: output.status.code(),
+ stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
+ });
+ }
+ self.get_branch(GetBranchRequest {
+ repository: request.repository,
+ name: refname.trim_start_matches("refs/heads/").to_string(),
+ })
+ }
+}
diff --git a/build.rs b/build.rs
new file mode 100644
index 0000000..eb690ce
--- /dev/null
+++ b/build.rs
@@ -0,0 +1,51 @@
+use std::fs;
+use std::path::{Path, PathBuf};
+
+fn main() -> Result<(), Box> {
+ let manifest_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR")?);
+ let proto_dir = manifest_dir.join("proto");
+ let out_dir = PathBuf::from(std::env::var("OUT_DIR")?);
+
+ fs::create_dir_all(&out_dir)?;
+ clean_generated_files(&out_dir)?;
+
+ let protos = proto_files(&proto_dir)?;
+ for proto in &protos {
+ println!("cargo:rerun-if-changed={}", proto.display());
+ }
+ println!("cargo:rerun-if-changed={}", proto_dir.display());
+ println!("cargo:rerun-if-changed=build.rs");
+
+ tonic_build::configure()
+ .build_client(true)
+ .build_server(true)
+ .emit_rerun_if_changed(false)
+ .out_dir(&out_dir)
+ .compile_protos(&protos, &[proto_dir])?;
+
+ Ok(())
+}
+
+fn proto_files(proto_dir: &Path) -> Result, Box> {
+ let mut files = fs::read_dir(proto_dir)?
+ .map(|entry| entry.map(|entry| entry.path()))
+ .collect::, _>>()?;
+
+ files.retain(|path| path.extension().is_some_and(|ext| ext == "proto"));
+ files.sort();
+ Ok(files)
+}
+
+fn clean_generated_files(out_dir: &Path) -> Result<(), Box> {
+ for entry in fs::read_dir(out_dir)? {
+ let path = entry?.path();
+ let is_generated_rs = path.extension().is_some_and(|ext| ext == "rs")
+ && path.file_name().is_some_and(|name| name != "mod.rs");
+
+ if is_generated_rs {
+ fs::remove_file(path)?;
+ }
+ }
+
+ Ok(())
+}
diff --git a/commit/cherry_pick_commit.rs b/commit/cherry_pick_commit.rs
new file mode 100644
index 0000000..fdf1690
--- /dev/null
+++ b/commit/cherry_pick_commit.rs
@@ -0,0 +1,161 @@
+use crate::bare::GitBare;
+use crate::commit::create_commit::command_ok;
+use crate::error::{GitError, GitResult};
+use crate::pb::{CherryPickCommitRequest, CreateCommitResponse, GetCommitRequest};
+
+impl GitBare {
+ pub fn cherry_pick_commit(
+ &self,
+ request: CherryPickCommitRequest,
+ ) -> GitResult {
+ let target_branch = request.branch.clone();
+ let cp_revision = match request.commit.and_then(|s| s.selector) {
+ Some(crate::pb::object_selector::Selector::Oid(oid)) => oid.hex,
+ Some(crate::pb::object_selector::Selector::Revision(name)) => name.revision,
+ None => return Err(GitError::InvalidArgument("commit is required".into())),
+ };
+
+ let repo = self.gix_repo()?;
+
+ let branch_ref = format!("refs/heads/{}", target_branch);
+ let branch_tip = repo
+ .find_reference(branch_ref.as_str())
+ .ok()
+ .and_then(|mut r| r.peel_to_id().ok())
+ .map(|id| id.to_string())
+ .ok_or_else(|| GitError::RefNotFound(target_branch.clone()))?;
+
+ let cp_id = repo.rev_parse_single(cp_revision.as_str())?;
+ let cp_obj = cp_id
+ .object()?
+ .try_into_commit()
+ .map_err(|e| GitError::Gix(e.to_string()))?;
+ let parent_id = cp_obj.parent_ids().next().map(|p| p.to_string());
+
+ let tmp_index = tempfile::Builder::new()
+ .prefix("gitks-cp-")
+ .tempfile_in(&self.bare_dir)?;
+ let idx_path = tmp_index.path().to_string_lossy().into_owned();
+ let bare = self.bare_dir.to_string_lossy().into_owned();
+
+ let read_tree = duct::cmd(
+ "git",
+ ["--git-dir", bare.as_str(), "read-tree", branch_tip.as_str()],
+ )
+ .env("GIT_INDEX_FILE", &idx_path)
+ .stdout_capture()
+ .stderr_capture()
+ .unchecked()
+ .run()?;
+ command_ok(read_tree)?;
+
+ let mut format_patch_args = vec![
+ "--git-dir".to_string(),
+ bare.clone(),
+ "format-patch".to_string(),
+ "--stdout".to_string(),
+ "--full-index".to_string(),
+ "--binary".to_string(),
+ "-1".to_string(),
+ ];
+ if parent_id.is_none() {
+ format_patch_args.push("--root".to_string());
+ }
+ format_patch_args.push(cp_revision.clone());
+
+ let diff = duct::cmd("git", &format_patch_args)
+ .env("GIT_INDEX_FILE", &idx_path)
+ .stdout_capture()
+ .stderr_capture()
+ .unchecked()
+ .run()?;
+ let patch_data = command_ok(diff)?;
+
+ let apply = duct::cmd(
+ "git",
+ [
+ "--git-dir",
+ bare.as_str(),
+ "apply",
+ "--cached",
+ "--allow-empty",
+ "-",
+ ],
+ )
+ .env("GIT_INDEX_FILE", &idx_path)
+ .stdin_bytes(patch_data.as_bytes())
+ .stdout_capture()
+ .stderr_capture()
+ .unchecked()
+ .run()?;
+ if !apply.status.success() {
+ return Err(GitError::Internal(format!(
+ "cherry-pick apply failed: {}",
+ String::from_utf8_lossy(&apply.stderr)
+ )));
+ }
+
+ let write_tree = duct::cmd("git", ["--git-dir", bare.as_str(), "write-tree"])
+ .env("GIT_INDEX_FILE", &idx_path)
+ .stdout_capture()
+ .stderr_capture()
+ .unchecked()
+ .run()?;
+ let tree_id = command_ok(write_tree)?.trim().to_string();
+
+ let message = cp_obj.message_raw()?.to_string();
+
+ let parents = vec![branch_tip.clone()];
+ let commit_id = self.commit_tree(
+ &tree_id,
+ &parents,
+ &message,
+ request.committer.as_ref(),
+ request.committer.as_ref(),
+ )?;
+
+ self.update_branch_ref(&target_branch, &commit_id, Some(&branch_tip), false)?;
+
+ Ok(CreateCommitResponse {
+ commit: Some(self.get_commit(GetCommitRequest {
+ repository: request.repository,
+ revision: Some(crate::pb::ObjectSelector {
+ selector: Some(crate::pb::object_selector::Selector::Revision(
+ crate::pb::ObjectName {
+ revision: commit_id,
+ },
+ )),
+ }),
+ include_stats: false,
+ include_raw: false,
+ })?),
+ branch: target_branch,
+ })
+ }
+
+ pub(crate) fn update_branch_ref(
+ &self,
+ branch: &str,
+ commit_id: &str,
+ old_value: Option<&str>,
+ force: bool,
+ ) -> GitResult<()> {
+ let refname = format!("refs/heads/{}", branch);
+ let mut args = vec![
+ "--git-dir".to_string(),
+ self.bare_dir.to_string_lossy().into_owned(),
+ "update-ref".into(),
+ refname,
+ commit_id.to_string(),
+ ];
+ if !force {
+ args.push(old_value.unwrap_or(crate::oid::ZERO_OID).to_string());
+ }
+ let update = duct::cmd("git", &args)
+ .stdout_capture()
+ .stderr_capture()
+ .unchecked()
+ .run()?;
+ command_ok(update).map(|_| ())
+ }
+}
diff --git a/commit/compare_commits.rs b/commit/compare_commits.rs
new file mode 100644
index 0000000..ac213d8
--- /dev/null
+++ b/commit/compare_commits.rs
@@ -0,0 +1,94 @@
+use crate::bare::GitBare;
+use crate::diff::get_diff_stats::diff_stats_for_range;
+use crate::error::{GitError, GitResult};
+use crate::paginate;
+use crate::pb::{
+ CommitStats, CompareCommitsRequest, CompareCommitsResponse, GetCommitRequest, object_selector,
+};
+
+impl GitBare {
+ pub fn compare_commits(
+ &self,
+ request: CompareCommitsRequest,
+ ) -> GitResult {
+ let repo = self.gix_repo()?;
+ let base = match request.base.clone().and_then(|s| s.selector) {
+ Some(object_selector::Selector::Oid(oid)) => oid.hex,
+ Some(object_selector::Selector::Revision(name)) => name.revision,
+ None => "HEAD".into(),
+ };
+ let head = match request.head.clone().and_then(|s| s.selector) {
+ Some(object_selector::Selector::Oid(oid)) => oid.hex,
+ Some(object_selector::Selector::Revision(name)) => name.revision,
+ None => "HEAD".into(),
+ };
+
+ let base_id = repo.rev_parse_single(base.as_str())?;
+ let head_id = repo.rev_parse_single(head.as_str())?;
+ let merge_base = repo
+ .merge_base(base_id.detach(), head_id.detach())
+ .ok()
+ .map(|id| self.oid_to_pb(id.to_string()));
+
+ let range = if request.straight {
+ format!("{base}..{head}")
+ } else {
+ format!("{base}...{head}")
+ };
+ let mut args = vec![
+ "--git-dir".to_string(),
+ self.bare_dir.to_string_lossy().into_owned(),
+ "rev-list".into(),
+ ];
+ if request.first_parent {
+ args.push("--first-parent".into());
+ }
+ args.push(range);
+
+ let rev_list = duct::cmd("git", &args)
+ .stdout_capture()
+ .stderr_capture()
+ .unchecked()
+ .run()?;
+ if !rev_list.status.success() {
+ return Err(GitError::CommandFailed {
+ status_code: rev_list.status.code(),
+ stderr: String::from_utf8_lossy(&rev_list.stderr).into_owned(),
+ });
+ }
+
+ let ids = String::from_utf8_lossy(&rev_list.stdout)
+ .lines()
+ .map(str::trim)
+ .filter(|line| !line.is_empty())
+ .map(ToOwned::to_owned)
+ .collect::>();
+ let (page_ids, page_info) = paginate::paginate(&ids, request.pagination.as_ref());
+
+ let mut commits = Vec::with_capacity(page_ids.len());
+ for id in page_ids {
+ commits.push(self.get_commit(GetCommitRequest {
+ repository: request.repository.clone(),
+ revision: Some(crate::pb::ObjectSelector {
+ selector: Some(object_selector::Selector::Revision(crate::pb::ObjectName {
+ revision: id,
+ })),
+ }),
+ include_stats: false,
+ include_raw: false,
+ })?);
+ }
+
+ let diff_stats = diff_stats_for_range(self, &base, &head, None)?;
+ Ok(CompareCommitsResponse {
+ commits,
+ stats: Some(CommitStats {
+ additions: diff_stats.additions,
+ deletions: diff_stats.deletions,
+ changed_files: diff_stats.changed_files,
+ }),
+ page_info: Some(page_info),
+ merge_base,
+ })
+ }
+}
diff --git a/commit/create_commit.rs b/commit/create_commit.rs
new file mode 100644
index 0000000..7a544ea
--- /dev/null
+++ b/commit/create_commit.rs
@@ -0,0 +1,375 @@
+use crate::bare::GitBare;
+use crate::error::{GitError, GitResult};
+use crate::oid::ZERO_OID;
+use crate::pb::{
+ CreateCommitRequest, CreateCommitResponse, GetCommitRequest, ObjectName, ObjectSelector,
+ create_commit_action, object_selector,
+};
+
+impl GitBare {
+ pub fn create_commit(&self, request: CreateCommitRequest) -> GitResult {
+ let repo = self.gix_repo()?;
+ let start_rev = match request.start_revision.clone().and_then(|s| s.selector) {
+ Some(object_selector::Selector::Oid(oid)) => oid.hex,
+ Some(object_selector::Selector::Revision(name)) => name.revision,
+ None => request.branch.clone(),
+ };
+ let parent_id = repo
+ .rev_parse_single(start_rev.as_str())
+ .ok()
+ .map(|id| id.to_string());
+ let current_branch_tip = repo
+ .find_reference(format!("refs/heads/{}", request.branch).as_str())
+ .ok()
+ .and_then(|mut r| r.peel_to_id().ok())
+ .map(|id| id.to_string());
+
+ let tree_id = if request.actions.is_empty() {
+ let Some(parent) = parent_id.as_ref() else {
+ return Err(GitError::InvalidArgument(
+ "cannot create an empty root commit without file actions".into(),
+ ));
+ };
+ self.rev_parse_tree(parent)?
+ } else {
+ self.tree_from_actions(parent_id.as_deref(), &request.actions)?
+ };
+
+ let message = commit_message_with_trailers(&request);
+ let parents: Vec = parent_id.iter().cloned().collect();
+ let commit_id = self.commit_tree(
+ &tree_id,
+ &parents,
+ &message,
+ request.author.as_ref(),
+ request.committer.as_ref(),
+ )?;
+ self.update_branch_after_commit(&request, &commit_id, current_branch_tip.as_deref())?;
+
+ let revision = Some(ObjectSelector {
+ selector: Some(object_selector::Selector::Revision(ObjectName {
+ revision: commit_id,
+ })),
+ });
+ Ok(CreateCommitResponse {
+ commit: Some(self.get_commit(GetCommitRequest {
+ repository: request.repository,
+ revision,
+ include_stats: false,
+ include_raw: false,
+ })?),
+ branch: request.branch,
+ })
+ }
+
+ fn rev_parse_tree(&self, revision: &str) -> GitResult {
+ let result = duct::cmd(
+ "git",
+ [
+ "--git-dir",
+ self.bare_dir.to_string_lossy().as_ref(),
+ "rev-parse",
+ &format!("{revision}^{{tree}}"),
+ ],
+ )
+ .stdout_capture()
+ .stderr_capture()
+ .unchecked()
+ .run()?;
+ command_ok(result).map(|stdout| stdout.trim().to_string())
+ }
+
+ fn tree_from_actions(
+ &self,
+ parent_id: Option<&str>,
+ actions: &[crate::pb::CreateCommitAction],
+ ) -> GitResult {
+ let tmp_index = tempfile::Builder::new()
+ .prefix("gitks-index-")
+ .tempfile_in(&self.bare_dir)
+ .map_err(GitError::Io)?;
+ let tmp_index_path = tmp_index.path().to_string_lossy().into_owned();
+
+ if let Some(parent) = parent_id {
+ let read_tree = duct::cmd(
+ "git",
+ [
+ "--git-dir",
+ self.bare_dir.to_string_lossy().as_ref(),
+ "read-tree",
+ parent,
+ ],
+ )
+ .env("GIT_INDEX_FILE", &tmp_index_path)
+ .stdout_capture()
+ .stderr_capture()
+ .unchecked()
+ .run()?;
+ command_ok(read_tree)?;
+ }
+
+ for action in actions {
+ self.apply_commit_action(&tmp_index_path, action)?;
+ }
+
+ let write_tree = duct::cmd(
+ "git",
+ [
+ "--git-dir",
+ self.bare_dir.to_string_lossy().as_ref(),
+ "write-tree",
+ ],
+ )
+ .env("GIT_INDEX_FILE", &tmp_index_path)
+ .stdout_capture()
+ .stderr_capture()
+ .unchecked()
+ .run()?;
+ command_ok(write_tree).map(|stdout| stdout.trim().to_string())
+ }
+
+ fn apply_commit_action(
+ &self,
+ index_path: &str,
+ action: &crate::pb::CreateCommitAction,
+ ) -> GitResult<()> {
+ let action_type = create_commit_action::Action::try_from(action.action)
+ .unwrap_or(create_commit_action::Action::CreateCommitActionUnspecified);
+ match action_type {
+ create_commit_action::Action::CreateCommitActionCreate
+ | create_commit_action::Action::CreateCommitActionUpdate => {
+ let hash = duct::cmd(
+ "git",
+ [
+ "--git-dir",
+ self.bare_dir.to_string_lossy().as_ref(),
+ "hash-object",
+ "-w",
+ "--stdin",
+ ],
+ )
+ .stdin_bytes(action.content.clone())
+ .stdout_capture()
+ .stderr_capture()
+ .unchecked()
+ .run()?;
+ let blob = command_ok(hash)?.trim().to_string();
+ let mode = if action.executable {
+ "100755"
+ } else {
+ "100644"
+ };
+ self.update_index_cacheinfo(index_path, mode, &blob, &action.file_path)
+ }
+ create_commit_action::Action::CreateCommitActionDelete => {
+ let result = duct::cmd(
+ "git",
+ [
+ "--git-dir",
+ self.bare_dir.to_string_lossy().as_ref(),
+ "update-index",
+ "--force-remove",
+ &action.file_path,
+ ],
+ )
+ .env("GIT_INDEX_FILE", index_path)
+ .env("GIT_WORK_TREE", self.bare_dir.to_string_lossy().as_ref())
+ .stdout_capture()
+ .stderr_capture()
+ .unchecked()
+ .run()?;
+ command_ok(result).map(|_| ())
+ }
+ create_commit_action::Action::CreateCommitActionMove => {
+ if action.previous_path.is_empty() {
+ return Err(GitError::InvalidArgument(
+ "MOVE action requires previous_path".into(),
+ ));
+ }
+ let (mode, oid) = self.index_entry(index_path, &action.previous_path)?;
+ self.update_index_cacheinfo(index_path, &mode, &oid, &action.file_path)?;
+ let remove = duct::cmd(
+ "git",
+ [
+ "--git-dir",
+ self.bare_dir.to_string_lossy().as_ref(),
+ "update-index",
+ "--force-remove",
+ &action.previous_path,
+ ],
+ )
+ .env("GIT_INDEX_FILE", index_path)
+ .env("GIT_WORK_TREE", self.bare_dir.to_string_lossy().as_ref())
+ .stdout_capture()
+ .stderr_capture()
+ .unchecked()
+ .run()?;
+ command_ok(remove).map(|_| ())
+ }
+ create_commit_action::Action::CreateCommitActionChmod => {
+ let (_old_mode, oid) = self.index_entry(index_path, &action.file_path)?;
+ let mode = if action.executable {
+ "100755"
+ } else {
+ "100644"
+ };
+ self.update_index_cacheinfo(index_path, mode, &oid, &action.file_path)
+ }
+ create_commit_action::Action::CreateCommitActionUnspecified => Err(
+ GitError::InvalidArgument("unspecified commit action".into()),
+ ),
+ }
+ }
+
+ fn index_entry(&self, index_path: &str, path: &str) -> GitResult<(String, String)> {
+ let result = duct::cmd(
+ "git",
+ [
+ "--git-dir",
+ self.bare_dir.to_string_lossy().as_ref(),
+ "ls-files",
+ "-s",
+ "--",
+ path,
+ ],
+ )
+ .env("GIT_INDEX_FILE", index_path)
+ .env("GIT_WORK_TREE", self.bare_dir.to_string_lossy().as_ref())
+ .stdout_capture()
+ .stderr_capture()
+ .unchecked()
+ .run()?;
+ let stdout = command_ok(result)?;
+ let line = stdout
+ .lines()
+ .next()
+ .ok_or_else(|| GitError::NotFound(path.to_string()))?;
+ let parts = line.split_whitespace().collect::>();
+ if parts.len() < 2 {
+ return Err(GitError::ParseError(format!(
+ "invalid index entry for {path}: {line}"
+ )));
+ }
+ Ok((parts[0].to_string(), parts[1].to_string()))
+ }
+
+ fn update_index_cacheinfo(
+ &self,
+ index_path: &str,
+ mode: &str,
+ oid: &str,
+ path: &str,
+ ) -> GitResult<()> {
+ let result = duct::cmd(
+ "git",
+ [
+ "--git-dir",
+ self.bare_dir.to_string_lossy().as_ref(),
+ "update-index",
+ "--add",
+ "--cacheinfo",
+ mode,
+ oid,
+ path,
+ ],
+ )
+ .env("GIT_INDEX_FILE", index_path)
+ .env("GIT_WORK_TREE", self.bare_dir.to_string_lossy().as_ref())
+ .stdout_capture()
+ .stderr_capture()
+ .unchecked()
+ .run()?;
+ command_ok(result).map(|_| ())
+ }
+
+ pub(crate) fn commit_tree(
+ &self,
+ tree_id: &str,
+ parent_ids: &[String],
+ message: &str,
+ author: Option<&crate::pb::Signature>,
+ committer: Option<&crate::pb::Signature>,
+ ) -> GitResult {
+ let mut args = vec![
+ "--git-dir".to_string(),
+ self.bare_dir.to_string_lossy().into_owned(),
+ "commit-tree".into(),
+ tree_id.to_string(),
+ ];
+ for parent in parent_ids {
+ args.push("-p".into());
+ args.push(parent.clone());
+ }
+ args.push("-m".into());
+ args.push(message.to_string());
+
+ let mut cmd = duct::cmd("git", &args).stdout_capture().stderr_capture();
+ if let Some(author) = author.and_then(|s| s.identity.as_ref()) {
+ cmd = cmd
+ .env("GIT_AUTHOR_NAME", &author.name)
+ .env("GIT_AUTHOR_EMAIL", &author.email);
+ }
+ if let Some(committer) = committer.and_then(|s| s.identity.as_ref()) {
+ cmd = cmd
+ .env("GIT_COMMITTER_NAME", &committer.name)
+ .env("GIT_COMMITTER_EMAIL", &committer.email);
+ }
+
+ let commit = cmd.unchecked().run()?;
+ command_ok(commit).map(|stdout| stdout.trim().to_string())
+ }
+
+ fn update_branch_after_commit(
+ &self,
+ request: &CreateCommitRequest,
+ commit_id: &str,
+ current_branch_tip: Option<&str>,
+ ) -> GitResult<()> {
+ let refname = format!("refs/heads/{}", request.branch);
+ let mut args = vec![
+ "--git-dir".to_string(),
+ self.bare_dir.to_string_lossy().into_owned(),
+ "update-ref".into(),
+ refname,
+ commit_id.to_string(),
+ ];
+ if !request.force {
+ args.push(current_branch_tip.unwrap_or(ZERO_OID).to_string());
+ }
+
+ let update = duct::cmd("git", &args)
+ .stdout_capture()
+ .stderr_capture()
+ .unchecked()
+ .run()?;
+ command_ok(update).map(|_| ())
+ }
+}
+
+fn commit_message_with_trailers(request: &CreateCommitRequest) -> String {
+ if request.trailers.is_empty() {
+ return request.message.clone();
+ }
+
+ let mut message = request.message.trim_end().to_string();
+ message.push_str("\n\n");
+ for trailer in &request.trailers {
+ let separator = if trailer.separator_present { ": " } else { " " };
+ message.push_str(&trailer.key);
+ message.push_str(separator);
+ message.push_str(&trailer.value);
+ message.push('\n');
+ }
+ message
+}
+
+pub(crate) fn command_ok(output: std::process::Output) -> GitResult {
+ if output.status.success() {
+ Ok(String::from_utf8_lossy(&output.stdout).into_owned())
+ } else {
+ Err(GitError::CommandFailed {
+ status_code: output.status.code(),
+ stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
+ })
+ }
+}
diff --git a/commit/get_commit.rs b/commit/get_commit.rs
new file mode 100644
index 0000000..02d1853
--- /dev/null
+++ b/commit/get_commit.rs
@@ -0,0 +1,76 @@
+use crate::bare::GitBare;
+use crate::error::{GitError, GitResult};
+use crate::pb::{Commit, GetCommitRequest, object_selector};
+
+impl GitBare {
+ pub fn get_commit(&self, request: GetCommitRequest) -> GitResult {
+ let repo = self.gix_repo()?;
+ let revision = match request.revision.and_then(|s| s.selector) {
+ Some(object_selector::Selector::Oid(oid)) => oid.hex,
+ Some(object_selector::Selector::Revision(name)) => name.revision,
+ None => "HEAD".into(),
+ };
+ let id = repo.rev_parse_single(revision.as_str())?;
+ let commit = id
+ .object()?
+ .try_into_commit()
+ .map_err(|e| GitError::Gix(e.to_string()))?;
+ let hex = commit.id.to_string();
+ let tree_hex = commit.tree_id()?.to_string();
+ let message = commit.message_raw()?.to_string();
+ let (subject, body) = message
+ .split_once('\n')
+ .map(|(s, b)| (s.to_string(), b.trim_start_matches('\n').to_string()))
+ .unwrap_or_else(|| (message.clone(), String::new()));
+ let author_sig = commit.author().ok();
+ let committer_sig = commit.committer().ok();
+ Ok(Commit {
+ oid: Some(self.oid_to_pb(hex.clone())),
+ abbreviated_oid: commit
+ .short_id()
+ .map(|s| s.to_string())
+ .unwrap_or_else(|_| hex.chars().take(7).collect()),
+ parent_oids: commit
+ .parent_ids()
+ .map(|p| self.oid_to_pb(p.to_string()))
+ .collect(),
+ tree_oid: Some(self.oid_to_pb(tree_hex)),
+ author: author_sig.as_ref().map(gix_sig_to_pb),
+ committer: committer_sig.as_ref().map(gix_sig_to_pb),
+ subject,
+ body,
+ message,
+ trailers: Vec::new(),
+ signature: None,
+ stats: None,
+ authored_at: author_sig.as_ref().map(|s| prost_types::Timestamp {
+ seconds: s.seconds(),
+ nanos: 0,
+ }),
+ committed_at: committer_sig.as_ref().map(|s| prost_types::Timestamp {
+ seconds: s.seconds(),
+ nanos: 0,
+ }),
+ raw: if request.include_raw {
+ commit.data.clone()
+ } else {
+ Vec::new()
+ },
+ })
+ }
+}
+
+pub(crate) fn gix_sig_to_pb(sig: &gix::actor::SignatureRef<'_>) -> crate::pb::Signature {
+ let time = sig.time().ok();
+ crate::pb::Signature {
+ identity: Some(crate::pb::Identity {
+ name: sig.name.to_string(),
+ email: sig.email.to_string(),
+ }),
+ when: Some(prost_types::Timestamp {
+ seconds: sig.seconds(),
+ nanos: 0,
+ }),
+ timezone_offset: time.map(|t| t.offset / 60).unwrap_or(0),
+ }
+}
diff --git a/commit/get_commit_ancestors.rs b/commit/get_commit_ancestors.rs
new file mode 100644
index 0000000..d6cc1c1
--- /dev/null
+++ b/commit/get_commit_ancestors.rs
@@ -0,0 +1,28 @@
+use crate::bare::GitBare;
+use crate::error::GitResult;
+use crate::pb::{GetCommitAncestorsRequest, GetCommitAncestorsResponse, ListCommitsRequest};
+
+impl GitBare {
+ pub fn get_commit_ancestors(
+ &self,
+ request: GetCommitAncestorsRequest,
+ ) -> GitResult {
+ let response = self.list_commits(ListCommitsRequest {
+ repository: request.repository,
+ revision: request.revision,
+ path: String::new(),
+ since: None,
+ until: None,
+ first_parent: request.first_parent,
+ all: false,
+ reverse: false,
+ max_parents: 0,
+ min_parents: 0,
+ pagination: request.pagination,
+ })?;
+ Ok(GetCommitAncestorsResponse {
+ commits: response.commits,
+ page_info: response.page_info,
+ })
+ }
+}
diff --git a/commit/list_commits.rs b/commit/list_commits.rs
new file mode 100644
index 0000000..90b18ab
--- /dev/null
+++ b/commit/list_commits.rs
@@ -0,0 +1,86 @@
+use crate::bare::GitBare;
+use crate::error::{GitError, GitResult};
+use crate::paginate;
+use crate::pb::{GetCommitRequest, ListCommitsRequest, ListCommitsResponse, object_selector};
+
+impl GitBare {
+ pub fn list_commits(&self, request: ListCommitsRequest) -> GitResult {
+ let revision = match request.revision.clone().and_then(|s| s.selector) {
+ Some(object_selector::Selector::Oid(oid)) => oid.hex,
+ Some(object_selector::Selector::Revision(name)) => name.revision,
+ None => "HEAD".into(),
+ };
+
+ let mut args = vec![
+ "--git-dir".to_string(),
+ self.bare_dir.to_string_lossy().into_owned(),
+ "rev-list".into(),
+ ];
+ if request.first_parent {
+ args.push("--first-parent".into());
+ }
+ if request.reverse {
+ args.push("--reverse".into());
+ }
+ if request.max_parents > 0 {
+ args.push(format!("--max-parents={}", request.max_parents));
+ }
+ if request.min_parents > 0 {
+ args.push(format!("--min-parents={}", request.min_parents));
+ }
+ if let Some(since) = request.since.as_ref() {
+ args.push(format!("--since=@{}", since.seconds));
+ }
+ if let Some(until) = request.until.as_ref() {
+ args.push(format!("--until=@{}", until.seconds));
+ }
+ if request.all {
+ args.push("--all".into());
+ } else {
+ args.push(revision);
+ }
+ if !request.path.is_empty() {
+ args.push("--".into());
+ args.push(request.path.clone());
+ }
+
+ let result = duct::cmd("git", &args)
+ .stdout_capture()
+ .stderr_capture()
+ .unchecked()
+ .run()?;
+ if !result.status.success() {
+ return Err(GitError::CommandFailed {
+ status_code: result.status.code(),
+ stderr: String::from_utf8_lossy(&result.stderr).into_owned(),
+ });
+ }
+
+ let ids = String::from_utf8_lossy(&result.stdout)
+ .lines()
+ .map(str::trim)
+ .filter(|line| !line.is_empty())
+ .map(ToOwned::to_owned)
+ .collect::>();
+ let (page_ids, page_info) = paginate::paginate(&ids, request.pagination.as_ref());
+
+ let mut commits = Vec::with_capacity(page_ids.len());
+ for id in page_ids {
+ commits.push(self.get_commit(GetCommitRequest {
+ repository: request.repository.clone(),
+ revision: Some(crate::pb::ObjectSelector {
+ selector: Some(object_selector::Selector::Revision(crate::pb::ObjectName {
+ revision: id,
+ })),
+ }),
+ include_stats: false,
+ include_raw: false,
+ })?);
+ }
+
+ Ok(ListCommitsResponse {
+ commits,
+ page_info: Some(page_info),
+ })
+ }
+}
diff --git a/commit/mod.rs b/commit/mod.rs
new file mode 100644
index 0000000..67c33bf
--- /dev/null
+++ b/commit/mod.rs
@@ -0,0 +1,7 @@
+pub mod cherry_pick_commit;
+pub mod compare_commits;
+pub mod create_commit;
+pub mod get_commit;
+pub mod get_commit_ancestors;
+pub mod list_commits;
+pub mod revert_commit;
diff --git a/commit/revert_commit.rs b/commit/revert_commit.rs
new file mode 100644
index 0000000..0a20d84
--- /dev/null
+++ b/commit/revert_commit.rs
@@ -0,0 +1,146 @@
+use crate::bare::GitBare;
+use crate::commit::create_commit::command_ok;
+use crate::error::{GitError, GitResult};
+use crate::pb::{CreateCommitResponse, GetCommitRequest, RevertCommitRequest};
+
+impl GitBare {
+ pub fn revert_commit(&self, request: RevertCommitRequest) -> GitResult {
+ let target_branch = request.branch.clone();
+ let revert_revision = match request.commit.and_then(|s| s.selector) {
+ Some(crate::pb::object_selector::Selector::Oid(oid)) => oid.hex,
+ Some(crate::pb::object_selector::Selector::Revision(name)) => name.revision,
+ None => return Err(GitError::InvalidArgument("commit is required".into())),
+ };
+
+ let repo = self.gix_repo()?;
+
+ let branch_ref = format!("refs/heads/{}", target_branch);
+ let branch_tip = repo
+ .find_reference(branch_ref.as_str())
+ .ok()
+ .and_then(|mut r| r.peel_to_id().ok())
+ .map(|id| id.to_string())
+ .ok_or_else(|| GitError::RefNotFound(target_branch.clone()))?;
+
+ let revert_id = repo.rev_parse_single(revert_revision.as_str())?;
+ let revert_obj = revert_id
+ .object()?
+ .try_into_commit()
+ .map_err(|e| GitError::Gix(e.to_string()))?;
+
+ let parent_ids: Vec = revert_obj.parent_ids().map(|p| p.to_string()).collect();
+ if parent_ids.len() > 1 {
+ return Err(GitError::InvalidArgument(
+ "reverting merge commits is not supported without mainline".into(),
+ ));
+ }
+ let parent_hex = parent_ids
+ .first()
+ .ok_or_else(|| GitError::InvalidArgument("cannot revert root commit".into()))?;
+
+ let tmp_index = tempfile::Builder::new()
+ .prefix("gitks-revert-")
+ .tempfile_in(&self.bare_dir)?;
+ let idx_path = tmp_index.path().to_string_lossy().into_owned();
+ let bare = self.bare_dir.to_string_lossy().into_owned();
+
+ let read_tree = duct::cmd(
+ "git",
+ ["--git-dir", bare.as_str(), "read-tree", branch_tip.as_str()],
+ )
+ .env("GIT_INDEX_FILE", &idx_path)
+ .stdout_capture()
+ .stderr_capture()
+ .unchecked()
+ .run()?;
+ command_ok(read_tree)?;
+
+ let diff = duct::cmd(
+ "git",
+ [
+ "--git-dir",
+ bare.as_str(),
+ "diff",
+ "--binary",
+ "--full-index",
+ revert_revision.as_str(),
+ parent_hex.as_str(),
+ ],
+ )
+ .stdout_capture()
+ .stderr_capture()
+ .unchecked()
+ .run()?;
+ let patch_data = command_ok(diff)?;
+
+ let apply = duct::cmd(
+ "git",
+ [
+ "--git-dir",
+ bare.as_str(),
+ "apply",
+ "--cached",
+ "--allow-empty",
+ "-",
+ ],
+ )
+ .env("GIT_INDEX_FILE", &idx_path)
+ .stdin_bytes(patch_data.as_bytes())
+ .stdout_capture()
+ .stderr_capture()
+ .unchecked()
+ .run()?;
+ if !apply.status.success() {
+ return Err(GitError::Internal(format!(
+ "revert apply failed: {}",
+ String::from_utf8_lossy(&apply.stderr)
+ )));
+ }
+
+ let write_tree = duct::cmd("git", ["--git-dir", bare.as_str(), "write-tree"])
+ .env("GIT_INDEX_FILE", &idx_path)
+ .stdout_capture()
+ .stderr_capture()
+ .unchecked()
+ .run()?;
+ let tree_id = command_ok(write_tree)?.trim().to_string();
+
+ let subject = revert_obj
+ .message_raw()?
+ .to_string()
+ .lines()
+ .next()
+ .unwrap_or_default()
+ .to_string();
+ let message = format!(
+ "Revert \"{}\"\n\nThis reverts commit {}.",
+ subject, revert_revision
+ );
+
+ let commit_id = self.commit_tree(
+ &tree_id,
+ std::slice::from_ref(&branch_tip),
+ &message,
+ request.committer.as_ref(),
+ request.committer.as_ref(),
+ )?;
+
+ self.update_branch_ref(&target_branch, &commit_id, Some(&branch_tip), false)?;
+
+ Ok(CreateCommitResponse {
+ commit: Some(self.get_commit(GetCommitRequest {
+ repository: request.repository,
+ revision: Some(crate::pb::ObjectSelector {
+ selector: Some(crate::pb::object_selector::Selector::Revision(
+ crate::pb::ObjectName {
+ revision: commit_id,
+ },
+ )),
+ }),
+ include_stats: false,
+ include_raw: false,
+ })?),
+ branch: target_branch,
+ })
+ }
+}
diff --git a/commit/types.rs b/commit/types.rs
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/commit/types.rs
@@ -0,0 +1 @@
+
diff --git a/diff/get_commit_diff.rs b/diff/get_commit_diff.rs
new file mode 100644
index 0000000..88841f3
--- /dev/null
+++ b/diff/get_commit_diff.rs
@@ -0,0 +1,86 @@
+use crate::bare::GitBare;
+use crate::error::{GitError, GitResult};
+use crate::pb::{GetCommitDiffRequest, GetDiffRequest, GetDiffResponse};
+
+impl GitBare {
+ pub fn get_commit_diff(&self, request: GetCommitDiffRequest) -> GitResult {
+ let commit = match request.commit.and_then(|s| s.selector) {
+ Some(crate::pb::object_selector::Selector::Oid(oid)) => oid.hex,
+ Some(crate::pb::object_selector::Selector::Revision(name)) => name.revision,
+ None => "HEAD".into(),
+ };
+ let base = self.first_parent_or_empty_tree(&commit)?;
+ self.get_diff(GetDiffRequest {
+ repository: request.repository,
+ base: Some(crate::pb::ObjectSelector {
+ selector: Some(crate::pb::object_selector::Selector::Revision(
+ crate::pb::ObjectName { revision: base },
+ )),
+ }),
+ head: Some(crate::pb::ObjectSelector {
+ selector: Some(crate::pb::object_selector::Selector::Revision(
+ crate::pb::ObjectName { revision: commit },
+ )),
+ }),
+ options: request.options,
+ pagination: request.pagination,
+ })
+ }
+
+ fn first_parent_or_empty_tree(&self, commit: &str) -> GitResult {
+ let result = duct::cmd(
+ "git",
+ [
+ "--git-dir",
+ self.bare_dir.to_string_lossy().as_ref(),
+ "rev-list",
+ "--parents",
+ "-n",
+ "1",
+ commit,
+ ],
+ )
+ .stdout_capture()
+ .stderr_capture()
+ .unchecked()
+ .run()?;
+ if !result.status.success() {
+ return Err(GitError::CommandFailed {
+ status_code: result.status.code(),
+ stderr: String::from_utf8_lossy(&result.stderr).into_owned(),
+ });
+ }
+ let output = String::from_utf8_lossy(&result.stdout);
+ let parts = output.split_whitespace().collect::>();
+ if let Some(parent) = parts.get(1) {
+ return Ok((*parent).to_string());
+ }
+
+ let empty_tree = duct::cmd(
+ "git",
+ [
+ "--git-dir",
+ self.bare_dir.to_string_lossy().as_ref(),
+ "hash-object",
+ "-t",
+ "tree",
+ "-w",
+ "--stdin",
+ ],
+ )
+ .stdin_bytes(Vec::::new())
+ .stdout_capture()
+ .stderr_capture()
+ .unchecked()
+ .run()?;
+ if !empty_tree.status.success() {
+ return Err(GitError::CommandFailed {
+ status_code: empty_tree.status.code(),
+ stderr: String::from_utf8_lossy(&empty_tree.stderr).into_owned(),
+ });
+ }
+ Ok(String::from_utf8_lossy(&empty_tree.stdout)
+ .trim()
+ .to_string())
+ }
+}
diff --git a/diff/get_diff.rs b/diff/get_diff.rs
new file mode 100644
index 0000000..47843c9
--- /dev/null
+++ b/diff/get_diff.rs
@@ -0,0 +1,330 @@
+use crate::bare::GitBare;
+use crate::diff::get_diff_stats::{diff_stats_for_range, push_diff_options};
+use crate::error::{GitError, GitResult};
+use crate::paginate;
+use crate::pb::diff_file::ChangeType;
+use crate::pb::{DiffFile, GetDiffRequest, GetDiffResponse};
+
+#[derive(Debug, Clone)]
+struct NameStatusEntry {
+ status: char,
+ old_path: String,
+ new_path: String,
+ similarity: f64,
+}
+
+#[derive(Debug, Clone, Default)]
+struct TreeMeta {
+ oid_hex: String,
+ mode: u32,
+}
+
+impl GitBare {
+ pub fn get_diff(&self, request: GetDiffRequest) -> GitResult {
+ let base = match request.base.and_then(|s| s.selector) {
+ Some(crate::pb::object_selector::Selector::Oid(oid)) => oid.hex,
+ Some(crate::pb::object_selector::Selector::Revision(name)) => name.revision,
+ None => "HEAD".into(),
+ };
+ let head = match request.head.and_then(|s| s.selector) {
+ Some(crate::pb::object_selector::Selector::Oid(oid)) => oid.hex,
+ Some(crate::pb::object_selector::Selector::Revision(name)) => name.revision,
+ None => "HEAD".into(),
+ };
+
+ let options = request.options.as_ref();
+ let entries = self.diff_name_status(&base, &head, options)?;
+ let max_files = options.and_then(|o| (o.max_files > 0).then_some(o.max_files as usize));
+ let overflow = max_files.is_some_and(|max| entries.len() > max);
+ let entries_to_build =
+ max_files.map_or(entries.as_slice(), |max| &entries[..entries.len().min(max)]);
+
+ let mut files = Vec::with_capacity(entries_to_build.len());
+ for entry in entries_to_build {
+ let old_meta = if !entry.old_path.is_empty() {
+ self.tree_meta(&base, &entry.old_path).ok().flatten()
+ } else {
+ None
+ };
+ let new_meta = if !entry.new_path.is_empty() {
+ self.tree_meta(&head, &entry.new_path).ok().flatten()
+ } else {
+ None
+ };
+ let (additions, deletions, binary) = self.path_numstat(&base, &head, entry)?;
+ let (patch, too_large) = self.path_patch(&base, &head, entry, options)?;
+
+ files.push(DiffFile {
+ old_path: entry.old_path.clone(),
+ new_path: entry.new_path.clone(),
+ old_oid: old_meta.as_ref().map(|m| self.oid_to_pb(&m.oid_hex)),
+ new_oid: new_meta.as_ref().map(|m| self.oid_to_pb(&m.oid_hex)),
+ old_mode: old_meta.as_ref().map(|m| m.mode).unwrap_or(0),
+ new_mode: new_meta.as_ref().map(|m| m.mode).unwrap_or(0),
+ change_type: change_type(entry.status) as i32,
+ binary,
+ too_large,
+ additions,
+ deletions,
+ hunks: Vec::new(),
+ patch,
+ similarity: entry.similarity,
+ });
+ }
+
+ let stats = diff_stats_for_range(self, &base, &head, options)?;
+ let (files, page_info) = paginate::paginate(&files, request.pagination.as_ref());
+
+ Ok(GetDiffResponse {
+ files,
+ stats: Some(stats),
+ page_info: Some(page_info),
+ overflow,
+ })
+ }
+
+ fn diff_name_status(
+ &self,
+ base: &str,
+ head: &str,
+ options: Option<&crate::pb::DiffOptions>,
+ ) -> GitResult> {
+ let mut args = vec![
+ "--git-dir".to_string(),
+ self.bare_dir.to_string_lossy().into_owned(),
+ "diff".into(),
+ "--name-status".into(),
+ "-z".into(),
+ ];
+ push_diff_options(&mut args, options);
+ args.push(base.to_string());
+ args.push(head.to_string());
+ if let Some(options) = options
+ && !options.pathspec.is_empty()
+ {
+ args.push("--".into());
+ args.extend(options.pathspec.iter().cloned());
+ }
+
+ let result = duct::cmd("git", &args)
+ .stdout_capture()
+ .stderr_capture()
+ .unchecked()
+ .run()?;
+ if !result.status.success() {
+ return Err(GitError::CommandFailed {
+ status_code: result.status.code(),
+ stderr: String::from_utf8_lossy(&result.stderr).into_owned(),
+ });
+ }
+
+ let parts = result
+ .stdout
+ .split(|b| *b == 0)
+ .filter(|part| !part.is_empty())
+ .map(|part| String::from_utf8_lossy(part).into_owned())
+ .collect::>();
+
+ let mut entries = Vec::new();
+ let mut idx = 0;
+ while idx < parts.len() {
+ let status_token = &parts[idx];
+ idx += 1;
+ let status = status_token.chars().next().unwrap_or('M');
+ let similarity = status_token
+ .get(1..)
+ .and_then(|s| s.parse::().ok())
+ .unwrap_or(0.0);
+
+ if matches!(status, 'R' | 'C') {
+ if idx + 1 >= parts.len() {
+ break;
+ }
+ let old_path = parts[idx].clone();
+ let new_path = parts[idx + 1].clone();
+ idx += 2;
+ entries.push(NameStatusEntry {
+ status,
+ old_path,
+ new_path,
+ similarity,
+ });
+ } else {
+ if idx >= parts.len() {
+ break;
+ }
+ let path = parts[idx].clone();
+ idx += 1;
+ let (old_path, new_path) = match status {
+ 'A' => (String::new(), path),
+ 'D' => (path, String::new()),
+ _ => (path.clone(), path),
+ };
+ entries.push(NameStatusEntry {
+ status,
+ old_path,
+ new_path,
+ similarity,
+ });
+ }
+ }
+
+ Ok(entries)
+ }
+
+ fn tree_meta(&self, revision: &str, path: &str) -> GitResult