From 31f6426eefbfcd8ddf3f81b1a1dccac82a5a067d Mon Sep 17 00:00:00 2001 From: Georg Bauer Date: Tue, 28 Jul 2026 18:25:24 +0200 Subject: [PATCH] Replace gix with configurable libgit2 diffs --- Cargo.lock | 1136 +---------------------------------- Cargo.toml | 1 - src/app.rs | 40 +- src/app/git.rs | 308 +++++----- src/app/preferences.rs | 38 ++ src/app/projects.rs | 32 +- src/app/view.rs | 1 + src/app/view/preferences.rs | 60 ++ src/config.rs | 89 ++- 9 files changed, 410 insertions(+), 1295 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 103d770..11e94cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -46,12 +46,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - [[package]] name = "android-activity" version = "0.6.1" @@ -95,15 +89,6 @@ dependencies = [ "libc", ] -[[package]] -name = "arc-swap" -version = "1.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" -dependencies = [ - "rustversion", -] - [[package]] name = "arrayref" version = "0.3.9" @@ -356,21 +341,6 @@ dependencies = [ "serde", ] -[[package]] -name = "bisync" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5020822f6d6f23196ccaf55e228db36f9de1cf788052b37992e17cbc96ec41a7" -dependencies = [ - "bisync_macros", -] - -[[package]] -name = "bisync_macros" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d21f40d350a700f6aa107e45fb26448cf489d34794b2ba4522181dc9f1173af6" - [[package]] name = "bit-set" version = "0.8.0" @@ -407,15 +377,6 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - [[package]] name = "block-buffer" version = "0.12.1" @@ -456,17 +417,6 @@ dependencies = [ "piper", ] -[[package]] -name = "bstr" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f7dc094d718f2e1c1559ad110e27eeaae14a5465d3d56dd6dbd793079fbd530" -dependencies = [ - "memchr", - "regex-automata", - "serde_core", -] - [[package]] name = "bumpalo" version = "3.20.3" @@ -493,12 +443,6 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - [[package]] name = "byteorder-lite" version = "0.1.0" @@ -674,15 +618,6 @@ dependencies = [ "x11rb", ] -[[package]] -name = "clru" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "197fd99cb113a8d5d9b6376f3aa817f32c1078f2343b714fff7d2ca44fdf67d5" -dependencies = [ - "hashbrown 0.16.1", -] - [[package]] name = "codespan-reporting" version = "0.12.0" @@ -819,15 +754,6 @@ dependencies = [ "unicode-segmentation", ] -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - [[package]] name = "cpufeatures" version = "0.3.0" @@ -880,16 +806,6 @@ dependencies = [ "wgpu", ] -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "typenum", -] - [[package]] name = "crypto-common" version = "0.2.2" @@ -949,57 +865,12 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "dashmap" -version = "6.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c" -dependencies = [ - "cfg-if", - "crossbeam-utils", - "hashbrown 0.14.5", - "lock_api", - "once_cell", - "parking_lot_core", -] - [[package]] name = "data-url" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" -[[package]] -name = "defmt" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" -dependencies = [ - "bitflags 1.3.2", - "defmt-macros", -] - -[[package]] -name = "defmt-macros" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" -dependencies = [ - "defmt-parser", - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "defmt-parser" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" -dependencies = [ - "thiserror 2.0.19", -] - [[package]] name = "deranged" version = "0.5.8" @@ -1052,25 +923,15 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer 0.10.4", - "crypto-common 0.1.7", -] - [[package]] name = "digest" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ - "block-buffer 0.12.1", + "block-buffer", "const-oid", - "crypto-common 0.2.2", + "crypto-common", ] [[package]] @@ -1167,7 +1028,6 @@ dependencies = [ "diesel", "diesel_migrations", "git2", - "gix", "iced", "image", "memmap2", @@ -1205,27 +1065,12 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edf234dd1594d6dd434a8fb8cada51ddbbc593e40e4a01556a0b31c62da2775b" -[[package]] -name = "dunce" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" - [[package]] name = "either" version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" -[[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] - [[package]] name = "endi" version = "1.1.1" @@ -1326,16 +1171,6 @@ dependencies = [ "regex-syntax", ] -[[package]] -name = "faster-hex" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7223ae2d2f179b803433d9c830478527e92b8117eab39460edae7f1614d9fb73" -dependencies = [ - "heapless", - "serde", -] - [[package]] name = "fastrand" version = "2.5.0" @@ -1361,16 +1196,6 @@ dependencies = [ "rustc_version", ] -[[package]] -name = "filetime" -version = "0.2.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" -dependencies = [ - "cfg-if", - "libc", -] - [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -1647,16 +1472,6 @@ dependencies = [ "system-deps", ] -[[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 = "gethostname" version = "1.1.0" @@ -1777,792 +1592,6 @@ dependencies = [ "url", ] -[[package]] -name = "gix" -version = "0.86.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb3790fd8981cba7949f1ba924ef865d902df731627bc5998d14164063892fce" -dependencies = [ - "gix-actor", - "gix-attributes", - "gix-command", - "gix-commitgraph", - "gix-config", - "gix-date", - "gix-diff", - "gix-dir", - "gix-discover", - "gix-error", - "gix-features", - "gix-filter", - "gix-fs", - "gix-glob", - "gix-hash", - "gix-hashtable", - "gix-ignore", - "gix-index", - "gix-lock", - "gix-object", - "gix-odb", - "gix-pack", - "gix-path", - "gix-pathspec", - "gix-protocol", - "gix-ref", - "gix-refspec", - "gix-revision", - "gix-revwalk", - "gix-sec", - "gix-shallow", - "gix-status", - "gix-submodule", - "gix-tempfile", - "gix-trace", - "gix-traverse", - "gix-url", - "gix-utils", - "gix-validate", - "gix-worktree", - "gix-worktree-stream", - "gix-zlib", - "nonempty", - "smallvec", - "thiserror 2.0.19", -] - -[[package]] -name = "gix-actor" -version = "0.41.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33f9308ad6fd35b2a865cbe4117ac61b2be59e4a9ef1621c7a9794f7c8e52c5b" -dependencies = [ - "bstr", - "gix-date", - "gix-error", -] - -[[package]] -name = "gix-attributes" -version = "0.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31c593692ebdc1e38858d9a2b56f6a594c501e24a38971fe6685571f5a07be0" -dependencies = [ - "bstr", - "gix-features", - "gix-glob", - "gix-path", - "gix-quote", - "gix-trace", - "smallvec", - "thiserror 2.0.19", - "unicode-bom", -] - -[[package]] -name = "gix-bitmap" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd1d118d0f5d88b96e6f6e13b566475fef4797ead4a02c26fed36c1375066f7" -dependencies = [ - "gix-error", -] - -[[package]] -name = "gix-chunk" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a871e5cab12ba568845714473505deefffb3c04eb47f4708ce344cd459c1cc" -dependencies = [ - "gix-error", -] - -[[package]] -name = "gix-command" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00706d4fef135ef4b01680d5218c6ee40cda8baf697b864296cbc887d19118f6" -dependencies = [ - "bstr", - "gix-path", - "gix-quote", - "gix-trace", - "shell-words", -] - -[[package]] -name = "gix-commitgraph" -version = "0.38.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2cd7f054ae2727223fe46dd39c012f066b12f532962d336d29ee193261787da" -dependencies = [ - "bstr", - "gix-chunk", - "gix-error", - "gix-hash", - "memmap2", - "nonempty", -] - -[[package]] -name = "gix-config" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "103d11bef95c467577ecfa8b7b86a22e65af3507b2c9bfa3809a4afbae7df301" -dependencies = [ - "bstr", - "gix-config-value", - "gix-features", - "gix-glob", - "gix-path", - "gix-ref", - "gix-sec", - "gix-utils", - "smallvec", - "thiserror 2.0.19", - "unicode-bom", -] - -[[package]] -name = "gix-config-value" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f813e312a3f7f327187823cd4c754a3e4d948c98907d11e8f37554a2b6b8059" -dependencies = [ - "bitflags 2.13.1", - "bstr", - "gix-path", - "libc", - "thiserror 2.0.19", -] - -[[package]] -name = "gix-date" -version = "0.15.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e47b9e8cdc688296609b706428de570f88b1e0eed7156dde7b4a89d26fa4567" -dependencies = [ - "bstr", - "gix-error", - "itoa", - "jiff", -] - -[[package]] -name = "gix-diff" -version = "0.66.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fee7d89a3c507491cdfc57a1d1e0e300214720b4f7709ebc253e422f99822bfc" -dependencies = [ - "bstr", - "gix-attributes", - "gix-command", - "gix-filter", - "gix-fs", - "gix-hash", - "gix-imara-diff", - "gix-index", - "gix-object", - "gix-path", - "gix-pathspec", - "gix-tempfile", - "gix-trace", - "gix-traverse", - "gix-worktree", - "thiserror 2.0.19", -] - -[[package]] -name = "gix-dir" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f24bc78f283946ac64757c8a61ffa71f0230aa1e8d98cbb0771db479871dd5b6" -dependencies = [ - "bstr", - "gix-discover", - "gix-fs", - "gix-ignore", - "gix-index", - "gix-object", - "gix-path", - "gix-pathspec", - "gix-trace", - "gix-utils", - "gix-worktree", - "thiserror 2.0.19", -] - -[[package]] -name = "gix-discover" -version = "0.54.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9f517766fa1101dfe2606c1a19a8ffa699099030995a9194445446dfe261bdf" -dependencies = [ - "bstr", - "dunce", - "gix-fs", - "gix-path", - "gix-ref", - "gix-sec", - "thiserror 2.0.19", -] - -[[package]] -name = "gix-error" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a9292309fd944e71b2a3c96d3c03a6feb8852db646febdde7cbb9f79cb5f329" -dependencies = [ - "bstr", -] - -[[package]] -name = "gix-features" -version = "0.49.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20aa09e83a48dc02c5f5f08578aa79d3ab1bab4618b8c362f88684645a02bdcc" -dependencies = [ - "bytes", - "crc32fast", - "crossbeam-channel", - "gix-path", - "gix-trace", - "gix-utils", - "libc", - "once_cell", - "parking_lot", - "prodash", - "walkdir", -] - -[[package]] -name = "gix-filter" -version = "0.33.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e7b5dbf524d97e839f642930c76d7f011c0791e7d11d8148989ac5af7c76aa8" -dependencies = [ - "bstr", - "encoding_rs", - "gix-attributes", - "gix-command", - "gix-hash", - "gix-object", - "gix-packetline", - "gix-path", - "gix-quote", - "gix-trace", - "gix-utils", - "smallvec", - "thiserror 2.0.19", -] - -[[package]] -name = "gix-fs" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "865cf13fcaf5455220546cb9607c416bd1be9a6caafd143655a362fdeab64e80" -dependencies = [ - "bstr", - "gix-features", - "gix-path", - "gix-utils", - "thiserror 2.0.19", -] - -[[package]] -name = "gix-glob" -version = "0.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "421e92a711554fa5827d1b0599d3389acdd0f6729e97a8c5a57d79af1e50bf36" -dependencies = [ - "bitflags 2.13.1", - "bstr", - "gix-features", - "gix-path", -] - -[[package]] -name = "gix-hash" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13adaa73415fd6c902310923f68d0b98e8cecf14b33ea58c02cc387cee56f54e" -dependencies = [ - "faster-hex", - "gix-features", - "sha1-checked", - "thiserror 2.0.19", -] - -[[package]] -name = "gix-hashtable" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78fccd6fea3bcf0b39c076bae60ae49b08daaf538b950202101a981f9d3c01d3" -dependencies = [ - "gix-hash", - "hashbrown 0.17.1", - "parking_lot", -] - -[[package]] -name = "gix-ignore" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12cff8e8aa125e39377456073e63df3334d9e5741372ddcc226198015076dda2" -dependencies = [ - "bstr", - "gix-glob", - "gix-path", - "gix-trace", - "unicode-bom", -] - -[[package]] -name = "gix-imara-diff" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a791e6620676a875f362f3156ed213e73ca099a09bf992c18812abe65cc37b1" -dependencies = [ - "bstr", - "hashbrown 0.17.1", -] - -[[package]] -name = "gix-index" -version = "0.54.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5009c4e7e9f9b4cfaaab1153e49133eb04d79c015b5702d6c3d2ab94271a89c6" -dependencies = [ - "bitflags 2.13.1", - "bstr", - "filetime", - "fnv", - "gix-bitmap", - "gix-features", - "gix-fs", - "gix-hash", - "gix-lock", - "gix-object", - "gix-traverse", - "gix-utils", - "gix-validate", - "hashbrown 0.17.1", - "itoa", - "libc", - "memmap2", - "rustix 1.1.4", - "smallvec", - "thiserror 2.0.19", -] - -[[package]] -name = "gix-lock" -version = "24.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4c69157820343bf1c6e4b88b9808e920900de02e18aaf5862b30ada43814848" -dependencies = [ - "gix-tempfile", - "gix-utils", - "thiserror 2.0.19", -] - -[[package]] -name = "gix-object" -version = "0.63.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e48c235e7f886eb819fc878af75be889333dd3c38bee02ed7af48ae2cf596c4" -dependencies = [ - "bstr", - "gix-actor", - "gix-date", - "gix-features", - "gix-hash", - "gix-hashtable", - "gix-utils", - "gix-validate", - "itoa", - "smallvec", - "thiserror 2.0.19", -] - -[[package]] -name = "gix-odb" -version = "0.83.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8dd494ffb5037e62b8220109e894d2861ff2150a2cacbfccdba57ae1ebab2b96" -dependencies = [ - "arc-swap", - "gix-features", - "gix-fs", - "gix-hash", - "gix-hashtable", - "gix-object", - "gix-pack", - "gix-path", - "gix-quote", - "gix-zlib", - "memmap2", - "parking_lot", - "tempfile", - "thiserror 2.0.19", -] - -[[package]] -name = "gix-pack" -version = "0.73.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d5446127b269706e85998065267ddd2ccc3550179da6780b22fe496175ccb20" -dependencies = [ - "clru", - "gix-chunk", - "gix-error", - "gix-features", - "gix-hash", - "gix-hashtable", - "gix-object", - "gix-path", - "gix-zlib", - "memmap2", - "smallvec", - "thiserror 2.0.19", - "uluru", -] - -[[package]] -name = "gix-packetline" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3766025c72319c4accdd854a18e6f0dd176c8eb0f3bc8a60a7765be2b50cabf2" -dependencies = [ - "bstr", - "faster-hex", - "gix-trace", - "thiserror 2.0.19", -] - -[[package]] -name = "gix-path" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ed3e8d7a82e886e17a72e03d4ba0c13db6f2219b6cd4e2900b4cae426ec20c9" -dependencies = [ - "bstr", - "gix-trace", - "gix-validate", - "thiserror 2.0.19", -] - -[[package]] -name = "gix-pathspec" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f6fa5f8007f008187c3f60b4373209ca83d1cc947f35ede03e16cd15a4d137" -dependencies = [ - "bitflags 2.13.1", - "bstr", - "gix-attributes", - "gix-config-value", - "gix-glob", - "gix-path", - "thiserror 2.0.19", -] - -[[package]] -name = "gix-protocol" -version = "0.64.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dede40e89c1e90f548415f50636bb051f6d9c60f68b8b710bc07825722d19588" -dependencies = [ - "bisync", - "bstr", - "gix-date", - "gix-features", - "gix-hash", - "gix-ref", - "gix-shallow", - "gix-transport", - "gix-utils", - "nonempty", - "thiserror 2.0.19", -] - -[[package]] -name = "gix-quote" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6e541fc33cc2b783b7979040d445a0c86a2eca747c8faea4ca84230d06ae6ef" -dependencies = [ - "bstr", - "gix-error", - "gix-utils", -] - -[[package]] -name = "gix-ref" -version = "0.66.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eeb0c90a8f6202ceaaa22996cbf837c943ccb2d8af9ff3490f0758305e6b7883" -dependencies = [ - "gix-actor", - "gix-features", - "gix-fs", - "gix-hash", - "gix-lock", - "gix-object", - "gix-path", - "gix-tempfile", - "gix-utils", - "gix-validate", - "memmap2", - "thiserror 2.0.19", -] - -[[package]] -name = "gix-refspec" -version = "0.44.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7406282cc0259b51f6aee299ca3d31279a020530363152a2e6c96e8a7f7bbc83" -dependencies = [ - "bstr", - "gix-error", - "gix-glob", - "gix-hash", - "gix-revision", - "gix-validate", - "smallvec", - "thiserror 2.0.19", -] - -[[package]] -name = "gix-revision" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e55e09d4a1ecf2beecc8c09cafcad37979e805b31f588b0e957e191df5783681" -dependencies = [ - "bstr", - "gix-commitgraph", - "gix-date", - "gix-error", - "gix-hash", - "gix-object", - "gix-revwalk", - "nonempty", -] - -[[package]] -name = "gix-revwalk" -version = "0.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36c113c0a53294dc6280ffc06cbcc4f50f820397e97d6a00b429a44b8db26e29" -dependencies = [ - "gix-commitgraph", - "gix-date", - "gix-error", - "gix-hash", - "gix-hashtable", - "gix-object", - "smallvec", - "thiserror 2.0.19", -] - -[[package]] -name = "gix-sec" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af4fe6c152c1d50aea36f299825702cd37e303307832fec1d0fdd5844e47ce2f" -dependencies = [ - "bitflags 2.13.1", - "gix-path", - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "gix-shallow" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ecc9f4b40537043e4bbd7d3d1760e74fb8e7b07a546166b558acaa73ad97f4a" -dependencies = [ - "bstr", - "gix-hash", - "gix-lock", - "nonempty", - "thiserror 2.0.19", -] - -[[package]] -name = "gix-status" -version = "0.33.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f83b1c74e69b90411fbfe89ccebc47aa49457ce4eb9b942b1311258fc863d22" -dependencies = [ - "bstr", - "filetime", - "gix-diff", - "gix-dir", - "gix-features", - "gix-filter", - "gix-fs", - "gix-hash", - "gix-index", - "gix-object", - "gix-path", - "gix-pathspec", - "gix-worktree", - "hashbrown 0.16.1", - "portable-atomic", - "thiserror 2.0.19", - "windows-sys 0.61.2", -] - -[[package]] -name = "gix-submodule" -version = "0.33.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd98077a56d08886112e6b08dc94076d03539f4bc0b9d7880e4be2b8a640d8c" -dependencies = [ - "bstr", - "gix-config", - "gix-path", - "gix-pathspec", - "gix-refspec", - "gix-url", - "thiserror 2.0.19", -] - -[[package]] -name = "gix-tempfile" -version = "24.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b675b920bd5a61d17ad542772f03ec34c60feb8ff683e1560c03ae967363731e" -dependencies = [ - "dashmap", - "gix-fs", - "libc", - "parking_lot", - "tempfile", -] - -[[package]] -name = "gix-trace" -version = "0.1.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be3eb81d9dc914335923e50d52829c551feefd6a72d176c4130c546b67a60814" - -[[package]] -name = "gix-transport" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c1bcf30081eb8ab04540a5795c67a2fcb22cb2e976e8b1a4ea657b1ed61469" -dependencies = [ - "bstr", - "gix-command", - "gix-features", - "gix-packetline", - "gix-path", - "gix-quote", - "gix-sec", - "gix-url", - "thiserror 2.0.19", -] - -[[package]] -name = "gix-traverse" -version = "0.60.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008c5cd879e46e86b5c2469e633611978b18775d53d05668d691bc13088bd409" -dependencies = [ - "bitflags 2.13.1", - "gix-commitgraph", - "gix-date", - "gix-hash", - "gix-hashtable", - "gix-object", - "gix-revwalk", - "smallvec", - "thiserror 2.0.19", -] - -[[package]] -name = "gix-url" -version = "0.37.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d10e53b8eae21ee601687f47bbbd6cb2ed7162cb4c1cafdd422fb7ec64cbee" -dependencies = [ - "bstr", - "gix-path", - "gix-utils", - "percent-encoding", - "thiserror 2.0.19", -] - -[[package]] -name = "gix-utils" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1795bd2a970ca8b2185318c2abb97d955c71992f1cf28de73ad3b593a9f3ce8" -dependencies = [ - "bstr", - "fastrand", - "getrandom 0.4.3", - "unicode-normalization", -] - -[[package]] -name = "gix-validate" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a034e84d1e04e1b1f20f51f12491da230b6ac8b925d0c8e1b89bcd87a7c5ccc" -dependencies = [ - "bstr", -] - -[[package]] -name = "gix-worktree" -version = "0.55.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31eb8e675122e83585e461fe28f68ff8c5ed55b49017b697e7e76423ff973424" -dependencies = [ - "bstr", - "gix-attributes", - "gix-features", - "gix-fs", - "gix-glob", - "gix-hash", - "gix-ignore", - "gix-index", - "gix-object", - "gix-path", - "gix-validate", -] - -[[package]] -name = "gix-worktree-stream" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b088c8724e7be120c4798dd86925cf05332c9d356a463542578600c50c7a549" -dependencies = [ - "gix-attributes", - "gix-error", - "gix-features", - "gix-filter", - "gix-fs", - "gix-hash", - "gix-object", - "gix-path", - "gix-traverse", - "parking_lot", -] - -[[package]] -name = "gix-zlib" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e8813f5579b3075ff9c90f7c59cd2b62b4ebb639361f0911648b22d7446cc7c" -dependencies = [ - "thiserror 2.0.19", - "zlib-rs", -] - [[package]] name = "gl_generator" version = "0.14.0" @@ -2797,21 +1826,6 @@ dependencies = [ "smallvec", ] -[[package]] -name = "hash32" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" -dependencies = [ - "byteorder", -] - -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" - [[package]] name = "hashbrown" version = "0.15.5" @@ -2827,8 +1841,6 @@ version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ - "allocator-api2", - "equivalent", "foldhash 0.2.0", ] @@ -2837,21 +1849,6 @@ name = "hashbrown" version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash 0.2.0", -] - -[[package]] -name = "heapless" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" -dependencies = [ - "hash32", - "stable_deref_trait", -] [[package]] name = "heck" @@ -3294,59 +2291,6 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" -[[package]] -name = "jiff" -version = "0.2.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc" -dependencies = [ - "defmt", - "jiff-core", - "jiff-static", - "jiff-tzdb-platform", - "log", - "portable-atomic", - "portable-atomic-util", - "serde_core", - "windows-link", -] - -[[package]] -name = "jiff-core" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09" -dependencies = [ - "defmt", -] - -[[package]] -name = "jiff-static" -version = "0.2.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204" -dependencies = [ - "jiff-core", - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "jiff-tzdb" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e" - -[[package]] -name = "jiff-tzdb-platform" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" -dependencies = [ - "jiff-tzdb", -] - [[package]] name = "jni" version = "0.22.4" @@ -3855,12 +2799,6 @@ dependencies = [ "jni-sys 0.3.1", ] -[[package]] -name = "nonempty" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9737e026353e5cd0736f98eddae28665118eb6f6600902a7f50db585621fecb6" - [[package]] name = "num-conv" version = "0.2.2" @@ -4629,15 +3567,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "prodash" -version = "31.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "962200e2d7d551451297d9fdce85138374019ada198e30ea9ede38034e27604c" -dependencies = [ - "parking_lot", -] - [[package]] name = "profiling" version = "1.0.18" @@ -5193,27 +4122,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "sha1" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "digest 0.10.7", -] - -[[package]] -name = "sha1-checked" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89f599ac0c323ebb1c6082821a54962b839832b03984598375bff3975b804423" -dependencies = [ - "digest 0.10.7", - "sha1", -] - [[package]] name = "sha2" version = "0.11.0" @@ -5221,16 +4129,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" dependencies = [ "cfg-if", - "cpufeatures 0.3.0", - "digest 0.11.3", + "cpufeatures", + "digest", ] -[[package]] -name = "shell-words" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" - [[package]] name = "shellexpand" version = "3.1.2" @@ -5989,15 +4891,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "uluru" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c8a2469e56e6e5095c82ccd3afb98dad95f7af7929aab6d8ba8d6e0f73657da" -dependencies = [ - "arrayvec", -] - [[package]] name = "unicase" version = "2.9.0" @@ -6016,12 +4909,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dfa6e8c60bb66d49db113e0125ee8711b7647b5579dc7f5f19c42357ed039fe" -[[package]] -name = "unicode-bom" -version = "2.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eec5d1121208364f6793f7d2e222bf75a915c19557537745b195b253dd64217" - [[package]] name = "unicode-ccc" version = "0.4.0" @@ -6040,15 +4927,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" -[[package]] -name = "unicode-normalization" -version = "0.1.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" -dependencies = [ - "tinyvec", -] - [[package]] name = "unicode-properties" version = "0.1.4" @@ -7297,12 +6175,6 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "zlib-rs" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b142a20ec14a91d5bc708c1dc21b080c550113d8aa77afa29635673a65dd02c5" - [[package]] name = "zmij" version = "1.0.23" diff --git a/Cargo.toml b/Cargo.toml index e497ed3..7121b50 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,6 @@ cc = "1.3.0" [dependencies] diesel = { version = "2.3.11", features = ["sqlite", "returning_clauses_for_sqlite_3_35", "64-column-tables"] } diesel_migrations = "2.3.2" -gix = { version = "0.86.0", default-features = false, features = ["max-performance-safe", "sha1", "status"] } git2 = { version = "0.20.4", features = ["vendored-libgit2", "vendored-openssl"] } iced = { version = "0.14.0", features = ["advanced", "highlighter", "image-without-codecs", "markdown", "svg", "tokio"] } image = { version = "0.25.10", default-features = false, features = ["gif", "jpeg", "png", "webp"] } diff --git a/src/app.rs b/src/app.rs index cbc969d..f63f02c 100644 --- a/src/app.rs +++ b/src/app.rs @@ -14,7 +14,9 @@ use preferences::PreferenceDraft; #[cfg(test)] use preferences::{parse_optional_gib, parse_streaming_cache}; -use crate::config::{Config, DevBrainConfig, EndpointConfig}; +use crate::config::{ + Config, DevBrainConfig, EndpointConfig, GitConfig, GitDiffAlgorithm, GitDiffWhitespace, +}; use crate::database::{Database, ProjectWithSessions, SessionState, StoredMessage}; #[cfg(target_os = "macos")] use crate::engine::ChatTurn; @@ -248,6 +250,7 @@ pub(super) enum PreferenceSection { Model, Endpoint, DevBrain, + Git, Generation, Execution, Acceleration, @@ -256,10 +259,11 @@ pub(super) enum PreferenceSection { } impl PreferenceSection { - const ALL: [Self; 8] = [ + const ALL: [Self; 9] = [ Self::Model, Self::Endpoint, Self::DevBrain, + Self::Git, Self::Generation, Self::Execution, Self::Acceleration, @@ -272,6 +276,7 @@ impl PreferenceSection { Self::Model => "preferences-model", Self::Endpoint => "preferences-endpoint", Self::DevBrain => "preferences-dev-brain", + Self::Git => "preferences-git", Self::Generation => "preferences-generation", Self::Execution => "preferences-execution", Self::Acceleration => "preferences-acceleration", @@ -285,6 +290,7 @@ impl PreferenceSection { Self::Model => "Model & lifecycle", Self::Endpoint => "Local endpoint", Self::DevBrain => "Dev Brain", + Self::Git => "Git diffs", Self::Generation => "Generation", Self::Execution => "Execution", Self::Acceleration => "Acceleration & memory", @@ -344,6 +350,12 @@ pub(crate) enum Message { PreferenceEndpointCorsChanged(bool), PreferenceDevBrainEnabledChanged(bool), PreferenceDevBrainVaultChanged(String), + PreferenceGitDiffAlgorithmChanged(GitDiffAlgorithm), + PreferenceGitContextLinesChanged(String), + PreferenceGitInterhunkLinesChanged(String), + PreferenceGitIndentHeuristicChanged(bool), + PreferenceGitWhitespaceChanged(GitDiffWhitespace), + PreferenceGitIgnoreBlankLinesChanged(bool), ChooseDevBrainVault, DevBrainVaultPicked(Option), PreferenceContextChanged(String), @@ -1174,6 +1186,30 @@ impl App { self.preference_draft.dev_brain_vault_path = value; self.preference_error = None; } + Message::PreferenceGitDiffAlgorithmChanged(algorithm) => { + self.preference_draft.git_diff_algorithm = algorithm; + self.preference_error = None; + } + Message::PreferenceGitContextLinesChanged(value) => { + self.preference_draft.git_context_lines = value; + self.preference_error = None; + } + Message::PreferenceGitInterhunkLinesChanged(value) => { + self.preference_draft.git_interhunk_lines = value; + self.preference_error = None; + } + Message::PreferenceGitIndentHeuristicChanged(enabled) => { + self.preference_draft.git_indent_heuristic = enabled; + self.preference_error = None; + } + Message::PreferenceGitWhitespaceChanged(whitespace) => { + self.preference_draft.git_whitespace = whitespace; + self.preference_error = None; + } + Message::PreferenceGitIgnoreBlankLinesChanged(enabled) => { + self.preference_draft.git_ignore_blank_lines = enabled; + self.preference_error = None; + } Message::ChooseDevBrainVault => { return Task::perform( async { diff --git a/src/app/git.rs b/src/app/git.rs index 9a885eb..03bb809 100644 --- a/src/app/git.rs +++ b/src/app/git.rs @@ -1,5 +1,7 @@ use super::*; use std::collections::BTreeMap; +#[cfg(unix)] +use std::os::unix::ffi::OsStringExt; #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub(super) enum GitChangeKind { @@ -181,7 +183,7 @@ impl App { self.error = Some("The selected Git file is no longer available.".into()); return; }; - match load_diff(&worktree.root, file) { + match load_diff(&worktree.root, file, &self.config.git) { Ok(diff) => { self.git_diff = Some(diff); self.error = None; @@ -420,25 +422,22 @@ impl App { } fn read_worktree(project_path: &Path) -> Result { - let repository = gix::discover(project_path).map_err(|error| error.to_string())?; + let repository = git2::Repository::discover(project_path).map_err(|error| error.to_string())?; let root = repository .workdir() .ok_or_else(|| "bare repositories do not have a worktree".to_owned())? .to_path_buf(); - let dirwalk = repository - .dirwalk_options() - .map_err(|error| error.to_string())?; + let mut options = git2::StatusOptions::new(); + options.include_untracked(true).recurse_untracked_dirs(true); let mut files = BTreeMap::::new(); - let changes = repository - .status(gix::progress::Discard) - .map_err(|error| error.to_string())? - .index_worktree_options_mut(|options| options.dirwalk_options = Some(dirwalk)) - .untracked_files(gix::status::UntrackedFiles::Files) - .into_iter(Vec::::new()) + let statuses = repository + .statuses(Some(&mut options)) .map_err(|error| error.to_string())?; - for change in changes { - let change = change.map_err(|error| error.to_string())?; - let path = gix::path::from_bstr(change.location()).into_owned(); + for entry in statuses.iter() { + #[cfg(unix)] + let path = PathBuf::from(std::ffi::OsString::from_vec(entry.path_bytes().to_vec())); + #[cfg(not(unix))] + let path = PathBuf::from(String::from_utf8_lossy(entry.path_bytes()).into_owned()); let display_path = path.to_string_lossy().into_owned(); let file = files.entry(path.clone()).or_insert_with(|| GitFile { path, @@ -446,18 +445,30 @@ fn read_worktree(project_path: &Path) -> Result { staged_kind: None, worktree_kind: None, }); - match change { - gix::status::Item::IndexWorktree(change) => { - file.worktree_kind = change.summary().map(worktree_kind); - } - gix::status::Item::TreeIndex(change) => { - file.staged_kind = Some(match change { - gix::diff::index::Change::Addition { .. } => GitChangeKind::Added, - gix::diff::index::Change::Deletion { .. } => GitChangeKind::Deleted, - gix::diff::index::Change::Modification { .. } - | gix::diff::index::Change::Rewrite { .. } => GitChangeKind::Modified, - }); - } + let status = entry.status(); + if status.intersects(git2::Status::INDEX_NEW) { + file.staged_kind = Some(GitChangeKind::Added); + } else if status.intersects(git2::Status::INDEX_DELETED) { + file.staged_kind = Some(GitChangeKind::Deleted); + } else if status.intersects( + git2::Status::INDEX_MODIFIED + | git2::Status::INDEX_RENAMED + | git2::Status::INDEX_TYPECHANGE, + ) { + file.staged_kind = Some(GitChangeKind::Modified); + } + if status.intersects(git2::Status::WT_NEW) { + file.worktree_kind = Some(GitChangeKind::Added); + } else if status.intersects(git2::Status::WT_DELETED) { + file.worktree_kind = Some(GitChangeKind::Deleted); + } else if status.intersects( + git2::Status::WT_MODIFIED | git2::Status::WT_RENAMED | git2::Status::WT_TYPECHANGE, + ) { + file.worktree_kind = Some(GitChangeKind::Modified); + } + if status.intersects(git2::Status::CONFLICTED) { + file.staged_kind = Some(GitChangeKind::Modified); + file.worktree_kind = Some(GitChangeKind::Modified); } } files.retain(|_, file| file.staged_kind.is_some() || file.worktree_kind.is_some()); @@ -467,76 +478,29 @@ fn read_worktree(project_path: &Path) -> Result { }) } -fn worktree_kind(summary: gix::status::index_worktree::iter::Summary) -> GitChangeKind { - use gix::status::index_worktree::iter::Summary; - match summary { - Summary::Added | Summary::IntentToAdd => GitChangeKind::Added, - Summary::Removed => GitChangeKind::Deleted, - Summary::Modified - | Summary::TypeChange - | Summary::Renamed - | Summary::Copied - | Summary::Conflict => GitChangeKind::Modified, - } -} - -fn load_diff(root: &Path, file: &GitFile) -> Result { - let repository = gix::discover(root).map_err(|error| error.to_string())?; - let index = repository - .index_or_empty() - .map_err(|error| error.to_string())?; - let index_path = - gix::path::to_unix_separators_on_windows(gix::path::into_bstr(file.path.as_path())); - let index_blob = index - .entry_by_path(index_path.as_ref()) - .filter(|entry| { - entry - .mode - .to_tree_entry_mode() - .is_some_and(|mode| mode.is_blob_or_symlink()) - }) - .map(|entry| { - repository - .find_blob(entry.id) - .map(|mut blob| blob.take_data()) - .map_err(|error| error.to_string()) - }) - .transpose()?; +fn load_diff(root: &Path, file: &GitFile, settings: &GitConfig) -> Result { + let repository = git2::Repository::discover(root).map_err(|error| error.to_string())?; + let index = repository.index().map_err(|error| error.to_string())?; let mut lines = Vec::new(); if file.staged_kind.is_some() { - let tree = repository - .find_tree( - repository - .head_tree_id_or_empty() - .map_err(|error| error.to_string())?, - ) + let head_tree = repository + .head() + .ok() + .and_then(|head| head.peel_to_tree().ok()); + let mut options = diff_options(file, settings, false); + let diff = repository + .diff_tree_to_index(head_tree.as_ref(), Some(&index), Some(&mut options)) .map_err(|error| error.to_string())?; - let head_blob = tree - .lookup_entry_by_path(&file.path) - .map_err(|error| error.to_string())? - .filter(|entry| entry.mode().is_blob_or_symlink()) - .map(|entry| { - repository - .find_blob(entry.object_id()) - .map(|mut blob| blob.take_data()) - .map_err(|error| error.to_string()) - }) - .transpose()?; lines.push(diff_section("Staged changes")); - lines.extend(diff_blobs( - head_blob.as_deref(), - index_blob.as_deref(), - &file.display_path, - )?); + lines.extend(render_diff(&diff)?); } if file.worktree_kind.is_some() { - let worktree_blob = read_worktree_blob(&root.join(&file.path))?; + let mut options = diff_options(file, settings, true); + let diff = repository + .diff_index_to_workdir(Some(&index), Some(&mut options)) + .map_err(|error| error.to_string())?; lines.push(diff_section("Worktree changes")); - lines.extend(diff_blobs( - index_blob.as_deref(), - worktree_blob.as_deref(), - &file.display_path, - )?); + lines.extend(render_diff(&diff)?); } if lines.is_empty() { lines.push(diff_message("No textual diff is available.")); @@ -547,6 +511,67 @@ fn load_diff(root: &Path, file: &GitFile) -> Result { }) } +fn diff_options(file: &GitFile, settings: &GitConfig, worktree: bool) -> git2::DiffOptions { + let mut options = git2::DiffOptions::new(); + options + .pathspec(&file.path) + .context_lines(settings.context_lines) + .interhunk_lines(settings.interhunk_lines) + .indent_heuristic(settings.indent_heuristic) + .ignore_blank_lines(settings.ignore_blank_lines); + match settings.diff_algorithm { + GitDiffAlgorithm::Default => {} + GitDiffAlgorithm::Patience => { + options.patience(true); + } + GitDiffAlgorithm::Minimal => { + options.minimal(true); + } + } + match settings.whitespace { + GitDiffWhitespace::ShowAll => {} + GitDiffWhitespace::IgnoreAll => { + options.ignore_whitespace(true); + } + GitDiffWhitespace::IgnoreChanges => { + options.ignore_whitespace_change(true); + } + GitDiffWhitespace::IgnoreEndOfLine => { + options.ignore_whitespace_eol(true); + } + } + if worktree { + options + .include_untracked(true) + .recurse_untracked_dirs(true) + .show_untracked_content(true); + } + options +} + +fn render_diff(diff: &git2::Diff<'_>) -> Result, String> { + let mut output = Vec::new(); + diff.print(git2::DiffFormat::Patch, |_, _, line| { + if matches!( + line.origin_value(), + git2::DiffLineType::Context + | git2::DiffLineType::Addition + | git2::DiffLineType::Deletion + ) { + output.push(line.origin() as u8); + } + output.extend_from_slice(line.content()); + true + }) + .map_err(|error| error.to_string())?; + let lines = parse_unified_diff(&String::from_utf8_lossy(&output)); + Ok(if lines.is_empty() { + vec![diff_message("No textual diff is available.")] + } else { + lines + }) +} + fn diff_section(text: &str) -> GitDiffLine { GitDiffLine { kind: GitDiffLineKind::Section, @@ -565,58 +590,6 @@ fn diff_message(text: &str) -> GitDiffLine { } } -fn read_worktree_blob(path: &Path) -> Result>, String> { - match fs::symlink_metadata(path) { - Ok(metadata) if metadata.file_type().is_symlink() => fs::read_link(path) - .map(|target| Some(target.as_os_str().as_encoded_bytes().to_vec())) - .map_err(|error| error.to_string()), - Ok(metadata) if metadata.is_file() => { - fs::read(path).map(Some).map_err(|error| error.to_string()) - } - Ok(_) => Ok(None), - Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None), - Err(error) => Err(error.to_string()), - } -} - -fn diff_blobs( - before: Option<&[u8]>, - after: Option<&[u8]>, - path: &str, -) -> Result, String> { - let before_label = before.map_or_else(|| "/dev/null".into(), |_| format!("a/{path}")); - let after_label = after.map_or_else(|| "/dev/null".into(), |_| format!("b/{path}")); - let before = before.unwrap_or_default(); - let after = after.unwrap_or_default(); - let mut lines = vec![ - diff_message(&format!("--- {before_label}")), - diff_message(&format!("+++ {after_label}")), - ]; - if before == after { - lines.push(diff_message("No textual diff is available.")); - return Ok(lines); - } - if before.iter().take(8_000).any(|byte| *byte == 0) - || after.iter().take(8_000).any(|byte| *byte == 0) - { - lines.push(diff_message("Binary file changed.")); - return Ok(lines); - } - let input = gix::diff::blob::InternedInput::new(before, after); - let diff = - gix::diff::blob::diff_with_slider_heuristics(gix::diff::blob::Algorithm::Histogram, &input); - let output = gix::diff::blob::UnifiedDiff::new( - &diff, - &input, - gix::diff::blob::unified_diff::ConsumeBinaryHunk::new(Vec::new(), "\n"), - Default::default(), - ) - .consume() - .map_err(|error| error.to_string())?; - lines.extend(parse_unified_diff(&String::from_utf8_lossy(&output))); - Ok(lines) -} - fn parse_unified_diff(diff: &str) -> Vec { let mut old_number = None; let mut new_number = None; @@ -924,7 +897,7 @@ mod tests { static NEXT_FIXTURE: AtomicU64 = AtomicU64::new(0); #[test] - fn gitoxide_status_merges_staged_and_worktree_changes() { + fn libgit2_status_merges_staged_and_worktree_changes() { let directory = repository_fixture(); std::fs::write(directory.join("added.txt"), "added\n").unwrap(); git2::Repository::open(&directory) @@ -952,7 +925,7 @@ mod tests { .unwrap(); assert_eq!(changed.staged_kind, Some(GitChangeKind::Modified)); assert_eq!(changed.worktree_kind, Some(GitChangeKind::Modified)); - let diff = load_diff(&worktree.root, changed).unwrap(); + let diff = load_diff(&worktree.root, changed, &GitConfig::default()).unwrap(); assert_eq!( diff.lines .iter() @@ -1118,6 +1091,55 @@ mod tests { ))); } + #[test] + fn configured_git_diff_options_change_the_rendered_patch() { + let directory = empty_repository_fixture(); + std::fs::write( + directory.join("changed.txt"), + "first\nsecond\nthird\nfourth\nfifth\n", + ) + .unwrap(); + stage_paths(&directory, &["changed.txt".into()]).unwrap(); + commit_index(&directory, "Initial").unwrap(); + std::fs::write( + directory.join("changed.txt"), + "first\nsecond changed\nthird\nfourth changed\nfifth\n", + ) + .unwrap(); + let worktree = read_worktree(&directory).unwrap(); + let changed = worktree + .files + .iter() + .find(|file| file.display_path == "changed.txt") + .unwrap(); + let settings = GitConfig { + diff_algorithm: GitDiffAlgorithm::Patience, + context_lines: 0, + interhunk_lines: 0, + indent_heuristic: true, + whitespace: GitDiffWhitespace::ShowAll, + ignore_blank_lines: false, + }; + + let diff = load_diff(&worktree.root, changed, &settings).unwrap(); + assert_eq!( + diff.lines + .iter() + .filter(|line| line.kind == GitDiffLineKind::Context) + .count(), + 0 + ); + assert_eq!( + diff.lines + .iter() + .filter(|line| line.kind == GitDiffLineKind::Hunk) + .count(), + 2 + ); + + std::fs::remove_dir_all(directory).unwrap(); + } + fn repository_fixture() -> PathBuf { let directory = empty_repository_fixture(); std::fs::write(directory.join("changed.txt"), "original\n").unwrap(); diff --git a/src/app/preferences.rs b/src/app/preferences.rs index f108f7b..50e6501 100644 --- a/src/app/preferences.rs +++ b/src/app/preferences.rs @@ -12,6 +12,12 @@ pub(super) struct PreferenceDraft { pub(super) endpoint_cors: bool, pub(super) dev_brain_enabled: bool, pub(super) dev_brain_vault_path: String, + pub(super) git_diff_algorithm: GitDiffAlgorithm, + pub(super) git_context_lines: String, + pub(super) git_interhunk_lines: String, + pub(super) git_indent_heuristic: bool, + pub(super) git_whitespace: GitDiffWhitespace, + pub(super) git_ignore_blank_lines: bool, pub(super) context_tokens: String, pub(super) max_generated_tokens: String, pub(super) system_prompt: text_editor::Content, @@ -64,6 +70,12 @@ impl PreferenceDraft { endpoint_cors: config.endpoint.cors, dev_brain_enabled: config.dev_brain.enabled, dev_brain_vault_path: config.dev_brain.vault_path.clone().unwrap_or_default(), + git_diff_algorithm: config.git.diff_algorithm, + git_context_lines: config.git.context_lines.to_string(), + git_interhunk_lines: config.git.interhunk_lines.to_string(), + git_indent_heuristic: config.git.indent_heuristic, + git_whitespace: config.git.whitespace, + git_ignore_blank_lines: config.git.ignore_blank_lines, context_tokens: generation.context_tokens.to_string(), max_generated_tokens: generation.max_generated_tokens.to_string(), system_prompt: text_editor::Content::with_text(&generation.system_prompt), @@ -126,6 +138,17 @@ impl PreferenceDraft { Ok(preferences) } + pub(super) fn git(&self) -> Result { + Ok(GitConfig { + diff_algorithm: self.git_diff_algorithm, + context_lines: parse_u32("Git diff context lines", &self.git_context_lines)?, + interhunk_lines: parse_u32("Git diff interhunk lines", &self.git_interhunk_lines)?, + indent_heuristic: self.git_indent_heuristic, + whitespace: self.git_whitespace, + ignore_blank_lines: self.git_ignore_blank_lines, + }) + } + pub(super) fn reset(&mut self) { *self = Self::from_saved(&Config::default()); } @@ -248,6 +271,13 @@ fn parse_optional_u32(name: &str, value: &str) -> Result, String> { parse_optional_number(name, value) } +fn parse_u32(name: &str, value: &str) -> Result { + value + .trim() + .parse() + .map_err(|_| format!("{name} must be a non-negative whole number.")) +} + fn parse_optional_u8(name: &str, value: &str) -> Result, String> { parse_optional_number(name, value) } @@ -363,6 +393,13 @@ impl App { return; } }; + let git = match self.preference_draft.git() { + Ok(git) => git, + Err(error) => { + self.preference_error = Some(error); + return; + } + }; let config = Config { model: self.preference_draft.model, idle_timeout_minutes, @@ -378,6 +415,7 @@ impl App { }, generation, runtime, + git, interface: self.config.interface.clone(), }; if let Err(error) = config.validate() { diff --git a/src/app/projects.rs b/src/app/projects.rs index 00cf799..e3ed5dd 100644 --- a/src/app/projects.rs +++ b/src/app/projects.rs @@ -368,31 +368,31 @@ impl App { } fn read_git_state(path: &Path) -> Option { - let repository = gix::discover(path).ok()?; + let repository = git2::Repository::discover(path).ok()?; let mut branches = repository - .references() + .branches(Some(git2::BranchType::Local)) .ok()? - .local_branches() - .ok()? - .map(|reference| { - reference - .map(|reference| String::from_utf8_lossy(reference.name().shorten()).into_owned()) + .map(|branch| { + branch.and_then(|(branch, _)| { + branch + .name_bytes() + .map(|name| String::from_utf8_lossy(name).into_owned()) + }) }) .collect::, _>>() .ok()?; branches.sort(); branches.dedup(); - let current = repository - .head_name() - .ok()? - .map(|name| String::from_utf8_lossy(name.shorten()).into_owned()); + let head = repository.find_reference("HEAD").ok()?; + let current = head.symbolic_target_bytes().and_then(|target| { + target + .strip_prefix(b"refs/heads/") + .map(|name| String::from_utf8_lossy(name).into_owned()) + }); let label = current.clone().unwrap_or_else(|| { - repository - .head_id() - .ok() - .and_then(|id| id.shorten().ok()) - .map(|id| format!("detached @ {id}")) + head.target() + .map(|id| format!("detached @ {}", &id.to_string()[..7])) .unwrap_or_else(|| "No branch".to_owned()) }); Some(GitState { diff --git a/src/app/view.rs b/src/app/view.rs index a4f962e..5d80989 100644 --- a/src/app/view.rs +++ b/src/app/view.rs @@ -12,6 +12,7 @@ use super::{ ModelDownload, ModelOperation, PreferenceSection, ProjectChoice, chat_scroll_id, composer_id, models_path, preferences_scroll_id, }; +use crate::config::{GIT_DIFF_ALGORITHMS, GIT_DIFF_WHITESPACE_MODES}; use crate::database::{ProjectWithSessions, Session, SessionState}; use crate::model::{ self, DownloadPhase, MODEL_CHOICES, ManagedArtifact, ManagedArtifactState, ModelChoice, diff --git a/src/app/view/preferences.rs b/src/app/view/preferences.rs index be9ad69..6ddc54b 100644 --- a/src/app/view/preferences.rs +++ b/src/app/view/preferences.rs @@ -183,6 +183,65 @@ impl App { ] .spacing(10), ); + let git_group = preference_group( + PreferenceSection::Git, + "GIT DIFFS", + column![ + row![ + hint( + text("Algorithm").size(13).width(Length::Fill), + "Default uses libgit2's Myers diff. Patience favors unique matching lines; Minimal spends more time finding the smallest edit script.", + ), + pick_list( + &GIT_DIFF_ALGORITHMS[..], + Some(self.preference_draft.git_diff_algorithm), + Message::PreferenceGitDiffAlgorithmChanged, + ) + .width(240), + ] + .spacing(12) + .align_y(Alignment::Center), + preference_input_row( + "Context lines", + "Unchanged lines shown before and after each changed block. libgit2 defaults to 3.", + text_input("3", &self.preference_draft.git_context_lines) + .on_input(Message::PreferenceGitContextLinesChanged), + ), + preference_input_row( + "Interhunk lines", + "Merge nearby changed blocks when no more than this many unchanged lines separate them. Zero keeps libgit2's default separation.", + text_input("0", &self.preference_draft.git_interhunk_lines) + .on_input(Message::PreferenceGitInterhunkLinesChanged), + ), + row![ + hint( + text("Whitespace").size(13).width(Length::Fill), + "Controls which whitespace-only edits libgit2 omits from the displayed diff.", + ), + pick_list( + &GIT_DIFF_WHITESPACE_MODES[..], + Some(self.preference_draft.git_whitespace), + Message::PreferenceGitWhitespaceChanged, + ) + .width(240), + ] + .spacing(12) + .align_y(Alignment::Center), + hint( + checkbox(self.preference_draft.git_indent_heuristic) + .label("Use indentation heuristic") + .on_toggle(Message::PreferenceGitIndentHeuristicChanged), + "Shift ambiguous hunk boundaries toward indentation changes, which usually makes source-code diffs easier to read.", + ), + hint( + checkbox(self.preference_draft.git_ignore_blank_lines) + .label("Ignore blank-line changes") + .on_toggle(Message::PreferenceGitIgnoreBlankLinesChanged), + "Hide hunks whose changed lines are all blank.", + ), + ] + .spacing(10), + ); let generation_group = preference_group( PreferenceSection::Generation, "GENERATION", @@ -580,6 +639,7 @@ impl App { model_group, endpoint_group, dev_brain_group, + git_group, generation_group, execution_group, acceleration_group, diff --git a/src/config.rs b/src/config.rs index 561f982..f8830f8 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,5 +1,6 @@ use serde::{Deserialize, Serialize}; use serde_norway::{Mapping, Value}; +use std::fmt; use std::fs; use std::path::Path; @@ -19,6 +20,7 @@ pub struct Config { pub endpoint: EndpointConfig, pub generation: GenerationPreferences, pub runtime: RuntimePreferences, + pub git: GitConfig, pub interface: InterfaceConfig, } @@ -32,11 +34,89 @@ impl Default for Config { endpoint: EndpointConfig::default(), generation: GenerationPreferences::default(), runtime: RuntimePreferences::default(), + git: GitConfig::default(), interface: InterfaceConfig::default(), } } } +pub const GIT_DIFF_ALGORITHMS: [GitDiffAlgorithm; 3] = [ + GitDiffAlgorithm::Default, + GitDiffAlgorithm::Patience, + GitDiffAlgorithm::Minimal, +]; + +#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "kebab-case")] +pub enum GitDiffAlgorithm { + #[default] + Default, + Patience, + Minimal, +} + +impl fmt::Display for GitDiffAlgorithm { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(match self { + Self::Default => "Default (Myers)", + Self::Patience => "Patience", + Self::Minimal => "Minimal", + }) + } +} + +pub const GIT_DIFF_WHITESPACE_MODES: [GitDiffWhitespace; 4] = [ + GitDiffWhitespace::ShowAll, + GitDiffWhitespace::IgnoreAll, + GitDiffWhitespace::IgnoreChanges, + GitDiffWhitespace::IgnoreEndOfLine, +]; + +#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "kebab-case")] +pub enum GitDiffWhitespace { + #[default] + ShowAll, + IgnoreAll, + IgnoreChanges, + IgnoreEndOfLine, +} + +impl fmt::Display for GitDiffWhitespace { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(match self { + Self::ShowAll => "Show all changes", + Self::IgnoreAll => "Ignore all whitespace", + Self::IgnoreChanges => "Ignore whitespace amount", + Self::IgnoreEndOfLine => "Ignore end-of-line whitespace", + }) + } +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(default, deny_unknown_fields)] +pub struct GitConfig { + pub diff_algorithm: GitDiffAlgorithm, + pub context_lines: u32, + pub interhunk_lines: u32, + pub indent_heuristic: bool, + pub whitespace: GitDiffWhitespace, + pub ignore_blank_lines: bool, +} + +impl Default for GitConfig { + fn default() -> Self { + Self { + diff_algorithm: GitDiffAlgorithm::Default, + context_lines: 3, + interhunk_lines: 0, + indent_heuristic: false, + whitespace: GitDiffWhitespace::ShowAll, + ignore_blank_lines: false, + } + } +} + #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(default, deny_unknown_fields)] pub struct DevBrainConfig { @@ -201,6 +281,12 @@ mod tests { }, ..RuntimePreferences::default() }, + git: GitConfig { + diff_algorithm: GitDiffAlgorithm::Patience, + context_lines: 5, + whitespace: GitDiffWhitespace::IgnoreEndOfLine, + ..GitConfig::default() + }, ..Config::default() }; config.save(&path).unwrap(); @@ -210,7 +296,8 @@ mod tests { text, "model: glm-5.2\na2ui_enabled: false\n\ generation:\n context_tokens: 65536\n reasoning_mode: none\n\ - runtime:\n ssd:\n enabled: true\n cache: 64GB\n" + runtime:\n ssd:\n enabled: true\n cache: 64GB\n\ + git:\n diff_algorithm: patience\n context_lines: 5\n whitespace: ignore-end-of-line\n" ); assert_eq!(Config::load(&path).unwrap(), config); fs::remove_dir_all(&directory).unwrap();