diff --git a/Cargo.lock b/Cargo.lock index f9ef0fe68a..1a7486a9b5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2293,22 +2293,12 @@ dependencies = [ "bstr", "git2", "gitbutler-diff", - "gitbutler-error", - "gitbutler-fs", - "gitbutler-id", - "gitbutler-oxidize", "gitbutler-reference", - "gitbutler-serde", "gitbutler-stack", - "gitbutler-time", "gix", - "hex", "itertools 0.13.0", "lazy_static", - "md5", "serde", - "toml 0.8.19", - "tracing", ] [[package]] @@ -2373,6 +2363,8 @@ dependencies = [ "anyhow", "git2", "gitbutler-commit", + "gitbutler-oxidize", + "gix", ] [[package]] @@ -2416,6 +2408,7 @@ version = "0.0.0" dependencies = [ "bstr", "git2", + "gix", "uuid", ] @@ -2461,13 +2454,15 @@ dependencies = [ "gitbutler-diff", "gitbutler-operating-modes", "gitbutler-oplog", + "gitbutler-oxidize", "gitbutler-project", "gitbutler-reference", "gitbutler-repo", "gitbutler-stack", - "gitbutler-time", "gitbutler-workspace", + "gix", "serde", + "tracing", ] [[package]] @@ -2516,7 +2511,7 @@ version = "0.0.0" dependencies = [ "assert_cmd", "futures", - "gix-path", + "gix-path 0.10.13 (registry+https://github.com/rust-lang/crates.io-index)", "nix", "rand 0.8.5", "serde", @@ -2532,18 +2527,13 @@ name = "gitbutler-hunk-dependency" version = "0.0.0" dependencies = [ "anyhow", - "bstr", "git2", "gitbutler-diff", "gitbutler-id", - "gitbutler-reference", "gitbutler-serde", "gitbutler-stack", - "gix", "itertools 0.13.0", "serde", - "tokio", - "uuid", ] [[package]] @@ -2673,7 +2663,6 @@ dependencies = [ "gitbutler-commit", "gitbutler-config", "gitbutler-error", - "gitbutler-git", "gitbutler-oxidize", "gitbutler-project", "gitbutler-reference", @@ -2760,7 +2749,7 @@ dependencies = [ "gitbutler-testsupport", "gitbutler-time", "gix", - "gix-utils", + "gix-utils 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)", "itertools 0.13.0", "serde", "tempfile", @@ -2972,8 +2961,7 @@ dependencies = [ [[package]] name = "gix" version = "0.68.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b04c66359b5e17f92395abc433861df0edf48f39f3f590818d1d7217327dd6a1" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd#520c832cfcfb34eb7617be55ebe2719ab35595fd" dependencies = [ "gix-actor 0.33.1", "gix-attributes 0.23.1", @@ -2999,7 +2987,7 @@ dependencies = [ "gix-object 0.46.0", "gix-odb", "gix-pack", - "gix-path", + "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd)", "gix-pathspec", "gix-prompt", "gix-protocol", @@ -3007,15 +2995,15 @@ dependencies = [ "gix-refspec", "gix-revision", "gix-revwalk 0.17.0", - "gix-sec", + "gix-sec 0.10.10 (git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd)", "gix-status", "gix-submodule", "gix-tempfile 15.0.0", - "gix-trace", + "gix-trace 0.1.11 (git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd)", "gix-transport", "gix-traverse 0.43.0", "gix-url", - "gix-utils", + "gix-utils 0.1.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd)", "gix-validate 0.9.2", "gix-worktree 0.38.0", "gix-worktree-state", @@ -3032,7 +3020,7 @@ checksum = "a0e454357e34b833cc3a00b6efbbd3dd4d18b24b9fb0c023876ec2645e8aa3f2" dependencies = [ "bstr", "gix-date 0.8.7", - "gix-utils", + "gix-utils 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)", "itoa 1.0.11", "thiserror 1.0.69", "winnow 0.6.20", @@ -3041,12 +3029,11 @@ dependencies = [ [[package]] name = "gix-actor" version = "0.33.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b24171f514cef7bb4dfb72a0b06dacf609b33ba8ad2489d4c4559a03b7afb3" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd#520c832cfcfb34eb7617be55ebe2719ab35595fd" dependencies = [ "bstr", "gix-date 0.9.2", - "gix-utils", + "gix-utils 0.1.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd)", "itoa 1.0.11", "thiserror 2.0.3", "winnow 0.6.20", @@ -3060,9 +3047,9 @@ checksum = "ebccbf25aa4a973dd352564a9000af69edca90623e8a16dad9cbc03713131311" dependencies = [ "bstr", "gix-glob 0.16.5", - "gix-path", - "gix-quote", - "gix-trace", + "gix-path 0.10.13 (registry+https://github.com/rust-lang/crates.io-index)", + "gix-quote 0.4.14 (registry+https://github.com/rust-lang/crates.io-index)", + "gix-trace 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", "kstring", "smallvec", "thiserror 1.0.69", @@ -3072,14 +3059,13 @@ dependencies = [ [[package]] name = "gix-attributes" version = "0.23.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddf9bf852194c0edfe699a2d36422d2c1f28f73b7c6d446c3f0ccd3ba232cadc" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd#520c832cfcfb34eb7617be55ebe2719ab35595fd" dependencies = [ "bstr", "gix-glob 0.17.1", - "gix-path", - "gix-quote", - "gix-trace", + "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd)", + "gix-quote 0.4.14 (git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd)", + "gix-trace 0.1.11 (git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd)", "kstring", "smallvec", "thiserror 2.0.3", @@ -3095,6 +3081,14 @@ dependencies = [ "thiserror 2.0.3", ] +[[package]] +name = "gix-bitmap" +version = "0.2.13" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd#520c832cfcfb34eb7617be55ebe2719ab35595fd" +dependencies = [ + "thiserror 2.0.3", +] + [[package]] name = "gix-chunk" version = "0.4.10" @@ -3104,15 +3098,22 @@ dependencies = [ "thiserror 2.0.3", ] +[[package]] +name = "gix-chunk" +version = "0.4.10" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd#520c832cfcfb34eb7617be55ebe2719ab35595fd" +dependencies = [ + "thiserror 2.0.3", +] + [[package]] name = "gix-command" version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d7d6b8f3a64453fd7e8191eb80b351eb7ac0839b40a1237cd2c137d5079fe53" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd#520c832cfcfb34eb7617be55ebe2719ab35595fd" dependencies = [ "bstr", - "gix-path", - "gix-trace", + "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd)", + "gix-trace 0.1.11 (git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd)", "shell-words", ] @@ -3123,7 +3124,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "133b06f67f565836ec0c473e2116a60fb74f80b6435e21d88013ac0e3c60fc78" dependencies = [ "bstr", - "gix-chunk", + "gix-chunk 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)", "gix-features 0.38.2", "gix-hash 0.14.2", "memmap2", @@ -3133,11 +3134,10 @@ dependencies = [ [[package]] name = "gix-commitgraph" version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8da6591a7868fb2b6dabddea6b09988b0b05e0213f938dbaa11a03dd7a48d85" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd#520c832cfcfb34eb7617be55ebe2719ab35595fd" dependencies = [ "bstr", - "gix-chunk", + "gix-chunk 0.4.10 (git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd)", "gix-features 0.39.1", "gix-hash 0.15.1", "memmap2", @@ -3147,16 +3147,15 @@ dependencies = [ [[package]] name = "gix-config" version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6649b406ca1f99cb148959cf00468b231f07950f8ec438cc0903cda563606f19" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd#520c832cfcfb34eb7617be55ebe2719ab35595fd" dependencies = [ "bstr", "gix-config-value", "gix-features 0.39.1", "gix-glob 0.17.1", - "gix-path", + "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd)", "gix-ref 0.49.0", - "gix-sec", + "gix-sec 0.10.10 (git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd)", "memchr", "once_cell", "smallvec", @@ -3168,12 +3167,11 @@ dependencies = [ [[package]] name = "gix-config-value" version = "0.14.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49aaeef5d98390a3bcf9dbc6440b520b793d1bf3ed99317dc407b02be995b28e" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd#520c832cfcfb34eb7617be55ebe2719ab35595fd" dependencies = [ "bitflags 2.6.0", "bstr", - "gix-path", + "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd)", "libc", "thiserror 2.0.3", ] @@ -3181,16 +3179,15 @@ dependencies = [ [[package]] name = "gix-credentials" version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2be87bb8685fc7e6e7032ef71c45068ffff609724a0c897b8047fde10db6ae71" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd#520c832cfcfb34eb7617be55ebe2719ab35595fd" dependencies = [ "bstr", "gix-command", "gix-config-value", - "gix-path", + "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd)", "gix-prompt", - "gix-sec", - "gix-trace", + "gix-sec 0.10.10 (git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd)", + "gix-trace 0.1.11 (git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd)", "gix-url", "thiserror 2.0.3", ] @@ -3210,8 +3207,7 @@ dependencies = [ [[package]] name = "gix-date" version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "691142b1a34d18e8ed6e6114bc1a2736516c5ad60ef3aa9bd1b694886e3ca92d" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd#520c832cfcfb34eb7617be55ebe2719ab35595fd" dependencies = [ "bstr", "itoa 1.0.11", @@ -3222,8 +3218,7 @@ dependencies = [ [[package]] name = "gix-diff" version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a327be31a392144b60ab0b1c863362c32a1c8f7effdfa2141d5d5b6b916ef3bf" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd#520c832cfcfb34eb7617be55ebe2719ab35595fd" dependencies = [ "bstr", "gix-command", @@ -3231,9 +3226,9 @@ dependencies = [ "gix-fs 0.12.0", "gix-hash 0.15.1", "gix-object 0.46.0", - "gix-path", + "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd)", "gix-tempfile 15.0.0", - "gix-trace", + "gix-trace 0.1.11 (git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd)", "gix-traverse 0.43.0", "gix-worktree 0.38.0", "imara-diff", @@ -3243,8 +3238,7 @@ dependencies = [ [[package]] name = "gix-dir" version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acd6a0618958f9cce78a32724f8e06c4f4a57ca7080f645736d53676dc9b4db9" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd#520c832cfcfb34eb7617be55ebe2719ab35595fd" dependencies = [ "bstr", "gix-discover 0.37.0", @@ -3252,10 +3246,10 @@ dependencies = [ "gix-ignore 0.12.1", "gix-index 0.37.0", "gix-object 0.46.0", - "gix-path", + "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd)", "gix-pathspec", - "gix-trace", - "gix-utils", + "gix-trace 0.1.11 (git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd)", + "gix-utils 0.1.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd)", "gix-worktree 0.38.0", "thiserror 2.0.3", ] @@ -3270,25 +3264,24 @@ dependencies = [ "dunce", "gix-fs 0.11.3", "gix-hash 0.14.2", - "gix-path", + "gix-path 0.10.13 (registry+https://github.com/rust-lang/crates.io-index)", "gix-ref 0.44.1", - "gix-sec", + "gix-sec 0.10.10 (registry+https://github.com/rust-lang/crates.io-index)", "thiserror 1.0.69", ] [[package]] name = "gix-discover" version = "0.37.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83bf6dfa4e266a4a9becb4d18fc801f92c3f7cc6c433dd86fdadbcf315ffb6ef" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd#520c832cfcfb34eb7617be55ebe2719ab35595fd" dependencies = [ "bstr", "dunce", "gix-fs 0.12.0", "gix-hash 0.15.1", - "gix-path", + "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd)", "gix-ref 0.49.0", - "gix-sec", + "gix-sec 0.10.10 (git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd)", "thiserror 2.0.3", ] @@ -3299,8 +3292,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac7045ac9fe5f9c727f38799d002a7ed3583cd777e3322a7c4b43e3cf437dc69" dependencies = [ "gix-hash 0.14.2", - "gix-trace", - "gix-utils", + "gix-trace 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", + "gix-utils 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)", "libc", "prodash 28.0.0", "sha1_smol", @@ -3310,16 +3303,15 @@ dependencies = [ [[package]] name = "gix-features" version = "0.39.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d85d673f2e022a340dba4713bed77ef2cf4cd737d2f3e0f159d45e0935fd81f" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd#520c832cfcfb34eb7617be55ebe2719ab35595fd" dependencies = [ "bytes", "crc32fast", "crossbeam-channel", "flate2", "gix-hash 0.15.1", - "gix-trace", - "gix-utils", + "gix-trace 0.1.11 (git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd)", + "gix-utils 0.1.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd)", "libc", "once_cell", "parking_lot", @@ -3333,8 +3325,7 @@ dependencies = [ [[package]] name = "gix-filter" version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5108cc58d58b27df10ac4de7f31b2eb96d588a33e5eba23739b865f5d8db7995" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd#520c832cfcfb34eb7617be55ebe2719ab35595fd" dependencies = [ "bstr", "encoding_rs", @@ -3343,10 +3334,10 @@ dependencies = [ "gix-hash 0.15.1", "gix-object 0.46.0", "gix-packetline-blocking", - "gix-path", - "gix-quote", - "gix-trace", - "gix-utils", + "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd)", + "gix-quote 0.4.14 (git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd)", + "gix-trace 0.1.11 (git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd)", + "gix-utils 0.1.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd)", "smallvec", "thiserror 2.0.3", ] @@ -3359,18 +3350,17 @@ checksum = "f2bfe6249cfea6d0c0e0990d5226a4cb36f030444ba9e35e0639275db8f98575" dependencies = [ "fastrand", "gix-features 0.38.2", - "gix-utils", + "gix-utils 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "gix-fs" version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34740384d8d763975858fa2c176b68652a6fcc09f616e24e3ce967b0d370e4d8" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd#520c832cfcfb34eb7617be55ebe2719ab35595fd" dependencies = [ "fastrand", "gix-features 0.39.1", - "gix-utils", + "gix-utils 0.1.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd)", ] [[package]] @@ -3382,19 +3372,18 @@ dependencies = [ "bitflags 2.6.0", "bstr", "gix-features 0.38.2", - "gix-path", + "gix-path 0.10.13 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "gix-glob" version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aaf69a6bec0a3581567484bf99a4003afcaf6c469fd4214352517ea355cf3435" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd#520c832cfcfb34eb7617be55ebe2719ab35595fd" dependencies = [ "bitflags 2.6.0", "bstr", "gix-features 0.39.1", - "gix-path", + "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd)", ] [[package]] @@ -3410,8 +3399,7 @@ dependencies = [ [[package]] name = "gix-hash" version = "0.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b5eccc17194ed0e67d49285e4853307e4147e95407f91c1c3e4a13ba9f4e4ce" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd#520c832cfcfb34eb7617be55ebe2719ab35595fd" dependencies = [ "faster-hex", "thiserror 2.0.3", @@ -3431,8 +3419,7 @@ dependencies = [ [[package]] name = "gix-hashtable" version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ef65b256631078ef733bc5530c4e6b1c2e7d5c2830b75d4e9034ab3997d18fe" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd#520c832cfcfb34eb7617be55ebe2719ab35595fd" dependencies = [ "gix-hash 0.15.1", "hashbrown 0.14.5", @@ -3447,21 +3434,20 @@ checksum = "e447cd96598460f5906a0f6c75e950a39f98c2705fc755ad2f2020c9e937fab7" dependencies = [ "bstr", "gix-glob 0.16.5", - "gix-path", - "gix-trace", + "gix-path 0.10.13 (registry+https://github.com/rust-lang/crates.io-index)", + "gix-trace 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", "unicode-bom", ] [[package]] name = "gix-ignore" version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6b1fb24d2a4af0aa7438e2771d60c14a80cf2c9bd55c29cf1712b841f05bb8a" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd#520c832cfcfb34eb7617be55ebe2719ab35595fd" dependencies = [ "bstr", "gix-glob 0.17.1", - "gix-path", - "gix-trace", + "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd)", + "gix-trace 0.1.11 (git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd)", "unicode-bom", ] @@ -3475,14 +3461,14 @@ dependencies = [ "bstr", "filetime", "fnv", - "gix-bitmap", + "gix-bitmap 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)", "gix-features 0.38.2", "gix-fs 0.11.3", "gix-hash 0.14.2", "gix-lock 14.0.0", "gix-object 0.42.3", "gix-traverse 0.39.2", - "gix-utils", + "gix-utils 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)", "gix-validate 0.8.5", "hashbrown 0.14.5", "itoa 1.0.11", @@ -3496,21 +3482,20 @@ dependencies = [ [[package]] name = "gix-index" version = "0.37.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "270645fd20556b64c8ffa1540d921b281e6994413a0ca068596f97e9367a257a" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd#520c832cfcfb34eb7617be55ebe2719ab35595fd" dependencies = [ "bitflags 2.6.0", "bstr", "filetime", "fnv", - "gix-bitmap", + "gix-bitmap 0.2.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd)", "gix-features 0.39.1", "gix-fs 0.12.0", "gix-hash 0.15.1", "gix-lock 15.0.1", "gix-object 0.46.0", "gix-traverse 0.43.0", - "gix-utils", + "gix-utils 0.1.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd)", "gix-validate 0.9.2", "hashbrown 0.14.5", "itoa 1.0.11", @@ -3528,26 +3513,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bc7fe297f1f4614774989c00ec8b1add59571dc9b024b4c00acb7dedd4e19d" dependencies = [ "gix-tempfile 14.0.2", - "gix-utils", + "gix-utils 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)", "thiserror 1.0.69", ] [[package]] name = "gix-lock" version = "15.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd3ab68a452db63d9f3ebdacb10f30dba1fa0d31ac64f4203d395ed1102d940" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd#520c832cfcfb34eb7617be55ebe2719ab35595fd" dependencies = [ "gix-tempfile 15.0.0", - "gix-utils", + "gix-utils 0.1.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd)", "thiserror 2.0.3", ] [[package]] name = "gix-merge" version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d4d0d11ac3fc1a3082f88b5e4cca706512f6b813154c4541264beed7850e62" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd#520c832cfcfb34eb7617be55ebe2719ab35595fd" dependencies = [ "bstr", "gix-command", @@ -3557,12 +3540,12 @@ dependencies = [ "gix-hash 0.15.1", "gix-index 0.37.0", "gix-object 0.46.0", - "gix-path", - "gix-quote", + "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd)", + "gix-quote 0.4.14 (git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd)", "gix-revision", "gix-revwalk 0.17.0", "gix-tempfile 15.0.0", - "gix-trace", + "gix-trace 0.1.11 (git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd)", "gix-worktree 0.38.0", "imara-diff", "thiserror 2.0.3", @@ -3571,8 +3554,7 @@ dependencies = [ [[package]] name = "gix-negotiate" version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d27f830a16405386e9c83b9d5be8261fe32bbd6b3caf15bd1b284c6b2b7ef1a8" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd#520c832cfcfb34eb7617be55ebe2719ab35595fd" dependencies = [ "bitflags 2.6.0", "gix-commitgraph 0.25.1", @@ -3595,7 +3577,7 @@ dependencies = [ "gix-date 0.8.7", "gix-features 0.38.2", "gix-hash 0.14.2", - "gix-utils", + "gix-utils 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)", "gix-validate 0.8.5", "itoa 1.0.11", "smallvec", @@ -3606,8 +3588,7 @@ dependencies = [ [[package]] name = "gix-object" version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65d93e2bbfa83a307e47f45e45de7b6c04d7375a8bd5907b215f4bf45237d879" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd#520c832cfcfb34eb7617be55ebe2719ab35595fd" dependencies = [ "bstr", "gix-actor 0.33.1", @@ -3615,7 +3596,8 @@ dependencies = [ "gix-features 0.39.1", "gix-hash 0.15.1", "gix-hashtable 0.6.0", - "gix-utils", + "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd)", + "gix-utils 0.1.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd)", "gix-validate 0.9.2", "itoa 1.0.11", "smallvec", @@ -3626,8 +3608,7 @@ dependencies = [ [[package]] name = "gix-odb" version = "0.65.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93bed6e1b577c25a6bb8e6ecbf4df525f29a671ddf5f2221821a56a8dbeec4e3" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd#520c832cfcfb34eb7617be55ebe2719ab35595fd" dependencies = [ "arc-swap", "gix-date 0.9.2", @@ -3637,8 +3618,8 @@ dependencies = [ "gix-hashtable 0.6.0", "gix-object 0.46.0", "gix-pack", - "gix-path", - "gix-quote", + "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd)", + "gix-quote 0.4.14 (git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd)", "parking_lot", "tempfile", "thiserror 2.0.3", @@ -3647,16 +3628,15 @@ dependencies = [ [[package]] name = "gix-pack" version = "0.55.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b91fec04d359544fecbb8e85117ec746fbaa9046ebafcefb58cb74f20dc76d4" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd#520c832cfcfb34eb7617be55ebe2719ab35595fd" dependencies = [ "clru", - "gix-chunk", + "gix-chunk 0.4.10 (git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd)", "gix-features 0.39.1", "gix-hash 0.15.1", "gix-hashtable 0.6.0", "gix-object 0.46.0", - "gix-path", + "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd)", "gix-tempfile 15.0.0", "memmap2", "parking_lot", @@ -3668,24 +3648,22 @@ dependencies = [ [[package]] name = "gix-packetline" version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a720e5bebf494c3ceffa85aa89f57a5859450a0da0a29ebe89171e23543fa78" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd#520c832cfcfb34eb7617be55ebe2719ab35595fd" dependencies = [ "bstr", "faster-hex", - "gix-trace", + "gix-trace 0.1.11 (git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd)", "thiserror 2.0.3", ] [[package]] name = "gix-packetline-blocking" version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce9004ce1bc00fd538b11c1ec8141a1558fb3af3d2b7ac1ac5c41881f9e42d2a" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd#520c832cfcfb34eb7617be55ebe2719ab35595fd" dependencies = [ "bstr", "faster-hex", - "gix-trace", + "gix-trace 0.1.11 (git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd)", "thiserror 2.0.3", ] @@ -3696,7 +3674,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afc292ef1a51e340aeb0e720800338c805975724c1dfbd243185452efd8645b7" dependencies = [ "bstr", - "gix-trace", + "gix-trace 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", + "home", + "once_cell", + "thiserror 2.0.3", +] + +[[package]] +name = "gix-path" +version = "0.10.13" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd#520c832cfcfb34eb7617be55ebe2719ab35595fd" +dependencies = [ + "bstr", + "gix-trace 0.1.11 (git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd)", "home", "once_cell", "thiserror 2.0.3", @@ -3705,23 +3695,21 @@ dependencies = [ [[package]] name = "gix-pathspec" version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c472dfbe4a4e96fcf7efddcd4771c9037bb4fdea2faaabf2f4888210c75b81e" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd#520c832cfcfb34eb7617be55ebe2719ab35595fd" dependencies = [ "bitflags 2.6.0", "bstr", "gix-attributes 0.23.1", "gix-config-value", "gix-glob 0.17.1", - "gix-path", + "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd)", "thiserror 2.0.3", ] [[package]] name = "gix-prompt" version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a7822afc4bc9c5fbbc6ce80b00f41c129306b7685cac3248dbfa14784960594" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd#520c832cfcfb34eb7617be55ebe2719ab35595fd" dependencies = [ "gix-command", "gix-config-value", @@ -3733,8 +3721,7 @@ dependencies = [ [[package]] name = "gix-protocol" version = "0.46.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a7e7e51a0dea531d3448c297e2fa919b2de187111a210c324b7e9f81508b8ca" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd#520c832cfcfb34eb7617be55ebe2719ab35595fd" dependencies = [ "bstr", "gix-credentials", @@ -3742,7 +3729,7 @@ dependencies = [ "gix-features 0.39.1", "gix-hash 0.15.1", "gix-transport", - "gix-utils", + "gix-utils 0.1.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd)", "maybe-async", "thiserror 2.0.3", "winnow 0.6.20", @@ -3755,7 +3742,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64a1e282216ec2ab2816cd57e6ed88f8009e634aec47562883c05ac8a7009a63" dependencies = [ "bstr", - "gix-utils", + "gix-utils 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)", + "thiserror 2.0.3", +] + +[[package]] +name = "gix-quote" +version = "0.4.14" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd#520c832cfcfb34eb7617be55ebe2719ab35595fd" +dependencies = [ + "bstr", + "gix-utils 0.1.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd)", "thiserror 2.0.3", ] @@ -3772,9 +3769,9 @@ dependencies = [ "gix-hash 0.14.2", "gix-lock 14.0.0", "gix-object 0.42.3", - "gix-path", + "gix-path 0.10.13 (registry+https://github.com/rust-lang/crates.io-index)", "gix-tempfile 14.0.2", - "gix-utils", + "gix-utils 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)", "gix-validate 0.8.5", "memmap2", "thiserror 1.0.69", @@ -3784,8 +3781,7 @@ dependencies = [ [[package]] name = "gix-ref" version = "0.49.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1eae462723686272a58f49501015ef7c0d67c3e042c20049d8dd9c7eff92efde" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd#520c832cfcfb34eb7617be55ebe2719ab35595fd" dependencies = [ "gix-actor 0.33.1", "gix-features 0.39.1", @@ -3793,9 +3789,9 @@ dependencies = [ "gix-hash 0.15.1", "gix-lock 15.0.1", "gix-object 0.46.0", - "gix-path", + "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd)", "gix-tempfile 15.0.0", - "gix-utils", + "gix-utils 0.1.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd)", "gix-validate 0.9.2", "memmap2", "thiserror 2.0.3", @@ -3805,8 +3801,7 @@ dependencies = [ [[package]] name = "gix-refspec" version = "0.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00c056bb747868c7eb0aeb352c9f9181ab8ca3d0a2550f16470803500c6c413d" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd#520c832cfcfb34eb7617be55ebe2719ab35595fd" dependencies = [ "bstr", "gix-hash 0.15.1", @@ -3819,8 +3814,7 @@ dependencies = [ [[package]] name = "gix-revision" version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44488e0380847967bc3e3cacd8b22652e02ea1eb58afb60edd91847695cd2d8d" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd#520c832cfcfb34eb7617be55ebe2719ab35595fd" dependencies = [ "bitflags 2.6.0", "bstr", @@ -3830,7 +3824,7 @@ dependencies = [ "gix-hashtable 0.6.0", "gix-object 0.46.0", "gix-revwalk 0.17.0", - "gix-trace", + "gix-trace 0.1.11 (git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd)", "thiserror 2.0.3", ] @@ -3852,8 +3846,7 @@ dependencies = [ [[package]] name = "gix-revwalk" version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "510026fc32f456f8f067d8f37c34088b97a36b2229d88a6a5023ef179fcb109d" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd#520c832cfcfb34eb7617be55ebe2719ab35595fd" dependencies = [ "gix-commitgraph 0.25.1", "gix-date 0.9.2", @@ -3871,7 +3864,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8b876ef997a955397809a2ec398d6a45b7a55b4918f2446344330f778d14fd6" dependencies = [ "bitflags 2.6.0", - "gix-path", + "gix-path 0.10.13 (registry+https://github.com/rust-lang/crates.io-index)", + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "gix-sec" +version = "0.10.10" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd#520c832cfcfb34eb7617be55ebe2719ab35595fd" +dependencies = [ + "bitflags 2.6.0", + "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd)", "libc", "windows-sys 0.52.0", ] @@ -3879,8 +3883,7 @@ dependencies = [ [[package]] name = "gix-status" version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "201396192ee4c4dd9e8a84fed4b0d2b33d639fca815fb99b0f653dfeddf38585" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd#520c832cfcfb34eb7617be55ebe2719ab35595fd" dependencies = [ "bstr", "filetime", @@ -3892,7 +3895,7 @@ dependencies = [ "gix-hash 0.15.1", "gix-index 0.37.0", "gix-object 0.46.0", - "gix-path", + "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd)", "gix-pathspec", "gix-worktree 0.38.0", "portable-atomic", @@ -3902,12 +3905,11 @@ dependencies = [ [[package]] name = "gix-submodule" version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2455f8c0fcb6ebe2a6e83c8f522d30615d763eb2ef7a23c7d929f9476e89f5c" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd#520c832cfcfb34eb7617be55ebe2719ab35595fd" dependencies = [ "bstr", "gix-config", - "gix-path", + "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd)", "gix-pathspec", "gix-refspec", "gix-url", @@ -3932,8 +3934,7 @@ dependencies = [ [[package]] name = "gix-tempfile" version = "15.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2feb86ef094cc77a4a9a5afbfe5de626897351bbbd0de3cb9314baf3049adb82" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd#520c832cfcfb34eb7617be55ebe2719ab35595fd" dependencies = [ "dashmap", "gix-fs 0.12.0", @@ -3974,6 +3975,11 @@ name = "gix-trace" version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04bdde120c29f1fc23a24d3e115aeeea3d60d8e65bab92cc5f9d90d9302eb952" + +[[package]] +name = "gix-trace" +version = "0.1.11" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd#520c832cfcfb34eb7617be55ebe2719ab35595fd" dependencies = [ "tracing-core", ] @@ -3981,8 +3987,7 @@ dependencies = [ [[package]] name = "gix-transport" version = "0.43.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39a1a41357b7236c03e0c984147f823d87c3e445a8581bac7006df141577200b" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd#520c832cfcfb34eb7617be55ebe2719ab35595fd" dependencies = [ "base64 0.22.1", "bstr", @@ -3991,8 +3996,8 @@ dependencies = [ "gix-credentials", "gix-features 0.39.1", "gix-packetline", - "gix-quote", - "gix-sec", + "gix-quote 0.4.14 (git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd)", + "gix-sec 0.10.10 (git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd)", "gix-url", "thiserror 2.0.3", ] @@ -4017,8 +4022,7 @@ dependencies = [ [[package]] name = "gix-traverse" version = "0.43.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff2ec9f779680f795363db1c563168b32b8d6728ec58564c628e85c92d29faf" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd#520c832cfcfb34eb7617be55ebe2719ab35595fd" dependencies = [ "bitflags 2.6.0", "gix-commitgraph 0.25.1", @@ -4034,12 +4038,12 @@ dependencies = [ [[package]] name = "gix-url" version = "0.28.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e09f97db3618fb8e473d7d97e77296b50aaee0ddcd6a867f07443e3e87391099" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd#520c832cfcfb34eb7617be55ebe2719ab35595fd" dependencies = [ "bstr", "gix-features 0.39.1", - "gix-path", + "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd)", + "percent-encoding", "thiserror 2.0.3", "url", ] @@ -4049,6 +4053,15 @@ name = "gix-utils" version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba427e3e9599508ed98a6ddf8ed05493db114564e338e41f6a996d2e4790335f" +dependencies = [ + "fastrand", + "unicode-normalization", +] + +[[package]] +name = "gix-utils" +version = "0.1.13" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd#520c832cfcfb34eb7617be55ebe2719ab35595fd" dependencies = [ "bstr", "fastrand", @@ -4068,8 +4081,7 @@ dependencies = [ [[package]] name = "gix-validate" version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd520d09f9f585b34b32aba1d0b36ada89ab7fefb54a8ca3fe37fc482a750937" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd#520c832cfcfb34eb7617be55ebe2719ab35595fd" dependencies = [ "bstr", "thiserror 2.0.3", @@ -4090,15 +4102,14 @@ dependencies = [ "gix-ignore 0.11.4", "gix-index 0.33.1", "gix-object 0.42.3", - "gix-path", + "gix-path 0.10.13 (registry+https://github.com/rust-lang/crates.io-index)", "gix-validate 0.8.5", ] [[package]] name = "gix-worktree" version = "0.38.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "756dbbe15188fa22540d5eab941f8f9cf511a5364d5aec34c88083c09f4bea13" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd#520c832cfcfb34eb7617be55ebe2719ab35595fd" dependencies = [ "bstr", "gix-attributes 0.23.1", @@ -4109,15 +4120,14 @@ dependencies = [ "gix-ignore 0.12.1", "gix-index 0.37.0", "gix-object 0.46.0", - "gix-path", + "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd)", "gix-validate 0.9.2", ] [[package]] name = "gix-worktree-state" version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ebd5eead61d37b334bc31810c9980aa72d659044513cae0e342a88fed2c22ba" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd#520c832cfcfb34eb7617be55ebe2719ab35595fd" dependencies = [ "bstr", "gix-features 0.39.1", @@ -4127,7 +4137,7 @@ dependencies = [ "gix-hash 0.15.1", "gix-index 0.37.0", "gix-object 0.46.0", - "gix-path", + "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=520c832cfcfb34eb7617be55ebe2719ab35595fd)", "gix-worktree 0.38.0", "io-close", "thiserror 2.0.3", diff --git a/Cargo.toml b/Cargo.toml index bcd70418f4..3375e2f333 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,7 +43,7 @@ resolver = "2" [workspace.dependencies] bstr = "1.11.0" # Add the `tracing` or `tracing-detail` features to see more of gitoxide in the logs. Useful to see which programs it invokes. -gix = { version = "0.68.0", default-features = false, features = [] } +gix = { git = "https://github.com/GitoxideLabs/gitoxide", rev = "520c832cfcfb34eb7617be55ebe2719ab35595fd", default-features = false, features = [] } git2 = { version = "0.19.0", features = [ "vendored-openssl", "vendored-libgit2", diff --git a/crates/gitbutler-branch-actions/src/base.rs b/crates/gitbutler-branch-actions/src/base.rs index cdb7118c0f..6cd4ea401d 100644 --- a/crates/gitbutler-branch-actions/src/base.rs +++ b/crates/gitbutler-branch-actions/src/base.rs @@ -11,10 +11,10 @@ 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_oxidize::{git2_to_gix_object_id, gix_to_git2_oid, GixRepositoryExt}; use gitbutler_project::FetchResult; use gitbutler_reference::{Refname, RemoteRefname}; -use gitbutler_repo::{GixRepositoryExt, LogUntil, RepositoryExt}; +use gitbutler_repo::{LogUntil, RepositoryExt}; use gitbutler_repo_actions::RepoActionsExt; use gitbutler_stack::{BranchOwnershipClaims, Stack, Target, VirtualBranchesHandle}; use serde::Serialize; diff --git a/crates/gitbutler-branch-actions/src/branch.rs b/crates/gitbutler-branch-actions/src/branch.rs index 0931362ac1..86a435a571 100644 --- a/crates/gitbutler-branch-actions/src/branch.rs +++ b/crates/gitbutler-branch-actions/src/branch.rs @@ -7,11 +7,11 @@ use gitbutler_branch::BranchIdentity; use gitbutler_branch::ReferenceExtGix; use gitbutler_command_context::CommandContext; use gitbutler_diff::DiffByPathMap; -use gitbutler_oxidize::{git2_to_gix_object_id, gix_to_git2_oid}; +use gitbutler_oxidize::{git2_to_gix_object_id, gix_to_git2_oid, GixRepositoryExt}; use gitbutler_project::access::WorktreeReadPermission; use gitbutler_reference::normalize_branch_name; use gitbutler_reference::RemoteRefname; -use gitbutler_repo::{GixRepositoryExt, RepositoryExt as _}; +use gitbutler_repo::RepositoryExt as _; use gitbutler_serde::BStringForFrontend; use gitbutler_stack::{Stack as GitButlerBranch, StackId, Target}; use gix::object::tree::diff::Action; 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 2d6cb5af4a..ab07a8b1d4 100644 --- a/crates/gitbutler-branch-actions/src/branch_manager/branch_creation.rs +++ b/crates/gitbutler-branch-actions/src/branch_manager/branch_creation.rs @@ -1,4 +1,9 @@ +use super::BranchManager; use crate::r#virtual as vbranch; +use crate::{ + conflicts::RepoConflictsExt, hunk::VirtualBranchHunk, integration::update_workspace_commit, + VirtualBranchesExt, +}; use anyhow::{anyhow, bail, Context, Result}; use gitbutler_branch::BranchCreateRequest; use gitbutler_branch::{self, dedup}; @@ -6,9 +11,9 @@ use gitbutler_cherry_pick::RepositoryExt as _; use gitbutler_commit::{commit_ext::CommitExt, commit_headers::HasCommitHeaders}; use gitbutler_error::error::Marker; use gitbutler_oplog::SnapshotExt; +use gitbutler_oxidize::GixRepositoryExt; 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, @@ -19,12 +24,6 @@ use gitbutler_time::time::now_since_unix_epoch_ms; use gitbutler_workspace::checkout_branch_trees; use tracing::instrument; -use super::BranchManager; -use crate::{ - conflicts::RepoConflictsExt, hunk::VirtualBranchHunk, integration::update_workspace_commit, - VirtualBranchesExt, -}; - impl BranchManager<'_> { #[instrument(level = tracing::Level::DEBUG, skip(self, perm), err(Debug))] pub fn create_virtual_branch( 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 011fec8ba7..a84ce79b58 100644 --- a/crates/gitbutler-branch-actions/src/branch_manager/branch_removal.rs +++ b/crates/gitbutler-branch-actions/src/branch_manager/branch_removal.rs @@ -5,11 +5,10 @@ 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_oxidize::{git2_to_gix_object_id, GixRepositoryExt}; 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; diff --git a/crates/gitbutler-branch-actions/src/branch_upstream_integration.rs b/crates/gitbutler-branch-actions/src/branch_upstream_integration.rs index 4f70937fa7..de635be5a4 100644 --- a/crates/gitbutler-branch-actions/src/branch_upstream_integration.rs +++ b/crates/gitbutler-branch-actions/src/branch_upstream_integration.rs @@ -1083,7 +1083,7 @@ mod test { fn hard_reset_to_externally_amended_commit() { let test_repository = TestingRepository::open(); - let base_commit = dbg!(test_repository.commit_tree(None, &[])); + let base_commit = test_repository.commit_tree(None, &[]); let local_a = test_repository.commit_tree_with_message( Some(&base_commit), "A", @@ -1145,7 +1145,7 @@ mod test { fn hard_reset_to_externally_removed_commit() { let test_repository = TestingRepository::open(); - let base_commit = dbg!(test_repository.commit_tree(None, &[])); + let base_commit = test_repository.commit_tree(None, &[]); let local_a = test_repository.commit_tree_with_message( Some(&base_commit), "A", @@ -1212,7 +1212,7 @@ mod test { fn hard_reset_to_externally_amended_branch() { let test_repository = TestingRepository::open(); - let base_commit = dbg!(test_repository.commit_tree(None, &[])); + let base_commit = test_repository.commit_tree(None, &[]); let local_a = test_repository.commit_tree_with_message( Some(&base_commit), "A", diff --git a/crates/gitbutler-branch-actions/src/integration.rs b/crates/gitbutler-branch-actions/src/integration.rs index 5648b907c4..4a590753c1 100644 --- a/crates/gitbutler-branch-actions/src/integration.rs +++ b/crates/gitbutler-branch-actions/src/integration.rs @@ -9,9 +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_oxidize::{git2_to_gix_object_id, gix_to_git2_oid, GixRepositoryExt}; use gitbutler_project::access::WorktreeWritePermission; -use gitbutler_repo::{GixRepositoryExt, SignaturePurpose}; +use gitbutler_repo::SignaturePurpose; use gitbutler_repo::{LogUntil, RepositoryExt}; use gitbutler_stack::{Stack, VirtualBranchesHandle}; use tracing::instrument; diff --git a/crates/gitbutler-branch-actions/src/upstream_integration.rs b/crates/gitbutler-branch-actions/src/upstream_integration.rs index 0378652459..260e57a707 100644 --- a/crates/gitbutler-branch-actions/src/upstream_integration.rs +++ b/crates/gitbutler-branch-actions/src/upstream_integration.rs @@ -4,12 +4,12 @@ use anyhow::{anyhow, bail, Context, 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_oxidize::{git2_to_gix_object_id, gix_to_git2_oid, GixRepositoryExt}; use gitbutler_project::access::WorktreeWritePermission; use gitbutler_repo::RepositoryExt as _; use gitbutler_repo::{ rebase::{cherry_rebase_group, gitbutler_merge_commits}, - GixRepositoryExt, LogUntil, + LogUntil, }; use gitbutler_repo_actions::RepoActionsExt as _; use gitbutler_stack::stack_context::StackContext; diff --git a/crates/gitbutler-branch-actions/src/virtual.rs b/crates/gitbutler-branch-actions/src/virtual.rs index 40e3adbb37..859c47cdc0 100644 --- a/crates/gitbutler-branch-actions/src/virtual.rs +++ b/crates/gitbutler-branch-actions/src/virtual.rs @@ -22,12 +22,14 @@ use gitbutler_diff::{trees, GitHunk, Hunk}; use gitbutler_error::error::Code; use gitbutler_hunk_dependency::RangeCalculationError; use gitbutler_operating_modes::assure_open_workspace_mode; -use gitbutler_oxidize::{git2_signature_to_gix_signature, git2_to_gix_object_id, gix_to_git2_oid}; +use gitbutler_oxidize::{ + git2_signature_to_gix_signature, git2_to_gix_object_id, gix_to_git2_oid, GixRepositoryExt, +}; use gitbutler_project::access::WorktreeWritePermission; use gitbutler_reference::{normalize_branch_name, Refname, RemoteRefname}; use gitbutler_repo::{ rebase::{cherry_rebase, cherry_rebase_group}, - GixRepositoryExt, LogUntil, RepositoryExt, + LogUntil, RepositoryExt, }; use gitbutler_repo_actions::RepoActionsExt; use gitbutler_stack::{ diff --git a/crates/gitbutler-branch-actions/tests/virtual_branches/upstream.rs b/crates/gitbutler-branch-actions/tests/virtual_branches/upstream.rs index a85175979a..162dfdb3df 100644 --- a/crates/gitbutler-branch-actions/tests/virtual_branches/upstream.rs +++ b/crates/gitbutler-branch-actions/tests/virtual_branches/upstream.rs @@ -98,7 +98,9 @@ fn detect_integrated_commits() { .into_iter() .find(|b| b.id == branch1_id) .unwrap(); - repository.merge(&branch.upstream.as_ref().unwrap().name); + repository + .merge(&branch.upstream.as_ref().unwrap().name) + .unwrap(); repository.fetch(); } diff --git a/crates/gitbutler-branch/Cargo.toml b/crates/gitbutler-branch/Cargo.toml index 0d4d26dc37..7f14f08389 100644 --- a/crates/gitbutler-branch/Cargo.toml +++ b/crates/gitbutler-branch/Cargo.toml @@ -4,27 +4,18 @@ version = "0.0.0" edition = "2021" authors = ["GitButler "] publish = false +autotests = false [dependencies] anyhow = "1.0.93" git2.workspace = true gix = { workspace = true, features = [] } gitbutler-reference.workspace = true -gitbutler-serde.workspace = true -gitbutler-id.workspace = true -gitbutler-error.workspace = true -gitbutler-fs.workspace = true gitbutler-diff.workspace = true -gitbutler-oxidize.workspace = true -gitbutler-time.workspace = true gitbutler-stack.workspace = true itertools = "0.13" -toml.workspace = true serde = { workspace = true, features = ["std"] } bstr.workspace = true -md5 = "0.7.0" -hex = "0.4.3" -tracing.workspace = true lazy_static = "1.4.0" [[test]] diff --git a/crates/gitbutler-cherry-pick/Cargo.toml b/crates/gitbutler-cherry-pick/Cargo.toml index 28c52d9ab9..adcc25f41e 100644 --- a/crates/gitbutler-cherry-pick/Cargo.toml +++ b/crates/gitbutler-cherry-pick/Cargo.toml @@ -8,4 +8,6 @@ publish = false [dependencies] gitbutler-commit.workspace = true git2.workspace = true +gitbutler-oxidize.workspace = true +gix.workspace = true anyhow.workspace = true diff --git a/crates/gitbutler-cherry-pick/src/lib.rs b/crates/gitbutler-cherry-pick/src/lib.rs index 9f59056286..a64fcf1592 100644 --- a/crates/gitbutler-cherry-pick/src/lib.rs +++ b/crates/gitbutler-cherry-pick/src/lib.rs @@ -1,14 +1,8 @@ -// tree_writer.insert(".conflict-side-0", side0.id(), 0o040000)?; -// tree_writer.insert(".conflict-side-1", side1.id(), 0o040000)?; -// tree_writer.insert(".conflict-base-0", base_tree.id(), 0o040000)?; -// tree_writer.insert(".auto-resolution", resolved_tree_id, 0o040000)?; -// tree_writer.insert(".conflict-files", conflicted_files_blob, 0o100644)?; - use std::ops::Deref; -use anyhow::Context; -use git2::MergeOptions; +use anyhow::{Context, Result}; use gitbutler_commit::commit_ext::CommitExt; +use gitbutler_oxidize::git2_to_gix_object_id; #[derive(Default)] pub enum ConflictedTreeKey { @@ -40,68 +34,105 @@ impl Deref for ConflictedTreeKey { } pub trait RepositoryExt { - fn cherry_pick_gitbutler( - &self, + /// Find the real tree of a commit, which is the tree of the commit if it's not in a conflicted state + /// or the tree according to `side` if it is conflicted. + /// + /// Unless you want to find a particular side, you likely want to pass Default::default() + /// as the [`side`](ConflictedTreeKey) which will give the automatically resolved resolution + fn find_real_tree(&self, commit: &git2::Commit, side: ConflictedTreeKey) -> Result; +} + +pub trait GixRepositoryExt { + /// Cherry-pick, but understands GitButler conflicted states. + /// Note that it will automatically resolve conflicts in *our* favor, so any tree produced + /// here can be used. + /// + /// This method *should* always be used in favour of native functions. + fn cherry_pick_gitbutler<'repo>( + &'repo self, head: &git2::Commit, to_rebase: &git2::Commit, - merge_options: Option<&MergeOptions>, - ) -> Result; - fn find_real_tree( - &self, - commit: &git2::Commit, + ) -> Result>; + + /// Find the real tree of a commit, which is the tree of the commit if it's not in a conflicted state + /// or the tree according to `side` if it is conflicted. + /// + /// Unless you want to find a particular side, you likely want to pass Default::default() + /// as the [`side`](ConflictedTreeKey) which will give the automatically resolved resolution + fn find_real_tree<'repo>( + &'repo self, + commit_id: &gix::oid, side: ConflictedTreeKey, - ) -> Result; + ) -> Result>; } impl RepositoryExt for git2::Repository { - /// cherry-pick, but understands GitButler conflicted states - /// - /// cherry_pick_gitbutler should always be used in favour of libgit2 or gitoxide - /// cherry pick functions - fn cherry_pick_gitbutler( - &self, + fn find_real_tree(&self, commit: &git2::Commit, side: ConflictedTreeKey) -> Result { + let tree = commit.tree()?; + if commit.is_conflicted() { + let conflicted_side = tree + .get_name(&side) + .context("Failed to get conflicted side of commit")?; + self.find_tree(conflicted_side.id()) + .context("failed to find subtree") + } else { + self.find_tree(tree.id()).context("failed to find subtree") + } + } +} + +impl GixRepositoryExt for gix::Repository { + fn cherry_pick_gitbutler<'repo>( + &'repo self, head: &git2::Commit, to_rebase: &git2::Commit, - merge_options: Option<&MergeOptions>, - ) -> Result { + ) -> Result> { // we need to do a manual 3-way patch merge // find the base, which is the parent of to_rebase let base = if to_rebase.is_conflicted() { // Use to_rebase's recorded base - self.find_real_tree(to_rebase, ConflictedTreeKey::Base)? + self.find_real_tree( + &git2_to_gix_object_id(to_rebase.id()), + ConflictedTreeKey::Base, + )? } else { let base_commit = to_rebase.parent(0)?; // Use the parent's auto-resolution - self.find_real_tree(&base_commit, Default::default())? + self.find_real_tree(&git2_to_gix_object_id(base_commit.id()), Default::default())? }; // Get the auto-resolution - let ours = self.find_real_tree(head, Default::default())?; + let ours = self.find_real_tree(&git2_to_gix_object_id(head.id()), Default::default())?; // Get the original theirs - let thiers = self.find_real_tree(to_rebase, ConflictedTreeKey::Theirs)?; + let theirs = self.find_real_tree( + &git2_to_gix_object_id(to_rebase.id()), + ConflictedTreeKey::Theirs, + )?; - self.merge_trees(&base, &ours, &thiers, merge_options) - .context("failed to merge trees for cherry pick") + use gitbutler_oxidize::GixRepositoryExt; + self.merge_trees( + base, + ours, + theirs, + self.default_merge_labels(), + self.merge_options_force_ours()?, + ) + .context("failed to merge trees for cherry pick") } - /// Find the real tree of a commit, which is the tree of the commit if it's not in a conflicted state - /// or the parent parent tree if it is in a conflicted state - /// - /// Unless you want to find a particular side, you likly want to pass Default::default() - /// as the ConfclitedTreeKey which will give the automatically resolved resolution - fn find_real_tree( - &self, - commit: &git2::Commit, + fn find_real_tree<'repo>( + &'repo self, + commit_id: &gix::oid, side: ConflictedTreeKey, - ) -> Result { - let tree = commit.tree()?; - if commit.is_conflicted() { + ) -> Result> { + let commit = self.find_commit(commit_id)?; + Ok(if commit.is_conflicted() { + let tree = commit.tree()?; let conflicted_side = tree - .get_name(&side) + .find_entry(&*side) .context("Failed to get conflicted side of commit")?; - self.find_tree(conflicted_side.id()) - .context("failed to find subtree") + conflicted_side.id() } else { - self.find_tree(tree.id()).context("failed to find subtree") - } + commit.tree_id()? + }) } } diff --git a/crates/gitbutler-commit/Cargo.toml b/crates/gitbutler-commit/Cargo.toml index fb1de5b991..c6ee344b85 100644 --- a/crates/gitbutler-commit/Cargo.toml +++ b/crates/gitbutler-commit/Cargo.toml @@ -7,5 +7,6 @@ publish = false [dependencies] git2.workspace = true +gix.workspace = true bstr.workspace = true uuid.workspace = true diff --git a/crates/gitbutler-commit/src/commit_ext.rs b/crates/gitbutler-commit/src/commit_ext.rs index 75d0b0ebe2..6bf142a48d 100644 --- a/crates/gitbutler-commit/src/commit_ext.rs +++ b/crates/gitbutler-commit/src/commit_ext.rs @@ -32,6 +32,29 @@ impl CommitExt for git2::Commit<'_> { } } +impl CommitExt for gix::Commit<'_> { + fn message_bstr(&self) -> &BStr { + self.message_raw() + .expect("valid commit that can be parsed: TODO - allow it to return errors?") + } + + fn change_id(&self) -> Option { + self.gitbutler_headers().map(|headers| headers.change_id) + } + + fn is_signed(&self) -> bool { + self.decode().map_or(false, |decoded| { + decoded.extra_headers().pgp_signature().is_some() + }) + } + + fn is_conflicted(&self) -> bool { + self.gitbutler_headers() + .and_then(|headers| headers.conflicted.map(|conflicted| conflicted > 0)) + .unwrap_or(false) + } +} + fn contains<'a, I>(iter: I, item: &git2::Commit<'a>) -> bool where I: IntoIterator>, diff --git a/crates/gitbutler-commit/src/commit_headers.rs b/crates/gitbutler-commit/src/commit_headers.rs index d2a2342a4d..bfa102fadd 100644 --- a/crates/gitbutler-commit/src/commit_headers.rs +++ b/crates/gitbutler-commit/src/commit_headers.rs @@ -1,4 +1,4 @@ -use bstr::{BStr, BString}; +use bstr::{BStr, BString, ByteSlice}; use uuid::Uuid; /// Header used to determine which version of the headers is in use. This should never be changed @@ -113,6 +113,41 @@ impl HasCommitHeaders for git2::Commit<'_> { } } +impl HasCommitHeaders for gix::Commit<'_> { + fn gitbutler_headers(&self) -> Option { + let decoded = self.decode().ok()?; + if let Some(header) = decoded.extra_headers().find(HEADERS_VERSION_HEADER) { + let version_number = header.to_owned(); + + // Parse v2 headers + if version_number == V2_HEADERS_VERSION { + let change_id = decoded.extra_headers().find(V2_CHANGE_ID_HEADER)?; + // We can safely assume that the change id should be UTF8 + let change_id = change_id.to_str().ok()?.to_string(); + + let conflicted = decoded + .extra_headers() + .find(V2_CONFLICTED_HEADER) + .and_then(|value| value.to_str().ok()?.parse::().ok()); + + Some(CommitHeadersV2 { + change_id, + conflicted, + }) + } else { + // Must be for a version we don't recognise + None + } + } else { + // Parse v1 headers + let change_id = decoded.extra_headers().find(V1_CHANGE_ID_HEADER)?; + let change_id = change_id.to_str().ok()?.to_string(); + let headers = CommitHeadersV1 { change_id }; + Some(headers.into()) + } + } +} + /// Lifecycle impl CommitHeadersV2 { /// Used to create a CommitHeadersV2. This does not allow a change_id to be diff --git a/crates/gitbutler-diff/Cargo.toml b/crates/gitbutler-diff/Cargo.toml index 0b5e44bb6c..215ba0324f 100644 --- a/crates/gitbutler-diff/Cargo.toml +++ b/crates/gitbutler-diff/Cargo.toml @@ -4,6 +4,7 @@ version = "0.0.0" edition = "2021" authors = ["GitButler "] publish = false +autotests = false [dependencies] git2.workspace = true diff --git a/crates/gitbutler-edit-mode/Cargo.toml b/crates/gitbutler-edit-mode/Cargo.toml index 1525e51983..1cb654bf77 100644 --- a/crates/gitbutler-edit-mode/Cargo.toml +++ b/crates/gitbutler-edit-mode/Cargo.toml @@ -7,6 +7,7 @@ publish = false [dependencies] git2.workspace = true +gix.workspace = true anyhow.workspace = true bstr.workspace = true gitbutler-branch.workspace = true @@ -16,11 +17,12 @@ gitbutler-command-context.workspace = true gitbutler-operating-modes.workspace = true gitbutler-project.workspace = true gitbutler-branch-actions.workspace = true +gitbutler-oxidize.workspace = true gitbutler-reference.workspace = true -gitbutler-time.workspace = true gitbutler-oplog.workspace = true gitbutler-diff.workspace = true gitbutler-stack.workspace = true gitbutler-cherry-pick.workspace = true gitbutler-workspace.workspace = true serde.workspace = true +tracing.workspace = true \ No newline at end of file diff --git a/crates/gitbutler-edit-mode/src/lib.rs b/crates/gitbutler-edit-mode/src/lib.rs index 2e1f3df884..72eee0caaa 100644 --- a/crates/gitbutler-edit-mode/src/lib.rs +++ b/crates/gitbutler-edit-mode/src/lib.rs @@ -8,7 +8,7 @@ use git2::build::CheckoutBuilder; use gitbutler_branch_actions::internal::list_virtual_branches; use gitbutler_branch_actions::{update_workspace_commit, RemoteBranchFile}; use gitbutler_cherry_pick::{ConflictedTreeKey, RepositoryExt as _}; -use gitbutler_command_context::CommandContext; +use gitbutler_command_context::{gix_repository_for_merging, CommandContext}; use gitbutler_commit::{ commit_ext::CommitExt, commit_headers::{CommitHeadersV2, HasCommitHeaders}, @@ -18,6 +18,7 @@ use gitbutler_operating_modes::{ operating_mode, read_edit_mode_metadata, write_edit_mode_metadata, EditModeMetadata, OperatingMode, EDIT_BRANCH_REF, WORKSPACE_BRANCH_REF, }; +use gitbutler_oxidize::{git2_to_gix_object_id, gix_to_git2_index, GixRepositoryExt}; use gitbutler_project::access::{WorktreeReadPermission, WorktreeWritePermission}; use gitbutler_reference::{ReferenceName, Refname}; use gitbutler_repo::{rebase::cherry_rebase, RepositoryExt}; @@ -28,42 +29,48 @@ use serde::Serialize; pub mod commands; +/// Returns an index of the the tree of `commit` if it is unconflicted, *or* produce a merged tree +/// if `commit` is conflicted. That tree is turned into an index that records the conflicts that occurred +/// during the merge. fn get_commit_index(repository: &git2::Repository, commit: &git2::Commit) -> Result { let commit_tree = commit.tree().context("Failed to get commit's tree")?; // Checkout the commit as unstaged changes if commit.is_conflicted() { let base = commit_tree .get_name(".conflict-base-0") - .context("Failed to get base")?; - let base = repository - .find_tree(base.id()) - .context("Failed to find base tree")?; - // Ours + .context("Failed to get base")? + .id(); let ours = commit_tree .get_name(".conflict-side-0") - .context("Failed to get base")?; - let ours = repository - .find_tree(ours.id()) - .context("Failed to find base tree")?; - // Theirs + .context("Failed to get base")? + .id(); let theirs = commit_tree .get_name(".conflict-side-1") - .context("Failed to get base")?; - let theirs = repository - .find_tree(theirs.id()) - .context("Failed to find base tree")?; - - let index = repository - .merge_trees(&base, &ours, &theirs, None) - .context("Failed to merge trees")?; - - Ok(index) + .context("Failed to get base")? + .id(); + + let gix_repo = gix_repository_for_merging(repository.path())?; + // Merge without favoring a side this time to get a tree containing the actual conflicts. + let mut merge_result = gix_repo.merge_trees( + git2_to_gix_object_id(base), + git2_to_gix_object_id(ours), + git2_to_gix_object_id(theirs), + gix_repo.default_merge_labels(), + gix_repo.tree_merge_options()?, + )?; + let merged_tree_id = merge_result.tree.write()?; + let mut index = gix_repo.index_from_tree(&merged_tree_id)?; + if !merge_result.index_changed_after_applying_conflicts( + &mut index, + gix::merge::tree::TreatAsUnresolved::git(), + gix::merge::tree::apply_index_entries::RemovalMode::Mark, + ) { + tracing::warn!("There must be an issue with conflict-commit creation as re-merging the conflicting trees didn't yield a conflicting index."); + } + gix_to_git2_index(&index) } else { let mut index = git2::Index::new()?; - index - .read_tree(&commit_tree) - .context("Failed to set index tree")?; - + index.read_tree(&commit_tree)?; Ok(index) } } diff --git a/crates/gitbutler-hunk-dependency/Cargo.toml b/crates/gitbutler-hunk-dependency/Cargo.toml index 9b3484a259..252f4b179c 100644 --- a/crates/gitbutler-hunk-dependency/Cargo.toml +++ b/crates/gitbutler-hunk-dependency/Cargo.toml @@ -4,21 +4,17 @@ version = "0.0.0" edition = "2021" authors = ["GitButler "] publish = false +autotests = false [dependencies] anyhow = "1.0.93" git2.workspace = true -gix = { workspace = true, features = [] } gitbutler-diff.workspace = true -gitbutler-reference.workspace = true gitbutler-serde.workspace = true gitbutler-stack.workspace = true gitbutler-id.workspace = true itertools = "0.13" serde = { workspace = true, features = ["std"] } -bstr.workspace = true -tokio.workspace = true -uuid = { workspace = true, features = ["fast-rng"] } [[test]] name = "hunk-dependency" diff --git a/crates/gitbutler-hunk-dependency/src/hunk.rs b/crates/gitbutler-hunk-dependency/src/hunk.rs index 958bc42fe2..75842030ac 100644 --- a/crates/gitbutler-hunk-dependency/src/hunk.rs +++ b/crates/gitbutler-hunk-dependency/src/hunk.rs @@ -100,154 +100,3 @@ impl HunkRange { Ok(self.start > incoming_last_line) } } - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_deleted_file_intersects_everything() { - let range = HunkRange { - change_type: gitbutler_diff::ChangeType::Deleted, - stack_id: StackId::generate(), - commit_id: git2::Oid::from_str("a").unwrap(), - start: 0, - lines: 0, - line_shift: 0, - }; - - assert!(range.intersects(1, 1).unwrap()); - assert!(range.intersects(2, 2).unwrap()); - assert!(range.intersects(1, 1).unwrap()); - assert!(range.intersects(12, 10).unwrap()); - assert!(range.intersects(4, 0).unwrap()); - assert!(range.intersects(0, 0).unwrap()); - } - - #[test] - fn test_hunk_at_the_beginning() { - let range = HunkRange { - change_type: gitbutler_diff::ChangeType::Modified, - stack_id: StackId::generate(), - commit_id: git2::Oid::from_str("a").unwrap(), - start: 1, - lines: 10, - line_shift: 10, - }; - - assert!(range.intersects(1, 1).unwrap()); - assert!(range.intersects(1, 10).unwrap()); - assert!(range.intersects(4, 2).unwrap()); - assert!(range.intersects(10, 20).unwrap()); - assert!(range.intersects(4, 0).unwrap()); - // Adding lines at the beginning of the file. - assert!(!range.intersects(0, 0).unwrap()); - - assert!(!range.intersects(11, 20).unwrap()); - assert!(!range.intersects(30, 1).unwrap()); - } - - #[test] - fn test_hunk_in_the_middle() { - let range = HunkRange { - change_type: gitbutler_diff::ChangeType::Modified, - stack_id: StackId::generate(), - commit_id: git2::Oid::from_str("a").unwrap(), - start: 10, - lines: 10, - line_shift: 0, - }; - - assert!(range.intersects(1, 10).unwrap()); - assert!(range.intersects(1, 20).unwrap()); - assert!(range.intersects(1, 30).unwrap()); - assert!(range.intersects(4, 10).unwrap()); - assert!(range.intersects(19, 0).unwrap()); - assert!(range.intersects(10, 0).unwrap()); - assert!(range.intersects(10, 10).unwrap()); - assert!(range.intersects(10, 20).unwrap()); - assert!(range.intersects(11, 20).unwrap()); - assert!(range.intersects(15, 1).unwrap()); - - // Adding lines at the beginning of the file. - assert!(!range.intersects(0, 0).unwrap()); - - assert!(!range.intersects(20, 0).unwrap()); - assert!(!range.intersects(1, 1).unwrap()); - assert!(!range.intersects(1, 9).unwrap()); - assert!(!range.intersects(20, 1).unwrap()); - assert!(!range.intersects(30, 1).unwrap()); - } - - #[test] - fn test_is_covered_by() { - let range = HunkRange { - change_type: gitbutler_diff::ChangeType::Modified, - stack_id: StackId::generate(), - commit_id: git2::Oid::from_str("a").unwrap(), - start: 10, - lines: 10, - line_shift: 0, - }; - - assert!(range.covered_by(1, 20)); - assert!(range.covered_by(1, 30)); - assert!(range.covered_by(4, 16)); - assert!(range.covered_by(10, 20)); - // Adding lines at the beginning of the file. - assert!(!range.covered_by(0, 0)); - - assert!(!range.covered_by(10, 9)); - assert!(!range.covered_by(11, 20)); - assert!(!range.covered_by(15, 1)); - assert!(!range.covered_by(1, 1)); - assert!(!range.covered_by(1, 18)); - assert!(!range.covered_by(20, 1)); - assert!(!range.covered_by(30, 10)); - } - - #[test] - fn test_contains() { - let range = HunkRange { - change_type: gitbutler_diff::ChangeType::Modified, - stack_id: StackId::generate(), - commit_id: git2::Oid::from_str("a").unwrap(), - start: 10, - lines: 10, - line_shift: 0, - }; - - assert!(!range.contains(0, 0)); - assert!(!range.contains(1, 20)); - assert!(!range.contains(1, 30)); - assert!(!range.contains(4, 16)); - assert!(!range.contains(10, 20)); - assert!(!range.contains(10, 10)); - assert!(!range.contains(19, 0)); - assert!(range.contains(11, 8)); - assert!(range.contains(11, 9)); - assert!(range.contains(10, 0)); - assert!(range.contains(18, 0)); - } - - #[test] - fn test_follows() { - let range = HunkRange { - change_type: gitbutler_diff::ChangeType::Modified, - stack_id: StackId::generate(), - commit_id: git2::Oid::from_str("a").unwrap(), - start: 10, - lines: 10, - line_shift: 0, - }; - - assert!(range.follows(0, 0).unwrap()); - assert!(range.follows(1, 9).unwrap()); - assert!(range.follows(9, 1).unwrap()); - assert!(!range.follows(10, 0).unwrap()); - assert!(!range.follows(11, 0).unwrap()); - assert!(!range.follows(10, 1).unwrap()); - assert!(!range.follows(11, 1).unwrap()); - assert!(!range.follows(20, 1).unwrap()); - } -} diff --git a/crates/gitbutler-hunk-dependency/src/input.rs b/crates/gitbutler-hunk-dependency/src/input.rs index 0f12f6b48e..2915253f4c 100644 --- a/crates/gitbutler-hunk-dependency/src/input.rs +++ b/crates/gitbutler-hunk-dependency/src/input.rs @@ -90,53 +90,3 @@ fn parse_header(hunk_info: &str) -> (u32, u32) { }; (start, lines) } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn diff_simple() -> anyhow::Result<()> { - let header = parse_diff_from_string( - "@@ -1,6 +1,7 @@ -1 -2 -3 -+4 -5 -6 -7 -", - gitbutler_diff::ChangeType::Modified, - )?; - assert_eq!(header.old_start, 4); - assert_eq!(header.old_lines, 0); - assert_eq!(header.new_start, 4); - assert_eq!(header.new_lines, 1); - assert_eq!(header.net_lines()?, 1); - Ok(()) - } - - #[test] - fn diff_complex() -> anyhow::Result<()> { - let header = parse_diff_from_string( - "@@ -5,7 +5,6 @@ -5 -6 -7 --8 --9 -+a -10 -11 -", - gitbutler_diff::ChangeType::Modified, - )?; - assert_eq!(header.old_start, 8); - assert_eq!(header.old_lines, 2); - assert_eq!(header.new_start, 8); - assert_eq!(header.new_lines, 1); - assert_eq!(header.net_lines()?, -1); - Ok(()) - } -} diff --git a/crates/gitbutler-hunk-dependency/src/path.rs b/crates/gitbutler-hunk-dependency/src/path.rs index 78652e19ed..57219793d1 100644 --- a/crates/gitbutler-hunk-dependency/src/path.rs +++ b/crates/gitbutler-hunk-dependency/src/path.rs @@ -835,7 +835,6 @@ fn insert_hunk_ranges( } #[cfg(test)] - mod tests { use super::*; diff --git a/crates/gitbutler-hunk-dependency/tests/hunk.rs b/crates/gitbutler-hunk-dependency/tests/hunk.rs new file mode 100644 index 0000000000..99da092296 --- /dev/null +++ b/crates/gitbutler-hunk-dependency/tests/hunk.rs @@ -0,0 +1,148 @@ +use gitbutler_hunk_dependency::HunkRange; +use gitbutler_stack::StackId; + +#[test] +fn test_deleted_file_intersects_everything() { + let range = HunkRange { + change_type: gitbutler_diff::ChangeType::Deleted, + stack_id: StackId::generate(), + commit_id: git2::Oid::from_str("a").unwrap(), + start: 0, + lines: 0, + line_shift: 0, + }; + + assert!(range.intersects(1, 1).unwrap()); + assert!(range.intersects(2, 2).unwrap()); + assert!(range.intersects(1, 1).unwrap()); + assert!(range.intersects(12, 10).unwrap()); + assert!(range.intersects(4, 0).unwrap()); + assert!(range.intersects(0, 0).unwrap()); +} + +#[test] +fn test_hunk_at_the_beginning() { + let range = HunkRange { + change_type: gitbutler_diff::ChangeType::Modified, + stack_id: StackId::generate(), + commit_id: git2::Oid::from_str("a").unwrap(), + start: 1, + lines: 10, + line_shift: 0, + }; + + assert!(range.intersects(1, 1).unwrap()); + assert!(range.intersects(1, 10).unwrap()); + assert!(range.intersects(4, 2).unwrap()); + assert!(range.intersects(10, 20).unwrap()); + assert!(range.intersects(4, 0).unwrap()); + // Adding lines at the beginning of the file. + assert!(!range.intersects(0, 0).unwrap()); + + assert!(!range.intersects(11, 20).unwrap()); + assert!(!range.intersects(30, 1).unwrap()); +} + +#[test] +fn test_hunk_in_the_middle() { + let range = HunkRange { + change_type: gitbutler_diff::ChangeType::Modified, + stack_id: StackId::generate(), + commit_id: git2::Oid::from_str("a").unwrap(), + start: 10, + lines: 10, + line_shift: 0, + }; + + assert!(range.intersects(1, 10).unwrap()); + assert!(range.intersects(1, 20).unwrap()); + assert!(range.intersects(1, 30).unwrap()); + assert!(range.intersects(4, 10).unwrap()); + assert!(range.intersects(19, 0).unwrap()); + assert!(range.intersects(10, 0).unwrap()); + assert!(range.intersects(10, 10).unwrap()); + assert!(range.intersects(10, 20).unwrap()); + assert!(range.intersects(11, 20).unwrap()); + assert!(range.intersects(15, 1).unwrap()); + + // Adding lines at the beginning of the file. + assert!(!range.intersects(0, 0).unwrap()); + + assert!(!range.intersects(20, 0).unwrap()); + assert!(!range.intersects(1, 1).unwrap()); + assert!(!range.intersects(1, 9).unwrap()); + assert!(!range.intersects(20, 1).unwrap()); + assert!(!range.intersects(30, 1).unwrap()); +} + +#[test] +fn test_is_covered_by() { + let range = HunkRange { + change_type: gitbutler_diff::ChangeType::Modified, + stack_id: StackId::generate(), + commit_id: git2::Oid::from_str("a").unwrap(), + start: 10, + lines: 10, + line_shift: 0, + }; + + assert!(range.covered_by(1, 20)); + assert!(range.covered_by(1, 30)); + assert!(range.covered_by(4, 16)); + assert!(range.covered_by(10, 20)); + // Adding lines at the beginning of the file. + assert!(!range.covered_by(0, 0)); + + assert!(!range.covered_by(10, 9)); + assert!(!range.covered_by(11, 20)); + assert!(!range.covered_by(15, 1)); + assert!(!range.covered_by(1, 1)); + assert!(!range.covered_by(1, 18)); + assert!(!range.covered_by(20, 1)); + assert!(!range.covered_by(30, 10)); +} + +#[test] +fn test_contains() { + let range = HunkRange { + change_type: gitbutler_diff::ChangeType::Modified, + stack_id: StackId::generate(), + commit_id: git2::Oid::from_str("a").unwrap(), + start: 10, + lines: 10, + line_shift: 0, + }; + + assert!(!range.contains(0, 0)); + assert!(!range.contains(1, 20)); + assert!(!range.contains(1, 30)); + assert!(!range.contains(4, 16)); + assert!(!range.contains(10, 20)); + assert!(!range.contains(10, 10)); + assert!(!range.contains(19, 0)); + assert!(range.contains(11, 8)); + assert!(range.contains(11, 9)); + assert!(range.contains(10, 0)); + assert!(range.contains(18, 0)); +} + +#[test] +fn test_follows() { + let range = HunkRange { + change_type: gitbutler_diff::ChangeType::Modified, + stack_id: StackId::generate(), + commit_id: git2::Oid::from_str("a").unwrap(), + start: 10, + lines: 10, + line_shift: 0, + }; + + assert!(range.follows(0, 0).unwrap()); + assert!(range.follows(1, 9).unwrap()); + assert!(range.follows(9, 1).unwrap()); + assert!(!range.follows(10, 0).unwrap()); + assert!(!range.follows(11, 0).unwrap()); + assert!(!range.follows(10, 1).unwrap()); + assert!(!range.follows(11, 1).unwrap()); + assert!(!range.follows(20, 1).unwrap()); +} diff --git a/crates/gitbutler-hunk-dependency/tests/input.rs b/crates/gitbutler-hunk-dependency/tests/input.rs new file mode 100644 index 0000000000..172f684620 --- /dev/null +++ b/crates/gitbutler-hunk-dependency/tests/input.rs @@ -0,0 +1,46 @@ +use gitbutler_hunk_dependency::parse_diff_from_string; + +#[test] +fn diff_simple() -> anyhow::Result<()> { + let header = parse_diff_from_string( + "@@ -1,6 +1,7 @@ +1 +2 +3 ++4 +5 +6 +7 +", + gitbutler_diff::ChangeType::Modified, + )?; + assert_eq!(header.old_start, 4); + assert_eq!(header.old_lines, 0); + assert_eq!(header.new_start, 4); + assert_eq!(header.new_lines, 1); + assert_eq!(header.net_lines()?, 1); + Ok(()) +} + +#[test] +fn diff_complex() -> anyhow::Result<()> { + let header = parse_diff_from_string( + "@@ -5,7 +5,6 @@ +5 +6 +7 +-8 +-9 ++a +10 +11 +", + gitbutler_diff::ChangeType::Modified, + )?; + assert_eq!(header.old_start, 8); + assert_eq!(header.old_lines, 2); + assert_eq!(header.new_start, 8); + assert_eq!(header.new_lines, 1); + assert_eq!(header.net_lines()?, -1); + Ok(()) +} diff --git a/crates/gitbutler-hunk-dependency/tests/mod.rs b/crates/gitbutler-hunk-dependency/tests/mod.rs index 4da9789237..5a7bc86f88 100644 --- a/crates/gitbutler-hunk-dependency/tests/mod.rs +++ b/crates/gitbutler-hunk-dependency/tests/mod.rs @@ -1 +1,3 @@ -pub mod path; +mod hunk; +mod input; +mod path; diff --git a/crates/gitbutler-oplog/Cargo.toml b/crates/gitbutler-oplog/Cargo.toml index 6602e7eb5e..fad4d6ec9b 100644 --- a/crates/gitbutler-oplog/Cargo.toml +++ b/crates/gitbutler-oplog/Cargo.toml @@ -4,6 +4,7 @@ version = "0.0.0" edition = "2021" authors = ["GitButler "] publish = false +autotests = false [dependencies] anyhow = "1.0.93" diff --git a/crates/gitbutler-oplog/src/oplog.rs b/crates/gitbutler-oplog/src/oplog.rs index 3774ba4edc..c908979e11 100644 --- a/crates/gitbutler-oplog/src/oplog.rs +++ b/crates/gitbutler-oplog/src/oplog.rs @@ -15,13 +15,15 @@ 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_oxidize::{ + git2_to_gix_object_id, gix_time_to_git2, gix_to_git2_oid, GixRepositoryExt, +}; 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; diff --git a/crates/gitbutler-oxidize/src/ext.rs b/crates/gitbutler-oxidize/src/ext.rs new file mode 100644 index 0000000000..637277c51a --- /dev/null +++ b/crates/gitbutler-oxidize/src/ext.rs @@ -0,0 +1,123 @@ +use crate::git2_to_gix_object_id; +use anyhow::{Context, Result}; +use gix::merge::tree::{Options, TreatAsUnresolved}; + +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 label 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()), + } + } + + /// Tree merge options that enforce undecidable conflicts to be forcefully resolved + /// to favor ours, both when dealing with content merges and with tree merges. + fn merge_options_force_ours(&self) -> Result; + + /// 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::TreatAsUnresolved, + )>; + + /// Just like [`Self::merge_options_fail_fast()`], but additionally don't perform rename tracking. + /// This is useful if the merge result isn't going to be used, and we are only interested in knowing + /// if a merge would succeed. + fn merge_options_no_rewrites_fail_fast( + &self, + ) -> Result<(gix::merge::tree::Options, TreatAsUnresolved)>; +} + +impl GixRepositoryExt for gix::Repository { + fn for_tree_diffing(mut self) -> anyhow::Result { + let bytes = self.compute_object_cache_size_for_tree_diffs(&***self.index_or_empty()?); + 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_no_rewrites_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_force_ours(&self) -> Result { + Ok(self + .tree_merge_options()? + .with_tree_favor(Some(gix::merge::tree::TreeFavor::Ours)) + .with_file_favor(Some(gix::merge::tree::FileFavor::Ours))) + } + + fn merge_options_fail_fast(&self) -> Result<(gix::merge::tree::Options, TreatAsUnresolved)> { + let conflict_kind = TreatAsUnresolved::forced_resolution(); + let options = self + .tree_merge_options()? + .with_fail_on_conflict(Some(conflict_kind)); + Ok((options, conflict_kind)) + } + + fn merge_options_no_rewrites_fail_fast( + &self, + ) -> Result<(gix::merge::tree::Options, TreatAsUnresolved)> { + let (options, conflict_kind) = self.merge_options_fail_fast()?; + Ok((options.with_rewrites(None), conflict_kind)) + } +} diff --git a/crates/gitbutler-oxidize/src/lib.rs b/crates/gitbutler-oxidize/src/lib.rs index bc28bd15eb..0aa7ad7b49 100644 --- a/crates/gitbutler-oxidize/src/lib.rs +++ b/crates/gitbutler-oxidize/src/lib.rs @@ -4,6 +4,9 @@ use anyhow::Context; use gix::bstr::ByteSlice; use std::borrow::Borrow; +mod ext; +pub use ext::GixRepositoryExt; + pub fn gix_time_to_git2(time: gix::date::Time) -> git2::Time { git2::Time::new(time.seconds, time.offset) } @@ -50,3 +53,51 @@ pub fn gix_to_git2_signature( &time, )?) } + +/// Convert a `gix` index into a `git2` one, while skipping over entries that are marked for removal. +/// +/// Note that this is quite inefficient as it will have to re-allocate all paths. +/// +/// ## Note +/// +/// * Flags aren't fully supported right now, they are truncated, but good enough to get the *stage* right. +pub fn gix_to_git2_index(index: &gix::index::State) -> anyhow::Result { + let mut out = git2::Index::new()?; + for entry @ gix::index::Entry { + stat: + gix::index::entry::Stat { + mtime, + ctime, + dev, + ino, + uid, + gid, + size, + }, + id, + flags, + mode, + .. + } in index.entries() + { + if flags.contains(gix::index::entry::Flags::REMOVE) { + continue; + } + let git2_entry = git2::IndexEntry { + ctime: git2::IndexTime::new(ctime.secs as i32, ctime.nsecs), + mtime: git2::IndexTime::new(mtime.secs as i32, mtime.nsecs), + dev: *dev, + ino: *ino, + mode: mode.bits(), + uid: *uid, + gid: *gid, + file_size: *size, + id: gix_to_git2_oid(*id), + flags: flags.bits() as u16, + flags_extended: 0, + path: entry.path(index).to_owned().into(), + }; + out.add(&git2_entry)? + } + Ok(out) +} diff --git a/crates/gitbutler-repo/Cargo.toml b/crates/gitbutler-repo/Cargo.toml index 7ba98b4acc..3ae71be7d7 100644 --- a/crates/gitbutler-repo/Cargo.toml +++ b/crates/gitbutler-repo/Cargo.toml @@ -4,6 +4,7 @@ version = "0.0.0" edition = "2021" authors = ["GitButler "] publish = false +autotests = false [dependencies] git2.workspace = true @@ -18,7 +19,6 @@ resolve-path = "0.1.0" gitbutler-command-context.workspace = true gitbutler-config.workspace = true gitbutler-project.workspace = true -# gitbutler-branch.workspace = true gitbutler-reference.workspace = true gitbutler-error.workspace = true gitbutler-commit.workspace = true @@ -38,5 +38,4 @@ path = "tests/mod.rs" [dev-dependencies] gitbutler-testsupport.workspace = true gitbutler-user.workspace = true -gitbutler-git = { workspace = true, features = ["test-askpass-path"] } serde_json = { version = "1.0", features = ["std", "arbitrary_precision"] } diff --git a/crates/gitbutler-repo/src/lib.rs b/crates/gitbutler-repo/src/lib.rs index f0662deb91..95fbd0fd6e 100644 --- a/crates/gitbutler-repo/src/lib.rs +++ b/crates/gitbutler-repo/src/lib.rs @@ -5,7 +5,7 @@ pub use commands::{FileInfo, RepoCommands}; pub use remote::GitRemote; mod repository_ext; -pub use repository_ext::{GixRepositoryExt, LogUntil, RepositoryExt}; +pub use repository_ext::{LogUntil, RepositoryExt}; pub mod credentials; diff --git a/crates/gitbutler-repo/src/rebase.rs b/crates/gitbutler-repo/src/rebase.rs index 9b5e432949..03a3dd6207 100644 --- a/crates/gitbutler-repo/src/rebase.rs +++ b/crates/gitbutler-repo/src/rebase.rs @@ -1,20 +1,17 @@ -use std::{ - collections::HashSet, - path::{Path, PathBuf}, -}; +use std::{collections::HashSet, path::PathBuf}; +use crate::{LogUntil, RepositoryExt as _}; use anyhow::{Context, Result}; use bstr::ByteSlice; -use gitbutler_cherry_pick::{ConflictedTreeKey, RepositoryExt}; -use gitbutler_command_context::CommandContext; +use gitbutler_cherry_pick::{ConflictedTreeKey, GixRepositoryExt, RepositoryExt}; +use gitbutler_command_context::{gix_repository_for_merging, CommandContext}; use gitbutler_commit::{ commit_ext::CommitExt, commit_headers::{CommitHeadersV2, HasCommitHeaders}, }; +use gitbutler_oxidize::{git2_to_gix_object_id, gix_to_git2_oid, GixRepositoryExt as _}; use serde::{Deserialize, Serialize}; -use crate::{LogUntil, RepositoryExt as _}; - /// cherry-pick based rebase, which handles empty commits /// this function takes a commit range and generates a Vector of commit oids /// and then passes them to `cherry_rebase_group` to rebase them onto the target commit @@ -59,7 +56,8 @@ pub fn cherry_rebase_group( .rev() .collect::, _>>() .context("failed to read commits to rebase")?; - + let gix_repo = gix_repository_for_merging(repository.path())?; + let conflict_kind = gix::merge::tree::TreatAsUnresolved::forced_resolution(); let new_head_id = commits_to_rebase .into_iter() .fold( @@ -76,19 +74,26 @@ pub fn cherry_rebase_group( return Ok(to_rebase); }; - let mut cherrypick_index = repository - .cherry_pick_gitbutler(&head, &to_rebase, None) + let mut cherrypick_result = gix_repo + .cherry_pick_gitbutler(&head, &to_rebase) .context("failed to cherry pick")?; - if cherrypick_index.has_conflicts() { + let tree_id = cherrypick_result.tree.write()?; + if cherrypick_result.has_unresolved_conflicts(conflict_kind) { commit_conflicted_cherry_result( repository, head, to_rebase, - &mut cherrypick_index, + cherrypick_result, + conflict_kind, ) } else { - commit_unconflicted_cherry_result(repository, head, to_rebase, cherrypick_index) + commit_unconflicted_cherry_result( + repository, + head, + to_rebase, + gix_to_git2_oid(tree_id), + ) } }, )? @@ -101,19 +106,15 @@ fn commit_unconflicted_cherry_result<'repository>( repository: &'repository git2::Repository, head: git2::Commit<'repository>, to_rebase: git2::Commit, - mut cherrypick_index: git2::Index, + merge_tree_id: git2::Oid, ) -> Result> { - let merge_tree_oid = cherrypick_index - .write_tree_to(repository) - .context("failed to write merge tree")?; - // Remove empty commits - if merge_tree_oid == head.tree_id() { + if merge_tree_id == head.tree_id() { return Ok(head); } let merge_tree = repository - .find_tree(merge_tree_oid) + .find_tree(merge_tree_id) .context("failed to find merge tree")?; // Set conflicted header to None @@ -147,7 +148,8 @@ fn commit_conflicted_cherry_result<'repository>( repository: &'repository git2::Repository, head: git2::Commit, to_rebase: git2::Commit, - cherrypick_index: &mut git2::Index, + mut cherry_pick_result: gix::merge::tree::Outcome<'_>, + treat_as_unresolved: gix::merge::tree::TreatAsUnresolved, ) -> Result> { let commit_headers = to_rebase.gitbutler_headers(); @@ -164,9 +166,9 @@ fn commit_conflicted_cherry_result<'repository>( b"You have checked out a GitButler Conflicted commit. You probably didn't mean to do this."; let readme_blob = repository.blob(readme_content)?; - let conflicted_files = resolve_index(repository, cherrypick_index)?; - - let resolved_tree_id = cherrypick_index.write_tree_to(repository)?; + let resolved_tree_id = cherry_pick_result.tree.write()?; + let conflicted_files = + extract_conflicted_files(resolved_tree_id, cherry_pick_result, treat_as_unresolved)?; // convert files into a string and save as a blob let conflicted_files_string = toml::to_string(&conflicted_files)?; @@ -184,7 +186,7 @@ fn commit_conflicted_cherry_result<'repository>( tree_writer.insert(&*ConflictedTreeKey::Base, base_tree.id(), 0o040000)?; tree_writer.insert( &*ConflictedTreeKey::AutoResolution, - resolved_tree_id, + gix_to_git2_oid(resolved_tree_id), 0o040000, )?; tree_writer.insert( @@ -199,9 +201,9 @@ fn commit_conflicted_cherry_result<'repository>( let commit_headers = commit_headers .or_else(|| Some(Default::default())) - .map(|commit_headers| CommitHeadersV2 { - conflicted: Some(conflicted_files.total_entries() as u64), - ..commit_headers + .map(|mut commit_headers| { + commit_headers.conflicted = conflicted_files.to_headers().conflicted; + commit_headers }); let (_, committer) = repository.signatures()?; @@ -225,11 +227,77 @@ fn commit_conflicted_cherry_result<'repository>( .context("failed to find commit") } +fn extract_conflicted_files( + merged_tree_id: gix::Id<'_>, + merge_result: gix::merge::tree::Outcome<'_>, + treat_as_unresolved: gix::merge::tree::TreatAsUnresolved, +) -> Result { + use gix::index::entry::Stage; + let repo = merged_tree_id.repo; + let mut index = repo.index_from_tree(&merged_tree_id)?; + merge_result.index_changed_after_applying_conflicts( + &mut index, + treat_as_unresolved, + gix::merge::tree::apply_index_entries::RemovalMode::Mark, + ); + let (mut ancestor_entries, mut our_entries, mut their_entries) = + (Vec::new(), Vec::new(), Vec::new()); + for entry in index.entries() { + let stage = entry.stage(); + let storage = match stage { + Stage::Unconflicted => { + continue; + } + Stage::Base => &mut ancestor_entries, + Stage::Ours => &mut our_entries, + Stage::Theirs => &mut their_entries, + }; + + let path = entry.path(&index); + storage.push(gix::path::from_bstr(path).into_owned()); + } + let mut out = ConflictEntries { + ancestor_entries, + our_entries, + their_entries, + }; + + // Since we typically auto-resolve with 'ours', it maybe that conflicting entries don't have an + // unconflicting counterpart anymore, so they are not applied (which is also what Git does). + // So to have something to show for - we *must* produce a conflict, extract paths manually. + // TODO(ST): instead of doing this, don't pre-record the paths. Instead redo the merge without + // merge-strategy so that the index entries can be used instead. + if !out.has_entries() { + fn push_unique(v: &mut Vec, change: &gix::diff::tree_with_rewrites::Change) { + let path = gix::path::from_bstr(change.location()).into_owned(); + if !v.contains(&path) { + v.push(path); + } + } + for conflict in merge_result + .conflicts + .iter() + .filter(|c| c.is_unresolved(treat_as_unresolved)) + { + let (ours, theirs) = conflict.changes_in_resolution(); + push_unique(&mut out.our_entries, ours); + push_unique(&mut out.their_entries, theirs); + } + } + assert_eq!( + out.has_entries(), + merge_result.has_unresolved_conflicts(treat_as_unresolved), + "Must have entries to indicate conflicting files, or bad things will happen later: {:#?}", + merge_result.conflicts + ); + Ok(out) +} + /// Merge two commits together /// /// The `target_commit` and `incoming_commit` must have a common ancestor. /// -/// If there is a merge conflict, the +/// If there is a merge conflict, we will **auto-resolve** to favor *our* side, the `incoming_commit`. pub fn gitbutler_merge_commits<'repository>( repository: &'repository git2::Repository, target_commit: git2::Commit<'repository>, @@ -248,17 +316,21 @@ pub fn gitbutler_merge_commits<'repository>( let target_merge_tree = repository.find_real_tree(&target_commit, Default::default())?; let incoming_merge_tree = repository.find_real_tree(&incoming_commit, Default::default())?; - let mut merged_index = - repository.merge_trees(&base_tree, &incoming_merge_tree, &target_merge_tree, None)?; + let gix_repo = gix_repository_for_merging(repository.path())?; + let mut merge_result = gix_repo.merge_trees( + git2_to_gix_object_id(base_tree.id()), + git2_to_gix_object_id(incoming_merge_tree.id()), + git2_to_gix_object_id(target_merge_tree.id()), + gix_repo.default_merge_labels(), + gix_repo.merge_options_force_ours()?, + )?; + let merged_tree_id = merge_result.tree.write()?; let tree_oid; - let conflicted_files; - - if merged_index.has_conflicts() { - conflicted_files = resolve_index(repository, &mut merged_index)?; - - // Index gets resolved from the `resolve_index` call above, so we can safly write it out - let resolved_tree_id = merged_index.write_tree_to(repository)?; + let forced_resolution = gix::merge::tree::TreatAsUnresolved::forced_resolution(); + let commit_headers = if merge_result.has_unresolved_conflicts(forced_resolution) { + let conflicted_files = + extract_conflicted_files(merged_tree_id, merge_result, forced_resolution)?; // convert files into a string and save as a blob let conflicted_files_string = toml::to_string(&conflicted_files)?; @@ -273,7 +345,7 @@ pub fn gitbutler_merge_commits<'repository>( tree_writer.insert(&*ConflictedTreeKey::Base, base_tree.id(), 0o040000)?; tree_writer.insert( &*ConflictedTreeKey::AutoResolution, - resolved_tree_id, + gix_to_git2_oid(merged_tree_id), 0o040000, )?; tree_writer.insert( @@ -284,32 +356,18 @@ pub fn gitbutler_merge_commits<'repository>( // in case someone checks this out with vanilla Git, we should warn why it looks like this let readme_content = - b"You have checked out a GitButler Conflicted commit. You probably didn't mean to do this."; + b"You have checked out a GitButler Conflicted commit. You probably didn't mean to do this."; let readme_blob = repository.blob(readme_content)?; tree_writer.insert("README.txt", readme_blob, 0o100644)?; tree_oid = tree_writer.write().context("failed to write tree")?; + conflicted_files.to_headers() } else { - conflicted_files = Default::default(); - tree_oid = merged_index.write_tree_to(repository)?; - } - - let conflicted_file_count = conflicted_files.total_entries() as u64; - - let commit_headers = if conflicted_file_count > 0 { - CommitHeadersV2 { - conflicted: Some(conflicted_file_count), - ..Default::default() - } - } else { - CommitHeadersV2 { - conflicted: None, - ..Default::default() - } + tree_oid = gix_to_git2_oid(merged_tree_id); + CommitHeadersV2::default() }; let (author, committer) = repository.signatures()?; - let commit_oid = crate::RepositoryExt::commit_with_signature( repository, None, @@ -355,621 +413,23 @@ impl ConflictEntries { set.len() } -} - -/// Automatically resolves an index with a preferences for the "our" side -/// -/// Within our rebasing and merging logic, "their" is the commit that is getting -/// cherry picked, and "our" is the commit that it is getting cherry picked on -/// to. -/// -/// This means that if we experience a conflict, we drop the changes that are -/// in the commit that is getting cherry picked in favor of what came before it -fn resolve_index( - repository: &git2::Repository, - index: &mut git2::Index, -) -> Result { - fn bytes_to_path(path: &[u8]) -> Result { - let path = std::str::from_utf8(path)?; - Ok(Path::new(path).to_owned()) - } - - let mut ancestor_entries = vec![]; - let mut our_entries = vec![]; - let mut their_entries = vec![]; - - // Set the index on an in-memory repository - let in_memory_repository = repository.in_memory_repo()?; - in_memory_repository.set_index(index)?; - - let index_conflicts = index.conflicts()?.flatten().collect::>(); - - for mut conflict in index_conflicts { - // There may be a case when there is an ancestor in the index without - // a "their" OR "our" side. This is probably caused by the same file - // getting renamed and modified in the two commits. - if let Some(ancestor) = &conflict.ancestor { - let path = bytes_to_path(&ancestor.path)?; - index.remove_path(&path)?; - - ancestor_entries.push(path); - } - - if let (Some(their), None) = (&conflict.their, &conflict.our) { - // Their (the commit we're rebasing)'s change gets dropped - let their_path = bytes_to_path(&their.path)?; - index.remove_path(&their_path)?; - - their_entries.push(their_path); - } else if let (None, Some(our)) = (&conflict.their, &mut conflict.our) { - // Our (the commit we're rebasing onto)'s gets kept - let blob = repository.find_blob(our.id)?; - our.flags = 0; // For some unknown reason we need to set flags to 0 - index.add_frombuffer(our, blob.content())?; - - let our_path = bytes_to_path(&our.path)?; - - our_entries.push(our_path); - } else if let (Some(their), Some(our)) = (&conflict.their, &mut conflict.our) { - // We keep our (the commit we're rebasing onto)'s side of the - // conflict - let their_path = bytes_to_path(&their.path)?; - let blob = repository.find_blob(our.id)?; - - index.remove_path(&their_path)?; - our.flags = 0; // For some unknown reason we need to set flags to 0 - index.add_frombuffer(our, blob.content())?; - - let our_path = bytes_to_path(&our.path)?; - - their_entries.push(their_path); - our_entries.push(our_path); - } - } - - Ok(ConflictEntries { - ancestor_entries, - our_entries, - their_entries, - }) -} - -#[cfg(test)] -mod test { - #[cfg(test)] - mod cherry_rebase_group { - use crate::repository_ext::RepositoryExt as _; - use gitbutler_commit::commit_ext::CommitExt; - use gitbutler_testsupport::testing_repository::{ - assert_commit_tree_matches, TestingRepository, - }; - - use crate::{rebase::cherry_rebase_group, LogUntil}; - - #[test] - fn unconflicting_rebase() { - let test_repository = TestingRepository::open(); - - // Make some commits - let a = test_repository.commit_tree(None, &[("foo.txt", "a"), ("bar.txt", "a")]); - let b = test_repository.commit_tree(Some(&a), &[("foo.txt", "b"), ("bar.txt", "a")]); - let c = test_repository.commit_tree(Some(&b), &[("foo.txt", "c"), ("bar.txt", "a")]); - let d = test_repository.commit_tree(Some(&a), &[("foo.txt", "a"), ("bar.txt", "x")]); - - let result = cherry_rebase_group( - &test_repository.repository, - d.id(), - &[c.id(), b.id()], - false, - ) - .unwrap(); - - let commit: git2::Commit = test_repository.repository.find_commit(result).unwrap(); - - let commits: Vec = test_repository - .repository - .log(commit.id(), LogUntil::End, false) - .unwrap(); - - assert!(commits.into_iter().all(|commit| !commit.is_conflicted())); - - assert_commit_tree_matches( - &test_repository.repository, - &commit, - &[("foo.txt", b"c"), ("bar.txt", b"x")], - ); - } - - #[test] - fn single_commit_ends_up_conflicted() { - let test_repository = TestingRepository::open(); - - let a = test_repository.commit_tree(None, &[("foo.txt", "a")]); - let b = test_repository.commit_tree(Some(&a), &[("foo.txt", "b")]); - let c = test_repository.commit_tree(Some(&a), &[("foo.txt", "c")]); - - // Rebase C on top of B - let result = - cherry_rebase_group(&test_repository.repository, b.id(), &[c.id()], false).unwrap(); - - let commit: git2::Commit = test_repository.repository.find_commit(result).unwrap(); - - assert!(commit.is_conflicted()); - - assert_commit_tree_matches( - &test_repository.repository, - &commit, - &[ - (".auto-resolution/foo.txt", b"b"), // Prefer the commit we're rebasing onto - (".conflict-base-0/foo.txt", b"a"), // The content of A - (".conflict-side-0/foo.txt", b"b"), // "Our" side, content of B - (".conflict-side-1/foo.txt", b"c"), // "Their" side, content of C - ], - ); - } - - #[test] - fn rebase_single_conflicted_commit() { - let test_repository = TestingRepository::open(); - - let a = test_repository.commit_tree(None, &[("foo.txt", "a")]); - let b = test_repository.commit_tree(Some(&a), &[("foo.txt", "b")]); - let c = test_repository.commit_tree(Some(&a), &[("foo.txt", "c")]); - let d = test_repository.commit_tree(Some(&a), &[("foo.txt", "d")]); - - // Rebase C on top of B => C' - let result = - cherry_rebase_group(&test_repository.repository, b.id(), &[c.id()], false).unwrap(); - - // Rebase C' on top of D => C'' - let result = - cherry_rebase_group(&test_repository.repository, d.id(), &[result], false).unwrap(); - - let commit: git2::Commit = test_repository.repository.find_commit(result).unwrap(); - - assert!(commit.is_conflicted()); - - assert_commit_tree_matches( - &test_repository.repository, - &commit, - &[ - (".auto-resolution/foo.txt", b"d"), // Prefer the commit we're rebasing onto - (".conflict-base-0/foo.txt", b"a"), // The content of A - (".conflict-side-0/foo.txt", b"d"), // "Our" side, content of B - (".conflict-side-1/foo.txt", b"c"), // "Their" side, content of C - ], - ); - } - - /// Test what happens if you were to keep rebasing a branch on top of origin/master - #[test] - fn rebase_onto_series_multiple_times() { - let test_repository = TestingRepository::open(); - - let a = test_repository.commit_tree(None, &[("foo.txt", "a")]); - let b = test_repository.commit_tree(Some(&a), &[("foo.txt", "b")]); - let c = test_repository.commit_tree(Some(&b), &[("foo.txt", "c")]); - let d = test_repository.commit_tree(Some(&a), &[("foo.txt", "d")]); - - // Rebase D on top of B => D' - let result = - cherry_rebase_group(&test_repository.repository, b.id(), &[d.id()], false).unwrap(); - - let commit: git2::Commit = test_repository.repository.find_commit(result).unwrap(); - assert!(commit.is_conflicted()); - - assert_commit_tree_matches( - &test_repository.repository, - &commit, - &[ - (".auto-resolution/foo.txt", b"b"), // Prefer the commit we're rebasing onto - (".conflict-base-0/foo.txt", b"a"), // The content of A - (".conflict-side-0/foo.txt", b"b"), // "Our" side, content of B - (".conflict-side-1/foo.txt", b"d"), // "Their" side, content of D - ], - ); - - // Rebase D' on top of C => D'' - let result = - cherry_rebase_group(&test_repository.repository, c.id(), &[result], false).unwrap(); - - let commit: git2::Commit = test_repository.repository.find_commit(result).unwrap(); - assert!(commit.is_conflicted()); - - assert_commit_tree_matches( - &test_repository.repository, - &commit, - &[ - (".auto-resolution/foo.txt", b"c"), // Prefer the commit we're rebasing onto - (".conflict-base-0/foo.txt", b"a"), // The content of A - (".conflict-side-0/foo.txt", b"c"), // "Our" side, content of C - (".conflict-side-1/foo.txt", b"d"), // "Their" side, content of D - ], - ); - } - - #[test] - fn multiple_commit_ends_up_conflicted() { - let test_repository = TestingRepository::open(); - - let a = test_repository.commit_tree(None, &[("foo.txt", "a"), ("bar.txt", "a")]); - let b = test_repository.commit_tree(Some(&a), &[("foo.txt", "b"), ("bar.txt", "a")]); - let c = test_repository.commit_tree(Some(&b), &[("foo.txt", "b"), ("bar.txt", "b")]); - let d = test_repository.commit_tree(Some(&a), &[("foo.txt", "c"), ("bar.txt", "c")]); - - // Rebase C on top of B - let result = cherry_rebase_group( - &test_repository.repository, - d.id(), - &[c.id(), b.id()], - false, - ) - .unwrap(); - - let commit: git2::Commit = test_repository.repository.find_commit(result).unwrap(); - - let commits: Vec = test_repository - .repository - .log(commit.id(), LogUntil::Commit(d.id()), false) - .unwrap(); - - assert!(commits.iter().all(|commit| commit.is_conflicted())); - - // Rebased version of B (B') - assert_commit_tree_matches( - &test_repository.repository, - &commits[1], - &[ - (".auto-resolution/foo.txt", b"c"), - (".auto-resolution/bar.txt", b"c"), - (".conflict-base-0/foo.txt", b"a"), // Commit A contents - (".conflict-base-0/bar.txt", b"a"), - (".conflict-side-0/foo.txt", b"c"), // (ours) Commit D contents - (".conflict-side-0/bar.txt", b"c"), - (".conflict-side-1/foo.txt", b"b"), // (theirs) Commit B contents - (".conflict-side-1/bar.txt", b"a"), - ], - ); - - // Rebased version of C - assert_commit_tree_matches( - &test_repository.repository, - &commits[0], - &[ - (".auto-resolution/foo.txt", b"c"), - (".auto-resolution/bar.txt", b"c"), - (".conflict-base-0/foo.txt", b"b"), // Commit B contents - (".conflict-base-0/bar.txt", b"a"), - (".conflict-side-0/foo.txt", b"c"), // (ours) Commit B' contents - (".conflict-side-0/bar.txt", b"c"), - (".conflict-side-1/foo.txt", b"b"), // (theirs) Commit C contents - (".conflict-side-1/bar.txt", b"b"), - ], - ); - } - } - #[cfg(test)] - mod gitbutler_merge_commits { - use crate::rebase::gitbutler_merge_commits; - use gitbutler_commit::commit_ext::CommitExt as _; - use gitbutler_testsupport::testing_repository::{ - assert_commit_tree_matches, TestingRepository, - }; - - #[test] - fn unconflicting_merge() { - let test_repository = TestingRepository::open(); - - // Make some commits - let a = test_repository.commit_tree(None, &[("foo.txt", "a")]); - let b = test_repository.commit_tree(Some(&a), &[("foo.txt", "b")]); - let c = test_repository.commit_tree(Some(&a), &[("foo.txt", "a"), ("bar.txt", "a")]); - - let result = - gitbutler_merge_commits(&test_repository.repository, b, c, "master", "feature") - .unwrap(); - - assert!(!result.is_conflicted()); - - assert_commit_tree_matches( - &test_repository.repository, - &result, - &[("foo.txt", b"b"), ("bar.txt", b"a")], - ); - } - - #[test] - fn conflicting_merge() { - let test_repository = TestingRepository::open(); - - // Make some commits - let a = test_repository.commit_tree(None, &[("foo.txt", "a")]); - let b = test_repository.commit_tree(Some(&a), &[("foo.txt", "b")]); - let c = test_repository.commit_tree(Some(&a), &[("foo.txt", "c")]); - - let result = - gitbutler_merge_commits(&test_repository.repository, b, c, "master", "feature") - .unwrap(); - - assert!(result.is_conflicted()); - - assert_commit_tree_matches( - &test_repository.repository, - &result, - &[ - (".auto-resolution/foo.txt", b"c"), // Prefer the "Our" side, C - (".conflict-base-0/foo.txt", b"a"), // The content of A - (".conflict-side-0/foo.txt", b"c"), // "Our" side, content of B - (".conflict-side-1/foo.txt", b"b"), // "Their" side, content of C - ], - ); - } - - #[test] - fn merging_conflicted_commit_with_unconflicted_incoming() { - let test_repository = TestingRepository::open(); - - // Make some commits - let a = test_repository.commit_tree(None, &[("foo.txt", "a")]); - let b = test_repository.commit_tree(Some(&a), &[("foo.txt", "b")]); - let c = test_repository.commit_tree(Some(&a), &[("foo.txt", "c")]); - let d = test_repository.commit_tree(Some(&a), &[("foo.txt", "a"), ("bar.txt", "a")]); - - let bc_result = - gitbutler_merge_commits(&test_repository.repository, b, c, "master", "feature") - .unwrap(); - - let result = gitbutler_merge_commits( - &test_repository.repository, - bc_result, - d, - "master", - "feature", - ) - .unwrap(); - - // While its based on a conflicted commit, merging `bc_result` and `d` - // should not conflict, because the auto-resolution of `bc_result`, - // and `a` can be cleanly merged when `a` is the base. - // - // bc_result auto-resoultion tree: - // foo.txt: c - - assert!(!result.is_conflicted()); - - assert_commit_tree_matches( - &test_repository.repository, - &result, - &[("foo.txt", b"c"), ("bar.txt", b"a")], - ); - } - - #[test] - fn merging_conflicted_commit_with_conflicted_incoming() { - let test_repository = TestingRepository::open(); - - // Make some commits - let a = test_repository.commit_tree(None, &[("foo.txt", "a"), ("bar.txt", "a")]); - let b = test_repository.commit_tree(Some(&a), &[("foo.txt", "b"), ("bar.txt", "a")]); - let c = test_repository.commit_tree(Some(&a), &[("foo.txt", "c"), ("bar.txt", "a")]); - let d = test_repository.commit_tree(Some(&a), &[("foo.txt", "a"), ("bar.txt", "b")]); - let e = test_repository.commit_tree(Some(&a), &[("foo.txt", "a"), ("bar.txt", "c")]); - - let bc_result = - gitbutler_merge_commits(&test_repository.repository, b, c, "master", "feature") - .unwrap(); - - let de_result = - gitbutler_merge_commits(&test_repository.repository, d, e, "master", "feature") - .unwrap(); - - let result = gitbutler_merge_commits( - &test_repository.repository, - bc_result, - de_result, - "master", - "feature", - ) - .unwrap(); - - // We don't expect result to be conflicted, because we've chosen the - // setup such that the auto-resolution of `bc_result` and `de_result` - // don't conflict when merged themselves. - // - // bc_result auto-resoultion tree: - // foo.txt: c - // bar.txt: a - // - // bc_result auto-resoultion tree: - // foo.txt: a - // bar.txt: c - - assert!(!result.is_conflicted()); - - assert_commit_tree_matches( - &test_repository.repository, - &result, - &[("foo.txt", b"c"), ("bar.txt", b"c")], - ); - } - - #[test] - fn merging_conflicted_commit_with_conflicted_incoming_and_results_in_conflicted() { - let test_repository = TestingRepository::open(); - - // Make some commits - let a = test_repository.commit_tree(None, &[("foo.txt", "a")]); - let b = test_repository.commit_tree(Some(&a), &[("foo.txt", "b")]); - let c = test_repository.commit_tree(Some(&a), &[("foo.txt", "c")]); - let d = test_repository.commit_tree(Some(&a), &[("foo.txt", "d")]); - let e = test_repository.commit_tree(Some(&a), &[("foo.txt", "f")]); - - let bc_result = - gitbutler_merge_commits(&test_repository.repository, b, c, "master", "feature") - .unwrap(); - - let de_result = - gitbutler_merge_commits(&test_repository.repository, d, e, "master", "feature") - .unwrap(); - - let result = gitbutler_merge_commits( - &test_repository.repository, - bc_result, - de_result, - "master", - "feature", - ) - .unwrap(); - - // bc_result auto-resoultion tree: - // foo.txt: c - // - // bc_result auto-resoultion tree: - // foo.txt: f - // - // This conflicts and results in auto-resolution f - // - // We however expect the theirs side to be "b" and the ours side to - // be "f" - - assert!(result.is_conflicted()); - - assert_commit_tree_matches( - &test_repository.repository, - &result, - &[ - (".auto-resolution/foo.txt", b"f"), // Incoming change preferred - (".conflict-base-0/foo.txt", b"a"), // Base should match A - (".conflict-side-0/foo.txt", b"f"), // Side 0 should be incoming change - (".conflict-side-1/foo.txt", b"b"), // Side 1 should be target change - ], - ); - } - } - #[cfg(test)] - mod resolve_index { - use crate::rebase::resolve_index; - use gitbutler_testsupport::testing_repository::TestingRepository; - - #[test] - fn test_same_file_twice() { - let test_repository = TestingRepository::open(); - - // Make some commits - let a = test_repository.commit_tree(None, &[("foo.txt", "a")]); - let b = test_repository.commit_tree(None, &[("foo.txt", "b")]); - let c = test_repository.commit_tree(None, &[("foo.txt", "c")]); - test_repository.commit_tree(None, &[("foo.txt", "asdfasdf")]); - - // Merge the index - let mut index: git2::Index = test_repository - .repository - .merge_trees( - &a.tree().unwrap(), // Base - &b.tree().unwrap(), // Ours - &c.tree().unwrap(), // Theirs - None, - ) - .unwrap(); - - assert!(index.has_conflicts()); - - // Call our index resolution function - resolve_index(&test_repository.repository, &mut index).unwrap(); - - // Ensure there are no conflicts - assert!(!index.has_conflicts()); - - let tree = index.write_tree_to(&test_repository.repository).unwrap(); - let tree: git2::Tree = test_repository.repository.find_tree(tree).unwrap(); - - let blob = tree.get_name("foo.txt").unwrap().id(); // We fail here to get the entry because the tree is empty - let blob: git2::Blob = test_repository.repository.find_blob(blob).unwrap(); - - assert_eq!(blob.content(), b"b") - } - - #[test] - fn test_diverging_renames() { - let test_repository = TestingRepository::open(); - - // Make some commits - let a = test_repository.commit_tree(None, &[("foo.txt", "a")]); - let b = test_repository.commit_tree(None, &[("bar.txt", "a")]); - let c = test_repository.commit_tree(None, &[("baz.txt", "a")]); - test_repository.commit_tree(None, &[("foo.txt", "asdfasdf")]); - - // Merge the index - let mut index: git2::Index = test_repository - .repository - .merge_trees( - &a.tree().unwrap(), // Base - &b.tree().unwrap(), // Ours - &c.tree().unwrap(), // Theirs - None, - ) - .unwrap(); - - assert!(index.has_conflicts()); - - // Call our index resolution function - resolve_index(&test_repository.repository, &mut index).unwrap(); - - // Ensure there are no conflicts - assert!(!index.has_conflicts()); - - let tree = index.write_tree_to(&test_repository.repository).unwrap(); - let tree: git2::Tree = test_repository.repository.find_tree(tree).unwrap(); - - assert!(tree.get_name("foo.txt").is_none()); - assert!(tree.get_name("baz.txt").is_none()); - - let blob = tree.get_name("bar.txt").unwrap().id(); // We fail here to get the entry because the tree is empty - let blob: git2::Blob = test_repository.repository.find_blob(blob).unwrap(); - - assert_eq!(blob.content(), b"a") - } - - #[test] - fn test_converging_renames() { - let test_repository = TestingRepository::open(); - - // Make some commits - let a = test_repository.commit_tree(None, &[("foo.txt", "a"), ("bar.txt", "b")]); - let b = test_repository.commit_tree(None, &[("baz.txt", "a")]); - let c = test_repository.commit_tree(None, &[("baz.txt", "b")]); - test_repository.commit_tree(None, &[("foo.txt", "asdfasdf")]); - - // Merge the index - let mut index: git2::Index = test_repository - .repository - .merge_trees( - &a.tree().unwrap(), // Base - &b.tree().unwrap(), // Ours - &c.tree().unwrap(), // Theirs - None, - ) - .unwrap(); - - assert!(index.has_conflicts()); - - // Call our index resolution function - resolve_index(&test_repository.repository, &mut index).unwrap(); - - // Ensure there are no conflicts - assert!(!index.has_conflicts()); - - let tree = index.write_tree_to(&test_repository.repository).unwrap(); - let tree: git2::Tree = test_repository.repository.find_tree(tree).unwrap(); - - assert!(tree.get_name("foo.txt").is_none()); - assert!(tree.get_name("bar.txt").is_none()); - - let blob = tree.get_name("baz.txt").unwrap().id(); // We fail here to get the entry because the tree is empty - let blob: git2::Blob = test_repository.repository.find_blob(blob).unwrap(); - - assert_eq!(blob.content(), b"a") + /// Assure that the returned headers will always indicate a conflict. + /// This is a fail-safe in case this instance has no paths stored as auto-resolution + /// removed the path that would otherwise be conflicting. + /// In other words: conflicting index entries aren't reliable when conflicts were resolved + /// with the 'ours' strategy. + fn to_headers(&self) -> CommitHeadersV2 { + CommitHeadersV2 { + conflicted: Some({ + let entries = self.total_entries(); + if entries > 0 { + entries as u64 + } else { + 1 + } + }), + ..Default::default() } } } diff --git a/crates/gitbutler-repo/src/repository_ext.rs b/crates/gitbutler-repo/src/repository_ext.rs index d518b30af0..8a99641237 100644 --- a/crates/gitbutler-repo/src/repository_ext.rs +++ b/crates/gitbutler-repo/src/repository_ext.rs @@ -12,7 +12,6 @@ use gitbutler_oxidize::{ use gitbutler_reference::{Refname, RemoteRefname}; use gix::filter::plumbing::pipeline::convert::ToGitOutcome; use gix::fs::is_executable; -use gix::merge::tree::{Options, TreatAsUnresolved}; use gix::objs::WriteTo; use std::io; #[cfg(unix)] @@ -743,111 +742,6 @@ impl CheckoutIndexBuilder<'_> { } } -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::TreatAsUnresolved, - )>; - - /// Just like [`Self::merge_options_fail_fast()`], but additionally don't perform rename tracking. - /// This is useful if the merge result isn't going to be used, and we are only interested in knowing - /// if a merge would succeed. - fn merge_options_no_rewrites_fail_fast(&self) -> Result<(Options, TreatAsUnresolved)>; -} - -impl GixRepositoryExt for gix::Repository { - fn for_tree_diffing(mut self) -> anyhow::Result { - let bytes = self.compute_object_cache_size_for_tree_diffs(&***self.index_or_empty()?); - 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_no_rewrites_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, TreatAsUnresolved)> { - let conflict_kind = gix::merge::tree::TreatAsUnresolved::Renames; - let options = self - .tree_merge_options()? - .with_fail_on_conflict(Some(conflict_kind)); - Ok((options, conflict_kind)) - } - - fn merge_options_no_rewrites_fail_fast(&self) -> Result<(Options, TreatAsUnresolved)> { - let (options, conflict_kind) = self.merge_options_fail_fast()?; - Ok((options.with_rewrites(None), conflict_kind)) - } -} - type OidFilter = dyn Fn(&git2::Commit) -> Result; /// Generally, all traversals will use no particular ordering, it's implementation defined in `git2`. diff --git a/crates/gitbutler-repo/tests/mod.rs b/crates/gitbutler-repo/tests/mod.rs index f17b63c9c5..5595c4f124 100644 --- a/crates/gitbutler-repo/tests/mod.rs +++ b/crates/gitbutler-repo/tests/mod.rs @@ -1,3 +1,4 @@ mod create_wd_tree; mod credentials; mod merge_base_octopussy; +mod rebase; diff --git a/crates/gitbutler-repo/tests/rebase.rs b/crates/gitbutler-repo/tests/rebase.rs new file mode 100644 index 0000000000..9029a07b0d --- /dev/null +++ b/crates/gitbutler-repo/tests/rebase.rs @@ -0,0 +1,409 @@ +mod cherry_rebase_group { + use gitbutler_commit::commit_ext::CommitExt; + use gitbutler_repo::RepositoryExt as _; + use gitbutler_testsupport::testing_repository::{ + assert_commit_tree_matches, TestingRepository, + }; + + use gitbutler_repo::{rebase::cherry_rebase_group, LogUntil}; + + #[test] + fn unconflicting_rebase() { + let test_repository = TestingRepository::open(); + + // Make some commits + let a = test_repository.commit_tree(None, &[("foo.txt", "a"), ("bar.txt", "a")]); + let b = test_repository.commit_tree(Some(&a), &[("foo.txt", "b"), ("bar.txt", "a")]); + let c = test_repository.commit_tree(Some(&b), &[("foo.txt", "c"), ("bar.txt", "a")]); + let d = test_repository.commit_tree(Some(&a), &[("foo.txt", "a"), ("bar.txt", "x")]); + + let result = cherry_rebase_group( + &test_repository.repository, + d.id(), + &[c.id(), b.id()], + false, + ) + .unwrap(); + + let commit: git2::Commit = test_repository.repository.find_commit(result).unwrap(); + + let commits: Vec = test_repository + .repository + .log(commit.id(), LogUntil::End, false) + .unwrap(); + + assert!(commits.into_iter().all(|commit| !commit.is_conflicted())); + + assert_commit_tree_matches( + &test_repository.repository, + &commit, + &[("foo.txt", b"c"), ("bar.txt", b"x")], + ); + } + + #[test] + fn single_commit_ends_up_conflicted() { + let test_repository = TestingRepository::open(); + + let a = test_repository.commit_tree(None, &[("foo.txt", "a")]); + let b = test_repository.commit_tree(Some(&a), &[("foo.txt", "b")]); + let c = test_repository.commit_tree(Some(&a), &[("foo.txt", "c")]); + + // Rebase C on top of B + let result = + cherry_rebase_group(&test_repository.repository, b.id(), &[c.id()], false).unwrap(); + + let commit: git2::Commit = test_repository.repository.find_commit(result).unwrap(); + + assert!(commit.is_conflicted()); + + assert_commit_tree_matches( + &test_repository.repository, + &commit, + &[ + (".auto-resolution/foo.txt", b"b"), // Prefer the commit we're rebasing onto + (".conflict-base-0/foo.txt", b"a"), // The content of A + (".conflict-side-0/foo.txt", b"b"), // "Our" side, content of B + (".conflict-side-1/foo.txt", b"c"), // "Their" side, content of C + ], + ); + } + + #[test] + fn rebase_single_conflicted_commit() { + let test_repository = TestingRepository::open(); + + let a = test_repository.commit_tree(None, &[("foo.txt", "a")]); + let b = test_repository.commit_tree(Some(&a), &[("foo.txt", "b")]); + let c = test_repository.commit_tree(Some(&a), &[("foo.txt", "c")]); + let d = test_repository.commit_tree(Some(&a), &[("foo.txt", "d")]); + + // Rebase C on top of B => C' + let result = + cherry_rebase_group(&test_repository.repository, b.id(), &[c.id()], false).unwrap(); + + // Rebase C' on top of D => C'' + let result = + cherry_rebase_group(&test_repository.repository, d.id(), &[result], false).unwrap(); + + let commit: git2::Commit = test_repository.repository.find_commit(result).unwrap(); + + assert!(commit.is_conflicted()); + + assert_commit_tree_matches( + &test_repository.repository, + &commit, + &[ + (".auto-resolution/foo.txt", b"d"), // Prefer the commit we're rebasing onto + (".conflict-base-0/foo.txt", b"a"), // The content of A + (".conflict-side-0/foo.txt", b"d"), // "Our" side, content of B + (".conflict-side-1/foo.txt", b"c"), // "Their" side, content of C + ], + ); + } + + /// Test what happens if you were to keep rebasing a branch on top of origin/master + #[test] + fn rebase_onto_series_multiple_times() { + let test_repository = TestingRepository::open(); + + let a = test_repository.commit_tree(None, &[("foo.txt", "a")]); + let b = test_repository.commit_tree(Some(&a), &[("foo.txt", "b")]); + let c = test_repository.commit_tree(Some(&b), &[("foo.txt", "c")]); + let d = test_repository.commit_tree(Some(&a), &[("foo.txt", "d")]); + + // Rebase D on top of B => D' + let result = + cherry_rebase_group(&test_repository.repository, b.id(), &[d.id()], false).unwrap(); + + let commit: git2::Commit = test_repository.repository.find_commit(result).unwrap(); + assert!(commit.is_conflicted()); + + assert_commit_tree_matches( + &test_repository.repository, + &commit, + &[ + (".auto-resolution/foo.txt", b"b"), // Prefer the commit we're rebasing onto + (".conflict-base-0/foo.txt", b"a"), // The content of A + (".conflict-side-0/foo.txt", b"b"), // "Our" side, content of B + (".conflict-side-1/foo.txt", b"d"), // "Their" side, content of D + ], + ); + + // Rebase D' on top of C => D'' + let result = + cherry_rebase_group(&test_repository.repository, c.id(), &[result], false).unwrap(); + + let commit: git2::Commit = test_repository.repository.find_commit(result).unwrap(); + assert!(commit.is_conflicted()); + + assert_commit_tree_matches( + &test_repository.repository, + &commit, + &[ + (".auto-resolution/foo.txt", b"c"), // Prefer the commit we're rebasing onto + (".conflict-base-0/foo.txt", b"a"), // The content of A + (".conflict-side-0/foo.txt", b"c"), // "Our" side, content of C + (".conflict-side-1/foo.txt", b"d"), // "Their" side, content of D + ], + ); + } + + #[test] + fn multiple_commit_ends_up_conflicted() { + let test_repository = TestingRepository::open(); + + let a = test_repository.commit_tree(None, &[("foo.txt", "a"), ("bar.txt", "a")]); + let b = test_repository.commit_tree(Some(&a), &[("foo.txt", "b"), ("bar.txt", "a")]); + let c = test_repository.commit_tree(Some(&b), &[("foo.txt", "b"), ("bar.txt", "b")]); + let d = test_repository.commit_tree(Some(&a), &[("foo.txt", "c"), ("bar.txt", "c")]); + + // Rebase C on top of B + let result = cherry_rebase_group( + &test_repository.repository, + d.id(), + &[c.id(), b.id()], + false, + ) + .unwrap(); + + let commit: git2::Commit = test_repository.repository.find_commit(result).unwrap(); + + let commits: Vec = test_repository + .repository + .log(commit.id(), LogUntil::Commit(d.id()), false) + .unwrap(); + + assert!(commits.iter().all(|commit| commit.is_conflicted())); + + // Rebased version of B (B') + assert_commit_tree_matches( + &test_repository.repository, + &commits[1], + &[ + (".auto-resolution/foo.txt", b"c"), + (".auto-resolution/bar.txt", b"c"), + (".conflict-base-0/foo.txt", b"a"), // Commit A contents + (".conflict-base-0/bar.txt", b"a"), + (".conflict-side-0/foo.txt", b"c"), // (ours) Commit D contents + (".conflict-side-0/bar.txt", b"c"), + (".conflict-side-1/foo.txt", b"b"), // (theirs) Commit B contents + (".conflict-side-1/bar.txt", b"a"), + ], + ); + + // Rebased version of C + assert_commit_tree_matches( + &test_repository.repository, + &commits[0], + &[ + (".auto-resolution/foo.txt", b"c"), + (".auto-resolution/bar.txt", b"c"), + (".conflict-base-0/foo.txt", b"b"), // Commit B contents + (".conflict-base-0/bar.txt", b"a"), + (".conflict-side-0/foo.txt", b"c"), // (ours) Commit B' contents + (".conflict-side-0/bar.txt", b"c"), + (".conflict-side-1/foo.txt", b"b"), // (theirs) Commit C contents + (".conflict-side-1/bar.txt", b"b"), + ], + ); + } +} + +mod gitbutler_merge_commits { + use gitbutler_commit::commit_ext::CommitExt as _; + use gitbutler_repo::rebase::gitbutler_merge_commits; + use gitbutler_testsupport::testing_repository::{ + assert_commit_tree_matches, TestingRepository, + }; + + #[test] + fn unconflicting_merge() { + let test_repository = TestingRepository::open(); + + // Make some commits + let a = test_repository.commit_tree(None, &[("foo.txt", "a")]); + let b = test_repository.commit_tree(Some(&a), &[("foo.txt", "b")]); + let c = test_repository.commit_tree(Some(&a), &[("foo.txt", "a"), ("bar.txt", "a")]); + + let result = + gitbutler_merge_commits(&test_repository.repository, b, c, "master", "feature") + .unwrap(); + + assert!(!result.is_conflicted()); + + assert_commit_tree_matches( + &test_repository.repository, + &result, + &[("foo.txt", b"b"), ("bar.txt", b"a")], + ); + } + + #[test] + fn conflicting_merge() { + let test_repository = TestingRepository::open(); + + // Make some commits + let a = test_repository.commit_tree(None, &[("foo.txt", "a")]); + let b = test_repository.commit_tree(Some(&a), &[("foo.txt", "b")]); + let c = test_repository.commit_tree(Some(&a), &[("foo.txt", "c")]); + + let result = + gitbutler_merge_commits(&test_repository.repository, b, c, "master", "feature") + .unwrap(); + + assert!(result.is_conflicted()); + + assert_commit_tree_matches( + &test_repository.repository, + &result, + &[ + (".auto-resolution/foo.txt", b"c"), // Prefer the "Our" side, C + (".conflict-base-0/foo.txt", b"a"), // The content of A + (".conflict-side-0/foo.txt", b"c"), // "Our" side, content of B + (".conflict-side-1/foo.txt", b"b"), // "Their" side, content of C + ], + ); + } + + #[test] + fn merging_conflicted_commit_with_unconflicted_incoming() { + let test_repository = TestingRepository::open(); + + // Make some commits + let a = test_repository.commit_tree(None, &[("foo.txt", "a")]); + let b = test_repository.commit_tree(Some(&a), &[("foo.txt", "b")]); + let c = test_repository.commit_tree(Some(&a), &[("foo.txt", "c")]); + let d = test_repository.commit_tree(Some(&a), &[("foo.txt", "a"), ("bar.txt", "a")]); + + let bc_result = + gitbutler_merge_commits(&test_repository.repository, b, c, "master", "feature") + .unwrap(); + + let result = gitbutler_merge_commits( + &test_repository.repository, + bc_result, + d, + "master", + "feature", + ) + .unwrap(); + + // While its based on a conflicted commit, merging `bc_result` and `d` + // should not conflict, because the auto-resolution of `bc_result`, + // and `a` can be cleanly merged when `a` is the base. + // + // bc_result auto-resoultion tree: + // foo.txt: c + + assert!(!result.is_conflicted()); + + assert_commit_tree_matches( + &test_repository.repository, + &result, + &[("foo.txt", b"c"), ("bar.txt", b"a")], + ); + } + + #[test] + fn merging_conflicted_commit_with_conflicted_incoming() { + let test_repository = TestingRepository::open(); + + // Make some commits + let a = test_repository.commit_tree(None, &[("foo.txt", "a"), ("bar.txt", "a")]); + let b = test_repository.commit_tree(Some(&a), &[("foo.txt", "b"), ("bar.txt", "a")]); + let c = test_repository.commit_tree(Some(&a), &[("foo.txt", "c"), ("bar.txt", "a")]); + let d = test_repository.commit_tree(Some(&a), &[("foo.txt", "a"), ("bar.txt", "b")]); + let e = test_repository.commit_tree(Some(&a), &[("foo.txt", "a"), ("bar.txt", "c")]); + + let bc_result = + gitbutler_merge_commits(&test_repository.repository, b, c, "master", "feature") + .unwrap(); + + let de_result = + gitbutler_merge_commits(&test_repository.repository, d, e, "master", "feature") + .unwrap(); + + let result = gitbutler_merge_commits( + &test_repository.repository, + bc_result, + de_result, + "master", + "feature", + ) + .unwrap(); + + // We don't expect result to be conflicted, because we've chosen the + // setup such that the auto-resolution of `bc_result` and `de_result` + // don't conflict when merged themselves. + // + // bc_result auto-resolution tree: + // foo.txt: c + // bar.txt: a + // + // bc_result auto-resolution tree: + // foo.txt: a + // bar.txt: c + + assert!(!result.is_conflicted()); + + assert_commit_tree_matches( + &test_repository.repository, + &result, + &[("foo.txt", b"c"), ("bar.txt", b"c")], + ); + } + + #[test] + fn merging_conflicted_commit_with_conflicted_incoming_and_results_in_conflicted() { + let test_repository = TestingRepository::open(); + + // Make some commits + let a = test_repository.commit_tree(None, &[("foo.txt", "a")]); + let b = test_repository.commit_tree(Some(&a), &[("foo.txt", "b")]); + let c = test_repository.commit_tree(Some(&a), &[("foo.txt", "c")]); + let d = test_repository.commit_tree(Some(&a), &[("foo.txt", "d")]); + let e = test_repository.commit_tree(Some(&a), &[("foo.txt", "f")]); + + let bc_result = + gitbutler_merge_commits(&test_repository.repository, b, c, "master", "feature") + .unwrap(); + + let de_result = + gitbutler_merge_commits(&test_repository.repository, d, e, "master", "feature") + .unwrap(); + + let result = gitbutler_merge_commits( + &test_repository.repository, + bc_result, + de_result, + "master", + "feature", + ) + .unwrap(); + + // bc_result auto-resoultion tree: + // foo.txt: c + // + // bc_result auto-resoultion tree: + // foo.txt: f + // + // This conflicts and results in auto-resolution f + // + // We however expect the theirs side to be "b" and the ours side to + // be "f" + + assert!(result.is_conflicted()); + + assert_commit_tree_matches( + &test_repository.repository, + &result, + &[ + (".auto-resolution/foo.txt", b"f"), // Incoming change preferred + (".conflict-base-0/foo.txt", b"a"), // Base should match A + (".conflict-side-0/foo.txt", b"f"), // Side 0 should be incoming change + (".conflict-side-1/foo.txt", b"b"), // Side 1 should be target change + ], + ); + } +} diff --git a/crates/gitbutler-secret/Cargo.toml b/crates/gitbutler-secret/Cargo.toml index 337f695b8f..1cec7d27c5 100644 --- a/crates/gitbutler-secret/Cargo.toml +++ b/crates/gitbutler-secret/Cargo.toml @@ -4,6 +4,7 @@ version = "0.0.0" edition = "2021" authors = ["GitButler "] publish = false +autotests = false [dependencies] anyhow = "1.0.93" diff --git a/crates/gitbutler-stack/Cargo.toml b/crates/gitbutler-stack/Cargo.toml index a7ef8e40cd..6c3ab311c2 100644 --- a/crates/gitbutler-stack/Cargo.toml +++ b/crates/gitbutler-stack/Cargo.toml @@ -4,6 +4,7 @@ version = "0.0.0" edition = "2021" authors = ["GitButler "] publish = false +autotests = false [dependencies] git2.workspace = true diff --git a/crates/gitbutler-stack/tests/mod.rs b/crates/gitbutler-stack/tests/mod.rs index fb9807d0d5..5ad2fc744b 100644 --- a/crates/gitbutler-stack/tests/mod.rs +++ b/crates/gitbutler-stack/tests/mod.rs @@ -1,5 +1,5 @@ -pub mod file_ownership; -pub mod ownership; +mod file_ownership; +mod ownership; use anyhow::Result; use gitbutler_command_context::CommandContext; diff --git a/crates/gitbutler-testsupport/src/test_project.rs b/crates/gitbutler-testsupport/src/test_project.rs index d40bc0b512..7b4cb3e004 100644 --- a/crates/gitbutler-testsupport/src/test_project.rs +++ b/crates/gitbutler-testsupport/src/test_project.rs @@ -1,10 +1,10 @@ +use crate::{init_opts, VAR_NO_CLEANUP}; +use gitbutler_oxidize::{git2_to_gix_object_id, gix_to_git2_oid, GixRepositoryExt}; use gitbutler_reference::{LocalRefname, Refname}; use gitbutler_repo::RepositoryExt; use std::{fs, path, path::PathBuf}; use tempfile::TempDir; -use crate::{init_opts, VAR_NO_CLEANUP}; - pub fn temp_dir() -> TempDir { tempfile::tempdir().unwrap() } @@ -214,64 +214,60 @@ impl TestProject { } /// works like if we'd open and merge a PR on github. does not update local. - pub fn merge(&self, branch_name: &Refname) { + pub fn merge(&self, branch_name: &Refname) -> anyhow::Result<()> { let branch_name: Refname = match branch_name { - Refname::Local(local) => format!("refs/heads/{}", local.branch()).parse().unwrap(), - Refname::Remote(remote) => format!("refs/heads/{}", remote.branch()).parse().unwrap(), - _ => "INVALID".parse().unwrap(), // todo + Refname::Local(local) => format!("refs/heads/{}", local.branch()).parse()?, + Refname::Remote(remote) => format!("refs/heads/{}", remote.branch()).parse()?, + _ => "INVALID".parse()?, // todo }; let branch = self .remote_repository - .maybe_find_branch_by_refname(&branch_name) - .unwrap(); - let branch_commit = branch.as_ref().unwrap().get().peel_to_commit().unwrap(); + .maybe_find_branch_by_refname(&branch_name)? + .expect("branch exists"); + let branch_commit = branch.get().peel_to_commit()?; let master_branch = { - let name: Refname = "refs/heads/master".parse().unwrap(); - self.remote_repository - .maybe_find_branch_by_refname(&name) - .unwrap() + let name: Refname = "refs/heads/master".parse()?; + self.remote_repository.maybe_find_branch_by_refname(&name)? }; let master_branch_commit = master_branch .as_ref() - .unwrap() + .expect("master branch exists") .get() - .peel_to_commit() - .unwrap(); + .peel_to_commit()?; - let merge_base = { - let oid = self - .remote_repository - .merge_base(branch_commit.id(), master_branch_commit.id()) - .unwrap(); - self.remote_repository.find_commit(oid).unwrap() - }; + let gix_repo = gix::open_opts( + self.remote_repository.path(), + gix::open::Options::isolated(), + )?; let merge_tree = { - let mut merge_index = self - .remote_repository - .merge_trees( - &merge_base.tree().unwrap(), - &master_branch.unwrap().get().peel_to_tree().unwrap(), - &branch.unwrap().get().peel_to_tree().unwrap(), - None, - ) - .unwrap(); - let repo: &git2::Repository = &self.remote_repository; - let oid = merge_index.write_tree_to(repo).unwrap(); - self.remote_repository.find_tree(oid).unwrap() + let mut merge_result = gix_repo.merge_commits( + git2_to_gix_object_id(master_branch_commit.id()), + git2_to_gix_object_id(branch.get().peel_to_commit()?.id()), + gix_repo.default_merge_labels(), + gix::merge::commit::Options::default(), + )?; + assert!( + !merge_result + .tree_merge + .has_unresolved_conflicts(Default::default()), + "test-merges should have non-conflicting trees" + ); + let tree_id = merge_result.tree_merge.tree.write()?; + self.remote_repository.find_tree(gix_to_git2_oid(tree_id))? }; let repo: &git2::Repository = &self.remote_repository; repo.commit_with_signature( - Some(&"refs/heads/master".parse().unwrap()), + Some(&"refs/heads/master".parse()?), &branch_commit.author(), &branch_commit.committer(), &format!("Merge pull request from {}", branch_name), &merge_tree, &[&master_branch_commit, &branch_commit], None, - ) - .unwrap(); + )?; + Ok(()) } pub fn find_commit(&self, oid: git2::Oid) -> Result, git2::Error> { diff --git a/crates/gitbutler-user/Cargo.toml b/crates/gitbutler-user/Cargo.toml index 9dcf5b5f77..afa51794b1 100644 --- a/crates/gitbutler-user/Cargo.toml +++ b/crates/gitbutler-user/Cargo.toml @@ -4,6 +4,7 @@ version = "0.0.0" edition = "2021" authors = ["GitButler "] publish = false +autotests = false [dependencies] gitbutler-secret.workspace = true diff --git a/crates/gitbutler-user/tests/mod.rs b/crates/gitbutler-user/tests/mod.rs index 503cf97d20..e8889f08e0 100644 --- a/crates/gitbutler-user/tests/mod.rs +++ b/crates/gitbutler-user/tests/mod.rs @@ -1,3 +1,3 @@ // TODO(kv): These tests should live in the crate where the secret handling is implemented. -// For purposes of separating thing out of gitbutler-core, moving them here termporarely +// For purposes of separating thing out of gitbutler-core, moving them here temporarily pub mod secret; diff --git a/crates/gitbutler-workspace/Cargo.toml b/crates/gitbutler-workspace/Cargo.toml index d65c60a312..8c21397006 100644 --- a/crates/gitbutler-workspace/Cargo.toml +++ b/crates/gitbutler-workspace/Cargo.toml @@ -4,6 +4,7 @@ version = "0.0.0" edition = "2021" authors = ["GitButler "] publish = false +autotests = false [dependencies] anyhow.workspace = true diff --git a/crates/gitbutler-workspace/src/branch_trees.rs b/crates/gitbutler-workspace/src/branch_trees.rs index fab4038d5f..aabe46b850 100644 --- a/crates/gitbutler-workspace/src/branch_trees.rs +++ b/crates/gitbutler-workspace/src/branch_trees.rs @@ -2,10 +2,10 @@ 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_oxidize::{git2_to_gix_object_id, gix_to_git2_oid, GixRepositoryExt}; use gitbutler_project::access::WorktreeWritePermission; use gitbutler_repo::rebase::cherry_rebase_group; -use gitbutler_repo::{GixRepositoryExt, RepositoryExt as _}; +use gitbutler_repo::RepositoryExt as _; use gitbutler_stack::{Stack, VirtualBranchesHandle}; use tracing::instrument; diff --git a/crates/gitbutler-workspace/tests/mod.rs b/crates/gitbutler-workspace/tests/mod.rs index 481ac248b3..ddc69695ec 100644 --- a/crates/gitbutler-workspace/tests/mod.rs +++ b/crates/gitbutler-workspace/tests/mod.rs @@ -1,7 +1,6 @@ mod branch_trees; -#[cfg(test)] -mod test { +mod checkout_branch_trees { use std::fs; use gitbutler_branch::BranchCreateRequest;