gix-merge-0.16.0/.cargo_vcs_info.json0000644000000001471046102023000130300ustar { "git": { "sha1": "53f880c7604232c367870088176e42efd8a5b783" }, "path_in_vcs": "gix-merge" }gix-merge-0.16.0/Cargo.lock0000644000001031561046102023000110070ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "allocator-api2" version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "anyhow" version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "arbitrary" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" dependencies = [ "derive_arbitrary", ] [[package]] name = "bitflags" version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "block-buffer" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array", ] [[package]] name = "bstr" version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" dependencies = [ "memchr", "regex-automata", "serde", ] [[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cpufeatures" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ "libc", ] [[package]] name = "crossbeam-utils" version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crypto-common" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "typenum", ] [[package]] name = "dashmap" version = "6.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" dependencies = [ "cfg-if", "crossbeam-utils", "hashbrown 0.14.5", "lock_api", "once_cell", "parking_lot_core", ] [[package]] name = "derive_arbitrary" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "diff" version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", ] [[package]] name = "document-features" version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" dependencies = [ "litrs", ] [[package]] name = "encoding_rs" version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ "cfg-if", ] [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", "windows-sys", ] [[package]] name = "faster-hex" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7223ae2d2f179b803433d9c830478527e92b8117eab39460edae7f1614d9fb73" dependencies = [ "heapless", "serde", ] [[package]] name = "fastrand" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "filetime" version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" dependencies = [ "cfg-if", "libc", "libredox", ] [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foldhash" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "foldhash" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" [[package]] name = "generic-array" version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", ] [[package]] name = "getrandom" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", "r-efi", "wasip2", "wasip3", ] [[package]] name = "gix-actor" version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "272916673b83714734b15d4ef3c8b5f1ccddb15fea8ff548430b97c1ab7b7ed8" dependencies = [ "bstr", "gix-date", "gix-error", "serde", ] [[package]] name = "gix-attributes" version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe17c5a1c0b6f2ef1476aa1d3222ea50cdff67608016613a58bfc3e078046000" dependencies = [ "bstr", "gix-glob", "gix-path", "gix-quote", "gix-trace", "kstring", "smallvec", "thiserror", "unicode-bom", ] [[package]] name = "gix-bitmap" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ecbfc77ec6852294e341ecc305a490b59f2813e6ca42d79efda5099dcab1894" dependencies = [ "gix-error", ] [[package]] name = "gix-chunk" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edf288be9b60fe7231de03771faa292be1493d84786f68727e33ad1f91764320" dependencies = [ "gix-error", ] [[package]] name = "gix-command" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86335306511abe43d75c866d4b1f3d90932fe202edcd43e1314036333e7384d8" dependencies = [ "bstr", "gix-path", "gix-quote", "gix-trace", "shell-words", ] [[package]] name = "gix-commitgraph" version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe3b5aa0f24e19028c261d229aeeedafcaaa52ebd71021cc15184620fc9d32eb" dependencies = [ "bstr", "gix-chunk", "gix-error", "gix-hash", "memmap2", "nonempty", ] [[package]] name = "gix-date" version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b94cdae4eb4b0f4136e3d9b3aa2d2cd03cfb5bb9b636b31263aea2df86d41543" dependencies = [ "bstr", "gix-error", "itoa", "jiff", "serde", "smallvec", ] [[package]] name = "gix-diff" version = "0.63.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc08e0fa1a91ff5f24affeab052f198056645e1de004910bde7b82b50ea5982a" dependencies = [ "bstr", "gix-command", "gix-filter", "gix-fs", "gix-hash", "gix-imara-diff", "gix-object", "gix-path", "gix-tempfile", "gix-trace", "gix-traverse", "gix-worktree", "thiserror", ] [[package]] name = "gix-error" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e207b971746ab724fccdfced2e4e19e854744611904a0195d3aa8fda8a110613" dependencies = [ "bstr", ] [[package]] name = "gix-features" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af375693ad5333d0a2c66b4c5b2cbe9ccc38e34f8e8bf24e4ae42c12307fdc4f" dependencies = [ "gix-trace", "gix-utils", "libc", "prodash", ] [[package]] name = "gix-filter" version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dac917dbe9653c9b615d248db91907a365bd779750c9e1b457a9d9fdeece3a08" dependencies = [ "bstr", "encoding_rs", "gix-attributes", "gix-command", "gix-hash", "gix-object", "gix-packetline", "gix-path", "gix-quote", "gix-trace", "gix-utils", "smallvec", "thiserror", ] [[package]] name = "gix-fs" version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b5d9f7e55a0f9a936a877fa4f9758692a308550a39a45684286941a20a8e5c0" dependencies = [ "bstr", "fastrand", "gix-features", "gix-path", "gix-utils", "thiserror", ] [[package]] name = "gix-glob" version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08bf29249a069bf2507f5964f80997f37b134d320ea348d66527726b9be2c38c" dependencies = [ "bitflags", "bstr", "gix-features", "gix-path", ] [[package]] name = "gix-hash" version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bcf70d1e252337eed16360f8b8ebb71865ece58eab7954b39ce38b420de703d2" dependencies = [ "faster-hex", "gix-features", "serde", "sha1-checked", "thiserror", ] [[package]] name = "gix-hashtable" version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d33b455e07b3c16d3b2eeebc7b38d2dafcbf8a653de1138ef55d4c2a1fd0b08b" dependencies = [ "gix-hash", "hashbrown 0.16.1", "parking_lot", ] [[package]] name = "gix-ignore" version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6bb13fbbeeafee943e52b61fcc88dfddf6a452fcaf0c4d0cdc8f218fa25bbec5" dependencies = [ "bstr", "gix-glob", "gix-path", "gix-trace", "unicode-bom", ] [[package]] name = "gix-imara-diff" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39eb0623e15e4cb83c02ce6a959e48fadd1ae3b715b36b5acc01816e01388c82" dependencies = [ "bstr", "hashbrown 0.15.5", ] [[package]] name = "gix-index" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54c3ef97ad08121e4327a6226bd63fed6b9e3c6b976d48bddd4356d9d41191db" dependencies = [ "bitflags", "bstr", "filetime", "fnv", "gix-bitmap", "gix-features", "gix-fs", "gix-hash", "gix-lock", "gix-object", "gix-traverse", "gix-utils", "gix-validate", "hashbrown 0.16.1", "itoa", "libc", "memmap2", "rustix", "smallvec", "thiserror", ] [[package]] name = "gix-lock" version = "23.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09b3bc074e5723027b482dcd9ab99d95804a53742f6de812d0172fbba4a186c1" dependencies = [ "gix-tempfile", "gix-utils", "thiserror", ] [[package]] name = "gix-merge" version = "0.16.0" dependencies = [ "arbitrary", "bstr", "document-features", "gix-command", "gix-diff", "gix-filter", "gix-fs", "gix-hash", "gix-imara-diff", "gix-index", "gix-object", "gix-path", "gix-quote", "gix-revision", "gix-revwalk", "gix-tempfile", "gix-trace", "gix-worktree", "nonempty", "pretty_assertions", "serde", "termtree", "thiserror", ] [[package]] name = "gix-object" version = "0.60.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a38075a95d7cc5df8afd38e72c617026c1456952207a4120a7f55a3fbf93b4d7" dependencies = [ "bstr", "gix-actor", "gix-date", "gix-features", "gix-hash", "gix-hashtable", "gix-utils", "gix-validate", "itoa", "serde", "smallvec", "thiserror", ] [[package]] name = "gix-packetline" version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "362246df440ee691699f0664cbf7006a6ece477db6734222be95e4198e5656e6" dependencies = [ "bstr", "faster-hex", "gix-trace", "thiserror", ] [[package]] name = "gix-path" version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "671a6059e8a4c1b7f406e24716499cefa3926e060876fb1959ef225efeee346e" dependencies = [ "bstr", "gix-trace", "gix-validate", "thiserror", ] [[package]] name = "gix-quote" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e97b73791a64bc0fa7dd2c5b3e551136115f97750b876ed1c952c7a7dbaf8be" dependencies = [ "bstr", "gix-error", "gix-utils", ] [[package]] name = "gix-revision" version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fb5288fac706d3ea3e4e2ba9ec38b78743b8c02f422e18cb342299cfd6ab7e8" dependencies = [ "bitflags", "bstr", "gix-commitgraph", "gix-date", "gix-error", "gix-hash", "gix-object", "gix-revwalk", "gix-trace", "nonempty", ] [[package]] name = "gix-revwalk" version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "313813706b073a12ff7f9b2896bf3e6504cdac7cfbc97b1920114724705069f0" dependencies = [ "gix-commitgraph", "gix-date", "gix-error", "gix-hash", "gix-hashtable", "gix-object", "smallvec", "thiserror", ] [[package]] name = "gix-tempfile" version = "23.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "691ea1e31435c7e7d4d04705ec9d1c0d9482c46b2acf512bc723939d8f0af7fb" dependencies = [ "dashmap", "gix-fs", "libc", "parking_lot", "tempfile", ] [[package]] name = "gix-trace" version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f23569e55f2ffaf958617353b9734a7d52a7c19c439eeaa5e3efc217fd2270e" [[package]] name = "gix-traverse" version = "0.57.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a14b7052c0786676c03e71fcfde7d7f0f8e8316e642b5cec6bb3998719b2ce5c" dependencies = [ "bitflags", "gix-commitgraph", "gix-date", "gix-hash", "gix-hashtable", "gix-object", "gix-revwalk", "smallvec", "thiserror", ] [[package]] name = "gix-utils" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e477b4f07a6e8da4ba791c53c858102959703c60d70f199932010d5b94adb2c" dependencies = [ "fastrand", "unicode-normalization", ] [[package]] name = "gix-validate" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e26ac2602b43eadfdca0560b81d3341944162a3c9f64ccdeef8fc501ad80dad5" dependencies = [ "bstr", ] [[package]] name = "gix-worktree" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d69955eb5e2910832f88d041964b809eee01dadd579237e0b55efec58fd406fd" dependencies = [ "bstr", "gix-attributes", "gix-fs", "gix-glob", "gix-hash", "gix-ignore", "gix-index", "gix-object", "gix-path", "gix-validate", ] [[package]] name = "hash32" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" dependencies = [ "byteorder", ] [[package]] name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] name = "hashbrown" version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "foldhash 0.1.5", ] [[package]] name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ "allocator-api2", "equivalent", "foldhash 0.2.0", ] [[package]] name = "hashbrown" version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" [[package]] name = "heapless" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" dependencies = [ "hash32", "stable_deref_trait", ] [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "id-arena" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" [[package]] name = "indexmap" version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", "hashbrown 0.17.0", "serde", "serde_core", ] [[package]] name = "itoa" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jiff" version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" dependencies = [ "jiff-static", "jiff-tzdb-platform", "log", "portable-atomic", "portable-atomic-util", "serde_core", "windows-sys", ] [[package]] name = "jiff-static" version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "jiff-tzdb" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c900ef84826f1338a557697dc8fc601df9ca9af4ac137c7fb61d4c6f2dfd3076" [[package]] name = "jiff-tzdb-platform" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" dependencies = [ "jiff-tzdb", ] [[package]] name = "kstring" version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "558bf9508a558512042d3095138b1f7b8fe90c5467d94f9f1da28b3731c5dbd1" dependencies = [ "static_assertions", ] [[package]] name = "leb128fmt" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" version = "0.2.185" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" [[package]] name = "libredox" version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" dependencies = [ "bitflags", "libc", "plain", "redox_syscall 0.7.4", ] [[package]] name = "linux-raw-sys" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litrs" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" [[package]] name = "lock_api" version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ "scopeguard", ] [[package]] name = "log" version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "memchr" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "memmap2" version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" dependencies = [ "libc", ] [[package]] name = "nonempty" version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9737e026353e5cd0736f98eddae28665118eb6f6600902a7f50db585621fecb6" [[package]] name = "once_cell" version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "parking_lot" version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", ] [[package]] name = "parking_lot_core" version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall 0.5.18", "smallvec", "windows-link", ] [[package]] name = "plain" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" [[package]] name = "portable-atomic" version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" dependencies = [ "portable-atomic", ] [[package]] name = "pretty_assertions" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" dependencies = [ "diff", "yansi", ] [[package]] name = "prettyplease" version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", "syn", ] [[package]] name = "proc-macro2" version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "prodash" version = "31.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "962200e2d7d551451297d9fdce85138374019ada198e30ea9ede38034e27604c" dependencies = [ "parking_lot", ] [[package]] name = "quote" version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "redox_syscall" version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ "bitflags", ] [[package]] name = "redox_syscall" version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f450ad9c3b1da563fb6948a8e0fb0fb9269711c9c73d9ea1de5058c79c8d643a" dependencies = [ "bitflags", ] [[package]] name = "regex-automata" version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" [[package]] name = "rustix" version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", "windows-sys", ] [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "semver" version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", "serde_derive", ] [[package]] name = "serde_core" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_json" version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", "serde", "serde_core", "zmij", ] [[package]] name = "sha1" version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", "digest", ] [[package]] name = "sha1-checked" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89f599ac0c323ebb1c6082821a54962b839832b03984598375bff3975b804423" dependencies = [ "digest", "sha1", ] [[package]] name = "shell-words" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" [[package]] name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" dependencies = [ "serde", ] [[package]] name = "stable_deref_trait" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "static_assertions" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "syn" version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "tempfile" version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", "getrandom", "once_cell", "rustix", "windows-sys", ] [[package]] name = "termtree" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4d1330fe7f7f872cd05165130b10602d667b205fd85be09be2814b115d4ced9" [[package]] name = "thiserror" version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tinyvec" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ "tinyvec_macros", ] [[package]] name = "tinyvec_macros" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "typenum" version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" [[package]] name = "unicode-bom" version = "2.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7eec5d1121208364f6793f7d2e222bf75a915c19557537745b195b253dd64217" [[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-normalization" version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" dependencies = [ "tinyvec", ] [[package]] name = "unicode-xid" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wasip2" version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ "wit-bindgen 0.57.1", ] [[package]] name = "wasip3" version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ "wit-bindgen 0.51.0", ] [[package]] name = "wasm-encoder" version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" dependencies = [ "leb128fmt", "wasmparser", ] [[package]] name = "wasm-metadata" version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ "anyhow", "indexmap", "wasm-encoder", "wasmparser", ] [[package]] name = "wasmparser" version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ "bitflags", "hashbrown 0.15.5", "indexmap", "semver", ] [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-sys" version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ "windows-link", ] [[package]] name = "wit-bindgen" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" dependencies = [ "wit-bindgen-rust-macro", ] [[package]] name = "wit-bindgen" version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" [[package]] name = "wit-bindgen-core" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" dependencies = [ "anyhow", "heck", "wit-parser", ] [[package]] name = "wit-bindgen-rust" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", "heck", "indexmap", "prettyplease", "syn", "wasm-metadata", "wit-bindgen-core", "wit-component", ] [[package]] name = "wit-bindgen-rust-macro" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" dependencies = [ "anyhow", "prettyplease", "proc-macro2", "quote", "syn", "wit-bindgen-core", "wit-bindgen-rust", ] [[package]] name = "wit-component" version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", "bitflags", "indexmap", "log", "serde", "serde_derive", "serde_json", "wasm-encoder", "wasm-metadata", "wasmparser", "wit-parser", ] [[package]] name = "wit-parser" version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" dependencies = [ "anyhow", "id-arena", "indexmap", "log", "semver", "serde", "serde_derive", "serde_json", "unicode-xid", "wasmparser", ] [[package]] name = "yansi" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" [[package]] name = "zmij" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" gix-merge-0.16.0/Cargo.toml0000644000000113751046102023000110330ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" rust-version = "1.82" name = "gix-merge" version = "0.16.0" authors = ["Sebastian Thiel "] build = false include = [ "/src/**/*", "/LICENSE-*", ] autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "A crate of the gitoxide project implementing merge algorithms" readme = false license = "MIT OR Apache-2.0" repository = "https://github.com/GitoxideLabs/gitoxide" [package.metadata.docs.rs] all-features = true features = [ "sha1", "document-features", ] [package.metadata.cargo-machete] ignored = [ "document-features", "serde", ] [features] serde = [ "dep:serde", "gix-hash/serde", "gix-object/serde", ] sha1 = ["gix-hash/sha1"] [lib] name = "gix_merge" path = "src/lib.rs" doctest = false [dependencies.bstr] version = "1.12.0" default-features = false [dependencies.document-features] version = "0.2.0" optional = true [dependencies.gix-command] version = "^0.9.0" [dependencies.gix-diff] version = "^0.63.0" features = ["blob"] default-features = false [dependencies.gix-filter] version = "^0.30.0" [dependencies.gix-fs] version = "^0.21.0" [dependencies.gix-hash] version = "^0.25.0" [dependencies.gix-index] version = "^0.51.0" [dependencies.gix-object] version = "^0.60.0" [dependencies.gix-path] version = "^0.12.0" [dependencies.gix-quote] version = "^0.7.1" [dependencies.gix-revision] version = "^0.45.0" features = ["merge_base"] default-features = false [dependencies.gix-revwalk] version = "^0.31.0" [dependencies.gix-tempfile] version = "^23.0.0" [dependencies.gix-trace] version = "^0.1.19" [dependencies.gix-worktree] version = "^0.52.0" features = ["attributes"] default-features = false [dependencies.imara-diff] version = "^0.2.1" package = "gix-imara-diff" [dependencies.nonempty] version = "0.12.0" [dependencies.serde] version = "1.0.114" features = ["derive"] optional = true default-features = false [dependencies.thiserror] version = "2.0.18" [dev-dependencies.arbitrary] version = "1.4.2" features = ["derive"] [dev-dependencies.pretty_assertions] version = "1.4.0" [dev-dependencies.termtree] version = "1.0.0" [lints.clippy] bool_to_int_with_if = "allow" borrow_as_ptr = "allow" cast_lossless = "allow" cast_possible_truncation = "allow" cast_possible_wrap = "allow" cast_precision_loss = "allow" cast_sign_loss = "allow" checked_conversions = "allow" copy_iterator = "allow" default_trait_access = "allow" doc_markdown = "allow" empty_docs = "allow" enum_glob_use = "allow" explicit_deref_methods = "allow" explicit_into_iter_loop = "allow" explicit_iter_loop = "allow" filter_map_next = "allow" fn_params_excessive_bools = "allow" from_iter_instead_of_collect = "allow" if_not_else = "allow" ignored_unit_patterns = "allow" implicit_clone = "allow" inconsistent_struct_constructor = "allow" inefficient_to_string = "allow" inline_always = "allow" items_after_statements = "allow" iter_not_returning_iterator = "allow" iter_without_into_iter = "allow" large_enum_variant = "allow" large_stack_arrays = "allow" manual_assert = "allow" manual_is_variant_and = "allow" manual_let_else = "allow" manual_string_new = "allow" many_single_char_names = "allow" match_bool = "allow" match_same_arms = "allow" match_wild_err_arm = "allow" match_wildcard_for_single_variants = "allow" missing_errors_doc = "allow" missing_panics_doc = "allow" module_name_repetitions = "allow" must_use_candidate = "allow" mut_mut = "allow" naive_bytecount = "allow" needless_continue = "allow" needless_for_each = "allow" needless_pass_by_value = "allow" needless_raw_string_hashes = "allow" no_effect_underscore_binding = "allow" option_option = "allow" range_plus_one = "allow" redundant_else = "allow" result_large_err = "allow" return_self_not_must_use = "allow" should_panic_without_expect = "allow" similar_names = "allow" single_match_else = "allow" stable_sort_primitive = "allow" struct_excessive_bools = "allow" struct_field_names = "allow" too_long_first_doc_paragraph = "allow" too_many_lines = "allow" transmute_ptr_to_ptr = "allow" trivially_copy_pass_by_ref = "allow" unnecessary_join = "allow" unnecessary_wraps = "allow" unreadable_literal = "allow" unused_self = "allow" used_underscore_binding = "allow" wildcard_imports = "allow" [lints.clippy.pedantic] level = "warn" priority = -1 [lints.rust] gix-merge-0.16.0/Cargo.toml.orig000064400000000000000000000050521046102023000144650ustar 00000000000000[package] name = "gix-merge" version = "0.16.0" repository = "https://github.com/GitoxideLabs/gitoxide" license = "MIT OR Apache-2.0" description = "A crate of the gitoxide project implementing merge algorithms" authors = ["Sebastian Thiel "] edition = "2021" rust-version = "1.82" include = ["/src/**/*", "/LICENSE-*"] [lints] workspace = true [lib] doctest = false [features] ## Enable support for the SHA-1 hash by enabling the respective feature in the `gix-hash` crate. sha1 = ["gix-hash/sha1"] ## Data structures implement `serde::Serialize` and `serde::Deserialize`. serde = ["dep:serde", "gix-hash/serde", "gix-object/serde"] [dependencies] gix-hash = { version = "^0.25.0", path = "../gix-hash" } gix-object = { version = "^0.60.0", path = "../gix-object" } gix-filter = { version = "^0.30.0", path = "../gix-filter" } gix-worktree = { version = "^0.52.0", path = "../gix-worktree", default-features = false, features = ["attributes"] } gix-command = { version = "^0.9.0", path = "../gix-command" } gix-path = { version = "^0.12.0", path = "../gix-path" } gix-fs = { version = "^0.21.0", path = "../gix-fs" } gix-tempfile = { version = "^23.0.0", path = "../gix-tempfile" } gix-trace = { version = "^0.1.19", path = "../gix-trace" } gix-quote = { version = "^0.7.1", path = "../gix-quote" } gix-revision = { version = "^0.45.0", path = "../gix-revision", default-features = false, features = ["merge_base"] } gix-revwalk = { version = "^0.31.0", path = "../gix-revwalk" } gix-diff = { version = "^0.63.0", path = "../gix-diff", default-features = false, features = ["blob"] } gix-index = { version = "^0.51.0", path = "../gix-index" } imara-diff = { package = "gix-imara-diff", version = "^0.2.1", path = "../gix-imara-diff" } thiserror = "2.0.18" bstr = { version = "1.12.0", default-features = false } nonempty = "0.12.0" serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"] } document-features = { version = "0.2.0", optional = true } [dev-dependencies] gix-testtools = { path = "../tests/tools" } gix-odb = { path = "../gix-odb" } gix-utils = { path = "../gix-utils" } termtree = "1.0.0" pretty_assertions = "1.4.0" arbitrary = { version = "1.4.2", features = ["derive"] } [package.metadata.docs.rs] all-features = true features = ["sha1", "document-features"] [package.metadata.cargo-machete] ignored = [ # Doc-only macro dependency used under `all(doc, feature = "document-features")`. "document-features", # Optional `serde` feature is retained for downstream API compatibility. "serde", ] gix-merge-0.16.0/LICENSE-APACHE000064400000000000000000000236761046102023000135360ustar 00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS gix-merge-0.16.0/LICENSE-MIT000064400000000000000000000017771046102023000132440ustar 00000000000000Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. gix-merge-0.16.0/src/blob/builtin_driver/binary.rs000064400000000000000000000031751046102023000201620ustar 00000000000000/// What to do when having to pick a side to resolve a conflict. #[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] pub enum ResolveWith { /// Chose the ancestor to resolve a conflict. Ancestor, /// Chose our side to resolve a conflict. Ours, /// Chose their side to resolve a conflict. Theirs, } /// Tell the caller of [`merge()`](function::merge) which side was picked. #[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] pub enum Pick { /// Chose the ancestor. Ancestor, /// Chose our side. Ours, /// Chose their side. Theirs, } pub(super) mod function { use crate::blob::{ builtin_driver::binary::{Pick, ResolveWith}, Resolution, }; /// As this algorithm doesn't look at the actual data, it returns a choice solely based on logic. /// This also means that the caller has to assure this only gets called if the input *doesn't* match. /// /// It always results in a conflict with `current` being picked unless `on_conflict` is not `None`, /// which is when we always return [`Resolution::CompleteWithAutoResolvedConflict`]. pub fn merge(on_conflict: Option) -> (Pick, Resolution) { match on_conflict { None => (Pick::Ours, Resolution::Conflict), Some(resolve) => ( match resolve { ResolveWith::Ours => Pick::Ours, ResolveWith::Theirs => Pick::Theirs, ResolveWith::Ancestor => Pick::Ancestor, }, Resolution::CompleteWithAutoResolvedConflict, ), } } } gix-merge-0.16.0/src/blob/builtin_driver/mod.rs000064400000000000000000000014621046102023000174520ustar 00000000000000use crate::blob::BuiltinDriver; impl BuiltinDriver { /// Return the name of this instance. pub fn as_str(&self) -> &str { match self { BuiltinDriver::Text => "text", BuiltinDriver::Binary => "binary", BuiltinDriver::Union => "union", } } /// Get all available built-in drivers. pub fn all() -> &'static [Self] { &[BuiltinDriver::Text, BuiltinDriver::Binary, BuiltinDriver::Union] } /// Try to match one of our variants to `name`, case-sensitive, and return its instance. pub fn by_name(name: &str) -> Option { Self::all().iter().find(|variant| variant.as_str() == name).copied() } } /// pub mod binary; pub use binary::function::merge as binary; /// pub mod text; pub use text::function::merge as text; gix-merge-0.16.0/src/blob/builtin_driver/text/function.rs000064400000000000000000000343461046102023000215130ustar 00000000000000use std::ops::Range; use crate::blob::{ builtin_driver::text::{ utils::{ assure_ends_with_nl, collect_hunks, contains_lines, detect_line_ending, detect_line_ending_or_nl, fill_ancestor, hunks_differ_in_diff3, take_intersecting, tokens, write_ancestor, write_conflict_marker, write_hunks, zealously_contract_hunks, Hunk, Side, }, Conflict, ConflictStyle, Labels, Merge, Options, }, Resolution, }; impl<'input, 'data> Merge<'input, 'data> { /// Prepare merge state for `current`, `ancestor`, and `other` using `diff_algorithm`. /// /// This computes the hunk structure once so it can be rendered multiple times with /// [`Merge::run()`], which is useful when experimenting with multiple conflict styles /// for the same input triplet. /// /// The returned [`Merge`] keeps a reference to the provided `input`, which guarantees /// that subsequent calls to [`Merge::run()`] use the exact same interner state. pub fn new( input: &'input mut imara_diff::InternedInput<&'data [u8]>, current: &'data [u8], ancestor: &'data [u8], other: &'data [u8], diff_algorithm: imara_diff::Algorithm, ) -> Merge<'input, 'data> { input.update_before(tokens(ancestor)); input.update_after(tokens(current)); let hunks = collect_hunks(diff_algorithm, input, Side::Current, Vec::new()); let current_tokens = std::mem::take(&mut input.after); input.update_after(tokens(other)); let mut hunks = collect_hunks(diff_algorithm, input, Side::Other, hunks); hunks.sort_by_key(|a| a.before.start); Merge { input, current_tokens, hunks, } } /// Merge `current` and `other` with `ancestor` as base using to `conflict` /// as strategy. /// /// Use `labels` to annotate conflict sections. /// /// Place the merged result in `out` (cleared before use) and return the resolution. pub fn run( &self, out: &mut Vec, Labels { ancestor: ancestor_label, current: current_label, other: other_label, }: Labels<'_>, conflict: Conflict, ) -> Resolution { out.clear(); let input = self.input; let current_tokens = &self.current_tokens; if self.hunks.is_empty() { write_ancestor(input, 0, input.before.len(), out); return Resolution::Complete; } let mut hunks = self.hunks.iter().cloned().peekable(); let mut intersecting = Vec::new(); let mut ancestor_integrated_until = 0; let mut resolution = Resolution::Complete; let mut current_hunks = Vec::with_capacity(2); while take_intersecting(&mut hunks, &mut current_hunks, &mut intersecting).is_some() { if intersecting.is_empty() { let hunk = current_hunks.pop().expect("always pushed during intersection check"); write_ancestor(input, ancestor_integrated_until, hunk.before.start as usize, out); ancestor_integrated_until = hunk.before.end; write_hunks(std::slice::from_ref(&hunk), input, current_tokens, out); continue; } let filled_hunks_side = current_hunks.first().expect("at least one hunk").side; { let filled_hunks_range = before_range_from_hunks(¤t_hunks); let intersecting_range = before_range_from_hunks(&intersecting); let extended_range = filled_hunks_range.start..intersecting_range.end.max(filled_hunks_range.end); fill_ancestor(&extended_range, &mut current_hunks); fill_ancestor(&extended_range, &mut intersecting); } match conflict { Conflict::Keep { style, marker_size } => { let marker_size = marker_size.get(); let (hunks_front_and_back, num_hunks_front) = match style { ConflictStyle::Merge | ConflictStyle::ZealousDiff3 => { zealously_contract_hunks(&mut current_hunks, &mut intersecting, input, current_tokens) } ConflictStyle::Diff3 => (Vec::new(), 0), }; let (our_hunks, their_hunks) = match filled_hunks_side { Side::Current => (¤t_hunks, &intersecting), Side::Other => (&intersecting, ¤t_hunks), Side::Ancestor => { unreachable!("initial hunks are never ancestors") } }; let (front_hunks, back_hunks) = hunks_front_and_back.split_at(num_hunks_front); let first_hunk = first_hunk(front_hunks, our_hunks, their_hunks, back_hunks); let last_hunk = last_hunk(front_hunks, our_hunks, their_hunks, back_hunks); write_ancestor(input, ancestor_integrated_until, first_hunk.before.start as usize, out); write_hunks(front_hunks, input, current_tokens, out); // DEVIATION: this makes tests (mostly) pass, but probably is very different from what Git does. let hunk_storage; let nl = detect_line_ending( if front_hunks.is_empty() { hunk_storage = Hunk { before: ancestor_integrated_until..first_hunk.before.start, after: Default::default(), side: Side::Ancestor, }; std::slice::from_ref(&hunk_storage) } else { front_hunks }, input, current_tokens, ) .or_else(|| detect_line_ending(our_hunks, input, current_tokens)) .unwrap_or(b"\n".into()); match style { ConflictStyle::Merge => { if contains_lines(our_hunks) || contains_lines(their_hunks) { resolution = Resolution::Conflict; write_conflict_marker(out, b'<', current_label, marker_size, nl); write_hunks(our_hunks, input, current_tokens, out); write_conflict_marker(out, b'=', None, marker_size, nl); write_hunks(their_hunks, input, current_tokens, out); write_conflict_marker(out, b'>', other_label, marker_size, nl); } } ConflictStyle::Diff3 | ConflictStyle::ZealousDiff3 => { if contains_lines(our_hunks) || contains_lines(their_hunks) { if hunks_differ_in_diff3(style, our_hunks, their_hunks, input, current_tokens) { resolution = Resolution::Conflict; write_conflict_marker(out, b'<', current_label, marker_size, nl); write_hunks(our_hunks, input, current_tokens, out); let ancestor_hunk = Hunk { before: first_hunk.before.start..last_hunk.before.end, after: Default::default(), side: Side::Ancestor, }; let ancestor_hunk = std::slice::from_ref(&ancestor_hunk); let ancestor_nl = detect_line_ending_or_nl(ancestor_hunk, input, current_tokens); write_conflict_marker(out, b'|', ancestor_label, marker_size, ancestor_nl); write_hunks(ancestor_hunk, input, current_tokens, out); write_conflict_marker(out, b'=', None, marker_size, nl); write_hunks(their_hunks, input, current_tokens, out); write_conflict_marker(out, b'>', other_label, marker_size, nl); } else { write_hunks(our_hunks, input, current_tokens, out); } } } } write_hunks(back_hunks, input, current_tokens, out); ancestor_integrated_until = last_hunk.before.end; } Conflict::ResolveWithOurs | Conflict::ResolveWithTheirs => { let (our_hunks, their_hunks) = match filled_hunks_side { Side::Current => (¤t_hunks, &intersecting), Side::Other => (&intersecting, ¤t_hunks), Side::Ancestor => { unreachable!("initial hunks are never ancestors") } }; if hunks_differ_in_diff3(ConflictStyle::Diff3, our_hunks, their_hunks, input, current_tokens) { resolution = Resolution::CompleteWithAutoResolvedConflict; } let hunks_to_write = if conflict == Conflict::ResolveWithOurs { our_hunks } else { their_hunks }; if let Some(first_hunk) = hunks_to_write.first() { write_ancestor(input, ancestor_integrated_until, first_hunk.before.start as usize, out); } write_hunks(hunks_to_write, input, current_tokens, out); if let Some(last_hunk) = hunks_to_write.last() { ancestor_integrated_until = last_hunk.before.end; } } Conflict::ResolveWithUnion => { let (hunks_front_and_back, num_hunks_front) = zealously_contract_hunks(&mut current_hunks, &mut intersecting, input, current_tokens); let (our_hunks, their_hunks) = match filled_hunks_side { Side::Current => (¤t_hunks, &intersecting), Side::Other => (&intersecting, ¤t_hunks), Side::Ancestor => { unreachable!("initial hunks are never ancestors") } }; if hunks_differ_in_diff3(ConflictStyle::Diff3, our_hunks, their_hunks, input, current_tokens) { resolution = Resolution::CompleteWithAutoResolvedConflict; } let (front_hunks, back_hunks) = hunks_front_and_back.split_at(num_hunks_front); let first_hunk = first_hunk(front_hunks, our_hunks, their_hunks, back_hunks); write_ancestor(input, ancestor_integrated_until, first_hunk.before.start as usize, out); write_hunks(front_hunks, input, current_tokens, out); assure_ends_with_nl(out, detect_line_ending_or_nl(front_hunks, input, current_tokens)); write_hunks(our_hunks, input, current_tokens, out); assure_ends_with_nl(out, detect_line_ending_or_nl(our_hunks, input, current_tokens)); write_hunks(their_hunks, input, current_tokens, out); if !back_hunks.is_empty() { assure_ends_with_nl(out, detect_line_ending_or_nl(their_hunks, input, current_tokens)); } write_hunks(back_hunks, input, current_tokens, out); let last_hunk = last_hunk(front_hunks, our_hunks, their_hunks, back_hunks); ancestor_integrated_until = last_hunk.before.end; } } } write_ancestor(input, ancestor_integrated_until, input.before.len(), out); resolution } } /// Merge `current` and `other` with `ancestor` as base according to `opts`. /// /// Use `labels` to annotate conflict sections. /// /// `input` is for reusing memory for lists of tokens, but note that it grows indefinitely /// while tokens for `current`, `ancestor` and `other` are added. /// Place the merged result in `out` (cleared before use) and return the resolution. /// /// # Important /// /// *The caller* is responsible for clearing `input`, otherwise tokens will accumulate. /// This idea is to save time if the input is known to be very similar. #[allow(clippy::too_many_arguments)] pub fn merge<'a>( out: &mut Vec, input: &mut imara_diff::InternedInput<&'a [u8]>, Labels { ancestor: ancestor_label, current: current_label, other: other_label, }: Labels<'_>, current: &'a [u8], ancestor: &'a [u8], other: &'a [u8], Options { diff_algorithm, conflict, }: Options, ) -> Resolution { out.clear(); let merge = Merge::new(input, current, ancestor, other, diff_algorithm); merge.run( out, Labels { ancestor: ancestor_label, current: current_label, other: other_label, }, conflict, ) } fn first_hunk<'a>(front: &'a [Hunk], ours: &'a [Hunk], theirs: &'a [Hunk], back: &'a [Hunk]) -> &'a Hunk { front .first() .or(ours.first()) .or(theirs.first()) .or(back.first()) .expect("at least one hunk - we aborted if there are none anywhere") } /// Note that last-hunk could be [`first_hunk()`], so the hunk must only be used accordingly. fn last_hunk<'a>(front: &'a [Hunk], ours: &'a [Hunk], theirs: &'a [Hunk], back: &'a [Hunk]) -> &'a Hunk { back.last() .or(theirs.last()) .or(ours.last()) .or(front.last()) .expect("at least one hunk - we aborted if there are none anywhere") } fn before_range_from_hunks(hunks: &[Hunk]) -> Range { hunks .first() .zip(hunks.last()) .map(|(f, l)| f.before.start..l.before.end) .expect("at least one entry") } gix-merge-0.16.0/src/blob/builtin_driver/text/mod.rs000064400000000000000000000112451046102023000204360ustar 00000000000000use std::num::NonZeroU8; use bstr::BStr; /// The way the built-in [text driver](crate::blob::BuiltinDriver::Text) will express /// merge conflicts in the resulting file. #[derive(Default, Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] pub enum ConflictStyle { /// Only show the zealously minified conflicting lines of the local changes and the incoming (other) changes, /// hiding the base version entirely. /// /// ```text /// line1-changed-by-both /// <<<<<<< local /// line2-to-be-changed-in-incoming /// ======= /// line2-changed /// >>>>>>> incoming /// ``` #[default] Merge, /// Show non-minimized hunks of local changes, the base, and the incoming (other) changes. /// /// This mode does not hide any information. /// /// ```text /// <<<<<<< local /// line1-changed-by-both /// line2-to-be-changed-in-incoming /// ||||||| 9a8d80c /// line1-to-be-changed-by-both /// line2-to-be-changed-in-incoming /// ======= /// line1-changed-by-both /// line2-changed /// >>>>>>> incoming /// ``` Diff3, /// Like [`Diff3](Self::Diff3), but will show *minimized* hunks of local change and the incoming (other) changes, /// as well as non-minimized hunks of the base. /// /// ```text /// line1-changed-by-both /// <<<<<<< local /// line2-to-be-changed-in-incoming /// ||||||| 9a8d80c /// line1-to-be-changed-by-both /// line2-to-be-changed-in-incoming /// ======= /// line2-changed /// >>>>>>> incoming /// ``` ZealousDiff3, } /// The set of labels to annotate conflict markers with. /// /// That way it becomes clearer where the content of conflicts are originating from. #[derive(Default, Copy, Clone, Debug, Eq, PartialEq)] pub struct Labels<'a> { /// The label for the common *ancestor*. pub ancestor: Option<&'a BStr>, /// The label for the *current* (or *our*) side. pub current: Option<&'a BStr>, /// The label for the *other* (or *their*) side. pub other: Option<&'a BStr>, } /// Options for the builtin [text driver](crate::blob::BuiltinDriver::Text). #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub struct Options { /// Determine of the diff will be performed. /// Defaults to [`imara_diff::Algorithm::Myers`]. pub diff_algorithm: imara_diff::Algorithm, /// Decide what to do to automatically resolve conflicts, or to keep them. pub conflict: Conflict, } impl Default for Options { fn default() -> Self { Options { conflict: Default::default(), diff_algorithm: imara_diff::Algorithm::Myers, } } } /// What to do to resolve a conflict. #[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] pub enum Conflict { /// Keep the conflict by marking it in the source file. Keep { /// How to visualize conflicts in merged files. style: ConflictStyle, /// The amount of markers to draw, defaults to 7, i.e. `<<<<<<<` marker_size: NonZeroU8, }, /// Chose our side to resolve a conflict. ResolveWithOurs, /// Chose their side to resolve a conflict. ResolveWithTheirs, /// Place our and their lines one after another, in any order ResolveWithUnion, } impl Conflict { /// The amount of conflict marker characters to print by default. // TODO: use NonZeroU8::new().unwrap() here once the MSRV supports it. pub const DEFAULT_MARKER_SIZE: u8 = 7; /// The amount of conflict markers to print if this instance contains them, or `None` otherwise pub fn marker_size(&self) -> Option { match self { Conflict::Keep { marker_size, .. } => Some(marker_size.get()), Conflict::ResolveWithOurs | Conflict::ResolveWithTheirs | Conflict::ResolveWithUnion => None, } } } impl Default for Conflict { fn default() -> Self { Conflict::Keep { style: Default::default(), marker_size: Conflict::DEFAULT_MARKER_SIZE.try_into().unwrap(), } } } /// /// Prepared merge state for rendering the same merge with multiple conflict strategies. /// /// Construct this with [`Merge::new()`] to compute the expensive diff state once, then call /// [`Merge::run()`] repeatedly with different [`Conflict`] values. It keeps a reference to the /// [`imara_diff::InternedInput`] used to construct it so rendering cannot accidentally use a /// different interner than the one the stored tokens were derived from. #[derive(Clone)] pub struct Merge<'input, 'data> { input: &'input imara_diff::InternedInput<&'data [u8]>, current_tokens: Vec, hunks: Vec, } pub(super) mod function; mod utils; gix-merge-0.16.0/src/blob/builtin_driver/text/utils.rs000064400000000000000000000421051046102023000210160ustar 00000000000000use std::{iter::Peekable, ops::Range}; use bstr::{BStr, ByteSlice, ByteVec}; use crate::blob::builtin_driver::text::ConflictStyle; /// Used only when `diff3` is the conflict style as `zdiff3` automatically reduces hunks into nothing. /// Here we check if all hunks are the same. pub fn hunks_differ_in_diff3( style: ConflictStyle, a: &[Hunk], b: &[Hunk], input: &imara_diff::InternedInput<&[u8]>, current_tokens: &[imara_diff::Token], ) -> bool { if style != ConflictStyle::Diff3 { return true; } let tokens_for_hunk = |hunk: &Hunk| -> &[imara_diff::Token] { &tokens_for_side(hunk.side, input, current_tokens)[hunk.after.start as usize..hunk.after.end as usize] }; a.iter() .flat_map(tokens_for_hunk) .ne(b.iter().flat_map(tokens_for_hunk)) } pub fn contains_lines(hunks: &[Hunk]) -> bool { hunks.iter().any(|h| !h.after.is_empty()) } /// ## Deviation /// /// This implementation definitely isn't the same as in Git, primarily because it seemed impossible /// to understand what's going on there without investing more time than it seemed worth. pub fn detect_line_ending( hunks: &[Hunk], input: &imara_diff::InternedInput<&[u8]>, current_tokens: &[imara_diff::Token], ) -> Option<&'static BStr> { fn is_eol_crlf( hunks: &[Hunk], input: &imara_diff::InternedInput<&[u8]>, current_tokens: &[imara_diff::Token], ) -> Option { let (range, side) = hunks.iter().rev().find_map(|h| { (!h.after.is_empty()) .then_some((&h.after, h.side)) .or((!h.before.is_empty()).then_some((&h.before, Side::Ancestor))) })?; let tokens = tokens_for_side(side, input, current_tokens); { let last_line = tokens .get(range.end as usize - 1) .map(|token| &input.interner[*token])?; if last_line.last() == Some(&b'\n') { return last_line.get(last_line.len().checked_sub(2)?).map(|c| *c == b'\r'); } } let second_to_last_line = tokens .get(range.end.checked_sub(2)? as usize) .map(|token| &input.interner[*token])?; second_to_last_line .get(second_to_last_line.len().checked_sub(2)?) .map(|c| *c == b'\r') } is_eol_crlf(hunks, input, current_tokens).map(|is_crlf| if is_crlf { b"\r\n".into() } else { b"\n".into() }) } pub fn detect_line_ending_or_nl( hunks: &[Hunk], input: &imara_diff::InternedInput<&[u8]>, current_tokens: &[imara_diff::Token], ) -> &'static BStr { detect_line_ending(hunks, input, current_tokens).unwrap_or(b"\n".into()) } fn tokens_for_side<'a>( side: Side, input: &'a imara_diff::InternedInput<&[u8]>, current_tokens: &'a [imara_diff::Token], ) -> &'a [imara_diff::Token] { match side { Side::Current => current_tokens, Side::Other => &input.after, Side::Ancestor => &input.before, } } pub fn assure_ends_with_nl(out: &mut Vec, nl: &BStr) { if !out.is_empty() && !out.ends_with(b"\n") { out.push_str(nl); } } pub fn write_conflict_marker(out: &mut Vec, marker: u8, label: Option<&BStr>, marker_size: u8, nl: &BStr) { assure_ends_with_nl(out, nl); out.extend(std::iter::repeat_n(marker, marker_size as usize)); if let Some(label) = label { out.push(b' '); out.extend_from_slice(label); } out.push_str(nl); } pub fn write_ancestor(input: &imara_diff::InternedInput<&[u8]>, from: u32, to: usize, out: &mut Vec) { if to < from as usize { return; } if let Some(tokens) = input.before.get(from as usize..to) { write_tokens(&input.interner, tokens, out); } } /// Look at all hunks in `in_out` and fill in the ancestor in the range of `ancestor_range`. /// This is all based on knowing the ranges are sequences of tokens. pub fn fill_ancestor(Range { start, end }: &Range, in_out: &mut Vec) { fn is_nonzero(num: &u32) -> bool { *num > 0 } if in_out.is_empty() { return; } let first = &in_out[0]; let mut first_idx = 0; if let Some(lines_to_add) = first.before.start.checked_sub(*start).filter(is_nonzero) { in_out.insert(0, ancestor_hunk(*start, lines_to_add)); first_idx += 1; } let mut added_hunks = false; for (idx, next_idx) in (first_idx..in_out.len()).map(|idx| (idx, idx + 1)) { let Some(next_hunk) = in_out.get(next_idx) else { break }; let hunk = &in_out[idx]; if let Some(lines_to_add) = next_hunk.before.start.checked_sub(hunk.before.end).filter(is_nonzero) { in_out.push(ancestor_hunk(hunk.before.end, lines_to_add)); added_hunks = true; } } let in_out_len = in_out.len(); if added_hunks { in_out[first_idx..in_out_len].sort_by_key(|hunk| hunk.before.start); } let last = &in_out[in_out_len - 1]; if let Some(lines_to_add) = end.checked_sub(last.before.end).filter(is_nonzero) { in_out.push(ancestor_hunk(last.before.end, lines_to_add)); } } fn ancestor_hunk(start: u32, num_lines: u32) -> Hunk { let range = start..start + num_lines; Hunk { before: range.clone(), after: range, side: Side::Ancestor, } } /// Reduce the area of `a_hunks` and the hunks in `b_hunks` so that only those lines that are /// actually different remain. Note that we have to compare the resolved values, not only the tokens, /// so `current_tokens` is expected to be known to the `input` (and its `interner`). /// Hunks from all input arrays maybe removed in the process from the front and back, in case they /// are entirely equal to each other. /// Note also that `a_hunks` and `b_hunks` are treated to be consecutive, so [`fill_ancestor()`] must /// have been called beforehand, and are assumed to cover the same space in the ancestor buffer. /// /// Return a new vector of all the hunks that were removed from front and back, with partial hunks inserted, /// along with the amount of hunks that go front, with the remaining going towards the back. #[must_use] pub fn zealously_contract_hunks( a_hunks: &mut Vec, b_hunks: &mut Vec, input: &imara_diff::InternedInput<&[u8]>, current_tokens: &[imara_diff::Token], ) -> (Vec, usize) { let line_content = |token_idx: u32, side: Side| { let tokens = match side { Side::Current => current_tokens, Side::Other => &input.after, Side::Ancestor => &input.before, }; &input.interner[tokens[token_idx as usize]] }; let (mut last_a_hunk_idx, mut last_b_hunk_idx) = (0, 0); let (mut out, hunks_in_front) = { let (mut remove_leading_a_hunks_from, mut remove_leading_b_hunks_from) = (None, None); let (mut a_hunk_token_equal_till, mut b_hunk_token_equal_till) = (None, None); for ((a_token_idx, a_hunk_idx, a_hunk_side), (b_token_idx, b_hunk_idx, b_hunk_side)) in iterate_hunks(a_hunks).zip(iterate_hunks(b_hunks)) { let a_line = line_content(a_token_idx, a_hunk_side).as_bstr(); let b_line = line_content(b_token_idx, b_hunk_side).as_bstr(); if last_a_hunk_idx != a_hunk_idx { a_hunk_token_equal_till = None; last_a_hunk_idx = a_hunk_idx; } if last_b_hunk_idx != b_hunk_idx { b_hunk_token_equal_till = None; last_b_hunk_idx = b_hunk_idx; } if a_line == b_line { (remove_leading_a_hunks_from, remove_leading_b_hunks_from) = (Some(a_hunk_idx), Some(b_hunk_idx)); (a_hunk_token_equal_till, b_hunk_token_equal_till) = (Some(a_token_idx), Some(b_token_idx)); } else { break; } } let mut out = Vec::with_capacity(remove_leading_a_hunks_from.unwrap_or_else(|| { if a_hunk_token_equal_till.is_some() { 1 } else { 0 } })); truncate_hunks_from_from_front( a_hunks, remove_leading_a_hunks_from, a_hunk_token_equal_till, Some(&mut out), ); truncate_hunks_from_from_front(b_hunks, remove_leading_b_hunks_from, b_hunk_token_equal_till, None); let hunks_in_front = out.len(); (out, hunks_in_front) }; (last_a_hunk_idx, last_b_hunk_idx) = (0, 0); { let (mut remove_trailing_a_hunks_from, mut remove_trailing_b_hunks_from) = (None, None); let (mut a_hunk_token_equal_from, mut b_hunk_token_equal_from) = (None, None); for ((a_token_idx, a_hunk_idx, a_hunk_side), (b_token_idx, b_hunk_idx, b_hunk_side)) in iterate_hunks_rev(a_hunks).zip(iterate_hunks_rev(b_hunks)) { let a_line = line_content(a_token_idx, a_hunk_side).as_bstr(); let b_line = line_content(b_token_idx, b_hunk_side).as_bstr(); if last_a_hunk_idx != a_hunk_idx { a_hunk_token_equal_from = None; last_a_hunk_idx = a_hunk_idx; } if last_b_hunk_idx != b_hunk_idx { b_hunk_token_equal_from = None; last_b_hunk_idx = b_hunk_idx; } if a_line == b_line { (remove_trailing_a_hunks_from, remove_trailing_b_hunks_from) = (Some(a_hunk_idx), Some(b_hunk_idx)); (a_hunk_token_equal_from, b_hunk_token_equal_from) = (Some(a_token_idx), Some(b_token_idx)); } else { break; } } truncate_hunks_from_from_back( a_hunks, remove_trailing_a_hunks_from, a_hunk_token_equal_from, Some(&mut out), ); truncate_hunks_from_from_back(b_hunks, remove_trailing_b_hunks_from, b_hunk_token_equal_from, None); } (out, hunks_in_front) } fn range_by_side(hunk: &mut Hunk) -> &mut Range { match hunk.side { Side::Current | Side::Other => &mut hunk.after, Side::Ancestor => &mut hunk.before, } } fn truncate_hunks_from_from_front( hunks: &mut Vec, hunks_to_remove_until_idx: Option, hunk_token_equal_till: Option, mut out_hunks: Option<&mut Vec>, ) { let Some(hunks_to_remove_until_idx) = hunks_to_remove_until_idx else { assert!(hunk_token_equal_till.is_none()); return; }; let mut last_index_to_remove = Some(hunks_to_remove_until_idx); let hunk = &mut hunks[hunks_to_remove_until_idx]; let range = range_by_side(hunk); if let Some(hunk_token_equal_till) = hunk_token_equal_till { let orig_start = range.start; let new_start = hunk_token_equal_till + 1; range.start = new_start; if Range::::is_empty(range) { range.start = orig_start; } else if let Some(out) = out_hunks.as_deref_mut() { last_index_to_remove = hunks_to_remove_until_idx.checked_sub(1); let mut removed_hunk = hunk.clone(); let new_range = range_by_side(&mut removed_hunk); new_range.start = orig_start; new_range.end = new_start; out.push(removed_hunk); } else { last_index_to_remove = hunks_to_remove_until_idx.checked_sub(1); } } if let Some(last_index_to_remove) = last_index_to_remove { let mut current_idx = 0; hunks.retain(|hunk| { if current_idx > last_index_to_remove { true } else { current_idx += 1; if let Some(out) = out_hunks.as_deref_mut() { out.push(hunk.clone()); } false } }); } } fn truncate_hunks_from_from_back( hunks: &mut Vec, remove_trailing_hunks_from_idx: Option, hunk_token_equal_from: Option, mut out_hunks: Option<&mut Vec>, ) { let Some(mut remove_trailing_hunks_from_idx) = remove_trailing_hunks_from_idx else { assert!(hunk_token_equal_from.is_none()); return; }; let hunk = &mut hunks[remove_trailing_hunks_from_idx]; let range = range_by_side(hunk); if let Some(hunk_token_equal_from) = hunk_token_equal_from { let orig_end = range.end; let new_end = hunk_token_equal_from; range.end = new_end; if Range::::is_empty(range) { range.end = orig_end; } else if let Some(out) = out_hunks.as_deref_mut() { remove_trailing_hunks_from_idx += 1; let mut removed_hunk = hunk.clone(); let new_range = range_by_side(&mut removed_hunk); new_range.start = new_end; new_range.end = orig_end; out.push(removed_hunk); } else { remove_trailing_hunks_from_idx += 1; } } if let Some(out) = out_hunks { out.extend_from_slice(&hunks[remove_trailing_hunks_from_idx..]); } hunks.truncate(remove_trailing_hunks_from_idx); } /// Return an iterator over `(token_idx, hunk_idx, hunk_side)` from `hunks`. fn iterate_hunks(hunks: &[Hunk]) -> impl Iterator + '_ { hunks.iter().enumerate().flat_map(|(hunk_idx, hunk)| { match hunk.side { Side::Current | Side::Other => &hunk.after, Side::Ancestor => &hunk.before, } .clone() .map(move |idx| (idx, hunk_idx, hunk.side)) }) } /// Return a reverse iterator over `(token_idx, hunk_idx, hunk_side)` from `hunks`. fn iterate_hunks_rev(hunks: &[Hunk]) -> impl Iterator + '_ { hunks.iter().enumerate().rev().flat_map(|(hunk_idx, hunk)| { match hunk.side { Side::Current | Side::Other => &hunk.after, Side::Ancestor => &hunk.before, } .clone() .rev() .map(move |idx| (idx, hunk_idx, hunk.side)) }) } pub fn write_hunks( hunks: &[Hunk], input: &imara_diff::InternedInput<&[u8]>, current_tokens: &[imara_diff::Token], out: &mut Vec, ) { for hunk in hunks { let (tokens, range) = match hunk.side { Side::Current => (current_tokens, &hunk.after), Side::Other => (input.after.as_slice(), &hunk.after), Side::Ancestor => (input.before.as_slice(), &hunk.before), }; write_tokens(&input.interner, &tokens[usize_range(range)], out); } } fn usize_range(range: &Range) -> Range { range.start as usize..range.end as usize } fn write_tokens(interner: &imara_diff::Interner<&[u8]>, tokens: &[imara_diff::Token], out: &mut Vec) { for token in tokens { out.extend_from_slice(interner[*token]); } } /// Find all hunks in `iter` which aren't from the same side as `hunk` and intersect with it. /// Also put `hunk` into `input` so it's the first item, and possibly put more hunks of the side of `hunk` so /// `iter` doesn't have any overlapping hunks left. /// Return `Some` if `intersecting` is non-empty after the operation, indicating overlapping hunks were found. pub fn take_intersecting( iter: &mut Peekable>, input: &mut Vec, intersecting: &mut Vec, ) -> Option<()> { input.clear(); input.push(iter.next()?); intersecting.clear(); fn left_overlaps_right(left: &Hunk, right: &Hunk) -> bool { left.side != right.side && (right.before.contains(&left.before.start) || (right.before.is_empty() && right.before.start == left.before.start)) } loop { let hunk = input.last().expect("just pushed"); while iter.peek().filter(|b_hunk| left_overlaps_right(b_hunk, hunk)).is_some() { intersecting.extend(iter.next()); } // The hunks that overlap might themselves overlap with a following hunk of the other side. // If so, split it so it doesn't overlap anymore. let mut found_more_intersections = false; while intersecting .last_mut() .zip(iter.peek_mut()) .filter(|(last_intersecting, candidate)| left_overlaps_right(candidate, last_intersecting)) .is_some() { input.extend(iter.next()); found_more_intersections = true; } if !found_more_intersections { break; } } Some(()) } pub fn tokens(input: &[u8]) -> imara_diff::sources::ByteLines<'_> { imara_diff::sources::byte_lines(input) } #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum Side { Current, Other, /// A special marker that is just used to be able to mix-in hunks that only point to the ancestor. /// Only `before` matters then. Ancestor, } #[derive(Debug, Clone)] pub struct Hunk { pub before: Range, pub after: Range, pub side: Side, } pub fn collect_hunks( algorithm: imara_diff::Algorithm, input: &imara_diff::InternedInput<&[u8]>, side: Side, mut hunks: Vec, ) -> Vec { let mut diff = imara_diff::Diff::compute(algorithm, input); diff.postprocess_lines(input); hunks.extend(diff.hunks().map(|hunk| Hunk { before: hunk.before, after: hunk.after, side, })); hunks } gix-merge-0.16.0/src/blob/mod.rs000064400000000000000000000167661046102023000144460ustar 00000000000000// TODO: remove this - only needed while &mut Vec isn't used. #![allow(clippy::ptr_arg)] use std::path::PathBuf; use bstr::BString; use crate::blob::platform::{DriverChoice, ResourceRef}; /// pub mod builtin_driver; /// pub mod pipeline; /// pub mod platform; /// Define if a merge is conflicted or not. #[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] pub enum Resolution { /// Everything could be resolved during the merge, and there was no conflict. Complete, /// Conflicts were resolved automatically, even thought the result is complete /// and free of conflict markers. /// This can only be the case for text-file content merges. CompleteWithAutoResolvedConflict, /// A conflict is still present in the form of conflict markers. Conflict, } /// A way to classify the side of a resource for merging. #[derive(Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)] pub enum ResourceKind { /// Our side of the state. CurrentOrOurs, /// Their side of the state. OtherOrTheirs, /// The state of the common base of both ours and theirs. CommonAncestorOrBase, } /// Define a built-in way of performing a three-way merge, including auto-resolution support. /// /// Some values are related to diffing, some are related to conversions. #[derive(Default, Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub enum BuiltinDriver { /// Perform a merge between text-sources such that conflicts are marked according to /// `merge.conflictStyle` in the Git configuration. /// /// If any of the inputs, *base*, *ours* or *theirs* looks like non-text/binary, /// the [`Binary`](Self::Binary) driver will be used instead. /// /// Also see [`builtin_driver::text::ConflictStyle`]. #[default] Text, /// Merge 'unmergable' content by choosing *ours* or *theirs*, without performing /// an actual merge. /// /// Note that if the merge operation is for virtual ancestor (a merge for merge-bases), /// then *ours* will always be chosen. Binary, /// Merge text-sources and resolve conflicts by adding conflicting lines one after another, /// in random order, without adding conflict markers either. /// /// This can be useful for files that change a lot, but will remain usable merely by adding /// all changed lines. Union, } /// Define a driver program that performs a three-way merge. /// /// Some values are related to diffing, some are related to conversions. #[derive(Default, Debug, Clone, PartialEq, Eq)] pub struct Driver { /// The name of the driver, as referred to by `[merge "name"]` in the git configuration. pub name: BString, /// The human-readable version of `name`, only to be used for displaying driver-information to the user. pub display_name: BString, /// The command to execute to perform the merge entirely like ` %O %A %B %L %P %S %X %Y`. /// /// * **%O** /// - the common ancestor version, or *base*. /// * **%A** /// - the current version, or *ours*. /// * **%B** /// - the other version, or *theirs*. /// * **%L** /// - The conflict-marker size as positive number. /// * **%P** /// - The path in which the merged result would be stored, as workspace-relative path, of the current/ours side. /// * **%S** /// - The conflict-label for the common ancestor or *base*. /// * **%X** /// - The conflict-label for the current version or *ours*. /// * **%Y** /// - The conflict-label for the other version or *theirs*. /// /// Note that conflict-labels are behind the conflict markers, to annotate them. /// /// A typical invocation with all arguments substituted could then look like this: /// /// ```sh /// .merge_file_nR2Qs1 .merge_file_WYXCJe .merge_file_UWbzrm 7 file e2a2970 HEAD feature /// ``` /// /// The driver is expected to leave its version in the file at `%A`, by overwriting it. pub command: BString, /// If `true`, this is the `name` of the driver to use when a virtual-merge-base is created, as a merge of all /// available merge-bases if there are more than one. /// /// This value can also be special built-in drivers named `text`, `binary` or `union`. Note that user-defined /// drivers with the same name will be preferred over built-in ones, but only for files whose git attributes /// specified the driver by *name*. pub recursive: Option, } /// A conversion pipeline to take an object or path from what's stored in Git to what can be merged, while /// following the guidance of git-attributes at the respective path to learn how the merge should be performed. /// /// Depending on the source, different conversions are performed: /// /// * `worktree on disk` -> `object for storage in git` /// * `object` -> `possibly renormalized object` /// - Renormalization means that the `object` is converted to what would be checked out into the work-tree, /// just to turn it back into an object. #[derive(Clone)] pub struct Pipeline { /// A way to read data directly from the worktree. pub roots: pipeline::WorktreeRoots, /// A pipeline to convert objects from the worktree to Git, and also from Git to the worktree, and back to Git. pub filter: gix_filter::Pipeline, /// Options affecting the way we read files. pub options: pipeline::Options, /// A buffer to produce disk-accessible paths from worktree roots. path: PathBuf, } /// A utility for gathering and processing all state necessary to perform a three-way merge. /// /// It can re-use buffers if all three parts of participating in the merge are /// set repeatedly. #[derive(Clone)] pub struct Platform { /// The current version (ours). current: Option, /// The ancestor version (base). ancestor: Option, /// The other version (theirs). other: Option, /// A way to convert objects into a diff-able format. pub filter: Pipeline, /// A way to access `.gitattributes` pub attr_stack: gix_worktree::Stack, /// Further configuration that affects the merge. pub options: platform::Options, /// All available merge drivers. /// /// They are referenced in git-attributes by name, and we hand out indices into this array. drivers: Vec, /// Pre-configured attributes to obtain additional merge-related information. attrs: gix_filter::attributes::search::Outcome, /// The way we convert resources into mergeable states. pub filter_mode: pipeline::Mode, } /// The product of a [`prepare_merge()`](Platform::prepare_merge()) call to finally /// perform the merge and retrieve the merge results. #[derive(Copy, Clone)] pub struct PlatformRef<'parent> { /// The platform that hosts the resources, used to access drivers. pub(super) parent: &'parent Platform, /// The current or our side of the merge operation. pub current: ResourceRef<'parent>, /// The ancestor or base of the merge operation. pub ancestor: ResourceRef<'parent>, /// The other or their side of the merge operation. pub other: ResourceRef<'parent>, /// Which driver to use according to the resource's configuration, /// using the path of `current` to read git-attributes. pub driver: DriverChoice, /// Possibly processed options for use when performing the actual merge. /// /// They may be inspected before the merge, or altered at will. pub options: platform::merge::Options, } gix-merge-0.16.0/src/blob/pipeline.rs000064400000000000000000000374551046102023000154720ustar 00000000000000use std::{ io::Read, path::{Path, PathBuf}, }; use bstr::BStr; use gix_filter::{ driver::apply::{Delay, MaybeDelayed}, pipeline::convert::{ToGitOutcome, ToWorktreeOutcome}, }; use gix_object::tree::EntryKind; use super::{Pipeline, ResourceKind}; /// Options for use in a [`Pipeline`]. #[derive(Default, Clone, PartialEq, Eq, Debug, Hash, Ord, PartialOrd)] pub struct Options { /// The amount of bytes that an object has to reach before being treated as binary. /// These objects will not be queried, nor will their data be processed in any way. /// If `0`, no file is ever considered binary due to their size. /// /// Note that for files stored in `git`, what counts is their stored, decompressed size, /// thus `git-lfs` files would typically not be considered binary unless one explicitly sets /// them. /// However, if they are to be retrieved from the worktree, the worktree size is what matters, /// even though that also might be a `git-lfs` file which is small in Git. pub large_file_threshold_bytes: u64, } /// The specific way to convert a resource. #[derive(Default, Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub enum Mode { /// Prepare resources as they are stored in `git`. /// /// This is naturally the case when object-ids are used, but a conversion is needed /// when data is read from a worktree. #[default] ToGit, /// For sources that are object-ids, convert them to what *would* be stored in the worktree, /// and back to what *would* be stored in Git. /// /// Sources that are located in a worktree are merely converted to what *would* be stored in Git. /// /// This is useful to prevent merge conflicts due to inconcistent whitespace. Renormalize, } /// A way to access roots for different kinds of resources that are possibly located and accessible in a worktree. #[derive(Clone, Debug, Default)] pub struct WorktreeRoots { /// The worktree root where the current (or our) version of the resource is present. pub current_root: Option, /// The worktree root where the other (or their) version of the resource is present. pub other_root: Option, /// The worktree root where containing the resource of the common ancestor of our and their version. pub common_ancestor_root: Option, } impl WorktreeRoots { /// Return the root path for the given `kind` pub fn by_kind(&self, kind: ResourceKind) -> Option<&Path> { match kind { ResourceKind::CurrentOrOurs => self.current_root.as_deref(), ResourceKind::CommonAncestorOrBase => self.common_ancestor_root.as_deref(), ResourceKind::OtherOrTheirs => self.other_root.as_deref(), } } /// Return `true` if all worktree roots are unset. pub fn is_unset(&self) -> bool { self.current_root.is_none() && self.other_root.is_none() && self.common_ancestor_root.is_none() } } /// Lifecycle impl Pipeline { /// Create a new instance of a pipeline which produces blobs suitable for merging. /// /// `roots` allow to read worktree files directly, and `worktree_filter` is used /// to transform object database data directly. /// `options` are used to further configure the way we act. pub fn new(roots: WorktreeRoots, worktree_filter: gix_filter::Pipeline, options: Options) -> Self { Pipeline { roots, filter: worktree_filter, options, path: Default::default(), } } } /// Access impl Pipeline {} /// Data as returned by [`Pipeline::convert_to_mergeable()`]. #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug)] pub enum Data { /// The data to use for merging was written into the buffer that was passed during the call to [`Pipeline::convert_to_mergeable()`]. Buffer, /// The file or blob is above the big-file threshold and cannot be processed. /// /// In this state, the file cannot be merged. TooLarge { /// The size of the object prior to performing any filtering or as it was found on disk. /// /// Note that technically, the size isn't always representative of the same 'state' of the /// content, as once it can be the size of the blob in git, and once it's the size of file /// in the worktree - both can differ a lot depending on filters. size: u64, }, } /// pub mod convert_to_mergeable { use std::collections::TryReserveError; use bstr::BString; use gix_object::tree::EntryKind; /// The error returned by [Pipeline::convert_to_mergeable()](super::Pipeline::convert_to_mergeable()). #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] pub enum Error { #[error("Entry at '{rela_path}' must be regular file or symlink, but was {actual:?}")] InvalidEntryKind { rela_path: BString, actual: EntryKind }, #[error("Entry at '{rela_path}' could not be read as symbolic link")] ReadLink { rela_path: BString, source: std::io::Error }, #[error("Entry at '{rela_path}' could not be opened for reading or read from")] OpenOrRead { rela_path: BString, source: std::io::Error }, #[error("Entry at '{rela_path}' could not be copied from a filter process to a memory buffer")] StreamCopy { rela_path: BString, source: std::io::Error }, #[error(transparent)] FindObject(#[from] gix_object::find::existing_object::Error), #[error(transparent)] ConvertToWorktree(#[from] gix_filter::pipeline::convert::to_worktree::Error), #[error(transparent)] ConvertToGit(#[from] gix_filter::pipeline::convert::to_git::Error), #[error("Memory allocation failed")] OutOfMemory(#[from] TryReserveError), } } /// Conversion impl Pipeline { /// Convert the object at `id`, `mode`, `rela_path` and `kind`, providing access to `attributes` and `objects`. /// The resulting merge-able data is written into `out`, if it's not too large. /// The returned [`Data`] contains information on how to use `out`, which will be cleared if it is `None`, indicating /// that no object was found at the location *on disk* - it's always an error to provide an object ID that doesn't exist /// in the object database. /// /// `attributes` must be returning the attributes at `rela_path` and is used for obtaining worktree filter settings, /// and `objects` must be usable if `kind` is a resource in the object database, /// i.e. if no worktree root is available. It's notable that if a worktree root is present for `kind`, /// then a `rela_path` is used to access it on disk. /// /// If `id` [is null](gix_hash::ObjectId::is_null()) or the file in question doesn't exist in the worktree in case /// [a root](WorktreeRoots) is present, then `out` will be left cleared and the output data will be `None`. /// This is useful to simplify the calling code as empty buffers signal that nothing is there. /// /// Note that `mode` is trusted, and we will not re-validate that the entry in the worktree actually is of that mode. /// Only blobs are allowed. /// /// Use `convert` to control what kind of the resource will be produced. #[allow(clippy::too_many_arguments)] pub fn convert_to_mergeable( &mut self, id: &gix_hash::oid, mode: EntryKind, rela_path: &BStr, kind: ResourceKind, attributes: &mut dyn FnMut(&BStr, &mut gix_filter::attributes::search::Outcome), objects: &dyn gix_object::FindObjectOrHeader, convert: Mode, out: &mut Vec, ) -> Result, convert_to_mergeable::Error> { if !matches!(mode, EntryKind::Blob | EntryKind::BlobExecutable) { return Err(convert_to_mergeable::Error::InvalidEntryKind { rela_path: rela_path.to_owned(), actual: mode, }); } out.clear(); match self.roots.by_kind(kind) { Some(root) => { self.path.clear(); self.path.push(root); self.path.push(gix_path::from_bstr(rela_path)); let size_in_bytes = (self.options.large_file_threshold_bytes > 0) .then(|| { none_if_missing(self.path.metadata().map(|md| md.len())).map_err(|err| { convert_to_mergeable::Error::OpenOrRead { rela_path: rela_path.to_owned(), source: err, } }) }) .transpose()?; let data = match size_in_bytes { Some(None) => None, // missing as identified by the size check Some(Some(size)) if size > self.options.large_file_threshold_bytes => Some(Data::TooLarge { size }), _ => { let file = none_if_missing(std::fs::File::open(&self.path)).map_err(|err| { convert_to_mergeable::Error::OpenOrRead { rela_path: rela_path.to_owned(), source: err, } })?; if let Some(file) = file { match convert { Mode::ToGit | Mode::Renormalize => { let res = self.filter.convert_to_git( file, gix_path::from_bstr(rela_path).as_ref(), attributes, &mut |buf| { if convert == Mode::Renormalize { Ok(None) } else { objects.try_find(id, buf).map(|obj| obj.map(|_| ())) } }, )?; match res { ToGitOutcome::Unchanged(mut file) => { file.read_to_end(out).map_err(|err| { convert_to_mergeable::Error::OpenOrRead { rela_path: rela_path.to_owned(), source: err, } })?; } ToGitOutcome::Process(mut stream) => { stream.read_to_end(out).map_err(|err| { convert_to_mergeable::Error::OpenOrRead { rela_path: rela_path.to_owned(), source: err, } })?; } ToGitOutcome::Buffer(buf) => { out.clear(); out.try_reserve(buf.len())?; out.extend_from_slice(buf); } } } } Some(Data::Buffer) } else { None } } }; Ok(data) } None => { let data = if id.is_null() { None } else { let header = objects .try_header(id) .map_err(gix_object::find::existing_object::Error::Find)? .ok_or_else(|| gix_object::find::existing_object::Error::NotFound { oid: id.to_owned() })?; let is_binary = self.options.large_file_threshold_bytes > 0 && header.size > self.options.large_file_threshold_bytes; let data = if is_binary { Data::TooLarge { size: header.size } } else { objects .try_find(id, out) .map_err(gix_object::find::existing_object::Error::Find)? .ok_or_else(|| gix_object::find::existing_object::Error::NotFound { oid: id.to_owned() })?; if convert == Mode::Renormalize { { let res = self .filter .convert_to_worktree(out, rela_path, attributes, Delay::Forbid)?; match res { ToWorktreeOutcome::Unchanged(_) => {} ToWorktreeOutcome::Buffer(src) => { out.clear(); out.try_reserve(src.len())?; out.extend_from_slice(src); } ToWorktreeOutcome::Process(MaybeDelayed::Immediate(mut stream)) => { std::io::copy(&mut stream, out).map_err(|err| { convert_to_mergeable::Error::StreamCopy { rela_path: rela_path.to_owned(), source: err, } })?; } ToWorktreeOutcome::Process(MaybeDelayed::Delayed(_)) => { unreachable!("we prohibit this") } } } let res = self.filter.convert_to_git( &**out, &gix_path::from_bstr(rela_path), attributes, &mut |_buf| Ok(None), )?; match res { ToGitOutcome::Unchanged(_) => {} ToGitOutcome::Process(mut stream) => { stream .read_to_end(out) .map_err(|err| convert_to_mergeable::Error::OpenOrRead { rela_path: rela_path.to_owned(), source: err, })?; } ToGitOutcome::Buffer(buf) => { out.clear(); out.try_reserve(buf.len())?; out.extend_from_slice(buf); } } } Data::Buffer }; Some(data) }; Ok(data) } } } } fn none_if_missing(res: std::io::Result) -> std::io::Result> { match res { Ok(data) => Ok(Some(data)), Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None), Err(err) => Err(err), } } gix-merge-0.16.0/src/blob/platform/merge.rs000064400000000000000000000543711046102023000166040ustar 00000000000000use std::{io::Read, path::PathBuf}; use crate::blob::{builtin_driver, PlatformRef, Resolution}; /// Options for the use in the [`PlatformRef::merge()`] call. #[derive(Default, Copy, Clone, Debug, Eq, PartialEq)] pub struct Options { /// If `true`, the resources being merged are contained in a virtual ancestor, /// which is the case when merge bases are merged into one. /// This flag affects the choice of merge drivers. pub is_virtual_ancestor: bool, /// Determine how to resolve conflicts. If `None`, no conflict resolution is possible, and it picks a side. pub resolve_binary_with: Option, /// Options for the builtin [text driver](crate::blob::BuiltinDriver::Text). pub text: builtin_driver::text::Options, } /// The error returned by [`PlatformRef::merge()`]. #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] pub enum Error { #[error(transparent)] PrepareExternalDriver(#[from] inner::prepare_external_driver::Error), #[error("Failed to launch external merge driver: {cmd}")] SpawnExternalDriver { cmd: String, source: std::io::Error }, #[error("External merge driver failed with non-zero exit status {status:?}: {cmd}")] ExternalDriverFailure { status: std::process::ExitStatus, cmd: String, }, #[error("IO failed when dealing with merge-driver output")] ExternalDriverIO(#[from] std::io::Error), } /// The product of a [`PlatformRef::prepare_external_driver()`] operation. /// /// This type allows to creation of [`std::process::Command`], ready to run, with `stderr` and `stdout` set to *inherit*, /// but `stdin` closed. /// It's expected to leave its result in the file substituted at `current` which is then supposed to be read back from there. // TODO: remove dead-code annotation #[allow(dead_code)] pub struct Command { /// The pre-configured command cmd: std::process::Command, /// A tempfile holding the *current* (ours) state of the resource. current: gix_tempfile::Handle, /// The path at which `current` is located, for reading the result back from later. current_path: PathBuf, /// A tempfile holding the *ancestor* (base) state of the resource. ancestor: gix_tempfile::Handle, /// A tempfile holding the *other* (their) state of the resource. other: gix_tempfile::Handle, } // Just to keep things here but move them a level up later. pub(super) mod inner { /// pub mod prepare_external_driver { use std::{ io::Write, ops::{Deref, DerefMut}, path::{Path, PathBuf}, process::Stdio, }; use bstr::{BString, ByteVec}; use gix_tempfile::{AutoRemove, ContainingDirectory}; use crate::blob::{ builtin_driver, builtin_driver::text::Conflict, platform::{merge, DriverChoice}, BuiltinDriver, Driver, PlatformRef, ResourceKind, }; /// The error returned by [PlatformRef::prepare_external_driver()](PlatformRef::prepare_external_driver()). #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] pub enum Error { #[error("The resource of kind {kind:?} was too large to be processed")] ResourceTooLarge { kind: ResourceKind }, #[error( "Tempfile to store content of '{rela_path}' ({kind:?}) for passing to external merge command could not be created" )] CreateTempfile { rela_path: BString, kind: ResourceKind, source: std::io::Error, }, #[error( "Could not write content of '{rela_path}' ({kind:?}) to tempfile for passing to external merge command" )] WriteTempfile { rela_path: BString, kind: ResourceKind, source: std::io::Error, }, } /// Plumbing impl<'parent> PlatformRef<'parent> { /// Given `merge_command` and `context`, typically obtained from git-configuration, and the currently set merge-resources, /// prepare the invocation and temporary files needed to launch it according to protocol. /// See the documentation of [`Driver::command`] for possible substitutions. /// /// Please note that this is an expensive operation this will always create three temporary files to hold all sides of the merge. /// /// The resulting command should be spawned, and when successful, [the result file can be opened](merge::Command::open_result_file) /// to read back the result into a suitable buffer. /// /// ### Deviation /// /// * We allow passing more context than Git would by taking a whole `context`, /// it's up to the caller to decide how much is filled. /// * Our tempfiles aren't suffixed `.merge_file_XXXXXX` with `X` replaced with characters for uniqueness. pub fn prepare_external_driver( &self, merge_command: BString, builtin_driver::text::Labels { ancestor, current, other, }: builtin_driver::text::Labels<'_>, context: gix_command::Context, ) -> Result { fn write_data( data: &[u8], directory: &Path, ) -> std::io::Result<(gix_tempfile::Handle, PathBuf)> { let mut file = gix_tempfile::new(directory, ContainingDirectory::Exists, AutoRemove::Tempfile)?; file.write_all(data)?; let mut path = Default::default(); file.with_mut(|f| { f.path().clone_into(&mut path); })?; let file = file.close()?; Ok((file, path)) } let base = self.ancestor.data.as_slice().ok_or(Error::ResourceTooLarge { kind: ResourceKind::CommonAncestorOrBase, })?; let ours = self.current.data.as_slice().ok_or(Error::ResourceTooLarge { kind: ResourceKind::CurrentOrOurs, })?; let theirs = self.other.data.as_slice().ok_or(Error::ResourceTooLarge { kind: ResourceKind::OtherOrTheirs, })?; let tmp_dir = context .worktree_dir .as_deref() .or(context.git_dir.as_deref()) .unwrap_or(Path::new("")); let (base_tmp, base_path) = write_data(base, tmp_dir).map_err(|err| Error::CreateTempfile { rela_path: self.ancestor.rela_path.into(), kind: ResourceKind::CommonAncestorOrBase, source: err, })?; let (ours_tmp, ours_path) = write_data(ours, tmp_dir).map_err(|err| Error::CreateTempfile { rela_path: self.current.rela_path.into(), kind: ResourceKind::CurrentOrOurs, source: err, })?; let (theirs_tmp, theirs_path) = write_data(theirs, tmp_dir).map_err(|err| Error::CreateTempfile { rela_path: self.other.rela_path.into(), kind: ResourceKind::OtherOrTheirs, source: err, })?; let mut cmd = BString::from(Vec::with_capacity(merge_command.len())); let mut count = 0; for token in merge_command.split(|b| *b == b'%') { count += 1; let token = if count > 1 { match token.first() { Some(&b'O') => { cmd.push_str(gix_path::into_bstr(&base_path).as_ref()); &token[1..] } Some(&b'A') => { cmd.push_str(gix_path::into_bstr(&ours_path).as_ref()); &token[1..] } Some(&b'B') => { cmd.push_str(gix_path::into_bstr(&theirs_path).as_ref()); &token[1..] } Some(&b'L') => { let marker_size = self .options .text .conflict .marker_size() .unwrap_or(Conflict::DEFAULT_MARKER_SIZE); cmd.push_str(format!("{marker_size}")); &token[1..] } Some(&b'P') => { cmd.push_str(gix_quote::single(self.current.rela_path)); &token[1..] } Some(&b'S') => { cmd.push_str(gix_quote::single(ancestor.unwrap_or_default())); &token[1..] } Some(&b'X') => { cmd.push_str(gix_quote::single(current.unwrap_or_default())); &token[1..] } Some(&b'Y') => { cmd.push_str(gix_quote::single(other.unwrap_or_default())); &token[1..] } Some(_other) => { cmd.push(b'%'); token } None => b"%", } } else { token }; cmd.extend_from_slice(token); } Ok(merge::Command { cmd: gix_command::prepare(gix_path::from_bstring(cmd)) .with_context(context) .command_may_be_shell_script() .stdin(Stdio::null()) .stdout(Stdio::inherit()) .stderr(Stdio::inherit()) .into(), current: ours_tmp, current_path: ours_path, ancestor: base_tmp, other: theirs_tmp, }) } /// Return the configured driver program for use with [`Self::prepare_external_driver()`], or `Err` /// with the built-in driver to use instead. pub fn configured_driver(&self) -> Result<&'parent Driver, BuiltinDriver> { match self.driver { DriverChoice::BuiltIn(builtin) => Err(builtin), DriverChoice::Index(idx) => self.parent.drivers.get(idx).ok_or(BuiltinDriver::default()), } } } impl std::fmt::Debug for merge::Command { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.cmd.fmt(f) } } impl Deref for merge::Command { type Target = std::process::Command; fn deref(&self) -> &Self::Target { &self.cmd } } impl DerefMut for merge::Command { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.cmd } } impl merge::Command { /// Open the file which should have been written to the location of `ours`, to yield the result of the merge operation. /// Calling this makes sense only after the merge command has finished successfully. pub fn open_result_file(&self) -> std::io::Result { std::fs::File::open(&self.current_path) } } } /// pub mod builtin_merge { use crate::blob::{ builtin_driver, platform::{resource, resource::Data}, BuiltinDriver, PlatformRef, Resolution, }; /// An identifier to tell us how a merge conflict was resolved by [builtin_merge](PlatformRef::builtin_merge). #[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] pub enum Pick { /// In a binary merge, chose the ancestor. /// /// Use [`PlatformRef::buffer_by_pick()`] to retrieve it. Ancestor, /// In a binary merge, chose our side. /// /// Use [`PlatformRef::buffer_by_pick()`] to retrieve it. Ours, /// In a binary merge, chose their side. /// /// Use [`PlatformRef::buffer_by_pick()`] to retrieve it. Theirs, /// New data was produced with the result of the merge, to be found in the buffer that was passed to /// [builtin_merge()](PlatformRef::builtin_merge). /// This happens for any merge that isn't a binary merge. Buffer, } /// Plumbing impl<'parent> PlatformRef<'parent> { /// Perform the merge using the given `driver`, possibly placing the output in `out`. /// `input` can be used to keep tokens between runs, but note it will only grow in size unless cleared manually. /// Use `labels` to annotate conflict sections in case of a text-merge. /// Returns `None` if one of the buffers is too large, making a merge impossible. /// Note that if the *pick* wasn't [`Pick::Buffer`], then `out` will not have been cleared, /// and one has to take the data from the respective resource. /// /// If there is no buffer loaded as the resource is too big, we will automatically perform a binary merge /// which effectively chooses our side by default. pub fn builtin_merge( &self, driver: BuiltinDriver, out: &mut Vec, input: &mut imara_diff::InternedInput<&'parent [u8]>, labels: builtin_driver::text::Labels<'_>, ) -> (Pick, Resolution) { let base = self.ancestor.data.as_slice().unwrap_or_default(); let ours = self.current.data.as_slice().unwrap_or_default(); let theirs = self.other.data.as_slice().unwrap_or_default(); let driver = if driver != BuiltinDriver::Binary && (is_binary_buf(self.ancestor.data) || is_binary_buf(self.other.data) || is_binary_buf(self.current.data)) { BuiltinDriver::Binary } else { driver }; match driver { BuiltinDriver::Text => { let resolution = builtin_driver::text(out, input, labels, ours, base, theirs, self.options.text); (Pick::Buffer, resolution) } BuiltinDriver::Binary => { // easier to reason about the 'split' compared to merging both conditions #[allow(clippy::if_same_then_else)] if !(self.current.id.is_null() || self.other.id.is_null()) && self.current.id == self.other.id { (Pick::Ours, Resolution::Complete) } else if (self.current.id.is_null() || self.other.id.is_null()) && ours == theirs { (Pick::Ours, Resolution::Complete) } else { let (pick, resolution) = builtin_driver::binary(self.options.resolve_binary_with); let pick = match pick { builtin_driver::binary::Pick::Ours => Pick::Ours, builtin_driver::binary::Pick::Theirs => Pick::Theirs, builtin_driver::binary::Pick::Ancestor => Pick::Ancestor, }; (pick, resolution) } } BuiltinDriver::Union => { let resolution = builtin_driver::text( out, input, labels, ours, base, theirs, builtin_driver::text::Options { conflict: builtin_driver::text::Conflict::ResolveWithUnion, ..self.options.text }, ); (Pick::Buffer, resolution) } } } } fn is_binary_buf(data: resource::Data<'_>) -> bool { match data { Data::Missing => false, Data::Buffer(buf) => { let buf = &buf[..buf.len().min(8000)]; buf.contains(&0) } Data::TooLarge { .. } => true, } } } } /// Convenience impl<'parent> PlatformRef<'parent> { /// Perform the merge, possibly invoking an external merge command, and store the result in `out`, returning `(pick, resolution)`. /// Note that `pick` indicates which resource the buffer should be taken from, unless it's [`Pick::Buffer`](inner::builtin_merge::Pick::Buffer) /// to indicate it's `out`. /// Use `labels` to annotate conflict sections in case of a text-merge. /// The merge is configured by `opts` and possible merge driver command executions are affected by `context`. /// /// Note that at this stage, none-existing input data will simply default to an empty buffer when running the actual merge algorithm. /// Too-large resources will result in an error. /// /// Generally, it is assumed that standard logic, like deletions of files, is handled before any of this is called, so we are lenient /// in terms of buffer handling to make it more useful in the face of missing local files. pub fn merge( &self, out: &mut Vec, labels: builtin_driver::text::Labels<'_>, context: &gix_command::Context, ) -> Result<(inner::builtin_merge::Pick, Resolution), Error> { match self.configured_driver() { Ok(driver) => { let mut cmd = self.prepare_external_driver(driver.command.clone(), labels, context.clone())?; let status = cmd.status().map_err(|err| Error::SpawnExternalDriver { cmd: format!("{:?}", cmd.cmd), source: err, })?; if !status.success() { return Err(Error::ExternalDriverFailure { cmd: format!("{:?}", cmd.cmd), status, }); } out.clear(); cmd.open_result_file()?.read_to_end(out)?; Ok((inner::builtin_merge::Pick::Buffer, Resolution::Complete)) } Err(builtin) => { let mut input = imara_diff::InternedInput::new(&[][..], &[]); out.clear(); let (pick, resolution) = self.builtin_merge(builtin, out, &mut input, labels); Ok((pick, resolution)) } } } /// Using a `pick` obtained from [`merge()`](Self::merge), obtain the respective buffer suitable for reading or copying. /// Return `Ok(None)` if the `pick` corresponds to a buffer (that was written separately). /// Return `Err(())` if the buffer is *too large*, so it was never read. #[allow(clippy::result_unit_err)] pub fn buffer_by_pick(&self, pick: inner::builtin_merge::Pick) -> Result, ()> { match pick { inner::builtin_merge::Pick::Ancestor => self.ancestor.data.as_slice().map(Some).ok_or(()), inner::builtin_merge::Pick::Ours => self.current.data.as_slice().map(Some).ok_or(()), inner::builtin_merge::Pick::Theirs => self.other.data.as_slice().map(Some).ok_or(()), inner::builtin_merge::Pick::Buffer => Ok(None), } } /// Use `pick` to return the object id of the merged result, assuming that `buf` was passed as `out` to [merge()](Self::merge). /// In case of binary or large files, this will simply be the existing ID of the resource. /// In case of resources available in the object DB for binary merges, the object ID will be returned. /// If new content was produced due to a content merge, `buf` will be written out /// to the object database using `write_blob`. /// Beware that the returned ID could be `Ok(None)` if the underlying resource was loaded /// from the worktree *and* was too large so it was never loaded from disk. /// `Ok(None)` will also be returned if one of the resources was missing. /// `write_blob()` is used to turn buffers. pub fn id_by_pick( &self, pick: inner::builtin_merge::Pick, buf: &[u8], mut write_blob: impl FnMut(&[u8]) -> Result, ) -> Result, E> { let field = match pick { inner::builtin_merge::Pick::Ancestor => &self.ancestor, inner::builtin_merge::Pick::Ours => &self.current, inner::builtin_merge::Pick::Theirs => &self.other, inner::builtin_merge::Pick::Buffer => return write_blob(buf).map(Some), }; use crate::blob::platform::resource::Data; match field.data { Data::TooLarge { .. } | Data::Missing if !field.id.is_null() => Ok(Some(field.id.to_owned())), Data::TooLarge { .. } | Data::Missing => Ok(None), Data::Buffer(buf) if field.id.is_null() => write_blob(buf).map(Some), Data::Buffer(_) => Ok(Some(field.id.to_owned())), } } } gix-merge-0.16.0/src/blob/platform/mod.rs000064400000000000000000000113371046102023000162570ustar 00000000000000use bstr::{BStr, BString}; use gix_filter::attributes; use crate::blob::{pipeline, BuiltinDriver, Pipeline, Platform}; /// A stored value representing a resource that participates in a merge. #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Debug)] pub(super) struct Resource { /// The `id` of the value, or `null` if it's only living in a worktree. id: gix_hash::ObjectId, /// The repository-relative path where the resource lives in the tree. rela_path: BString, /// The outcome of converting a resource into a mergable format using [Pipeline::convert_to_mergeable()]. data: Option, /// The kind of the resource we are looking at. Only possible values are `Blob` and `BlobExecutable`. mode: gix_object::tree::EntryKind, /// A possibly empty buffer, depending on `conversion.data` which may indicate the data is considered binary /// or the resource doesn't exist. buffer: Vec, } /// A blob or executable ready to be merged in one way or another. #[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] pub struct ResourceRef<'a> { /// The data itself, suitable for merging, and if the object or worktree item is present at all. pub data: resource::Data<'a>, /// The location of the resource, relative to the working tree. pub rela_path: &'a BStr, /// The id of the content as it would be stored in `git`, or `null` if the content doesn't exist anymore at /// `rela_path` or if it was never computed. This can happen with content read from the worktree, which /// after its 'to-git' conversion never had its hash computed. pub id: &'a gix_hash::oid, } /// Options for use in [`Platform::new()`]. #[derive(Default, Clone, PartialEq, Eq, Debug, Hash, Ord, PartialOrd)] pub struct Options { /// Define which driver to use by name if the `merge` attribute for a resource is unspecified. /// /// This is the value of the `merge.default` git configuration. pub default_driver: Option, } /// The selection of the driver to use by a resource obtained with [`Platform::prepare_merge()`]. /// /// If available, an index into the `drivers` field to access more diff-related information of the driver for items /// at the given path, as previously determined by git-attributes. /// /// * `merge` is set /// - Use the [`BuiltinDriver::Text`] /// * `-merge` is unset /// - Use the [`BuiltinDriver::Binary`] /// * `!merge` is unspecified /// - Use [`Options::default_driver`] or [`BuiltinDriver::Text`]. /// * `merge=name` /// - Search for a user-configured or built-in driver called `name`. /// - If not found, silently default to [`BuiltinDriver::Text`] /// /// Note that drivers are queried even if there is no object available. #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug, Hash)] pub enum DriverChoice { /// Use the given built-in driver to perform the merge. BuiltIn(BuiltinDriver), /// Use the user-provided driver program using the index into [the platform drivers array](Platform::drivers()). Index(usize), } impl Default for DriverChoice { fn default() -> Self { DriverChoice::BuiltIn(Default::default()) } } /// Lifecycle impl Platform { /// Create a new instance with a way to `filter` data from the object database and turn it into something that is merge-able. /// `filter_mode` decides how to do that specifically. /// Use `attr_stack` to access attributes pertaining worktree filters and merge settings. /// `drivers` are the list of available merge drivers that individual paths can refer to by means of git attributes. /// `options` further configure the operation. pub fn new( filter: Pipeline, filter_mode: pipeline::Mode, attr_stack: gix_worktree::Stack, mut drivers: Vec, options: Options, ) -> Self { drivers.sort_by(|a, b| a.name.cmp(&b.name)); Platform { drivers, current: None, ancestor: None, other: None, filter, filter_mode, attr_stack, attrs: { let mut out = attributes::search::Outcome::default(); out.initialize_with_selection(&Default::default(), ["merge", "conflict-marker-size"]); out }, options, } } } /// Access impl Platform { /// Return all drivers that this instance was initialized with. /// /// They are sorted by [`name`](super::Driver::name) to support binary searches. pub fn drivers(&self) -> &[super::Driver] { &self.drivers } } /// pub mod set_resource; /// pub mod resource; /// pub mod merge; pub use merge::inner::{builtin_merge, prepare_external_driver}; /// pub mod prepare_merge; gix-merge-0.16.0/src/blob/platform/prepare_merge.rs000064400000000000000000000116531046102023000203160ustar 00000000000000use std::{num::NonZeroU8, str::FromStr}; use bstr::{BStr, BString, ByteSlice}; use gix_filter::attributes; use crate::blob::{ builtin_driver::text::Conflict, platform::{merge, DriverChoice, ResourceRef}, BuiltinDriver, Platform, PlatformRef, ResourceKind, }; /// The error returned by [Platform::prepare_merge_state()](Platform::prepare_merge()). #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] pub enum Error { #[error("The 'current', 'ancestor' or 'other' resource for the merge operation were not set")] UnsetResource, #[error("Failed to obtain attributes for {kind:?} resource at '{rela_path}'")] Attributes { rela_path: BString, kind: ResourceKind, source: std::io::Error, }, } /// Preparation impl Platform { /// Prepare all state needed for performing a merge, using all [previously set](Self::set_resource()) resources. /// `objects` is used to possibly lookup attribute files when obtaining merge-related attributes. /// /// `options` are to be used when merging later, and they may be altered to implement correct binary merges /// in the present of [virtual merge bases](merge::Options::is_virtual_ancestor). /// /// Note that no additional validation is performed here to facilitate inspection, which means that /// resource buffers might still be too large to be merged, preventing a successful merge at a later time. pub fn prepare_merge( &mut self, objects: &impl gix_object::Find, mut options: merge::Options, ) -> Result, Error> { let current = self.current.as_ref().ok_or(Error::UnsetResource)?; let ancestor = self.ancestor.as_ref().ok_or(Error::UnsetResource)?; let other = self.other.as_ref().ok_or(Error::UnsetResource)?; let entry = self .attr_stack .at_entry(current.rela_path.as_bstr(), None, objects) .map_err(|err| Error::Attributes { source: err, kind: ResourceKind::CurrentOrOurs, rela_path: current.rela_path.clone(), })?; entry.matching_attributes(&mut self.attrs); let mut attrs = self.attrs.iter_selected(); let merge_attr = attrs.next().expect("pre-initialized with 'merge'"); let marker_size_attr = attrs.next().expect("pre-initialized with 'conflict-marker-size'"); let mut driver = match merge_attr.assignment.state { attributes::StateRef::Set => DriverChoice::BuiltIn(BuiltinDriver::Text), attributes::StateRef::Unset => DriverChoice::BuiltIn(BuiltinDriver::Binary), attributes::StateRef::Value(_) | attributes::StateRef::Unspecified => { let name = match merge_attr.assignment.state { attributes::StateRef::Value(name) => Some(name.as_bstr()), attributes::StateRef::Unspecified => { self.options.default_driver.as_ref().map(|name| name.as_bstr()) } _ => unreachable!("only value and unspecified are possible here"), }; self.find_driver_by_name(name) } }; if let attributes::StateRef::Value(value) = marker_size_attr.assignment.state { if let Some(value) = u8::from_str(value.as_bstr().to_str_lossy().as_ref()) .ok() .and_then(NonZeroU8::new) { match &mut options.text.conflict { Conflict::Keep { marker_size, .. } => *marker_size = value, Conflict::ResolveWithOurs | Conflict::ResolveWithTheirs | Conflict::ResolveWithUnion => {} } } } if let Some(recursive_driver_name) = match driver { DriverChoice::Index(idx) => self.drivers.get(idx), _ => None, } .and_then(|driver| driver.recursive.as_deref()) .filter(|_| options.is_virtual_ancestor) { driver = self.find_driver_by_name(Some(recursive_driver_name.as_bstr())); options.resolve_binary_with = Some(crate::blob::builtin_driver::binary::ResolveWith::Ours); } let out = PlatformRef { parent: self, driver, current: ResourceRef::new(current), ancestor: ResourceRef::new(ancestor), other: ResourceRef::new(other), options, }; Ok(out) } fn find_driver_by_name(&self, name: Option<&BStr>) -> DriverChoice { name.and_then(|name| { self.drivers .binary_search_by(|d| d.name.as_bstr().cmp(name)) .ok() .map(DriverChoice::Index) .or_else(|| { name.to_str() .ok() .and_then(BuiltinDriver::by_name) .map(DriverChoice::BuiltIn) }) }) .unwrap_or_default() } } gix-merge-0.16.0/src/blob/platform/resource.rs000064400000000000000000000035471046102023000173330ustar 00000000000000use crate::blob::{ pipeline, platform::{Resource, ResourceRef}, }; impl<'a> ResourceRef<'a> { pub(super) fn new(cache: &'a Resource) -> Self { ResourceRef { data: cache.data.map_or(Data::Missing, |data| match data { pipeline::Data::Buffer => Data::Buffer(&cache.buffer), pipeline::Data::TooLarge { size } => Data::TooLarge { size }, }), rela_path: cache.rela_path.as_ref(), id: &cache.id, } } } /// The data of a mergeable resource, as it could be determined and computed previously. #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] pub enum Data<'a> { /// The object is missing, either because it didn't exist in the working tree or because its `id` was null. /// Such data equals an empty buffer. Missing, /// The textual data as processed and ready for merging, i.e. suitable for storage in Git. Buffer(&'a [u8]), /// The file or blob is above the big-file threshold and cannot be processed. /// /// In this state, the file cannot be merged. TooLarge { /// The size of the object prior to performing any filtering or as it was found on disk. /// /// Note that technically, the size isn't always representative of the same 'state' of the /// content, as once it can be the size of the blob in Git, and once it's the size of file /// in the worktree. size: u64, }, } impl<'a> Data<'a> { /// Return ourselves as slice of bytes if this instance stores data. /// Note that missing data is interpreted as empty slice, to facilitate additions and deletions. pub fn as_slice(&self) -> Option<&'a [u8]> { match self { Data::Buffer(d) => Some(d), Data::Missing => Some(&[]), Data::TooLarge { .. } => None, } } } gix-merge-0.16.0/src/blob/platform/set_resource.rs000064400000000000000000000071541046102023000202040ustar 00000000000000use bstr::{BStr, BString}; use crate::blob::{pipeline, platform::Resource, Platform, ResourceKind}; /// The error returned by [Platform::set_resource](Platform::set_resource). #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] pub enum Error { #[error("Can only diff blobs, not {mode:?}")] InvalidMode { mode: gix_object::tree::EntryKind }, #[error("Failed to read {kind:?} worktree data from '{rela_path}'")] Io { rela_path: BString, kind: ResourceKind, source: std::io::Error, }, #[error("Failed to obtain attributes for {kind:?} resource at '{rela_path}'")] Attributes { rela_path: BString, kind: ResourceKind, source: std::io::Error, }, #[error(transparent)] ConvertToMergeable(#[from] pipeline::convert_to_mergeable::Error), } /// Preparation impl Platform { /// Store enough information about a resource to eventually use it in a merge, where… /// /// * `id` is the hash of the resource. If it [is null](gix_hash::ObjectId::is_null()), it should either /// be a resource in the worktree, or it's considered a non-existing, deleted object. /// If an `id` is known, as the hash of the object as (would) be stored in `git`, then it should be provided /// for completeness. Note that it's not expected to be in `objects` if `rela_path` is set and a worktree-root /// is available for `kind`. /// * `mode` is the kind of object (only blobs and executables are allowed) /// * `rela_path` is the relative path as seen from the (work)tree root. /// * `kind` identifies the side of the merge this resource will be used for. /// * `objects` provides access to the object database in case the resource can't be read from a worktree. pub fn set_resource( &mut self, id: gix_hash::ObjectId, mode: gix_object::tree::EntryKind, rela_path: &BStr, kind: ResourceKind, objects: &impl gix_object::FindObjectOrHeader, ) -> Result<(), Error> { if !matches!( mode, gix_object::tree::EntryKind::Blob | gix_object::tree::EntryKind::BlobExecutable ) { return Err(Error::InvalidMode { mode }); } let entry = self .attr_stack .at_entry(rela_path, None, objects) .map_err(|err| Error::Attributes { source: err, kind, rela_path: rela_path.to_owned(), })?; let storage = match kind { ResourceKind::OtherOrTheirs => &mut self.other, ResourceKind::CommonAncestorOrBase => &mut self.ancestor, ResourceKind::CurrentOrOurs => &mut self.current, }; let mut buf_storage = Vec::new(); let out = self.filter.convert_to_mergeable( &id, mode, rela_path, kind, &mut |_, out| { let _ = entry.matching_attributes(out); }, objects, self.filter_mode, storage.as_mut().map_or(&mut buf_storage, |s| &mut s.buffer), )?; match storage { None => { *storage = Some(Resource { id, rela_path: rela_path.to_owned(), data: out, mode, buffer: buf_storage, }); } Some(storage) => { storage.id = id; storage.rela_path = rela_path.to_owned(); storage.data = out; storage.mode = mode; } } Ok(()) } } gix-merge-0.16.0/src/commit/function.rs000064400000000000000000000127471046102023000160610ustar 00000000000000use std::borrow::Cow; use gix_object::FindExt; use crate::{ blob::builtin_driver, commit::{Error, Options}, }; /// Like [`tree()`](crate::tree()), but it takes only two commits, `our_commit` and `their_commit` to automatically /// compute the merge-bases among them. /// If there are multiple merge bases, these will be auto-merged into one, recursively, if /// [`allow_missing_merge_base`](Options::allow_missing_merge_base) is `true`. /// /// `labels` are names where [`current`](crate::blob::builtin_driver::text::Labels::current) is a name for `our_commit` /// and [`other`](crate::blob::builtin_driver::text::Labels::other) is a name for `their_commit`. /// If [`ancestor`](crate::blob::builtin_driver::text::Labels::ancestor) is unset, it will be set by us based on the /// merge-bases of `our_commit` and `their_commit`. /// /// The `graph` is used to find the merge-base between `our_commit` and `their_commit`, and can also act as cache /// to speed up subsequent merge-base queries. /// /// Use `abbreviate_hash(id)` to shorten the given `id` according to standard git shortening rules. It's used in case /// the ancestor-label isn't explicitly set so that the merge base label becomes the shortened `id`. /// Note that it's a dyn closure only to make it possible to recursively call this function in case of multiple merge-bases. /// /// `write_object` is used only if it's allowed to merge multiple merge-bases into one, and if there /// are multiple merge bases, and to write merged buffers as blobs. /// /// ### Performance /// /// Note that `objects` *should* have an object cache to greatly accelerate tree-retrieval. /// /// ### Notes /// /// When merging merge-bases recursively, the options are adjusted automatically to act like Git, i.e. merge binary /// blobs and resolve with *ours*, while resorting to using the base/ancestor in case of unresolvable conflicts. /// /// ### Deviation /// /// * It's known that certain conflicts around symbolic links can be auto-resolved. We don't have an option for this /// at all, yet, primarily as Git seems to not implement the *ours*/*theirs* choice in other places even though it /// reasonably could. So we leave it to the caller to continue processing the returned tree at will. #[allow(clippy::too_many_arguments)] pub fn commit<'objects>( our_commit: gix_hash::ObjectId, their_commit: gix_hash::ObjectId, labels: builtin_driver::text::Labels<'_>, graph: &mut gix_revwalk::Graph<'_, '_, gix_revwalk::graph::Commit>, diff_resource_cache: &mut gix_diff::blob::Platform, blob_merge: &mut crate::blob::Platform, objects: &'objects (impl gix_object::FindObjectOrHeader + gix_object::Write), abbreviate_hash: &mut dyn FnMut(&gix_hash::oid) -> String, options: Options, ) -> Result, Error> { let merge_bases = gix_revision::merge_base(our_commit, &[their_commit], graph)?; let mut virtual_merge_bases = Vec::new(); let mut state = gix_diff::tree::State::default(); let mut commit_to_tree = |commit_id: gix_hash::ObjectId| objects.find_commit(&commit_id, &mut state.buf1).map(|c| c.tree()); let (merge_base_tree_id, ancestor_name): (_, Cow<'_, str>) = match merge_bases.clone() { Some(base_commit) if base_commit.len() == 1 => ( commit_to_tree(*base_commit.first())?, abbreviate_hash(base_commit.first()).into(), ), Some(base_commits) => { let virtual_base_tree = if options.use_first_merge_base { commit_to_tree(*base_commits.first())? } else { let mut base_commits: Vec<_> = base_commits.into(); let first = base_commits.pop().expect("at least two"); let second = base_commits.pop().expect("at least one left"); let out = crate::commit::virtual_merge_base( first, second, base_commits, graph, diff_resource_cache, blob_merge, objects, abbreviate_hash, options.tree_merge.clone(), )?; virtual_merge_bases = Vec::from(out.virtual_merge_bases); out.tree_id }; (virtual_base_tree, "merged common ancestors".into()) } None => { if options.allow_missing_merge_base { (gix_hash::ObjectId::empty_tree(our_commit.kind()), "empty tree".into()) } else { return Err(Error::NoMergeBase { our_commit_id: our_commit, their_commit_id: their_commit, }); } } }; let mut labels = labels; // TODO(borrowchk): this re-assignment shouldn't be needed. if labels.ancestor.is_none() { labels.ancestor = Some(ancestor_name.as_ref().into()); } let our_tree_id = objects.find_commit(&our_commit, &mut state.buf1)?.tree(); let their_tree_id = objects.find_commit(&their_commit, &mut state.buf1)?.tree(); let outcome = crate::tree( &merge_base_tree_id, &our_tree_id, &their_tree_id, labels, objects, |buf| objects.write_buf(gix_object::Kind::Blob, buf), &mut state, diff_resource_cache, blob_merge, options.tree_merge, )?; Ok(super::Outcome { tree_merge: outcome, merge_bases, merge_base_tree_id, virtual_merge_bases, }) } gix-merge-0.16.0/src/commit/mod.rs000064400000000000000000000055321046102023000150050ustar 00000000000000/// The error returned by [`commit()`](crate::commit()). #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] pub enum Error { #[error("Failed to obtain the merge base between the two commits to be merged")] MergeBase(#[from] gix_revision::merge_base::Error), #[error(transparent)] VirtualMergeBase(#[from] virtual_merge_base::Error), #[error(transparent)] MergeTree(#[from] crate::tree::Error), #[error("No common ancestor between {our_commit_id} and {their_commit_id}")] NoMergeBase { /// The commit on our side that was to be merged. our_commit_id: gix_hash::ObjectId, /// The commit on their side that was to be merged. their_commit_id: gix_hash::ObjectId, }, #[error("Could not find ancestor, our or their commit to extract tree from")] FindCommit(#[from] gix_object::find::existing_object::Error), } /// A way to configure [`commit()`](crate::commit()). #[derive(Default, Debug, Clone)] pub struct Options { /// If `true`, merging unrelated commits is allowed, with the merge-base being assumed as empty tree. pub allow_missing_merge_base: bool, /// Options to define how trees should be merged. pub tree_merge: crate::tree::Options, /// If `true`, do not merge multiple merge-bases into one. Instead, just use the first one. // TODO: test #[doc(alias = "no_recursive", alias = "git2")] pub use_first_merge_base: bool, } /// The result of [`commit()`](crate::commit()). #[derive(Clone)] pub struct Outcome<'a> { /// The outcome of the actual tree-merge. pub tree_merge: crate::tree::Outcome<'a>, /// The tree id of the base commit we used. This is either… /// * the single merge-base we found /// * the first of multiple merge-bases if [`use_first_merge_base`](Options::use_first_merge_base) was `true`. /// * the merged tree of all merge-bases, which then isn't linked to an actual commit. /// * an empty tree, if [`allow_missing_merge_base`](Options::allow_missing_merge_base) is enabled. pub merge_base_tree_id: gix_hash::ObjectId, /// The object ids of all the commits which were found to be merge-bases, or `None` if there was no merge-base. pub merge_bases: Option>, /// A list of virtual commits that were created to merge multiple merge-bases into one, the last one being /// the one we used as merge-base for the merge. /// As they are not reachable by anything they will be garbage collected, but knowing them provides options. /// Would be empty if no virtual commit was needed at all as there was only a single merge-base. /// Otherwise, the last commit id is the one with the `merge_base_tree_id`. pub virtual_merge_bases: Vec, } pub(super) mod function; /// pub mod virtual_merge_base; pub use virtual_merge_base::function::virtual_merge_base; gix-merge-0.16.0/src/commit/virtual_merge_base.rs000064400000000000000000000144501046102023000200640ustar 00000000000000/// The outcome produced by [`commit::merge_base()`](crate::commit::virtual_merge_base()). pub struct Outcome { /// The commit ids of all the virtual merge bases we have produced in the process of recursively merging the merge-bases. /// As they have been written to the object database, they are still available until they are garbage collected. /// The last one is the most recently produced and the one returned as `commit_id`. /// This is never empty. pub virtual_merge_bases: nonempty::NonEmpty, /// The id of the commit that was created to hold the merged tree. pub commit_id: gix_hash::ObjectId, /// The hash of the merged tree. pub tree_id: gix_hash::ObjectId, } /// The error returned by [`commit::merge_base()`](crate::commit::virtual_merge_base()). #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] pub enum Error { #[error(transparent)] MergeTree(#[from] crate::tree::Error), #[error("Failed to write tree for merged merge-base or virtual commit")] WriteObject(gix_object::write::Error), #[error("Failed to decode a commit needed to build a virtual merge-base")] DecodeCommit(#[from] gix_object::decode::Error), #[error( "Conflicts occurred when trying to resolve multiple merge-bases by merging them. This is most certainly a bug." )] VirtualMergeBaseConflict, #[error("Could not find commit to use as basis for a virtual commit")] FindCommit(#[from] gix_object::find::existing_object::Error), } pub(super) mod function { use gix_object::FindExt; use super::Error; use crate::{ blob::builtin_driver, tree::{treat_as_unresolved, TreatAsUnresolved}, }; /// Create a single virtual merge-base by merging `first_commit`, `second_commit` and `others` into one. /// Note that `first_commit` and `second_commit` are expected to have been popped off `others`, so `first_commit` /// was the last provided merge-base of function that provides multiple merge-bases for a pair of commits. /// /// The parameters `graph`, `diff_resource_cache`, `blob_merge`, `objects`, `abbreviate_hash` and `options` are passed /// directly to [`tree()`](crate::tree()) for merging the trees of two merge-bases at a time. /// Note that most of `options` are overwritten to match the requirements of a merge-base merge. #[allow(clippy::too_many_arguments)] pub fn virtual_merge_base<'objects>( first_commit: gix_hash::ObjectId, second_commit: gix_hash::ObjectId, mut others: Vec, graph: &mut gix_revwalk::Graph<'_, '_, gix_revwalk::graph::Commit>, diff_resource_cache: &mut gix_diff::blob::Platform, blob_merge: &mut crate::blob::Platform, objects: &'objects (impl gix_object::FindObjectOrHeader + gix_object::Write), abbreviate_hash: &mut dyn FnMut(&gix_hash::oid) -> String, mut options: crate::tree::Options, ) -> Result { let mut merged_commit_id = first_commit; others.push(second_commit); options.tree_conflicts = Some(crate::tree::ResolveWith::Ancestor); options.blob_merge.is_virtual_ancestor = true; options.blob_merge.text.conflict = builtin_driver::text::Conflict::ResolveWithOurs; let favor_ancestor = Some(builtin_driver::binary::ResolveWith::Ancestor); options.blob_merge.resolve_binary_with = favor_ancestor; options.symlink_conflicts = favor_ancestor; let labels = builtin_driver::text::Labels { current: Some("Temporary merge branch 1".into()), other: Some("Temporary merge branch 2".into()), ancestor: None, }; let mut virtual_merge_bases = Vec::new(); let mut tree_id = None; while let Some(next_commit_id) = others.pop() { options.marker_size_multiplier += 1; let mut out = crate::commit( merged_commit_id, next_commit_id, labels, graph, diff_resource_cache, blob_merge, objects, abbreviate_hash, crate::commit::Options { allow_missing_merge_base: false, tree_merge: options.clone(), use_first_merge_base: false, }, )?; // This shouldn't happen, but if for some buggy reason it does, we rather bail. if out.tree_merge.has_unresolved_conflicts(TreatAsUnresolved { content_merge: treat_as_unresolved::ContentMerge::Markers, tree_merge: treat_as_unresolved::TreeMerge::Undecidable, }) { return Err(Error::VirtualMergeBaseConflict.into()); } let merged_tree_id = out .tree_merge .tree .write(|tree| objects.write(tree)) .map_err(Error::WriteObject)?; tree_id = Some(merged_tree_id); merged_commit_id = create_virtual_commit(objects, merged_commit_id, next_commit_id, merged_tree_id)?; virtual_merge_bases.extend(out.virtual_merge_bases); virtual_merge_bases.push(merged_commit_id); } Ok(super::Outcome { virtual_merge_bases: nonempty::NonEmpty::from_vec(virtual_merge_bases) .expect("the virtual merge-base process always creates at least one commit"), commit_id: merged_commit_id, tree_id: tree_id.map_or_else( || { let mut buf = Vec::new(); objects.find_commit(&merged_commit_id, &mut buf).map(|c| c.tree()) }, Ok, )?, }) } fn create_virtual_commit( objects: &(impl gix_object::Find + gix_object::Write), parent_a: gix_hash::ObjectId, parent_b: gix_hash::ObjectId, tree_id: gix_hash::ObjectId, ) -> Result { let mut buf = Vec::new(); let commit_ref = objects.find_commit(&parent_a, &mut buf)?; let mut commit = commit_ref.to_owned()?; commit.parents = vec![parent_a, parent_b].into(); commit.tree = tree_id; objects.write(&commit).map_err(Error::WriteObject) } } gix-merge-0.16.0/src/lib.rs000064400000000000000000000010131046102023000134720ustar 00000000000000//! Provide facilities to merge *blobs*, *trees* and *commits*. //! //! * [blob-merges](blob) look at file content. //! * [tree-merges](mod@tree) look at trees and merge them structurally, triggering blob-merges as needed. //! * [commit-merges](mod@commit) are like tree merges, but compute or create the merge-base on the fly. #![deny(rust_2018_idioms)] #![deny(missing_docs)] #![forbid(unsafe_code)] /// pub mod blob; /// pub mod commit; pub use commit::function::commit; /// pub mod tree; pub use tree::function::tree; gix-merge-0.16.0/src/tree/function.rs000064400000000000000000002266501046102023000155300ustar 00000000000000use std::{borrow::Cow, convert::Infallible}; use bstr::{BString, ByteSlice}; use gix_diff::{tree::recorder::Location, tree_with_rewrites::Change}; use gix_hash::ObjectId; use gix_object::{ tree, tree::{EntryKind, EntryMode}, FindExt, }; use crate::tree::{ utils::{ apply_change, perform_blob_merge, possibly_rewritten_location, rewrite_location_with_renamed_directory, to_components, track, unique_path_in_tree, ChangeList, ChangeListRef, PossibleConflict, TrackedChange, TreeNodes, }, Conflict, ConflictIndexEntry, ConflictIndexEntryPathHint, ConflictMapping, ConflictMapping::{Original, Swapped}, ContentMerge, Error, Options, Outcome, Resolution, ResolutionFailure, ResolveWith, }; /// Perform a merge between `our_tree` and `their_tree`, using `base_tree` as merge-base. /// Note that `base_tree` can be an empty tree to indicate 'no common ancestor between the two sides'. /// /// * `labels` are relevant for text-merges and will be shown in conflicts. /// * `objects` provides access to trees when diffing them. /// * `write_blob_to_odb(content) -> Result` writes newly merged content into the odb to obtain an id /// that will be used in merged trees. /// * `diff_state` is state used for diffing trees. /// * `diff_resource_cache` is used for similarity checks. /// * `blob_merge` is a pre-configured platform to merge any content. /// - Note that it shouldn't be allowed to read from the worktree, given that this is a tree-merge. /// * `options` are used to affect how the merge is performed. /// /// ### Unbiased (Ours x Theirs == Theirs x Ours) /// /// The algorithm is implemented so that the result is the same no matter how the sides are ordered. /// /// ### Differences to Merge-ORT /// /// Merge-ORT (Git) defines the desired outcomes where are merely mimicked here. The algorithms are different, and it's /// clear that Merge-ORT is significantly more elaborate and general. /// /// It also writes out trees once it's done with them in a form of reduction process, here an editor is used /// to keep only the changes, to be written by the caller who receives it as part of the result. /// This may use more memory in the worst case scenario, but in average *shouldn't* perform much worse due to the /// natural sparsity of the editor. /// /// Our rename-tracking also produces copy information, but we discard it and simply treat it like an addition. /// /// Finally, our algorithm will consider reasonable solutions to merge-conflicts as conflicts that are resolved, leaving /// only content with conflict markers as unresolved ones. /// /// ### Performance /// /// Note that `objects` *should* have an object cache to greatly accelerate tree-retrieval. #[allow(clippy::too_many_arguments)] pub fn tree<'objects, E>( base_tree: &gix_hash::oid, our_tree: &gix_hash::oid, their_tree: &gix_hash::oid, mut labels: crate::blob::builtin_driver::text::Labels<'_>, objects: &'objects impl gix_object::FindObjectOrHeader, mut write_blob_to_odb: impl FnMut(&[u8]) -> Result, diff_state: &mut gix_diff::tree::State, diff_resource_cache: &mut gix_diff::blob::Platform, blob_merge: &mut crate::blob::Platform, options: Options, ) -> Result, Error> where E: Into>, { let ours_needs_diff = base_tree != our_tree; let theirs_needs_diff = base_tree != their_tree; let _span = gix_trace::coarse!("gix_merge::tree", ?base_tree, ?our_tree, ?their_tree, ?labels); let (mut base_buf, mut side_buf) = (Vec::new(), Vec::new()); let ancestor_tree = objects.find_tree(base_tree, &mut base_buf)?; let mut editor = tree::Editor::new(ancestor_tree.to_owned(), objects, base_tree.kind()); let ancestor_tree = gix_object::TreeRefIter::from_bytes(&base_buf, base_tree.kind()); let tree_conflicts = options.tree_conflicts; let mut our_changes = Vec::new(); if ours_needs_diff { let our_tree = objects.find_tree_iter(our_tree, &mut side_buf)?; gix_diff::tree_with_rewrites( ancestor_tree, our_tree, diff_resource_cache, diff_state, objects, |change| -> Result<_, Infallible> { track(change, &mut our_changes); Ok(std::ops::ControlFlow::Continue(())) }, gix_diff::tree_with_rewrites::Options { location: Some(Location::Path), rewrites: options.rewrites, }, )?; } let mut our_tree = TreeNodes::new(); for (idx, change) in our_changes.iter().enumerate() { our_tree.track_change(&change.inner, idx); } let mut their_changes = Vec::new(); if theirs_needs_diff { let their_tree = objects.find_tree_iter(their_tree, &mut side_buf)?; gix_diff::tree_with_rewrites( ancestor_tree, their_tree, diff_resource_cache, diff_state, objects, |change| -> Result<_, Infallible> { track(change, &mut their_changes); Ok(std::ops::ControlFlow::Continue(())) }, gix_diff::tree_with_rewrites::Options { location: Some(Location::Path), rewrites: options.rewrites, }, )?; } let mut their_tree = TreeNodes::new(); for (idx, change) in their_changes.iter().enumerate() { their_tree.track_change(&change.inner, idx); } let mut conflicts = Vec::new(); let mut failed_on_first_conflict = false; let mut should_fail_on_conflict = |mut conflict: Conflict| -> bool { if tree_conflicts.is_some() { if let Err(failure) = conflict.resolution { conflict.resolution = Ok(Resolution::Forced(failure)); } } if let Some(how) = options.fail_on_conflict { if conflict.resolution.is_err() || conflict.is_unresolved(how) { failed_on_first_conflict = true; } } conflicts.push(conflict); failed_on_first_conflict }; let ((mut our_changes, mut our_tree), (mut their_changes, mut their_tree)) = ((&mut our_changes, &mut our_tree), (&mut their_changes, &mut their_tree)); let mut outer_side = Original; if their_changes.is_empty() { ((our_changes, our_tree), (their_changes, their_tree)) = ((their_changes, their_tree), (our_changes, our_tree)); (labels.current, labels.other) = (labels.other, labels.current); outer_side = outer_side.swapped(); } #[derive(Debug)] enum MatchKind { /// A tree is supposed to be superseded by something else. EraseTree, /// A leaf node is superseded by a tree EraseLeaf, } 'outer: while their_changes.iter().rev().any(|c| !c.was_written) { let mut segment_start = 0; let mut last_seen_len = their_changes.len(); while segment_start != last_seen_len { for theirs_idx in segment_start..last_seen_len { // `their` can be a tree, and it could be used to efficiently prune child-changes as these // trees are always rewrites with parent ids (of course we validate), so child-changes could be handled // quickly. However, for now the benefit of having these trees is to have them as part of the match-tree // on *our* side so that it's clear that we passed a renamed directory (by identity). let TrackedChange { inner: theirs, was_written, needs_tree_insertion, rewritten_location, } = &their_changes[theirs_idx]; if theirs.entry_mode().is_tree() || *was_written { continue; } if needs_tree_insertion.is_some() { their_tree.insert(theirs, theirs_idx); } match our_tree .check_conflict( rewritten_location .as_ref() .map_or_else(|| theirs.source_location(), |t| t.0.as_bstr()), ) .filter(|ours| { ours.change_idx() .zip(needs_tree_insertion.flatten()) .is_none_or(|(ours_idx, ignore_idx)| ours_idx != ignore_idx) && our_tree.is_not_same_change_in_possible_conflict(theirs, ours, our_changes) }) { None => { if let Some((rewritten_location, ours_idx)) = rewritten_location { // `no_entry` to the index because that's not a conflict at all, // but somewhat advanced rename tracking. if should_fail_on_conflict(Conflict::with_resolution( Resolution::SourceLocationAffectedByRename { final_location: rewritten_location.to_owned(), }, (&our_changes[*ours_idx].inner, theirs, Original, outer_side), [None, None, None], )) { break 'outer; } editor.remove(to_components(theirs.location()))?; } apply_change(&mut editor, theirs, rewritten_location.as_ref().map(|t| &t.0))?; their_changes[theirs_idx].was_written = true; } Some(candidate) => { use crate::tree::utils::to_components_bstring_ref as toc; debug_assert!( rewritten_location.is_none(), "We should probably handle the case where a rewritten location is passed down here" ); let (ours_idx, match_kind) = match candidate { PossibleConflict::PassedRewrittenDirectory { change_idx } => { let ours = &our_changes[change_idx]; let location_after_passed_rename = rewrite_location_with_renamed_directory(theirs.location(), &ours.inner); if let Some(new_location) = location_after_passed_rename { their_tree.remove_existing_leaf(theirs.location()); push_deferred_with_rewrite( (theirs.clone(), Some(change_idx)), Some((new_location, change_idx)), their_changes, ); } else { apply_change(&mut editor, theirs, None)?; their_changes[theirs_idx].was_written = true; } their_changes[theirs_idx].was_written = true; continue; } PossibleConflict::TreeToNonTree { change_idx: Some(idx) } if matches!( our_changes[idx].inner, Change::Deletion { .. } | Change::Addition { .. } | Change::Rewrite { .. } ) => { (Some(idx), Some(MatchKind::EraseTree)) } PossibleConflict::NonTreeToTree { change_idx } => (change_idx, Some(MatchKind::EraseLeaf)), PossibleConflict::Match { change_idx: ours_idx } => (Some(ours_idx), None), _ => (None, None), }; let Some(ours_idx) = ours_idx else { let ours = match candidate { PossibleConflict::TreeToNonTree { change_idx, .. } | PossibleConflict::NonTreeToTree { change_idx, .. } => change_idx, PossibleConflict::Match { change_idx } | PossibleConflict::PassedRewrittenDirectory { change_idx } => Some(change_idx), } .map(|idx| &mut our_changes[idx]); if let Some(ours) = ours { gix_trace::debug!("Turning a case we could probably handle into a conflict for now. theirs: {theirs:#?} ours: {ours:#?} kind: {match_kind:?}"); let conflict = Conflict::unknown((&ours.inner, theirs, Original, outer_side)); if let Some(ResolveWith::Ours) = tree_conflicts { apply_our_resolution(&ours.inner, theirs, outer_side, &mut editor)?; *match outer_side { Original => &mut ours.was_written, Swapped => &mut their_changes[theirs_idx].was_written, } = true; } if should_fail_on_conflict(conflict) { break 'outer; } } else if matches!(candidate, PossibleConflict::TreeToNonTree { .. }) { let (mode, id) = theirs.entry_mode_and_id(); let location = theirs.location(); let renamed_location = unique_path_in_tree( location.as_bstr(), &editor, their_tree, labels.other.unwrap_or_default(), )?; match tree_conflicts { None => { editor.upsert(toc(&renamed_location), mode.kind(), id.to_owned())?; } Some(ResolveWith::Ours) => { if outer_side.is_swapped() { editor.upsert(to_components(location), mode.kind(), id.to_owned())?; } } Some(ResolveWith::Ancestor) => { // we found no matching node of 'ours', so nothing to apply here. } } let conflict = Conflict::without_resolution( ResolutionFailure::OursDirectoryTheirsNonDirectoryTheirsRenamed { renamed_unique_path_of_theirs: renamed_location, }, (theirs, theirs, Original, outer_side), [ None, None, index_entry_at_path( &mode.kind().into(), &id.to_owned(), ConflictIndexEntryPathHint::RenamedOrTheirs, ), ], ); their_changes[theirs_idx].was_written = true; if should_fail_on_conflict(conflict) { break 'outer; } } else if matches!(candidate, PossibleConflict::NonTreeToTree { .. }) { // We are writing on top of what was a file, a conflict we probably already saw and dealt with. let location = theirs.location(); let (mode, id) = theirs.entry_mode_and_id(); editor.upsert(to_components(location), mode.kind(), id.to_owned())?; their_changes[theirs_idx].was_written = true; } else { gix_trace::debug!("Couldn't figure out how to handle {match_kind:?} theirs: {theirs:#?} candidate: {candidate:#?}"); } continue; }; let ours = &our_changes[ours_idx].inner; match (ours, theirs) { ( Change::Modification { previous_id, previous_entry_mode, id: our_id, location: our_location, entry_mode: our_mode, .. }, Change::Rewrite { source_id: their_source_id, id: their_id, location: their_location, entry_mode: their_mode, source_location, .. }, ) | ( Change::Rewrite { source_id: their_source_id, id: their_id, location: their_location, entry_mode: their_mode, source_location, .. }, Change::Modification { previous_id, previous_entry_mode, id: our_id, location: our_location, entry_mode: our_mode, .. }, ) => { let side = if matches!(ours, Change::Modification { .. }) { Original } else { Swapped }; if let Some(merged_mode) = merge_modes(*our_mode, *their_mode) { debug_assert_eq!( previous_id, their_source_id, "both refer to the same base, so should always match" ); let their_rewritten_location = possibly_rewritten_location( pick_our_tree(side, our_tree, their_tree), their_location.as_ref(), pick_our_changes(side, our_changes, their_changes), ); let renamed_without_change = their_source_id == their_id; let (merged_blob_id, resolution) = if renamed_without_change { (*our_id, None) } else { let (our_location, our_id, our_mode, their_location, their_id, their_mode) = match side { Original => ( our_location, our_id, our_mode, their_location, their_id, their_mode, ), Swapped => ( their_location, their_id, their_mode, our_location, our_id, our_mode, ), }; let (merged_blob_id, resolution) = perform_blob_merge( labels, objects, blob_merge, &mut diff_state.buf1, &mut write_blob_to_odb, (our_location, *our_id, *our_mode), (their_location, *their_id, *their_mode), (source_location, *previous_id, *previous_entry_mode), (0, outer_side), &options, )?; (merged_blob_id, Some(resolution)) }; editor.remove(toc(our_location))?; pick_our_tree(side, our_tree, their_tree) .remove_existing_leaf(our_location.as_bstr()); let final_location = their_rewritten_location.clone(); let new_change = Change::Addition { location: their_rewritten_location.unwrap_or_else(|| their_location.to_owned()), relation: None, entry_mode: merged_mode, id: merged_blob_id, }; if should_fail_on_conflict(Conflict::with_resolution( Resolution::OursModifiedTheirsRenamedAndChangedThenRename { merged_mode: (merged_mode != *their_mode).then_some(merged_mode), merged_blob: resolution.map(|resolution| ContentMerge { resolution, merged_blob_id, }), final_location, }, (ours, theirs, side, outer_side), [ index_entry(previous_entry_mode, previous_id), index_entry(our_mode, our_id), index_entry(their_mode, their_id), ], )) { break 'outer; } // The other side gets the addition, not our side. push_deferred( (new_change, None), pick_our_changes_mut(side, their_changes, our_changes), ); } else { match tree_conflicts { None => { // keep both states - 'our_location' is the previous location as well. editor.upsert(toc(our_location), our_mode.kind(), *our_id)?; editor.upsert(toc(their_location), their_mode.kind(), *their_id)?; } Some(ResolveWith::Ours) => { editor.remove(toc(source_location))?; if side.to_global(outer_side).is_swapped() { editor.upsert(toc(their_location), their_mode.kind(), *their_id)?; } else { editor.upsert(toc(our_location), our_mode.kind(), *our_id)?; } } Some(ResolveWith::Ancestor) => {} } if should_fail_on_conflict(Conflict::without_resolution( ResolutionFailure::OursModifiedTheirsRenamedTypeMismatch, (ours, theirs, side, outer_side), [ index_entry_at_path( previous_entry_mode, previous_id, ConflictIndexEntryPathHint::RenamedOrTheirs, ), None, index_entry_at_path( their_mode, their_id, ConflictIndexEntryPathHint::RenamedOrTheirs, ), ], )) { break 'outer; } } } ( Change::Modification { location, previous_id, previous_entry_mode, entry_mode: our_mode, id: our_id, .. }, Change::Modification { entry_mode: their_mode, id: their_id, .. }, ) if !involves_submodule(our_mode, their_mode) && merge_modes(*our_mode, *their_mode).is_some() && our_id != their_id => { let (merged_blob_id, resolution) = perform_blob_merge( labels, objects, blob_merge, &mut diff_state.buf1, &mut write_blob_to_odb, (location, *our_id, *our_mode), (location, *their_id, *their_mode), (location, *previous_id, *previous_entry_mode), (0, outer_side), &options, )?; let merged_mode = merge_modes_prev(*our_mode, *their_mode, *previous_entry_mode) .expect("BUG: merge_modes() reports a valid mode, this one should do too"); editor.upsert(toc(location), merged_mode.kind(), merged_blob_id)?; if should_fail_on_conflict(Conflict::with_resolution( Resolution::OursModifiedTheirsModifiedThenBlobContentMerge { merged_blob: ContentMerge { resolution, merged_blob_id, }, }, (ours, theirs, Original, outer_side), [ index_entry(previous_entry_mode, previous_id), index_entry(our_mode, our_id), index_entry(their_mode, their_id), ], )) { break 'outer; } } ( Change::Addition { location, entry_mode: our_mode, id: our_id, .. }, Change::Addition { entry_mode: their_mode, id: their_id, .. }, ) if !involves_submodule(our_mode, their_mode) && our_id != their_id => { let conflict = if let Some(merged_mode) = merge_modes(*our_mode, *their_mode) { let side = if our_mode == their_mode || matches!(our_mode.kind(), EntryKind::Blob) { outer_side } else { outer_side.swapped() }; let (merged_blob_id, resolution) = perform_blob_merge( labels, objects, blob_merge, &mut diff_state.buf1, &mut write_blob_to_odb, (location, *our_id, merged_mode), (location, *their_id, merged_mode), (location, their_id.kind().null(), merged_mode), (0, side), &options, )?; editor.upsert(toc(location), merged_mode.kind(), merged_blob_id)?; Conflict::with_resolution( Resolution::OursModifiedTheirsModifiedThenBlobContentMerge { merged_blob: ContentMerge { resolution, merged_blob_id, }, }, (ours, theirs, Original, outer_side), [None, index_entry(our_mode, our_id), index_entry(their_mode, their_id)], ) } else { // Actually this has a preference, as symlinks are always left in place with the other side renamed. let ( logical_side, label_of_side_to_be_moved, (our_mode, our_id, our_path_hint), (their_mode, their_id, their_path_hint), ) = if matches!(our_mode.kind(), EntryKind::Link | EntryKind::Tree) { ( Original, labels.other.unwrap_or_default(), (*our_mode, *our_id, ConflictIndexEntryPathHint::Current), (*their_mode, *their_id, ConflictIndexEntryPathHint::RenamedOrTheirs), ) } else { ( Swapped, labels.current.unwrap_or_default(), (*their_mode, *their_id, ConflictIndexEntryPathHint::RenamedOrTheirs), (*our_mode, *our_id, ConflictIndexEntryPathHint::Current), ) }; let tree_with_rename = pick_our_tree(logical_side, their_tree, our_tree); let renamed_location = unique_path_in_tree( location.as_bstr(), &editor, tree_with_rename, label_of_side_to_be_moved, )?; let mut conflict = Conflict::without_resolution( ResolutionFailure::OursAddedTheirsAddedTypeMismatch { their_unique_location: renamed_location.clone(), }, (ours, theirs, logical_side, outer_side), [ None, index_entry_at_path(&our_mode, &our_id, our_path_hint), index_entry_at_path(&their_mode, &their_id, their_path_hint), ], ); match tree_conflicts { None => { let new_change = Change::Addition { location: renamed_location, entry_mode: their_mode, id: their_id, relation: None, }; editor.upsert(toc(location), our_mode.kind(), our_id)?; tree_with_rename.remove_existing_leaf(location.as_bstr()); push_deferred( (new_change, None), pick_our_changes_mut(logical_side, their_changes, our_changes), ); } Some(resolve) => { conflict.entries = Default::default(); match resolve { ResolveWith::Ours => match outer_side { Original => { editor.upsert(toc(location), our_mode.kind(), our_id)?; } Swapped => { editor.upsert(toc(location), their_mode.kind(), their_id)?; } }, ResolveWith::Ancestor => { // Do nothing - this discards both sides. // Note that one of these adds might be the result of a rename, which // means we effectively loose the original and can't get it back as that information is degenerated. } } } } conflict }; if should_fail_on_conflict(conflict) { break 'outer; } } ( Change::Modification { location, entry_mode, id, previous_entry_mode, previous_id, }, Change::Deletion { .. }, ) | ( Change::Deletion { .. }, Change::Modification { location, entry_mode, id, previous_entry_mode, previous_id, }, ) => { let (label_of_side_to_be_moved, side) = if matches!(ours, Change::Modification { .. }) { (labels.current.unwrap_or_default(), Original) } else { (labels.other.unwrap_or_default(), Swapped) }; let deletion_prefaces_addition_of_directory = { let change_on_right = match side { Original => their_changes.get(theirs_idx + 1), Swapped => our_changes.get(ours_idx + 1), }; change_on_right .map(|change| { change.inner.entry_mode().is_tree() && change.inner.location() == location && matches!(change.inner, Change::Addition { .. }) }) .unwrap_or_default() }; let should_break = if deletion_prefaces_addition_of_directory { let entries = [ index_entry(previous_entry_mode, previous_id), index_entry(entry_mode, id), None, ]; match tree_conflicts { None => { let our_tree = pick_our_tree(side, our_tree, their_tree); let renamed_path = unique_path_in_tree( location.as_bstr(), &editor, our_tree, label_of_side_to_be_moved, )?; editor.remove(toc(location))?; our_tree.remove_existing_leaf(location.as_bstr()); let new_change = Change::Addition { location: renamed_path.clone(), relation: None, entry_mode: *entry_mode, id: *id, }; let should_break = should_fail_on_conflict(Conflict::without_resolution( ResolutionFailure::OursModifiedTheirsDirectoryThenOursRenamed { renamed_unique_path_to_modified_blob: renamed_path, }, (ours, theirs, side, outer_side), entries, )); // Since we move *our* side, our tree needs to be modified. push_deferred( (new_change, None), pick_our_changes_mut(side, our_changes, their_changes), ); should_break } Some(ResolveWith::Ours) => { match side.to_global(outer_side) { Original => { // ours is modification editor.upsert(toc(location), entry_mode.kind(), *id)?; } Swapped => { // ours is deletion editor.remove(toc(location))?; } } should_fail_on_conflict(Conflict::without_resolution( ResolutionFailure::OursModifiedTheirsDeleted, (ours, theirs, side, outer_side), entries, )) } Some(ResolveWith::Ancestor) => { should_fail_on_conflict(Conflict::without_resolution( ResolutionFailure::OursModifiedTheirsDeleted, (ours, theirs, side, outer_side), entries, )) } } } else { let entries = [ index_entry(previous_entry_mode, previous_id), index_entry(entry_mode, id), None, ]; match tree_conflicts { None => { editor.upsert(toc(location), entry_mode.kind(), *id)?; } Some(ResolveWith::Ours) => { let ours = match outer_side { Original => ours, Swapped => theirs, }; match ours { Change::Modification { .. } => { editor.upsert(toc(location), entry_mode.kind(), *id)?; } Change::Deletion { .. } => { editor.remove(toc(location))?; } _ => unreachable!("parent-match assures this"), } } Some(ResolveWith::Ancestor) => {} } should_fail_on_conflict(Conflict::without_resolution( ResolutionFailure::OursModifiedTheirsDeleted, (ours, theirs, side, outer_side), entries, )) }; if should_break { break 'outer; } } ( Change::Modification { .. }, Change::Addition { location, entry_mode, id, .. }, ) if ours.location() != theirs.location() => { match tree_conflicts { None => { unreachable!("modification/deletion pair should prevent modification/addition from happening") } Some(ResolveWith::Ancestor) => {} Some(ResolveWith::Ours) => { if outer_side.is_swapped() { editor.upsert(toc(location), entry_mode.kind(), *id)?; } // we have already taken care of the 'root' of this - // everything that follows can safely be ignored } } } ( Change::Rewrite { source_location, source_entry_mode, source_id, entry_mode: our_mode, id: our_id, location: our_location, .. }, Change::Rewrite { entry_mode: their_mode, id: their_id, location: their_location, .. }, // NOTE: renames are only tracked among these kinds of types anyway, but we make sure. ) if our_mode.is_blob_or_symlink() && their_mode.is_blob_or_symlink() => { let (merged_blob_id, mut resolution) = if our_id == their_id { (*our_id, None) } else { let (id, resolution) = perform_blob_merge( labels, objects, blob_merge, &mut diff_state.buf1, &mut write_blob_to_odb, (our_location, *our_id, *our_mode), (their_location, *their_id, *their_mode), (source_location, *source_id, *source_entry_mode), (1, outer_side), &options, )?; (id, Some(resolution)) }; let merged_mode = merge_modes(*our_mode, *their_mode).expect("this case was assured earlier"); if matches!(tree_conflicts, None | Some(ResolveWith::Ours)) { editor.remove(toc(source_location))?; our_tree.remove_existing_leaf(source_location.as_bstr()); their_tree.remove_existing_leaf(source_location.as_bstr()); } let their_location = possibly_rewritten_location(our_tree, their_location.as_bstr(), our_changes) .map_or(Cow::Borrowed(their_location.as_bstr()), Cow::Owned); let our_location = possibly_rewritten_location(their_tree, our_location.as_bstr(), their_changes) .map_or(Cow::Borrowed(our_location.as_bstr()), Cow::Owned); let (our_addition, their_addition) = if our_location == their_location { ( None, Some(Change::Addition { location: our_location.into_owned(), relation: None, entry_mode: merged_mode, id: merged_blob_id, }), ) } else { if should_fail_on_conflict(Conflict::without_resolution( ResolutionFailure::OursRenamedTheirsRenamedDifferently { merged_blob: resolution.take().map(|resolution| ContentMerge { resolution, merged_blob_id, }), }, (ours, theirs, Original, outer_side), [ index_entry_at_path( source_entry_mode, source_id, ConflictIndexEntryPathHint::Source, ), index_entry_at_path( our_mode, &merged_blob_id, ConflictIndexEntryPathHint::Current, ), index_entry_at_path( their_mode, &merged_blob_id, ConflictIndexEntryPathHint::RenamedOrTheirs, ), ], )) { break 'outer; } match tree_conflicts { None => { let our_addition = Change::Addition { location: our_location.into_owned(), relation: None, entry_mode: merged_mode, id: merged_blob_id, }; let their_addition = Change::Addition { location: their_location.into_owned(), relation: None, entry_mode: merged_mode, id: merged_blob_id, }; (Some(our_addition), Some(their_addition)) } Some(ResolveWith::Ancestor) => (None, None), Some(ResolveWith::Ours) => { let our_addition = Change::Addition { location: match outer_side { Original => our_location, Swapped => their_location, } .into_owned(), relation: None, entry_mode: merged_mode, id: merged_blob_id, }; (Some(our_addition), None) } } }; if let Some(resolution) = resolution { if should_fail_on_conflict(Conflict::with_resolution( Resolution::OursModifiedTheirsModifiedThenBlobContentMerge { merged_blob: ContentMerge { resolution, merged_blob_id, }, }, (ours, theirs, Original, outer_side), [ index_entry_at_path( source_entry_mode, source_id, ConflictIndexEntryPathHint::Source, ), index_entry_at_path( our_mode, &merged_blob_id, ConflictIndexEntryPathHint::Current, ), index_entry_at_path( their_mode, &merged_blob_id, ConflictIndexEntryPathHint::RenamedOrTheirs, ), ], )) { break 'outer; } } if let Some(addition) = our_addition { push_deferred((addition, Some(ours_idx)), our_changes); } if let Some(addition) = their_addition { push_deferred((addition, Some(theirs_idx)), their_changes); } } ( Change::Deletion { .. }, Change::Rewrite { source_location, entry_mode: rewritten_mode, id: rewritten_id, location, .. }, ) | ( Change::Rewrite { source_location, entry_mode: rewritten_mode, id: rewritten_id, location, .. }, Change::Deletion { .. }, ) if !rewritten_mode.is_commit() => { let side = if matches!(ours, Change::Deletion { .. }) { Original } else { Swapped }; match tree_conflicts { None | Some(ResolveWith::Ours) => { editor.remove(toc(source_location))?; pick_our_tree(side, our_tree, their_tree) .remove_existing_leaf(source_location.as_bstr()); } Some(ResolveWith::Ancestor) => {} } let their_rewritten_location = possibly_rewritten_location( pick_our_tree(side, our_tree, their_tree), location.as_ref(), pick_our_changes(side, our_changes, their_changes), ) .unwrap_or_else(|| location.to_owned()); let our_addition = Change::Addition { location: their_rewritten_location, relation: None, entry_mode: *rewritten_mode, id: *rewritten_id, }; if should_fail_on_conflict(Conflict::without_resolution( ResolutionFailure::OursDeletedTheirsRenamed, (ours, theirs, side, outer_side), [ None, None, index_entry_at_path( rewritten_mode, rewritten_id, ConflictIndexEntryPathHint::RenamedOrTheirs, ), ], )) { break 'outer; } let ours_is_rewrite = side.is_swapped(); if tree_conflicts.is_none() || (matches!(tree_conflicts, Some(ResolveWith::Ours)) && ours_is_rewrite) { push_deferred( (our_addition, None), pick_our_changes_mut(side, their_changes, our_changes), ); } } ( Change::Rewrite { source_location, source_entry_mode, source_id, entry_mode: our_mode, id: our_id, location, .. }, Change::Addition { id: their_id, entry_mode: their_mode, location: add_location, .. }, ) | ( Change::Addition { id: their_id, entry_mode: their_mode, location: add_location, .. }, Change::Rewrite { source_location, source_entry_mode, source_id, entry_mode: our_mode, id: our_id, location, .. }, ) if !involves_submodule(our_mode, their_mode) => { let side = if matches!(ours, Change::Rewrite { .. }) { Original } else { Swapped }; if let Some(merged_mode) = merge_modes(*our_mode, *their_mode) { let (merged_blob_id, resolution) = if our_id == their_id { (*our_id, None) } else { let (id, resolution) = perform_blob_merge( labels, objects, blob_merge, &mut diff_state.buf1, &mut write_blob_to_odb, (location, *our_id, *our_mode), (location, *their_id, *their_mode), (source_location, source_id.kind().null(), *source_entry_mode), (0, outer_side), &options, )?; (id, Some(resolution)) }; editor.remove(toc(source_location))?; pick_our_tree(side, our_tree, their_tree).remove_leaf(source_location.as_bstr()); if let Some(resolution) = resolution { if should_fail_on_conflict(Conflict::with_resolution( Resolution::OursModifiedTheirsModifiedThenBlobContentMerge { merged_blob: ContentMerge { resolution, merged_blob_id, }, }, (ours, theirs, Original, outer_side), [None, index_entry(our_mode, our_id), index_entry(their_mode, their_id)], )) { break 'outer; } } // Because this constellation can only be found by the lookup tree, there is // no need to put it as addition, we know it's not going to intersect on the other side. editor.upsert(toc(location), merged_mode.kind(), merged_blob_id)?; } else { // We always remove the source from the tree - it might be re-added later. let ours_is_rename = tree_conflicts == Some(ResolveWith::Ours) && side == outer_side; let remove_rename_source = tree_conflicts.is_none() || ours_is_rename || add_location != source_location; if remove_rename_source { editor.remove(toc(source_location))?; pick_our_tree(side, our_tree, their_tree) .remove_leaf(source_location.as_bstr()); } let ( logical_side, label_of_side_to_be_moved, (our_mode, our_id, our_path_hint), (their_mode, their_id, their_path_hint), ) = if matches!(our_mode.kind(), EntryKind::Link | EntryKind::Tree) { ( Original, labels.other.unwrap_or_default(), (*our_mode, *our_id, ConflictIndexEntryPathHint::Current), (*their_mode, *their_id, ConflictIndexEntryPathHint::RenamedOrTheirs), ) } else { ( Swapped, labels.current.unwrap_or_default(), (*their_mode, *their_id, ConflictIndexEntryPathHint::RenamedOrTheirs), (*our_mode, *our_id, ConflictIndexEntryPathHint::Current), ) }; let tree_with_rename = pick_our_tree(logical_side, their_tree, our_tree); let renamed_location = unique_path_in_tree( location.as_bstr(), &editor, tree_with_rename, label_of_side_to_be_moved, )?; let upsert_rename_destination = tree_conflicts.is_none() || ours_is_rename; if upsert_rename_destination { editor.upsert(toc(location), our_mode.kind(), our_id)?; tree_with_rename.remove_existing_leaf(location.as_bstr()); } let conflict = Conflict::without_resolution( ResolutionFailure::OursAddedTheirsAddedTypeMismatch { their_unique_location: renamed_location.clone(), }, (ours, theirs, side, outer_side), [ None, index_entry_at_path(&our_mode, &our_id, our_path_hint), index_entry_at_path(&their_mode, &their_id, their_path_hint), ], ); if tree_conflicts.is_none() { let new_change_with_rename = Change::Addition { location: renamed_location, entry_mode: their_mode, id: their_id, relation: None, }; push_deferred( ( new_change_with_rename, Some(pick_idx(logical_side, theirs_idx, ours_idx)), ), pick_our_changes_mut(logical_side, their_changes, our_changes), ); } if should_fail_on_conflict(conflict) { break 'outer; } } } _unknown => { debug_assert!( match_kind.is_none() || (ours.location() == theirs.location() || ours.source_location() == theirs.source_location()), "BUG: right now it's not known to be possible to match changes from different paths: {match_kind:?} {candidate:?}" ); if let Some(ResolveWith::Ours) = tree_conflicts { apply_our_resolution(ours, theirs, outer_side, &mut editor)?; } if should_fail_on_conflict(Conflict::unknown((ours, theirs, Original, outer_side))) { break 'outer; } } } their_changes[theirs_idx].was_written = true; our_changes[ours_idx].was_written = true; } } } segment_start = last_seen_len; last_seen_len = their_changes.len(); } ((our_changes, our_tree), (their_changes, their_tree)) = ((their_changes, their_tree), (our_changes, our_tree)); (labels.current, labels.other) = (labels.other, labels.current); outer_side = outer_side.swapped(); } Ok(Outcome { tree: editor, conflicts, failed_on_first_unresolved_conflict: failed_on_first_conflict, }) } fn apply_our_resolution( local_ours: &Change, local_theirs: &Change, outer_side: ConflictMapping, editor: &mut gix_object::tree::Editor<'_>, ) -> Result<(), Error> { let ours = match outer_side { Original => local_ours, Swapped => local_theirs, }; Ok(apply_change(editor, ours, None)?) } fn involves_submodule(a: &EntryMode, b: &EntryMode) -> bool { a.is_commit() || b.is_commit() } /// Allows equal modes or prefers executables bits in case of blobs /// /// Note that this is often not correct as the previous mode of each side should be taken into account so that: /// /// on | on = on /// off | off = off /// on | off || off | on = conflict fn merge_modes(a: EntryMode, b: EntryMode) -> Option { match (a.kind(), b.kind()) { (_, _) if a == b => Some(a), (EntryKind::BlobExecutable, EntryKind::BlobExecutable | EntryKind::Blob) | (EntryKind::Blob, EntryKind::BlobExecutable) => Some(EntryKind::BlobExecutable.into()), _ => None, } } /// Use this version if there is a single common `prev` value for both `a` and `b` to detect /// if the mode was turned on or off. fn merge_modes_prev(a: EntryMode, b: EntryMode, prev: EntryMode) -> Option { match (a.kind(), b.kind()) { (_, _) if a == b => Some(a), (a @ EntryKind::BlobExecutable, b @ (EntryKind::BlobExecutable | EntryKind::Blob)) | (a @ EntryKind::Blob, b @ EntryKind::BlobExecutable) => { let prev = prev.kind(); let changed = if a == prev { b } else { a }; Some( match (prev, changed) { (EntryKind::Blob, EntryKind::BlobExecutable) => EntryKind::BlobExecutable, (EntryKind::BlobExecutable, EntryKind::Blob) => EntryKind::Blob, _ => unreachable!("upper match already assured we only deal with blobs"), } .into(), ) } _ => None, } } fn push_deferred(change_and_idx: (Change, Option), changes: &mut ChangeList) { push_deferred_with_rewrite(change_and_idx, None, changes); } fn push_deferred_with_rewrite( (change, ours_idx): (Change, Option), new_location: Option<(BString, usize)>, changes: &mut ChangeList, ) { changes.push(TrackedChange { inner: change, was_written: false, needs_tree_insertion: Some(ours_idx), rewritten_location: new_location, }); } fn pick_our_tree<'a>(side: ConflictMapping, ours: &'a mut TreeNodes, theirs: &'a mut TreeNodes) -> &'a mut TreeNodes { match side { Original => ours, Swapped => theirs, } } fn pick_our_changes<'a>( side: ConflictMapping, ours: &'a ChangeListRef, theirs: &'a ChangeListRef, ) -> &'a ChangeListRef { match side { Original => ours, Swapped => theirs, } } fn pick_idx(side: ConflictMapping, ours: usize, theirs: usize) -> usize { match side { Original => ours, Swapped => theirs, } } fn pick_our_changes_mut<'a>( side: ConflictMapping, ours: &'a mut ChangeList, theirs: &'a mut ChangeList, ) -> &'a mut ChangeList { match side { Original => ours, Swapped => theirs, } } fn index_entry(mode: &gix_object::tree::EntryMode, id: &gix_hash::ObjectId) -> Option { Some(ConflictIndexEntry { mode: *mode, id: *id, path_hint: None, }) } fn index_entry_at_path( mode: &gix_object::tree::EntryMode, id: &gix_hash::ObjectId, hint: ConflictIndexEntryPathHint, ) -> Option { Some(ConflictIndexEntry { mode: *mode, id: *id, path_hint: Some(hint), }) } gix-merge-0.16.0/src/tree/mod.rs000064400000000000000000000776301046102023000144640ustar 00000000000000use bstr::BString; use gix_diff::{tree_with_rewrites::Change, Rewrites}; /// The error returned by [`tree()`](crate::tree()). #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] pub enum Error { #[error("Could not find ancestor, our or their tree to get started")] FindTree(#[from] gix_object::find::existing_object::Error), #[error("Could not find ancestor, our or their tree iterator to get started")] FindTreeIter(#[from] gix_object::find::existing_iter::Error), #[error("Failed to diff our side or their side")] DiffTree(#[from] gix_diff::tree_with_rewrites::Error), #[error("Could not apply merge result to base tree")] TreeEdit(#[from] gix_object::tree::editor::Error), #[error("Failed to load resource to prepare for blob merge")] BlobMergeSetResource(#[from] crate::blob::platform::set_resource::Error), #[error(transparent)] BlobMergePrepare(#[from] crate::blob::platform::prepare_merge::Error), #[error(transparent)] BlobMerge(#[from] crate::blob::platform::merge::Error), #[error("Failed to write merged blob content as blob to the object database")] WriteBlobToOdb(Box), #[error("The merge was performed, but the binary merge result couldn't be selected as it wasn't found")] MergeResourceNotFound, } /// The outcome produced by [`tree()`](crate::tree()). #[derive(Clone)] pub struct Outcome<'a> { /// The ready-made (but unwritten) *base* tree, including all non-conflicting changes, and the changes that had /// conflicts which could be resolved automatically. /// /// This means, if all of their changes were conflicting, this will be equivalent to the *base* tree. pub tree: gix_object::tree::Editor<'a>, /// The set of conflicts we encountered. Can be empty to indicate there was no conflict. /// Note that conflicts might have been auto-resolved, but they are listed here for completeness. /// Use [`has_unresolved_conflicts()`](Outcome::has_unresolved_conflicts()) to see if any action is needed /// before using [`tree`](Outcome::tree). pub conflicts: Vec, /// `true` if `conflicts` contains only a single [*unresolved* conflict](ResolutionFailure) in the last slot, but /// possibly more [resolved ones](Resolution) before that. /// This also makes this outcome a very partial merge that cannot be completed. /// Only set if [`fail_on_conflict`](Options::fail_on_conflict) is `true`. pub failed_on_first_unresolved_conflict: bool, } /// Determine what should be considered an unresolved conflict. #[derive(Default, Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct TreatAsUnresolved { /// Determine which content merges should be considered unresolved. pub content_merge: treat_as_unresolved::ContentMerge, /// Determine which tree merges should be considered unresolved. pub tree_merge: treat_as_unresolved::TreeMerge, } /// pub mod treat_as_unresolved { use crate::tree::TreatAsUnresolved; /// Which kind of content merges should be considered unresolved? #[derive(Default, Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub enum ContentMerge { /// Content merges that still show conflict markers. #[default] Markers, /// Content merges who would have conflicted if it wasn't for a /// [resolution strategy](crate::blob::builtin_driver::text::Conflict::ResolveWithOurs). ForcedResolution, } /// Which kind of tree merges should be considered unresolved? #[derive(Default, Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub enum TreeMerge { /// All failed renames. Undecidable, /// All failed renames, and the ones where a tree item was renamed to avoid a clash. #[default] EvasiveRenames, /// All of `EvasiveRenames`, and tree merges that would have conflicted but which were resolved /// with a [resolution strategy](super::ResolveWith). ForcedResolution, } /// Instantiation/Presets impl TreatAsUnresolved { /// Return an instance with the highest sensitivity to what should be considered unresolved as it /// includes entries which have been resolved using a [merge strategy](super::ResolveWith). pub fn forced_resolution() -> Self { Self { content_merge: ContentMerge::ForcedResolution, tree_merge: TreeMerge::ForcedResolution, } } /// Return an instance that considers unresolved any conflict that Git would also consider unresolved. /// This is the same as the `default()` implementation. pub fn git() -> Self { Self::default() } /// Only undecidable tree merges and conflict markers are considered unresolved. /// This also means that renamed entries to make space for a conflicting one is considered acceptable, /// making this preset the most lenient. pub fn undecidable() -> Self { Self { content_merge: ContentMerge::Markers, tree_merge: TreeMerge::Undecidable, } } } } impl Outcome<'_> { /// Return `true` if there is any conflict that would still need to be resolved as they would yield undesirable trees. /// This is based on `how` to determine what should be considered unresolved. pub fn has_unresolved_conflicts(&self, how: TreatAsUnresolved) -> bool { self.conflicts.iter().any(|c| c.is_unresolved(how)) } /// Returns `true` if `index` changed as we applied conflicting stages to it, using `how` to determine if a /// conflict should be considered unresolved. /// `removal_mode` decides how unconflicted entries should be removed if they are superseded by /// their conflicted counterparts. /// It's important that `index` is at the state of [`Self::tree`]. /// /// Note that in practice, whenever there is a single [conflict](Conflict), this function will return `true`. pub fn index_changed_after_applying_conflicts( &self, index: &mut gix_index::State, how: TreatAsUnresolved, removal_mode: apply_index_entries::RemovalMode, ) -> bool { apply_index_entries(&self.conflicts, how, index, removal_mode) } } /// A description of a conflict (i.e. merge issue without an auto-resolution) as seen during a [tree-merge](crate::tree()). /// They may have a resolution that was applied automatically, or be left for the caller to resolved. #[derive(Debug, Clone)] pub struct Conflict { /// A record on how the conflict resolution succeeded with `Ok(_)` or failed with `Err(_)`. /// Note that in case of `Err(_)`, edits may still have been made to the tree to aid resolution. /// On failure, one can examine `ours` and `theirs` to potentially find a custom solution. /// Note that the descriptions of resolutions or resolution failures may be swapped compared /// to the actual changes. This is due to changes like `modification|deletion` being treated the /// same as `deletion|modification`, i.e. *ours* is not more privileged than theirs. /// To compensate for that, use [`changes_in_resolution()`](Conflict::changes_in_resolution()). pub resolution: Result, /// The change representing *our* side. pub ours: Change, /// The change representing *their* side. pub theirs: Change, /// An array to store an entry for each stage of the conflict. /// /// * `entries[0]` => Base /// * `entries[1]` => Ours /// * `entries[2]` => Theirs /// /// Note that ours and theirs might be swapped, so one should access it through [`Self::entries()`] to compensate for that. pub entries: [Option; 3], /// Determine how to interpret the `ours` and `theirs` fields. This is used to implement [`Self::changes_in_resolution()`] /// and [`Self::into_parts_by_resolution()`]. map: ConflictMapping, } /// A conflicting entry for insertion into the index. /// It will always be either on stage 1 (ancestor/base), 2 (ours) or 3 (theirs) #[derive(Debug, Clone, Copy)] pub struct ConflictIndexEntry { /// The kind of object at this stage. /// Note that it's possible that this is a directory, for instance if a directory was replaced with a file. pub mode: gix_object::tree::EntryMode, /// The id defining the state of the object. pub id: gix_hash::ObjectId, /// Hidden, maybe one day we can do without? path_hint: Option, } /// A hint for [`apply_index_entries()`] to know which paths to use for an entry. /// This is only used when necessary. #[derive(Debug, Clone, Copy)] enum ConflictIndexEntryPathHint { /// Use the previous path, i.e. rename source. Source, /// Use the current path as it is in the tree. Current, /// Use the path of the final destination, or *their* name. /// It's definitely finicky, as we don't store the actual path and instead refer to it. RenamedOrTheirs, } /// A utility to help define which side is what in the [`Conflict`] type. #[derive(Debug, Clone, Copy, Eq, PartialEq)] enum ConflictMapping { /// The sides are as described in the field documentation, i.e. `ours` is `ours`. Original, /// The sides are the opposite of the field documentation. i.e. `ours` is `theirs` and `theirs` is `ours`. Swapped, } impl ConflictMapping { fn is_swapped(&self) -> bool { matches!(self, ConflictMapping::Swapped) } fn swapped(self) -> ConflictMapping { match self { ConflictMapping::Original => ConflictMapping::Swapped, ConflictMapping::Swapped => ConflictMapping::Original, } } fn to_global(self, global: ConflictMapping) -> ConflictMapping { match global { ConflictMapping::Original => self, ConflictMapping::Swapped => self.swapped(), } } } impl Conflict { /// Return `true` if this instance is considered unresolved based on the criterion specified by `how`. pub fn is_unresolved(&self, how: TreatAsUnresolved) -> bool { use crate::blob; let content_merge_unresolved = |info: &ContentMerge| match how.content_merge { treat_as_unresolved::ContentMerge::Markers => matches!(info.resolution, blob::Resolution::Conflict), treat_as_unresolved::ContentMerge::ForcedResolution => { matches!( info.resolution, blob::Resolution::Conflict | blob::Resolution::CompleteWithAutoResolvedConflict ) } }; match how.tree_merge { treat_as_unresolved::TreeMerge::Undecidable => { self.resolution.is_err() || self.content_merge().is_some_and(|info| content_merge_unresolved(&info)) } treat_as_unresolved::TreeMerge::EvasiveRenames | treat_as_unresolved::TreeMerge::ForcedResolution => { match &self.resolution { Ok(success) => match success { Resolution::SourceLocationAffectedByRename { .. } => false, Resolution::Forced(_) => { how.tree_merge == treat_as_unresolved::TreeMerge::ForcedResolution || self .content_merge() .is_some_and(|merged_blob| content_merge_unresolved(&merged_blob)) } Resolution::OursModifiedTheirsRenamedAndChangedThenRename { merged_blob, final_location, .. } => final_location.is_some() || merged_blob.as_ref().is_some_and(content_merge_unresolved), Resolution::OursModifiedTheirsModifiedThenBlobContentMerge { merged_blob } => { content_merge_unresolved(merged_blob) } }, Err(_failure) => true, } } } } /// Returns the changes of fields `ours` and `theirs` so they match their description in the /// [`Resolution`] or [`ResolutionFailure`] respectively. /// Without this, the sides may appear swapped as `ours|theirs` is treated the same as `theirs/ours` /// if both types are different, like `modification|deletion`. pub fn changes_in_resolution(&self) -> (&Change, &Change) { match self.map { ConflictMapping::Original => (&self.ours, &self.theirs), ConflictMapping::Swapped => (&self.theirs, &self.ours), } } /// Similar to [`changes_in_resolution()`](Self::changes_in_resolution()), but returns the parts /// of the structure so the caller can take ownership. This can be useful when applying your own /// resolutions for resolution failures. pub fn into_parts_by_resolution(self) -> (Result, Change, Change) { match self.map { ConflictMapping::Original => (self.resolution, self.ours, self.theirs), ConflictMapping::Swapped => (self.resolution, self.theirs, self.ours), } } /// Return the index entries for insertion into the index, to match with what's returned by [`Self::changes_in_resolution()`]. pub fn entries(&self) -> [Option; 3] { match self.map { ConflictMapping::Original => self.entries, ConflictMapping::Swapped => [self.entries[0], self.entries[2], self.entries[1]], } } /// Return information about the content merge if it was performed. pub fn content_merge(&self) -> Option { fn failure_merged_blob(failure: &ResolutionFailure) -> Option { match failure { ResolutionFailure::OursRenamedTheirsRenamedDifferently { merged_blob } => *merged_blob, ResolutionFailure::Unknown | ResolutionFailure::OursDirectoryTheirsNonDirectoryTheirsRenamed { .. } | ResolutionFailure::OursModifiedTheirsDeleted | ResolutionFailure::OursModifiedTheirsRenamedTypeMismatch | ResolutionFailure::OursModifiedTheirsDirectoryThenOursRenamed { renamed_unique_path_to_modified_blob: _, } | ResolutionFailure::OursAddedTheirsAddedTypeMismatch { .. } | ResolutionFailure::OursDeletedTheirsRenamed => None, } } match &self.resolution { Ok(success) => match success { Resolution::Forced(failure) => failure_merged_blob(failure), Resolution::SourceLocationAffectedByRename { .. } => None, Resolution::OursModifiedTheirsRenamedAndChangedThenRename { merged_blob, .. } => *merged_blob, Resolution::OursModifiedTheirsModifiedThenBlobContentMerge { merged_blob } => Some(*merged_blob), }, Err(failure) => failure_merged_blob(failure), } } } /// Describes of a conflict involving *our* change and *their* change was specifically resolved. /// /// Note that all resolutions are side-agnostic, so *ours* could also have been *theirs* and vice versa. /// Also note that symlink merges are always done via binary merge, using the same logic. #[derive(Debug, Clone)] pub enum Resolution { /// *ours* had a renamed directory and *theirs* made a change in the now renamed directory. /// We moved that change into its location. SourceLocationAffectedByRename { /// The repository-relative path to the location that the change ended up in after /// being affected by a renamed directory. final_location: BString, }, /// *ours* was a modified blob and *theirs* renamed that blob. /// We moved the changed blob from *ours* to its new location, and merged it successfully. /// If this is a `copy`, the source of the copy was set to be the changed blob as well so both match. OursModifiedTheirsRenamedAndChangedThenRename { /// If one side added the executable bit, we always add it in the merged result. merged_mode: Option, /// If `Some(…)`, the content of the involved blob had to be merged. merged_blob: Option, /// The repository relative path to the location the blob finally ended up in. /// It's `Some()` only if *they* rewrote the blob into a directory which *we* renamed on *our* side. final_location: Option, }, /// *ours* and *theirs* carried changes and where content-merged. /// /// Note that *ours* and *theirs* may also be rewrites with the same destination and mode, /// or additions. OursModifiedTheirsModifiedThenBlobContentMerge { /// The outcome of the content merge. merged_blob: ContentMerge, }, /// This is a resolution failure was forcefully turned into a usable resolution, i.e. [making a choice](ResolveWith) /// is turned into a valid resolution. Forced(ResolutionFailure), } /// Describes of a conflict involving *our* change and *their* failed to be resolved. #[derive(Debug, Clone)] pub enum ResolutionFailure { /// *ours* was renamed, but *theirs* was renamed differently. Both versions will be present in the tree, OursRenamedTheirsRenamedDifferently { /// If `Some(…)`, the content of the involved blob had to be merged. merged_blob: Option, }, /// *ours* was modified, but *theirs* was turned into a directory, so *ours* was renamed to a non-conflicting path. OursModifiedTheirsDirectoryThenOursRenamed { /// The path at which `ours` can be found in the tree - it's in the same directory that it was in before. renamed_unique_path_to_modified_blob: BString, }, /// *ours* is a directory, but *theirs* is a non-directory (i.e. file), which wants to be in its place, even though /// *ours* has a modification in that subtree. /// Rename *theirs* to retain that modification. /// /// Important: there is no actual modification on *ours* side, so *ours* is filled in with *theirs* as the data structure /// cannot represent this case. // TODO: Can we have a better data-structure? This would be for a rewrite though. OursDirectoryTheirsNonDirectoryTheirsRenamed { /// The non-conflicting path of *their* non-tree entry. renamed_unique_path_of_theirs: BString, }, /// *ours* was added (or renamed into place) with a different mode than theirs, e.g. blob and symlink, and we kept /// the symlink in its original location, renaming the other side to `their_unique_location`. OursAddedTheirsAddedTypeMismatch { /// The location at which *their* state was placed to resolve the name and type clash, named to indicate /// where the entry is coming from. their_unique_location: BString, }, /// *ours* was modified, and they renamed the same file, but there is also a non-mergable type-change. /// Here we keep both versions of the file. OursModifiedTheirsRenamedTypeMismatch, /// *ours* was deleted, but *theirs* was renamed. OursDeletedTheirsRenamed, /// *ours* was modified and *theirs* was deleted. We keep the modified one and ignore the deletion. OursModifiedTheirsDeleted, /// *ours* and *theirs* are in an untested state so it can't be handled yet, and is considered a conflict /// without adding our *or* their side to the resulting tree. Unknown, } /// Information about a blob content merge for use in a [`Resolution`]. /// Note that content merges always count as success to avoid duplication of cases, which forces callers /// to check for the [`resolution`](Self::resolution) field. #[derive(Debug, Copy, Clone)] pub struct ContentMerge { /// The fully merged blob. pub merged_blob_id: gix_hash::ObjectId, /// Identify the kind of resolution of the blob merge. Note that it may be conflicting. pub resolution: crate::blob::Resolution, } /// A way to configure [`tree()`](crate::tree()). #[derive(Default, Debug, Clone)] pub struct Options { /// If *not* `None`, rename tracking will be performed when determining the changes of each side of the merge. /// /// Note that [empty blobs](Rewrites::track_empty) should not be tracked for best results. pub rewrites: Option, /// Decide how blob-merges should be done. This relates to if conflicts can be resolved or not. pub blob_merge: crate::blob::platform::merge::Options, /// The context to use when invoking merge-drivers. pub blob_merge_command_ctx: gix_command::Context, /// If `Some(what-is-unresolved)`, the first unresolved conflict will cause the entire merge to stop. /// This is useful to see if there is any conflict, without performing the whole operation, something /// that can be very relevant during merges that would cause a lot of blob-diffs. pub fail_on_conflict: Option, /// This value also affects the size of merge-conflict markers, to allow differentiating /// merge conflicts on each level, for any value greater than 0, with values `N` causing `N*2` /// markers to be added to the configured value. /// /// This is used automatically when merging merge-bases recursively. pub marker_size_multiplier: u8, /// If `None`, when symlinks clash *ours* will be chosen and a conflict will occur. /// Otherwise, the same logic applies as for the merge of binary resources. pub symlink_conflicts: Option, /// If `None`, tree irreconcilable tree conflicts will result in [resolution failures](ResolutionFailure). /// Otherwise, one can choose a side. Note that it's still possible to determine that auto-resolution happened /// despite this choice, which allows carry forward the conflicting information, possibly for later resolution. /// If `Some(…)`, irreconcilable conflicts are reconciled by making a choice. /// Note that [`Conflict::entries()`] will still be set, to not degenerate information, even though they then represent /// the entries what would fit the index if no forced resolution was performed. /// It's up to the caller to handle that information mindfully. pub tree_conflicts: Option, } /// Decide how to resolve tree-related conflicts, but only those that have [no way of being correct](ResolutionFailure). #[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] pub enum ResolveWith { /// On irreconcilable conflict, choose neither *our* nor *their* state, but keep the common *ancestor* state instead. Ancestor, /// On irreconcilable conflict, choose *our* side. /// /// Note that in order to get something equivalent to *theirs*, put *theirs* into the side of *ours*, /// swapping the sides essentially. Ours, } pub(super) mod function; mod utils; /// pub mod apply_index_entries { /// Determines how we deal with the removal of unconflicted entries if these are superseded by their conflicted counterparts, /// i.e. stage 1, 2 and 3. #[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] pub enum RemovalMode { /// Add the [`gix_index::entry::Flags::REMOVE`] flag to entries that are to be removed. /// /// **Note** that this also means that unconflicted and conflicted stages will be visible in the same index. /// When written, entries marked for removal will automatically be ignored. However, this also means that /// one must not use the in-memory index or take specific care of entries that are marked for removal. Mark, /// Entries marked for removal (even those that were already marked) will be removed from memory at the end. /// /// This is an expensive step that leaves a consistent index, ready for use. Prune, } pub(super) mod function { use std::collections::{hash_map, HashMap}; use bstr::{BStr, ByteSlice}; use crate::tree::{ apply_index_entries::RemovalMode, Conflict, ConflictIndexEntryPathHint, Resolution, ResolutionFailure, TreatAsUnresolved, }; /// Returns `true` if `index` changed as we applied conflicting stages to it, using `how` to determine if a /// conflict should be considered unresolved. /// Once a stage of a path conflicts, the unconflicting stage is removed even though it might be the one /// that is currently checked out. /// This removal is only done by flagging it with [gix_index::entry::Flags::REMOVE], which means /// these entries won't be written back to disk but will still be present in the index if `removal_mode` /// is [`RemovalMode::Mark`]. For proper removal, choose [`RemovalMode::Prune`]. /// It's important that `index` matches the tree that was produced as part of the merge that also /// brought about `conflicts`, or else this function will fail if it cannot find the path matching /// the conflicting entries. /// /// Note that in practice, whenever there is a single [conflict](Conflict), this function will return `true`. /// Errors can only occour if `index` isn't the one created from the merged tree that produced the `conflicts`. pub fn apply_index_entries( conflicts: &[Conflict], how: TreatAsUnresolved, index: &mut gix_index::State, removal_mode: RemovalMode, ) -> bool { if index.is_sparse() { gix_trace::error!("Refusing to apply index entries to sparse index - it's not tested yet"); return false; } let len = index.entries().len(); let mut idx_by_path_stage = HashMap::<(gix_index::entry::Stage, &BStr), usize>::default(); for conflict in conflicts.iter().filter(|c| c.is_unresolved(how)) { let (renamed_path, current_path): (Option<&BStr>, &BStr) = match &conflict.resolution { Ok(success) => match success { Resolution::Forced(_) => continue, Resolution::SourceLocationAffectedByRename { final_location } => { (Some(final_location.as_bstr()), final_location.as_bstr()) } Resolution::OursModifiedTheirsRenamedAndChangedThenRename { final_location, .. } => ( final_location.as_ref().map(|p| p.as_bstr()), conflict.changes_in_resolution().1.location(), ), Resolution::OursModifiedTheirsModifiedThenBlobContentMerge { .. } => { (None, conflict.ours.location()) } }, Err(failure) => match failure { ResolutionFailure::OursDirectoryTheirsNonDirectoryTheirsRenamed { renamed_unique_path_of_theirs, } => (Some(renamed_unique_path_of_theirs.as_bstr()), conflict.ours.location()), ResolutionFailure::OursRenamedTheirsRenamedDifferently { .. } => { (Some(conflict.theirs.location()), conflict.ours.location()) } ResolutionFailure::OursModifiedTheirsRenamedTypeMismatch | ResolutionFailure::OursDeletedTheirsRenamed | ResolutionFailure::OursModifiedTheirsDeleted | ResolutionFailure::Unknown => (None, conflict.ours.location()), ResolutionFailure::OursModifiedTheirsDirectoryThenOursRenamed { renamed_unique_path_to_modified_blob, } => ( Some(renamed_unique_path_to_modified_blob.as_bstr()), conflict.ours.location(), ), ResolutionFailure::OursAddedTheirsAddedTypeMismatch { their_unique_location } => { (Some(their_unique_location.as_bstr()), conflict.ours.location()) } }, }; let source_path = conflict.ours.source_location(); let entries_with_stage = conflict.entries().into_iter().enumerate().filter_map(|(idx, entry)| { entry.filter(|e| e.mode.is_no_tree()).map(|e| { ( match idx { 0 => gix_index::entry::Stage::Base, 1 => gix_index::entry::Stage::Ours, 2 => gix_index::entry::Stage::Theirs, _ => unreachable!("fixed size array with three items"), }, match e.path_hint { None => renamed_path.unwrap_or(current_path), Some(ConflictIndexEntryPathHint::Source) => source_path, Some(ConflictIndexEntryPathHint::Current) => current_path, Some(ConflictIndexEntryPathHint::RenamedOrTheirs) => { renamed_path.unwrap_or_else(|| conflict.changes_in_resolution().1.location()) } }, e, ) }) }); if !entries_with_stage.clone().any(|(_, path, _)| { index .entry_index_by_path_and_stage_bounded(path, gix_index::entry::Stage::Unconflicted, len) .is_some() }) { continue; } for (stage, path, entry) in entries_with_stage { if let Some(pos) = index.entry_index_by_path_and_stage_bounded(path, gix_index::entry::Stage::Unconflicted, len) { index.entries_mut()[pos].flags.insert(gix_index::entry::Flags::REMOVE); } match idx_by_path_stage.entry((stage, path)) { hash_map::Entry::Occupied(map_entry) => { // This can happen due to the way the algorithm works. // The same happens in Git, but it stores the index-related data as part of its deduplicating tree. // We store each conflict we encounter, which also may duplicate their index entries, sometimes, but // with different values. The most recent value wins. // Instead of trying to deduplicate the index entries when the merge runs, we put the cost // to the tree-assembly - there is no way around it. let index_entry = &mut index.entries_mut()[*map_entry.get()]; index_entry.mode = entry.mode.into(); index_entry.id = entry.id; } hash_map::Entry::Vacant(map_entry) => { map_entry.insert(index.entries().len()); index.dangerously_push_entry( Default::default(), entry.id, stage.into(), entry.mode.into(), path, ); } } } } let res = index.entries().len() != len; match removal_mode { RemovalMode::Mark => {} RemovalMode::Prune => { index.remove_entries(|_, _, e| e.flags.contains(gix_index::entry::Flags::REMOVE)); } } index.sort_entries(); res } } } pub use apply_index_entries::function::apply_index_entries; gix-merge-0.16.0/src/tree/utils.rs000064400000000000000000000573371046102023000150470ustar 00000000000000//! ## About `debug_assert!() //! //! The idea is to have code that won't panic in production. Thus, if in production that assertion would fail, //! we will rather let the code run and hope it will either be correct enough or fail in more graceful ways later. //! //! Once such a case becomes a bug and is reproduced in testing, the debug-assertion will kick in and hopefully //! contribute to finding a fix faster. use std::collections::HashMap; use bstr::{BStr, BString, ByteSlice, ByteVec}; use gix_diff::tree_with_rewrites::{Change, ChangeRef}; use gix_hash::ObjectId; use gix_object::{ tree, tree::{EntryKind, EntryMode}, }; use crate::{ blob::{builtin_driver::binary::Pick, ResourceKind}, tree::{ Conflict, ConflictIndexEntry, ConflictIndexEntryPathHint, ConflictMapping, Error, Options, Resolution, ResolutionFailure, }, }; /// Assuming that `their_location` is the destination of *their* rewrite, check if *it* passes /// over a directory rewrite in *our* tree. If so, rewrite it so that we get the path /// it would have had if it had been renamed along with *our* directory. pub fn possibly_rewritten_location( check_tree: &TreeNodes, their_location: &BStr, our_changes: &ChangeListRef, ) -> Option { check_tree.check_conflict(their_location).and_then(|pc| match pc { PossibleConflict::PassedRewrittenDirectory { change_idx } => { let passed_change = &our_changes[change_idx]; rewrite_location_with_renamed_directory(their_location, &passed_change.inner) } _ => None, }) } pub fn rewrite_location_with_renamed_directory(their_location: &BStr, passed_change: &Change) -> Option { match passed_change { Change::Rewrite { source_location, location, .. } if passed_change.entry_mode().is_tree() => { // This is safe even without dealing with slashes as we found this rewrite // by walking each component, and we know it's a tree for added safety. let suffix = their_location.strip_prefix(source_location.as_bytes())?; let mut rewritten = location.to_owned(); rewritten.push_str(suffix); Some(rewritten) } _ => None, } } /// Produce a unique path within the directory that contains the file at `file_path` like `a/b`, using `editor` /// and `tree` to assure unique names, to obtain the tree at `a/` and `side_name` to more clearly signal /// where the file is coming from. pub fn unique_path_in_tree( file_path: &BStr, editor: &tree::Editor<'_>, tree: &TreeNodes, side_name: &BStr, ) -> Result { let mut buf = file_path.to_owned(); buf.push(b'~'); buf.extend( side_name .as_bytes() .iter() .copied() .map(|b| if b == b'/' { b'_' } else { b }), ); // We could use a cursor here, but clashes are so unlikely that this wouldn't be meaningful for performance. let base_len = buf.len(); let mut suffix = 0; while editor.get(to_components_bstring_ref(&buf)).is_some() || tree.check_conflict(buf.as_bstr()).is_some() { buf.truncate(base_len); buf.push_str(format!("_{suffix}")); suffix += 1; } Ok(buf) } /// Perform a merge between two blobs and return the result of its object id. #[allow(clippy::too_many_arguments)] pub fn perform_blob_merge( mut labels: crate::blob::builtin_driver::text::Labels<'_>, objects: &impl gix_object::FindObjectOrHeader, blob_merge: &mut crate::blob::Platform, buf: &mut Vec, write_blob_to_odb: &mut impl FnMut(&[u8]) -> Result, (our_location, our_id, our_mode): (&BString, ObjectId, EntryMode), (their_location, their_id, their_mode): (&BString, ObjectId, EntryMode), (previous_location, previous_id, previous_mode): (&BString, ObjectId, EntryMode), (extra_markers, outer_side): (u8, ConflictMapping), options: &Options, ) -> Result<(ObjectId, crate::blob::Resolution), Error> where E: Into>, { if our_id == their_id { // This can happen if the merge modes are different. debug_assert_ne!( our_mode, their_mode, "BUG: we must think anything has to be merged if the modes and the ids are the same" ); return Ok((their_id, crate::blob::Resolution::Complete)); } if matches!(our_mode.kind(), EntryKind::Link) && matches!(their_mode.kind(), EntryKind::Link) { let (pick, resolution) = crate::blob::builtin_driver::binary(options.symlink_conflicts); let (our_id, their_id) = match outer_side { ConflictMapping::Original => (our_id, their_id), ConflictMapping::Swapped => (their_id, our_id), }; let id = match pick { Pick::Ancestor => previous_id, Pick::Ours => our_id, Pick::Theirs => their_id, }; return Ok((id, resolution)); } let (our_kind, their_kind) = match outer_side { ConflictMapping::Original => (ResourceKind::CurrentOrOurs, ResourceKind::OtherOrTheirs), ConflictMapping::Swapped => (ResourceKind::OtherOrTheirs, ResourceKind::CurrentOrOurs), }; blob_merge.set_resource(our_id, our_mode.kind(), our_location.as_bstr(), our_kind, objects)?; blob_merge.set_resource( their_id, their_mode.kind(), their_location.as_bstr(), their_kind, objects, )?; blob_merge.set_resource( previous_id, previous_mode.kind(), previous_location.as_bstr(), ResourceKind::CommonAncestorOrBase, objects, )?; fn combined(side: &BStr, location: &BString) -> BString { let mut buf = side.to_owned(); buf.push_byte(b':'); buf.push_str(location); buf } if outer_side.is_swapped() { (labels.current, labels.other) = (labels.other, labels.current); } let (ancestor, current, other); let labels = if our_location == their_location { labels } else { ancestor = labels.ancestor.map(|side| combined(side, previous_location)); current = labels.current.map(|side| combined(side, our_location)); other = labels.other.map(|side| combined(side, their_location)); crate::blob::builtin_driver::text::Labels { ancestor: ancestor.as_ref().map(|n| n.as_bstr()), current: current.as_ref().map(|n| n.as_bstr()), other: other.as_ref().map(|n| n.as_bstr()), } }; let prep = blob_merge.prepare_merge(objects, with_extra_markers(options, extra_markers))?; let (pick, resolution) = prep.merge(buf, labels, &options.blob_merge_command_ctx)?; let merged_blob_id = prep .id_by_pick(pick, buf, write_blob_to_odb) .map_err(|err| Error::WriteBlobToOdb(err.into()))? .ok_or(Error::MergeResourceNotFound)?; Ok((merged_blob_id, resolution)) } fn with_extra_markers(opts: &Options, extra_makers: u8) -> crate::blob::platform::merge::Options { let mut out = opts.blob_merge; if let crate::blob::builtin_driver::text::Conflict::Keep { marker_size, .. } = &mut out.text.conflict { *marker_size = marker_size.saturating_add(extra_makers.saturating_add(opts.marker_size_multiplier.saturating_mul(2))); } out } /// A way to attach metadata to each change. #[derive(Debug)] pub struct TrackedChange { /// The actual change pub inner: Change, /// If `true`, this change counts as written to the tree using a [`tree::Editor`]. pub was_written: bool, /// If `Some(ours_idx_to_ignore)`, this change must be placed into the tree before handling it. /// This makes sure that new changes aren't visible too early, which would mean the algorithm /// knows things too early which can be misleading. /// The `ours_idx_to_ignore` assures that the same rewrite won't be used as matching side, which /// would lead to strange effects. Only set if it's a rewrite though. pub needs_tree_insertion: Option>, /// A new `(location, change_idx)` pair for the change that can happen if the location is touching a rewrite in a parent /// directory, but otherwise doesn't have a match. This means we shall redo the operation but with /// the changed path. /// The second tuple entry `change_idx` is the change-idx we passed over, which refers to the other side that interfered. pub rewritten_location: Option<(BString, usize)>, } pub type ChangeList = Vec; pub type ChangeListRef = [TrackedChange]; /// Only keep leaf nodes, or trees that are the renamed, pushing `change` on `changes`. /// Doing so makes it easy to track renamed or rewritten or copied directories, and properly /// handle *their* changes that fall within them. /// Note that it also rewrites `change` if it is a copy, turning it into an addition so copies don't have an effect /// on the merge algorithm. pub fn track(change: ChangeRef<'_>, changes: &mut ChangeList) { if change.entry_mode().is_tree() && matches!(change, ChangeRef::Modification { .. }) { return; } let is_tree = change.entry_mode().is_tree(); changes.push(TrackedChange { inner: match change.into_owned() { Change::Rewrite { id, entry_mode, location, relation, copy, .. } if copy => Change::Addition { location, relation, entry_mode, id, }, other => other, }, was_written: is_tree, needs_tree_insertion: None, rewritten_location: None, }); } /// Unconditionally apply `change` to `editor`. pub fn apply_change( editor: &mut tree::Editor<'_>, change: &Change, alternative_location: Option<&BString>, ) -> Result<(), tree::editor::Error> { use to_components_bstring_ref as to_components; if change.entry_mode().is_tree() { return Ok(()); } let (location, mode, id) = match change { Change::Addition { location, entry_mode, id, .. } | Change::Modification { location, entry_mode, id, .. } => (location, entry_mode, id), Change::Deletion { location, .. } => { editor.remove(to_components(alternative_location.unwrap_or(location)))?; return Ok(()); } Change::Rewrite { source_location, entry_mode, id, location, copy, .. } => { if !*copy { editor.remove(to_components(source_location))?; } (location, entry_mode, id) } }; editor.upsert( to_components(alternative_location.unwrap_or(location)), mode.kind(), *id, )?; Ok(()) } /// A potential conflict that needs to be checked. It comes in several varieties and always happens /// if paths overlap in some way between *theirs* and *ours*. #[derive(Debug)] pub enum PossibleConflict { /// *our* changes have a tree here, but *they* place a non-tree or edit an existing item (that we removed). TreeToNonTree { /// The possibly available change at this node. change_idx: Option, }, /// A non-tree in *our* tree turned into a tree in *theirs* - this can be done with additions in *theirs*, /// or if we added a blob, while they added a directory. NonTreeToTree { /// The possibly available change at this node. change_idx: Option, }, /// A perfect match, i.e. *our* change at `a/b/c` corresponds to *their* change at the same path. Match { /// The index to *our* change at *their* path. change_idx: usize, }, /// *their* change at `a/b/c` passed `a/b` which is an index to *our* change indicating a directory that was rewritten, /// with all its contents being renamed. However, *theirs* has been added *into* that renamed directory. PassedRewrittenDirectory { change_idx: usize }, } impl PossibleConflict { pub(super) fn change_idx(&self) -> Option { match self { PossibleConflict::TreeToNonTree { change_idx, .. } | PossibleConflict::NonTreeToTree { change_idx, .. } => { *change_idx } PossibleConflict::Match { change_idx, .. } | PossibleConflict::PassedRewrittenDirectory { change_idx, .. } => Some(*change_idx), } } } /// The flat list of all tree-nodes so we can avoid having a linked-tree using pointers /// which is useful for traversal and initial setup as that can then trivially be non-recursive. pub struct TreeNodes(Vec); /// Trees lead to other trees, or leafs (without children), and it can be represented by a renamed directory. #[derive(Debug, Default, Clone)] struct TreeNode { /// A mapping of path components to their children to quickly see if `theirs` in some way is potentially /// conflicting with `ours`. children: HashMap, /// The index to a change, which is always set if this is a leaf node (with no children), and if there are children and this /// is a rewritten tree. change_idx: Option, /// Keep track of where the location of this node is derived from. location: ChangeLocation, } #[derive(Debug, Default, Clone, Copy)] enum ChangeLocation { /// The change is at its current (and only) location, or in the source location of a rename. #[default] CurrentLocation, /// This is always the destination of a rename. RenamedLocation, } impl TreeNode { fn is_leaf_node(&self) -> bool { self.children.is_empty() } } impl TreeNodes { pub fn new() -> Self { TreeNodes(vec![TreeNode::default()]) } /// Insert our `change` at `change_idx`, into a linked-tree, assuring that each `change` is non-conflicting /// with this tree structure, i.e. each leaf path is only seen once. /// Note that directories can be added in between. pub fn track_change(&mut self, change: &Change, change_idx: usize) { for (path, location_hint) in [ Some((change.source_location(), ChangeLocation::CurrentLocation)), match change { Change::Addition { .. } | Change::Deletion { .. } | Change::Modification { .. } => None, Change::Rewrite { location, .. } => Some((location.as_bstr(), ChangeLocation::RenamedLocation)), }, ] .into_iter() .flatten() { let mut components = to_components(path).peekable(); let mut next_index = self.0.len(); let mut cursor = &mut self.0[0]; while let Some(component) = components.next() { let is_last = components.peek().is_none(); match cursor.children.get(component).copied() { None => { let new_node = TreeNode { children: Default::default(), change_idx: is_last.then_some(change_idx), location: location_hint, }; cursor.children.insert(component.to_owned(), next_index); self.0.push(new_node); cursor = &mut self.0[next_index]; next_index += 1; } Some(index) => { cursor = &mut self.0[index]; if is_last && !cursor.is_leaf_node() { // NOTE: we might encounter the same path multiple times in rare conditions. // At least we avoid overwriting existing intermediate changes, for good measure. if cursor.change_idx.is_none() { cursor.change_idx = Some(change_idx); } } } } } } } /// Search the tree with `our` changes for `theirs` by [`source_location()`](Change::source_location())). /// If there is an entry but both are the same, or if there is no entry, return `None`. pub fn check_conflict(&self, theirs_location: &BStr) -> Option { if self.0.len() == 1 { return None; } let components = to_components(theirs_location); let mut cursor = &self.0[0]; let mut cursor_idx = 0; let mut intermediate_change = None; for component in components { if cursor.change_idx.is_some() { intermediate_change = cursor.change_idx.map(|change_idx| (change_idx, cursor_idx)); } match cursor.children.get(component).copied() { // *their* change is outside *our* tree None => { let res = if cursor.is_leaf_node() { Some(PossibleConflict::NonTreeToTree { change_idx: cursor.change_idx, }) } else { // a change somewhere else, i.e. `a/c` and we know `a/b` only. intermediate_change.and_then(|(change, cursor_idx)| { let cursor = &self.0[cursor_idx]; // If this is a destination location of a rename, then the `their_location` // is already at the right spot, and we can just ignore it. if matches!(cursor.location, ChangeLocation::CurrentLocation) { Some(PossibleConflict::PassedRewrittenDirectory { change_idx: change }) } else { None } }) }; return res; } Some(child_idx) => { cursor_idx = child_idx; cursor = &self.0[cursor_idx]; } } } if cursor.is_leaf_node() { PossibleConflict::Match { change_idx: cursor.change_idx.expect("leaf nodes always have a change"), } } else { PossibleConflict::TreeToNonTree { change_idx: cursor.change_idx, } } .into() } /// Compare both changes and return `true` if they are *not* exactly the same. /// One two changes are the same, they will have the same effect. /// Since this is called after [`Self::check_conflict`], *our* change will not be applied, /// only theirs, which naturally avoids double-application /// (which shouldn't have side effects, but let's not risk it) pub fn is_not_same_change_in_possible_conflict( &self, theirs: &Change, conflict: &PossibleConflict, our_changes: &ChangeListRef, ) -> bool { conflict .change_idx() .is_none_or(|idx| our_changes[idx].inner != *theirs) } pub fn remove_existing_leaf(&mut self, location: &BStr) { self.remove_leaf_inner(location, true); } pub fn remove_leaf(&mut self, location: &BStr) { self.remove_leaf_inner(location, false); } fn remove_leaf_inner(&mut self, location: &BStr, must_exist: bool) { let mut components = to_components(location).peekable(); let mut cursor = &mut self.0[0]; while let Some(component) = components.next() { match cursor.children.get(component).copied() { None => debug_assert!(!must_exist, "didn't find '{location}' for removal"), Some(existing_idx) => { let is_last = components.peek().is_none(); if is_last { cursor.children.remove(component); cursor = &mut self.0[existing_idx]; debug_assert!( cursor.is_leaf_node(), "BUG: we should really only try to remove leaf nodes: {cursor:?}" ); cursor.change_idx = None; } else { cursor = &mut self.0[existing_idx]; } } } } } /// Insert `new_change` which affects this tree into it and put it into `storage` to obtain the index. /// Panic if that change already exists as it must be made so that it definitely doesn't overlap with this tree. pub fn insert(&mut self, new_change: &Change, new_change_idx: usize) { let mut next_index = self.0.len(); let mut cursor = &mut self.0[0]; for component in to_components(new_change.location()) { match cursor.children.get(component).copied() { None => { cursor.children.insert(component.to_owned(), next_index); self.0.push(TreeNode::default()); cursor = &mut self.0[next_index]; next_index += 1; } Some(existing_idx) => { cursor = &mut self.0[existing_idx]; } } } debug_assert!( !matches!(new_change, Change::Rewrite { .. }), "BUG: we thought we wouldn't do that current.location is related?" ); cursor.change_idx = Some(new_change_idx); cursor.location = ChangeLocation::CurrentLocation; } } pub fn to_components_bstring_ref(rela_path: &BString) -> impl Iterator { rela_path.split(|b| *b == b'/').map(Into::into) } pub fn to_components(rela_path: &BStr) -> impl Iterator { rela_path.split(|b| *b == b'/').map(Into::into) } impl Conflict { pub(super) fn without_resolution( resolution: ResolutionFailure, changes: (&Change, &Change, ConflictMapping, ConflictMapping), entries: [Option; 3], ) -> Self { Conflict::maybe_resolved(Err(resolution), changes, entries) } pub(super) fn with_resolution( resolution: Resolution, changes: (&Change, &Change, ConflictMapping, ConflictMapping), entries: [Option; 3], ) -> Self { Conflict::maybe_resolved(Ok(resolution), changes, entries) } fn maybe_resolved( resolution: Result, (ours, theirs, map, outer_map): (&Change, &Change, ConflictMapping, ConflictMapping), entries: [Option; 3], ) -> Self { Conflict { resolution, ours: ours.clone(), theirs: theirs.clone(), entries, map: map.to_global(outer_map), } } pub(super) fn unknown(changes: (&Change, &Change, ConflictMapping, ConflictMapping)) -> Self { let (source_mode, source_id) = changes.0.source_entry_mode_and_id(); let (our_mode, our_id) = changes.0.entry_mode_and_id(); let (their_mode, their_id) = changes.1.entry_mode_and_id(); let entries = [ Some(ConflictIndexEntry { mode: source_mode, id: source_id.into(), path_hint: Some(ConflictIndexEntryPathHint::Source), }), Some(ConflictIndexEntry { mode: our_mode, id: our_id.into(), path_hint: Some(ConflictIndexEntryPathHint::Current), }), Some(ConflictIndexEntry { mode: their_mode, id: their_id.into(), path_hint: Some(ConflictIndexEntryPathHint::RenamedOrTheirs), }), ]; Conflict::maybe_resolved(Err(ResolutionFailure::Unknown), changes, entries) } }