🏡 index : ~doyle/rgit.git

author Jordan Doyle <jordan@doyle.la> 2024-09-27 2:39:25.0 +04:00:00
committer Jordan Doyle <jordan@doyle.la> 2024-09-27 3:18:09.0 +04:00:00
commit
b133174174139fd60ace00fe0d21dc03ca267b05 [patch]
tree
4c16abb1fb0d2a35f799d66cae2a08e68530aa7b
parent
9245909d716bdfc3c54c35839f4009df7e943f9c
download
b133174174139fd60ace00fe0d21dc03ca267b05.tar.gz

Move from libgit2 to gitoxide



Diff

 Cargo.lock                    | 1270 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
 Cargo.toml                    |    2 +-
 README.md                     |    6 +++---
 flake.nix                     |    3 +--
 src/git.rs                    |  875 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
 src/main.rs                   |    1 +
 src/unified_diff_builder.rs   |  143 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 doc/man/rgit.1.md             |    2 +-
 src/database/indexer.rs       |  114 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
 src/methods/filters.rs        |    4 ++--
 src/database/schema/commit.rs |   56 ++++++++++++++++++++++++++++++--------------------------
 src/database/schema/tag.rs    |   10 +++++-----
 src/methods/repo/commit.rs    |    4 ++--
 src/methods/repo/diff.rs      |   62 ++++++++++++++++++++++++++++++++++++++++++++++++++------------
 14 files changed, 2064 insertions(+), 488 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index 5e9beef..25f3fb0 100644
--- a/Cargo.lock
+++ a/Cargo.lock
@@ -18,6 +18,19 @@
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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -25,6 +38,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"
@@ -95,6 +114,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"
@@ -435,6 +460,7 @@
checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706"
dependencies = [
 "memchr",
 "regex-automata 0.4.6",
 "serde",
]

@@ -568,6 +594,12 @@
 "serde",
 "winapi",
]

[[package]]
name = "clru"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbd0f76e066e64fdc5631e3bb46381254deab9ef1158292f27c8c57e3bf3fe59"

[[package]]
name = "colorchoice"
@@ -775,6 +807,20 @@
 "darling_core",
 "quote",
 "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]]
@@ -842,6 +888,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"
@@ -927,6 +979,12 @@
 "bit-set",
 "regex",
]

[[package]]
name = "faster-hex"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2a2b11eda1d40935b26cf18f6833c526845ae8c41e58d09af6adeb6f0269183"

[[package]]
name = "fastrand"
@@ -981,125 +1039,913 @@
 "futures-core",
 "futures-executor",
 "futures-io",
 "futures-sink",
 "futures-task",
 "futures-util",
]

[[package]]
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 = "futures-channel"
version = "0.3.30"
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 @@
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
dependencies = [
 "ahash",
 "allocator-api2",
]

[[package]]
name = "hdrhistogram"
@@ -1265,6 +2115,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"
@@ -1366,6 +2222,16 @@
dependencies = [
 "unicode-bidi",
 "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]]
@@ -1386,6 +2252,16 @@
dependencies = [
 "equivalent",
 "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]]
@@ -1408,6 +2284,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"
@@ -1425,6 +2326,15 @@
checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d"
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]]
@@ -1444,20 +2354,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"
@@ -1466,7 +2362,7 @@
checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19"
dependencies = [
 "cfg-if",
 "windows-targets 0.52.5",
 "windows-targets 0.48.5",
]

[[package]]
@@ -1485,22 +2381,8 @@
 "bzip2-sys",
 "cc",
 "glob",
 "libc",
 "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]]
@@ -1510,7 +2392,6 @@
checksum = "5e143b5e666b2695d28f6bca6497720813f699c9602dd7f5cac91008b8ada7f9"
dependencies = [
 "cc",
 "libc",
 "pkg-config",
 "vcpkg",
]
@@ -1575,6 +2456,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"
@@ -1758,27 +2648,9 @@
version = "69.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b829e3d7e9cc74c7e315ee8edb185bf4190da5acde74afd7fc59c35b1f086e7"
dependencies = [
 "cc",
 "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]]
@@ -1823,7 +2695,7 @@
 "libc",
 "redox_syscall 0.5.1",
 "smallvec",
 "windows-targets 0.52.5",
 "windows-targets 0.52.6",
]

[[package]]
@@ -1898,6 +2770,12 @@
 "serde",
 "time",
]

[[package]]
name = "portable-atomic"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d30538d42559de6b034bc76fd6dd4c38961b1ee5c6c56e3808c50128fdbc22ce"

[[package]]
name = "powerfmt"
@@ -1918,6 +2796,16 @@
checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b"
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]]
@@ -2111,7 +2999,7 @@
 "console-subscriber",
 "flate2",
 "futures",
 "git2",
 "gix",
 "hex",
 "httparse",
 "humantime",
@@ -2315,6 +3203,12 @@
 "serde",
 "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"
@@ -2347,6 +3241,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"
@@ -2399,6 +3303,12 @@
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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2499,6 +3409,19 @@
 "filetime",
 "libc",
 "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]]
@@ -2829,6 +3752,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"
@@ -2844,6 +3776,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"
@@ -2886,9 +3824,9 @@

[[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",
@@ -3062,8 +4000,17 @@
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.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
 "windows-targets 0.52.5",
 "windows-targets 0.52.6",
]

[[package]]
@@ -3083,18 +4030,18 @@

[[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 @@

[[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 @@

[[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 @@

[[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 @@

[[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 @@

[[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 @@

[[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 @@

[[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"
@@ -3235,6 +4191,26 @@
 "quote",
 "syn 2.0.63",
 "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]]
diff --git a/Cargo.toml b/Cargo.toml
index 6d4a926..a8788e8 100644
--- a/Cargo.toml
+++ a/Cargo.toml
@@ -20,7 +20,7 @@
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
+++ a/README.md
@@ -1,10 +1,10 @@
# rgit

## Introduction

[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 @@
  [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/flake.nix b/flake.nix
index d260678..806e941 100644
--- a/flake.nix
+++ a/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/git.rs b/src/git.rs
index fe0dda4..9e91187 100644
--- a/src/git.rs
+++ a/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 @@
        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 @@
            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 @@
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 @@
        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 @@
                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);

                if let Some(blob) = object.as_blob() {
                    tree_items.push(TreeItem::File(File {
                        mode: item.filemode(),
                        size: blob.size(),
                let path = path
                    .clone()
                    .unwrap_or_default()
                    .join(item.filename().to_path_lossy());

                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 @@
                .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 @@
                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 @@
        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 @@
        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()?
            };
@@ -361,42 +371,24 @@
            if cont.send(()).is_err() {
                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 @@
    }

    #[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))
                })
@@ -433,6 +432,98 @@
                .context("Failed to join Tokio task")?
            })
            .await
    }
}

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
    }
}

@@ -473,20 +564,21 @@

#[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 @@
    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 @@
    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 @@

#[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 (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 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 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 @@
    }
}

#[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);
        }

        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()),
            );
        }

    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),
        // 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(),
                );
            }
        }

        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>,
}

        let _res = format_file_inner(&mut diff_output, &line, &extension, syntax_set, false);
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);

        if class.is_some() {
            diff_output.push_str("</span>");
        Self {
            syntax_set,
            extension,
        }
    }

    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();
    }
}

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);
    }

        true
    })
    .context("Failed to prepare diff")?;
    fn remove(&mut self, data: &str, dst: &mut String) {
        self.write(dst, "remote-line", data);
    }

    Ok(diff_output)
    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
+++ a/src/main.rs
@@ -52,6 +52,7 @@
mod layers;
mod methods;
mod syntax_highlight;
mod unified_diff_builder;

const CRATE_VERSION: &str = clap::crate_version!();

diff --git a/src/unified_diff_builder.rs b/src/unified_diff_builder.rs
new file mode 100644
index 0000000..f6f52ab 100644
--- /dev/null
+++ a/src/unified_diff_builder.rs
@@ -1,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
    }
}
diff --git a/doc/man/rgit.1.md b/doc/man/rgit.1.md
index 4b5b657..f0270b3 100644
--- a/doc/man/rgit.1.md
+++ a/doc/man/rgit.1.md
@@ -15,7 +15,7 @@
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/src/database/indexer.rs b/src/database/indexer.rs
index 3469009..8ed543a 100644
--- a/src/database/indexer.rs
+++ a/src/database/indexer.rs
@@ -8,11 +8,13 @@
};

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 @@

        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 @@
            }
        };

        git_repository.object_cache_size(10 * 1024 * 1024);

        let res = Repository {
            id,
            name,
@@ -97,21 +101,25 @@
    }
}

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;
@@ -138,6 +146,14 @@
        };

        let references = match git_repository.references() {
            Ok(v) => v,
            Err(error) => {
                error!(%error, "Failed to read references for {relative_path}");
                continue;
            }
        };

        let references = match references.all() {
            Ok(v) => v,
            Err(error) => {
                error!(%error, "Failed to read references for {relative_path}");
@@ -147,26 +163,34 @@

        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 @@

#[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 @@
        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 @@
            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 @@
                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 @@

        return branch_index_update(
            reference,
            reference_name,
            relative_path,
            db_repository,
            db,
@@ -299,16 +325,17 @@
    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 @@
#[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 @@
    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/methods/filters.rs b/src/methods/filters.rs
index 3ee1329..9d6748b 100644
--- a/src/methods/filters.rs
+++ a/src/methods/filters.rs
@@ -17,8 +17,8 @@
        .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/database/schema/commit.rs b/src/database/schema/commit.rs
index 98b56a8..87b065e 100644
--- a/src/database/schema/commit.rs
+++ a/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 @@

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 @@

#[derive(Debug)]
pub enum CommitHash<'a> {
    Oid(Oid),
    Oid(ObjectId),
    Bytes(&'a [u8]),
}

@@ -97,14 +99,16 @@
    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
+++ a/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 @@
}

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/methods/repo/commit.rs b/src/methods/repo/commit.rs
index e089ab7..3c82c5d 100644
--- a/src/methods/repo/commit.rs
+++ a/src/methods/repo/commit.rs
@@ -54,9 +54,9 @@
    };

    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
+++ a/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,
@@ -16,7 +8,18 @@
        repo::{commit::UriQuery, Repository, RepositoryPath, Result},
    },
    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 @@
) -> 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,15 +56,48 @@
) -> 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 = [(
        http::header::CONTENT_TYPE,
        HeaderValue::from_static("text/plain"),
    )];

    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, commit.diff_plain.clone()).into_response())
    Ok((headers, data.freeze()).into_response())
}