diff --git a/Cargo.lock b/Cargo.lock index 840253f75b..cfff351c14 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2687,6 +2687,7 @@ dependencies = [ "gitbutler-command-context", "gitbutler-diff", "gitbutler-fs", + "gitbutler-oxidize", "gitbutler-project", "gitbutler-reference", "gitbutler-repo", @@ -3038,7 +3039,7 @@ dependencies = [ [[package]] name = "gix" version = "0.67.0" -source = "git+https://github.com/Byron/gitoxide?rev=3fb989be21c739bbfeac93953c1685e7c6cd2106#3fb989be21c739bbfeac93953c1685e7c6cd2106" +source = "git+https://github.com/Byron/gitoxide?rev=a8765330fc16997dee275866b18a128dec1c5d55#a8765330fc16997dee275866b18a128dec1c5d55" dependencies = [ "gix-actor 0.33.0", "gix-attributes 0.23.0", @@ -3106,7 +3107,7 @@ dependencies = [ [[package]] name = "gix-actor" version = "0.33.0" -source = "git+https://github.com/Byron/gitoxide?rev=3fb989be21c739bbfeac93953c1685e7c6cd2106#3fb989be21c739bbfeac93953c1685e7c6cd2106" +source = "git+https://github.com/Byron/gitoxide?rev=a8765330fc16997dee275866b18a128dec1c5d55#a8765330fc16997dee275866b18a128dec1c5d55" dependencies = [ "bstr", "gix-date 0.9.1", @@ -3136,7 +3137,7 @@ dependencies = [ [[package]] name = "gix-attributes" version = "0.23.0" -source = "git+https://github.com/Byron/gitoxide?rev=3fb989be21c739bbfeac93953c1685e7c6cd2106#3fb989be21c739bbfeac93953c1685e7c6cd2106" +source = "git+https://github.com/Byron/gitoxide?rev=a8765330fc16997dee275866b18a128dec1c5d55#a8765330fc16997dee275866b18a128dec1c5d55" dependencies = [ "bstr", "gix-glob 0.17.0", @@ -3161,7 +3162,7 @@ dependencies = [ [[package]] name = "gix-bitmap" version = "0.2.12" -source = "git+https://github.com/Byron/gitoxide?rev=3fb989be21c739bbfeac93953c1685e7c6cd2106#3fb989be21c739bbfeac93953c1685e7c6cd2106" +source = "git+https://github.com/Byron/gitoxide?rev=a8765330fc16997dee275866b18a128dec1c5d55#a8765330fc16997dee275866b18a128dec1c5d55" dependencies = [ "thiserror", ] @@ -3178,7 +3179,7 @@ dependencies = [ [[package]] name = "gix-chunk" version = "0.4.9" -source = "git+https://github.com/Byron/gitoxide?rev=3fb989be21c739bbfeac93953c1685e7c6cd2106#3fb989be21c739bbfeac93953c1685e7c6cd2106" +source = "git+https://github.com/Byron/gitoxide?rev=a8765330fc16997dee275866b18a128dec1c5d55#a8765330fc16997dee275866b18a128dec1c5d55" dependencies = [ "thiserror", ] @@ -3186,7 +3187,7 @@ dependencies = [ [[package]] name = "gix-command" version = "0.3.10" -source = "git+https://github.com/Byron/gitoxide?rev=3fb989be21c739bbfeac93953c1685e7c6cd2106#3fb989be21c739bbfeac93953c1685e7c6cd2106" +source = "git+https://github.com/Byron/gitoxide?rev=a8765330fc16997dee275866b18a128dec1c5d55#a8765330fc16997dee275866b18a128dec1c5d55" dependencies = [ "bstr", "gix-path 0.10.12", @@ -3211,7 +3212,7 @@ dependencies = [ [[package]] name = "gix-commitgraph" version = "0.25.0" -source = "git+https://github.com/Byron/gitoxide?rev=3fb989be21c739bbfeac93953c1685e7c6cd2106#3fb989be21c739bbfeac93953c1685e7c6cd2106" +source = "git+https://github.com/Byron/gitoxide?rev=a8765330fc16997dee275866b18a128dec1c5d55#a8765330fc16997dee275866b18a128dec1c5d55" dependencies = [ "bstr", "gix-chunk 0.4.9", @@ -3224,7 +3225,7 @@ dependencies = [ [[package]] name = "gix-config" version = "0.41.0" -source = "git+https://github.com/Byron/gitoxide?rev=3fb989be21c739bbfeac93953c1685e7c6cd2106#3fb989be21c739bbfeac93953c1685e7c6cd2106" +source = "git+https://github.com/Byron/gitoxide?rev=a8765330fc16997dee275866b18a128dec1c5d55#a8765330fc16997dee275866b18a128dec1c5d55" dependencies = [ "bstr", "gix-config-value", @@ -3244,7 +3245,7 @@ dependencies = [ [[package]] name = "gix-config-value" version = "0.14.9" -source = "git+https://github.com/Byron/gitoxide?rev=3fb989be21c739bbfeac93953c1685e7c6cd2106#3fb989be21c739bbfeac93953c1685e7c6cd2106" +source = "git+https://github.com/Byron/gitoxide?rev=a8765330fc16997dee275866b18a128dec1c5d55#a8765330fc16997dee275866b18a128dec1c5d55" dependencies = [ "bitflags 2.6.0", "bstr", @@ -3256,7 +3257,7 @@ dependencies = [ [[package]] name = "gix-credentials" version = "0.25.0" -source = "git+https://github.com/Byron/gitoxide?rev=3fb989be21c739bbfeac93953c1685e7c6cd2106#3fb989be21c739bbfeac93953c1685e7c6cd2106" +source = "git+https://github.com/Byron/gitoxide?rev=a8765330fc16997dee275866b18a128dec1c5d55#a8765330fc16997dee275866b18a128dec1c5d55" dependencies = [ "bstr", "gix-command", @@ -3284,7 +3285,7 @@ dependencies = [ [[package]] name = "gix-date" version = "0.9.1" -source = "git+https://github.com/Byron/gitoxide?rev=3fb989be21c739bbfeac93953c1685e7c6cd2106#3fb989be21c739bbfeac93953c1685e7c6cd2106" +source = "git+https://github.com/Byron/gitoxide?rev=a8765330fc16997dee275866b18a128dec1c5d55#a8765330fc16997dee275866b18a128dec1c5d55" dependencies = [ "bstr", "itoa 1.0.11", @@ -3295,7 +3296,7 @@ dependencies = [ [[package]] name = "gix-diff" version = "0.47.0" -source = "git+https://github.com/Byron/gitoxide?rev=3fb989be21c739bbfeac93953c1685e7c6cd2106#3fb989be21c739bbfeac93953c1685e7c6cd2106" +source = "git+https://github.com/Byron/gitoxide?rev=a8765330fc16997dee275866b18a128dec1c5d55#a8765330fc16997dee275866b18a128dec1c5d55" dependencies = [ "bstr", "gix-command", @@ -3315,7 +3316,7 @@ dependencies = [ [[package]] name = "gix-dir" version = "0.9.0" -source = "git+https://github.com/Byron/gitoxide?rev=3fb989be21c739bbfeac93953c1685e7c6cd2106#3fb989be21c739bbfeac93953c1685e7c6cd2106" +source = "git+https://github.com/Byron/gitoxide?rev=a8765330fc16997dee275866b18a128dec1c5d55#a8765330fc16997dee275866b18a128dec1c5d55" dependencies = [ "bstr", "gix-discover 0.36.0", @@ -3350,7 +3351,7 @@ dependencies = [ [[package]] name = "gix-discover" version = "0.36.0" -source = "git+https://github.com/Byron/gitoxide?rev=3fb989be21c739bbfeac93953c1685e7c6cd2106#3fb989be21c739bbfeac93953c1685e7c6cd2106" +source = "git+https://github.com/Byron/gitoxide?rev=a8765330fc16997dee275866b18a128dec1c5d55#a8765330fc16997dee275866b18a128dec1c5d55" dependencies = [ "bstr", "dunce", @@ -3380,7 +3381,7 @@ dependencies = [ [[package]] name = "gix-features" version = "0.39.0" -source = "git+https://github.com/Byron/gitoxide?rev=3fb989be21c739bbfeac93953c1685e7c6cd2106#3fb989be21c739bbfeac93953c1685e7c6cd2106" +source = "git+https://github.com/Byron/gitoxide?rev=a8765330fc16997dee275866b18a128dec1c5d55#a8765330fc16997dee275866b18a128dec1c5d55" dependencies = [ "bytes", "crc32fast", @@ -3402,7 +3403,7 @@ dependencies = [ [[package]] name = "gix-filter" version = "0.14.0" -source = "git+https://github.com/Byron/gitoxide?rev=3fb989be21c739bbfeac93953c1685e7c6cd2106#3fb989be21c739bbfeac93953c1685e7c6cd2106" +source = "git+https://github.com/Byron/gitoxide?rev=a8765330fc16997dee275866b18a128dec1c5d55#a8765330fc16997dee275866b18a128dec1c5d55" dependencies = [ "bstr", "encoding_rs", @@ -3433,7 +3434,7 @@ dependencies = [ [[package]] name = "gix-fs" version = "0.12.0" -source = "git+https://github.com/Byron/gitoxide?rev=3fb989be21c739bbfeac93953c1685e7c6cd2106#3fb989be21c739bbfeac93953c1685e7c6cd2106" +source = "git+https://github.com/Byron/gitoxide?rev=a8765330fc16997dee275866b18a128dec1c5d55#a8765330fc16997dee275866b18a128dec1c5d55" dependencies = [ "fastrand 2.1.1", "gix-features 0.39.0", @@ -3455,7 +3456,7 @@ dependencies = [ [[package]] name = "gix-glob" version = "0.17.0" -source = "git+https://github.com/Byron/gitoxide?rev=3fb989be21c739bbfeac93953c1685e7c6cd2106#3fb989be21c739bbfeac93953c1685e7c6cd2106" +source = "git+https://github.com/Byron/gitoxide?rev=a8765330fc16997dee275866b18a128dec1c5d55#a8765330fc16997dee275866b18a128dec1c5d55" dependencies = [ "bitflags 2.6.0", "bstr", @@ -3476,7 +3477,7 @@ dependencies = [ [[package]] name = "gix-hash" version = "0.15.0" -source = "git+https://github.com/Byron/gitoxide?rev=3fb989be21c739bbfeac93953c1685e7c6cd2106#3fb989be21c739bbfeac93953c1685e7c6cd2106" +source = "git+https://github.com/Byron/gitoxide?rev=a8765330fc16997dee275866b18a128dec1c5d55#a8765330fc16997dee275866b18a128dec1c5d55" dependencies = [ "faster-hex", "thiserror", @@ -3496,7 +3497,7 @@ dependencies = [ [[package]] name = "gix-hashtable" version = "0.6.0" -source = "git+https://github.com/Byron/gitoxide?rev=3fb989be21c739bbfeac93953c1685e7c6cd2106#3fb989be21c739bbfeac93953c1685e7c6cd2106" +source = "git+https://github.com/Byron/gitoxide?rev=a8765330fc16997dee275866b18a128dec1c5d55#a8765330fc16997dee275866b18a128dec1c5d55" dependencies = [ "gix-hash 0.15.0", "hashbrown 0.14.5", @@ -3519,7 +3520,7 @@ dependencies = [ [[package]] name = "gix-ignore" version = "0.12.0" -source = "git+https://github.com/Byron/gitoxide?rev=3fb989be21c739bbfeac93953c1685e7c6cd2106#3fb989be21c739bbfeac93953c1685e7c6cd2106" +source = "git+https://github.com/Byron/gitoxide?rev=a8765330fc16997dee275866b18a128dec1c5d55#a8765330fc16997dee275866b18a128dec1c5d55" dependencies = [ "bstr", "gix-glob 0.17.0", @@ -3559,7 +3560,7 @@ dependencies = [ [[package]] name = "gix-index" version = "0.36.0" -source = "git+https://github.com/Byron/gitoxide?rev=3fb989be21c739bbfeac93953c1685e7c6cd2106#3fb989be21c739bbfeac93953c1685e7c6cd2106" +source = "git+https://github.com/Byron/gitoxide?rev=a8765330fc16997dee275866b18a128dec1c5d55#a8765330fc16997dee275866b18a128dec1c5d55" dependencies = [ "bitflags 2.6.0", "bstr", @@ -3597,7 +3598,7 @@ dependencies = [ [[package]] name = "gix-lock" version = "15.0.0" -source = "git+https://github.com/Byron/gitoxide?rev=3fb989be21c739bbfeac93953c1685e7c6cd2106#3fb989be21c739bbfeac93953c1685e7c6cd2106" +source = "git+https://github.com/Byron/gitoxide?rev=a8765330fc16997dee275866b18a128dec1c5d55#a8765330fc16997dee275866b18a128dec1c5d55" dependencies = [ "gix-tempfile 15.0.0", "gix-utils 0.1.13", @@ -3607,7 +3608,7 @@ dependencies = [ [[package]] name = "gix-merge" version = "0.0.0" -source = "git+https://github.com/Byron/gitoxide?rev=3fb989be21c739bbfeac93953c1685e7c6cd2106#3fb989be21c739bbfeac93953c1685e7c6cd2106" +source = "git+https://github.com/Byron/gitoxide?rev=a8765330fc16997dee275866b18a128dec1c5d55#a8765330fc16997dee275866b18a128dec1c5d55" dependencies = [ "bstr", "gix-command", @@ -3630,7 +3631,7 @@ dependencies = [ [[package]] name = "gix-negotiate" version = "0.16.0" -source = "git+https://github.com/Byron/gitoxide?rev=3fb989be21c739bbfeac93953c1685e7c6cd2106#3fb989be21c739bbfeac93953c1685e7c6cd2106" +source = "git+https://github.com/Byron/gitoxide?rev=a8765330fc16997dee275866b18a128dec1c5d55#a8765330fc16997dee275866b18a128dec1c5d55" dependencies = [ "bitflags 2.6.0", "gix-commitgraph 0.25.0", @@ -3664,7 +3665,7 @@ dependencies = [ [[package]] name = "gix-object" version = "0.45.0" -source = "git+https://github.com/Byron/gitoxide?rev=3fb989be21c739bbfeac93953c1685e7c6cd2106#3fb989be21c739bbfeac93953c1685e7c6cd2106" +source = "git+https://github.com/Byron/gitoxide?rev=a8765330fc16997dee275866b18a128dec1c5d55#a8765330fc16997dee275866b18a128dec1c5d55" dependencies = [ "bstr", "gix-actor 0.33.0", @@ -3683,7 +3684,7 @@ dependencies = [ [[package]] name = "gix-odb" version = "0.64.0" -source = "git+https://github.com/Byron/gitoxide?rev=3fb989be21c739bbfeac93953c1685e7c6cd2106#3fb989be21c739bbfeac93953c1685e7c6cd2106" +source = "git+https://github.com/Byron/gitoxide?rev=a8765330fc16997dee275866b18a128dec1c5d55#a8765330fc16997dee275866b18a128dec1c5d55" dependencies = [ "arc-swap", "gix-date 0.9.1", @@ -3703,7 +3704,7 @@ dependencies = [ [[package]] name = "gix-pack" version = "0.54.0" -source = "git+https://github.com/Byron/gitoxide?rev=3fb989be21c739bbfeac93953c1685e7c6cd2106#3fb989be21c739bbfeac93953c1685e7c6cd2106" +source = "git+https://github.com/Byron/gitoxide?rev=a8765330fc16997dee275866b18a128dec1c5d55#a8765330fc16997dee275866b18a128dec1c5d55" dependencies = [ "clru", "gix-chunk 0.4.9", @@ -3723,7 +3724,7 @@ dependencies = [ [[package]] name = "gix-packetline" version = "0.18.0" -source = "git+https://github.com/Byron/gitoxide?rev=3fb989be21c739bbfeac93953c1685e7c6cd2106#3fb989be21c739bbfeac93953c1685e7c6cd2106" +source = "git+https://github.com/Byron/gitoxide?rev=a8765330fc16997dee275866b18a128dec1c5d55#a8765330fc16997dee275866b18a128dec1c5d55" dependencies = [ "bstr", "faster-hex", @@ -3734,7 +3735,7 @@ dependencies = [ [[package]] name = "gix-packetline-blocking" version = "0.18.0" -source = "git+https://github.com/Byron/gitoxide?rev=3fb989be21c739bbfeac93953c1685e7c6cd2106#3fb989be21c739bbfeac93953c1685e7c6cd2106" +source = "git+https://github.com/Byron/gitoxide?rev=a8765330fc16997dee275866b18a128dec1c5d55#a8765330fc16997dee275866b18a128dec1c5d55" dependencies = [ "bstr", "faster-hex", @@ -3758,7 +3759,7 @@ dependencies = [ [[package]] name = "gix-path" version = "0.10.12" -source = "git+https://github.com/Byron/gitoxide?rev=3fb989be21c739bbfeac93953c1685e7c6cd2106#3fb989be21c739bbfeac93953c1685e7c6cd2106" +source = "git+https://github.com/Byron/gitoxide?rev=a8765330fc16997dee275866b18a128dec1c5d55#a8765330fc16997dee275866b18a128dec1c5d55" dependencies = [ "bstr", "gix-trace 0.1.11", @@ -3770,7 +3771,7 @@ dependencies = [ [[package]] name = "gix-pathspec" version = "0.8.0" -source = "git+https://github.com/Byron/gitoxide?rev=3fb989be21c739bbfeac93953c1685e7c6cd2106#3fb989be21c739bbfeac93953c1685e7c6cd2106" +source = "git+https://github.com/Byron/gitoxide?rev=a8765330fc16997dee275866b18a128dec1c5d55#a8765330fc16997dee275866b18a128dec1c5d55" dependencies = [ "bitflags 2.6.0", "bstr", @@ -3784,7 +3785,7 @@ dependencies = [ [[package]] name = "gix-prompt" version = "0.8.8" -source = "git+https://github.com/Byron/gitoxide?rev=3fb989be21c739bbfeac93953c1685e7c6cd2106#3fb989be21c739bbfeac93953c1685e7c6cd2106" +source = "git+https://github.com/Byron/gitoxide?rev=a8765330fc16997dee275866b18a128dec1c5d55#a8765330fc16997dee275866b18a128dec1c5d55" dependencies = [ "gix-command", "gix-config-value", @@ -3796,7 +3797,7 @@ dependencies = [ [[package]] name = "gix-protocol" version = "0.46.0" -source = "git+https://github.com/Byron/gitoxide?rev=3fb989be21c739bbfeac93953c1685e7c6cd2106#3fb989be21c739bbfeac93953c1685e7c6cd2106" +source = "git+https://github.com/Byron/gitoxide?rev=a8765330fc16997dee275866b18a128dec1c5d55#a8765330fc16997dee275866b18a128dec1c5d55" dependencies = [ "bstr", "gix-credentials", @@ -3824,7 +3825,7 @@ dependencies = [ [[package]] name = "gix-quote" version = "0.4.13" -source = "git+https://github.com/Byron/gitoxide?rev=3fb989be21c739bbfeac93953c1685e7c6cd2106#3fb989be21c739bbfeac93953c1685e7c6cd2106" +source = "git+https://github.com/Byron/gitoxide?rev=a8765330fc16997dee275866b18a128dec1c5d55#a8765330fc16997dee275866b18a128dec1c5d55" dependencies = [ "bstr", "gix-utils 0.1.13", @@ -3856,7 +3857,7 @@ dependencies = [ [[package]] name = "gix-ref" version = "0.48.0" -source = "git+https://github.com/Byron/gitoxide?rev=3fb989be21c739bbfeac93953c1685e7c6cd2106#3fb989be21c739bbfeac93953c1685e7c6cd2106" +source = "git+https://github.com/Byron/gitoxide?rev=a8765330fc16997dee275866b18a128dec1c5d55#a8765330fc16997dee275866b18a128dec1c5d55" dependencies = [ "gix-actor 0.33.0", "gix-features 0.39.0", @@ -3876,7 +3877,7 @@ dependencies = [ [[package]] name = "gix-refspec" version = "0.26.0" -source = "git+https://github.com/Byron/gitoxide?rev=3fb989be21c739bbfeac93953c1685e7c6cd2106#3fb989be21c739bbfeac93953c1685e7c6cd2106" +source = "git+https://github.com/Byron/gitoxide?rev=a8765330fc16997dee275866b18a128dec1c5d55#a8765330fc16997dee275866b18a128dec1c5d55" dependencies = [ "bstr", "gix-hash 0.15.0", @@ -3889,7 +3890,7 @@ dependencies = [ [[package]] name = "gix-revision" version = "0.30.0" -source = "git+https://github.com/Byron/gitoxide?rev=3fb989be21c739bbfeac93953c1685e7c6cd2106#3fb989be21c739bbfeac93953c1685e7c6cd2106" +source = "git+https://github.com/Byron/gitoxide?rev=a8765330fc16997dee275866b18a128dec1c5d55#a8765330fc16997dee275866b18a128dec1c5d55" dependencies = [ "bitflags 2.6.0", "bstr", @@ -3921,7 +3922,7 @@ dependencies = [ [[package]] name = "gix-revwalk" version = "0.16.0" -source = "git+https://github.com/Byron/gitoxide?rev=3fb989be21c739bbfeac93953c1685e7c6cd2106#3fb989be21c739bbfeac93953c1685e7c6cd2106" +source = "git+https://github.com/Byron/gitoxide?rev=a8765330fc16997dee275866b18a128dec1c5d55#a8765330fc16997dee275866b18a128dec1c5d55" dependencies = [ "gix-commitgraph 0.25.0", "gix-date 0.9.1", @@ -3947,7 +3948,7 @@ dependencies = [ [[package]] name = "gix-sec" version = "0.10.9" -source = "git+https://github.com/Byron/gitoxide?rev=3fb989be21c739bbfeac93953c1685e7c6cd2106#3fb989be21c739bbfeac93953c1685e7c6cd2106" +source = "git+https://github.com/Byron/gitoxide?rev=a8765330fc16997dee275866b18a128dec1c5d55#a8765330fc16997dee275866b18a128dec1c5d55" dependencies = [ "bitflags 2.6.0", "gix-path 0.10.12", @@ -3958,7 +3959,7 @@ dependencies = [ [[package]] name = "gix-status" version = "0.14.0" -source = "git+https://github.com/Byron/gitoxide?rev=3fb989be21c739bbfeac93953c1685e7c6cd2106#3fb989be21c739bbfeac93953c1685e7c6cd2106" +source = "git+https://github.com/Byron/gitoxide?rev=a8765330fc16997dee275866b18a128dec1c5d55#a8765330fc16997dee275866b18a128dec1c5d55" dependencies = [ "bstr", "filetime", @@ -3980,7 +3981,7 @@ dependencies = [ [[package]] name = "gix-submodule" version = "0.15.0" -source = "git+https://github.com/Byron/gitoxide?rev=3fb989be21c739bbfeac93953c1685e7c6cd2106#3fb989be21c739bbfeac93953c1685e7c6cd2106" +source = "git+https://github.com/Byron/gitoxide?rev=a8765330fc16997dee275866b18a128dec1c5d55#a8765330fc16997dee275866b18a128dec1c5d55" dependencies = [ "bstr", "gix-config", @@ -4009,7 +4010,7 @@ dependencies = [ [[package]] name = "gix-tempfile" version = "15.0.0" -source = "git+https://github.com/Byron/gitoxide?rev=3fb989be21c739bbfeac93953c1685e7c6cd2106#3fb989be21c739bbfeac93953c1685e7c6cd2106" +source = "git+https://github.com/Byron/gitoxide?rev=a8765330fc16997dee275866b18a128dec1c5d55#a8765330fc16997dee275866b18a128dec1c5d55" dependencies = [ "dashmap", "gix-fs 0.12.0", @@ -4054,7 +4055,7 @@ checksum = "6cae0e8661c3ff92688ce1c8b8058b3efb312aba9492bbe93661a21705ab431b" [[package]] name = "gix-trace" version = "0.1.11" -source = "git+https://github.com/Byron/gitoxide?rev=3fb989be21c739bbfeac93953c1685e7c6cd2106#3fb989be21c739bbfeac93953c1685e7c6cd2106" +source = "git+https://github.com/Byron/gitoxide?rev=a8765330fc16997dee275866b18a128dec1c5d55#a8765330fc16997dee275866b18a128dec1c5d55" dependencies = [ "tracing-core", ] @@ -4062,7 +4063,7 @@ dependencies = [ [[package]] name = "gix-transport" version = "0.43.0" -source = "git+https://github.com/Byron/gitoxide?rev=3fb989be21c739bbfeac93953c1685e7c6cd2106#3fb989be21c739bbfeac93953c1685e7c6cd2106" +source = "git+https://github.com/Byron/gitoxide?rev=a8765330fc16997dee275866b18a128dec1c5d55#a8765330fc16997dee275866b18a128dec1c5d55" dependencies = [ "base64 0.22.1", "bstr", @@ -4097,7 +4098,7 @@ dependencies = [ [[package]] name = "gix-traverse" version = "0.42.0" -source = "git+https://github.com/Byron/gitoxide?rev=3fb989be21c739bbfeac93953c1685e7c6cd2106#3fb989be21c739bbfeac93953c1685e7c6cd2106" +source = "git+https://github.com/Byron/gitoxide?rev=a8765330fc16997dee275866b18a128dec1c5d55#a8765330fc16997dee275866b18a128dec1c5d55" dependencies = [ "bitflags 2.6.0", "gix-commitgraph 0.25.0", @@ -4113,7 +4114,7 @@ dependencies = [ [[package]] name = "gix-url" version = "0.28.0" -source = "git+https://github.com/Byron/gitoxide?rev=3fb989be21c739bbfeac93953c1685e7c6cd2106#3fb989be21c739bbfeac93953c1685e7c6cd2106" +source = "git+https://github.com/Byron/gitoxide?rev=a8765330fc16997dee275866b18a128dec1c5d55#a8765330fc16997dee275866b18a128dec1c5d55" dependencies = [ "bstr", "gix-features 0.39.0", @@ -4135,7 +4136,7 @@ dependencies = [ [[package]] name = "gix-utils" version = "0.1.13" -source = "git+https://github.com/Byron/gitoxide?rev=3fb989be21c739bbfeac93953c1685e7c6cd2106#3fb989be21c739bbfeac93953c1685e7c6cd2106" +source = "git+https://github.com/Byron/gitoxide?rev=a8765330fc16997dee275866b18a128dec1c5d55#a8765330fc16997dee275866b18a128dec1c5d55" dependencies = [ "bstr", "fastrand 2.1.1", @@ -4155,7 +4156,7 @@ dependencies = [ [[package]] name = "gix-validate" version = "0.9.1" -source = "git+https://github.com/Byron/gitoxide?rev=3fb989be21c739bbfeac93953c1685e7c6cd2106#3fb989be21c739bbfeac93953c1685e7c6cd2106" +source = "git+https://github.com/Byron/gitoxide?rev=a8765330fc16997dee275866b18a128dec1c5d55#a8765330fc16997dee275866b18a128dec1c5d55" dependencies = [ "bstr", "thiserror", @@ -4183,7 +4184,7 @@ dependencies = [ [[package]] name = "gix-worktree" version = "0.37.0" -source = "git+https://github.com/Byron/gitoxide?rev=3fb989be21c739bbfeac93953c1685e7c6cd2106#3fb989be21c739bbfeac93953c1685e7c6cd2106" +source = "git+https://github.com/Byron/gitoxide?rev=a8765330fc16997dee275866b18a128dec1c5d55#a8765330fc16997dee275866b18a128dec1c5d55" dependencies = [ "bstr", "gix-attributes 0.23.0", @@ -4201,7 +4202,7 @@ dependencies = [ [[package]] name = "gix-worktree-state" version = "0.14.0" -source = "git+https://github.com/Byron/gitoxide?rev=3fb989be21c739bbfeac93953c1685e7c6cd2106#3fb989be21c739bbfeac93953c1685e7c6cd2106" +source = "git+https://github.com/Byron/gitoxide?rev=a8765330fc16997dee275866b18a128dec1c5d55#a8765330fc16997dee275866b18a128dec1c5d55" dependencies = [ "bstr", "gix-features 0.39.0", diff --git a/Cargo.toml b/Cargo.toml index 9c65a2540a..a79f1a888b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,7 +42,7 @@ resolver = "2" [workspace.dependencies] bstr = "1.10.0" # Add the `tracing` or `tracing-detail` features to see more of gitoxide in the logs. Useful to see which programs it invokes. -gix = { git = "https://github.com/Byron/gitoxide", rev = "3fb989be21c739bbfeac93953c1685e7c6cd2106", default-features = false, features = [ +gix = { git = "https://github.com/Byron/gitoxide", rev = "a8765330fc16997dee275866b18a128dec1c5d55", default-features = false, features = [ ] } git2 = { version = "0.19.0", features = [ "vendored-openssl", diff --git a/crates/gitbutler-branch-actions/Cargo.toml b/crates/gitbutler-branch-actions/Cargo.toml index 59c735aec5..ee4b9abbdb 100644 --- a/crates/gitbutler-branch-actions/Cargo.toml +++ b/crates/gitbutler-branch-actions/Cargo.toml @@ -9,7 +9,7 @@ publish = false tracing.workspace = true anyhow = "1.0.92" git2.workspace = true -gix = { workspace = true, features = ["blob-diff", "revision", "blob-merge"] } +gix = { workspace = true, features = ["blob-diff", "revision", "merge"] } tokio.workspace = true gitbutler-oplog.workspace = true gitbutler-repo.workspace = true diff --git a/crates/gitbutler-branch-actions/src/base.rs b/crates/gitbutler-branch-actions/src/base.rs index 55b7b61b6b..d452a89b39 100644 --- a/crates/gitbutler-branch-actions/src/base.rs +++ b/crates/gitbutler-branch-actions/src/base.rs @@ -1,24 +1,24 @@ use std::{path::Path, time}; -use anyhow::{anyhow, Context, Result}; +use crate::{ + conflicts::RepoConflictsExt, + hunk::VirtualBranchHunk, + integration::update_workspace_commit, + remote::{commit_to_remote_commit, RemoteCommit}, + VirtualBranchesExt, +}; +use anyhow::{anyhow, bail, Context, Result}; use gitbutler_branch::GITBUTLER_WORKSPACE_REFERENCE; use gitbutler_command_context::CommandContext; use gitbutler_error::error::Marker; +use gitbutler_oxidize::{git2_to_gix_object_id, gix_to_git2_oid}; use gitbutler_project::FetchResult; use gitbutler_reference::{Refname, RemoteRefname}; -use gitbutler_repo::{LogUntil, RepositoryExt}; +use gitbutler_repo::{GixRepositoryExt, LogUntil, RepositoryExt}; use gitbutler_repo_actions::RepoActionsExt; use gitbutler_stack::{BranchOwnershipClaims, Stack, Target, VirtualBranchesHandle}; use serde::Serialize; -use crate::{ - conflicts::RepoConflictsExt, - hunk::VirtualBranchHunk, - integration::update_workspace_commit, - remote::{commit_to_remote_commit, RemoteCommit}, - VirtualBranchesExt, -}; - #[derive(Debug, Serialize, PartialEq, Clone)] #[serde(rename_all = "camelCase")] pub struct BaseBranch { @@ -50,8 +50,8 @@ pub(crate) fn get_base_branch_data(ctx: &CommandContext) -> Result { } fn go_back_to_integration(ctx: &CommandContext, default_target: &Target) -> Result { - let statuses = ctx - .repository() + let repo = ctx.repository(); + let statuses = repo .statuses(Some( git2::StatusOptions::new() .show(git2::StatusShow::IndexAndWorkdir) @@ -67,41 +67,36 @@ fn go_back_to_integration(ctx: &CommandContext, default_target: &Target) -> Resu .list_branches_in_workspace() .context("failed to read virtual branches")?; - let target_commit = ctx - .repository() + let target_commit = repo .find_commit(default_target.sha) .context("failed to find target commit")?; - let base_tree = target_commit - .tree() - .context("failed to get base tree from commit")?; - let mut final_tree = target_commit - .tree() - .context("failed to get base tree from commit")?; + let base_tree = git2_to_gix_object_id(target_commit.tree_id()); + let mut final_tree_id = git2_to_gix_object_id(target_commit.tree_id()); + let gix_repo = ctx.gix_repository_for_merging()?; + let (merge_options_fail_fast, conflict_kind) = gix_repo.merge_options_fail_fast()?; for branch in &virtual_branches { // merge this branches tree with our tree - let branch_head = ctx - .repository() - .find_commit(branch.head()) - .context("failed to find branch head")?; - let branch_tree = branch_head - .tree() - .context("failed to get branch head tree")?; - let mut result = ctx - .repository() - .merge_trees(&base_tree, &final_tree, &branch_tree, None) - .context("failed to merge")?; - let final_tree_oid = result - .write_tree_to(ctx.repository()) - .context("failed to write tree")?; - final_tree = ctx - .repository() - .find_tree(final_tree_oid) - .context("failed to find written tree")?; + let branch_tree_id = git2_to_gix_object_id( + repo.find_commit(branch.head()) + .context("failed to find branch head")? + .tree_id(), + ); + let mut merge = gix_repo.merge_trees( + base_tree, + final_tree_id, + branch_tree_id, + gix_repo.default_merge_labels(), + merge_options_fail_fast.clone(), + )?; + if merge.has_unresolved_conflicts(conflict_kind) { + bail!("Merge failed with conflicts"); + } + final_tree_id = merge.tree.write()?.detach(); } - ctx.repository() - .checkout_tree_builder(&final_tree) + let final_tree = repo.find_tree(gix_to_git2_oid(final_tree_id))?; + repo.checkout_tree_builder(&final_tree) .force() .checkout() .context("failed to checkout tree")?; 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 06aaa34076..047f137983 100644 --- a/crates/gitbutler-branch-actions/src/branch_manager/branch_creation.rs +++ b/crates/gitbutler-branch-actions/src/branch_manager/branch_creation.rs @@ -8,6 +8,7 @@ use gitbutler_error::error::Marker; use gitbutler_oplog::SnapshotExt; use gitbutler_project::access::WorktreeWritePermission; use gitbutler_reference::{Refname, RemoteRefname}; +use gitbutler_repo::GixRepositoryExt; use gitbutler_repo::{ rebase::{cherry_rebase_group, gitbutler_merge_commits}, LogUntil, RepositoryExt, @@ -301,29 +302,27 @@ impl BranchManager<'_> { ))?; // Branch is out of date, merge or rebase it - let merge_base_tree = repo + let merge_base_tree_id = repo .find_commit(merge_base) .context(format!("failed to find merge base commit {}", merge_base))? .tree() - .context("failed to find merge base tree")?; - - let branch_tree = repo - .find_tree(branch.tree) - .context("failed to find branch tree")?; + .context("failed to find merge base tree")? + .id(); + let branch_tree_id = branch.tree; // We don't support having two branches applied that conflict with each other { - let uncommited_changes_tree = repo.create_wd_tree()?; - let branch_merged_with_other_applied_branches = repo - .merge_trees( - &merge_base_tree, - &branch_tree, - &uncommited_changes_tree, - None, + let uncommited_changes_tree_id = repo.create_wd_tree()?.id(); + let gix_repo = self.ctx.gix_repository_for_merging_non_persisting()?; + let merges_cleanly = gix_repo + .merges_cleanly_compat( + merge_base_tree_id, + branch_tree_id, + uncommited_changes_tree_id, ) .context("failed to merge trees")?; - if branch_merged_with_other_applied_branches.has_conflicts() { + if !merges_cleanly { for branch in vb_state .list_branches_in_workspace()? .iter() diff --git a/crates/gitbutler-branch-actions/src/branch_manager/branch_removal.rs b/crates/gitbutler-branch-actions/src/branch_manager/branch_removal.rs index 0340b639c5..0c08b43242 100644 --- a/crates/gitbutler-branch-actions/src/branch_manager/branch_removal.rs +++ b/crates/gitbutler-branch-actions/src/branch_manager/branch_removal.rs @@ -5,8 +5,11 @@ use git2::Commit; use gitbutler_branch::BranchExt; use gitbutler_commit::commit_headers::CommitHeadersV2; use gitbutler_oplog::SnapshotExt; +use gitbutler_oxidize::git2_to_gix_object_id; +use gitbutler_oxidize::gix_to_git2_oid; use gitbutler_project::access::WorktreeWritePermission; use gitbutler_reference::{normalize_branch_name, ReferenceName, Refname}; +use gitbutler_repo::GixRepositoryExt; use gitbutler_repo::RepositoryExt; use gitbutler_repo::SignaturePurpose; use gitbutler_repo_actions::RepoActionsExt; @@ -79,7 +82,10 @@ impl BranchManager<'_> { let repo = self.ctx.repository(); - let base_tree = target_commit.tree().context("failed to get target tree")?; + let base_tree_id = target_commit + .tree() + .context("failed to get target tree")? + .id(); let applied_statuses = get_applied_status(self.ctx, None) .context("failed to get status by branch")? @@ -98,13 +104,14 @@ impl BranchManager<'_> { num_branches = applied_statuses.len() - 1 ) .entered(); - applied_statuses + let gix_repo = self.ctx.gix_repository()?; + let merge_options = gix_repo.tree_merge_options()?; + let final_tree_id = applied_statuses .into_iter() .filter(|(branch, _)| branch.id != branch_id) - .fold( - target_commit.tree().context("failed to get target tree"), - |final_tree, status| { - let final_tree = final_tree?; + .try_fold( + git2_to_gix_object_id(target_commit.tree_id()), + |final_tree_id, status| -> Result<_> { let branch = status.0; let files = status .1 @@ -113,14 +120,18 @@ impl BranchManager<'_> { .collect::)>>(); let tree_oid = gitbutler_diff::write::hunks_onto_oid(self.ctx, branch.head(), files)?; - let branch_tree = repo.find_tree(tree_oid)?; - let mut result = - repo.merge_trees(&base_tree, &final_tree, &branch_tree, None)?; - let final_tree_oid = result.write_tree_to(repo)?; - repo.find_tree(final_tree_oid) - .context("failed to find tree") + let mut merge = gix_repo.merge_trees( + git2_to_gix_object_id(base_tree_id), + final_tree_id, + git2_to_gix_object_id(tree_oid), + gix_repo.default_merge_labels(), + merge_options.clone(), + )?; + let final_tree_id = merge.tree.write()?.detach(); + Ok(final_tree_id) }, - )? + )?; + repo.find_tree(gix_to_git2_oid(final_tree_id))? }; let _span = tracing::debug_span!("checkout final tree").entered(); diff --git a/crates/gitbutler-branch-actions/src/branch_trees.rs b/crates/gitbutler-branch-actions/src/branch_trees.rs index 19ec2af9ba..2c66308ae4 100644 --- a/crates/gitbutler-branch-actions/src/branch_trees.rs +++ b/crates/gitbutler-branch-actions/src/branch_trees.rs @@ -1,17 +1,19 @@ +use crate::VirtualBranchesExt as _; use anyhow::{bail, Result}; use gitbutler_cherry_pick::RepositoryExt; use gitbutler_command_context::CommandContext; use gitbutler_commit::commit_ext::CommitExt as _; +use gitbutler_oxidize::{git2_to_gix_object_id, gix_to_git2_oid}; use gitbutler_project::access::WorktreeWritePermission; use gitbutler_repo::rebase::cherry_rebase_group; -use gitbutler_repo::RepositoryExt as _; +use gitbutler_repo::{GixRepositoryExt, RepositoryExt as _}; use gitbutler_stack::Stack; - -use crate::VirtualBranchesExt as _; +use tracing::instrument; /// Checks out the combined trees of all branches in the workspace. /// /// This function will fail if the applied branches conflict with each other. +#[instrument(level = tracing::Level::DEBUG, skip(ctx, _perm), err(Debug))] pub fn checkout_branch_trees<'a>( ctx: &'a CommandContext, _perm: &mut WorktreeWritePermission, @@ -38,23 +40,30 @@ pub fn checkout_branch_trees<'a>( let merge_base = repository .merge_base_octopussy(&branches.iter().map(|b| b.head()).collect::>())?; - let merge_base_tree = repository.find_commit(merge_base)?.tree()?; - - let mut final_tree = merge_base_tree.clone(); + let gix_repo = ctx.gix_repository_for_merging()?; + let merge_base_tree_id = + git2_to_gix_object_id(repository.find_commit(merge_base)?.tree_id()); + let mut final_tree_id = merge_base_tree_id; + let (merge_options_fail_fast, conflict_kind) = gix_repo.merge_options_fail_fast()?; for branch in branches { - let theirs = repository.find_tree(branch.tree)?; - let mut merge_index = - repository.merge_trees(&merge_base_tree, &final_tree, &theirs, None)?; - - if merge_index.has_conflicts() { + let their_tree_id = git2_to_gix_object_id(branch.tree); + let mut merge = gix_repo.merge_trees( + merge_base_tree_id, + final_tree_id, + their_tree_id, + gix_repo.default_merge_labels(), + merge_options_fail_fast.clone(), + )?; + + if merge.has_unresolved_conflicts(conflict_kind) { bail!("There appears to be conflicts between the virtual branches"); }; - let tree_oid = merge_index.write_tree_to(repository)?; - final_tree = repository.find_tree(tree_oid)?; + final_tree_id = merge.tree.write()?.detach(); } + let final_tree = repository.find_tree(gix_to_git2_oid(final_tree_id))?; repository .checkout_tree_builder(&final_tree) .force() diff --git a/crates/gitbutler-branch-actions/src/integration.rs b/crates/gitbutler-branch-actions/src/integration.rs index 1c99ec24c4..0182017f69 100644 --- a/crates/gitbutler-branch-actions/src/integration.rs +++ b/crates/gitbutler-branch-actions/src/integration.rs @@ -9,8 +9,9 @@ use gitbutler_command_context::CommandContext; use gitbutler_commit::commit_ext::CommitExt; use gitbutler_error::error::Marker; use gitbutler_operating_modes::OPEN_WORKSPACE_REFS; +use gitbutler_oxidize::{git2_to_gix_object_id, gix_to_git2_oid}; use gitbutler_project::access::WorktreeWritePermission; -use gitbutler_repo::SignaturePurpose; +use gitbutler_repo::{GixRepositoryExt, SignaturePurpose}; use gitbutler_repo::{LogUntil, RepositoryExt}; use gitbutler_stack::{Stack, VirtualBranchesHandle}; use tracing::instrument; @@ -41,6 +42,7 @@ pub(crate) fn get_workspace_head(ctx: &CommandContext) -> Result { let target_commit = repo.find_commit(target.sha)?; let mut workspace_tree = repo.find_real_tree(&target_commit, Default::default())?; + let mut workspace_tree_id = git2_to_gix_object_id(workspace_tree.id()); if conflicts::is_conflicting(ctx, None)? { let merge_parent = conflicts::merge_parent(ctx)?.ok_or(anyhow!("No merge parent"))?; @@ -49,15 +51,24 @@ pub(crate) fn get_workspace_head(ctx: &CommandContext) -> Result { let merge_base = repo.merge_base(first_branch.head(), merge_parent)?; workspace_tree = repo.find_commit(merge_base)?.tree()?; } else { + let gix_repo = ctx.gix_repository_for_merging()?; + let (merge_options_fail_fast, conflict_kind) = gix_repo.merge_options_fail_fast()?; + let merge_tree_id = git2_to_gix_object_id(repo.find_commit(target.sha)?.tree_id()); for branch in virtual_branches.iter_mut() { - let merge_tree = repo.find_commit(target.sha)?.tree()?; - let branch_tree = repo.find_commit(branch.head())?; - let branch_tree = repo.find_real_tree(&branch_tree, Default::default())?; - - let mut index = repo.merge_trees(&merge_tree, &workspace_tree, &branch_tree, None)?; + let branch_head = repo.find_commit(branch.head())?; + let branch_tree_id = + git2_to_gix_object_id(repo.find_real_tree(&branch_head, Default::default())?.id()); + + let mut merge = gix_repo.merge_trees( + merge_tree_id, + workspace_tree_id, + branch_tree_id, + gix_repo.default_merge_labels(), + merge_options_fail_fast.clone(), + )?; - if !index.has_conflicts() { - workspace_tree = repo.find_tree(index.write_tree_to(repo)?)?; + if !merge.has_unresolved_conflicts(conflict_kind) { + workspace_tree_id = merge.tree.write()?.detach(); } else { // This branch should have already been unapplied during the "update" command but for some reason that failed tracing::warn!("Merge conflict between base and {:?}", branch.name); @@ -65,6 +76,7 @@ pub(crate) fn get_workspace_head(ctx: &CommandContext) -> Result { vb_state.set_branch(branch.clone())?; } } + workspace_tree = repo.find_tree(gix_to_git2_oid(workspace_tree_id))?; } let committer = gitbutler_repo::signature(SignaturePurpose::Committer)?; diff --git a/crates/gitbutler-branch-actions/src/stack.rs b/crates/gitbutler-branch-actions/src/stack.rs index dc37ddd975..8cd9b0bd13 100644 --- a/crates/gitbutler-branch-actions/src/stack.rs +++ b/crates/gitbutler-branch-actions/src/stack.rs @@ -7,7 +7,6 @@ use gitbutler_oplog::entry::{OperationKind, SnapshotDetails}; use gitbutler_oplog::{OplogExt, SnapshotExt}; use gitbutler_project::Project; use gitbutler_reference::normalize_branch_name; -use gitbutler_repo::GixRepositoryExt; use gitbutler_repo_actions::RepoActionsExt; use gitbutler_stack::{ CommitOrChangeId, ForgeIdentifier, PatchReference, PatchReferenceUpdate, Series, @@ -192,10 +191,7 @@ pub fn push_stack(project: &Project, branch_id: StackId, with_force: bool) -> Re // First fetch, because we dont want to push integrated series ctx.fetch(&default_target.push_remote_name(), None)?; - let gix_repo = ctx - .gix_repository()? - .for_tree_diffing()? - .with_object_memory(); + let gix_repo = ctx.gix_repository_for_merging_non_persisting()?; let cache = gix_repo.commit_graph_if_enabled()?; let mut graph = gix_repo.revision_graph(cache.as_ref()); let mut check_commit = IsCommitIntegrated::new(ctx, &default_target, &gix_repo, &mut graph)?; diff --git a/crates/gitbutler-branch-actions/src/upstream_integration.rs b/crates/gitbutler-branch-actions/src/upstream_integration.rs index 4c29d6780b..0ecff6e034 100644 --- a/crates/gitbutler-branch-actions/src/upstream_integration.rs +++ b/crates/gitbutler-branch-actions/src/upstream_integration.rs @@ -1,20 +1,20 @@ +use crate::{ + branch_trees::{checkout_branch_trees, compute_updated_branch_head, BranchHeadAndTree}, + BranchManagerExt, VirtualBranchesExt as _, +}; use anyhow::{anyhow, bail, Result}; use gitbutler_cherry_pick::RepositoryExt as _; use gitbutler_command_context::CommandContext; +use gitbutler_oxidize::git2_to_gix_object_id; use gitbutler_project::access::WorktreeWritePermission; use gitbutler_repo::{ rebase::{cherry_rebase_group, gitbutler_merge_commits}, - LogUntil, RepositoryExt as _, + GixRepositoryExt, LogUntil, RepositoryExt as _, }; use gitbutler_repo_actions::RepoActionsExt as _; use gitbutler_stack::{Stack, StackId, Target, VirtualBranchesHandle}; use serde::{Deserialize, Serialize}; -use crate::{ - branch_trees::{checkout_branch_trees, compute_updated_branch_head, BranchHeadAndTree}, - BranchManagerExt, VirtualBranchesExt as _, -}; - #[derive(Serialize, PartialEq, Debug)] #[serde(tag = "type", content = "subject", rename_all = "camelCase")] pub enum BranchStatus { @@ -41,9 +41,9 @@ pub enum BaseBranchResolutionApproach { HardReset, } -#[derive(Serialize, Deserialize, PartialEq, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)] #[serde(tag = "type", content = "subject", rename_all = "camelCase")] -enum ResolutionApproach { +pub enum ResolutionApproach { Rebase, Merge, Unapply, @@ -75,11 +75,11 @@ impl BranchStatus { #[derive(Serialize, Deserialize, PartialEq, Debug)] #[serde(rename_all = "camelCase")] pub struct Resolution { - branch_id: StackId, + pub branch_id: StackId, /// Used to ensure a given branch hasn't changed since the UI issued the command. #[serde(with = "gitbutler_serde::oid")] - branch_tree: git2::Oid, - approach: ResolutionApproach, + pub branch_tree: git2::Oid, + pub approach: ResolutionApproach, } enum IntegrationResult { @@ -140,8 +140,19 @@ pub fn upstream_integration_statuses( .. } = context; // look up the target and see if there is a new oid - let old_target_tree = repository.find_real_tree(old_target, Default::default())?; - let new_target_tree = repository.find_real_tree(new_target, Default::default())?; + let old_target_tree_id = git2_to_gix_object_id( + repository + .find_real_tree(old_target, Default::default())? + .id(), + ); + let new_target_tree_id = git2_to_gix_object_id( + repository + .find_real_tree(new_target, Default::default())? + .id(), + ); + let gix_repo = gitbutler_command_context::gix_repository_for_merging(repository.path())?; + let gix_repo_in_memory = gix_repo.clone().with_object_memory(); + let (merge_options_fail_fast, conflict_kind) = gix_repo.merge_options_fail_fast()?; if new_target.id() == old_target.id() { return Ok(BranchStatuses::UpToDate); @@ -151,8 +162,10 @@ pub fn upstream_integration_statuses( .iter() .map(|virtual_branch| { let tree = repository.find_tree(virtual_branch.tree)?; + let tree_id = git2_to_gix_object_id(tree.id()); let head = repository.find_commit(virtual_branch.head())?; let head_tree = repository.find_real_tree(&head, Default::default())?; + let head_tree_id = git2_to_gix_object_id(head_tree.id()); // Try cherry pick the branch's head commit onto the target to // see if it conflics. This is equivalent to doing a merge @@ -168,25 +181,33 @@ pub fn upstream_integration_statuses( }; } - let head_merge_index = - repository.merge_trees(&old_target_tree, &new_target_tree, &head_tree, None)?; - let mut tree_merge_index = - repository.merge_trees(&old_target_tree, &new_target_tree, &tree, None)?; + let mut tree_merge = gix_repo.merge_trees( + old_target_tree_id, + new_target_tree_id, + tree_id, + gix_repo.default_merge_labels(), + merge_options_fail_fast.clone(), + )?; // Is the branch conflicted? // A branch can't be integrated if its conflicted { - let commits_conflicted = head_merge_index.has_conflicts(); + let commits_conflicted = gix_repo_in_memory + .merge_trees( + old_target_tree_id, + new_target_tree_id, + head_tree_id, + Default::default(), + merge_options_fail_fast.clone(), + )? + .has_unresolved_conflicts(conflict_kind); + gix_repo_in_memory.objects.reset_object_memory(); // See whether uncommited changes are potentially conflicted let potentially_conflicted_uncommited_changes = if has_uncommited_changes { // If the commits are conflicted, we can guarentee that the // tree will be conflicted. - if commits_conflicted { - true - } else { - tree_merge_index.has_conflicts() - } + commits_conflicted || tree_merge.has_unresolved_conflicts(conflict_kind) } else { // If there are no uncommited changes, then there can't be // any conflicts. @@ -205,13 +226,18 @@ pub fn upstream_integration_statuses( // Is the branch fully integrated? { + if tree_merge.has_unresolved_conflicts(conflict_kind) { + bail!( + "Merge result unexpectedly has conflicts between base, \ + ours, theirs: {old_target_tree_id}, {new_target_tree_id}, {tree_id}" + ) + } // We're safe to write the tree as we've ensured it's // unconflicted in the previous test. - let tree_merge_index_tree = tree_merge_index.write_tree_to(repository)?; + let tree_merge_index_tree_id = tree_merge.tree.write()?.detach(); - // Identical trees will have the same Oid so we can compare - // the two - if tree_merge_index_tree == new_target_tree.id() { + // Identical trees will have the same Oid so we can compare the two + if tree_merge_index_tree_id == new_target_tree_id { return Ok((virtual_branch.id, BranchStatus::FullyIntegrated)); } } diff --git a/crates/gitbutler-branch-actions/src/virtual.rs b/crates/gitbutler-branch-actions/src/virtual.rs index d7e084ec87..2e11a2c9e5 100644 --- a/crates/gitbutler-branch-actions/src/virtual.rs +++ b/crates/gitbutler-branch-actions/src/virtual.rs @@ -33,7 +33,6 @@ use gitbutler_stack::{ VirtualBranchesHandle, }; use gitbutler_time::time::now_since_unix_epoch_ms; -use gix::objs::Write; use serde::Serialize; use std::collections::HashSet; use std::{collections::HashMap, path::PathBuf, vec}; @@ -180,25 +179,34 @@ pub fn unapply_ownership( .find_commit(workspace_commit_id) .context("failed to find target commit")?; - let base_tree = target_commit.tree().context("failed to get target tree")?; - let final_tree = applied_statuses.into_iter().fold( - target_commit.tree().context("failed to get target tree"), - |final_tree, status| { - let final_tree = final_tree?; + let base_tree_id = git2_to_gix_object_id(target_commit.tree_id()); + let gix_repo = ctx.gix_repository_for_merging()?; + let (merge_options_fail_fast, conflict_kind) = gix_repo.merge_options_fail_fast()?; + let final_tree_id = applied_statuses.into_iter().try_fold( + git2_to_gix_object_id(target_commit.tree_id()), + |final_tree_id, status| -> Result<_> { let files = status .1 .into_iter() .map(|file| (file.path, file.hunks)) .collect::)>>(); - let tree_oid = gitbutler_diff::write::hunks_onto_oid(ctx, workspace_commit_id, files)?; - let branch_tree = repo.find_tree(tree_oid)?; - let mut result = repo.merge_trees(&base_tree, &final_tree, &branch_tree, None)?; - let final_tree_oid = result.write_tree_to(ctx.repository())?; - repo.find_tree(final_tree_oid) - .context("failed to find tree") + let branch_tree_id = + gitbutler_diff::write::hunks_onto_oid(ctx, workspace_commit_id, files)?; + let mut merge = gix_repo.merge_trees( + base_tree_id, + final_tree_id, + git2_to_gix_object_id(branch_tree_id), + gix_repo.default_merge_labels(), + merge_options_fail_fast.clone(), + )?; + if merge.has_unresolved_conflicts(conflict_kind) { + bail!("Tree has conflicts after merge") + } + Ok(merge.tree.write()?.detach()) }, )?; + let final_tree = repo.find_tree(gix_to_git2_oid(final_tree_id))?; let final_tree_oid = gitbutler_diff::write::hunks_onto_tree(ctx, &final_tree, diff, true)?; let final_tree = repo .find_tree(final_tree_oid) @@ -302,10 +310,7 @@ pub fn list_virtual_branches_cached( let branches_span = tracing::debug_span!("handle branches", num_branches = status.branches.len()).entered(); let repo = ctx.repository(); - let gix_repo = ctx - .gix_repository()? - .for_tree_diffing()? - .with_object_memory(); + let gix_repo = ctx.gix_repository_for_merging_non_persisting()?; // We will perform virtual merges, no need to write them to the ODB. let cache = gix_repo.commit_graph_if_enabled()?; let mut graph = gix_repo.revision_graph(cache.as_ref()); @@ -1038,9 +1043,7 @@ impl IsCommitIntegrated<'_, '_, '_> { } // try to merge our tree into the upstream tree - let mut merge_options = self.gix_repo.tree_merge_options()?; - let conflict_kind = gix::merge::tree::UnresolvedConflict::Renames; - merge_options.fail_on_conflict = Some(conflict_kind); + let (merge_options, conflict_kind) = self.gix_repo.merge_options_fail_fast()?; let mut merge_output = self .gix_repo .merge_trees( @@ -1056,10 +1059,7 @@ impl IsCommitIntegrated<'_, '_, '_> { return Ok(false); } - let merge_tree_id = merge_output - .tree - .write(|tree| self.gix_repo.write(tree)) - .map_err(|err| anyhow!("failed to write tree: {err}"))?; + let merge_tree_id = merge_output.tree.write()?.detach(); // if the merge_tree is the same as the new_target_tree and there are no files (uncommitted changes) // then the vbranch is fully merged @@ -1094,11 +1094,18 @@ pub fn is_remote_branch_mergeable( let wd_tree = ctx.repository().create_wd_tree()?; let branch_tree = branch_commit.tree().context("failed to find branch tree")?; - let mergeable = !ctx - .repository() - .merge_trees(&base_tree, &branch_tree, &wd_tree, None) + let gix_repo_in_memory = ctx.gix_repository_for_merging()?.with_object_memory(); + let (merge_options_fail_fast, conflict_kind) = gix_repo_in_memory.merge_options_fail_fast()?; + let mergeable = !gix_repo_in_memory + .merge_trees( + git2_to_gix_object_id(base_tree.id()), + git2_to_gix_object_id(branch_tree.id()), + git2_to_gix_object_id(wd_tree.id()), + Default::default(), + merge_options_fail_fast, + ) .context("failed to merge trees")? - .has_conflicts(); + .has_unresolved_conflicts(conflict_kind); Ok(mergeable) } diff --git a/crates/gitbutler-cli/src/args.rs b/crates/gitbutler-cli/src/args.rs index 7886a25d9a..310f32db91 100644 --- a/crates/gitbutler-cli/src/args.rs +++ b/crates/gitbutler-cli/src/args.rs @@ -14,8 +14,31 @@ pub struct Args { pub cmd: Subcommands, } +#[derive(Debug, Clone, clap::ValueEnum)] +pub enum UpdateMode { + Rebase, + Merge, + Unapply, + Delete, +} + #[derive(Debug, clap::Subcommand)] pub enum Subcommands { + /// Unapply the given ownership claim. + UnapplyOwnership { + /// The path to remove the claim from. + filepath: PathBuf, + /// The first line of hunks that should be removed. + from_line: u32, + /// The last line of hunks that should be removed. + to_line: u32, + }, + /// Update the local workspace against an updated remote or target branch. + IntegrateUpstream { + /// Specify how all branches should be merged in. + #[clap(value_enum)] + mode: UpdateMode, + }, /// List and manipulate virtual branches. #[clap(visible_alias = "branches")] Branch(vbranch::Platform), @@ -42,6 +65,11 @@ pub mod vbranch { ListLocal, /// Provide the current state of all applied virtual branches. Status, + /// Switch to the GitButler workspace. + SetBase { + /// The name of the remote branch to integrate with, like `origin/main`. + short_tracking_branch_name: String, + }, /// Make the named branch the default so all worktree or index changes are associated with it automatically. SetDefault { /// The name of the new default virtual branch. @@ -52,6 +80,11 @@ pub mod vbranch { /// The name of the virtual branch to unapply. name: String, }, + /// Add a branch to the workspace. + Apply { + /// The name of the virtual branch to apply. + name: String, + }, /// Create a new commit to named virtual branch with all changes currently in the worktree or staging area assigned to it. Commit { /// The commit message @@ -141,6 +174,11 @@ pub mod snapshot { /// The snapshot to restore snapshot_id: String, }, + /// Show what is stored in a given snapshot. + Diff { + /// The hex-hash of the commit-id of the snapshot. + snapshot_id: String, + }, } } diff --git a/crates/gitbutler-cli/src/command/mod.rs b/crates/gitbutler-cli/src/command/mod.rs index dcbb03d961..c4ed3ab7c4 100644 --- a/crates/gitbutler-cli/src/command/mod.rs +++ b/crates/gitbutler-cli/src/command/mod.rs @@ -1,8 +1,8 @@ pub mod prepare; pub mod project; pub mod vbranch; - pub mod snapshot { + use crate::command::debug_print; use anyhow::Result; use gitbutler_oplog::OplogExt; use gitbutler_project::Project; @@ -24,9 +24,64 @@ pub mod snapshot { project.restore_snapshot(snapshot_id.parse()?)?; Ok(()) } + + pub fn diff(project: Project, snapshot_id: String) -> Result<()> { + debug_print(project.snapshot_diff(snapshot_id.parse()?)) + } } fn debug_print(this: impl std::fmt::Debug) -> anyhow::Result<()> { println!("{:#?}", this); Ok(()) } + +pub mod ownership { + use gitbutler_diff::Hunk; + use gitbutler_project::Project; + use gitbutler_stack::{BranchOwnershipClaims, OwnershipClaim}; + use std::path::PathBuf; + + pub fn unapply( + project: Project, + file_path: PathBuf, + from_line: u32, + to_line: u32, + ) -> anyhow::Result<()> { + let claims = BranchOwnershipClaims { + claims: vec![OwnershipClaim { + file_path, + hunks: vec![Hunk { + hash: None, + start: from_line, + end: to_line, + }], + }], + }; + gitbutler_branch_actions::unapply_ownership(&project, &claims) + } +} + +pub mod workspace { + use crate::args::UpdateMode; + use gitbutler_branch_actions::upstream_integration; + use gitbutler_project::Project; + + pub fn update(project: Project, mode: UpdateMode) -> anyhow::Result<()> { + let approach = match mode { + UpdateMode::Rebase => upstream_integration::ResolutionApproach::Rebase, + UpdateMode::Merge => upstream_integration::ResolutionApproach::Merge, + UpdateMode::Unapply => upstream_integration::ResolutionApproach::Unapply, + UpdateMode::Delete => upstream_integration::ResolutionApproach::Delete, + }; + let resolutions: Vec<_> = gitbutler_branch_actions::list_virtual_branches(&project)? + .0 + .into_iter() + .map(|b| upstream_integration::Resolution { + branch_id: b.id, + branch_tree: b.tree, + approach, + }) + .collect(); + gitbutler_branch_actions::integrate_upstream(&project, &resolutions, None) + } +} diff --git a/crates/gitbutler-cli/src/command/vbranch.rs b/crates/gitbutler-cli/src/command/vbranch.rs index 36c9b381fe..48cec91bca 100644 --- a/crates/gitbutler-cli/src/command/vbranch.rs +++ b/crates/gitbutler-cli/src/command/vbranch.rs @@ -1,12 +1,22 @@ -use anyhow::{bail, Result}; +use anyhow::{bail, Context, Result}; use gitbutler_branch::{BranchCreateRequest, BranchIdentity, BranchUpdateRequest}; -use gitbutler_branch_actions::{get_branch_listing_details, list_branches}; +use gitbutler_branch_actions::{get_branch_listing_details, list_branches, BranchManagerExt}; use gitbutler_command_context::CommandContext; use gitbutler_project::Project; use gitbutler_stack::{Stack, VirtualBranchesHandle}; use crate::command::debug_print; +pub fn set_base(project: Project, short_tracking_branch_name: String) -> Result<()> { + let branch_name = format!("refs/remotes/{}", short_tracking_branch_name) + .parse() + .context("Invalid branch name")?; + debug_print(gitbutler_branch_actions::set_base_branch( + &project, + &branch_name, + )?) +} + pub fn list_all(project: Project) -> Result<()> { let ctx = CommandContext::open(&project)?; debug_print(list_branches(&ctx, None, None)?) @@ -53,6 +63,23 @@ pub fn unapply(project: Project, branch_name: String) -> Result<()> { )?) } +pub fn apply(project: Project, branch_name: String) -> Result<()> { + let branch = branch_by_name(&project, &branch_name)?; + let ctx = CommandContext::open(&project)?; + let mut guard = project.exclusive_worktree_access(); + debug_print( + ctx.branch_manager().create_virtual_branch_from_branch( + branch + .source_refname + .as_ref() + .context("local reference name was missing")?, + None, + None, + guard.write_permission(), + )?, + ) +} + pub fn create(project: Project, branch_name: String, set_default: bool) -> Result<()> { let new = gitbutler_branch_actions::create_virtual_branch( &project, diff --git a/crates/gitbutler-cli/src/main.rs b/crates/gitbutler-cli/src/main.rs index c8de8cf0c2..717b4b8c53 100644 --- a/crates/gitbutler-cli/src/main.rs +++ b/crates/gitbutler-cli/src/main.rs @@ -17,14 +17,32 @@ fn main() -> Result<()> { let _op_span = tracing::info_span!("cli-op").entered(); match args.cmd { + args::Subcommands::IntegrateUpstream { mode } => { + let project = command::prepare::project_from_path(args.current_dir)?; + command::workspace::update(project, mode) + } + args::Subcommands::UnapplyOwnership { + filepath, + from_line, + to_line, + } => { + let project = command::prepare::project_from_path(args.current_dir)?; + command::ownership::unapply(project, filepath, from_line, to_line) + } args::Subcommands::Branch(vbranch::Platform { cmd }) => { let project = command::prepare::project_from_path(args.current_dir)?; match cmd { + Some(vbranch::SubCommands::SetBase { + short_tracking_branch_name, + }) => command::vbranch::set_base(project, short_tracking_branch_name), Some(vbranch::SubCommands::ListLocal) => command::vbranch::list_local(project), Some(vbranch::SubCommands::Status) => command::vbranch::status(project), Some(vbranch::SubCommands::Unapply { name }) => { command::vbranch::unapply(project, name) } + Some(vbranch::SubCommands::Apply { name }) => { + command::vbranch::apply(project, name) + } Some(vbranch::SubCommands::SetDefault { name }) => { command::vbranch::set_default(project, name) } @@ -71,6 +89,9 @@ fn main() -> Result<()> { Some(snapshot::SubCommands::Restore { snapshot_id }) => { command::snapshot::restore(project, snapshot_id) } + Some(snapshot::SubCommands::Diff { snapshot_id }) => { + command::snapshot::diff(project, snapshot_id) + } None => command::snapshot::list(project), } } diff --git a/crates/gitbutler-command-context/src/lib.rs b/crates/gitbutler-command-context/src/lib.rs index c4dc09dc6e..5532bc2dd0 100644 --- a/crates/gitbutler-command-context/src/lib.rs +++ b/crates/gitbutler-command-context/src/lib.rs @@ -1,5 +1,6 @@ use anyhow::Result; use gitbutler_project::Project; +use std::path::Path; pub struct CommandContext { /// The git repository of the `project` itself. @@ -86,6 +87,23 @@ impl CommandContext { Ok(gix::open(self.repository().path())?) } + /// Return a newly opened `gitoxide` repository, with all configuration available + /// to correctly figure out author and committer names (i.e. with most global configuration loaded), + /// *and* which will perform diffs quickly thanks to an adequate object cache. + pub fn gix_repository_for_merging(&self) -> Result { + gix_repository_for_merging(self.repository().path()) + } + + /// Return a newly opened `gitoxide` repository, with all configuration available + /// to correctly figure out author and committer names (i.e. with most global configuration loaded), + /// *and* which will perform diffs quickly thanks to an adequate object cache, *and* + /// which **writes all objects into memory**. + /// + /// This means *changes are non-persisting*. + pub fn gix_repository_for_merging_non_persisting(&self) -> Result { + Ok(self.gix_repository_for_merging()?.with_object_memory()) + } + /// Return a newly opened `gitoxide` repository with only the repository-local configuration /// available. This is a little faster as it has to open less files upon startup. /// @@ -99,5 +117,15 @@ impl CommandContext { } } +/// Return a newly opened `gitoxide` repository, with all configuration available +/// to correctly figure out author and committer names (i.e. with most global configuration loaded), +/// *and* which will perform diffs quickly thanks to an adequate object cache. +pub fn gix_repository_for_merging(worktree_or_git_dir: &Path) -> Result { + let mut repo = gix::open(worktree_or_git_dir)?; + let bytes = repo.compute_object_cache_size_for_tree_diffs(&***repo.index_or_empty()?); + repo.object_cache_size_if_unset(bytes); + Ok(repo) +} + mod repository_ext; pub use repository_ext::RepositoryExtLite; diff --git a/crates/gitbutler-oplog/Cargo.toml b/crates/gitbutler-oplog/Cargo.toml index dbfb7c7570..a51acf984e 100644 --- a/crates/gitbutler-oplog/Cargo.toml +++ b/crates/gitbutler-oplog/Cargo.toml @@ -17,6 +17,7 @@ gix = { workspace = true, features = ["dirwalk", "credentials", "parallel"] } toml.workspace = true gitbutler-command-context.workspace = true gitbutler-project.workspace = true +gitbutler-oxidize.workspace = true gitbutler-branch.workspace = true gitbutler-serde.workspace = true gitbutler-fs.workspace = true diff --git a/crates/gitbutler-oplog/src/oplog.rs b/crates/gitbutler-oplog/src/oplog.rs index 9e33bc5b0b..af5475d75c 100644 --- a/crates/gitbutler-oplog/src/oplog.rs +++ b/crates/gitbutler-oplog/src/oplog.rs @@ -6,25 +6,28 @@ use std::{ time::Duration, }; +use super::{ + entry::{OperationKind, Snapshot, SnapshotDetails, Trailer}, + reflog::set_reference_to_oplog, + state::OplogHandle, +}; use anyhow::{anyhow, bail, Context, Result}; -use git2::{DiffOptions, FileMode}; +use git2::FileMode; use gitbutler_command_context::RepositoryExtLite; use gitbutler_diff::{hunks_by_filepath, FileDiff}; +use gitbutler_oxidize::{git2_to_gix_object_id, gix_time_to_git2, gix_to_git2_oid}; use gitbutler_project::{ access::{WorktreeReadPermission, WorktreeWritePermission}, Project, }; -use gitbutler_repo::RepositoryExt; use gitbutler_repo::SignaturePurpose; +use gitbutler_repo::{GixRepositoryExt, RepositoryExt}; use gitbutler_stack::{Stack, VirtualBranchesHandle, VirtualBranchesState}; +use gix::bstr::ByteSlice; +use gix::object::tree::diff::Change; +use gix::prelude::ObjectIdExt; use tracing::instrument; -use super::{ - entry::{OperationKind, Snapshot, SnapshotDetails, Trailer}, - reflog::set_reference_to_oplog, - state::OplogHandle, -}; - const SNAPSHOT_FILE_LIMIT_BYTES: u64 = 32 * 1024 * 1024; /// The Oplog allows for crating snapshots of the current state of the project as well as restoring to a previous snapshot. @@ -165,10 +168,10 @@ impl OplogExt for Project { limit: usize, oplog_commit_id: Option, ) -> Result> { - let repo_path = self.path.as_path(); - let repo = git2::Repository::open(repo_path)?; + let worktree_dir = self.path.as_path(); + let repo = gitbutler_command_context::gix_repository_for_merging(worktree_dir)?; - let traversal_root_id = match oplog_commit_id { + let traversal_root_id = git2_to_gix_object_id(match oplog_commit_id { Some(id) => id, None => { let oplog_state = OplogHandle::new(&self.gb_dir()); @@ -178,30 +181,29 @@ impl OplogExt for Project { return Ok(vec![]); } } - }; - - let oplog_head_commit = repo.find_commit(traversal_root_id)?; - - let mut revwalk = repo.revwalk()?; - revwalk.push(oplog_head_commit.id())?; + }) + .attach(&repo); let mut snapshots = Vec::new(); + let mut wd_trees_cache: HashMap = HashMap::new(); - let mut wd_trees_cache: HashMap = HashMap::new(); - - for commit_id in revwalk { + for commit_info in traversal_root_id.ancestors().all()? { if snapshots.len() == limit { break; } - let commit_id = commit_id?; - let commit = repo.find_commit(commit_id)?; - - if commit.parent_count() > 1 { + let commit_id = commit_info?.id(); + let commit = commit_id.object()?.into_commit(); + let mut parents = commit.parent_ids(); + let (first_parent, second_parent) = (parents.next(), parents.next()); + if second_parent.is_some() { break; } let tree = commit.tree()?; - if tree.get_name("virtual_branches.toml").is_none() { + if tree + .lookup_entry_by_path("virtual_branches.toml")? + .is_none() + { // We reached a tree that is not a snapshot tracing::warn!("Commit {commit_id} didn't seem to be an oplog commit - skipping"); continue; @@ -210,36 +212,55 @@ impl OplogExt for Project { // Get tree id from cache or calculate it let wd_tree = get_workdir_tree(&mut wd_trees_cache, commit_id, &repo)?; + let commit_id = gix_to_git2_oid(commit_id); let details = commit - .message() + .message_raw()? + .to_str() + .ok() .and_then(|msg| SnapshotDetails::from_str(msg).ok()); + let commit_time = gix_time_to_git2(commit.time()?); - if let Ok(parent) = commit.parent(0) { + if let Some(parent_id) = first_parent { // Get tree id from cache or calculate it - let parent_tree = get_workdir_tree(&mut wd_trees_cache, parent.id(), &repo)?; - - let mut opts = DiffOptions::new(); - opts.include_untracked(true); - opts.ignore_submodules(true); - let diff = - repo.diff_tree_to_tree(Some(&parent_tree), Some(&wd_tree), Some(&mut opts))?; let mut files_changed = Vec::new(); - diff.print(git2::DiffFormat::NameOnly, |delta, _, _| { - if let Some(path) = delta.new_file().path() { - files_changed.push(path.to_path_buf()); - } - true - })?; + let mut resource_cache = repo.diff_resource_cache_for_tree_diff()?; + let (mut lines_added, mut lines_removed) = (0, 0); + let parent_tree = get_workdir_tree(&mut wd_trees_cache, parent_id, &repo)?; + parent_tree + .changes()? + .options(|opts| { + opts.track_rewrites(None).track_path(); + }) + .for_each_to_obtain_tree(&wd_tree, |change| -> Result<_> { + match change { + Change::Addition { location, .. } => { + files_changed.push(gix::path::from_bstr(location).into_owned()); + } + Change::Deletion { .. } + | Change::Modification { .. } + | Change::Rewrite { .. } => {} + } + if let Some(counts) = change + .diff(&mut resource_cache) + .ok() + .and_then(|mut platform| platform.line_counts().ok().flatten()) + { + lines_added += u64::from(counts.insertions); + lines_removed += u64::from(counts.removals); + } + resource_cache.clear_resource_cache_keep_allocation(); + + Ok(gix::object::tree::diff::Action::Continue) + })?; - let stats = diff.stats()?; snapshots.push(Snapshot { commit_id, details, - lines_added: stats.insertions(), - lines_removed: stats.deletions(), + lines_added: lines_added as usize, + lines_removed: lines_removed as usize, files_changed, - created_at: commit.time(), + created_at: commit_time, }); } else { // this is the very first snapshot @@ -249,7 +270,7 @@ impl OplogExt for Project { lines_added: 0, lines_removed: 0, files_changed: Vec::new(), - created_at: commit.time(), + created_at: commit_time, }); break; } @@ -279,13 +300,14 @@ impl OplogExt for Project { fn snapshot_diff(&self, sha: git2::Oid) -> Result> { let worktree_dir = self.path.as_path(); + let gix_repo = gitbutler_command_context::gix_repository_for_merging(worktree_dir)?; let repo = git2::Repository::init(worktree_dir)?; let commit = repo.find_commit(sha)?; - let wd_tree_id = tree_from_applied_vbranches(&repo, commit.id())?; + let wd_tree_id = tree_from_applied_vbranches(&gix_repo, commit.id())?; let wd_tree = repo.find_tree(wd_tree_id)?; - let old_wd_tree_id = tree_from_applied_vbranches(&repo, commit.parent(0)?.id())?; + let old_wd_tree_id = tree_from_applied_vbranches(&gix_repo, commit.parent(0)?.id())?; let old_wd_tree = repo.find_tree(old_wd_tree_id)?; repo.ignore_large_files_in_diffs(SNAPSHOT_FILE_LIMIT_BYTES)?; @@ -314,20 +336,20 @@ impl OplogExt for Project { /// Get a tree of the working dir (applied branches merged) fn get_workdir_tree<'a>( - wd_trees_cache: &mut HashMap, - commit_id: git2::Oid, - repo: &'a git2::Repository, -) -> Result, anyhow::Error> { + wd_trees_cache: &mut HashMap, + commit_id: impl Into, + repo: &'a gix::Repository, +) -> Result, anyhow::Error> { + let commit_id = commit_id.into(); if let Entry::Vacant(e) = wd_trees_cache.entry(commit_id) { - if let Ok(wd_tree_id) = tree_from_applied_vbranches(repo, commit_id) { - e.insert(wd_tree_id); + if let Ok(wd_tree_id) = tree_from_applied_vbranches(repo, gix_to_git2_oid(commit_id)) { + e.insert(git2_to_gix_object_id(wd_tree_id)); } } - let wd_tree_id = wd_trees_cache.get(&commit_id).ok_or(anyhow!( + let id = wd_trees_cache.get(&commit_id).copied().ok_or(anyhow!( "Could not get a tree of all applied virtual branches merged" ))?; - let wd_tree = repo.find_tree(wd_tree_id.to_owned())?; - Ok(wd_tree) + Ok(repo.find_tree(id)?) } fn prepare_snapshot(ctx: &Project, _shared_access: &WorktreeReadPermission) -> Result { @@ -574,7 +596,8 @@ fn restore_snapshot( "We will not change a worktree which for some reason isn't on the workspace branch", )?; - let workdir_tree_id = tree_from_applied_vbranches(&repo, snapshot_commit_id)?; + let gix_repo = gitbutler_command_context::gix_repository_for_merging(worktree_dir)?; + let workdir_tree_id = tree_from_applied_vbranches(&gix_repo, snapshot_commit_id)?; let workdir_tree = repo.find_tree(workdir_tree_id)?; repo.ignore_large_files_in_diffs(SNAPSHOT_FILE_LIMIT_BYTES)?; @@ -794,59 +817,54 @@ fn deserialize_commit( } /// Creates a tree that is the merge of all applied branches from a given snapshot and returns the tree id. +/// Note that `repo` must have caching setup for merges. fn tree_from_applied_vbranches( - repo: &git2::Repository, + repo: &gix::Repository, snapshot_commit_id: git2::Oid, ) -> Result { - let snapshot_commit = repo.find_commit(snapshot_commit_id)?; + let snapshot_commit = repo.find_commit(git2_to_gix_object_id(snapshot_commit_id))?; let snapshot_tree = snapshot_commit.tree()?; let target_tree_entry = snapshot_tree - .get_name("target_tree") - .context("failed to get target tree entry")?; - let target_tree = repo - .find_tree(target_tree_entry.id()) - .context("failed to convert target tree entry to tree")?; + .lookup_entry_by_path("target_tree")? + .context("no entry at 'target_entry'")?; + let target_tree_id = target_tree_entry.id().detach(); let vb_toml_entry = snapshot_tree - .get_name("virtual_branches.toml") + .lookup_entry_by_path("virtual_branches.toml")? .context("failed to get virtual_branches.toml blob")?; - // virtual_branches.toml blob let vb_toml_blob = repo .find_blob(vb_toml_entry.id()) .context("failed to convert virtual_branches tree entry to blob")?; - let vbs_from_toml: VirtualBranchesState = toml::from_str(from_utf8(vb_toml_blob.content())?)?; - let applied_branch_trees: Vec = vbs_from_toml + let vbs_from_toml: VirtualBranchesState = toml::from_str(from_utf8(&vb_toml_blob.data)?)?; + let applied_branch_trees: Vec<_> = vbs_from_toml .list_branches_in_workspace()? .iter() - .map(|b| b.tree) + .map(|b| git2_to_gix_object_id(b.tree)) .collect(); - let mut workdir_tree_id = target_tree.id(); - let base_tree = target_tree; - let mut current_ours = base_tree.clone(); - - for branch in applied_branch_trees { - let branch_tree = repo.find_tree(branch)?; - let mut merge_options: git2::MergeOptions = git2::MergeOptions::new(); - merge_options.fail_on_conflict(false); - let mut workdir_temp_index = repo.merge_trees( - &base_tree, - ¤t_ours, - &branch_tree, - Some(&merge_options), + let mut workdir_tree_id = target_tree_id; + let base_tree_id = target_tree_id; + let mut current_ours_id = target_tree_id; + + let (merge_option_fail_fast, conflict_kind) = repo.merge_options_fail_fast()?; + for branch_id in applied_branch_trees { + let mut merge = repo.merge_trees( + base_tree_id, + current_ours_id, + branch_id, + repo.default_merge_labels(), + merge_option_fail_fast.clone(), )?; - match workdir_temp_index.write_tree_to(repo) { - Ok(id) => { - workdir_tree_id = id; - current_ours = repo.find_tree(workdir_tree_id)?; - } - Err(_err) => { - tracing::warn!("Failed to merge tree {branch} - this branch is probably applied at a time when it should not be"); - } + if merge.has_unresolved_conflicts(conflict_kind) { + tracing::warn!("Failed to merge tree {branch_id} - this branch is probably applied at a time when it should not be"); + } else { + let id = merge.tree.write()?.detach(); + workdir_tree_id = id; + current_ours_id = id; } } - Ok(workdir_tree_id) + Ok(gix_to_git2_oid(workdir_tree_id)) } diff --git a/crates/gitbutler-oxidize/src/lib.rs b/crates/gitbutler-oxidize/src/lib.rs index 224da1ed1e..bc28bd15eb 100644 --- a/crates/gitbutler-oxidize/src/lib.rs +++ b/crates/gitbutler-oxidize/src/lib.rs @@ -4,6 +4,10 @@ use anyhow::Context; use gix::bstr::ByteSlice; use std::borrow::Borrow; +pub fn gix_time_to_git2(time: gix::date::Time) -> git2::Time { + git2::Time::new(time.seconds, time.offset) +} + pub fn git2_to_gix_object_id(id: git2::Oid) -> gix::ObjectId { gix::ObjectId::try_from(id.as_bytes()).expect("git2 oid is always valid") } diff --git a/crates/gitbutler-repo/Cargo.toml b/crates/gitbutler-repo/Cargo.toml index acf2d1cdda..af7a68cef1 100644 --- a/crates/gitbutler-repo/Cargo.toml +++ b/crates/gitbutler-repo/Cargo.toml @@ -7,7 +7,7 @@ publish = false [dependencies] git2.workspace = true -gix = { workspace = true, features = ["status", "tree-editor"] } +gix = { workspace = true, features = ["merge", "status", "tree-editor"] } anyhow = "1.0.92" bstr.workspace = true tracing.workspace = true diff --git a/crates/gitbutler-repo/src/repository_ext.rs b/crates/gitbutler-repo/src/repository_ext.rs index 3fadd4cf05..99d140c8cd 100644 --- a/crates/gitbutler-repo/src/repository_ext.rs +++ b/crates/gitbutler-repo/src/repository_ext.rs @@ -17,6 +17,7 @@ use gitbutler_oxidize::{ }; use gitbutler_reference::{Refname, RemoteRefname}; use gix::fs::is_executable; +use gix::merge::tree::{Options, UnresolvedConflict}; use gix::objs::WriteTo; use tracing::instrument; @@ -699,6 +700,47 @@ pub trait GixRepositoryExt: Sized { /// Configure the repository for diff operations between trees. /// This means it needs an object cache relative to the amount of files in the repository. fn for_tree_diffing(self) -> Result; + + /// Returns `true` if the merge between `our_tree` and `their_tree` is free of conflicts. + /// Conflicts entail content merges with conflict markers, or anything else that doesn't merge cleanly in the tree. + /// + /// # Important + /// + /// Make sure the repository is configured [`with_object_memory()`](gix::Repository::with_object_memory()). + fn merges_cleanly_compat( + &self, + ancestor_tree: git2::Oid, + our_tree: git2::Oid, + their_tree: git2::Oid, + ) -> Result; + + /// Just like the above, but with `gix` types. + fn merges_cleanly( + &self, + ancestor_tree: gix::ObjectId, + our_tree: gix::ObjectId, + their_tree: gix::ObjectId, + ) -> Result; + + /// Return default lable names when merging trees. + /// + /// Note that these should probably rather be branch names, but that's for another day. + fn default_merge_labels(&self) -> gix::merge::blob::builtin_driver::text::Labels<'static> { + gix::merge::blob::builtin_driver::text::Labels { + ancestor: Some("base".into()), + current: Some("ours".into()), + other: Some("theirs".into()), + } + } + + /// Return options suitable for merging so that the merge stops immediately after the first conflict. + /// It also returns the conflict kind to use when checking for unresolved conflicts. + fn merge_options_fail_fast( + &self, + ) -> Result<( + gix::merge::tree::Options, + gix::merge::tree::UnresolvedConflict, + )>; } impl GixRepositoryExt for gix::Repository { @@ -707,6 +749,46 @@ impl GixRepositoryExt for gix::Repository { self.object_cache_size_if_unset(bytes); Ok(self) } + + fn merges_cleanly_compat( + &self, + ancestor_tree: git2::Oid, + our_tree: git2::Oid, + their_tree: git2::Oid, + ) -> Result { + self.merges_cleanly( + git2_to_gix_object_id(ancestor_tree), + git2_to_gix_object_id(our_tree), + git2_to_gix_object_id(their_tree), + ) + } + + fn merges_cleanly( + &self, + ancestor_tree: gix::ObjectId, + our_tree: gix::ObjectId, + their_tree: gix::ObjectId, + ) -> Result { + let (options, conflict_kind) = self.merge_options_fail_fast()?; + let merge_outcome = self + .merge_trees( + ancestor_tree, + our_tree, + their_tree, + Default::default(), + options, + ) + .context("failed to merge trees")?; + Ok(!merge_outcome.has_unresolved_conflicts(conflict_kind)) + } + + fn merge_options_fail_fast(&self) -> Result<(Options, UnresolvedConflict)> { + let conflict_kind = gix::merge::tree::UnresolvedConflict::Renames; + let options = self + .tree_merge_options()? + .with_fail_on_conflict(Some(conflict_kind)); + Ok((options, conflict_kind)) + } } type OidFilter = dyn Fn(&git2::Commit) -> Result; diff --git a/crates/gitbutler-repo/tests/create_wd_tree.rs b/crates/gitbutler-repo/tests/create_wd_tree.rs index 0078bf0230..02bffdef4c 100644 --- a/crates/gitbutler-repo/tests/create_wd_tree.rs +++ b/crates/gitbutler-repo/tests/create_wd_tree.rs @@ -29,13 +29,7 @@ mod head_upsert_truthtable { // | add | delete | no-action | #[test] fn index_new_worktree_delete() { - let test_repository = TestingRepository::open(); - - let commit = test_repository.commit_tree(None, &[]); - test_repository - .repository - .branch("master", &commit, true) - .unwrap(); + let test_repository = TestingRepository::open_with_initial_commit(&[]); std::fs::write(test_repository.tempdir.path().join("file1.txt"), "content1").unwrap(); @@ -53,13 +47,8 @@ mod head_upsert_truthtable { // | modify | delete | remove | #[test] fn index_modify_worktree_delete() { - let test_repository = TestingRepository::open(); - - let commit = test_repository.commit_tree(None, &[("file1.txt", "content1")]); - test_repository - .repository - .branch("master", &commit, true) - .unwrap(); + let test_repository = + TestingRepository::open_with_initial_commit(&[("file1.txt", "content1")]); std::fs::write(test_repository.tempdir.path().join("file1.txt"), "content2").unwrap(); @@ -77,13 +66,8 @@ mod head_upsert_truthtable { // | | delete | remove | #[test] fn worktree_delete() { - let test_repository = TestingRepository::open(); - - let commit = test_repository.commit_tree(None, &[("file1.txt", "content1")]); - test_repository - .repository - .branch("master", &commit, true) - .unwrap(); + let test_repository = + TestingRepository::open_with_initial_commit(&[("file1.txt", "content1")]); std::fs::remove_file(test_repository.tempdir.path().join("file1.txt")).unwrap(); @@ -95,13 +79,8 @@ mod head_upsert_truthtable { // | delete | | remove | #[test] fn index_delete() { - let test_repository = TestingRepository::open(); - - let commit = test_repository.commit_tree(None, &[("file1.txt", "content1")]); - test_repository - .repository - .branch("master", &commit, true) - .unwrap(); + let test_repository = + TestingRepository::open_with_initial_commit(&[("file1.txt", "content1")]); let mut index = test_repository.repository.index().unwrap(); index.remove_all(["*"], None).unwrap(); @@ -120,13 +99,8 @@ mod head_upsert_truthtable { // | delete | add | upsert | #[test] fn index_delete_worktree_add() { - let test_repository = TestingRepository::open(); - - let commit = test_repository.commit_tree(None, &[("file1.txt", "content1")]); - test_repository - .repository - .branch("master", &commit, true) - .unwrap(); + let test_repository = + TestingRepository::open_with_initial_commit(&[("file1.txt", "content1")]); let mut index = test_repository.repository.index().unwrap(); index.remove_all(["*"], None).unwrap(); @@ -147,13 +121,7 @@ mod head_upsert_truthtable { // | add | | upsert | #[test] fn index_add() { - let test_repository = TestingRepository::open(); - - let commit = test_repository.commit_tree(None, &[]); - test_repository - .repository - .branch("master", &commit, true) - .unwrap(); + let test_repository = TestingRepository::open_with_initial_commit(&[]); std::fs::write(test_repository.tempdir.path().join("file1.txt"), "content2").unwrap(); @@ -174,13 +142,7 @@ mod head_upsert_truthtable { // | | add | upsert | #[test] fn worktree_add() { - let test_repository = TestingRepository::open(); - - let commit = test_repository.commit_tree(None, &[]); - test_repository - .repository - .branch("master", &commit, true) - .unwrap(); + let test_repository = TestingRepository::open_with_initial_commit(&[]); std::fs::write(test_repository.tempdir.path().join("file1.txt"), "content2").unwrap(); @@ -197,13 +159,7 @@ mod head_upsert_truthtable { // | add | modify | upsert | #[test] fn index_add_worktree_modify() { - let test_repository = TestingRepository::open(); - - let commit = test_repository.commit_tree(None, &[]); - test_repository - .repository - .branch("master", &commit, true) - .unwrap(); + let test_repository = TestingRepository::open_with_initial_commit(&[]); std::fs::write(test_repository.tempdir.path().join("file1.txt"), "content1").unwrap(); @@ -226,13 +182,8 @@ mod head_upsert_truthtable { // | modify | modify | upsert | #[test] fn index_modify_worktree_modify() { - let test_repository = TestingRepository::open(); - - let commit = test_repository.commit_tree(None, &[("file1.txt", "content1")]); - test_repository - .repository - .branch("master", &commit, true) - .unwrap(); + let test_repository = + TestingRepository::open_with_initial_commit(&[("file1.txt", "content1")]); std::fs::write(test_repository.tempdir.path().join("file1.txt"), "content2").unwrap(); @@ -255,16 +206,7 @@ mod head_upsert_truthtable { #[test] fn lists_uncommited_changes() { - let test_repository = TestingRepository::open(); - - // Initial commit - // Create wd tree requires the HEAD branch to exist and for there - // to be at least one commit on that branch. - let commit = test_repository.commit_tree(None, &[]); - test_repository - .repository - .branch("master", &commit, true) - .unwrap(); + let test_repository = TestingRepository::open_with_initial_commit(&[]); std::fs::write(test_repository.tempdir.path().join("file1.txt"), "content1").unwrap(); std::fs::write(test_repository.tempdir.path().join("file2.txt"), "content2").unwrap(); @@ -280,16 +222,7 @@ fn lists_uncommited_changes() { #[test] fn does_not_include_staged_but_deleted_files() { - let test_repository = TestingRepository::open(); - - // Initial commit - // Create wd tree requires the HEAD branch to exist and for there - // to be at least one commit on that branch. - let commit = test_repository.commit_tree(None, &[]); - test_repository - .repository - .branch("master", &commit, true) - .unwrap(); + let test_repository = TestingRepository::open_with_initial_commit(&[]); std::fs::write(test_repository.tempdir.path().join("file1.txt"), "content1").unwrap(); std::fs::write(test_repository.tempdir.path().join("file2.txt"), "content2").unwrap(); @@ -312,16 +245,10 @@ fn does_not_include_staged_but_deleted_files() { #[test] fn should_be_empty_after_checking_out_empty_tree() { - let test_repository = TestingRepository::open(); - - let commit = test_repository.commit_tree( - None, - &[("file1.txt", "content1"), ("file2.txt", "content2")], - ); - test_repository - .repository - .branch("master", &commit, true) - .unwrap(); + let test_repository = TestingRepository::open_with_initial_commit(&[ + ("file1.txt", "content1"), + ("file2.txt", "content2"), + ]); // Checkout an empty tree { @@ -353,16 +280,10 @@ fn should_be_empty_after_checking_out_empty_tree() { #[test] fn should_track_deleted_files() { - let test_repository = TestingRepository::open(); - - let commit = test_repository.commit_tree( - None, - &[("file1.txt", "content1"), ("file2.txt", "content2")], - ); - test_repository - .repository - .branch("master", &commit, true) - .unwrap(); + let test_repository = TestingRepository::open_with_initial_commit(&[ + ("file1.txt", "content1"), + ("file2.txt", "content2"), + ]); // Make sure the index is empty, perhaps the user did this action let mut index: git2::Index = test_repository.repository.index().unwrap(); @@ -384,13 +305,7 @@ fn should_track_deleted_files() { #[test] fn should_not_change_index() { - let test_repository = TestingRepository::open(); - - let commit = test_repository.commit_tree(None, &[("file1.txt", "content1")]); - test_repository - .repository - .branch("master", &commit, true) - .unwrap(); + let test_repository = TestingRepository::open_with_initial_commit(&[("file1.txt", "content1")]); let mut index = test_repository.repository.index().unwrap(); index.remove_all(["*"], None).unwrap(); @@ -417,20 +332,10 @@ fn should_not_change_index() { #[test] fn tree_behavior() { - let test_repository = TestingRepository::open(); - - let commit = test_repository.commit_tree( - None, - &[ - ("dir1/file1.txt", "content1"), - ("dir2/file2.txt", "content2"), - ], - ); - - test_repository - .repository - .branch("master", &commit, true) - .unwrap(); + let test_repository = TestingRepository::open_with_initial_commit(&[ + ("dir1/file1.txt", "content1"), + ("dir2/file2.txt", "content2"), + ]); // Update a file in a directory std::fs::write( @@ -464,13 +369,7 @@ fn tree_behavior() { fn executable_blobs() { use std::{io::Write, os::unix::fs::PermissionsExt as _}; - let test_repository = TestingRepository::open(); - - let commit = test_repository.commit_tree(None, &[]); - test_repository - .repository - .branch("master", &commit, true) - .unwrap(); + let test_repository = TestingRepository::open_with_initial_commit(&[]); let mut file = File::create(test_repository.tempdir.path().join("file1.txt")).unwrap(); file.set_permissions(Permissions::from_mode(0o755)).unwrap(); @@ -492,13 +391,7 @@ fn executable_blobs() { #[cfg(unix)] #[test] fn links() { - let test_repository = TestingRepository::open(); - - let commit = test_repository.commit_tree(None, &[("target", "helloworld")]); - test_repository - .repository - .branch("master", &commit, true) - .unwrap(); + let test_repository = TestingRepository::open_with_initial_commit(&[("target", "helloworld")]); std::os::unix::fs::symlink("target", test_repository.tempdir.path().join("link1.txt")).unwrap(); diff --git a/crates/gitbutler-testsupport/src/testing_repository.rs b/crates/gitbutler-testsupport/src/testing_repository.rs index 5a9555718f..ba86aba037 100644 --- a/crates/gitbutler-testsupport/src/testing_repository.rs +++ b/crates/gitbutler-testsupport/src/testing_repository.rs @@ -15,6 +15,22 @@ impl TestingRepository { pub fn open() -> Self { let tempdir = tempdir().unwrap(); let repository = git2::Repository::init_opts(tempdir.path(), &init_opts()).unwrap(); + // TODO(ST): remove this once `gix::Repository::index_or_load_from_tree_or_empty()` + // is available and used to get merge/diff resource caches. Also: name this + // `open_unborn()` to make it clear. + // For now we need a resemblance of an initialized repo. + let signature = git2::Signature::now("Caleb", "caleb@gitbutler.com").unwrap(); + let empty_tree_id = repository.treebuilder(None).unwrap().write().unwrap(); + repository + .commit( + Some("refs/heads/master"), + &signature, + &signature, + "init to prevent load index failure", + &repository.find_tree(empty_tree_id).unwrap(), + &[], + ) + .unwrap(); let config = repository.config().unwrap(); match config.open_level(git2::ConfigLevel::Local) { @@ -34,9 +50,39 @@ impl TestingRepository { } } + pub fn open_with_initial_commit(files: &[(&str, &str)]) -> Self { + let tempdir = tempdir().unwrap(); + let repository = git2::Repository::init_opts(tempdir.path(), &init_opts()).unwrap(); + + let config = repository.config().unwrap(); + match config.open_level(git2::ConfigLevel::Local) { + Ok(mut local) => { + local.set_str("commit.gpgsign", "false").unwrap(); + local.set_str("user.name", "gitbutler-test").unwrap(); + local + .set_str("user.email", "gitbutler-test@example.com") + .unwrap(); + } + Err(err) => panic!("{}", err), + } + + let repository = Self { + tempdir, + repository, + }; + { + let commit = repository.commit_tree(None, files); + repository + .repository + .branch("master", &commit, true) + .unwrap(); + } + repository + } + pub fn commit_tree<'a>( &'a self, - parent: Option<&git2::Commit<'a>>, + parent: Option<&git2::Commit<'_>>, files: &[(&str, &str)], ) -> git2::Commit<'a> { self.commit_tree_with_message(parent, &Uuid::new_v4().to_string(), files) @@ -44,7 +90,7 @@ impl TestingRepository { pub fn commit_tree_with_message<'a>( &'a self, - parent: Option<&git2::Commit<'a>>, + parent: Option<&git2::Commit<'_>>, message: &str, files: &[(&str, &str)], ) -> git2::Commit<'a> {