gitscope
← rgit / Diff · b133174 → b133174
Changes b133174..b133174
Patch commit
diff --git a/Cargo.lock b/Cargo.lock
index 5e9beef..25f3fb0 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -17,6 +17,19 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
+[[package]]
+name = "ahash"
+version = "0.8.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
+dependencies = [
+ "cfg-if",
+ "getrandom",
+ "once_cell",
+ "version_check",
+ "zerocopy",
+]
+
[[package]]
name = "aho-corasick"
version = "1.1.3"
@@ -26,6 +39,12 @@ dependencies = [
"memchr",
]
+[[package]]
+name = "allocator-api2"
+version = "0.2.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
+
[[package]]
name = "ansi_colours"
version = "1.2.2"
@@ -96,6 +115,12 @@ version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
+[[package]]
+name = "arrayvec"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
+
[[package]]
name = "askama"
version = "0.12.1"
@@ -435,6 +460,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706"
dependencies = [
"memchr",
+ "regex-automata 0.4.6",
"serde",
]
@@ -569,6 +595,12 @@ dependencies = [
"winapi",
]
+[[package]]
+name = "clru"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cbd0f76e066e64fdc5631e3bb46381254deab9ef1158292f27c8c57e3bf3fe59"
+
[[package]]
name = "colorchoice"
version = "1.0.1"
@@ -777,6 +809,20 @@ dependencies = [
"syn 1.0.109",
]
+[[package]]
+name = "dashmap"
+version = "6.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf"
+dependencies = [
+ "cfg-if",
+ "crossbeam-utils",
+ "hashbrown 0.14.5",
+ "lock_api",
+ "once_cell",
+ "parking_lot_core",
+]
+
[[package]]
name = "deranged"
version = "0.3.11"
@@ -843,6 +889,12 @@ dependencies = [
"const-random",
]
+[[package]]
+name = "dunce"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
+
[[package]]
name = "either"
version = "1.11.0"
@@ -928,6 +980,12 @@ dependencies = [
"regex",
]
+[[package]]
+name = "faster-hex"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2a2b11eda1d40935b26cf18f6833c526845ae8c41e58d09af6adeb6f0269183"
+
[[package]]
name = "fastrand"
version = "2.1.0"
@@ -987,119 +1045,907 @@ dependencies = [
]
[[package]]
-name = "futures-channel"
-version = "0.3.30"
+name = "futures-channel"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
+
+[[package]]
+name = "futures-macro"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.63",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
+
+[[package]]
+name = "futures-task"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
+
+[[package]]
+name = "futures-util"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "pin-utils",
+ "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.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "gimli"
+version = "0.28.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
+
+[[package]]
+name = "gix"
+version = "0.66.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9048b8d1ae2104f045cb37e5c450fc49d5d8af22609386bfc739c11ba88995eb"
+dependencies = [
+ "gix-actor",
+ "gix-archive",
+ "gix-attributes",
+ "gix-command",
+ "gix-commitgraph",
+ "gix-config",
+ "gix-credentials",
+ "gix-date",
+ "gix-diff",
+ "gix-dir",
+ "gix-discover",
+ "gix-features",
+ "gix-filter",
+ "gix-fs",
+ "gix-glob",
+ "gix-hash",
+ "gix-hashtable",
+ "gix-ignore",
+ "gix-index",
+ "gix-lock",
+ "gix-mailmap",
+ "gix-negotiate",
+ "gix-object",
+ "gix-odb",
+ "gix-pack",
+ "gix-path",
+ "gix-pathspec",
+ "gix-prompt",
+ "gix-ref",
+ "gix-refspec",
+ "gix-revision",
+ "gix-revwalk",
+ "gix-sec",
+ "gix-status",
+ "gix-submodule",
+ "gix-tempfile",
+ "gix-trace",
+ "gix-traverse",
+ "gix-url",
+ "gix-utils",
+ "gix-validate",
+ "gix-worktree",
+ "gix-worktree-state",
+ "gix-worktree-stream",
+ "once_cell",
+ "parking_lot",
+ "regex",
+ "signal-hook",
+ "smallvec",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-actor"
+version = "0.32.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc19e312cd45c4a66cd003f909163dc2f8e1623e30a0c0c6df3776e89b308665"
+dependencies = [
+ "bstr",
+ "gix-date",
+ "gix-utils",
+ "itoa",
+ "thiserror",
+ "winnow",
+]
+
+[[package]]
+name = "gix-archive"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9147c08a55c1398b755539e2cdd63ff690ffe4a2e5e5e0780ee6ef2b49b0a60a"
+dependencies = [
+ "bstr",
+ "gix-date",
+ "gix-object",
+ "gix-worktree-stream",
+ "jiff",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-attributes"
+version = "0.22.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebccbf25aa4a973dd352564a9000af69edca90623e8a16dad9cbc03713131311"
+dependencies = [
+ "bstr",
+ "gix-glob",
+ "gix-path",
+ "gix-quote",
+ "gix-trace",
+ "kstring",
+ "smallvec",
+ "thiserror",
+ "unicode-bom",
+]
+
+[[package]]
+name = "gix-bitmap"
+version = "0.2.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a371db66cbd4e13f0ed9dc4c0fea712d7276805fccc877f77e96374d317e87ae"
+dependencies = [
+ "thiserror",
+]
+
+[[package]]
+name = "gix-chunk"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "45c8751169961ba7640b513c3b24af61aa962c967aaf04116734975cd5af0c52"
+dependencies = [
+ "thiserror",
+]
+
+[[package]]
+name = "gix-command"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dff2e692b36bbcf09286c70803006ca3fd56551a311de450be317a0ab8ea92e7"
+dependencies = [
+ "bstr",
+ "gix-path",
+ "gix-trace",
+ "shell-words",
+]
+
+[[package]]
+name = "gix-commitgraph"
+version = "0.24.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "133b06f67f565836ec0c473e2116a60fb74f80b6435e21d88013ac0e3c60fc78"
+dependencies = [
+ "bstr",
+ "gix-chunk",
+ "gix-features",
+ "gix-hash",
+ "memmap2",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-config"
+version = "0.40.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78e797487e6ca3552491de1131b4f72202f282fb33f198b1c34406d765b42bb0"
+dependencies = [
+ "bstr",
+ "gix-config-value",
+ "gix-features",
+ "gix-glob",
+ "gix-path",
+ "gix-ref",
+ "gix-sec",
+ "memchr",
+ "once_cell",
+ "smallvec",
+ "thiserror",
+ "unicode-bom",
+ "winnow",
+]
+
+[[package]]
+name = "gix-config-value"
+version = "0.14.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03f76169faa0dec598eac60f83d7fcdd739ec16596eca8fb144c88973dbe6f8c"
+dependencies = [
+ "bitflags 2.5.0",
+ "bstr",
+ "gix-path",
+ "libc",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-credentials"
+version = "0.24.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ce391d305968782f1ae301c4a3d42c5701df7ff1d8bc03740300f6fd12bce78"
+dependencies = [
+ "bstr",
+ "gix-command",
+ "gix-config-value",
+ "gix-path",
+ "gix-prompt",
+ "gix-sec",
+ "gix-trace",
+ "gix-url",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-date"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35c84b7af01e68daf7a6bb8bb909c1ff5edb3ce4326f1f43063a5a96d3c3c8a5"
+dependencies = [
+ "bstr",
+ "itoa",
+ "jiff",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-diff"
+version = "0.46.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92c9afd80fff00f8b38b1c1928442feb4cd6d2232a6ed806b6b193151a3d336c"
+dependencies = [
+ "bstr",
+ "gix-command",
+ "gix-filter",
+ "gix-fs",
+ "gix-hash",
+ "gix-object",
+ "gix-path",
+ "gix-tempfile",
+ "gix-trace",
+ "gix-worktree",
+ "imara-diff",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-dir"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ed3a9076661359a1c5a27c12ad6c3ebe2dd96b8b3c0af6488ab7c128b7bdd98"
+dependencies = [
+ "bstr",
+ "gix-discover",
+ "gix-fs",
+ "gix-ignore",
+ "gix-index",
+ "gix-object",
+ "gix-path",
+ "gix-pathspec",
+ "gix-trace",
+ "gix-utils",
+ "gix-worktree",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-discover"
+version = "0.35.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0577366b9567376bc26e815fd74451ebd0e6218814e242f8e5b7072c58d956d2"
+dependencies = [
+ "bstr",
+ "dunce",
+ "gix-fs",
+ "gix-hash",
+ "gix-path",
+ "gix-ref",
+ "gix-sec",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-features"
+version = "0.38.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac7045ac9fe5f9c727f38799d002a7ed3583cd777e3322a7c4b43e3cf437dc69"
+dependencies = [
+ "bytes",
+ "bytesize",
+ "crc32fast",
+ "crossbeam-channel",
+ "flate2",
+ "gix-hash",
+ "gix-trace",
+ "gix-utils",
+ "libc",
+ "once_cell",
+ "parking_lot",
+ "prodash",
+ "sha1_smol",
+ "thiserror",
+ "walkdir",
+]
+
+[[package]]
+name = "gix-filter"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4121790ae140066e5b953becc72e7496278138d19239be2e63b5067b0843119e"
+dependencies = [
+ "bstr",
+ "encoding_rs",
+ "gix-attributes",
+ "gix-command",
+ "gix-hash",
+ "gix-object",
+ "gix-packetline-blocking",
+ "gix-path",
+ "gix-quote",
+ "gix-trace",
+ "gix-utils",
+ "smallvec",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-fs"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2bfe6249cfea6d0c0e0990d5226a4cb36f030444ba9e35e0639275db8f98575"
+dependencies = [
+ "fastrand",
+ "gix-features",
+ "gix-utils",
+]
+
+[[package]]
+name = "gix-glob"
+version = "0.16.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74908b4bbc0a0a40852737e5d7889f676f081e340d5451a16e5b4c50d592f111"
+dependencies = [
+ "bitflags 2.5.0",
+ "bstr",
+ "gix-features",
+ "gix-path",
+]
+
+[[package]]
+name = "gix-hash"
+version = "0.14.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f93d7df7366121b5018f947a04d37f034717e113dcf9ccd85c34b58e57a74d5e"
+dependencies = [
+ "faster-hex",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-hashtable"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ddf80e16f3c19ac06ce415a38b8591993d3f73aede049cb561becb5b3a8e242"
+dependencies = [
+ "gix-hash",
+ "hashbrown 0.14.5",
+ "parking_lot",
+]
+
+[[package]]
+name = "gix-ignore"
+version = "0.11.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e447cd96598460f5906a0f6c75e950a39f98c2705fc755ad2f2020c9e937fab7"
+dependencies = [
+ "bstr",
+ "gix-glob",
+ "gix-path",
+ "gix-trace",
+ "unicode-bom",
+]
+
+[[package]]
+name = "gix-index"
+version = "0.35.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0cd4203244444017682176e65fd0180be9298e58ed90bd4a8489a357795ed22d"
+dependencies = [
+ "bitflags 2.5.0",
+ "bstr",
+ "filetime",
+ "fnv",
+ "gix-bitmap",
+ "gix-features",
+ "gix-fs",
+ "gix-hash",
+ "gix-lock",
+ "gix-object",
+ "gix-traverse",
+ "gix-utils",
+ "gix-validate",
+ "hashbrown 0.14.5",
+ "itoa",
+ "libc",
+ "memmap2",
+ "rustix",
+ "smallvec",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-lock"
+version = "14.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3bc7fe297f1f4614774989c00ec8b1add59571dc9b024b4c00acb7dedd4e19d"
+dependencies = [
+ "gix-tempfile",
+ "gix-utils",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-mailmap"
+version = "0.24.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7d522c8ec2501e1a5b2b4cb54e83cb5d9a52471c9d23b3a1e8dadaf063752f7"
+dependencies = [
+ "bstr",
+ "gix-actor",
+ "gix-date",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-negotiate"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4063bf329a191a9e24b6f948a17ccf6698c0380297f5e169cee4f1d2ab9475b"
+dependencies = [
+ "bitflags 2.5.0",
+ "gix-commitgraph",
+ "gix-date",
+ "gix-hash",
+ "gix-object",
+ "gix-revwalk",
+ "smallvec",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-object"
+version = "0.44.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f5b801834f1de7640731820c2df6ba88d95480dc4ab166a5882f8ff12b88efa"
+dependencies = [
+ "bstr",
+ "gix-actor",
+ "gix-date",
+ "gix-features",
+ "gix-hash",
+ "gix-utils",
+ "gix-validate",
+ "itoa",
+ "smallvec",
+ "thiserror",
+ "winnow",
+]
+
+[[package]]
+name = "gix-odb"
+version = "0.63.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3158068701c17df54f0ab2adda527f5a6aca38fd5fd80ceb7e3c0a2717ec747"
+dependencies = [
+ "arc-swap",
+ "gix-date",
+ "gix-features",
+ "gix-fs",
+ "gix-hash",
+ "gix-object",
+ "gix-pack",
+ "gix-path",
+ "gix-quote",
+ "parking_lot",
+ "tempfile",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-pack"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3223aa342eee21e1e0e403cad8ae9caf9edca55ef84c347738d10681676fd954"
+dependencies = [
+ "clru",
+ "gix-chunk",
+ "gix-features",
+ "gix-hash",
+ "gix-hashtable",
+ "gix-object",
+ "gix-path",
+ "memmap2",
+ "smallvec",
+ "thiserror",
+ "uluru",
+]
+
+[[package]]
+name = "gix-packetline-blocking"
+version = "0.17.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9802304baa798dd6f5ff8008a2b6516d54b74a69ca2d3a2b9e2d6c3b5556b40"
+dependencies = [
+ "bstr",
+ "faster-hex",
+ "gix-trace",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-path"
+version = "0.10.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebfc4febd088abdcbc9f1246896e57e37b7a34f6909840045a1767c6dafac7af"
+dependencies = [
+ "bstr",
+ "gix-trace",
+ "home",
+ "once_cell",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-pathspec"
+version = "0.7.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d23bf239532b4414d0e63b8ab3a65481881f7237ed9647bb10c1e3cc54c5ceb"
+dependencies = [
+ "bitflags 2.5.0",
+ "bstr",
+ "gix-attributes",
+ "gix-config-value",
+ "gix-glob",
+ "gix-path",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-prompt"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74fde865cdb46b30d8dad1293385d9bcf998d3a39cbf41bee67d0dab026fe6b1"
+dependencies = [
+ "gix-command",
+ "gix-config-value",
+ "parking_lot",
+ "rustix",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-quote"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cbff4f9b9ea3fa7a25a70ee62f545143abef624ac6aa5884344e70c8b0a1d9ff"
+dependencies = [
+ "bstr",
+ "gix-utils",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-ref"
+version = "0.47.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae0d8406ebf9aaa91f55a57f053c5a1ad1a39f60fdf0303142b7be7ea44311e5"
+dependencies = [
+ "gix-actor",
+ "gix-features",
+ "gix-fs",
+ "gix-hash",
+ "gix-lock",
+ "gix-object",
+ "gix-path",
+ "gix-tempfile",
+ "gix-utils",
+ "gix-validate",
+ "memmap2",
+ "thiserror",
+ "winnow",
+]
+
+[[package]]
+name = "gix-refspec"
+version = "0.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebb005f82341ba67615ffdd9f7742c87787544441c88090878393d0682869ca6"
+dependencies = [
+ "bstr",
+ "gix-hash",
+ "gix-revision",
+ "gix-validate",
+ "smallvec",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-revision"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba4621b219ac0cdb9256883030c3d56a6c64a6deaa829a92da73b9a576825e1e"
+dependencies = [
+ "bstr",
+ "gix-date",
+ "gix-hash",
+ "gix-hashtable",
+ "gix-object",
+ "gix-revwalk",
+ "gix-trace",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-revwalk"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b41e72544b93084ee682ef3d5b31b1ba4d8fa27a017482900e5e044d5b1b3984"
+dependencies = [
+ "gix-commitgraph",
+ "gix-date",
+ "gix-hash",
+ "gix-hashtable",
+ "gix-object",
+ "smallvec",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-sec"
+version = "0.10.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fe4d52f30a737bbece5276fab5d3a8b276dc2650df963e293d0673be34e7a5f"
+dependencies = [
+ "bitflags 2.5.0",
+ "gix-path",
+ "libc",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "gix-status"
+version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
+checksum = "f70d35ba639f0c16a6e4cca81aa374a05f07b23fa36ee8beb72c100d98b4ffea"
dependencies = [
- "futures-core",
- "futures-sink",
+ "bstr",
+ "filetime",
+ "gix-diff",
+ "gix-dir",
+ "gix-features",
+ "gix-filter",
+ "gix-fs",
+ "gix-hash",
+ "gix-index",
+ "gix-object",
+ "gix-path",
+ "gix-pathspec",
+ "gix-worktree",
+ "portable-atomic",
+ "thiserror",
]
[[package]]
-name = "futures-core"
-version = "0.3.30"
+name = "gix-submodule"
+version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
+checksum = "529d0af78cc2f372b3218f15eb1e3d1635a21c8937c12e2dd0b6fc80c2ca874b"
+dependencies = [
+ "bstr",
+ "gix-config",
+ "gix-path",
+ "gix-pathspec",
+ "gix-refspec",
+ "gix-url",
+ "thiserror",
+]
[[package]]
-name = "futures-executor"
-version = "0.3.30"
+name = "gix-tempfile"
+version = "14.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d"
+checksum = "046b4927969fa816a150a0cda2e62c80016fe11fb3c3184e4dddf4e542f108aa"
dependencies = [
- "futures-core",
- "futures-task",
- "futures-util",
+ "dashmap",
+ "gix-fs",
+ "libc",
+ "once_cell",
+ "parking_lot",
+ "signal-hook",
+ "signal-hook-registry",
+ "tempfile",
]
[[package]]
-name = "futures-io"
-version = "0.3.30"
+name = "gix-trace"
+version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
+checksum = "6cae0e8661c3ff92688ce1c8b8058b3efb312aba9492bbe93661a21705ab431b"
[[package]]
-name = "futures-macro"
-version = "0.3.30"
+name = "gix-traverse"
+version = "0.41.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
+checksum = "030da39af94e4df35472e9318228f36530989327906f38e27807df305fccb780"
dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.63",
+ "bitflags 2.5.0",
+ "gix-commitgraph",
+ "gix-date",
+ "gix-hash",
+ "gix-hashtable",
+ "gix-object",
+ "gix-revwalk",
+ "smallvec",
+ "thiserror",
]
[[package]]
-name = "futures-sink"
-version = "0.3.30"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
-
-[[package]]
-name = "futures-task"
-version = "0.3.30"
+name = "gix-url"
+version = "0.27.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
+checksum = "fd280c5e84fb22e128ed2a053a0daeacb6379469be6a85e3d518a0636e160c89"
+dependencies = [
+ "bstr",
+ "gix-features",
+ "gix-path",
+ "home",
+ "thiserror",
+ "url",
+]
[[package]]
-name = "futures-util"
-version = "0.3.30"
+name = "gix-utils"
+version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
+checksum = "35192df7fd0fa112263bad8021e2df7167df4cc2a6e6d15892e1e55621d3d4dc"
dependencies = [
- "futures-channel",
- "futures-core",
- "futures-io",
- "futures-macro",
- "futures-sink",
- "futures-task",
- "memchr",
- "pin-project-lite",
- "pin-utils",
- "slab",
+ "bstr",
+ "fastrand",
+ "unicode-normalization",
]
[[package]]
-name = "generic-array"
-version = "0.14.7"
+name = "gix-validate"
+version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+checksum = "81f2badbb64e57b404593ee26b752c26991910fd0d81fe6f9a71c1a8309b6c86"
dependencies = [
- "typenum",
- "version_check",
+ "bstr",
+ "thiserror",
]
[[package]]
-name = "getrandom"
-version = "0.2.15"
+name = "gix-worktree"
+version = "0.36.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
+checksum = "c312ad76a3f2ba8e865b360d5cb3aa04660971d16dec6dd0ce717938d903149a"
dependencies = [
- "cfg-if",
- "libc",
- "wasi",
+ "bstr",
+ "gix-attributes",
+ "gix-features",
+ "gix-fs",
+ "gix-glob",
+ "gix-hash",
+ "gix-ignore",
+ "gix-index",
+ "gix-object",
+ "gix-path",
+ "gix-validate",
]
[[package]]
-name = "gimli"
-version = "0.28.1"
+name = "gix-worktree-state"
+version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
+checksum = "7b05c4b313fa702c0bacd5068dd3e01671da73b938fade97676859fee286de43"
+dependencies = [
+ "bstr",
+ "gix-features",
+ "gix-filter",
+ "gix-fs",
+ "gix-glob",
+ "gix-hash",
+ "gix-index",
+ "gix-object",
+ "gix-path",
+ "gix-worktree",
+ "io-close",
+ "thiserror",
+]
[[package]]
-name = "git2"
-version = "0.18.3"
+name = "gix-worktree-stream"
+version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "232e6a7bfe35766bf715e55a88b39a700596c0ccfd88cd3680b4cdb40d66ef70"
+checksum = "68e81b87c1a3ece22a54b682d6fdc37fbb3977132da972cafe5ec07175fddbca"
dependencies = [
- "bitflags 2.5.0",
- "libc",
- "libgit2-sys",
- "log",
- "openssl-probe",
- "openssl-sys",
- "url",
+ "gix-attributes",
+ "gix-features",
+ "gix-filter",
+ "gix-fs",
+ "gix-hash",
+ "gix-object",
+ "gix-path",
+ "gix-traverse",
+ "parking_lot",
+ "thiserror",
]
[[package]]
@@ -1151,6 +1997,10 @@ name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
+dependencies = [
+ "ahash",
+ "allocator-api2",
+]
[[package]]
name = "hdrhistogram"
@@ -1266,6 +2116,12 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
+[[package]]
+name = "human_format"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c3b1f728c459d27b12448862017b96ad4767b1ec2ec5e6434e99f1577f085b8"
+
[[package]]
name = "humansize"
version = "2.1.3"
@@ -1368,6 +2224,16 @@ dependencies = [
"unicode-normalization",
]
+[[package]]
+name = "imara-diff"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc9da1a252bd44cd341657203722352efc9bc0c847d06ea6d2dc1cd1135e0a01"
+dependencies = [
+ "ahash",
+ "hashbrown 0.14.5",
+]
+
[[package]]
name = "indexmap"
version = "1.9.3"
@@ -1388,6 +2254,16 @@ dependencies = [
"hashbrown 0.14.5",
]
+[[package]]
+name = "io-close"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9cadcf447f06744f8ce713d2d6239bb5bde2c357a452397a9ed90c625da390bc"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
[[package]]
name = "is_terminal_polyfill"
version = "1.70.0"
@@ -1409,6 +2285,31 @@ version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
+[[package]]
+name = "jiff"
+version = "0.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a45489186a6123c128fdf6016183fcfab7113e1820eb813127e036e287233fb"
+dependencies = [
+ "jiff-tzdb-platform",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "jiff-tzdb"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91335e575850c5c4c673b9bd467b0e025f164ca59d0564f69d0c2ee0ffad4653"
+
+[[package]]
+name = "jiff-tzdb-platform"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9835f0060a626fe59f160437bc725491a6af23133ea906500027d1bd2f8f4329"
+dependencies = [
+ "jiff-tzdb",
+]
+
[[package]]
name = "jobserver"
version = "0.1.31"
@@ -1427,6 +2328,15 @@ dependencies = [
"wasm-bindgen",
]
+[[package]]
+name = "kstring"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "558bf9508a558512042d3095138b1f7b8fe90c5467d94f9f1da28b3731c5dbd1"
+dependencies = [
+ "static_assertions",
+]
+
[[package]]
name = "lazy_static"
version = "1.4.0"
@@ -1445,20 +2355,6 @@ version = "0.2.154"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346"
-[[package]]
-name = "libgit2-sys"
-version = "0.16.2+1.7.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ee4126d8b4ee5c9d9ea891dd875cfdc1e9d0950437179104b183d7d8a74d24e8"
-dependencies = [
- "cc",
- "libc",
- "libssh2-sys",
- "libz-sys",
- "openssl-sys",
- "pkg-config",
-]
-
[[package]]
name = "libloading"
version = "0.8.3"
@@ -1466,7 +2362,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19"
dependencies = [
"cfg-if",
- "windows-targets 0.52.5",
+ "windows-targets 0.48.5",
]
[[package]]
@@ -1489,20 +2385,6 @@ dependencies = [
"libz-sys",
]
-[[package]]
-name = "libssh2-sys"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2dc8a030b787e2119a731f1951d6a773e2280c660f8ec4b0f5e1505a386e71ee"
-dependencies = [
- "cc",
- "libc",
- "libz-sys",
- "openssl-sys",
- "pkg-config",
- "vcpkg",
-]
-
[[package]]
name = "libz-sys"
version = "1.1.16"
@@ -1510,7 +2392,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e143b5e666b2695d28f6bca6497720813f699c9602dd7f5cac91008b8ada7f9"
dependencies = [
"cc",
- "libc",
"pkg-config",
"vcpkg",
]
@@ -1576,6 +2457,15 @@ version = "2.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d"
+[[package]]
+name = "memmap2"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f"
+dependencies = [
+ "libc",
+]
+
[[package]]
name = "mime"
version = "0.3.17"
@@ -1763,24 +2653,6 @@ dependencies = [
"pkg-config",
]
-[[package]]
-name = "openssl-probe"
-version = "0.1.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
-
-[[package]]
-name = "openssl-sys"
-version = "0.9.102"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2"
-dependencies = [
- "cc",
- "libc",
- "pkg-config",
- "vcpkg",
-]
-
[[package]]
name = "ordered-multimap"
version = "0.7.3"
@@ -1823,7 +2695,7 @@ dependencies = [
"libc",
"redox_syscall 0.5.1",
"smallvec",
- "windows-targets 0.52.5",
+ "windows-targets 0.52.6",
]
[[package]]
@@ -1899,6 +2771,12 @@ dependencies = [
"time",
]
+[[package]]
+name = "portable-atomic"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d30538d42559de6b034bc76fd6dd4c38961b1ee5c6c56e3808c50128fdbc22ce"
+
[[package]]
name = "powerfmt"
version = "0.2.0"
@@ -1920,6 +2798,16 @@ dependencies = [
"unicode-ident",
]
+[[package]]
+name = "prodash"
+version = "28.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "744a264d26b88a6a7e37cbad97953fa233b94d585236310bcbc88474b4092d79"
+dependencies = [
+ "bytesize",
+ "human_format",
+]
+
[[package]]
name = "prost"
version = "0.12.4"
@@ -2111,7 +2999,7 @@ dependencies = [
"console-subscriber",
"flate2",
"futures",
- "git2",
+ "gix",
"hex",
"httparse",
"humantime",
@@ -2316,6 +3204,12 @@ dependencies = [
"unsafe-libyaml",
]
+[[package]]
+name = "sha1_smol"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d"
+
[[package]]
name = "sha2"
version = "0.10.8"
@@ -2348,6 +3242,16 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+[[package]]
+name = "signal-hook"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801"
+dependencies = [
+ "libc",
+ "signal-hook-registry",
+]
+
[[package]]
name = "signal-hook-registry"
version = "1.4.2"
@@ -2398,6 +3302,12 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
+[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+
[[package]]
name = "std_prelude"
version = "0.2.12"
@@ -2501,6 +3411,19 @@ dependencies = [
"xattr",
]
+[[package]]
+name = "tempfile"
+version = "3.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64"
+dependencies = [
+ "cfg-if",
+ "fastrand",
+ "once_cell",
+ "rustix",
+ "windows-sys 0.59.0",
+]
+
[[package]]
name = "terminal_size"
version = "0.3.0"
@@ -2830,6 +3753,15 @@ version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
+[[package]]
+name = "uluru"
+version = "3.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c8a2469e56e6e5095c82ccd3afb98dad95f7af7929aab6d8ba8d6e0f73657da"
+dependencies = [
+ "arrayvec",
+]
+
[[package]]
name = "unicase"
version = "2.7.0"
@@ -2845,6 +3777,12 @@ version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75"
+[[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.12"
@@ -2886,9 +3824,9 @@ checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
[[package]]
name = "url"
-version = "2.5.0"
+version = "2.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633"
+checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
dependencies = [
"form_urlencoded",
"idna",
@@ -3063,7 +4001,16 @@ version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
- "windows-targets 0.52.5",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+dependencies = [
+ "windows-targets 0.52.6",
]
[[package]]
@@ -3083,18 +4030,18 @@ dependencies = [
[[package]]
name = "windows-targets"
-version = "0.52.5"
+version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
- "windows_aarch64_gnullvm 0.52.5",
- "windows_aarch64_msvc 0.52.5",
- "windows_i686_gnu 0.52.5",
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
"windows_i686_gnullvm",
- "windows_i686_msvc 0.52.5",
- "windows_x86_64_gnu 0.52.5",
- "windows_x86_64_gnullvm 0.52.5",
- "windows_x86_64_msvc 0.52.5",
+ "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]]
@@ -3105,9 +4052,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_gnullvm"
-version = "0.52.5"
+version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
@@ -3117,9 +4064,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_aarch64_msvc"
-version = "0.52.5"
+version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
@@ -3129,15 +4076,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_gnu"
-version = "0.52.5"
+version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
-version = "0.52.5"
+version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
@@ -3147,9 +4094,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_i686_msvc"
-version = "0.52.5"
+version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
@@ -3159,9 +4106,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnu"
-version = "0.52.5"
+version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
@@ -3171,9 +4118,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_gnullvm"
-version = "0.52.5"
+version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
@@ -3183,9 +4130,18 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "windows_x86_64_msvc"
-version = "0.52.5"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "winnow"
+version = "0.6.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
+checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b"
+dependencies = [
+ "memchr",
+]
[[package]]
name = "xattr"
@@ -3237,6 +4193,26 @@ dependencies = [
"synstructure",
]
+[[package]]
+name = "zerocopy"
+version = "0.7.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.7.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.63",
+]
+
[[package]]
name = "zerofrom"
version = "0.1.3"
diff --git a/Cargo.toml b/Cargo.toml
index 6d4a926..a8788e8 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -20,7 +20,7 @@ console-subscriber = { version = "0.2", features = ["parking_lot"] }
comrak = "0.21.0"
clap = { version = "4.4.10", features = ["cargo", "derive"] }
futures = "0.3"
-git2 = "0.18.0"
+gix = "0.66"
hex = "0.4"
humantime = "2.1"
itertools = "0.12"
diff --git a/README.md b/README.md
index b6d3227..26d4cb6 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
[See it in action!](https://git.inept.dev/)
-A gitweb/cgit-like interface for the modern age. Written in Rust using Axum, git2, Askama and RocksDB.
+A gitweb/cgit-like interface for the modern age. Written in Rust using Axum, gitoxide, Askama and RocksDB.
Includes a dark mode for late night committing.
@@ -38,13 +38,13 @@ Includes a dark mode for late night committing.
[RocksDB][] is used to store all metadata about a repository, including commits, branches, and tags. Metadata is reindexed, and the reindex interval is configurable (default: every 5 minutes), resulting in up to 97% faster load times for large repositories.
- **On-Demand Loading**
- Files, trees, and diffs are loaded using [git2][] directly upon request. A small in-memory cache is included for rendered READMEs and diffs, enhancing performance.
+ Files, trees, and diffs are loaded using [gitoxide][] directly upon request. A small in-memory cache is included for rendered READMEs and diffs, enhancing performance.
- **Dark Mode Support**
Enjoy a dark mode for late-night committing, providing a visually comfortable experience during extended coding sessions.
[RocksDB]: https://github.com/facebook/rocksdb
-[git2]: https://github.com/rust-lang/git2-rs
+[gitoxide]: https://github.com/Byron/gitoxide
## Getting Started
diff --git a/doc/man/rgit.1.md b/doc/man/rgit.1.md
index 4b5b657..f0270b3 100644
--- a/doc/man/rgit.1.md
+++ b/doc/man/rgit.1.md
@@ -15,7 +15,7 @@ SYNOPSIS
DESCRIPTION
===========
-A gitweb/cgit-like interface for the modern age. Written in Rust using Axum, git2, Askama, and RocksDB.
+A gitweb/cgit-like interface for the modern age. Written in Rust using Axum, gitoxide, Askama, and RocksDB.
_bind_address_
diff --git a/flake.nix b/flake.nix
index d260678..806e941 100644
--- a/flake.nix
+++ b/flake.nix
@@ -15,10 +15,9 @@
defaultPackage = naersk-lib.buildPackage {
root = ./.;
nativeBuildInputs = with pkgs; [ pkg-config clang ];
- buildInputs = with pkgs; [ openssl zlib libssh2 libgit2 ];
+ buildInputs = with pkgs; [ ];
LIBCLANG_PATH = "${pkgs.clang.cc.lib}/lib";
ROCKSDB_LIB_DIR = "${pkgs.rocksdb}/lib";
- LIBSSH2_SYS_USE_PKG_CONFIG = "true";
};
devShell = with pkgs; mkShell {
buildInputs = [ cargo rustc rustfmt pre-commit rustPackages.clippy ];
diff --git a/src/database/indexer.rs b/src/database/indexer.rs
index 3469009..8ed543a 100644
--- a/src/database/indexer.rs
+++ b/src/database/indexer.rs
@@ -8,11 +8,13 @@ use std::{
};
use anyhow::Context;
-use git2::{ErrorCode, Reference, Sort};
+use gix::bstr::ByteSlice;
+use gix::refs::Category;
+use gix::Reference;
use ini::Ini;
use itertools::Itertools;
use rocksdb::WriteBatch;
-use time::OffsetDateTime;
+use time::{OffsetDateTime, UtcOffset};
use tracing::{error, info, info_span, instrument, warn};
use crate::database::schema::{
@@ -69,7 +71,7 @@ fn update_repository_metadata(scan_path: &Path, db: &rocksdb::DB) {
let repository_path = scan_path.join(relative);
- let git_repository = match git2::Repository::open(repository_path.clone()) {
+ let mut git_repository = match gix::open(repository_path.clone()) {
Ok(v) => v,
Err(error) => {
warn!(%error, "Failed to open repository {} to update metadata, skipping", relative.display());
@@ -77,6 +79,8 @@ fn update_repository_metadata(scan_path: &Path, db: &rocksdb::DB) {
}
};
+ git_repository.object_cache_size(10 * 1024 * 1024);
+
let res = Repository {
id,
name,
@@ -97,22 +101,26 @@ fn update_repository_metadata(scan_path: &Path, db: &rocksdb::DB) {
}
}
-fn find_default_branch(repo: &git2::Repository) -> Result<Option<String>, git2::Error> {
- Ok(repo.head()?.name().map(ToString::to_string))
+fn find_default_branch(repo: &gix::Repository) -> Result<Option<String>, anyhow::Error> {
+ Ok(Some(repo.head()?.name().as_bstr().to_string()))
}
-fn find_last_committed_time(repo: &git2::Repository) -> Result<OffsetDateTime, git2::Error> {
+fn find_last_committed_time(repo: &gix::Repository) -> Result<OffsetDateTime, anyhow::Error> {
let mut timestamp = OffsetDateTime::UNIX_EPOCH;
- for reference in repo.references()? {
- let Ok(commit) = reference?.peel_to_commit() else {
+ for reference in repo.references()?.all()? {
+ let Ok(commit) = reference.unwrap().peel_to_commit() else {
continue;
};
- let committed_time = commit.committer().when().seconds();
- let committed_time = OffsetDateTime::from_unix_timestamp(committed_time)
+ let committer = commit.committer()?;
+ let mut committed_time = OffsetDateTime::from_unix_timestamp(committer.time.seconds)
.unwrap_or(OffsetDateTime::UNIX_EPOCH);
+ if let Ok(offset) = UtcOffset::from_whole_seconds(committer.time.offset) {
+ committed_time = committed_time.to_offset(offset);
+ }
+
if committed_time > timestamp {
timestamp = committed_time;
}
@@ -145,28 +153,44 @@ fn update_repository_reflog(scan_path: &Path, db: Arc<rocksdb::DB>) {
}
};
+ let references = match references.all() {
+ Ok(v) => v,
+ Err(error) => {
+ error!(%error, "Failed to read references for {relative_path}");
+ continue;
+ }
+ };
+
let mut valid_references = Vec::new();
- for reference in references.filter_map(Result::ok) {
- let reference_name = String::from_utf8_lossy(reference.name_bytes());
- if !reference_name.starts_with("refs/heads/")
- && !reference_name.starts_with("refs/tags/")
- {
+ for reference in references {
+ let mut reference = match reference {
+ Ok(v) => v,
+ Err(error) => {
+ error!(%error, "Failed to read reference for {relative_path}");
+ continue;
+ }
+ };
+
+ let reference_name = reference.name();
+ if !matches!(
+ reference_name.category(),
+ Some(Category::Tag | Category::LocalBranch)
+ ) {
continue;
}
- valid_references.push(reference_name.to_string());
+ valid_references.push(reference_name.as_bstr().to_string());
if let Err(error) = branch_index_update(
- &reference,
- &reference_name,
+ &mut reference,
&relative_path,
db_repository.get(),
db.clone(),
&git_repository,
false,
) {
- error!(%error, "Failed to update reflog for {relative_path}@{reference_name}");
+ error!(%error, "Failed to update reflog for {relative_path}@{:?}", valid_references.last());
}
}
@@ -178,17 +202,16 @@ fn update_repository_reflog(scan_path: &Path, db: Arc<rocksdb::DB>) {
#[instrument(skip(reference, db_repository, db, git_repository))]
fn branch_index_update(
- reference: &Reference<'_>,
- reference_name: &str,
+ reference: &mut Reference<'_>,
relative_path: &str,
db_repository: &Repository<'_>,
db: Arc<rocksdb::DB>,
- git_repository: &git2::Repository,
+ git_repository: &gix::Repository,
force_reindex: bool,
) -> Result<(), anyhow::Error> {
info!("Refreshing indexes");
- let commit_tree = db_repository.commit_tree(db.clone(), reference_name);
+ let commit_tree = db_repository.commit_tree(db.clone(), reference.name().as_bstr().to_str()?);
if force_reindex {
commit_tree.drop_commits()?;
@@ -207,9 +230,13 @@ fn branch_index_update(
None
};
- let mut revwalk = git_repository.revwalk()?;
- revwalk.set_sorting(Sort::REVERSE)?;
- revwalk.push_ref(reference_name)?;
+ // TODO: stop collecting into a vec
+ let revwalk = git_repository
+ .rev_walk([commit.id().detach()])
+ .all()?
+ .collect::<Vec<_>>()
+ .into_iter()
+ .rev();
let tree_len = commit_tree.len()?;
let mut seen = false;
@@ -221,7 +248,7 @@ fn branch_index_update(
let rev = rev?;
if let (false, Some(latest_indexed)) = (seen, &latest_indexed) {
- if rev.as_bytes() == &*latest_indexed.get().hash {
+ if rev.id.as_bytes() == &*latest_indexed.get().hash {
seen = true;
}
@@ -234,11 +261,11 @@ fn branch_index_update(
info!("{} commits ingested", i + 1);
}
- let commit = git_repository.find_commit(rev)?;
- let author = commit.author();
- let committer = commit.committer();
+ let commit = rev.object()?;
+ let author = commit.author()?;
+ let committer = commit.committer()?;
- Commit::new(&commit, &author, &committer).insert(
+ Commit::new(&commit, author, committer)?.insert(
&commit_tree,
tree_len + i,
&mut batch,
@@ -255,7 +282,6 @@ fn branch_index_update(
return branch_index_update(
reference,
- reference_name,
relative_path,
db_repository,
db,
@@ -299,16 +325,17 @@ fn tag_index_scan(
relative_path: &str,
db_repository: &Repository<'_>,
db: Arc<rocksdb::DB>,
- git_repository: &git2::Repository,
+ git_repository: &gix::Repository,
) -> Result<(), anyhow::Error> {
let tag_tree = db_repository.tag_tree(db);
let git_tags: HashSet<_> = git_repository
.references()
.context("Failed to scan indexes on git repository")?
+ .all()?
.filter_map(Result::ok)
- .filter(|v| v.name_bytes().starts_with(b"refs/tags/"))
- .map(|v| String::from_utf8_lossy(v.name_bytes()).into_owned())
+ .filter(|v| v.name().category() == Some(Category::Tag))
+ .map(|v| v.name().as_bstr().to_string())
.collect();
let indexed_tags: HashSet<String> = tag_tree.list()?.into_iter().collect();
@@ -329,17 +356,17 @@ fn tag_index_scan(
#[instrument(skip(git_repository, tag_tree))]
fn tag_index_update(
tag_name: &str,
- git_repository: &git2::Repository,
+ git_repository: &gix::Repository,
tag_tree: &TagTree,
) -> Result<(), anyhow::Error> {
- let reference = git_repository
+ let mut reference = git_repository
.find_reference(tag_name)
.context("Failed to read newly discovered tag")?;
if let Ok(tag) = reference.peel_to_tag() {
info!("Inserting newly discovered tag to index");
- Tag::new(tag.tagger().as_ref()).insert(tag_tree, tag_name)?;
+ Tag::new(tag.tagger()?)?.insert(tag_tree, tag_name)?;
}
Ok(())
@@ -359,10 +386,13 @@ fn open_repo<P: AsRef<Path> + Debug>(
relative_path: P,
db_repository: &Repository<'_>,
db: &rocksdb::DB,
-) -> Option<git2::Repository> {
- match git2::Repository::open(scan_path.join(relative_path.as_ref())) {
- Ok(v) => Some(v),
- Err(e) if e.code() == ErrorCode::NotFound => {
+) -> Option<gix::Repository> {
+ match gix::open(scan_path.join(relative_path.as_ref())) {
+ Ok(mut v) => {
+ v.object_cache_size(10 * 1024 * 1024);
+ Some(v)
+ }
+ Err(gix::open::Error::Io(e)) if e.kind() == std::io::ErrorKind::NotFound => {
warn!("Repository gone from disk, removing from db");
if let Err(error) = db_repository.delete(db, relative_path) {
diff --git a/src/database/schema/commit.rs b/src/database/schema/commit.rs
index 98b56a8..87b065e 100644
--- a/src/database/schema/commit.rs
+++ b/src/database/schema/commit.rs
@@ -1,10 +1,12 @@
use std::{borrow::Cow, ops::Deref, sync::Arc};
use anyhow::Context;
-use git2::{Oid, Signature};
+use gix::actor::SignatureRef;
+use gix::bstr::ByteSlice;
+use gix::ObjectId;
use rocksdb::{IteratorMode, ReadOptions, WriteBatch};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
-use time::OffsetDateTime;
+use time::{OffsetDateTime, UtcOffset};
use tracing::debug;
use yoke::{Yoke, Yokeable};
@@ -27,21 +29,21 @@ pub struct Commit<'a> {
impl<'a> Commit<'a> {
pub fn new(
- commit: &'a git2::Commit<'_>,
- author: &'a git2::Signature<'_>,
- committer: &'a git2::Signature<'_>,
- ) -> Self {
- Self {
- summary: commit
- .summary_bytes()
- .map_or(Cow::Borrowed(""), String::from_utf8_lossy),
- message: commit
- .body_bytes()
- .map_or(Cow::Borrowed(""), String::from_utf8_lossy),
- committer: committer.into(),
- author: author.into(),
- hash: CommitHash::Oid(commit.id()),
- }
+ commit: &gix::Commit<'_>,
+ author: SignatureRef<'a>,
+ committer: SignatureRef<'a>,
+ ) -> Result<Self, anyhow::Error> {
+ let message = commit.message()?;
+
+ Ok(Self {
+ summary: message.summary().to_string().into(),
+ message: message
+ .body
+ .map_or(Cow::Borrowed(""), |v| v.to_string().into()),
+ committer: committer.try_into()?,
+ author: author.try_into()?,
+ hash: CommitHash::Oid(commit.id().detach()),
+ })
}
pub fn insert(&self, tree: &CommitTree, id: u64, tx: &mut WriteBatch) -> anyhow::Result<()> {
@@ -51,7 +53,7 @@ impl<'a> Commit<'a> {
#[derive(Debug)]
pub enum CommitHash<'a> {
- Oid(Oid),
+ Oid(ObjectId),
Bytes(&'a [u8]),
}
@@ -97,14 +99,16 @@ pub struct Author<'a> {
pub time: OffsetDateTime,
}
-impl<'a> From<&'a git2::Signature<'_>> for Author<'a> {
- fn from(author: &'a Signature<'_>) -> Self {
- Self {
- name: String::from_utf8_lossy(author.name_bytes()),
- email: String::from_utf8_lossy(author.email_bytes()),
- // TODO: this needs to deal with offset
- time: OffsetDateTime::from_unix_timestamp(author.when().seconds()).unwrap(),
- }
+impl<'a> TryFrom<SignatureRef<'a>> for Author<'a> {
+ type Error = anyhow::Error;
+
+ fn try_from(author: SignatureRef<'a>) -> Result<Self, anyhow::Error> {
+ Ok(Self {
+ name: author.name.to_str_lossy(),
+ email: author.email.to_str_lossy(),
+ time: OffsetDateTime::from_unix_timestamp(author.time.seconds)?
+ .to_offset(UtcOffset::from_whole_seconds(author.time.offset)?),
+ })
}
}
diff --git a/src/database/schema/tag.rs b/src/database/schema/tag.rs
index d3fd4d2..132f740 100644
--- a/src/database/schema/tag.rs
+++ b/src/database/schema/tag.rs
@@ -1,7 +1,7 @@
use std::{collections::HashSet, sync::Arc};
use anyhow::Context;
-use git2::Signature;
+use gix::actor::SignatureRef;
use serde::{Deserialize, Serialize};
use yoke::{Yoke, Yokeable};
@@ -16,10 +16,10 @@ pub struct Tag<'a> {
}
impl<'a> Tag<'a> {
- pub fn new(tagger: Option<&'a Signature<'_>>) -> Self {
- Self {
- tagger: tagger.map(Into::into),
- }
+ pub fn new(tagger: Option<SignatureRef<'a>>) -> Result<Self, anyhow::Error> {
+ Ok(Self {
+ tagger: tagger.map(TryFrom::try_from).transpose()?,
+ })
}
pub fn insert(&self, batch: &TagTree, name: &str) -> Result<(), anyhow::Error> {
diff --git a/src/git.rs b/src/git.rs
index fe0dda4..9e91187 100644
--- a/src/git.rs
+++ b/src/git.rs
@@ -1,37 +1,49 @@
-use std::{
- borrow::Cow,
- ffi::OsStr,
- fmt,
- fmt::Write,
- path::{Path, PathBuf},
- sync::Arc,
- time::Duration,
-};
-
use anyhow::{anyhow, Context, Result};
use axum::response::IntoResponse;
+use bytes::buf::Writer;
use bytes::{BufMut, Bytes, BytesMut};
use comrak::{ComrakPlugins, Options};
-use git2::{
- DiffFormat, DiffLineType, DiffOptions, DiffStatsFormat, Email, EmailCreateOptions, ObjectType,
- Oid, Signature, TreeWalkResult,
+use flate2::write::GzEncoder;
+use gix::{
+ actor::SignatureRef,
+ bstr::{BStr, BString, ByteSlice, ByteVec},
+ diff::blob::{platform::prepare_diff::Operation, Sink},
+ object::Kind,
+ objs::tree::EntryRef,
+ prelude::TreeEntryRefExt,
+ traverse::tree::visit::Action,
+ ObjectId,
};
use moka::future::Cache;
use parking_lot::Mutex;
+use std::{
+ borrow::Cow,
+ collections::{BTreeMap, VecDeque},
+ ffi::OsStr,
+ fmt::{self, Arguments, Write},
+ path::{Path, PathBuf},
+ str::FromStr,
+ sync::Arc,
+ time::Duration,
+};
use syntect::{
parsing::SyntaxSet,
parsing::{BasicScopeStackOp, ParseState, Scope, ScopeStack, SCOPE_REPO},
util::LinesWithEndings,
};
-use time::OffsetDateTime;
+use tar::Builder;
+use time::{OffsetDateTime, UtcOffset};
use tracing::{error, instrument, warn};
-use crate::syntax_highlight::ComrakSyntectAdapter;
+use crate::{
+ syntax_highlight::ComrakSyntectAdapter,
+ unified_diff_builder::{Callback, UnifiedDiffBuilder},
+};
type ReadmeCacheKey = (PathBuf, Option<Arc<str>>);
pub struct Git {
- commits: Cache<Oid, Arc<Commit>>,
+ commits: Cache<(ObjectId, bool), Arc<Commit>>,
readme_cache: Cache<ReadmeCacheKey, Option<(ReadmeFormat, Arc<str>)>>,
syntax_set: SyntaxSet,
}
@@ -60,9 +72,9 @@ impl Git {
repo_path: PathBuf,
branch: Option<Arc<str>>,
) -> Result<Arc<OpenRepository>> {
- let repo = tokio::task::spawn_blocking({
+ let mut repo = tokio::task::spawn_blocking({
let repo_path = repo_path.clone();
- move || git2::Repository::open(repo_path)
+ move || gix::open(repo_path)
})
.await
.context("Failed to join Tokio task")?
@@ -71,6 +83,8 @@ impl Git {
anyhow!("Failed to open repository")
})?;
+ repo.object_cache_size(10 * 1024 * 1024);
+
Ok(Arc::new(OpenRepository {
git: self,
cache_key: repo_path,
@@ -83,7 +97,7 @@ impl Git {
pub struct OpenRepository {
git: Arc<Git>,
cache_key: PathBuf,
- repo: Mutex<git2::Repository>,
+ repo: Mutex<gix::Repository>,
branch: Option<Arc<str>>,
}
@@ -95,7 +109,7 @@ impl OpenRepository {
formatted: bool,
) -> Result<PathDestination> {
let tree_id = tree_id
- .map(Oid::from_str)
+ .map(ObjectId::from_str)
.transpose()
.context("Failed to parse tree hash")?;
@@ -106,87 +120,88 @@ impl OpenRepository {
repo.find_tree(tree_id)
.context("Couldn't find tree with given id")?
} else if let Some(branch) = &self.branch {
- let reference = repo.resolve_reference_from_short_name(branch)?;
- reference
+ repo.find_reference(branch.as_ref())?
.peel_to_tree()
.context("Couldn't find tree for reference")?
} else {
- let head = repo.head().context("Failed to find HEAD")?;
- head.peel_to_tree()
- .context("Couldn't find tree from HEAD")?
+ repo.find_reference("HEAD")
+ .context("Failed to find HEAD")?
+ .peel_to_tree()
+ .context("Couldn't find HEAD for reference")?
};
if let Some(path) = path.as_ref() {
- let item = tree.get_path(path).context("Path doesn't exist in tree")?;
- let object = item
- .to_object(&repo)
- .context("Path in tree isn't an object")?;
-
- if let Some(blob) = object.as_blob() {
- // TODO: use Path here instead of a lossy utf8 conv
- let name = String::from_utf8_lossy(item.name_bytes());
- let path = path.clone().join(&*name);
-
- let extension = path
- .extension()
- .or_else(|| path.file_name())
- .map_or_else(|| Cow::Borrowed(""), OsStr::to_string_lossy);
- let content = match (formatted, blob.is_binary()) {
- (true, true) => Content::Binary(vec![]),
- (true, false) => Content::Text(
- format_file(
- &String::from_utf8_lossy(blob.content()),
+ let item = tree
+ .peel_to_entry_by_path(path)?
+ .context("Path doesn't exist in tree")?;
+ let object = item.object().context("Path in tree isn't an object")?;
+
+ match object.kind {
+ Kind::Blob => {
+ let path = path.join(item.filename().to_path_lossy());
+ let mut blob = object.into_blob();
+
+ let size = blob.data.len();
+ let extension = path
+ .extension()
+ .or_else(|| path.file_name())
+ .map_or_else(|| Cow::Borrowed(""), OsStr::to_string_lossy);
+
+ let content = match (formatted, String::from_utf8(blob.take_data())) {
+ (true, Err(_)) => Content::Binary(vec![]),
+ (true, Ok(data)) => Content::Text(Cow::Owned(format_file(
+ &data,
&extension,
&self.git.syntax_set,
- )?
- .into(),
- ),
- (false, true) => Content::Binary(blob.content().to_vec()),
- (false, false) => Content::Text(
- String::from_utf8_lossy(blob.content()).to_string().into(),
- ),
- };
+ )?)),
+ (false, Err(e)) => Content::Binary(e.into_bytes()),
+ (false, Ok(data)) => Content::Text(Cow::Owned(data)),
+ };
- return Ok(PathDestination::File(FileWithContent {
- metadata: File {
- mode: item.filemode(),
- size: blob.size(),
- path,
- name: name.into_owned(),
- },
- content,
- }));
- } else if let Ok(new_tree) = object.into_tree() {
- tree = new_tree;
- } else {
- anyhow::bail!("Given path not tree nor blob... what is it?!");
+ return Ok(PathDestination::File(FileWithContent {
+ metadata: File {
+ mode: item.mode().0,
+ size,
+ path: path.clone(),
+ name: item.filename().to_string(),
+ },
+ content,
+ }));
+ }
+ Kind::Tree => {
+ tree = object.into_tree();
+ }
+ _ => anyhow::bail!("bad object of type {:?}", object.kind),
}
}
let mut tree_items = Vec::new();
- for item in &tree {
+ for item in tree.iter() {
+ let item = item?;
let object = item
- .to_object(&repo)
+ .object()
.context("Expected item in tree to be object but it wasn't")?;
- let name = String::from_utf8_lossy(item.name_bytes()).into_owned();
- let path = path.clone().unwrap_or_default().join(&name);
+ let path = path
+ .clone()
+ .unwrap_or_default()
+ .join(item.filename().to_path_lossy());
- if let Some(blob) = object.as_blob() {
- tree_items.push(TreeItem::File(File {
- mode: item.filemode(),
- size: blob.size(),
+ tree_items.push(match object.kind {
+ Kind::Blob => TreeItem::File(File {
+ mode: item.mode().0,
+ size: object.into_blob().data.len(),
path,
- name,
- }));
- } else if let Some(_tree) = object.as_tree() {
- tree_items.push(TreeItem::Tree(Tree {
- mode: item.filemode(),
+ name: item.filename().to_string(),
+ }),
+ Kind::Tree => TreeItem::Tree(Tree {
+ mode: item.mode().0,
path,
- name,
- }));
- }
+ name: item.filename().to_string(),
+ }),
+ _ => continue,
+ });
}
Ok(PathDestination::Tree(tree_items))
@@ -206,21 +221,23 @@ impl OpenRepository {
.context("Given tag does not exist in repository")?
.peel_to_tag()
.context("Couldn't get to a tag from the given reference")?;
- let tag_target = tag.target().context("Couldn't find tagged object")?;
-
- let tagged_object = match tag_target.kind() {
- Some(ObjectType::Commit) => Some(TaggedObject::Commit(tag_target.id().to_string())),
- Some(ObjectType::Tree) => Some(TaggedObject::Tree(tag_target.id().to_string())),
- None | Some(_) => None,
+ let tag_target = tag
+ .target_id()
+ .context("Couldn't find tagged object")?
+ .object()?;
+
+ let tagged_object = match tag_target.kind {
+ Kind::Commit => Some(TaggedObject::Commit(tag_target.id.to_string())),
+ Kind::Tree => Some(TaggedObject::Tree(tag_target.id.to_string())),
+ _ => None,
};
+ let tag_info = tag.decode()?;
+
Ok(DetailedTag {
name: tag_name,
- tagger: tag.tagger().map(TryInto::try_into).transpose()?,
- message: tag
- .message_bytes()
- .map_or_else(|| Cow::Borrowed(""), String::from_utf8_lossy)
- .into_owned(),
+ tagger: tag_info.tagger.map(TryInto::try_into).transpose()?,
+ message: tag_info.message.to_string(),
tagged_object,
})
})
@@ -241,33 +258,34 @@ impl OpenRepository {
tokio::task::spawn_blocking(move || {
let repo = self.repo.lock();
- let head = if let Some(reference) = &self.branch {
- repo.resolve_reference_from_short_name(reference)?
+ let mut head = if let Some(reference) = &self.branch {
+ repo.find_reference(reference.as_ref())?
} else {
- repo.head().context("Couldn't find HEAD of repository")?
+ repo.find_reference("HEAD")
+ .context("Couldn't find HEAD of repository")?
};
let commit = head.peel_to_commit().context(
"Couldn't find the commit that the HEAD of the repository refers to",
)?;
- let tree = commit
+ let mut tree = commit
.tree()
.context("Couldn't get the tree that the HEAD refers to")?;
for name in README_FILES {
- let Some(tree_entry) = tree.get_name(name) else {
+ let Some(tree_entry) = tree.peel_to_entry_by_path(name)? else {
continue;
};
let Some(blob) = tree_entry
- .to_object(&repo)
+ .object()
.ok()
- .and_then(|v| v.into_blob().ok())
+ .and_then(|v| v.try_into_blob().ok())
else {
continue;
};
- let Ok(content) = std::str::from_utf8(blob.content()) else {
+ let Ok(content) = std::str::from_utf8(&blob.data) else {
continue;
};
@@ -291,33 +309,33 @@ impl OpenRepository {
tokio::task::spawn_blocking(move || {
let repo = self.repo.lock();
let head = repo.head().context("Couldn't find HEAD of repository")?;
- Ok(head.shorthand().map(ToString::to_string))
+ Ok(head.referent_name().map(|v| v.shorten().to_string()))
})
.await
.context("Failed to join Tokio task")?
}
#[instrument(skip(self))]
- pub async fn latest_commit(self: Arc<Self>) -> Result<Commit> {
+ pub async fn latest_commit(self: Arc<Self>, highlighted: bool) -> Result<Commit> {
tokio::task::spawn_blocking(move || {
let repo = self.repo.lock();
- let head = if let Some(reference) = &self.branch {
- repo.resolve_reference_from_short_name(reference)?
+ let mut head = if let Some(reference) = &self.branch {
+ repo.find_reference(reference.as_ref())?
} else {
- repo.head().context("Couldn't find HEAD of repository")?
+ repo.find_reference("HEAD")
+ .context("Couldn't find HEAD of repository")?
};
let commit = head
.peel_to_commit()
.context("Couldn't find commit HEAD of repository refers to")?;
- let (diff_plain, diff_output, diff_stats) =
- fetch_diff_and_stats(&repo, &commit, &self.git.syntax_set)?;
+ let (diff_output, diff_stats) =
+ fetch_diff_and_stats(&repo, &commit, highlighted.then_some(&self.git.syntax_set))?;
let mut commit = Commit::try_from(commit)?;
commit.diff_stats = diff_stats;
commit.diff = diff_output;
- commit.diff_plain = diff_plain;
Ok(commit)
})
.await
@@ -331,28 +349,20 @@ impl OpenRepository {
cont: tokio::sync::oneshot::Sender<()>,
commit: Option<&str>,
) -> Result<(), anyhow::Error> {
- const BUFFER_CAP: usize = 512 * 1024;
-
let commit = commit
- .map(Oid::from_str)
+ .map(ObjectId::from_str)
.transpose()
.context("failed to build oid")?;
tokio::task::spawn_blocking(move || {
- let buffer = BytesMut::with_capacity(BUFFER_CAP + 1024);
-
- let flate = flate2::write::GzEncoder::new(buffer.writer(), flate2::Compression::fast());
- let mut archive = tar::Builder::new(flate);
-
let repo = self.repo.lock();
let tree = if let Some(commit) = commit {
repo.find_commit(commit)?.tree()?
} else if let Some(reference) = &self.branch {
- repo.resolve_reference_from_short_name(reference)?
- .peel_to_tree()?
+ repo.find_reference(reference.as_ref())?.peel_to_tree()?
} else {
- repo.head()
+ repo.find_reference("HEAD")
.context("Couldn't find HEAD of repository")?
.peel_to_tree()?
};
@@ -362,41 +372,23 @@ impl OpenRepository {
return Err(anyhow!("requester gone"));
}
- let mut callback = |root: &str, entry: &git2::TreeEntry| -> TreeWalkResult {
- if let Ok(blob) = entry.to_object(&repo).unwrap().peel_to_blob() {
- let path =
- Path::new(root).join(String::from_utf8_lossy(entry.name_bytes()).as_ref());
-
- let mut header = tar::Header::new_gnu();
- if let Err(error) = header.set_path(&path) {
- warn!(%error, "Attempted to write invalid path to archive");
- return TreeWalkResult::Skip;
- }
- header.set_size(blob.size() as u64);
- #[allow(clippy::cast_sign_loss)]
- header.set_mode(entry.filemode() as u32);
- header.set_cksum();
-
- if let Err(error) = archive.append(&header, blob.content()) {
- error!(%error, "Failed to write blob to archive");
- return TreeWalkResult::Abort;
- }
- }
-
- if archive.get_ref().get_ref().get_ref().len() >= BUFFER_CAP {
- let b = archive.get_mut().get_mut().get_mut().split().freeze();
- if let Err(error) = res.blocking_send(Ok(b)) {
- error!(%error, "Failed to send buffer to client");
- return TreeWalkResult::Abort;
- }
- }
-
- TreeWalkResult::Ok
+ let buffer = BytesMut::with_capacity(BUFFER_CAP + 1024);
+ let mut visitor = ArchivalVisitor {
+ repository: &repo,
+ res,
+ archive: Builder::new(GzEncoder::new(buffer.writer(), flate2::Compression::fast())),
+ path_deque: VecDeque::new(),
+ path: BString::default(),
};
- tree.walk(git2::TreeWalkMode::PreOrder, &mut callback)?;
+ tree.traverse().breadthfirst(&mut visitor)?;
- res.blocking_send(Ok(archive.into_inner()?.finish()?.into_inner().freeze()))?;
+ visitor.res.blocking_send(Ok(visitor
+ .archive
+ .into_inner()?
+ .finish()?
+ .into_inner()
+ .freeze()))?;
Ok::<_, anyhow::Error>(())
})
@@ -406,26 +398,33 @@ impl OpenRepository {
}
#[instrument(skip(self))]
- pub async fn commit(self: Arc<Self>, commit: &str) -> Result<Arc<Commit>, Arc<anyhow::Error>> {
- let commit = Oid::from_str(commit)
+ pub async fn commit(
+ self: Arc<Self>,
+ commit: &str,
+ highlighted: bool,
+ ) -> Result<Arc<Commit>, Arc<anyhow::Error>> {
+ let commit = ObjectId::from_str(commit)
.map_err(anyhow::Error::from)
.map_err(Arc::new)?;
let git = self.git.clone();
git.commits
- .try_get_with(commit, async move {
+ .try_get_with((commit, highlighted), async move {
tokio::task::spawn_blocking(move || {
let repo = self.repo.lock();
let commit = repo.find_commit(commit)?;
- let (diff_plain, diff_output, diff_stats) =
- fetch_diff_and_stats(&repo, &commit, &self.git.syntax_set)?;
+
+ let (diff_output, diff_stats) = fetch_diff_and_stats(
+ &repo,
+ &commit,
+ highlighted.then_some(&self.git.syntax_set),
+ )?;
let mut commit = Commit::try_from(commit)?;
commit.diff_stats = diff_stats;
commit.diff = diff_output;
- commit.diff_plain = diff_plain;
Ok(Arc::new(commit))
})
@@ -436,6 +435,98 @@ impl OpenRepository {
}
}
+const BUFFER_CAP: usize = 512 * 1024;
+
+pub struct ArchivalVisitor<'a> {
+ repository: &'a gix::Repository,
+ res: tokio::sync::mpsc::Sender<Result<Bytes, anyhow::Error>>,
+ archive: Builder<GzEncoder<Writer<BytesMut>>>,
+ path_deque: VecDeque<BString>,
+ path: BString,
+}
+
+impl<'a> ArchivalVisitor<'a> {
+ fn pop_element(&mut self) {
+ if let Some(pos) = self.path.rfind_byte(b'/') {
+ self.path.resize(pos, 0);
+ } else {
+ self.path.clear();
+ }
+ }
+
+ fn push_element(&mut self, name: &BStr) {
+ if !self.path.is_empty() {
+ self.path.push(b'/');
+ }
+ self.path.push_str(name);
+ }
+}
+
+impl<'a> gix::traverse::tree::Visit for ArchivalVisitor<'a> {
+ fn pop_front_tracked_path_and_set_current(&mut self) {
+ self.path = self
+ .path_deque
+ .pop_front()
+ .expect("every call is matched with push_tracked_path_component");
+ }
+
+ fn push_back_tracked_path_component(&mut self, component: &BStr) {
+ self.push_element(component);
+ self.path_deque.push_back(self.path.clone());
+ }
+
+ fn push_path_component(&mut self, component: &BStr) {
+ self.push_element(component);
+ }
+
+ fn pop_path_component(&mut self) {
+ self.pop_element();
+ }
+
+ fn visit_tree(&mut self, _entry: &EntryRef<'_>) -> Action {
+ Action::Continue
+ }
+
+ fn visit_nontree(&mut self, entry: &EntryRef<'_>) -> Action {
+ let entry = entry.attach(self.repository);
+
+ let Ok(object) = entry.object() else {
+ return Action::Continue;
+ };
+
+ if object.kind != Kind::Blob {
+ return Action::Continue;
+ }
+
+ let blob = object.into_blob();
+
+ let mut header = tar::Header::new_gnu();
+ if let Err(error) = header.set_path(self.path.to_path_lossy()) {
+ warn!(%error, "Attempted to write invalid path to archive");
+ return Action::Continue;
+ }
+ header.set_size(blob.data.len() as u64);
+ #[allow(clippy::cast_sign_loss)]
+ header.set_mode(entry.mode().0.into());
+ header.set_cksum();
+
+ if let Err(error) = self.archive.append(&header, blob.data.as_slice()) {
+ warn!(%error, "Failed to append to archive");
+ return Action::Cancel;
+ }
+
+ if self.archive.get_ref().get_ref().get_ref().len() >= BUFFER_CAP {
+ let b = self.archive.get_mut().get_mut().get_mut().split().freeze();
+
+ if self.res.blocking_send(Ok(b)).is_err() {
+ return Action::Cancel;
+ }
+ }
+
+ Action::Continue
+ }
+}
+
fn parse_and_transform_markdown(s: &str, syntax_set: &SyntaxSet) -> String {
let mut plugins = ComrakPlugins::default();
@@ -473,20 +564,21 @@ pub enum TreeItem {
#[derive(Debug)]
pub struct Tree {
- pub mode: i32,
+ pub mode: u16,
pub name: String,
pub path: PathBuf,
}
#[derive(Debug)]
pub struct File {
- pub mode: i32,
+ pub mode: u16,
pub size: usize,
pub name: String,
pub path: PathBuf,
}
#[derive(Debug)]
+#[allow(unused)]
pub struct FileWithContent {
pub metadata: File,
pub content: Content,
@@ -544,14 +636,15 @@ pub struct CommitUser {
time: OffsetDateTime,
}
-impl TryFrom<Signature<'_>> for CommitUser {
+impl TryFrom<SignatureRef<'_>> for CommitUser {
type Error = anyhow::Error;
- fn try_from(v: Signature<'_>) -> Result<Self> {
+ fn try_from(v: SignatureRef<'_>) -> Result<Self> {
Ok(CommitUser {
- name: String::from_utf8_lossy(v.name_bytes()).into_owned(),
- email: String::from_utf8_lossy(v.email_bytes()).into_owned(),
- time: OffsetDateTime::from_unix_timestamp(v.when().seconds())?,
+ name: v.name.to_string(),
+ email: v.email.to_string(),
+ time: OffsetDateTime::from_unix_timestamp(v.time.seconds)?
+ .to_offset(UtcOffset::from_whole_seconds(v.time.offset)?),
})
}
}
@@ -581,30 +674,24 @@ pub struct Commit {
body: String,
pub diff_stats: String,
pub diff: String,
- pub diff_plain: Bytes,
}
-impl TryFrom<git2::Commit<'_>> for Commit {
+impl TryFrom<gix::Commit<'_>> for Commit {
type Error = anyhow::Error;
- fn try_from(commit: git2::Commit<'_>) -> Result<Self> {
+ fn try_from(commit: gix::Commit<'_>) -> Result<Self> {
+ let message = commit.message()?;
+
Ok(Commit {
- author: CommitUser::try_from(commit.author())?,
- committer: CommitUser::try_from(commit.committer())?,
+ author: CommitUser::try_from(commit.author()?)?,
+ committer: CommitUser::try_from(commit.committer()?)?,
oid: commit.id().to_string(),
- tree: commit.tree_id().to_string(),
+ tree: commit.tree_id()?.to_string(),
parents: commit.parent_ids().map(|v| v.to_string()).collect(),
- summary: commit
- .summary_bytes()
- .map_or_else(|| Cow::Borrowed(""), String::from_utf8_lossy)
- .into_owned(),
- body: commit
- .body_bytes()
- .map_or_else(|| Cow::Borrowed(""), String::from_utf8_lossy)
- .into_owned(),
+ summary: message.summary().to_string(),
+ body: message.body.map_or_else(String::new, ToString::to_string),
diff_stats: String::with_capacity(0),
diff: String::with_capacity(0),
- diff_plain: Bytes::new(),
})
}
}
@@ -641,42 +728,134 @@ impl Commit {
#[instrument(skip(repo, commit, syntax_set))]
fn fetch_diff_and_stats(
- repo: &git2::Repository,
- commit: &git2::Commit<'_>,
- syntax_set: &SyntaxSet,
-) -> Result<(Bytes, String, String)> {
+ repo: &gix::Repository,
+ commit: &gix::Commit<'_>,
+ syntax_set: Option<&SyntaxSet>,
+) -> Result<(String, String)> {
+ const WIDTH: usize = 80;
+
let current_tree = commit.tree().context("Couldn't get tree for the commit")?;
- let parent_tree = commit.parents().next().and_then(|v| v.tree().ok());
- let mut diff_opts = DiffOptions::new();
- let diff = repo.diff_tree_to_tree(
- parent_tree.as_ref(),
- Some(&current_tree),
- Some(&mut diff_opts),
+ let parent_tree = commit
+ .ancestors()
+ .first_parent_only()
+ .all()?
+ .nth(1)
+ .transpose()?
+ .map(|v| v.object())
+ .transpose()?
+ .map(|v| v.tree())
+ .transpose()?
+ .unwrap_or_else(|| repo.empty_tree());
+
+ let mut diffs = BTreeMap::<_, FileDiff>::new();
+ let mut diff_output = String::new();
+
+ let mut resource_cache = repo.diff_resource_cache_for_tree_diff()?;
+
+ let mut changes = parent_tree.changes()?;
+ changes.track_path().track_rewrites(None);
+ changes.for_each_to_obtain_tree_with_cache(
+ &current_tree,
+ &mut repo.diff_resource_cache_for_tree_diff()?,
+ |change| {
+ if let Some(syntax_set) = syntax_set {
+ DiffBuilder {
+ output: &mut diff_output,
+ resource_cache: &mut resource_cache,
+ diffs: &mut diffs,
+ formatter: SyntaxHighlightedDiffFormatter::new(
+ change.location.to_path().unwrap(),
+ syntax_set,
+ ),
+ }
+ .handle(change)
+ } else {
+ DiffBuilder {
+ output: &mut diff_output,
+ resource_cache: &mut resource_cache,
+ diffs: &mut diffs,
+ formatter: PlainDiffFormatter,
+ }
+ .handle(change)
+ }
+ },
)?;
- let mut diff_plain = BytesMut::new();
- let email = Email::from_diff(
- &diff,
- 1,
- 1,
- &commit.id(),
- commit.summary().unwrap_or(""),
- commit.body().unwrap_or(""),
- &commit.author(),
- &mut EmailCreateOptions::default(),
- )
- .context("Couldn't build diff for commit")?;
- diff_plain.extend_from_slice(email.as_slice());
-
- let diff_stats = diff
- .stats()?
- .to_buf(DiffStatsFormat::FULL, 80)?
- .as_str()
- .unwrap_or("")
- .to_string();
- let diff_output = format_diff(&diff, syntax_set)?;
-
- Ok((diff_plain.freeze(), diff_output, diff_stats))
+ let (max_file_name_length, max_change_length, files_changed, insertions, deletions) =
+ diffs.iter().fold(
+ (0, 0, 0, 0, 0),
+ |(max_file_name_length, max_change_length, files_changed, insertions, deletions),
+ (f, stats)| {
+ (
+ max_file_name_length.max(f.len()),
+ max_change_length
+ .max(((stats.insertions + stats.deletions).ilog10() + 1) as usize),
+ files_changed + 1,
+ insertions + stats.insertions,
+ deletions + stats.deletions,
+ )
+ },
+ );
+
+ let mut diff_stats = String::new();
+
+ let total_changes = insertions + deletions;
+
+ for (file, diff) in &diffs {
+ let local_changes = diff.insertions + diff.deletions;
+ let width = WIDTH.min(local_changes);
+
+ // Calculate proportions of `+` and `-` within the total width
+ let addition_width = (width * diff.insertions) / total_changes;
+ let deletion_width = (width * diff.deletions) / total_changes;
+
+ // Handle edge case where total width is less than total changes
+ let remaining_width = width - (addition_width + deletion_width);
+ let adjusted_addition_width = addition_width + remaining_width.min(diff.insertions);
+ let adjusted_deletion_width =
+ deletion_width + (remaining_width - remaining_width.min(diff.insertions));
+
+ // Generate the string representation
+ let plus_str = "+".repeat(adjusted_addition_width);
+ let minus_str = "-".repeat(adjusted_deletion_width);
+
+ writeln!(diff_stats, " {file:max_file_name_length$} | {local_changes:max_change_length$} {plus_str}{minus_str}").unwrap();
+ }
+
+ for (i, (singular_desc, plural_desc, amount)) in [
+ ("file changed", "files changed", files_changed),
+ ("insertion(+)", "insertions(+)", insertions),
+ ("deletion(-)", "deletions(-)", deletions),
+ ]
+ .into_iter()
+ .enumerate()
+ {
+ if amount == 0 {
+ continue;
+ }
+
+ let prefix = if i == 0 { "" } else { "," };
+
+ let desc = if amount == 1 {
+ singular_desc
+ } else {
+ plural_desc
+ };
+
+ write!(diff_stats, "{prefix} {amount} {desc}")?;
+ }
+
+ // TODO: emit 'create mode 100644 pure-black-background-f82588d3.jpg' here
+
+ writeln!(diff_stats)?;
+
+ Ok((diff_output, diff_stats))
+}
+
+#[derive(Default, Debug)]
+struct FileDiff {
+ insertions: usize,
+ deletions: usize,
}
fn format_file(content: &str, extension: &str, syntax_set: &SyntaxSet) -> Result<String> {
@@ -834,48 +1013,256 @@ impl<'a> fmt::Display for Escape<'a> {
}
}
-#[instrument(skip(diff, syntax_set))]
-fn format_diff(diff: &git2::Diff<'_>, syntax_set: &SyntaxSet) -> Result<String> {
- let mut diff_output = String::new();
+trait DiffFormatter {
+ fn file_header(&self, output: &mut String, data: fmt::Arguments<'_>);
+
+ fn binary(
+ &self,
+ output: &mut String,
+ left: &str,
+ right: &str,
+ left_content: &[u8],
+ right_content: &[u8],
+ );
+}
+
+struct DiffBuilder<'a, F> {
+ output: &'a mut String,
+ resource_cache: &'a mut gix::diff::blob::Platform,
+ diffs: &'a mut BTreeMap<String, FileDiff>,
+ formatter: F,
+}
+
+impl<'a, F: DiffFormatter + Callback> DiffBuilder<'a, F> {
+ fn handle(
+ &mut self,
+ change: gix::object::tree::diff::Change<'_, '_, '_>,
+ ) -> Result<gix::object::tree::diff::Action> {
+ if !change.event.entry_mode().is_blob_or_symlink() {
+ return Ok(gix::object::tree::diff::Action::Continue);
+ }
- diff.print(DiffFormat::Patch, |delta, _diff_hunk, diff_line| {
- let (class, should_highlight_as_source) = match diff_line.origin_value() {
- DiffLineType::Addition => (Some("add-line"), true),
- DiffLineType::Deletion => (Some("remove-line"), true),
- DiffLineType::Context => (Some("context"), true),
- DiffLineType::AddEOFNL => (Some("remove-line"), false),
- DiffLineType::DeleteEOFNL => (Some("add-line"), false),
- DiffLineType::FileHeader => (Some("file-header"), false),
- _ => (None, false),
+ let diff = self.diffs.entry(change.location.to_string()).or_default();
+ let change = change.diff(self.resource_cache)?;
+
+ let prep = change.resource_cache.prepare_diff()?;
+
+ self.formatter.file_header(
+ self.output,
+ format_args!(
+ "diff --git a/{} b/{}",
+ prep.old.rela_path, prep.new.rela_path
+ ),
+ );
+
+ if prep.old.id.is_null() {
+ self.formatter.file_header(
+ self.output,
+ format_args!("new file mode {}", prep.new.mode.as_octal_str()),
+ );
+ } else if prep.new.id.is_null() {
+ self.formatter.file_header(
+ self.output,
+ format_args!("deleted file mode {}", prep.old.mode.as_octal_str()),
+ );
+ } else if prep.new.mode != prep.old.mode {
+ self.formatter.file_header(
+ self.output,
+ format_args!("old mode {}", prep.old.mode.as_octal_str()),
+ );
+ self.formatter.file_header(
+ self.output,
+ format_args!("new mode {}", prep.new.mode.as_octal_str()),
+ );
+ }
+
+ // copy from
+ // copy to
+ // rename old
+ // rename new
+ // rename from
+ // rename to
+ // similarity index
+ // dissimilarity index
+
+ let (index_suffix_sep, index_suffix) = if prep.old.mode == prep.new.mode {
+ (" ", prep.new.mode.as_octal_str())
+ } else {
+ ("", BStr::new(&[]))
};
- let line = String::from_utf8_lossy(diff_line.content());
+ let old_path = if prep.old.id.is_null() {
+ Cow::Borrowed("/dev/null")
+ } else {
+ Cow::Owned(format!("a/{}", prep.old.rela_path))
+ };
- let extension = if should_highlight_as_source {
- if let Some(path) = delta.new_file().path() {
- path.extension()
- .or_else(|| path.file_name())
- .map_or_else(|| Cow::Borrowed(""), OsStr::to_string_lossy)
- } else {
- Cow::Borrowed("")
- }
+ let new_path = if prep.new.id.is_null() {
+ Cow::Borrowed("/dev/null")
} else {
- Cow::Borrowed("patch")
+ Cow::Owned(format!("a/{}", prep.new.rela_path))
};
- if let Some(class) = class {
- let _ = write!(diff_output, r#"<span class="diff-{class}">"#);
+ match prep.operation {
+ Operation::InternalDiff { algorithm } => {
+ self.formatter.file_header(
+ self.output,
+ format_args!(
+ "index {}..{}{index_suffix_sep}{index_suffix}",
+ prep.old.id.to_hex_with_len(7),
+ prep.new.id.to_hex_with_len(7)
+ ),
+ );
+ self.formatter
+ .file_header(self.output, format_args!("--- {old_path}"));
+ self.formatter
+ .file_header(self.output, format_args!("+++ {new_path}"));
+
+ let old_source = gix::diff::blob::sources::lines_with_terminator(
+ std::str::from_utf8(prep.old.data.as_slice().unwrap_or_default())?,
+ );
+ let new_source = gix::diff::blob::sources::lines_with_terminator(
+ std::str::from_utf8(prep.new.data.as_slice().unwrap_or_default())?,
+ );
+ let input = gix::diff::blob::intern::InternedInput::new(old_source, new_source);
+
+ let output = gix::diff::blob::diff(
+ algorithm,
+ &input,
+ UnifiedDiffBuilder::with_writer(&input, &mut *self.output, &mut self.formatter)
+ .with_counter(),
+ );
+
+ diff.deletions += output.removals as usize;
+ diff.insertions += output.insertions as usize;
+ }
+ Operation::ExternalCommand { .. } => {}
+ Operation::SourceOrDestinationIsBinary => {
+ self.formatter.file_header(
+ self.output,
+ format_args!(
+ "index {}..{}{index_suffix_sep}{index_suffix}",
+ prep.old.id, prep.new.id,
+ ),
+ );
+
+ self.formatter.binary(
+ self.output,
+ old_path.as_ref(),
+ new_path.as_ref(),
+ prep.old.data.as_slice().unwrap_or_default(),
+ prep.new.data.as_slice().unwrap_or_default(),
+ );
+ }
}
- let _res = format_file_inner(&mut diff_output, &line, &extension, syntax_set, false);
+ self.resource_cache.clear_resource_cache_keep_allocation();
+ Ok(gix::object::tree::diff::Action::Continue)
+ }
+}
+
+struct PlainDiffFormatter;
+
+impl DiffFormatter for PlainDiffFormatter {
+ fn file_header(&self, output: &mut String, data: fmt::Arguments<'_>) {
+ writeln!(output, "{data}").unwrap();
+ }
+
+ fn binary(
+ &self,
+ output: &mut String,
+ left: &str,
+ right: &str,
+ _left_content: &[u8],
+ _right_content: &[u8],
+ ) {
+ // todo: actually perform the diff and write a `GIT binary patch` out
+ writeln!(output, "Binary files {left} and {right} differ").unwrap();
+ }
+}
+
+impl Callback for PlainDiffFormatter {
+ fn addition(&mut self, data: &str, dst: &mut String) {
+ write!(dst, "+{data}").unwrap();
+ }
+
+ fn remove(&mut self, data: &str, dst: &mut String) {
+ write!(dst, "-{data}").unwrap();
+ }
+
+ fn context(&mut self, data: &str, dst: &mut String) {
+ write!(dst, " {data}").unwrap();
+ }
+}
+
+struct SyntaxHighlightedDiffFormatter<'a> {
+ syntax_set: &'a SyntaxSet,
+ extension: Cow<'a, str>,
+}
- if class.is_some() {
- diff_output.push_str("</span>");
+impl<'a> SyntaxHighlightedDiffFormatter<'a> {
+ fn new(path: &'a Path, syntax_set: &'a SyntaxSet) -> Self {
+ let extension = path
+ .extension()
+ .or_else(|| path.file_name())
+ .map_or_else(|| Cow::Borrowed(""), OsStr::to_string_lossy);
+
+ Self {
+ syntax_set,
+ extension,
}
+ }
- true
- })
- .context("Failed to prepare diff")?;
+ fn write(&self, output: &mut String, class: &str, data: &str) {
+ write!(output, r#"<span class="diff-{class}">"#).unwrap();
+ format_file_inner(
+ output,
+ data,
+ self.extension.as_ref(),
+ self.syntax_set,
+ false,
+ )
+ .unwrap();
+ write!(output, r#"</span>"#).unwrap();
+ }
+}
- Ok(diff_output)
+impl<'a> DiffFormatter for SyntaxHighlightedDiffFormatter<'a> {
+ fn file_header(&self, output: &mut String, data: Arguments<'_>) {
+ write!(output, r#"<span class="diff-file-header">"#).unwrap();
+ format_file_inner(output, &data.to_string(), "patch", self.syntax_set, false).unwrap();
+ writeln!(output, r#"</span>"#).unwrap();
+ }
+
+ fn binary(
+ &self,
+ output: &mut String,
+ left: &str,
+ right: &str,
+ _left_content: &[u8],
+ _right_content: &[u8],
+ ) {
+ format_file_inner(
+ output,
+ &format!("Binary files {left} and {right} differ"),
+ "patch",
+ self.syntax_set,
+ false,
+ )
+ .unwrap();
+ }
+}
+
+impl<'a> Callback for SyntaxHighlightedDiffFormatter<'a> {
+ fn addition(&mut self, data: &str, dst: &mut String) {
+ self.write(dst, "add-line", data);
+ }
+
+ fn remove(&mut self, data: &str, dst: &mut String) {
+ self.write(dst, "remote-line", data);
+ }
+
+ fn context(&mut self, data: &str, dst: &mut String) {
+ self.write(dst, "context", data);
+ }
}
diff --git a/src/main.rs b/src/main.rs
index 05dc0f7..d192f32 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -52,6 +52,7 @@ mod git;
mod layers;
mod methods;
mod syntax_highlight;
+mod unified_diff_builder;
const CRATE_VERSION: &str = clap::crate_version!();
diff --git a/src/methods/filters.rs b/src/methods/filters.rs
index 3ee1329..9d6748b 100644
--- a/src/methods/filters.rs
+++ b/src/methods/filters.rs
@@ -17,8 +17,8 @@ pub fn timeago(s: impl Borrow<time::OffsetDateTime>) -> Result<String, askama::E
.convert((time::OffsetDateTime::now_utc() - *s.borrow()).unsigned_abs()))
}
-pub fn file_perms(s: &i32) -> Result<String, askama::Error> {
- Ok(unix_mode::to_string(s.unsigned_abs()))
+pub fn file_perms(s: &u16) -> Result<String, askama::Error> {
+ Ok(unix_mode::to_string(u32::from(*s)))
}
pub fn hex(s: &[u8]) -> Result<String, askama::Error> {
diff --git a/src/methods/repo/commit.rs b/src/methods/repo/commit.rs
index e089ab7..3c82c5d 100644
--- a/src/methods/repo/commit.rs
+++ b/src/methods/repo/commit.rs
@@ -54,9 +54,9 @@ pub async fn handle(
};
let commit = if let Some(commit) = query.id.as_deref() {
- open_repo.commit(commit).await?
+ open_repo.commit(commit, true).await?
} else {
- Arc::new(open_repo.latest_commit().await?)
+ Arc::new(open_repo.latest_commit(true).await?)
};
Ok(into_response(View {
diff --git a/src/methods/repo/diff.rs b/src/methods/repo/diff.rs
index 0a6a6c6..708f95d 100644
--- a/src/methods/repo/diff.rs
+++ b/src/methods/repo/diff.rs
@@ -1,13 +1,5 @@
use std::sync::Arc;
-use askama::Template;
-use axum::{
- extract::Query,
- http::HeaderValue,
- response::{IntoResponse, Response},
- Extension,
-};
-
use crate::{
git::Commit,
http, into_response,
@@ -17,6 +9,17 @@ use crate::{
},
Git,
};
+use askama::Template;
+use axum::{
+ extract::Query,
+ http::HeaderValue,
+ response::{IntoResponse, Response},
+ Extension,
+};
+use bytes::{BufMut, BytesMut};
+use clap::crate_version;
+use std::fmt::Write;
+use time::format_description::well_known::Rfc2822;
#[derive(Template)]
#[template(path = "repo/diff.html")]
@@ -34,9 +37,9 @@ pub async fn handle(
) -> Result<impl IntoResponse> {
let open_repo = git.repo(repository_path, query.branch.clone()).await?;
let commit = if let Some(commit) = query.id {
- open_repo.commit(&commit).await?
+ open_repo.commit(&commit, true).await?
} else {
- Arc::new(open_repo.latest_commit().await?)
+ Arc::new(open_repo.latest_commit(true).await?)
};
Ok(into_response(View {
@@ -53,9 +56,9 @@ pub async fn handle_plain(
) -> Result<Response> {
let open_repo = git.repo(repository_path, query.branch).await?;
let commit = if let Some(commit) = query.id {
- open_repo.commit(&commit).await?
+ open_repo.commit(&commit, false).await?
} else {
- Arc::new(open_repo.latest_commit().await?)
+ Arc::new(open_repo.latest_commit(false).await?)
};
let headers = [(
@@ -63,5 +66,38 @@ pub async fn handle_plain(
HeaderValue::from_static("text/plain"),
)];
- Ok((headers, commit.diff_plain.clone()).into_response())
+ let mut data = BytesMut::new();
+
+ writeln!(data, "From {} Mon Sep 17 00:00:00 2001", commit.oid()).unwrap();
+ writeln!(
+ data,
+ "From: {} <{}>",
+ commit.author().name(),
+ commit.author().email()
+ )
+ .unwrap();
+
+ write!(data, "Date: ").unwrap();
+ let mut writer = data.writer();
+ commit
+ .author()
+ .time()
+ .format_into(&mut writer, &Rfc2822)
+ .unwrap();
+ let mut data = writer.into_inner();
+ writeln!(data).unwrap();
+
+ writeln!(data, "Subject: [PATCH] {}\n", commit.summary()).unwrap();
+
+ write!(data, "{}", commit.body()).unwrap();
+
+ writeln!(data, "---").unwrap();
+
+ data.extend_from_slice(commit.diff_stats.as_bytes());
+ data.extend_from_slice(b"\n");
+ data.extend_from_slice(commit.diff.as_bytes());
+
+ writeln!(data, "--\nrgit {}", crate_version!()).unwrap();
+
+ Ok((headers, data.freeze()).into_response())
}
diff --git a/src/unified_diff_builder.rs b/src/unified_diff_builder.rs
new file mode 100644
index 0000000..f6f52ab
--- /dev/null
+++ b/src/unified_diff_builder.rs
@@ -0,0 +1,143 @@
+//! Heavily based on [`gix::diff::blob::UnifiedDiffBuilder`] but provides
+//! a callback that can be used for styling the diffs.
+
+use std::fmt::Write;
+use std::ops::Range;
+
+use gix::diff::blob::intern::{InternedInput, Interner, Token};
+use gix::diff::blob::Sink;
+
+pub(crate) trait Callback {
+ fn addition(&mut self, data: &str, dst: &mut String);
+ fn remove(&mut self, data: &str, dst: &mut String);
+ fn context(&mut self, data: &str, dst: &mut String);
+}
+
+impl<C: Callback> Callback for &mut C {
+ fn addition(&mut self, data: &str, dst: &mut String) {
+ (*self).addition(data, dst);
+ }
+
+ fn remove(&mut self, data: &str, dst: &mut String) {
+ (*self).remove(data, dst);
+ }
+
+ fn context(&mut self, data: &str, dst: &mut String) {
+ (*self).context(data, dst);
+ }
+}
+
+/// A [`Sink`] that creates a textual diff
+/// in the format typically output by git or gnu-diff if the `-u` option is used
+pub struct UnifiedDiffBuilder<'a, C, W>
+where
+ C: Callback,
+ W: Write,
+{
+ before: &'a [Token],
+ after: &'a [Token],
+ interner: &'a Interner<&'a str>,
+
+ pos: u32,
+ before_hunk_start: u32,
+ after_hunk_start: u32,
+ before_hunk_len: u32,
+ after_hunk_len: u32,
+
+ callback: C,
+ buffer: String,
+ dst: W,
+}
+
+impl<'a, C, W> UnifiedDiffBuilder<'a, C, W>
+where
+ C: Callback,
+ W: Write,
+{
+ /// Create a new `UnifiedDiffBuilder` for the given `input`,
+ /// that will writes it output to the provided implementation of [`Write`].
+ pub fn with_writer(input: &'a InternedInput<&'a str>, writer: W, callback: C) -> Self {
+ Self {
+ before_hunk_start: 0,
+ after_hunk_start: 0,
+ before_hunk_len: 0,
+ after_hunk_len: 0,
+ buffer: String::with_capacity(8),
+ dst: writer,
+ interner: &input.interner,
+ before: &input.before,
+ after: &input.after,
+ callback,
+ pos: 0,
+ }
+ }
+
+ fn flush(&mut self) {
+ if self.before_hunk_len == 0 && self.after_hunk_len == 0 {
+ return;
+ }
+
+ let end = (self.pos + 3).min(u32::try_from(self.before.len()).unwrap_or(u32::MAX));
+ self.update_pos(end, end);
+
+ writeln!(
+ &mut self.dst,
+ "@@ -{},{} +{},{} @@",
+ self.before_hunk_start + 1,
+ self.before_hunk_len,
+ self.after_hunk_start + 1,
+ self.after_hunk_len,
+ )
+ .unwrap();
+ write!(&mut self.dst, "{}", &self.buffer).unwrap();
+ self.buffer.clear();
+ self.before_hunk_len = 0;
+ self.after_hunk_len = 0;
+ }
+
+ fn update_pos(&mut self, print_to: u32, move_to: u32) {
+ for token in &self.before[self.pos as usize..print_to as usize] {
+ self.callback
+ .context(self.interner[*token], &mut self.buffer);
+ }
+ let len = print_to - self.pos;
+ self.pos = move_to;
+ self.before_hunk_len += len;
+ self.after_hunk_len += len;
+ }
+}
+
+impl<C, W> Sink for UnifiedDiffBuilder<'_, C, W>
+where
+ C: Callback,
+ W: Write,
+{
+ type Out = W;
+
+ fn process_change(&mut self, before: Range<u32>, after: Range<u32>) {
+ if before.start - self.pos > 6 {
+ self.flush();
+ self.pos = before.start - 3;
+ self.before_hunk_start = self.pos;
+ self.after_hunk_start = after.start - 3;
+ }
+ self.update_pos(before.start, before.end);
+ self.before_hunk_len += before.end - before.start;
+ self.after_hunk_len += after.end - after.start;
+
+ for token in &self.before[before.start as usize..before.end as usize] {
+ self.callback
+ .remove(self.interner[*token], &mut self.buffer);
+ }
+
+ for token in &self.after[after.start as usize..after.end as usize] {
+ self.callback
+ .addition(self.interner[*token], &mut self.buffer);
+ }
+ }
+
+ fn finish(mut self) -> Self::Out {
+ self.flush();
+ self.dst
+ }
+}