diff --git a/Cargo.lock b/Cargo.lock index f82deb1851..dd015a1615 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1155,8 +1155,6 @@ dependencies = [ "anyhow", "bstr", "gix", - "serde", - "serde_json", ] [[package]] @@ -3360,7 +3358,6 @@ dependencies = [ "git2", "gitbutler-branch", "gitbutler-command-context", - "gitbutler-diff", "gitbutler-fs", "gitbutler-oxidize", "gitbutler-project", @@ -3760,21 +3757,21 @@ dependencies = [ [[package]] name = "gix" version = "0.73.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#202bc6da79854d1fb6bb32b9c6bb2a6f882c77f5" +source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#f3be6e380450d6b1e178d2fda0446674429bdfe6" dependencies = [ - "gix-actor 0.35.3", + "gix-actor 0.35.4", "gix-attributes 0.27.0", "gix-command", "gix-commitgraph 0.29.0", "gix-config", "gix-credentials", - "gix-date 0.10.4", + "gix-date 0.10.5", "gix-diff", "gix-dir", "gix-discover 0.41.0", "gix-features 0.43.1", "gix-filter", - "gix-fs 0.16.0", + "gix-fs 0.16.1", "gix-glob 0.21.0", "gix-hash 0.19.0", "gix-hashtable 0.9.0", @@ -3784,14 +3781,14 @@ dependencies = [ "gix-mailmap", "gix-merge", "gix-negotiate", - "gix-object 0.50.1", + "gix-object 0.50.2", "gix-odb", "gix-pack", "gix-path 0.10.20", "gix-pathspec", "gix-prompt", "gix-protocol", - "gix-ref 0.53.0", + "gix-ref 0.53.1", "gix-refspec", "gix-revision", "gix-revwalk 0.21.0", @@ -3830,11 +3827,11 @@ dependencies = [ [[package]] name = "gix-actor" -version = "0.35.3" -source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#202bc6da79854d1fb6bb32b9c6bb2a6f882c77f5" +version = "0.35.4" +source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#f3be6e380450d6b1e178d2fda0446674429bdfe6" dependencies = [ "bstr", - "gix-date 0.10.4", + "gix-date 0.10.5", "gix-utils 0.3.0 (git+https://github.com/GitoxideLabs/gitoxide?branch=main)", "itoa", "serde", @@ -3862,7 +3859,7 @@ dependencies = [ [[package]] name = "gix-attributes" version = "0.27.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#202bc6da79854d1fb6bb32b9c6bb2a6f882c77f5" +source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#f3be6e380450d6b1e178d2fda0446674429bdfe6" dependencies = [ "bstr", "gix-glob 0.21.0", @@ -3888,7 +3885,7 @@ dependencies = [ [[package]] name = "gix-bitmap" version = "0.2.14" -source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#202bc6da79854d1fb6bb32b9c6bb2a6f882c77f5" +source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#f3be6e380450d6b1e178d2fda0446674429bdfe6" dependencies = [ "thiserror 2.0.12", ] @@ -3905,7 +3902,7 @@ dependencies = [ [[package]] name = "gix-chunk" version = "0.4.11" -source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#202bc6da79854d1fb6bb32b9c6bb2a6f882c77f5" +source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#f3be6e380450d6b1e178d2fda0446674429bdfe6" dependencies = [ "thiserror 2.0.12", ] @@ -3913,7 +3910,7 @@ dependencies = [ [[package]] name = "gix-command" version = "0.6.2" -source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#202bc6da79854d1fb6bb32b9c6bb2a6f882c77f5" +source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#f3be6e380450d6b1e178d2fda0446674429bdfe6" dependencies = [ "bstr", "gix-path 0.10.20", @@ -3938,7 +3935,7 @@ dependencies = [ [[package]] name = "gix-commitgraph" version = "0.29.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#202bc6da79854d1fb6bb32b9c6bb2a6f882c77f5" +source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#f3be6e380450d6b1e178d2fda0446674429bdfe6" dependencies = [ "bstr", "gix-chunk 0.4.11 (git+https://github.com/GitoxideLabs/gitoxide?branch=main)", @@ -3951,14 +3948,14 @@ dependencies = [ [[package]] name = "gix-config" version = "0.46.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#202bc6da79854d1fb6bb32b9c6bb2a6f882c77f5" +source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#f3be6e380450d6b1e178d2fda0446674429bdfe6" dependencies = [ "bstr", "gix-config-value", "gix-features 0.43.1", "gix-glob 0.21.0", "gix-path 0.10.20", - "gix-ref 0.53.0", + "gix-ref 0.53.1", "gix-sec 0.12.0", "memchr", "once_cell", @@ -3971,7 +3968,7 @@ dependencies = [ [[package]] name = "gix-config-value" version = "0.15.1" -source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#202bc6da79854d1fb6bb32b9c6bb2a6f882c77f5" +source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#f3be6e380450d6b1e178d2fda0446674429bdfe6" dependencies = [ "bitflags 2.9.1", "bstr", @@ -3983,12 +3980,12 @@ dependencies = [ [[package]] name = "gix-credentials" version = "0.30.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#202bc6da79854d1fb6bb32b9c6bb2a6f882c77f5" +source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#f3be6e380450d6b1e178d2fda0446674429bdfe6" dependencies = [ "bstr", "gix-command", "gix-config-value", - "gix-date 0.10.4", + "gix-date 0.10.5", "gix-path 0.10.20", "gix-prompt", "gix-sec 0.12.0", @@ -4013,8 +4010,8 @@ dependencies = [ [[package]] name = "gix-date" -version = "0.10.4" -source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#202bc6da79854d1fb6bb32b9c6bb2a6f882c77f5" +version = "0.10.5" +source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#f3be6e380450d6b1e178d2fda0446674429bdfe6" dependencies = [ "bstr", "itoa", @@ -4027,16 +4024,16 @@ dependencies = [ [[package]] name = "gix-diff" version = "0.53.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#202bc6da79854d1fb6bb32b9c6bb2a6f882c77f5" +source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#f3be6e380450d6b1e178d2fda0446674429bdfe6" dependencies = [ "bstr", "gix-attributes 0.27.0", "gix-command", "gix-filter", - "gix-fs 0.16.0", + "gix-fs 0.16.1", "gix-hash 0.19.0", "gix-index 0.41.0", - "gix-object 0.50.1", + "gix-object 0.50.2", "gix-path 0.10.20", "gix-pathspec", "gix-tempfile 18.0.0", @@ -4050,14 +4047,14 @@ dependencies = [ [[package]] name = "gix-dir" version = "0.15.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#202bc6da79854d1fb6bb32b9c6bb2a6f882c77f5" +source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#f3be6e380450d6b1e178d2fda0446674429bdfe6" dependencies = [ "bstr", "gix-discover 0.41.0", - "gix-fs 0.16.0", + "gix-fs 0.16.1", "gix-ignore 0.16.0", "gix-index 0.41.0", - "gix-object 0.50.1", + "gix-object 0.50.2", "gix-path 0.10.20", "gix-pathspec", "gix-trace 0.1.13 (git+https://github.com/GitoxideLabs/gitoxide?branch=main)", @@ -4085,14 +4082,14 @@ dependencies = [ [[package]] name = "gix-discover" version = "0.41.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#202bc6da79854d1fb6bb32b9c6bb2a6f882c77f5" +source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#f3be6e380450d6b1e178d2fda0446674429bdfe6" dependencies = [ "bstr", "dunce", - "gix-fs 0.16.0", + "gix-fs 0.16.1", "gix-hash 0.19.0", "gix-path 0.10.20", - "gix-ref 0.53.0", + "gix-ref 0.53.1", "gix-sec 0.12.0", "thiserror 2.0.12", ] @@ -4114,7 +4111,7 @@ dependencies = [ [[package]] name = "gix-features" version = "0.43.1" -source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#202bc6da79854d1fb6bb32b9c6bb2a6f882c77f5" +source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#f3be6e380450d6b1e178d2fda0446674429bdfe6" dependencies = [ "bytes", "crc32fast", @@ -4134,14 +4131,14 @@ dependencies = [ [[package]] name = "gix-filter" version = "0.20.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#202bc6da79854d1fb6bb32b9c6bb2a6f882c77f5" +source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#f3be6e380450d6b1e178d2fda0446674429bdfe6" dependencies = [ "bstr", "encoding_rs", "gix-attributes 0.27.0", "gix-command", "gix-hash 0.19.0", - "gix-object 0.50.1", + "gix-object 0.50.2", "gix-packetline-blocking", "gix-path 0.10.20", "gix-quote 0.6.0 (git+https://github.com/GitoxideLabs/gitoxide?branch=main)", @@ -4167,8 +4164,8 @@ dependencies = [ [[package]] name = "gix-fs" -version = "0.16.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#202bc6da79854d1fb6bb32b9c6bb2a6f882c77f5" +version = "0.16.1" +source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#f3be6e380450d6b1e178d2fda0446674429bdfe6" dependencies = [ "bstr", "fastrand", @@ -4193,7 +4190,7 @@ dependencies = [ [[package]] name = "gix-glob" version = "0.21.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#202bc6da79854d1fb6bb32b9c6bb2a6f882c77f5" +source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#f3be6e380450d6b1e178d2fda0446674429bdfe6" dependencies = [ "bitflags 2.9.1", "bstr", @@ -4217,7 +4214,7 @@ dependencies = [ [[package]] name = "gix-hash" version = "0.19.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#202bc6da79854d1fb6bb32b9c6bb2a6f882c77f5" +source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#f3be6e380450d6b1e178d2fda0446674429bdfe6" dependencies = [ "faster-hex", "gix-features 0.43.1", @@ -4240,7 +4237,7 @@ dependencies = [ [[package]] name = "gix-hashtable" version = "0.9.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#202bc6da79854d1fb6bb32b9c6bb2a6f882c77f5" +source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#f3be6e380450d6b1e178d2fda0446674429bdfe6" dependencies = [ "gix-hash 0.19.0", "hashbrown 0.15.4", @@ -4263,7 +4260,7 @@ dependencies = [ [[package]] name = "gix-ignore" version = "0.16.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#202bc6da79854d1fb6bb32b9c6bb2a6f882c77f5" +source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#f3be6e380450d6b1e178d2fda0446674429bdfe6" dependencies = [ "bstr", "gix-glob 0.21.0", @@ -4304,7 +4301,7 @@ dependencies = [ [[package]] name = "gix-index" version = "0.41.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#202bc6da79854d1fb6bb32b9c6bb2a6f882c77f5" +source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#f3be6e380450d6b1e178d2fda0446674429bdfe6" dependencies = [ "bitflags 2.9.1", "bstr", @@ -4312,10 +4309,10 @@ dependencies = [ "fnv", "gix-bitmap 0.2.14 (git+https://github.com/GitoxideLabs/gitoxide?branch=main)", "gix-features 0.43.1", - "gix-fs 0.16.0", + "gix-fs 0.16.1", "gix-hash 0.19.0", "gix-lock 18.0.0", - "gix-object 0.50.1", + "gix-object 0.50.2", "gix-traverse 0.47.0", "gix-utils 0.3.0 (git+https://github.com/GitoxideLabs/gitoxide?branch=main)", "gix-validate 0.10.0 (git+https://github.com/GitoxideLabs/gitoxide?branch=main)", @@ -4343,7 +4340,7 @@ dependencies = [ [[package]] name = "gix-lock" version = "18.0.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#202bc6da79854d1fb6bb32b9c6bb2a6f882c77f5" +source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#f3be6e380450d6b1e178d2fda0446674429bdfe6" dependencies = [ "gix-tempfile 18.0.0", "gix-utils 0.3.0 (git+https://github.com/GitoxideLabs/gitoxide?branch=main)", @@ -4353,11 +4350,11 @@ dependencies = [ [[package]] name = "gix-mailmap" version = "0.27.2" -source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#202bc6da79854d1fb6bb32b9c6bb2a6f882c77f5" +source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#f3be6e380450d6b1e178d2fda0446674429bdfe6" dependencies = [ "bstr", - "gix-actor 0.35.3", - "gix-date 0.10.4", + "gix-actor 0.35.4", + "gix-date 0.10.5", "serde", "thiserror 2.0.12", ] @@ -4365,16 +4362,16 @@ dependencies = [ [[package]] name = "gix-merge" version = "0.6.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#202bc6da79854d1fb6bb32b9c6bb2a6f882c77f5" +source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#f3be6e380450d6b1e178d2fda0446674429bdfe6" dependencies = [ "bstr", "gix-command", "gix-diff", "gix-filter", - "gix-fs 0.16.0", + "gix-fs 0.16.1", "gix-hash 0.19.0", "gix-index 0.41.0", - "gix-object 0.50.1", + "gix-object 0.50.2", "gix-path 0.10.20", "gix-quote 0.6.0 (git+https://github.com/GitoxideLabs/gitoxide?branch=main)", "gix-revision", @@ -4389,13 +4386,13 @@ dependencies = [ [[package]] name = "gix-negotiate" version = "0.21.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#202bc6da79854d1fb6bb32b9c6bb2a6f882c77f5" +source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#f3be6e380450d6b1e178d2fda0446674429bdfe6" dependencies = [ "bitflags 2.9.1", "gix-commitgraph 0.29.0", - "gix-date 0.10.4", + "gix-date 0.10.5", "gix-hash 0.19.0", - "gix-object 0.50.1", + "gix-object 0.50.2", "gix-revwalk 0.21.0", "smallvec", "thiserror 2.0.12", @@ -4424,12 +4421,12 @@ dependencies = [ [[package]] name = "gix-object" -version = "0.50.1" -source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#202bc6da79854d1fb6bb32b9c6bb2a6f882c77f5" +version = "0.50.2" +source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#f3be6e380450d6b1e178d2fda0446674429bdfe6" dependencies = [ "bstr", - "gix-actor 0.35.3", - "gix-date 0.10.4", + "gix-actor 0.35.4", + "gix-date 0.10.5", "gix-features 0.43.1", "gix-hash 0.19.0", "gix-hashtable 0.9.0", @@ -4446,15 +4443,15 @@ dependencies = [ [[package]] name = "gix-odb" version = "0.70.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#202bc6da79854d1fb6bb32b9c6bb2a6f882c77f5" +source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#f3be6e380450d6b1e178d2fda0446674429bdfe6" dependencies = [ "arc-swap", - "gix-date 0.10.4", + "gix-date 0.10.5", "gix-features 0.43.1", - "gix-fs 0.16.0", + "gix-fs 0.16.1", "gix-hash 0.19.0", "gix-hashtable 0.9.0", - "gix-object 0.50.1", + "gix-object 0.50.2", "gix-pack", "gix-path 0.10.20", "gix-quote 0.6.0 (git+https://github.com/GitoxideLabs/gitoxide?branch=main)", @@ -4467,14 +4464,14 @@ dependencies = [ [[package]] name = "gix-pack" version = "0.60.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#202bc6da79854d1fb6bb32b9c6bb2a6f882c77f5" +source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#f3be6e380450d6b1e178d2fda0446674429bdfe6" dependencies = [ "clru", "gix-chunk 0.4.11 (git+https://github.com/GitoxideLabs/gitoxide?branch=main)", "gix-features 0.43.1", "gix-hash 0.19.0", "gix-hashtable 0.9.0", - "gix-object 0.50.1", + "gix-object 0.50.2", "gix-path 0.10.20", "gix-tempfile 18.0.0", "memmap2", @@ -4488,7 +4485,7 @@ dependencies = [ [[package]] name = "gix-packetline" version = "0.19.1" -source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#202bc6da79854d1fb6bb32b9c6bb2a6f882c77f5" +source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#f3be6e380450d6b1e178d2fda0446674429bdfe6" dependencies = [ "bstr", "faster-hex", @@ -4499,7 +4496,7 @@ dependencies = [ [[package]] name = "gix-packetline-blocking" version = "0.19.1" -source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#202bc6da79854d1fb6bb32b9c6bb2a6f882c77f5" +source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#f3be6e380450d6b1e178d2fda0446674429bdfe6" dependencies = [ "bstr", "faster-hex", @@ -4524,7 +4521,7 @@ dependencies = [ [[package]] name = "gix-path" version = "0.10.20" -source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#202bc6da79854d1fb6bb32b9c6bb2a6f882c77f5" +source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#f3be6e380450d6b1e178d2fda0446674429bdfe6" dependencies = [ "bstr", "gix-trace 0.1.13 (git+https://github.com/GitoxideLabs/gitoxide?branch=main)", @@ -4537,7 +4534,7 @@ dependencies = [ [[package]] name = "gix-pathspec" version = "0.12.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#202bc6da79854d1fb6bb32b9c6bb2a6f882c77f5" +source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#f3be6e380450d6b1e178d2fda0446674429bdfe6" dependencies = [ "bitflags 2.9.1", "bstr", @@ -4551,7 +4548,7 @@ dependencies = [ [[package]] name = "gix-prompt" version = "0.11.1" -source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#202bc6da79854d1fb6bb32b9c6bb2a6f882c77f5" +source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#f3be6e380450d6b1e178d2fda0446674429bdfe6" dependencies = [ "gix-command", "gix-config-value", @@ -4563,17 +4560,17 @@ dependencies = [ [[package]] name = "gix-protocol" version = "0.51.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#202bc6da79854d1fb6bb32b9c6bb2a6f882c77f5" +source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#f3be6e380450d6b1e178d2fda0446674429bdfe6" dependencies = [ "bstr", "gix-credentials", - "gix-date 0.10.4", + "gix-date 0.10.5", "gix-features 0.43.1", "gix-hash 0.19.0", "gix-lock 18.0.0", "gix-negotiate", - "gix-object 0.50.1", - "gix-ref 0.53.0", + "gix-object 0.50.2", + "gix-ref 0.53.1", "gix-refspec", "gix-revwalk 0.21.0", "gix-shallow", @@ -4600,7 +4597,7 @@ dependencies = [ [[package]] name = "gix-quote" version = "0.6.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#202bc6da79854d1fb6bb32b9c6bb2a6f882c77f5" +source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#f3be6e380450d6b1e178d2fda0446674429bdfe6" dependencies = [ "bstr", "gix-utils 0.3.0 (git+https://github.com/GitoxideLabs/gitoxide?branch=main)", @@ -4630,15 +4627,15 @@ dependencies = [ [[package]] name = "gix-ref" -version = "0.53.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#202bc6da79854d1fb6bb32b9c6bb2a6f882c77f5" +version = "0.53.1" +source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#f3be6e380450d6b1e178d2fda0446674429bdfe6" dependencies = [ - "gix-actor 0.35.3", + "gix-actor 0.35.4", "gix-features 0.43.1", - "gix-fs 0.16.0", + "gix-fs 0.16.1", "gix-hash 0.19.0", "gix-lock 18.0.0", - "gix-object 0.50.1", + "gix-object 0.50.2", "gix-path 0.10.20", "gix-tempfile 18.0.0", "gix-utils 0.3.0 (git+https://github.com/GitoxideLabs/gitoxide?branch=main)", @@ -4652,7 +4649,7 @@ dependencies = [ [[package]] name = "gix-refspec" version = "0.31.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#202bc6da79854d1fb6bb32b9c6bb2a6f882c77f5" +source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#f3be6e380450d6b1e178d2fda0446674429bdfe6" dependencies = [ "bstr", "gix-hash 0.19.0", @@ -4665,15 +4662,15 @@ dependencies = [ [[package]] name = "gix-revision" version = "0.35.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#202bc6da79854d1fb6bb32b9c6bb2a6f882c77f5" +source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#f3be6e380450d6b1e178d2fda0446674429bdfe6" dependencies = [ "bitflags 2.9.1", "bstr", "gix-commitgraph 0.29.0", - "gix-date 0.10.4", + "gix-date 0.10.5", "gix-hash 0.19.0", "gix-hashtable 0.9.0", - "gix-object 0.50.1", + "gix-object 0.50.2", "gix-revwalk 0.21.0", "gix-trace 0.1.13 (git+https://github.com/GitoxideLabs/gitoxide?branch=main)", "serde", @@ -4698,13 +4695,13 @@ dependencies = [ [[package]] name = "gix-revwalk" version = "0.21.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#202bc6da79854d1fb6bb32b9c6bb2a6f882c77f5" +source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#f3be6e380450d6b1e178d2fda0446674429bdfe6" dependencies = [ "gix-commitgraph 0.29.0", - "gix-date 0.10.4", + "gix-date 0.10.5", "gix-hash 0.19.0", "gix-hashtable 0.9.0", - "gix-object 0.50.1", + "gix-object 0.50.2", "smallvec", "thiserror 2.0.12", ] @@ -4724,7 +4721,7 @@ dependencies = [ [[package]] name = "gix-sec" version = "0.12.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#202bc6da79854d1fb6bb32b9c6bb2a6f882c77f5" +source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#f3be6e380450d6b1e178d2fda0446674429bdfe6" dependencies = [ "bitflags 2.9.1", "gix-path 0.10.20", @@ -4736,7 +4733,7 @@ dependencies = [ [[package]] name = "gix-shallow" version = "0.5.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#202bc6da79854d1fb6bb32b9c6bb2a6f882c77f5" +source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#f3be6e380450d6b1e178d2fda0446674429bdfe6" dependencies = [ "bstr", "gix-hash 0.19.0", @@ -4748,7 +4745,7 @@ dependencies = [ [[package]] name = "gix-status" version = "0.20.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#202bc6da79854d1fb6bb32b9c6bb2a6f882c77f5" +source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#f3be6e380450d6b1e178d2fda0446674429bdfe6" dependencies = [ "bstr", "filetime", @@ -4756,10 +4753,10 @@ dependencies = [ "gix-dir", "gix-features 0.43.1", "gix-filter", - "gix-fs 0.16.0", + "gix-fs 0.16.1", "gix-hash 0.19.0", "gix-index 0.41.0", - "gix-object 0.50.1", + "gix-object 0.50.2", "gix-path 0.10.20", "gix-pathspec", "gix-worktree 0.42.0", @@ -4770,7 +4767,7 @@ dependencies = [ [[package]] name = "gix-submodule" version = "0.20.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#202bc6da79854d1fb6bb32b9c6bb2a6f882c77f5" +source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#f3be6e380450d6b1e178d2fda0446674429bdfe6" dependencies = [ "bstr", "gix-config", @@ -4799,10 +4796,10 @@ dependencies = [ [[package]] name = "gix-tempfile" version = "18.0.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#202bc6da79854d1fb6bb32b9c6bb2a6f882c77f5" +source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#f3be6e380450d6b1e178d2fda0446674429bdfe6" dependencies = [ "dashmap", - "gix-fs 0.16.0", + "gix-fs 0.16.1", "libc", "once_cell", "parking_lot", @@ -4842,7 +4839,7 @@ checksum = "e2ccaf54b0b1743a695b482ca0ab9d7603744d8d10b2e5d1a332fef337bee658" [[package]] name = "gix-trace" version = "0.1.13" -source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#202bc6da79854d1fb6bb32b9c6bb2a6f882c77f5" +source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#f3be6e380450d6b1e178d2fda0446674429bdfe6" dependencies = [ "tracing-core", ] @@ -4850,7 +4847,7 @@ dependencies = [ [[package]] name = "gix-transport" version = "0.48.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#202bc6da79854d1fb6bb32b9c6bb2a6f882c77f5" +source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#f3be6e380450d6b1e178d2fda0446674429bdfe6" dependencies = [ "base64 0.22.1", "bstr", @@ -4886,14 +4883,14 @@ dependencies = [ [[package]] name = "gix-traverse" version = "0.47.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#202bc6da79854d1fb6bb32b9c6bb2a6f882c77f5" +source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#f3be6e380450d6b1e178d2fda0446674429bdfe6" dependencies = [ "bitflags 2.9.1", "gix-commitgraph 0.29.0", - "gix-date 0.10.4", + "gix-date 0.10.5", "gix-hash 0.19.0", "gix-hashtable 0.9.0", - "gix-object 0.50.1", + "gix-object 0.50.2", "gix-revwalk 0.21.0", "smallvec", "thiserror 2.0.12", @@ -4902,7 +4899,7 @@ dependencies = [ [[package]] name = "gix-url" version = "0.32.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#202bc6da79854d1fb6bb32b9c6bb2a6f882c77f5" +source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#f3be6e380450d6b1e178d2fda0446674429bdfe6" dependencies = [ "bstr", "gix-features 0.43.1", @@ -4926,7 +4923,7 @@ dependencies = [ [[package]] name = "gix-utils" version = "0.3.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#202bc6da79854d1fb6bb32b9c6bb2a6f882c77f5" +source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#f3be6e380450d6b1e178d2fda0446674429bdfe6" dependencies = [ "bstr", "fastrand", @@ -4946,7 +4943,7 @@ dependencies = [ [[package]] name = "gix-validate" version = "0.10.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#202bc6da79854d1fb6bb32b9c6bb2a6f882c77f5" +source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#f3be6e380450d6b1e178d2fda0446674429bdfe6" dependencies = [ "bstr", "thiserror 2.0.12", @@ -4974,17 +4971,17 @@ dependencies = [ [[package]] name = "gix-worktree" version = "0.42.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#202bc6da79854d1fb6bb32b9c6bb2a6f882c77f5" +source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#f3be6e380450d6b1e178d2fda0446674429bdfe6" dependencies = [ "bstr", "gix-attributes 0.27.0", "gix-features 0.43.1", - "gix-fs 0.16.0", + "gix-fs 0.16.1", "gix-glob 0.21.0", "gix-hash 0.19.0", "gix-ignore 0.16.0", "gix-index 0.41.0", - "gix-object 0.50.1", + "gix-object 0.50.2", "gix-path 0.10.20", "gix-validate 0.10.0 (git+https://github.com/GitoxideLabs/gitoxide?branch=main)", "serde", @@ -4993,16 +4990,16 @@ dependencies = [ [[package]] name = "gix-worktree-state" version = "0.20.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#202bc6da79854d1fb6bb32b9c6bb2a6f882c77f5" +source = "git+https://github.com/GitoxideLabs/gitoxide?branch=main#f3be6e380450d6b1e178d2fda0446674429bdfe6" dependencies = [ "bstr", "gix-features 0.43.1", "gix-filter", - "gix-fs 0.16.0", + "gix-fs 0.16.1", "gix-glob 0.21.0", "gix-hash 0.19.0", "gix-index 0.41.0", - "gix-object 0.50.1", + "gix-object 0.50.2", "gix-path 0.10.20", "gix-worktree 0.42.0", "io-close", diff --git a/crates/but-action/src/generate.rs b/crates/but-action/src/generate.rs index 5ef6096bdf..ebfc6aab94 100644 --- a/crates/but-action/src/generate.rs +++ b/crates/but-action/src/generate.rs @@ -10,7 +10,7 @@ use schemars::{JsonSchema, schema_for}; use crate::OpenAiProvider; -#[allow(dead_code)] +#[expect(dead_code)] pub fn commit_message_blocking( openai: &OpenAiProvider, external_summary: &str, diff --git a/crates/but-action/src/grouping.rs b/crates/but-action/src/grouping.rs index 5e8b526ff6..1ea5e95659 100644 --- a/crates/but-action/src/grouping.rs +++ b/crates/but-action/src/grouping.rs @@ -15,7 +15,7 @@ pub enum BranchSuggestion { } impl BranchSuggestion { - #[allow(dead_code)] + #[expect(dead_code)] pub fn name(&self) -> String { match self { BranchSuggestion::New(name) => name.clone(), @@ -63,7 +63,7 @@ pub struct Grouping { pub groups: Vec, } -#[allow(dead_code)] +#[expect(dead_code)] pub fn group(openai: &OpenAiProvider, project_status: &ProjectStatus) -> anyhow::Result { let system_message =" You are an expert in grouping file changes into logical units for version control. diff --git a/crates/but-action/src/openai.rs b/crates/but-action/src/openai.rs index 538436a373..bcd863c0bc 100644 --- a/crates/but-action/src/openai.rs +++ b/crates/but-action/src/openai.rs @@ -19,7 +19,6 @@ use schemars::{JsonSchema, schema_for}; use serde::de::DeserializeOwned; use tokio::sync::Mutex; -#[allow(unused)] #[derive(Debug, Clone, serde::Serialize, strum::Display)] pub enum CredentialsKind { EnvVarOpenAiKey, @@ -116,7 +115,6 @@ impl OpenAiProvider { } } -#[allow(dead_code)] pub fn structured_output_blocking< T: serde::Serialize + DeserializeOwned + JsonSchema + std::marker::Send + 'static, >( @@ -144,7 +142,6 @@ pub fn structured_output_blocking< .unwrap() } -#[allow(dead_code)] pub async fn structured_output( client: &Client, messages: Vec, @@ -177,7 +174,6 @@ pub async fn structured_output, @@ -197,7 +193,6 @@ pub fn tool_calling_blocking( .unwrap() } -#[allow(dead_code)] pub async fn tool_calling( client: &Client, messages: Vec, diff --git a/crates/but-core/src/diff/mod.rs b/crates/but-core/src/diff/mod.rs index 7572983850..172f435474 100644 --- a/crates/but-core/src/diff/mod.rs +++ b/crates/but-core/src/diff/mod.rs @@ -4,8 +4,10 @@ use bstr::{BStr, ByteSlice}; pub use tree_changes::tree_changes; mod worktree; -use crate::{ChangeState, ModeFlags, TreeChange, TreeStatus, TreeStatusKind}; -pub use worktree::worktree_changes; +use crate::{ + ChangeState, IgnoredWorktreeChange, ModeFlags, TreeChange, TreeStatus, TreeStatusKind, +}; +pub use worktree::{worktree_changes, worktree_changes_no_renames}; /// conversion functions for use in the UI pub mod ui; @@ -59,6 +61,24 @@ impl TreeChange { } } +impl std::fmt::Debug for TreeChange { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TreeChange") + .field("path", &self.path) + .field("status", &self.status) + .finish() + } +} + +impl std::fmt::Debug for IgnoredWorktreeChange { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("IgnoredWorktreeChange") + .field("path", &self.path) + .field("status", &self.status) + .finish() + } +} + impl ModeFlags { fn calculate(old: &ChangeState, new: &ChangeState) -> Option { Self::calculate_inner(old.kind, new.kind) diff --git a/crates/but-core/src/diff/tree_changes.rs b/crates/but-core/src/diff/tree_changes.rs index 7e2591f584..d9704dbb76 100644 --- a/crates/but-core/src/diff/tree_changes.rs +++ b/crates/but-core/src/diff/tree_changes.rs @@ -81,6 +81,7 @@ impl From for TreeChange { }, is_untracked: false, }, + status_item: None, }, Change::Deletion { location, @@ -95,6 +96,7 @@ impl From for TreeChange { kind: entry_mode.kind(), }, }, + status_item: None, }, Change::Modification { location, @@ -118,6 +120,7 @@ impl From for TreeChange { state, flags: ModeFlags::calculate(&previous_state, &state), }, + status_item: None, } } Change::Rewrite { @@ -147,6 +150,7 @@ impl From for TreeChange { state, flags: ModeFlags::calculate(&previous_state, &state), }, + status_item: None, } } } diff --git a/crates/but-core/src/diff/worktree.rs b/crates/but-core/src/diff/worktree.rs index 2c805a266e..b456b038a7 100644 --- a/crates/but-core/src/diff/worktree.rs +++ b/crates/but-core/src/diff/worktree.rs @@ -33,11 +33,35 @@ enum Origin { /// to get a commit with a tree equal to the current worktree. #[instrument(skip(repo), err(Debug))] pub fn worktree_changes(repo: &gix::Repository) -> anyhow::Result { - let rewrites = gix::diff::Rewrites::default(); /* standard Git rewrite handling for everything */ - debug_assert!( - rewrites.copies.is_none(), - "TODO: copy tracking needs specific support wherever 'previous_path()' is called." - ); + worktree_changes_inner(repo, RenameTracking::Always) +} + +/// Just like [`worktree_changes()`], but don't do any rename tracking for performance. +#[instrument(skip(repo), err(Debug))] +pub fn worktree_changes_no_renames(repo: &gix::Repository) -> anyhow::Result { + worktree_changes_inner(repo, RenameTracking::Disabled) +} + +enum RenameTracking { + Always, + Disabled, +} + +fn worktree_changes_inner( + repo: &gix::Repository, + renames: RenameTracking, +) -> anyhow::Result { + let (tree_index_rewrites, worktree_rewrites) = match renames { + RenameTracking::Always => { + let rewrites = gix::diff::Rewrites::default(); /* standard Git rewrite handling for everything */ + debug_assert!( + rewrites.copies.is_none(), + "TODO: copy tracking needs specific support wherever 'previous_path()' is called." + ); + (TrackRenames::Given(rewrites), Some(rewrites)) + } + RenameTracking::Disabled => (TrackRenames::Disabled, None), + }; let has_submodule_ignore_configuration = repo.modules()?.is_some_and(|modules| { modules .names() @@ -45,8 +69,8 @@ pub fn worktree_changes(repo: &gix::Repository) -> anyhow::Result anyhow::Result anyhow::Result anyhow::Result anyhow::Result anyhow::Result anyhow::Result anyhow::Result anyhow::Result anyhow::Result anyhow::Result { - let Some(checked_out_head_id) = change.checked_out_head_id else { + let Some(checked_out_head_id) = submodule_change.checked_out_head_id else { continue; }; // We can arrive here if the user configures to `ignore = none`, and there are @@ -306,6 +340,7 @@ pub fn worktree_changes(repo: &gix::Repository) -> anyhow::Result anyhow::Result anyhow::Result { ignored_changes.push(IgnoredWorktreeChange { path: rela_path, status: IgnoredWorktreeTreeChangeStatus::Conflict, + status_item: Some(change), }); continue; } @@ -470,15 +508,17 @@ pub fn worktree_changes(repo: &gix::Repository) -> anyhow::Result { + [Some(mut first), Some(mut second)] => { ignored_changes.push(IgnoredWorktreeChange { path: first.path.clone(), status: IgnoredWorktreeTreeChangeStatus::TreeIndex, + status_item: first.status_item.take(), }); changes.push(first); ignored_changes.push(IgnoredWorktreeChange { path: second.path.clone(), status: IgnoredWorktreeTreeChangeStatus::TreeIndex, + status_item: second.status_item.take(), }); changes.push(second); continue; @@ -487,6 +527,7 @@ pub fn worktree_changes(repo: &gix::Repository) -> anyhow::Result) -> anyhow::Result { /// /// For simplicity, copy-tracking is not representable right now, but `copy: bool` could be added /// if needed. Copy-tracking is deactivated as well. -#[derive(Debug, Clone)] +#[derive(Clone)] pub struct TreeChange { /// The *relative* path in the worktree where the entry can be found. pub path: BString, /// The specific information about this change. pub status: TreeStatus, + /// The status item that this change is derived from, used in places that need detailed information. + /// This is only set if this instance was created from a worktree-status, and is `None` when created + /// from a tree-diff. + pub status_item: Option, } /// Specifically defines a [`TreeChange`]. @@ -328,13 +332,17 @@ pub enum IgnoredWorktreeTreeChangeStatus { } /// A way to indicate that a path in the index isn't suitable for committing and needs to be dealt with. -#[derive(Debug, Clone, Serialize)] +#[derive(Clone, Serialize)] pub struct IgnoredWorktreeChange { /// The worktree-relative path to the change. #[serde(serialize_with = "gitbutler_serde::bstring_lossy::serialize")] - path: BString, + pub path: BString, /// The status that caused this change to be ignored. - status: IgnoredWorktreeTreeChangeStatus, + pub status: IgnoredWorktreeTreeChangeStatus, + /// The status item that this change is derived from, used in places that need detailed information. + /// It's `None` if the status item is already present in non-ignored changes. + #[serde(skip)] + pub status_item: Option, } /// The type returned by [`worktree_changes()`](diff::worktree_changes). @@ -350,7 +358,7 @@ pub struct WorktreeChanges { /// the *dominant* change to display. Note that it can stack with a content change, /// but *should not only in case of a `TypeChange*`*. #[derive(Debug, Copy, Clone, PartialEq, Eq)] -#[allow(missing_docs)] +#[expect(missing_docs)] pub enum ModeFlags { ExecutableBitAdded, ExecutableBitRemoved, diff --git a/crates/but-core/src/settings.rs b/crates/but-core/src/settings.rs index 003a71c33f..29f39e73c8 100644 --- a/crates/but-core/src/settings.rs +++ b/crates/but-core/src/settings.rs @@ -21,7 +21,7 @@ pub mod git { /// See [`GitConfigSettings`](crate::GitConfigSettings) for the docs. #[derive(Debug, PartialEq, Clone, Default, serde::Serialize, serde::Deserialize)] #[serde(rename_all = "camelCase")] - #[allow(missing_docs)] + #[expect(missing_docs)] pub struct GitConfigSettings { #[serde(rename = "signCommits")] pub gitbutler_sign_commits: Option, diff --git a/crates/but-core/src/ui.rs b/crates/but-core/src/ui.rs index c1cb0c0851..135c3f3b06 100644 --- a/crates/but-core/src/ui.rs +++ b/crates/but-core/src/ui.rs @@ -249,7 +249,7 @@ pub struct ChangeState { } #[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[allow(missing_docs)] +#[expect(missing_docs)] pub enum ModeFlags { ExecutableBitAdded, ExecutableBitRemoved, @@ -269,12 +269,20 @@ impl From for crate::TreeChange { crate::TreeChange { path: path_bytes, status: status.into(), + // Lossy conversion, but this is fine. + status_item: None, } } } impl From for TreeChange { - fn from(crate::TreeChange { path, status }: crate::TreeChange) -> Self { + fn from( + crate::TreeChange { + path, + status, + status_item: _, + }: crate::TreeChange, + ) -> Self { TreeChange { path: path.clone().into(), path_bytes: path, diff --git a/crates/but-graph/src/init/post.rs b/crates/but-graph/src/init/post.rs index d66b8d115a..0e915a9c2e 100644 --- a/crates/but-graph/src/init/post.rs +++ b/crates/but-graph/src/init/post.rs @@ -35,7 +35,6 @@ impl Graph { /// Now that the graph is complete, perform additional structural improvements with /// the requirement of them to be computationally cheap. #[instrument(skip(self, meta, repo, refs_by_id), err(Debug))] - #[allow(clippy::too_many_arguments)] pub(super) fn post_processed( mut self, meta: &OverlayMetadata<'_, T>, diff --git a/crates/but-graph/src/init/walk.rs b/crates/but-graph/src/init/walk.rs index f7ffd55fc4..f593339c0d 100644 --- a/crates/but-graph/src/init/walk.rs +++ b/crates/but-graph/src/init/walk.rs @@ -596,7 +596,7 @@ pub fn find( /// This means we process all workspaces if we aren't currently and clearly looking at a workspace. /// /// Also prune all non-standard workspaces early, or those that don't have a tip. -#[allow(clippy::type_complexity)] +#[expect(clippy::type_complexity)] pub fn obtain_workspace_infos( repo: &OverlayRepo<'_>, maybe_ref_name: Option<&gix::refs::FullNameRef>, @@ -695,7 +695,7 @@ pub fn propagate_flags_downward( /// If a remote tracking branch is in `target_refs`, we assume it was already scheduled and won't schedule it again. /// Note that remotes fully obey the limit. /// If the created remote segment belongs to the segment of `local_tracking_sidx`, return its Segment index along with its name. -#[allow(clippy::too_many_arguments)] +#[expect(clippy::too_many_arguments)] pub fn try_queue_remote_tracking_branches( repo: &OverlayRepo<'_>, refs: &[gix::refs::FullName], diff --git a/crates/but-graph/tests/graph/init/utils.rs b/crates/but-graph/tests/graph/init/utils.rs index 2649f8ba59..3cae0d6ce4 100644 --- a/crates/but-graph/tests/graph/init/utils.rs +++ b/crates/but-graph/tests/graph/init/utils.rs @@ -40,7 +40,6 @@ pub fn read_only_in_memory_scenario_named( } pub enum StackState { - #[allow(dead_code)] InWorkspace, Inactive, } diff --git a/crates/but-hunk-dependency/src/ranges/mod.rs b/crates/but-hunk-dependency/src/ranges/mod.rs index e318a11726..3a815f6a6b 100644 --- a/crates/but-hunk-dependency/src/ranges/mod.rs +++ b/crates/but-hunk-dependency/src/ranges/mod.rs @@ -23,7 +23,7 @@ pub struct WorkspaceRanges { /// An error that can say what went wrong when computing the hunk ranges for a commit in a stack at a given path. #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] -#[allow(missing_docs)] +#[expect(missing_docs)] pub struct CalculationError { pub error_message: String, pub stack_id: StackId, diff --git a/crates/but-hunk-dependency/tests/hunk_dependency/main.rs b/crates/but-hunk-dependency/tests/hunk_dependency/main.rs index 8cf6eef0d1..0f504cfdf4 100644 --- a/crates/but-hunk-dependency/tests/hunk_dependency/main.rs +++ b/crates/but-hunk-dependency/tests/hunk_dependency/main.rs @@ -61,7 +61,6 @@ mod types { /// A structure that has stable content so it can be asserted on, showing the hunk-ranges that intersect with each of the input ranges. #[derive(Debug)] - #[allow(dead_code)] pub struct WorkspaceDigest { /// All available ranges for a tracked path, basically all changes seen over a set of commits. pub ranges_by_path: Vec<(BString, Vec)>, @@ -72,7 +71,6 @@ mod types { } #[derive(Debug)] - #[allow(dead_code)] pub struct HunkIntersection { /// The hunk that was used for the intersection. pub hunk: but_core::unified_diff::DiffHunk, @@ -82,7 +80,6 @@ mod types { /// A structure that has stable content so it can be asserted on, showing the hunk-ranges that intersect with each of the input ranges. #[derive(Debug)] - #[allow(dead_code)] pub struct WorkspaceWithoutRanges<'a> { /// The ranges that intersected with an input hunk. pub intersections_by_path: &'a Vec<(BString, Vec)>, @@ -101,7 +98,7 @@ mod types { } #[derive(Debug)] - #[allow(dead_code)] + #[expect(dead_code)] pub struct StableHunkRange { change_type: TreeStatusKind, commit_id: gix::ObjectId, diff --git a/crates/but-hunk-dependency/tests/hunk_dependency/ui.rs b/crates/but-hunk-dependency/tests/hunk_dependency/ui.rs index adb9e7a9f2..4a22d050d6 100644 --- a/crates/but-hunk-dependency/tests/hunk_dependency/ui.rs +++ b/crates/but-hunk-dependency/tests/hunk_dependency/ui.rs @@ -269,8 +269,7 @@ mod util { } #[derive(Debug)] - #[allow(dead_code)] - #[allow(clippy::type_complexity)] + #[expect(dead_code)] pub struct StableHunkDependencies { pub diffs: Vec<(String, DiffHunk, Vec)>, pub errors: Vec, @@ -278,7 +277,6 @@ mod util { impl From for StableHunkDependencies { fn from(HunkDependencies { diffs, errors }: HunkDependencies) -> Self { - #[allow(clippy::type_complexity)] StableHunkDependencies { diffs: diffs .into_iter() @@ -296,7 +294,6 @@ mod util { pub struct TestContext { pub repo: gix::Repository, /// All the stacks in the workspace - #[allow(unused)] pub stacks_entries: Vec, /// The storage directory for GitButler. pub gitbutler_dir: PathBuf, @@ -320,7 +317,7 @@ mod util { impl TestContext { /// Find a stack which contains a branch with the given `short_name`. - #[allow(unused)] + #[expect(unused)] pub fn stack_with_branch(&self, short_name: &str) -> &but_workspace::ui::StackEntry { self.stacks_entries .iter() diff --git a/crates/but-rebase/src/commit.rs b/crates/but-rebase/src/commit.rs index 93b6f43a09..cd1d0ef8c8 100644 --- a/crates/but-rebase/src/commit.rs +++ b/crates/but-rebase/src/commit.rs @@ -25,7 +25,6 @@ pub enum DateMode { /// /// Signatures will be removed automatically if signing is disabled to prevent an amended commit /// to use the old signature. -#[allow(clippy::too_many_arguments)] pub fn create( repo: &gix::Repository, mut commit: gix::objs::Commit, diff --git a/crates/but-server/src/lib.rs b/crates/but-server/src/lib.rs index a75778c785..decaf03528 100644 --- a/crates/but-server/src/lib.rs +++ b/crates/but-server/src/lib.rs @@ -126,7 +126,7 @@ async fn handle_websocket(socket: WebSocket, broadcaster: Arc }); while let Some(Ok(msg)) = socket_recv.next().await { - #[allow(clippy::single_match)] + #[expect(clippy::single_match)] match msg { Message::Close(_) => { thread.abort(); diff --git a/crates/but-settings/tests/mod.rs b/crates/but-settings/tests/mod.rs index bbac40b142..5bad35ce69 100644 --- a/crates/but-settings/tests/mod.rs +++ b/crates/but-settings/tests/mod.rs @@ -1,7 +1,7 @@ use but_settings::AppSettings; #[test] -#[allow(clippy::bool_assert_comparison)] +#[expect(clippy::bool_assert_comparison)] fn test_load_settings() { let settings = AppSettings::load("tests/fixtures/modify_default_true_to_false.json".as_ref()).unwrap(); diff --git a/crates/but-status/Cargo.toml b/crates/but-status/Cargo.toml index 9b1d997165..34cafb9ed0 100644 --- a/crates/but-status/Cargo.toml +++ b/crates/but-status/Cargo.toml @@ -9,5 +9,3 @@ publish = false gix.workspace = true anyhow.workspace = true bstr.workspace = true -serde_json = "1.0.142" -serde.workspace = true diff --git a/crates/but-status/src/lib.rs b/crates/but-status/src/lib.rs index 57b3d9e1a5..9e879a7ba0 100644 --- a/crates/but-status/src/lib.rs +++ b/crates/but-status/src/lib.rs @@ -4,8 +4,8 @@ use gix::status::index_worktree; /// This includes files in the index that are considered conflicted. /// /// TODO: This is a copy of `create_wd_tree` from the old world. Ideally we -/// should share between the old and new worlds to prevent duplication beween -/// these. +/// should share between the old and new worlds to prevent duplication between +/// these. pub fn create_wd_tree( repo: &gix::Repository, untracked_limit_in_bytes: u64, @@ -83,7 +83,7 @@ pub fn create_wd_tree( rela_path, status: EntryStatus::Change(Change::Type { .. } | Change::Modification { .. }) - | EntryStatus::Conflict(_) + | EntryStatus::Conflict { .. } | EntryStatus::IntentToAdd, .. }) => { diff --git a/crates/but-testing/src/command/commit.rs b/crates/but-testing/src/command/commit.rs index 9003a2a741..677552780d 100644 --- a/crates/but-testing/src/command/commit.rs +++ b/crates/but-testing/src/command/commit.rs @@ -8,7 +8,7 @@ use gitbutler_project::Project; use gitbutler_stack::{VirtualBranchesHandle, VirtualBranchesState}; use std::path::Path; -#[allow(clippy::too_many_arguments)] +#[expect(clippy::too_many_arguments)] pub fn commit( repo: gix::Repository, project: Option, @@ -109,7 +109,7 @@ fn resolve_changes( ) } -#[allow(clippy::too_many_arguments)] +#[expect(clippy::too_many_arguments)] fn commit_with_project( repo: &gix::Repository, project: &Project, @@ -159,7 +159,7 @@ fn commit_with_project( Ok(()) } -#[allow(clippy::too_many_arguments)] +#[expect(clippy::too_many_arguments)] fn commit_without_project( repo: &gix::Repository, message: Option<&str>, diff --git a/crates/but-testing/src/command/diff.rs b/crates/but-testing/src/command/diff.rs index b269d5b161..5248b510cf 100644 --- a/crates/but-testing/src/command/diff.rs +++ b/crates/but-testing/src/command/diff.rs @@ -170,7 +170,7 @@ fn intersect_workspace_ranges( /// A structure that has stable content so it can be asserted on, showing the hunk-ranges that intersect with each of the input ranges. #[derive(Debug)] -#[allow(dead_code)] +#[expect(dead_code)] pub struct LockInfo { /// All available ranges for a tracked path, basically all changes seen over a set of commits. pub ranges_by_path: Vec<(BString, Vec)>, @@ -181,7 +181,7 @@ pub struct LockInfo { } #[derive(Debug)] -#[allow(dead_code)] +#[expect(dead_code)] pub struct HunkIntersection { /// The hunk that was used for the intersection. pub hunk: but_core::unified_diff::DiffHunk, diff --git a/crates/but-testing/src/command/mod.rs b/crates/but-testing/src/command/mod.rs index 980bb1a188..614ae10cc2 100644 --- a/crates/but-testing/src/command/mod.rs +++ b/crates/but-testing/src/command/mod.rs @@ -473,7 +473,7 @@ pub fn ref_info(args: &super::Args, ref_name: Option<&str>, expensive: bool) -> }?) } -#[allow(clippy::too_many_arguments)] +#[expect(clippy::too_many_arguments)] pub fn graph( args: &super::Args, ref_name: Option<&str>, diff --git a/crates/but-workspace/src/branch/create_reference.rs b/crates/but-workspace/src/branch/create_reference.rs index 65594ce45b..026492749d 100644 --- a/crates/but-workspace/src/branch/create_reference.rs +++ b/crates/but-workspace/src/branch/create_reference.rs @@ -106,7 +106,7 @@ impl<'a> ReferenceAnchor<'a> { } pub(super) mod function { - #![allow(clippy::indexing_slicing)] + #![expect(clippy::indexing_slicing)] use crate::branch::{ReferenceAnchor, ReferencePosition}; use anyhow::{Context, bail}; diff --git a/crates/but-workspace/src/commit_engine/index.rs b/crates/but-workspace/src/commit_engine/index.rs index ee4226b3ee..c2e6f61201 100644 --- a/crates/but-workspace/src/commit_engine/index.rs +++ b/crates/but-workspace/src/commit_engine/index.rs @@ -1,17 +1,13 @@ -use bstr::{BStr, BString, ByteSlice}; -use std::{collections::HashSet, path::Path}; +use bstr::{BStr, ByteSlice}; +use std::path::Path; -/// Turn `rhs` into `lhs` by modifying `rhs`. This will leave `rhs` intact as much as possible, but will remove +/// Turn `rhs` into `lhs` by modifying `rhs`. This will leave `rhs` intact as much as possible. /// Note that conflicting entries will be replaced by an addition or edit automatically. /// extensions that might be affected by these changes, for a lack of finesse with our edits. -/// -/// `filter_paths` is an optional filter that allows to specify which paths should be considered for the update. -/// If `None`, all paths are considered. pub fn apply_lhs_to_rhs( workdir: &Path, lhs: &gix::index::State, rhs: &mut gix::index::State, - filter_paths: Option>, ) -> anyhow::Result<()> { let mut num_sorted_entries = rhs.entries().len(); let mut needs_sorting = false; @@ -31,12 +27,6 @@ pub fn apply_lhs_to_rhs( use gix::diff::index::Change; for change in changes { - if let Some(filter_paths) = &filter_paths { - if !filter_paths.contains(&change.location().to_owned()) { - continue; - } - } - match change { Change::Addition { location, .. } => { delete_entry_by_path_bounded(rhs, location.as_bstr(), &mut num_sorted_entries); @@ -106,7 +96,7 @@ pub fn upsert_index_entry( Stage::Unconflicted, *num_sorted_entries, ) { - #[allow(clippy::indexing_slicing)] + #[expect(clippy::indexing_slicing)] let entry = &mut index.entries_mut()[pos]; // NOTE: it's needed to set the values to 0 here or else 1 in 40 times or so // git status will report the file didn't change even though it did. diff --git a/crates/but-workspace/src/commit_engine/mod.rs b/crates/but-workspace/src/commit_engine/mod.rs index abb1a9a6bf..c5788c716f 100644 --- a/crates/but-workspace/src/commit_engine/mod.rs +++ b/crates/but-workspace/src/commit_engine/mod.rs @@ -231,11 +231,28 @@ pub fn create_commit( bail!("cannot currently handle more than 1 parent") } + let target_tree = match &destination { + Destination::NewCommit { + parent_commit_id: None, + .. + } => gix::ObjectId::empty_tree(repo.object_hash()), + Destination::NewCommit { + parent_commit_id: Some(base_commit), + .. + } + | Destination::AmendCommit { + commit_id: base_commit, + .. + } => but_core::Commit::from_id(base_commit.attach(repo))? + .tree_id_or_auto_resolution()? + .detach(), + }; + let CreateTreeOutcome { rejected_specs, destination_tree, changed_tree_pre_cherry_pick, - } = create_tree(repo, &destination, move_source, changes, context_lines)?; + } = create_tree(repo, target_tree, move_source, changes, context_lines)?; let new_commit = if let Some(new_tree) = destination_tree { match destination { Destination::NewCommit { @@ -577,7 +594,6 @@ pub fn create_commit_and_update_refs( repo.workdir().expect("non-bare"), &tree_index, &mut disk_index, - None, )?; out.index = disk_index.into(); } else { @@ -611,7 +627,7 @@ pub fn create_commit_and_update_refs( /// if present. Alternatively, it uses the current `HEAD` as only reference point. /// Note that virtual branches will be updated and written back after this call, which will obtain /// an exclusive workspace lock as well. -#[allow(clippy::too_many_arguments)] +#[expect(clippy::too_many_arguments)] pub fn create_commit_and_update_refs_with_project( repo: &gix::Repository, project: &gitbutler_project::Project, @@ -662,7 +678,6 @@ pub fn create_commit_and_update_refs_with_project( } /// Create a commit exactly as specified, and sign it depending on Git and GitButler specific Git configuration. -#[allow(clippy::too_many_arguments)] fn create_possibly_signed_commit( repo: &gix::Repository, author: gix::actor::Signature, diff --git a/crates/but-workspace/src/commit_engine/tree/mod.rs b/crates/but-workspace/src/commit_engine/tree/mod.rs index 7b490261ab..32748bd8df 100644 --- a/crates/but-workspace/src/commit_engine/tree/mod.rs +++ b/crates/but-workspace/src/commit_engine/tree/mod.rs @@ -1,4 +1,4 @@ -use crate::commit_engine::{Destination, MoveSourceCommit, RejectionReason, apply_hunks}; +use crate::commit_engine::{MoveSourceCommit, RejectionReason, apply_hunks}; use crate::{DiffSpec, HunkHeader}; use bstr::{BStr, ByteSlice}; use but_core::{RepositoryExt, UnifiedDiff}; @@ -17,8 +17,8 @@ pub struct CreateTreeOutcome { /// when merging the workspace commit, or because the specified hunks didn't match exactly due to changes /// that happened in the meantime, or if a file without a change was specified. pub rejected_specs: Vec<(RejectionReason, DiffSpec)>, - /// The newly created seen from tree that acts as the destination of the changes, or `None` if no commit could be - /// created as all changes-requests were rejected. + /// The newly created seen from tree that acts as the destination of the changes, or `None` if no tree could be + /// created as all changes-requests were rejected (or there was no change). pub destination_tree: Option, /// If `destination_tree` is `Some(_)`, this field is `Some(_)` as well and denotes the base-tree + all changes. /// If the applied changes were from the worktree, it's `HEAD^{tree}` + changes. @@ -29,28 +29,11 @@ pub struct CreateTreeOutcome { /// Like [`create_commit()`], but lower-level and only returns a new tree, without finally associating it with a commit. pub fn create_tree( repo: &gix::Repository, - destination: &Destination, + target_tree: gix::ObjectId, move_source: Option, changes: Vec, context_lines: u32, ) -> anyhow::Result { - let target_tree = match destination { - Destination::NewCommit { - parent_commit_id: None, - .. - } => gix::ObjectId::empty_tree(repo.object_hash()), - Destination::NewCommit { - parent_commit_id: Some(base_commit), - .. - } - | Destination::AmendCommit { - commit_id: base_commit, - .. - } => but_core::Commit::from_id(base_commit.attach(repo))? - .tree_id_or_auto_resolution()? - .detach(), - }; - let mut changes: Vec<_> = changes.into_iter().map(Ok).collect(); let (new_tree, changed_tree_pre_cherry_pick) = if changes.is_empty() { (Some(target_tree), None) @@ -81,7 +64,7 @@ pub fn create_tree( let tree_with_changes = if new_tree == actual_base_tree && changes.iter().all(|c| { c.is_ok() - // Some rejections are OK and we want to create a commit anyway. + // Some rejections are OK, and we want to create a commit anyway. || !matches!( c, Err((RejectionReason::CherryPickMergeConflict,_)) diff --git a/crates/but-workspace/src/lib.rs b/crates/but-workspace/src/lib.rs index f4ce19c508..377958e456 100644 --- a/crates/but-workspace/src/lib.rs +++ b/crates/but-workspace/src/lib.rs @@ -1,4 +1,4 @@ -#![deny(missing_docs, rust_2018_idioms)] +#![deny(missing_docs)] #![deny(clippy::indexing_slicing)] //! ### Terminology @@ -55,6 +55,8 @@ pub use head::{head, merge_worktree_with_workspace}; /// Ignore the name of this module; it's just a place to put code by now. pub mod branch; +pub mod snapshot; + mod changeset; mod commit; diff --git a/crates/but-workspace/src/ref_info.rs b/crates/but-workspace/src/ref_info.rs index 2ec8dd369b..c455133b1a 100644 --- a/crates/but-workspace/src/ref_info.rs +++ b/crates/but-workspace/src/ref_info.rs @@ -1,4 +1,4 @@ -#![allow(clippy::indexing_slicing)] +#![expect(clippy::indexing_slicing)] // TODO: rename this module to `workspace`, make it private, and pub-use all content in the top-level, as we now literally // get the workspace, while possibly processing it for use in the UI. diff --git a/crates/but-workspace/src/snapshot/create_tree.rs b/crates/but-workspace/src/snapshot/create_tree.rs new file mode 100644 index 0000000000..1e28698051 --- /dev/null +++ b/crates/but-workspace/src/snapshot/create_tree.rs @@ -0,0 +1,319 @@ +use bstr::BString; +use but_graph::VirtualBranchesTomlMetadata; +use std::collections::BTreeSet; + +/// A way to determine what should be included in the snapshot when calling [create_tree()](function::create_tree). +#[derive(Debug, Clone)] +pub struct State { + /// The result of a previous worktree changes call, but [the one **without** renames](but_core::diff::worktree_changes_no_renames()). + /// + /// It contains detailed information about the complete set of possible changes to become part of the worktree. + pub changes: but_core::WorktreeChanges, + /// Repository-relative and slash-separated paths that match any change in the [`changes`](State::changes) field. + /// It is *not* error if there is no match, as there can be snapshots without working tree changes, but with other changes. + /// It's up to the caller to check for that via [`Outcome::is_empty()`]. + pub selection: BTreeSet, + /// If `true`, store the current `HEAD` reference, i.e. its target, as well as the targets of all refs it's pointing to by symbolic link. + pub head: bool, +} + +/// Contains all state that the snapshot contains. +#[derive(Debug, Copy, Clone)] +pub struct Outcome { + /// The snapshot itself, with all the subtrees available that are also listed in this structure. + pub snapshot_tree: gix::ObjectId, + /// For good measure, the input `HEAD^{tree}` that is used as the basis to learn about worktree changes. + pub head_tree: gix::ObjectId, + /// The `head_tree` with the selected worktree changes applied, suitable for being stored in a commit, + /// or `None` if there was no change in the worktree. + pub worktree: Option, + /// The tree representing the current changed index, without conflicts, or `None` if there was no change to the index. + pub index: Option, + /// A tree with files in a custom storage format to allow keeping conflicting blobs reachable, along with detailed conflict information + /// to allow restoring the conflict entries in the index. + pub index_conflicts: Option, + /// The tree representing the reference targets of all references within the *workspace*. + pub workspace_references: Option, + /// The tree representing the reference targets of all references reachable from `HEAD`, so typically `HEAD` itself, and the + /// target object of the reference it is pointing to. + pub head_references: Option, + /// The tree representing the metadata of all references within the *workspace*. + pub metadata: Option, +} + +impl Outcome { + /// Return `true` if the snapshot contains no information whatsoever, which is equivalent to being an empty tree. + pub fn is_empty(&self) -> bool { + self.snapshot_tree.is_empty_tree() + } +} + +/// A utility to more easily use *no* workspace or metadata. +pub fn no_workspace_and_meta() -> Option<( + &'static but_graph::projection::Workspace<'static>, + &'static VirtualBranchesTomlMetadata, +)> { + None +} + +pub(super) mod function { + use super::{Outcome, State}; + use crate::{DiffSpec, commit_engine}; + use anyhow::{Context, bail}; + use bstr::{BString, ByteSlice}; + use but_core::{ChangeState, RefMetadata}; + use gix::diff::index::Change; + use gix::object::tree::EntryKind; + use gix::status::plumbing::index_as_worktree::EntryStatus; + use std::collections::BTreeSet; + use tracing::instrument; + + /// Create a tree that represents the snapshot for the given `selection`, whereas the basis for these changes + /// is the `head_tree_id` *(i.e. the tree to which `HEAD` is ultimately pointing to)* - + /// make this an empty tree if the `HEAD` is unborn. + /// It's valid to have no changes to write, which is when the snapshot won't contain any worktree information + /// to the point where it may be entirely [empty](Outcome::is_empty()). + /// + /// If `workspace_and_meta` is not `None`, the workspace and metadata to store in the snapshot. + /// We will only store reference positions, and assume that their commits are safely stored in the reflog to not + /// be garbage collected. Metadata is only stored for the references that are included in the `workspace`. + /// + /// Note that objects will be written into the repository behind `head_tree_id` unless it's configured + /// to keep everything in memory. + /// + /// ### Snapshot Tree Format + /// + /// There are the following top-level trees, with their own sub-formats which aren't specified here. + /// However, it's notable that they have to be implemented so that they remain compatible to prior versions + /// of the tree. + /// + /// Note that all top-level entries are optional, and only present if there is a snapshot to store. + /// + /// * `HEAD` + /// - the tree to which `HEAD` was pointing at the time the snapshot was created. + /// - this is relevant when re-applying the worktree-changes and when recreating the `index`. + /// - only set if it is needed to restore some of the snapshot state. + /// * `worktree` + /// - the tree of `HEAD + uncommitted files`. Technically this means that now possibly untracked files are known to Git, + /// even though it might be that the respective objects aren't written to disk yet. + /// - Note that this tree may contain files with conflict markers as it will pick up the conflicting state visible on disk. + /// * `index` + /// - A representation of the non-conflicting and changed portions of the index, without its meta-data. + /// - may be empty if only conflicts exist. + /// * `index-conflicts` + /// - `/[1,2,3]` - the blobs at their respective stages. + #[instrument(skip(changes, _workspace_and_meta), err(Debug))] + pub fn create_tree( + head_tree_id: gix::Id<'_>, + State { + changes, + selection, + head: _, + }: State, + _workspace_and_meta: Option<(&but_graph::projection::Workspace, &impl RefMetadata)>, + ) -> anyhow::Result { + // Assure this is a tree early. + let head_tree = head_tree_id.object()?.into_tree(); + let repo = head_tree_id.repo; + let mut changes_to_apply: Vec<_> = changes + .changes + .iter() + .filter(|c| selection.contains(&c.path)) + .map(|c| Ok(DiffSpec::from(c))) + .collect(); + changes_to_apply.extend( + changes + .ignored_changes + .iter() + .filter_map(|c| match &c.status_item { + Some(gix::status::Item::IndexWorktree( + gix::status::index_worktree::Item::Modification { + status: EntryStatus::Conflict { .. }, + rela_path, + .. + }, + )) => Some(rela_path), + _ => None, + }) + .filter(|rela_path| selection.contains(rela_path.as_bstr())) + .map(|rela_path| { + // Create a pretend-addition to pick up conflicted paths as well. + Ok(DiffSpec::from(but_core::TreeChange { + path: rela_path.to_owned(), + status: but_core::TreeStatus::Addition { + state: ChangeState { + id: repo.object_hash().null(), + // This field isn't relevant when entries are read from disk. + kind: EntryKind::Tree, + }, + is_untracked: true, + }, + status_item: None, + })) + }), + ); + + let (new_tree, base_tree) = commit_engine::tree::apply_worktree_changes( + head_tree_id.into(), + repo, + &mut changes_to_apply, + 0, /* context lines don't matter */ + )?; + + let rejected = changes_to_apply + .into_iter() + .filter_map(Result::err) + .collect::>(); + if !rejected.is_empty() { + bail!( + "It should be impossible to fail to apply changes that are in the tree that was provided as HEAD^{{tree}} - {rejected:?}" + ) + } + + let mut edit = repo.empty_tree().edit()?; + + let worktree = (new_tree != base_tree).then_some(new_tree.detach()); + let mut needs_head = false; + if let Some(worktree) = worktree { + edit.upsert("worktree", EntryKind::Tree, worktree)?; + needs_head = true; + } + + let (index, index_conflicts) = snapshot_index(&mut edit, head_tree, changes, selection)? + .inspect(|(index, index_conflicts)| { + needs_head |= index_conflicts.is_some() && index.is_none(); + }) + .unwrap_or_default(); + + if needs_head { + edit.upsert("HEAD", EntryKind::Tree, head_tree_id)?; + } + + Ok(Outcome { + snapshot_tree: edit.write()?.into(), + head_tree: head_tree_id.detach(), + worktree, + index, + index_conflicts, + workspace_references: None, + head_references: None, + metadata: None, + }) + } + + /// `snapshot_tree` is the tree into which our `index` and `index-conflicts` trees are written. These will also be returned + /// if they were written. + /// + /// `base_tree_id` is the tree from which a clean index can be created, and which we will edit to incorporate the + /// non-conflicting index changes. + fn snapshot_index( + snapshot_tree: &mut gix::object::tree::Editor, + base_tree: gix::Tree, + changes: but_core::WorktreeChanges, + selection: BTreeSet, + ) -> anyhow::Result, Option)>> { + let mut conflicts = Vec::new(); + let changes: Vec<_> = changes + .changes + .into_iter() + .filter_map(|c| c.status_item) + .chain( + changes + .ignored_changes + .into_iter() + .filter_map(|c| c.status_item), + ) + .filter_map(|item| match item { + gix::status::Item::IndexWorktree( + gix::status::index_worktree::Item::Modification { + status: EntryStatus::Conflict { entries, .. }, + rela_path, + .. + }, + ) => { + conflicts.push((rela_path, entries)); + None + } + gix::status::Item::TreeIndex(c) => Some(c), + _ => None, + }) + .filter(|c| selection.iter().any(|path| path == c.location())) + .collect(); + + if changes.is_empty() && conflicts.is_empty() { + return Ok(None); + } + + let mut base_tree_edit = base_tree.edit()?; + for change in changes { + match change { + Change::Deletion { location, .. } => { + base_tree_edit.remove(location.as_bstr())?; + } + Change::Addition { + location, + entry_mode, + id, + .. + } + | Change::Modification { + location, + entry_mode, + id, + .. + } => { + base_tree_edit.upsert( + location.as_bstr(), + entry_mode + .to_tree_entry_mode() + .with_context(|| format!("Could not convert the index entry {entry_mode:?} at '{location}' into a tree entry kind"))? + .kind(), + id.into_owned(), + )?; + } + Change::Rewrite { .. } => { + unreachable!("BUG: this must have been deactivated") + } + } + } + + let index = base_tree_edit.write()?; + let index = (index != base_tree.id).then_some(index.detach()); + if let Some(index) = index { + snapshot_tree.upsert("index", EntryKind::Tree, index)?; + } + + let index_conflicts = if conflicts.is_empty() { + None + } else { + let mut root = snapshot_tree.cursor_at("index-conflicts")?; + for (rela_path, conflict_entries) in conflicts { + for (stage, entry) in conflict_entries + .into_iter() + .enumerate() + .filter_map(|(idx, e)| e.map(|e| (idx + 1, e))) + { + root.upsert( + format!("{rela_path}/{stage}"), + entry + .mode + .to_tree_entry_mode() + .with_context(|| { + format!( + "Could not convert the index entry {entry_mode:?} \ + at '{location}' into a tree entry kind", + entry_mode = entry.mode, + location = rela_path + ) + })? + .kind(), + entry.id, + )?; + } + } + root.write()?.detach().into() + }; + + Ok(Some((index, index_conflicts))) + } +} diff --git a/crates/but-workspace/src/snapshot/mod.rs b/crates/but-workspace/src/snapshot/mod.rs new file mode 100644 index 0000000000..bd2724b45d --- /dev/null +++ b/crates/but-workspace/src/snapshot/mod.rs @@ -0,0 +1,113 @@ +//! The ability to create a Git representation of diverse 'state' that can be restored at a later time. + +/// Structures to call the [create_tree()] function. +pub mod create_tree; +pub use create_tree::function::create_tree; + +/// Utilities related to resolving previously created snapshots. +pub mod resolve_tree; +pub use resolve_tree::function::resolve_tree; + +/// Utilities for associating snapshot-trees with commits and additional metadata. +mod commit { + use anyhow::anyhow; + use but_core::RefMetadata; + use serde::Serialize; + use std::fmt; + use std::fmt::{Display, Formatter}; + use std::str::FromStr; + + /// A commit representing a snapshot, along with metadata. + #[expect(dead_code)] + pub struct Commit<'repo> { + /// The id of the commit that was used for accessing its metadata. + id: gix::Id<'repo>, + /// The fully decoded commit. + inner: gix::objs::Commit, + } + + /// Represents a key value pair stored in a snapshot, like `key: value\n` + /// Using the git trailer format () + #[derive(Debug, PartialEq, Clone, Serialize)] + #[serde(rename_all = "camelCase")] + pub struct CommitTrailer { + /// Trailer key. + pub key: String, + /// Trailer value. + pub value: String, + } + + impl Display for CommitTrailer { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + let escaped_value = self.value.replace('\n', "\\n"); + write!(f, "{}: {}", self.key, escaped_value) + } + } + + impl FromStr for CommitTrailer { + type Err = anyhow::Error; + + fn from_str(s: &str) -> anyhow::Result { + let mut parts = s.splitn(2, ':'); + let (Some(key), Some(value)) = (parts.next(), parts.next()) else { + return Err(anyhow!("Invalid trailer format, expected `key: value`")); + }; + let unescaped_value = value.trim().replace("\\n", "\n"); + Ok(Self { + key: key.trim().to_string(), + value: unescaped_value, + }) + } + } + + /// Metadata attached to [`Commit`]s holding snapshots. + pub struct CommitMetadata { + /// The name of the operation that created the commit. + /// This is an internal string. + pub operation: String, + /// The title of the commit for user consumption, typically created using information from `trailers`. + pub title: String, + /// Properties to be stored with the commit. + pub trailers: Vec, + } + + /// Given a `snapshot_tree` as created by [`super::create_tree()`], associate it with the stash of `ref_name`. + /// If a stash already exists, put it on top, with a new commit to carry `metadata`. + pub fn create_stash_commit<'repo>( + _snapshot_tree: gix::Id<'repo>, + _ref_name: &gix::refs::FullNameRef, + _metadata: CommitMetadata, + ) -> anyhow::Result> { + todo!() + } + + /// List all stash commits available for `ref_name`, with the top-most (most recent) first, and the oldest one last. + pub fn list_stash_commits<'repo>( + _repo: &'repo gix::Repository, + _ref_name: &gix::refs::FullNameRef, + ) -> anyhow::Result>> { + todo!() + } + + /// List all references for which a stash is available. + /// Note that these might not actually exist in the `repo`, for instance if the actual reference was renamed. + pub fn list_stash_references(_repo: &gix::Repository) -> Vec { + todo!() + } + + /// Remove the top-most stash from the top of `ref_name` and write back all changes. + /// Just like Git, write merge conflicts and update the index, possibly update refs and metadata. + // TODO: should there be a dry-run version of this to know if it will conflict or not? Who likes having merge-conflicts + // in the worktree and no stash afterwards? Needs separate method to avoid touching refs. + pub fn pop_stash_commit( + _repo: &gix::Repository, + _ref_name: &gix::refs::FullNameRef, + _meta: &mut impl RefMetadata, + ) -> anyhow::Result<()> { + todo!() + } +} +pub use commit::{ + Commit, CommitMetadata, CommitTrailer, create_stash_commit, list_stash_commits, + list_stash_references, pop_stash_commit, +}; diff --git a/crates/but-workspace/src/snapshot/resolve_tree.rs b/crates/but-workspace/src/snapshot/resolve_tree.rs new file mode 100644 index 0000000000..e64d56abcb --- /dev/null +++ b/crates/but-workspace/src/snapshot/resolve_tree.rs @@ -0,0 +1,174 @@ +/// The information extracted from [`resolve_tree`](function::resolve_tree()). +pub struct Outcome<'repo> { + /// The cherry-pick result as merge between the target worktree and the snapshot, **possibly with conflicts**. + /// + /// This tree, may be checked out to the working tree, with or without conflicts - it's entirely left to the caller. + /// It's `None` if the was no worktree change. + pub worktree_cherry_pick: Option>, + /// If an index was stored in the snapshot, this is the reconstructed index, including conflicts. + /// Note that it has no information from disk whatsoever and should not be written like that. + /// + /// It's `None` if there were no index-only changes. + pub index: Option, + /// Reference edits that when applied in a transaction will set the workspace back to where it was. Only available + /// if it was part of the snapshot to begin with. + pub workspace_references: Option>, + /// The metadata to be applied to the ref-metadata store. + pub metadata: Option, +} + +/// Edits for application via [`but_core::RefMetadata`]. +pub struct MetadataEdits { + /// The workspace metadata stored in the snapshot. + pub workspace: (gix::refs::FullName, but_core::ref_metadata::Workspace), + /// The branch metadata stored in snapshots. + pub branches: Vec<(gix::refs::FullName, but_core::ref_metadata::Branch)>, +} + +/// Options for use in [super::resolve_tree()]. +#[derive(Debug, Clone, Default)] +pub struct Options { + /// If set, the non-default options to use when cherry-picking the worktree changes onto the target tree. + /// + /// If `None`, perform the merge just like Git. + pub worktree_cherry_pick: Option, +} + +pub(super) mod function { + use super::{Options, Outcome}; + use anyhow::{Context, bail}; + use bstr::ByteSlice; + use gitbutler_oxidize::GixRepositoryExt; + use gix::index::entry::{Flags, Stage}; + use std::collections::BTreeSet; + + /// Given the `snapshot_tree` as previously returned via [super::create_tree::Outcome::snapshot_tree], extract data and… + /// + /// * …cherry-pick the worktree changes onto the `target_worktree_tree_id`, which is assumed to represent the future working directory state + /// and which either contains the worktree changes or *preferably* is the `HEAD^{tree}` as the working directory is clean. + /// * …reconstruct the index to write into `.git/index` (including conflicts), assuming that the current `.git/index` is clean, *or* is the index from which the snapshot was taken. + /// * …produce reference edits to put the workspace refs back into place with. + /// * …produce metadata that if set will represent the metadata of the entire workspace. + /// + /// Note that none of this data is actually manifested in the repository or working tree, they only exists as objects in the Git database, + /// assuming in-memory objects aren't used in the repository. + pub fn resolve_tree( + snapshot_tree: gix::Id<'_>, + target_worktree_tree_id: gix::ObjectId, + Options { + worktree_cherry_pick: worktree_cherry_pick_options, + }: Options, + ) -> anyhow::Result> { + let repo = snapshot_tree.repo; + let snapshot_tree = snapshot_tree.object()?.try_into_tree()?; + let head_tree = snapshot_tree.lookup_entry_by_path("HEAD")?; + let worktree = snapshot_tree.lookup_entry_by_path("worktree")?; + let index = snapshot_tree.lookup_entry_by_path("index")?; + let index_conflicts = snapshot_tree.lookup_entry_by_path("index-conflicts")?; + + let worktree_cherry_pick = match (&head_tree, worktree) { + (Some(worktree_base), Some(worktree)) => { + let base = worktree_base.object_id(); + let ours = target_worktree_tree_id; + let theirs = worktree.object_id(); + + repo.merge_trees( + base, + ours, + theirs, + repo.default_merge_labels(), + worktree_cherry_pick_options + .map(Ok) + .unwrap_or_else(|| repo.tree_merge_options())?, + )? + .into() + } + (None, Some(_worktree)) => { + bail!( + "Snapshot tree {id} needs a 'HEAD' entry if it has a 'worktree' entry", + id = snapshot_tree.id + ) + } + (None, None) | (Some(_), None) => None, + }; + + let index = match (&head_tree, index, index_conflicts) { + (_, Some(index_tree), Some(index_conflicts)) => { + let (mut index, _path) = repo.index_from_tree(&index_tree.id())?.into_parts(); + resolve_conflicts(&mut index, index_conflicts.id())?; + Some(index) + } + (_, Some(index_tree), None) => { + let (index, _path) = repo.index_from_tree(&index_tree.id())?.into_parts(); + Some(index) + } + (Some(worktree_base), None, Some(index_conflicts)) => { + let (mut index, _path) = repo.index_from_tree(&worktree_base.id())?.into_parts(); + resolve_conflicts(&mut index, index_conflicts.id())?; + Some(index) + } + (None, None, Some(_index_conflicts)) => bail!( + "Snapshot tree {id} needs a 'HEAD' entry if it has only a 'index-conflicts' entry", + id = snapshot_tree.id + ), + (_, None, None) => None, + }; + + Ok(Outcome { + worktree_cherry_pick, + index, + workspace_references: None, + metadata: None, + }) + } + + #[expect(clippy::indexing_slicing)] + fn resolve_conflicts( + index: &mut gix::index::State, + conflict_tree: gix::Id, + ) -> anyhow::Result<()> { + let conflict_tree = conflict_tree.object()?.try_into_tree()?; + // Since we don't expect a lot of entries, quickly record everything. + let mut recorder = gix::traverse::tree::Recorder::default(); + conflict_tree.traverse().depthfirst(&mut recorder)?; + + let mut to_remove = BTreeSet::new(); + for record in &recorder.records { + if record.mode.is_tree() { + continue; + } + let rela_path = &record.filepath; + let rslash_pos = rela_path + .rfind("/") + .context("BUG: expecting /")?; + let stage: usize = rela_path[rslash_pos + 1..] + .to_str()? + .parse() + .context("Failed to parse stage that should only be [1,2,3]")?; + let rela_path = rela_path[..rslash_pos].as_bstr(); + let stage = match stage { + 0 => bail!("Unconflicted stage for '{rela_path}' is unexpected"), + 1 => Stage::Base, + 2 => Stage::Ours, + 3 => Stage::Theirs, + other => bail!("Invalid stage '{other}' for '{rela_path}'"), + }; + + index.dangerously_push_entry( + Default::default(), + record.oid, + Flags::from_stage(stage), + record.mode.into(), + rela_path, + ); + + to_remove.insert(rela_path); + } + index.remove_entries(|_idx, path, entry| { + entry.flags.stage() == Stage::Unconflicted && to_remove.contains(path) + }); + + index.sort_entries(); + Ok(()) + } +} diff --git a/crates/but-workspace/src/tree_manipulation/discard_worktree_changes.rs b/crates/but-workspace/src/tree_manipulation/discard_worktree_changes.rs index 491f82f315..8119c245c8 100644 --- a/crates/but-workspace/src/tree_manipulation/discard_worktree_changes.rs +++ b/crates/but-workspace/src/tree_manipulation/discard_worktree_changes.rs @@ -470,7 +470,7 @@ mod file { num_sorted_entries: usize, ) -> anyhow::Result<()> { if let Some(range) = index.entry_range(with_trailing_slash(rela_path).as_bstr()) { - #[allow(clippy::indexing_slicing)] + #[expect(clippy::indexing_slicing)] for entry in &mut index.entries_mut()[range] { entry.flags.insert(gix::index::entry::Flags::REMOVE); } @@ -517,7 +517,7 @@ mod file { ) else { continue; }; - #[allow(clippy::indexing_slicing)] + #[expect(clippy::indexing_slicing)] state.entries_mut()[entry_idx] .flags .insert(gix::index::entry::Flags::REMOVE); diff --git a/crates/but-workspace/src/tree_manipulation/hunk/mod.rs b/crates/but-workspace/src/tree_manipulation/hunk/mod.rs index c05a1b943a..a3f73a6e9c 100644 --- a/crates/but-workspace/src/tree_manipulation/hunk/mod.rs +++ b/crates/but-workspace/src/tree_manipulation/hunk/mod.rs @@ -12,7 +12,7 @@ pub(crate) enum HunkSubstraction { /// Like a boolean subtraction, remove `subtractions` from `hunk`, and return the remaining pieces. /// Note that the old and new ranges in `hunk` are split in lock-step, so that cutting out a piece from old will take /// the respective amount of lines from new if these are available. -#[allow(clippy::indexing_slicing)] +#[expect(clippy::indexing_slicing)] pub(crate) fn subtract_hunks( hunk: HunkHeader, subtractions: impl IntoIterator, diff --git a/crates/but-workspace/src/tree_manipulation/utils.rs b/crates/but-workspace/src/tree_manipulation/utils.rs index 6fd90b9722..7d0de48bd2 100644 --- a/crates/but-workspace/src/tree_manipulation/utils.rs +++ b/crates/but-workspace/src/tree_manipulation/utils.rs @@ -46,9 +46,10 @@ pub(crate) fn rebase_mapping_with_overrides( } pub enum ChangesSource { - #[allow(dead_code)] - Commit { id: gix::ObjectId }, - #[allow(dead_code)] + Commit { + id: gix::ObjectId, + }, + #[expect(dead_code)] Tree { after_id: gix::ObjectId, before_id: gix::ObjectId, diff --git a/crates/but-workspace/tests/fixtures/generated-archives/.gitignore b/crates/but-workspace/tests/fixtures/generated-archives/.gitignore index 40b13ab817..7736578fb2 100644 --- a/crates/but-workspace/tests/fixtures/generated-archives/.gitignore +++ b/crates/but-workspace/tests/fixtures/generated-archives/.gitignore @@ -6,10 +6,7 @@ /all-file-types-modified.tar /merge-with-two-branches-line-offset-two-files.tar /merge-with-two-branches-line-offset.tar -/unborn-with-submodules.tar -/unborn-untracked-crlf.tar -/unborn-untracked-all-file-types.tar -/unborn-untracked.tar +/unborn-*.tar /all-file-types-renamed-and-modified.tar /merge-with-two-branches-auto-resolved-merge.tar /two-commits-three-buckets.tar diff --git a/crates/but-workspace/tests/fixtures/scenario/unborn-all-file-types-added-to-index.sh b/crates/but-workspace/tests/fixtures/scenario/unborn-all-file-types-added-to-index.sh new file mode 100644 index 0000000000..10ba6f4050 --- /dev/null +++ b/crates/but-workspace/tests/fixtures/scenario/unborn-all-file-types-added-to-index.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +### Description +# A newly initialized git repository with an executable, a normal file, a symlink and a fifo, added to the index. +set -eu -o pipefail + +git init +echo content > untracked +echo exe > untracked-exe && chmod +x untracked-exe +ln -s untracked link +mkdir dir +mkfifo dir/fifo-should-be-ignored + +git add . + diff --git a/crates/but-workspace/tests/fixtures/scenario/unborn-empty.sh b/crates/but-workspace/tests/fixtures/scenario/unborn-empty.sh new file mode 100644 index 0000000000..3aa9cc6db2 --- /dev/null +++ b/crates/but-workspace/tests/fixtures/scenario/unborn-empty.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +### Description +# A newly initialized git repository, with no additional content. +git init diff --git a/crates/but-workspace/tests/workspace/commit_engine/amend_commit.rs b/crates/but-workspace/tests/workspace/commit_engine/amend_commit.rs index dfd877bd5d..23cb15e245 100644 --- a/crates/but-workspace/tests/workspace/commit_engine/amend_commit.rs +++ b/crates/but-workspace/tests/workspace/commit_engine/amend_commit.rs @@ -17,11 +17,11 @@ fn all_changes_and_renames_to_topmost_commit_no_parent() -> anyhow::Result<()> { let mut config = repo.config_snapshot_mut(); config.set_value( &gix::config::tree::gitoxide::Commit::COMMITTER_DATE, - "946771266 +0633", + "946771266 +0600", )?; config.set_value( &gix::config::tree::gitoxide::Commit::AUTHOR_DATE, - "946684866 +0633", + "946684866 +0600", )?; } let head_commit = repo.rev_parse_single("HEAD")?; @@ -51,7 +51,7 @@ fn all_changes_and_renames_to_topmost_commit_no_parent() -> anyhow::Result<()> { CreateCommitOutcome { rejected_specs: [], new_commit: Some( - Sha1(1e8275bd6e656965ed0729797362abc2e1fb633d), + Sha1(e69a4dfd9ef6d55fd4e49f801612fbe061677adb), ), changed_tree_pre_cherry_pick: Some( Sha1(e56fc9bacdd11ebe576b5d96d21127c423698126), @@ -74,8 +74,8 @@ fn all_changes_and_renames_to_topmost_commit_no_parent() -> anyhow::Result<()> { // which thus should never be removed. insta::assert_snapshot!(visualize_commit(&repo, &outcome)?, @r" tree e56fc9bacdd11ebe576b5d96d21127c423698126 - author author 946684866 +0633 - committer committer (From Env) 946771266 +0633 + author author 946684866 +0600 + committer committer (From Env) 946771266 +0600 gitbutler-headers-version 2 gitbutler-change-id 00000000-0000-0000-0000-000000003333 diff --git a/crates/but-workspace/tests/workspace/main.rs b/crates/but-workspace/tests/workspace/main.rs index c47728d367..4a87ff64d4 100644 --- a/crates/but-workspace/tests/workspace/main.rs +++ b/crates/but-workspace/tests/workspace/main.rs @@ -5,6 +5,7 @@ mod branch_details; mod commit_engine; mod flatten_diff_specs; mod ref_info; +mod snapshot; mod tree_manipulation; mod ui; diff --git a/crates/but-workspace/tests/workspace/snapshot/index_create_and_resolve.rs b/crates/but-workspace/tests/workspace/snapshot/index_create_and_resolve.rs new file mode 100644 index 0000000000..8ed8215331 --- /dev/null +++ b/crates/but-workspace/tests/workspace/snapshot/index_create_and_resolve.rs @@ -0,0 +1,127 @@ +use crate::snapshot::args_for_worktree_changes; +use crate::utils::{read_only_in_memory_scenario, visualize_index}; +use but_testsupport::visualize_tree; +use but_workspace::snapshot; +use gix::prelude::ObjectIdExt; + +#[test] +fn unborn_added_to_index() -> anyhow::Result<()> { + let repo = read_only_in_memory_scenario("unborn-all-file-types-added-to-index")?; + let (head_tree_id, state, no_workspace_and_meta) = args_for_worktree_changes(&repo)?; + + let out = snapshot::create_tree(head_tree_id, state, no_workspace_and_meta)?; + insta::assert_snapshot!(visualize_tree(out.snapshot_tree.attach(&repo)), @r#" + 085f2bf + ├── HEAD:4b825dc + ├── index:7f802e9 + │ ├── link:120000:faf96c1 "untracked" + │ ├── untracked:100644:d95f3ad "content\n" + │ └── untracked-exe:100755:86daf54 "exe\n" + └── worktree:7f802e9 + ├── link:120000:faf96c1 "untracked" + ├── untracked:100644:d95f3ad "content\n" + └── untracked-exe:100755:86daf54 "exe\n" + "#); + insta::assert_debug_snapshot!(out, @r" + Outcome { + snapshot_tree: Sha1(085f2bfb08640d035adff078f19d75477fea1a86), + head_tree: Sha1(4b825dc642cb6eb9a060e54bf8d69288fbee4904), + worktree: Some( + Sha1(7f802e9e3d48d97a7ca7bc4cbf7e0168bd587eed), + ), + index: Some( + Sha1(7f802e9e3d48d97a7ca7bc4cbf7e0168bd587eed), + ), + index_conflicts: None, + workspace_references: None, + head_references: None, + metadata: None, + } + "); + + let res_out = snapshot::resolve_tree( + out.snapshot_tree.attach(&repo), + out.head_tree, + snapshot::resolve_tree::Options::default(), + )?; + let mut cherry_pick = res_out + .worktree_cherry_pick + .expect("a worktree change was applied"); + assert_eq!(cherry_pick.tree.write()?, out.worktree.unwrap()); + let index = res_out + .index + .expect("the index was altered with many added files"); + insta::assert_snapshot!(visualize_index(&index), @r" + 120000:faf96c1 link + 100644:d95f3ad untracked + 100755:86daf54 untracked-exe + "); + + assert!(res_out.metadata.is_none()); + assert!( + res_out.workspace_references.is_none(), + "didn't ask to store this" + ); + Ok(()) +} + +#[test] +fn with_conflicts() -> anyhow::Result<()> { + let repo = read_only_in_memory_scenario("merge-with-two-branches-conflict")?; + let (head_tree_id, state, no_workspace_and_meta) = args_for_worktree_changes(&repo)?; + + let out = snapshot::create_tree(head_tree_id, state, no_workspace_and_meta)?; + insta::assert_snapshot!(visualize_tree(out.snapshot_tree.attach(&repo)), @r#" + 60bd065 + ├── HEAD:429a9b9 + │ └── file:100644:e6c4914 "20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n" + ├── index-conflicts:189678e + │ └── file:0c1481f + │ ├── 1:100644:e69de29 "" + │ ├── 2:100644:e6c4914 "20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n" + │ └── 3:100644:e33f5e9 "10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n" + └── worktree:35f29cc + └── file:100644:1330395 "<<<<<<< HEAD\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n=======\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n>>>>>>> A\n" + "#); + insta::assert_debug_snapshot!(out, @r" + Outcome { + snapshot_tree: Sha1(60bd065acc7ffead5a0c96fe88272bb7c518df35), + head_tree: Sha1(429a9b9078be82ae1e729a6af6f0649605431545), + worktree: Some( + Sha1(35f29cc82c94e40611d738100b21dedae6d72a74), + ), + index: None, + index_conflicts: Some( + Sha1(189678eb92add4601574a23c96a7c8d40bd9d154), + ), + workspace_references: None, + head_references: None, + metadata: None, + } + "); + + let res_out = snapshot::resolve_tree( + out.snapshot_tree.attach(&repo), + out.head_tree, + snapshot::resolve_tree::Options::default(), + )?; + let mut cherry_pick = res_out + .worktree_cherry_pick + .expect("a worktree change was applied"); + assert_eq!(cherry_pick.tree.write()?, out.worktree.unwrap()); + let index = res_out + .index + .expect("the index was altered with many added files"); + insta::assert_snapshot!(visualize_index(&index), @r" + 100644:e69de29 file:1 + 100644:e6c4914 file:2 + 100644:e33f5e9 file:3 + "); + + assert!(res_out.metadata.is_none()); + assert!( + res_out.workspace_references.is_none(), + "didn't ask to store this" + ); + Ok(()) +} diff --git a/crates/but-workspace/tests/workspace/snapshot/mod.rs b/crates/but-workspace/tests/workspace/snapshot/mod.rs new file mode 100644 index 0000000000..fbb4b0c1f0 --- /dev/null +++ b/crates/but-workspace/tests/workspace/snapshot/mod.rs @@ -0,0 +1,36 @@ +mod index_create_and_resolve; +mod worktree_create_and_resolve; + +mod utils { + use but_graph::VirtualBranchesTomlMetadata; + use but_workspace::snapshot; + + /// Produce all args needed for creating a snapshot tree, and assure everything is selected. + #[expect(clippy::type_complexity)] + pub fn args_for_worktree_changes( + repo: &gix::Repository, + ) -> anyhow::Result<( + gix::Id<'_>, + snapshot::create_tree::State, + Option<( + &'static but_graph::projection::Workspace<'static>, + &'static VirtualBranchesTomlMetadata, + )>, + )> { + let changes = but_core::diff::worktree_changes_no_renames(repo)?; + let state = snapshot::create_tree::State { + selection: changes + .changes + .iter() + .map(|c| c.path.clone()) + .chain(changes.ignored_changes.iter().map(|c| c.path.clone())) + .collect(), + changes, + head: false, + }; + let head_tree_id = repo.head_tree_id_or_empty()?; + + Ok((head_tree_id, state, None)) + } +} +pub use utils::args_for_worktree_changes; diff --git a/crates/but-workspace/tests/workspace/snapshot/worktree_create_and_resolve.rs b/crates/but-workspace/tests/workspace/snapshot/worktree_create_and_resolve.rs new file mode 100644 index 0000000000..e4da7607b6 --- /dev/null +++ b/crates/but-workspace/tests/workspace/snapshot/worktree_create_and_resolve.rs @@ -0,0 +1,156 @@ +use crate::snapshot::args_for_worktree_changes; +use crate::utils::read_only_in_memory_scenario; +use but_testsupport::visualize_tree; +use but_workspace::snapshot; +use gix::prelude::ObjectIdExt; + +#[test] +fn unborn_empty() -> anyhow::Result<()> { + let repo = read_only_in_memory_scenario("unborn-empty")?; + let (head_tree_id, state, no_workspace_and_meta) = args_for_worktree_changes(&repo)?; + + let out = snapshot::create_tree(head_tree_id, state, no_workspace_and_meta)?; + assert!( + out.is_empty(), + "There is nothing to pick up and no change at all." + ); + insta::assert_snapshot!(visualize_tree(out.snapshot_tree.attach(&repo)), @"4b825dc"); + insta::assert_debug_snapshot!(out, @r" + Outcome { + snapshot_tree: Sha1(4b825dc642cb6eb9a060e54bf8d69288fbee4904), + head_tree: Sha1(4b825dc642cb6eb9a060e54bf8d69288fbee4904), + worktree: None, + index: None, + index_conflicts: None, + workspace_references: None, + head_references: None, + metadata: None, + } + "); + let out = snapshot::resolve_tree( + out.snapshot_tree.attach(&repo), + out.head_tree, + snapshot::resolve_tree::Options::default(), + )?; + assert!(out.index.is_none()); + assert!(out.metadata.is_none()); + assert!( + out.worktree_cherry_pick.is_none(), + "no worktree to cherry-pick" + ); + assert!( + out.workspace_references.is_none(), + "didn't ask to store this" + ); + + Ok(()) +} + +#[test] +fn unborn_untracked() -> anyhow::Result<()> { + let repo = read_only_in_memory_scenario("unborn-untracked-all-file-types")?; + let (head_tree_id, state, no_workspace_and_meta) = args_for_worktree_changes(&repo)?; + + let out = snapshot::create_tree(head_tree_id, state, no_workspace_and_meta)?; + assert!(!out.is_empty(), "it picks up the untracked files"); + insta::assert_snapshot!(visualize_tree(out.snapshot_tree.attach(&repo)), @r#" + a863d4e + ├── HEAD:4b825dc + └── worktree:7f802e9 + ├── link:120000:faf96c1 "untracked" + ├── untracked:100644:d95f3ad "content\n" + └── untracked-exe:100755:86daf54 "exe\n" + "#); + insta::assert_debug_snapshot!(out, @r" + Outcome { + snapshot_tree: Sha1(a863d4e32304f2f8e5d80b18a4f6fd614c052590), + head_tree: Sha1(4b825dc642cb6eb9a060e54bf8d69288fbee4904), + worktree: Some( + Sha1(7f802e9e3d48d97a7ca7bc4cbf7e0168bd587eed), + ), + index: None, + index_conflicts: None, + workspace_references: None, + head_references: None, + metadata: None, + } + "); + + let res_out = snapshot::resolve_tree( + out.snapshot_tree.attach(&repo), + out.head_tree, + snapshot::resolve_tree::Options::default(), + )?; + let mut cherry_pick = res_out + .worktree_cherry_pick + .expect("a worktree change was applied"); + assert_eq!( + cherry_pick.tree.write()?, + out.worktree.unwrap(), + "Applying worktree changes to their base yields the worktree changes exactly.\ + Due to the way this works, we don't have to test much as we rely on gix merge to work." + ); + assert!(res_out.index.is_none()); + assert!(res_out.metadata.is_none()); + assert!( + res_out.workspace_references.is_none(), + "didn't ask to store this" + ); + Ok(()) +} + +#[test] +fn worktree_all_filetypes() -> anyhow::Result<()> { + let repo = read_only_in_memory_scenario("all-file-types-renamed-and-modified")?; + let (head_tree_id, state, no_workspace_and_meta) = args_for_worktree_changes(&repo)?; + + let out = snapshot::create_tree(head_tree_id, state, no_workspace_and_meta)?; + insta::assert_snapshot!(visualize_tree(out.snapshot_tree.attach(&repo)), @r#" + 9d274f3 + ├── HEAD:3fd29f0 + │ ├── executable:100755:01e79c3 "1\n2\n3\n" + │ ├── file:100644:3aac70f "5\n6\n7\n8\n" + │ └── link:120000:c4c364c "nonexisting-target" + └── worktree:e56fc9b + ├── executable-renamed:100755:8a1218a "1\n2\n3\n4\n5\n" + ├── file-renamed:100644:c5c4315 "5\n6\n7\n8\n9\n10\n" + └── link-renamed:120000:94e4e07 "other-nonexisting-target" + "#); + + insta::assert_debug_snapshot!(out, @r" + Outcome { + snapshot_tree: Sha1(9d274f3ad046ca7d50285c6c5056bfe89f16587c), + head_tree: Sha1(3fd29f0ca55ee4dc3ea6bf02a761c15fd6dc8428), + worktree: Some( + Sha1(e56fc9bacdd11ebe576b5d96d21127c423698126), + ), + index: None, + index_conflicts: None, + workspace_references: None, + head_references: None, + metadata: None, + } + "); + + let res_out = snapshot::resolve_tree( + out.snapshot_tree.attach(&repo), + out.head_tree, + snapshot::resolve_tree::Options::default(), + )?; + let mut cherry_pick = res_out + .worktree_cherry_pick + .expect("a worktree change was applied"); + assert_eq!( + cherry_pick.tree.write()?, + out.worktree.unwrap(), + "Applying worktree changes to their base yields the worktree changes exactly.\ + Due to the way this works, we don't have to test much as we rely on gix merge to work." + ); + assert!(res_out.index.is_none()); + assert!(res_out.metadata.is_none()); + assert!( + res_out.workspace_references.is_none(), + "didn't ask to store this" + ); + Ok(()) +} diff --git a/crates/but-workspace/tests/workspace/tree_manipulation/file.rs b/crates/but-workspace/tests/workspace/tree_manipulation/file.rs index ccb438013f..8e0ac6ad48 100644 --- a/crates/but-workspace/tests/workspace/tree_manipulation/file.rs +++ b/crates/but-workspace/tests/workspace/tree_manipulation/file.rs @@ -241,10 +241,10 @@ fn conflicts_are_invisible() -> anyhow::Result<()> { let (repo, _tmp) = writable_scenario("merge-with-two-branches-conflict"); insta::assert_snapshot!(git_status(&repo)?, @"UU file"); insta::assert_snapshot!(visualize_index(&**repo.index()?), @r" -100644:e69de29 file -100644:e6c4914 file -100644:e33f5e9 file -"); + 100644:e69de29 file:1 + 100644:e6c4914 file:2 + 100644:e33f5e9 file:3 + "); let dropped = discard_workspace_changes(&repo, Some(file_to_spec("file")), CONTEXT_LINES)?; assert_eq!( @@ -256,10 +256,10 @@ fn conflicts_are_invisible() -> anyhow::Result<()> { // Nothing was changed insta::assert_snapshot!(git_status(&repo)?, @"UU file"); insta::assert_snapshot!(visualize_index(&**repo.index()?), @r" -100644:e69de29 file -100644:e6c4914 file -100644:e33f5e9 file -"); + 100644:e69de29 file:1 + 100644:e6c4914 file:2 + 100644:e33f5e9 file:3 + "); insta::assert_snapshot!(visualize_disk_tree_skip_dot_git(repo.workdir().unwrap())?, @r" . ├── .git:40755 diff --git a/crates/but-workspace/tests/workspace/utils.rs b/crates/but-workspace/tests/workspace/utils.rs index d363853c6d..d87b5bca18 100644 --- a/crates/but-workspace/tests/workspace/utils.rs +++ b/crates/but-workspace/tests/workspace/utils.rs @@ -215,9 +215,17 @@ pub fn visualize_index(index: &gix::index::State) -> String { let path = entry.path(index); writeln!( &mut buf, - "{mode:o}:{id} {path}", + "{mode:o}:{id} {path}{stage}", id = &entry.id.to_hex_with_len(7), mode = entry.mode.bits(), + stage = { + let stage = entry.flags.stage(); + if stage == gix::index::entry::Stage::Unconflicted { + "".to_string() + } else { + format!(":{stage}", stage = stage as usize) + } + } ) .expect("enough memory") } diff --git a/crates/but/src/id/mod.rs b/crates/but/src/id/mod.rs index c423a9700e..12fd138cc9 100644 --- a/crates/but/src/id/mod.rs +++ b/crates/but/src/id/mod.rs @@ -13,7 +13,6 @@ pub enum CliId { Branch { name: String, }, - #[allow(dead_code)] Commit { oid: gix::ObjectId, }, @@ -54,7 +53,6 @@ impl CliId { s == self.to_string() } - #[allow(dead_code)] pub fn from_str(ctx: &mut CommandContext, s: &str) -> anyhow::Result> { if s.len() < 2 { return Err(anyhow::anyhow!("Id needs to be 3 characters long: {}", s)); diff --git a/crates/but/src/mcp_internal/project.rs b/crates/but/src/mcp_internal/project.rs index 603bbc0c84..acac7af0fd 100644 --- a/crates/but/src/mcp_internal/project.rs +++ b/crates/but/src/mcp_internal/project.rs @@ -15,7 +15,6 @@ pub fn project_repo(path: &Path) -> anyhow::Result { } pub enum RepositoryOpenMode { // We'll need this later for the commit command - #[allow(dead_code)] Merge, General, } diff --git a/crates/but/src/status/assignment.rs b/crates/but/src/status/assignment.rs index 2fd541c988..6f0c3d60d8 100644 --- a/crates/but/src/status/assignment.rs +++ b/crates/but/src/status/assignment.rs @@ -20,7 +20,7 @@ impl FileAssignment { assignments: filtered_assignments, } } - #[allow(dead_code)] + #[expect(dead_code)] pub fn hash(&self) -> String { let combined_ids: String = self .assignments diff --git a/crates/gitbutler-branch-actions/src/branch.rs b/crates/gitbutler-branch-actions/src/branch.rs index 987bda3126..8601738a47 100644 --- a/crates/gitbutler-branch-actions/src/branch.rs +++ b/crates/gitbutler-branch-actions/src/branch.rs @@ -329,7 +329,7 @@ fn branch_group_to_branch( } /// A sum type of branch that can be a plain git branch or a virtual branch -#[allow(clippy::large_enum_variant)] +#[expect(clippy::large_enum_variant)] enum GroupBranch<'a> { Local(gix::Reference<'a>), Remote(gix::Reference<'a>), diff --git a/crates/gitbutler-branch-actions/src/branch_manager/branch_creation.rs b/crates/gitbutler-branch-actions/src/branch_manager/branch_creation.rs index e14a743322..66d77b7dce 100644 --- a/crates/gitbutler-branch-actions/src/branch_manager/branch_creation.rs +++ b/crates/gitbutler-branch-actions/src/branch_manager/branch_creation.rs @@ -19,7 +19,6 @@ use gitbutler_repo_actions::RepoActionsExt; use gitbutler_stack::{BranchOwnershipClaims, Stack, StackId}; use gitbutler_time::time::now_since_unix_epoch_ms; use gitbutler_workspace::branch_trees::{update_uncommited_changes_with_tree, WorkspaceState}; -#[allow(deprecated)] use tracing::instrument; impl BranchManager<'_> { diff --git a/crates/gitbutler-branch-actions/src/branch_upstream_integration.rs b/crates/gitbutler-branch-actions/src/branch_upstream_integration.rs index 241d2bbd38..3fbf16ded8 100644 --- a/crates/gitbutler-branch-actions/src/branch_upstream_integration.rs +++ b/crates/gitbutler-branch-actions/src/branch_upstream_integration.rs @@ -14,7 +14,6 @@ use gitbutler_repo::{ }; use gitbutler_stack::StackId; use gitbutler_workspace::branch_trees::{update_uncommited_changes, WorkspaceState}; -#[allow(deprecated)] use gix::refs::FullName; use itertools::Itertools; use serde::{Deserialize, Serialize}; diff --git a/crates/gitbutler-branch-actions/src/commit_ops.rs b/crates/gitbutler-branch-actions/src/commit_ops.rs deleted file mode 100644 index 6b32f4be20..0000000000 --- a/crates/gitbutler-branch-actions/src/commit_ops.rs +++ /dev/null @@ -1,271 +0,0 @@ -use anyhow::{bail, Result}; -use gitbutler_oxidize::GixRepositoryExt; - -/// Finds the first parent of a given commit. -fn get_first_parent<'repo>(commit: &gix::Commit<'repo>) -> Result> { - let Some(first_parent) = commit.parent_ids().next() else { - bail!("Failed to find first parent of {}", commit.id()) - }; - let first_parent = first_parent.object()?.into_commit(); - Ok(first_parent) -} - -/// Gets the changes that one commit introduced compared to the base, -/// excluding anything between the commit and the base. -pub fn get_exclusive_tree( - repo: &gix::Repository, - commit_id: gix::ObjectId, - base_id: gix::ObjectId, -) -> Result { - let commit = repo.find_commit(commit_id)?; - let commit_parent = get_first_parent(&commit)?; - let base = repo.find_commit(base_id)?; - - let merged_tree = repo - .merge_trees( - commit_parent.tree_id()?, - commit.tree_id()?, - base.tree_id()?, - Default::default(), - repo.merge_options_force_ours()?, - )? - .tree - .write()?; - Ok(merged_tree.into()) -} - -#[derive(PartialEq, Debug)] -enum SubsetKind { - /// The subset_id is not equal to or a subset of superset_id. - /// superset_id MAY still be a strict subset of subset_id - NotSubset, - /// The subset_id is a strict subset of superset_id - Subset, - /// The subset_id and superset_id are equivalent commits - Equal, -} - -/// Takes two commits and determines if one is a subset of or equal to the other. -/// -/// ### Performance -/// -/// `repository` should have been configured [`with_object_memory()`](gix::Repository::with_object_memory()) -/// to prevent real objects to be written while probing for set inclusion. -#[allow(dead_code)] -fn is_subset( - repo: &gix::Repository, - superset_id: gix::ObjectId, - subset_id: gix::ObjectId, - common_base_id: gix::ObjectId, -) -> Result { - let exclusive_superset = get_exclusive_tree(repo, superset_id, common_base_id)?; - let exclusive_subset = get_exclusive_tree(repo, subset_id, common_base_id)?; - - if exclusive_superset == exclusive_subset { - return Ok(SubsetKind::Equal); - } - - let common_base = repo.find_commit(common_base_id)?; - - let (options, unresolved) = repo.merge_options_fail_fast()?; - let mut merged_exclusives = repo.merge_trees( - common_base.tree_id()?, - exclusive_superset, - exclusive_subset, - Default::default(), - options, - )?; - - if merged_exclusives.has_unresolved_conflicts(unresolved) - || exclusive_superset != merged_exclusives.tree.write()? - { - Ok(SubsetKind::NotSubset) - } else { - Ok(SubsetKind::Subset) - } -} - -#[cfg(test)] -mod test { - use gitbutler_testsupport::testing_repository::TestingRepository; - mod get_exclusive_tree { - use gitbutler_oxidize::OidExt; - - use super::super::get_exclusive_tree; - use super::*; - - #[test] - fn when_already_exclusive_returns_self() { - let test_repo = TestingRepository::open(); - let base_commit: git2::Commit = - test_repo.commit_tree(None, &[("foo.txt", "foo"), ("bar.txt", "bar")]); - let second_commit: git2::Commit = test_repo.commit_tree( - Some(&base_commit), - &[("foo.txt", "bar"), ("bar.txt", "baz")], - ); - - let exclusive_tree = get_exclusive_tree( - &test_repo.gix_repository(), - second_commit.id().to_gix(), - base_commit.id().to_gix(), - ) - .unwrap(); - - assert_eq!( - second_commit.tree_id().to_gix(), - exclusive_tree, - "The tree returned should match the second commit" - ) - } - - #[test] - fn when_on_top_of_other_commit_its_changes_are_dropped() { - let test_repo = TestingRepository::open(); - let base_commit: git2::Commit = test_repo.commit_tree(None, &[("foo.txt", "foo")]); - let second_commit: git2::Commit = test_repo.commit_tree( - Some(&base_commit), - &[("foo.txt", "bar"), ("bar.txt", "baz")], - ); - let third_commit: git2::Commit = test_repo.commit_tree( - Some(&second_commit), - &[("foo.txt", "bar"), ("bar.txt", "baz"), ("qux.txt", "bax")], - ); - - // The second commit changed foo.txt, and added bar.txt. We expect - // foo.txt to be reverted back to foo, and bar.txt should be dropped - let expected_commit: git2::Commit = - test_repo.commit_tree(None, &[("foo.txt", "foo"), ("qux.txt", "bax")]); - - let exclusive_tree = get_exclusive_tree( - &test_repo.gix_repository(), - third_commit.id().to_gix(), - base_commit.id().to_gix(), - ) - .unwrap(); - - assert_eq!(expected_commit.tree_id().to_gix(), exclusive_tree,) - } - } - - mod is_subset { - use gitbutler_oxidize::OidExt; - - use crate::commit_ops::SubsetKind; - - use super::super::is_subset; - use super::*; - - #[test] - fn a_commit_is_a_subset_of_itself() { - let test_repo = TestingRepository::open(); - let base_commit: git2::Commit = test_repo.commit_tree(None, &[("foo.txt", "foo")]); - let second_commit: git2::Commit = test_repo.commit_tree( - Some(&base_commit), - &[("foo.txt", "bar"), ("bar.txt", "baz")], - ); - - assert_eq!( - is_subset( - &test_repo.gix_repository(), - second_commit.id().to_gix(), - second_commit.id().to_gix(), - base_commit.id().to_gix() - ) - .unwrap(), - SubsetKind::Equal - ) - } - - #[test] - fn basic_subset() { - let test_repo = TestingRepository::open(); - let base_commit: git2::Commit = test_repo.commit_tree(None, &[("foo.txt", "foo")]); - let superset: git2::Commit = test_repo.commit_tree( - Some(&base_commit), - &[("foo.txt", "bar"), ("bar.txt", "baz"), ("baz.txt", "asdf")], - ); - let subset: git2::Commit = test_repo.commit_tree( - Some(&base_commit), - &[("foo.txt", "bar"), ("bar.txt", "baz")], - ); - - assert_eq!( - is_subset( - &test_repo.gix_repository(), - superset.id().to_gix(), - subset.id().to_gix(), - base_commit.id().to_gix() - ) - .unwrap(), - SubsetKind::Subset - ); - - assert_eq!( - is_subset( - &test_repo.gix_repository(), - subset.id().to_gix(), - superset.id().to_gix(), - base_commit.id().to_gix() - ) - .unwrap(), - SubsetKind::NotSubset - ); - } - - #[test] - fn complex_subset() { - let test_repo = TestingRepository::open(); - let base_commit: git2::Commit = test_repo.commit_tree(None, &[("foo.txt", "foo")]); - let i1: git2::Commit = test_repo.commit_tree( - Some(&base_commit), - &[("foo.txt", "baz"), ("amp.txt", "asfd")], - ); - let superset: git2::Commit = test_repo.commit_tree( - Some(&i1), - &[ - ("foo.txt", "baz"), - ("amp.txt", "asfd"), - ("bar.txt", "baz"), - ("baz.txt", "asdf"), - ], - ); - let i2: git2::Commit = test_repo.commit_tree( - Some(&base_commit), - &[("foo.txt", "xxx"), ("fuzz.txt", "asdf")], - ); - let subset: git2::Commit = test_repo.commit_tree( - Some(&i2), - &[("foo.txt", "xxx"), ("fuzz.txt", "asdf"), ("bar.txt", "baz")], - ); - - // This creates two commits "superset" and "subset" which when - // compared directly don't have a superset-subset relationship, - // but because we take the changes that the subset/superset commits - // exclusivly added compared to a common base, we are able to - // identify that the changes each commit introduced are infact - // a superset/subset of each other - - assert_eq!( - is_subset( - &test_repo.gix_repository(), - superset.id().to_gix(), - subset.id().to_gix(), - base_commit.id().to_gix() - ) - .unwrap(), - SubsetKind::Subset - ); - - assert_eq!( - is_subset( - &test_repo.gix_repository(), - subset.id().to_gix(), - superset.id().to_gix(), - base_commit.id().to_gix() - ) - .unwrap(), - SubsetKind::NotSubset - ); - } - } -} diff --git a/crates/gitbutler-branch-actions/src/lib.rs b/crates/gitbutler-branch-actions/src/lib.rs index 4fa20a284a..f1ba39c948 100644 --- a/crates/gitbutler-branch-actions/src/lib.rs +++ b/crates/gitbutler-branch-actions/src/lib.rs @@ -1,7 +1,6 @@ //! GitButler internal library containing functionality related to branches, i.e. the virtual branches implementation mod actions; // This is our API -#[allow(deprecated)] pub use actions::{ amend, can_apply_remote_branch, create_commit, create_virtual_branch, create_virtual_branch_from_branch, delete_local_branch, fetch_from_remotes, find_commit, @@ -75,6 +74,5 @@ pub use branch::{ pub use integration::GITBUTLER_WORKSPACE_COMMIT_TITLE; -mod commit_ops; pub mod hooks; pub mod stack; diff --git a/crates/gitbutler-branch-actions/src/move_commits.rs b/crates/gitbutler-branch-actions/src/move_commits.rs index 1cc5c660c9..2ec3d10333 100644 --- a/crates/gitbutler-branch-actions/src/move_commits.rs +++ b/crates/gitbutler-branch-actions/src/move_commits.rs @@ -1,6 +1,5 @@ use std::collections::HashMap; -#[allow(deprecated)] use crate::dependencies::commit_dependencies_from_workspace; use crate::VirtualBranchesExt; use crate::{compute_workspace_dependencies, BranchStatus}; diff --git a/crates/gitbutler-branch-actions/src/reorder.rs b/crates/gitbutler-branch-actions/src/reorder.rs index 3a11a7d470..5f1b6ca82d 100644 --- a/crates/gitbutler-branch-actions/src/reorder.rs +++ b/crates/gitbutler-branch-actions/src/reorder.rs @@ -6,7 +6,6 @@ use gitbutler_oxidize::{ObjectIdExt, OidExt}; use gitbutler_project::access::WorktreeWritePermission; use gitbutler_stack::{Stack, StackId}; -#[allow(deprecated)] use gitbutler_workspace::branch_trees::{update_uncommited_changes, WorkspaceState}; use itertools::Itertools; use serde::{Deserialize, Serialize}; diff --git a/crates/gitbutler-branch-actions/src/upstream_integration.rs b/crates/gitbutler-branch-actions/src/upstream_integration.rs index 10db2e4a78..188462e01b 100644 --- a/crates/gitbutler-branch-actions/src/upstream_integration.rs +++ b/crates/gitbutler-branch-actions/src/upstream_integration.rs @@ -17,7 +17,6 @@ use gitbutler_serde::BStringForFrontend; use gitbutler_stack::{Stack, StackId, Target, VirtualBranchesHandle}; use gitbutler_workspace::branch_trees::{update_uncommited_changes, WorkspaceState}; -#[allow(deprecated)] use gix::merge::tree::TreatAsUnresolved; use serde::{Deserialize, Serialize}; diff --git a/crates/gitbutler-branch-actions/src/virtual.rs b/crates/gitbutler-branch-actions/src/virtual.rs index 5c65be4ec5..6c74c97d15 100644 --- a/crates/gitbutler-branch-actions/src/virtual.rs +++ b/crates/gitbutler-branch-actions/src/virtual.rs @@ -219,7 +219,6 @@ pub(crate) fn set_ownership( pub type BranchStatus = HashMap>; pub type VirtualBranchHunksByPathMap = HashMap>; -#[allow(clippy::too_many_arguments)] pub fn commit( ctx: &CommandContext, stack_id: StackId, diff --git a/crates/gitbutler-branch-actions/tests/squash.rs b/crates/gitbutler-branch-actions/tests/squash.rs index dd7273a106..b327f976e1 100644 --- a/crates/gitbutler-branch-actions/tests/squash.rs +++ b/crates/gitbutler-branch-actions/tests/squash.rs @@ -490,7 +490,7 @@ fn test_ctx(ctx: &CommandContext) -> Result> { // - commit 3 // - commit 2 // - commit 1 (a-branch-1) -#[allow(unused)] +#[expect(unused)] struct TestContext<'a> { stack: gitbutler_stack::Stack, branch_1: StackBranch, diff --git a/crates/gitbutler-branch-actions/tests/virtual_branches/mod.rs b/crates/gitbutler-branch-actions/tests/virtual_branches/mod.rs index 55eebab846..296dbff6d6 100644 --- a/crates/gitbutler-branch-actions/tests/virtual_branches/mod.rs +++ b/crates/gitbutler-branch-actions/tests/virtual_branches/mod.rs @@ -49,7 +49,7 @@ impl Default for Test { impl Test { /// Consume this instance and keep the temp directory that held the local repository, returning it. /// Best used inside a `dbg!(test.debug_local_repo())` - #[allow(dead_code)] + #[expect(dead_code)] pub fn debug_local_repo(&mut self) -> Option { self.repo.debug_local_repo() } diff --git a/crates/gitbutler-diff/src/write.rs b/crates/gitbutler-diff/src/write.rs index 2f834f1009..c9ed6fd21a 100644 --- a/crates/gitbutler-diff/src/write.rs +++ b/crates/gitbutler-diff/src/write.rs @@ -239,7 +239,7 @@ pub fn apply>(base_image: S, patch: &Patch<'_, [u8]>) -> Result>(base_image: S, patch: &Patch<'_, [u8]>) -> Result>(base_image: S, patch: &Patch<'_, [u8]>) -> Result, diff --git a/crates/gitbutler-edit-mode/src/lib.rs b/crates/gitbutler-edit-mode/src/lib.rs index e555e67a0f..f4e894f08f 100644 --- a/crates/gitbutler-edit-mode/src/lib.rs +++ b/crates/gitbutler-edit-mode/src/lib.rs @@ -24,7 +24,6 @@ use gitbutler_repo::RepositoryExt; use gitbutler_repo::{signature, SignaturePurpose}; use gitbutler_stack::VirtualBranchesHandle; use gitbutler_workspace::branch_trees::{update_uncommited_changes_with_tree, WorkspaceState}; -#[allow(deprecated)] use serde::Serialize; pub mod commands; diff --git a/crates/gitbutler-feedback/src/zipper/mod.rs b/crates/gitbutler-feedback/src/zipper/mod.rs index 1b9ed34867..b38d76042b 100644 --- a/crates/gitbutler-feedback/src/zipper/mod.rs +++ b/crates/gitbutler-feedback/src/zipper/mod.rs @@ -82,7 +82,6 @@ where // Write file or directory explicitly // Some unzip tools unzip files with directory paths correctly, some do not! if path.is_file() { - #[allow(deprecated)] zip.start_file_from_path(name, options)?; let mut f = fs::File::open(path)?; @@ -92,7 +91,6 @@ where } else if !name.as_os_str().is_empty() { // Only if not root! Avoids path spec / warning // and mapname conversion failed error on unzip - #[allow(deprecated)] zip.add_directory_from_path(name, options)?; } } diff --git a/crates/gitbutler-filemonitor/vendor/debouncer/src/cache.rs b/crates/gitbutler-filemonitor/vendor/debouncer/src/cache.rs index 55bdb109ff..600923e797 100644 --- a/crates/gitbutler-filemonitor/vendor/debouncer/src/cache.rs +++ b/crates/gitbutler-filemonitor/vendor/debouncer/src/cache.rs @@ -103,7 +103,7 @@ impl FileIdMap { fn dir_scan_depth(is_recursive: bool) -> usize { if is_recursive { // TODO - #[allow(clippy::legacy_numeric_constants)] + #[expect(clippy::legacy_numeric_constants)] usize::max_value() } else { 1 diff --git a/crates/gitbutler-git/src/executor/mod.rs b/crates/gitbutler-git/src/executor/mod.rs index 019fcfd7e0..1b86c15ba3 100644 --- a/crates/gitbutler-git/src/executor/mod.rs +++ b/crates/gitbutler-git/src/executor/mod.rs @@ -34,7 +34,7 @@ pub mod tokio; /// we have some loose checks to ensure that the invariants are upheld, /// we cannot guarantee that they are upheld in all cases. Thus, it is /// up to the implementor to ensure that the invariants are upheld. -#[allow(unsafe_code)] +#[expect(unsafe_code)] pub unsafe trait GitExecutor { /// The error type returned by this executor, /// specifically in cases where the execution fails. diff --git a/crates/gitbutler-git/src/executor/tokio/mod.rs b/crates/gitbutler-git/src/executor/tokio/mod.rs index 1a810b7423..4c372e426a 100644 --- a/crates/gitbutler-git/src/executor/tokio/mod.rs +++ b/crates/gitbutler-git/src/executor/tokio/mod.rs @@ -18,7 +18,7 @@ pub use self::windows::TokioAskpassServer; /// via [`tokio::process::Command`]. pub struct TokioExecutor; -#[allow(unsafe_code)] +#[expect(unsafe_code)] unsafe impl super::GitExecutor for TokioExecutor { type Error = std::io::Error; type ServerHandle = TokioAskpassServer; @@ -144,7 +144,7 @@ mod tests { async fn test_askpass() { let secret = "super-secret-secret"; let executor = TokioExecutor; - #[allow(unsafe_code)] + #[expect(unsafe_code)] let sock_server: TokioAskpassServer = unsafe { executor.create_askpass_server() } .await .expect("create_askpass_server():"); diff --git a/crates/gitbutler-git/src/executor/tokio/unix.rs b/crates/gitbutler-git/src/executor/tokio/unix.rs index 30071269f0..929e106c00 100644 --- a/crates/gitbutler-git/src/executor/tokio/unix.rs +++ b/crates/gitbutler-git/src/executor/tokio/unix.rs @@ -34,7 +34,7 @@ impl Socket for BufStream { let mut buf = String::new(); ::read_line(self, &mut buf).await?; // TODO: use an array of `char` - #[allow(clippy::manual_pattern_char_comparison)] + #[expect(clippy::manual_pattern_char_comparison)] Ok(buf.trim_end_matches(|c| c == '\r' || c == '\n').into()) } diff --git a/crates/gitbutler-git/src/executor/tokio/windows.rs b/crates/gitbutler-git/src/executor/tokio/windows.rs index 7233d8c779..7cc3f3accc 100644 --- a/crates/gitbutler-git/src/executor/tokio/windows.rs +++ b/crates/gitbutler-git/src/executor/tokio/windows.rs @@ -25,7 +25,7 @@ impl Socket for BufStream { let handle: HANDLE = HANDLE(raw_handle); let mut out_pid: u32 = 0; - #[allow(unsafe_code)] + #[expect(unsafe_code)] let r = unsafe { GetNamedPipeClientProcessId(handle, &mut out_pid) }; match r { @@ -77,7 +77,7 @@ impl AskpassServer for TokioAskpassServer { type SocketHandle = BufStream; // We can ignore clippy here since we locked the mutex. - #[allow(clippy::await_holding_refcell_ref)] + #[expect(clippy::await_holding_refcell_ref)] async fn accept(&self, timeout: Option) -> Result { let server = self.server.lock().await; diff --git a/crates/gitbutler-git/src/repository.rs b/crates/gitbutler-git/src/repository.rs index 57f44d3166..b0d7ea9c90 100644 --- a/crates/gitbutler-git/src/repository.rs +++ b/crates/gitbutler-git/src/repository.rs @@ -121,7 +121,7 @@ where .await .map_err(Error::::Exec)?; - #[allow(unsafe_code)] + #[expect(unsafe_code)] let sock_server = unsafe { executor.create_askpass_server() } .await .map_err(Error::::Exec)?; @@ -219,7 +219,6 @@ where system.refresh_processes(sysinfo::ProcessesToUpdate::All, true); // We can ignore clippy here since the type is different depending on the platform. - #[allow(clippy::useless_conversion)] let peer_path = system .process(sysinfo::Pid::from_u32(peer_pid.try_into().map_err(|_| Error::::NoSuchPid(peer_pid))?)) .and_then(|p| p.exe().map(|exe| exe.to_string_lossy().into_owned())) @@ -338,7 +337,7 @@ where /// Any prompts for the user are passed to the asynchronous callback `on_prompt`, /// which should return the user's response or `None` if the operation should be /// aborted, in which case an `Err` value is returned from this function. -#[allow(clippy::too_many_arguments)] +#[expect(clippy::too_many_arguments)] pub async fn push( repo_path: P, executor: E, diff --git a/crates/gitbutler-oplog/Cargo.toml b/crates/gitbutler-oplog/Cargo.toml index 7e97e718d8..dfebf96435 100644 --- a/crates/gitbutler-oplog/Cargo.toml +++ b/crates/gitbutler-oplog/Cargo.toml @@ -24,7 +24,6 @@ gitbutler-branch.workspace = true gitbutler-serde.workspace = true gitbutler-fs.workspace = true gitbutler-reference.workspace = true -gitbutler-diff.workspace = true gitbutler-stack.workspace = true but-core.workspace = true diff --git a/crates/gitbutler-project/src/default_true.rs b/crates/gitbutler-project/src/default_true.rs index fbfa3c21c0..ec0f31a7e1 100644 --- a/crates/gitbutler-project/src/default_true.rs +++ b/crates/gitbutler-project/src/default_true.rs @@ -90,7 +90,7 @@ impl core::ops::Not for DefaultTrue { } #[test] -#[allow(clippy::bool_assert_comparison)] +#[expect(clippy::bool_assert_comparison)] fn default_true() { let default_true = DefaultTrue::default(); assert!(default_true); diff --git a/crates/gitbutler-repo-actions/src/askpass.rs b/crates/gitbutler-repo-actions/src/askpass.rs index b1f1666201..145c30f8d7 100644 --- a/crates/gitbutler-repo-actions/src/askpass.rs +++ b/crates/gitbutler-repo-actions/src/askpass.rs @@ -14,7 +14,7 @@ static mut GLOBAL_ASKPASS_BROKER: Option = None; /// before any other function from this module is called. **Calls to [`get_broker`] before [`init`] will panic.** /// /// This function is **NOT** thread safe. -#[allow(static_mut_refs)] +#[expect(static_mut_refs)] pub unsafe fn init(submit_prompt: impl Fn(PromptEvent) + Send + Sync + 'static) { GLOBAL_ASKPASS_BROKER.replace(AskpassBroker::init(submit_prompt)); } @@ -23,7 +23,7 @@ pub unsafe fn init(submit_prompt: impl Fn(PromptEvent) + Send + Sync + /// /// # Panics /// Will panic if [`init`] was not called before this function. -#[allow(static_mut_refs)] +#[expect(static_mut_refs)] pub fn get_broker() -> &'static AskpassBroker { unsafe { GLOBAL_ASKPASS_BROKER diff --git a/crates/gitbutler-repo/src/repository_ext.rs b/crates/gitbutler-repo/src/repository_ext.rs index fca8bc8b9c..6d42b3ca1a 100644 --- a/crates/gitbutler-repo/src/repository_ext.rs +++ b/crates/gitbutler-repo/src/repository_ext.rs @@ -56,7 +56,7 @@ pub trait RepositoryExt { /// This is for safety to assure the repository actually is in 'gitbutler mode'. fn workspace_ref_from_head(&self) -> Result>; - #[allow(clippy::too_many_arguments)] + #[expect(clippy::too_many_arguments)] fn commit_with_signature( &self, update_ref: Option<&Refname>, @@ -140,7 +140,6 @@ impl RepositoryExt for git2::Repository { } } - #[allow(clippy::too_many_arguments)] fn commit_with_signature( &self, update_ref: Option<&Refname>, diff --git a/crates/gitbutler-stack/src/lib.rs b/crates/gitbutler-stack/src/lib.rs index 3cf5b24e54..89cab713b5 100644 --- a/crates/gitbutler-stack/src/lib.rs +++ b/crates/gitbutler-stack/src/lib.rs @@ -16,7 +16,7 @@ pub use heads::add_head; pub use stack::{PatchReferenceUpdate, TargetUpdate}; // This is here because CommitOrChangeId::ChangeId is deprecated, for some reason allow cant be done on the CommitOrChangeId struct -#[allow(deprecated)] +#[expect(deprecated)] mod stack_branch; pub use stack::canned_branch_name; pub use stack_branch::{CommitOrChangeId, StackBranch}; diff --git a/crates/gitbutler-stack/src/stack.rs b/crates/gitbutler-stack/src/stack.rs index 6347024271..4620d439fe 100644 --- a/crates/gitbutler-stack/src/stack.rs +++ b/crates/gitbutler-stack/src/stack.rs @@ -186,7 +186,7 @@ impl From for virtual_branches_legacy_types::Stack { /// If there are multiple heads that point to the same patch, the `add` and `update` operations can specify the intended order. impl Stack { /// Creates a new `Branch` with the given name. The `in_workspace` flag is set to `true`. - #[allow(clippy::too_many_arguments)] + #[expect(clippy::too_many_arguments)] #[deprecated(note = "DO NOT USE THIS DIRECTLY, use `stack_ext::StackExt::create` instead.")] pub fn new( name: String, @@ -297,7 +297,7 @@ impl Stack { // TODO: When this is stable, make it error out on initialization failure /// Constructs and initializes a new Stack. /// If initialization fails, a warning is logged and the stack is returned as is. - #[allow(clippy::too_many_arguments)] + #[expect(clippy::too_many_arguments)] pub fn create( ctx: &CommandContext, name: String, @@ -311,7 +311,7 @@ impl Stack { allow_rebasing: bool, allow_duplicate_refs: bool, ) -> Result { - #[allow(deprecated)] + #[expect(deprecated)] // this should be the only place (other than tests) where this is allowed let mut branch = Stack::new( name, @@ -643,7 +643,6 @@ impl Stack { ) -> Result<()> { self.ensure_initialized()?; self.updated_timestamp_ms = gitbutler_time::time::now_ms(); - #[allow(deprecated)] // this is the only place where this is allowed self.set_head(commit_id); if let Some(tree) = tree { self.tree = tree; @@ -784,9 +783,8 @@ impl Stack { } /// Migrates all change IDs in stack heads to commit IDs. - #[allow(deprecated)] pub fn migrate_change_ids(&mut self, ctx: &CommandContext) -> Result<()> { - // If all of the heads are already commit IDs, there is nothing to do + // If all the heads are already commit IDs, there is nothing to do if self.heads.iter().all(|h| !h.uses_change_id()) { return Ok(()); } diff --git a/crates/gitbutler-stack/src/stack_branch.rs b/crates/gitbutler-stack/src/stack_branch.rs index d20660d19e..dcb18b3e8d 100644 --- a/crates/gitbutler-stack/src/stack_branch.rs +++ b/crates/gitbutler-stack/src/stack_branch.rs @@ -123,7 +123,7 @@ impl Display for CommitOrChangeId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { CommitOrChangeId::CommitId(id) => write!(f, "CommitId: {}", id), - #[allow(deprecated)] + #[expect(deprecated)] CommitOrChangeId::ChangeId(id) => write!(f, "ChangeId: {}", id), } } @@ -199,7 +199,7 @@ impl StackBranch { stack_head: git2::Oid, merge_base: git2::Oid, ) { - #[allow(deprecated)] + #[expect(deprecated)] if let CommitOrChangeId::ChangeId(_) = &self.head { if let core::result::Result::Ok(commit) = commit_by_oid_or_change_id(&self.head.clone(), repo, stack_head, merge_base) @@ -511,7 +511,7 @@ fn commit_by_oid_or_change_id<'a>( ) -> Result> { Ok(match reference_target { CommitOrChangeId::CommitId(commit_id) => repo.find_commit(commit_id.parse()?)?, - #[allow(deprecated)] + #[expect(deprecated)] CommitOrChangeId::ChangeId(change_id) => { commit_by_branch_id_and_change_id(repo, stack_head, merge_base, change_id)? } diff --git a/crates/gitbutler-stack/tests/mod.rs b/crates/gitbutler-stack/tests/mod.rs index 791481d196..0dbfe8c9c3 100644 --- a/crates/gitbutler-stack/tests/mod.rs +++ b/crates/gitbutler-stack/tests/mod.rs @@ -841,7 +841,6 @@ struct TestContext<'a> { /// Oldest commit first commits: Vec>, /// Oldest commit first - #[allow(dead_code)] other_commits: Vec>, handle: VirtualBranchesHandle, default_target: gitbutler_stack::Target, diff --git a/crates/gitbutler-stack/tests/ownership.rs b/crates/gitbutler-stack/tests/ownership.rs index bd1362ad71..2a9207c29a 100644 --- a/crates/gitbutler-stack/tests/ownership.rs +++ b/crates/gitbutler-stack/tests/ownership.rs @@ -5,7 +5,7 @@ use gitbutler_stack::{reconcile_claims, BranchOwnershipClaims, OwnershipClaim, S #[test] fn reconcile_ownership_simple() { - #[allow(deprecated)] // this is a test + #[expect(deprecated)] // this is a test let mut branch_a = Stack::new( "a".to_string(), None, @@ -39,7 +39,7 @@ fn reconcile_ownership_simple() { branch_a.created_timestamp_ms = u128::default(); branch_a.updated_timestamp_ms = u128::default(); - #[allow(deprecated)] // this is a test + #[expect(deprecated)] // this is a test let mut branch_b = Stack::new( "b".to_string(), None, diff --git a/crates/gitbutler-tauri/build.rs b/crates/gitbutler-tauri/build.rs index 0421f95f9a..22388b199f 100644 --- a/crates/gitbutler-tauri/build.rs +++ b/crates/gitbutler-tauri/build.rs @@ -16,7 +16,7 @@ fn main() { if !build_dir.exists() { // NOTE(qix-): Do not use `create_dir_all` here - the parent directory // NOTE(qix-): already exists, and we want to fail if not (for some reason). - #[allow(clippy::expect_fun_call, clippy::create_dir)] + #[expect(clippy::expect_fun_call, clippy::create_dir)] std::fs::create_dir(&build_dir).expect( format!( "failed to create apps/desktop/build directory: {:?}", diff --git a/crates/gitbutler-tauri/src/window.rs b/crates/gitbutler-tauri/src/window.rs index 5b1f307719..a7f807c7fd 100644 --- a/crates/gitbutler-tauri/src/window.rs +++ b/crates/gitbutler-tauri/src/window.rs @@ -138,7 +138,7 @@ pub(crate) mod state { /// Let's make it optional while it's only in our own way, while aiming for making that reasonably well working. exclusive_access: Option, // Database watcher handle. - #[allow(dead_code)] + #[expect(dead_code)] db_watcher: but_db::poll::DBWatcherHandle, } diff --git a/crates/gitbutler-tauri/src/workspace.rs b/crates/gitbutler-tauri/src/workspace.rs index fb21578f7a..695917f482 100644 --- a/crates/gitbutler-tauri/src/workspace.rs +++ b/crates/gitbutler-tauri/src/workspace.rs @@ -67,7 +67,6 @@ pub fn branch_details( #[tauri::command(async)] #[instrument(skip(app), err(Debug))] -#[allow(clippy::too_many_arguments)] pub fn create_commit_from_worktree_changes( app: State<'_, but_api::App>, project_id: ProjectId, @@ -128,7 +127,6 @@ pub fn discard_worktree_changes( #[tauri::command(async)] #[instrument(skip(app), err(Debug))] -#[allow(clippy::too_many_arguments)] pub fn move_changes_between_commits( app: State<'_, but_api::App>, project_id: ProjectId, @@ -153,7 +151,6 @@ pub fn move_changes_between_commits( #[tauri::command(async)] #[instrument(skip(app), err(Debug))] -#[allow(clippy::too_many_arguments)] pub fn split_branch( app: State<'_, but_api::App>, project_id: ProjectId, @@ -176,7 +173,6 @@ pub fn split_branch( #[tauri::command(async)] #[instrument(skip(app), err(Debug))] -#[allow(clippy::too_many_arguments)] pub fn split_branch_into_dependent_branch( app: State<'_, but_api::App>, project_id: ProjectId, @@ -199,7 +195,6 @@ pub fn split_branch_into_dependent_branch( #[tauri::command(async)] #[instrument(skip(app), err(Debug))] -#[allow(clippy::too_many_arguments)] pub fn uncommit_changes( app: State<'_, but_api::App>, project_id: ProjectId, diff --git a/crates/gitbutler-watcher/src/events.rs b/crates/gitbutler-watcher/src/events.rs index e1a4047fec..7b97d783ca 100644 --- a/crates/gitbutler-watcher/src/events.rs +++ b/crates/gitbutler-watcher/src/events.rs @@ -3,7 +3,7 @@ use gitbutler_project::ProjectId; /// An event telling the receiver something about the state of the application which just changed. #[derive(Debug, Clone)] -#[allow(missing_docs)] +#[expect(missing_docs)] pub enum Change { GitFetch(ProjectId), GitHead { diff --git a/crates/gitbutler-watcher/src/handler.rs b/crates/gitbutler-watcher/src/handler.rs index 3f81035fc2..a0c9ac37c1 100644 --- a/crates/gitbutler-watcher/src/handler.rs +++ b/crates/gitbutler-watcher/src/handler.rs @@ -23,13 +23,11 @@ pub struct Handler { // the tauri app, assuming that such application would not be `Send + Sync` everywhere and thus would // need extra protection. /// A function to send events - decoupled from app-handle for testing purposes. - #[allow(clippy::type_complexity)] send_event: Arc Result<()> + Send + Sync + 'static>, } impl Handler { /// A constructor whose primary use is the test-suite. - #[allow(clippy::too_many_arguments)] pub fn new(send_event: impl Fn(Change) -> Result<()> + Send + Sync + 'static) -> Self { Handler { send_event: Arc::new(send_event), diff --git a/crates/gitbutler-workspace/src/branch_trees.rs b/crates/gitbutler-workspace/src/branch_trees.rs index 6a5f29f864..f800b475e9 100644 --- a/crates/gitbutler-workspace/src/branch_trees.rs +++ b/crates/gitbutler-workspace/src/branch_trees.rs @@ -176,7 +176,7 @@ pub fn compute_updated_branch_head( new_head: git2::Oid, ctx: &CommandContext, ) -> Result { - #[allow(deprecated)] + #[expect(deprecated)] compute_updated_branch_head_for_commits( repo, gix_repo, diff --git a/crates/gitbutler-workspace/src/lib.rs b/crates/gitbutler-workspace/src/lib.rs index 9826f87de8..be7506c887 100644 --- a/crates/gitbutler-workspace/src/lib.rs +++ b/crates/gitbutler-workspace/src/lib.rs @@ -1,6 +1,6 @@ pub mod branch_trees; -#[allow(deprecated)] +#[expect(deprecated)] pub use branch_trees::{ compute_updated_branch_head, compute_updated_branch_head_for_commits, BranchHeadAndTree, };