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