Merge pull request #5416 from gitbutlerapp/tree-merge

Replace `merge-tree` in more places
This commit is contained in:
Kiril Videlov 2024-11-05 22:36:04 +01:00 committed by GitHub
commit 7fe5d0c85e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 713 additions and 444 deletions

103
Cargo.lock generated
View File

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

View File

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

View File

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

View File

@ -1,16 +1,5 @@
use std::{path::Path, time};
use anyhow::{anyhow, Context, Result};
use gitbutler_branch::GITBUTLER_WORKSPACE_REFERENCE;
use gitbutler_command_context::CommandContext;
use gitbutler_error::error::Marker;
use gitbutler_project::FetchResult;
use gitbutler_reference::{Refname, RemoteRefname};
use gitbutler_repo::{LogUntil, RepositoryExt};
use gitbutler_repo_actions::RepoActionsExt;
use gitbutler_stack::{BranchOwnershipClaims, Stack, Target, VirtualBranchesHandle};
use serde::Serialize;
use crate::{
conflicts::RepoConflictsExt,
hunk::VirtualBranchHunk,
@ -18,6 +7,17 @@ use crate::{
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::{GixRepositoryExt, LogUntil, RepositoryExt};
use gitbutler_repo_actions::RepoActionsExt;
use gitbutler_stack::{BranchOwnershipClaims, Stack, Target, VirtualBranchesHandle};
use serde::Serialize;
#[derive(Debug, Serialize, PartialEq, Clone)]
#[serde(rename_all = "camelCase")]
@ -50,8 +50,8 @@ pub(crate) fn get_base_branch_data(ctx: &CommandContext) -> Result<BaseBranch> {
}
fn go_back_to_integration(ctx: &CommandContext, default_target: &Target) -> Result<BaseBranch> {
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")?;

View File

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

View File

@ -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::<Vec<(PathBuf, Vec<VirtualBranchHunk>)>>();
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();

View File

@ -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::<Vec<_>>())?;
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)?;
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_index.has_conflicts() {
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()

View File

@ -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<git2::Oid> {
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<git2::Oid> {
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 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 index = repo.merge_trees(&merge_tree, &workspace_tree, &branch_tree, None)?;
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<git2::Oid> {
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)?;

View File

@ -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::{Branch, CommitOrChangeId, ForgeIdentifier, PatchReferenceUpdate, Series};
use gitbutler_stack::{Stack, StackId, Target};
@ -190,10 +189,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)?;

View File

@ -1,19 +1,19 @@
use anyhow::{anyhow, bail, Result};
use gitbutler_cherry_pick::RepositoryExt as _;
use gitbutler_command_context::CommandContext;
use gitbutler_project::access::WorktreeWritePermission;
use gitbutler_repo::{
rebase::{cherry_rebase_group, gitbutler_merge_commits},
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 _,
};
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},
GixRepositoryExt, LogUntil, RepositoryExt as _,
};
use gitbutler_repo_actions::RepoActionsExt as _;
use gitbutler_stack::{Stack, StackId, Target, VirtualBranchesHandle};
use serde::{Deserialize, Serialize};
#[derive(Serialize, PartialEq, Debug)]
#[serde(tag = "type", content = "subject", rename_all = "camelCase")]
@ -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));
}
}

View File

@ -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::<Vec<(PathBuf, Vec<VirtualBranchHunk>)>>();
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)
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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> {
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<gix::Repository> {
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<gix::Repository> {
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;

View File

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

View File

@ -6,24 +6,27 @@ use std::{
time::Duration,
};
use anyhow::{anyhow, bail, Context, Result};
use git2::{DiffOptions, FileMode};
use gitbutler_command_context::RepositoryExtLite;
use gitbutler_diff::{hunks_by_filepath, FileDiff};
use gitbutler_project::{
access::{WorktreeReadPermission, WorktreeWritePermission},
Project,
};
use gitbutler_repo::RepositoryExt;
use gitbutler_repo::SignaturePurpose;
use gitbutler_stack::{Stack, VirtualBranchesHandle, VirtualBranchesState};
use tracing::instrument;
use super::{
entry::{OperationKind, Snapshot, SnapshotDetails, Trailer},
reflog::set_reference_to_oplog,
state::OplogHandle,
};
use anyhow::{anyhow, bail, Context, Result};
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::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;
const SNAPSHOT_FILE_LIMIT_BYTES: u64 = 32 * 1024 * 1024;
@ -165,10 +168,10 @@ impl OplogExt for Project {
limit: usize,
oplog_commit_id: Option<git2::Oid>,
) -> Result<Vec<Snapshot>> {
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<gix::ObjectId, gix::ObjectId> = HashMap::new();
let mut wd_trees_cache: HashMap<git2::Oid, git2::Oid> = 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<HashMap<PathBuf, FileDiff>> {
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<git2::Oid, git2::Oid>,
commit_id: git2::Oid,
repo: &'a git2::Repository,
) -> Result<git2::Tree<'a>, anyhow::Error> {
wd_trees_cache: &mut HashMap<gix::ObjectId, gix::ObjectId>,
commit_id: impl Into<gix::ObjectId>,
repo: &'a gix::Repository,
) -> Result<gix::Tree<'a>, 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<git2::Oid> {
@ -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<git2::Oid> {
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<git2::Oid> = 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();
let mut workdir_tree_id = target_tree_id;
let base_tree_id = target_tree_id;
let mut current_ours_id = target_tree_id;
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,
&current_ours,
&branch_tree,
Some(&merge_options),
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))
}

View File

@ -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")
}

View File

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

View File

@ -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<Self>;
/// 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<bool>;
/// 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<bool>;
/// 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<bool> {
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<bool> {
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<bool>;

View File

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

View File

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