gix-blame-0.13.0/.cargo_vcs_info.json0000644000000001471046102023000130060ustar { "git": { "sha1": "53f880c7604232c367870088176e42efd8a5b783" }, "path_in_vcs": "gix-blame" }gix-blame-0.13.0/Cargo.lock0000644000000777361046102023000110030ustar # 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 = "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 = "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 = "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", ] [[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-blame" version = "0.13.0" dependencies = [ "gix-commitgraph", "gix-date", "gix-diff", "gix-error", "gix-hash", "gix-object", "gix-revwalk", "gix-trace", "gix-traverse", "gix-worktree", "pretty_assertions", "smallvec", "thiserror", ] [[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", "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", "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-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", "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-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 = "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", ] [[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" [[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 = "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-blame-0.13.0/Cargo.toml0000644000000101601046102023000110000ustar # 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-blame" version = "0.13.0" authors = [ "Christoph Rüßler ", "Sebastian Thiel ", ] build = false include = [ "/src/**/*", "/LICENSE-*", ] autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "A crate of the gitoxide project dedicated to implementing a 'blame' algorithm" readme = false license = "MIT OR Apache-2.0" repository = "https://github.com/GitoxideLabs/gitoxide" [package.metadata.docs.rs] features = ["sha1"] [features] sha1 = [ "gix-commitgraph/sha1", "gix-diff/sha1", "gix-hash/sha1", "gix-object/sha1", "gix-revwalk/sha1", "gix-traverse/sha1", "gix-worktree/sha1", ] [lib] name = "gix_blame" path = "src/lib.rs" [dependencies.gix-commitgraph] version = "^0.37.0" [dependencies.gix-date] version = "^0.15.3" [dependencies.gix-diff] version = "^0.63.0" features = ["blob"] default-features = false [dependencies.gix-error] version = "^0.2.3" [dependencies.gix-hash] version = "^0.25.0" [dependencies.gix-object] version = "^0.60.0" [dependencies.gix-revwalk] version = "^0.31.0" [dependencies.gix-trace] version = "^0.1.19" [dependencies.gix-traverse] version = "^0.57.0" [dependencies.gix-worktree] version = "^0.52.0" features = ["attributes"] default-features = false [dependencies.smallvec] version = "1.15.1" [dependencies.thiserror] version = "2.0.18" [dev-dependencies.pretty_assertions] version = "1.4.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-blame-0.13.0/Cargo.toml.orig000064400000000000000000000034121046102023000144410ustar 00000000000000lints.workspace = true [package] name = "gix-blame" version = "0.13.0" repository = "https://github.com/GitoxideLabs/gitoxide" license = "MIT OR Apache-2.0" description = "A crate of the gitoxide project dedicated to implementing a 'blame' algorithm" authors = ["Christoph Rüßler ", "Sebastian Thiel "] edition = "2021" rust-version = "1.82" include = ["/src/**/*", "/LICENSE-*"] [features] ## Enable support for the SHA-1 hash by forwarding the feature to dependencies. sha1 = [ "gix-commitgraph/sha1", "gix-diff/sha1", "gix-hash/sha1", "gix-object/sha1", "gix-revwalk/sha1", "gix-traverse/sha1", "gix-worktree/sha1", ] [dependencies] gix-error = { version = "^0.2.3", path = "../gix-error" } gix-commitgraph = { version = "^0.37.0", path = "../gix-commitgraph" } gix-revwalk = { version = "^0.31.0", path = "../gix-revwalk" } gix-trace = { version = "^0.1.19", path = "../gix-trace" } gix-date = { version = "^0.15.3", path = "../gix-date" } gix-diff = { version = "^0.63.0", path = "../gix-diff", default-features = false, features = ["blob"] } gix-object = { version = "^0.60.0", path = "../gix-object" } gix-hash = { version = "^0.25.0", path = "../gix-hash" } gix-worktree = { version = "^0.52.0", path = "../gix-worktree", default-features = false, features = ["attributes"] } gix-traverse = { version = "^0.57.0", path = "../gix-traverse" } smallvec = "1.15.1" thiserror = "2.0.18" [dev-dependencies] gix-ref = { path = "../gix-ref" } gix-filter = { path = "../gix-filter" } gix-fs = { path = "../gix-fs" } gix-index = { path = "../gix-index" } gix-odb = { path = "../gix-odb" } gix-testtools = { path = "../tests/tools" } pretty_assertions = "1.4.0" [package.metadata.docs.rs] features = ["sha1"] gix-blame-0.13.0/LICENSE-APACHE000064400000000000000000000236761046102023000135140ustar 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-blame-0.13.0/LICENSE-MIT000064400000000000000000000017771046102023000132220ustar 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-blame-0.13.0/src/error.rs000064400000000000000000000034321046102023000140420ustar 00000000000000use gix_object::bstr::BString; /// The error returned by [file()](crate::file()). #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] pub enum Error { #[error("No commit was given")] EmptyTraversal, #[error(transparent)] BlobDiffSetResource(#[from] gix_diff::blob::platform::set_resource::Error), #[error(transparent)] BlobDiffPrepare(#[from] gix_diff::blob::platform::prepare_diff::Error), #[error("The file to blame at '{file_path}' wasn't found in the first commit at {commit_id}")] FileMissing { /// The file-path to the object to blame. file_path: BString, /// The commit whose tree didn't contain `file_path`. commit_id: gix_hash::ObjectId, }, #[error("Couldn't find commit or tree in the object database")] FindObject(#[from] gix_object::find::Error), #[error("Could not find existing blob or commit")] FindExistingObject(#[from] gix_object::find::existing_object::Error), #[error("Could not find existing iterator over a tree")] FindExistingIter(#[from] gix_object::find::existing_iter::Error), #[error("Failed to obtain the next commit in the commit-graph traversal")] Traverse(#[source] Box), #[error(transparent)] DiffTree(#[from] gix_diff::tree::Error), #[error(transparent)] DiffTreeWithRewrites(#[from] gix_diff::tree_with_rewrites::Error), #[error("Invalid line range was given, line range is expected to be a 1-based inclusive range in the format ','")] InvalidOneBasedLineRange, #[error("Failure to decode commit during traversal")] DecodeCommit(#[from] gix_object::decode::Error), #[error("Failed to get parent from commitgraph during traversal")] GetParentFromCommitGraph(#[from] gix_error::Message), } gix-blame-0.13.0/src/file/function.rs000064400000000000000000001020651046102023000154570ustar 00000000000000use std::num::NonZeroU32; use gix_diff::{blob::TokenSource, tree::Visit}; use gix_hash::ObjectId; use gix_object::{ bstr::{BStr, BString}, FindExt, }; use gix_traverse::commit::find as find_commit; use smallvec::SmallVec; use super::{process_changes, Change, UnblamedHunk}; use crate::{types::BlamePathEntry, BlameEntry, Error, Options, Outcome, Statistics}; /// Produce a list of consecutive [`BlameEntry`] instances to indicate in which commits the ranges of the file /// at `suspect:` originated in. /// /// ## Parameters /// /// * `odb` /// - Access to database objects, also for used for diffing. /// - Should have an object cache for good diff performance. /// * `suspect` /// - The first commit to be responsible for parts of `file_path`. /// * `cache` /// - Optionally, the commitgraph cache. /// * `resource_cache` /// - Used for diffing trees. /// * `file_path` /// - A *slash-separated* worktree-relative path to the file to blame. /// * `options` /// - An instance of [`Options`]. /// /// ## The algorithm /// /// *For brevity, `HEAD` denotes the starting point of the blame operation. It could be any commit, or even commits that /// represent the worktree state. /// /// We begin with one or more *Unblamed Hunks* and a single suspect, usually the `HEAD` commit as the commit containing the /// *Blamed File*, so that it contains the entire file, with the first commit being a candidate for the entire *Blamed File*. /// We traverse the commit graph starting at the first suspect, and see if there have been changes to `file_path`. /// If so, we have found a *Source File* and a *Suspect* commit, and have hunks that represent these changes. /// Now the *Unblamed Hunk* is split at the boundaries of each matching change, creating a new *Unblamed Hunk* on each side, /// along with a [`BlameEntry`] to represent the match. /// This is repeated until there are no non-empty *Unblamed Hunk*s left. /// /// At a high level, what we want to do is the following: /// /// - get the commit /// - walk through its parents /// - for each parent, do a diff and mark lines that don’t have a suspect yet (this is the term /// used in `libgit2`), but that have been changed in this commit /// /// The algorithm in `libgit2` works by going through parents and keeping a linked list of blame /// suspects. It can be visualized as follows: /// /// <----------------------------------------> /// <---------------><-----------------------> /// <---><----------><-----------------------> /// <---><----------><-------><-----><-------> /// <---><---><-----><-------><-----><-------> /// <---><---><-----><-------><-----><-><-><-> pub fn file( odb: impl gix_object::Find + gix_object::FindHeader, suspect: ObjectId, cache: Option, resource_cache: &mut gix_diff::blob::Platform, file_path: &BStr, options: Options, ) -> Result { let _span = gix_trace::coarse!("gix_blame::file()", ?file_path, ?suspect); let mut stats = Statistics::default(); let (mut buf, mut buf2, mut buf3) = (Vec::new(), Vec::new(), Vec::new()); let blamed_file_entry_id = find_path_entry_in_commit( &odb, &suspect, file_path, cache.as_ref(), &mut buf, &mut buf2, &mut stats, )? .ok_or_else(|| Error::FileMissing { file_path: file_path.to_owned(), commit_id: suspect, })?; let blamed_file_blob = odb.find_blob(&blamed_file_entry_id, &mut buf)?.data.to_vec(); let num_lines_in_blamed = tokens_for_diffing(&blamed_file_blob).tokenize().count() as u32; // Binary or otherwise empty? if num_lines_in_blamed == 0 { return Ok(Outcome::default()); } let ranges_to_blame = options.ranges.to_zero_based_exclusive_ranges(num_lines_in_blamed); let mut hunks_to_blame = ranges_to_blame .into_iter() .map(|range| UnblamedHunk::new(range, suspect)) .collect::>(); let (mut buf, mut buf2) = (Vec::new(), Vec::new()); let commit = find_commit(cache.as_ref(), &odb, &suspect, &mut buf)?; let mut queue: gix_revwalk::PriorityQueue = gix_revwalk::PriorityQueue::new(); queue.insert(commit.commit_time()?, suspect); let mut out = Vec::new(); let mut diff_state = gix_diff::tree::State::default(); let mut previous_entry: Option<(ObjectId, ObjectId)> = None; let mut blame_path = if options.debug_track_path { Some(Vec::new()) } else { None }; 'outer: while let Some(suspect) = queue.pop_value() { stats.commits_traversed += 1; if hunks_to_blame.is_empty() { break; } let first_hunk_for_suspect = hunks_to_blame.iter().find(|hunk| hunk.has_suspect(&suspect)); let Some(first_hunk_for_suspect) = first_hunk_for_suspect else { // There are no `UnblamedHunk`s associated with this `suspect`, so we can continue with // the next one. continue 'outer; }; let current_file_path = first_hunk_for_suspect .source_file_name .clone() .unwrap_or_else(|| file_path.to_owned()); let commit = find_commit(cache.as_ref(), &odb, &suspect, &mut buf)?; let commit_time = commit.commit_time()?; if let Some(since) = options.since { if commit_time < since.seconds { if unblamed_to_out_is_done(&mut hunks_to_blame, &mut out, suspect) { break 'outer; } continue; } } let parent_ids: ParentIds = collect_parents(commit, &odb, cache.as_ref(), &mut buf2)?; if parent_ids.is_empty() { if queue.is_empty() { // I’m not entirely sure if this is correct yet. `suspect`, at this point, is the // `id` of the last `item` that was yielded by `queue`, so it makes sense to assign // the remaining lines to it, even though we don’t explicitly check whether that is // true here. We could perhaps use diff-tree-to-tree to compare `suspect` against // an empty tree to validate this assumption. if unblamed_to_out_is_done(&mut hunks_to_blame, &mut out, suspect) { if let Some(ref mut blame_path) = blame_path { let entry = previous_entry .take() .filter(|(id, _)| *id == suspect) .map(|(_, entry)| entry); let blame_path_entry = BlamePathEntry { source_file_path: current_file_path.clone(), previous_source_file_path: None, commit_id: suspect, blob_id: entry.unwrap_or(ObjectId::null(gix_hash::Kind::Sha1)), previous_blob_id: ObjectId::null(gix_hash::Kind::Sha1), parent_index: 0, }; blame_path.push(blame_path_entry); } break 'outer; } } // There is more, keep looking. continue; } let mut entry = previous_entry .take() .filter(|(id, _)| *id == suspect) .map(|(_, entry)| entry); if entry.is_none() { entry = find_path_entry_in_commit( &odb, &suspect, current_file_path.as_ref(), cache.as_ref(), &mut buf, &mut buf2, &mut stats, )?; } let Some(entry_id) = entry else { continue; }; // This block asserts that, for every `UnblamedHunk`, all lines in the *Blamed File* are // identical to the corresponding lines in the *Source File*. #[cfg(debug_assertions)] { let source_blob = odb.find_blob(&entry_id, &mut buf)?.data.to_vec(); let mut source_interner = gix_diff::blob::Interner::new(source_blob.len() / 100); let source_lines_as_tokens: Vec<_> = tokens_for_diffing(&source_blob) .tokenize() .map(|token| source_interner.intern(token)) .collect(); let mut blamed_interner = gix_diff::blob::Interner::new(blamed_file_blob.len() / 100); let blamed_lines_as_tokens: Vec<_> = tokens_for_diffing(&blamed_file_blob) .tokenize() .map(|token| blamed_interner.intern(token)) .collect(); for hunk in hunks_to_blame.iter() { if let Some(range_in_suspect) = hunk.get_range(&suspect) { let range_in_blamed_file = hunk.range_in_blamed_file.clone(); let source_lines = range_in_suspect .clone() .map(|i| BString::new(source_interner[source_lines_as_tokens[i as usize]].into())) .collect::>(); let blamed_lines = range_in_blamed_file .clone() .map(|i| BString::new(blamed_interner[blamed_lines_as_tokens[i as usize]].into())) .collect::>(); assert_eq!(source_lines, blamed_lines); } } } for (pid, (parent_id, parent_commit_time)) in parent_ids.iter().enumerate() { if let Some(parent_entry_id) = find_path_entry_in_commit( &odb, parent_id, current_file_path.as_ref(), cache.as_ref(), &mut buf, &mut buf2, &mut stats, )? { let no_change_in_entry = entry_id == parent_entry_id; if pid == 0 { previous_entry = Some((*parent_id, parent_entry_id)); } if no_change_in_entry { pass_blame_from_to(suspect, *parent_id, &mut hunks_to_blame); queue.insert(*parent_commit_time, *parent_id); continue 'outer; } } } let more_than_one_parent = parent_ids.len() > 1; for (index, (parent_id, parent_commit_time)) in parent_ids.iter().enumerate() { queue.insert(*parent_commit_time, *parent_id); let changes_for_file_path = tree_diff_at_file_path( &odb, current_file_path.as_ref(), suspect, *parent_id, cache.as_ref(), &mut stats, &mut diff_state, resource_cache, &mut buf, &mut buf2, &mut buf3, options.rewrites, )?; let Some(modification) = changes_for_file_path else { if more_than_one_parent { // None of the changes affected the file we’re currently blaming. // Copy blame to parent. for unblamed_hunk in &mut hunks_to_blame { unblamed_hunk.clone_blame(suspect, *parent_id); } } else { pass_blame_from_to(suspect, *parent_id, &mut hunks_to_blame); } continue; }; match modification { TreeDiffChange::Addition { id } => { if more_than_one_parent { // Do nothing under the assumption that this always (or almost always) // implies that the file comes from a different parent, compared to which // it was modified, not added. } else if unblamed_to_out_is_done(&mut hunks_to_blame, &mut out, suspect) { if let Some(ref mut blame_path) = blame_path { let blame_path_entry = BlamePathEntry { source_file_path: current_file_path.clone(), previous_source_file_path: None, commit_id: suspect, blob_id: id, previous_blob_id: ObjectId::null(gix_hash::Kind::Sha1), parent_index: index, }; blame_path.push(blame_path_entry); } break 'outer; } } TreeDiffChange::Deletion => { unreachable!("We already found file_path in suspect^{{tree}}, so it can't be deleted") } TreeDiffChange::Modification { previous_id, id } => { let changes = blob_changes( &odb, resource_cache, id, previous_id, file_path, file_path, options.diff_algorithm, &mut stats, )?; hunks_to_blame = process_changes(hunks_to_blame, changes.clone(), suspect, *parent_id); if let Some(ref mut blame_path) = blame_path { let has_blame_been_passed = hunks_to_blame.iter().any(|hunk| hunk.has_suspect(parent_id)); if has_blame_been_passed { let blame_path_entry = BlamePathEntry { source_file_path: current_file_path.clone(), previous_source_file_path: Some(current_file_path.clone()), commit_id: suspect, blob_id: id, previous_blob_id: previous_id, parent_index: index, }; blame_path.push(blame_path_entry); } } } TreeDiffChange::Rewrite { source_location, source_id, id, } => { let changes = blob_changes( &odb, resource_cache, id, source_id, file_path, source_location.as_ref(), options.diff_algorithm, &mut stats, )?; hunks_to_blame = process_changes(hunks_to_blame, changes, suspect, *parent_id); let mut has_blame_been_passed = false; for hunk in hunks_to_blame.iter_mut() { if hunk.has_suspect(parent_id) { hunk.source_file_name = Some(source_location.clone()); has_blame_been_passed = true; } } if has_blame_been_passed { if let Some(ref mut blame_path) = blame_path { let blame_path_entry = BlamePathEntry { source_file_path: current_file_path.clone(), previous_source_file_path: Some(source_location.clone()), commit_id: suspect, blob_id: id, previous_blob_id: source_id, parent_index: index, }; blame_path.push(blame_path_entry); } } } } } hunks_to_blame.retain_mut(|unblamed_hunk| { if unblamed_hunk.suspects.len() == 1 { if let Some(entry) = BlameEntry::from_unblamed_hunk(unblamed_hunk, suspect) { // At this point, we have copied blame for every hunk to a parent. Hunks // that have only `suspect` left in `suspects` have not passed blame to any // parent, and so they can be converted to a `BlameEntry` and moved to // `out`. out.push(entry); return false; } } unblamed_hunk.remove_blame(suspect); true }); } debug_assert_eq!( hunks_to_blame, vec![], "only if there is no portion of the file left we have completed the blame" ); // I don’t know yet whether it would make sense to use a data structure instead that preserves // order on insertion. out.sort_by_key(|a| a.start_in_blamed_file); Ok(Outcome { entries: coalesce_blame_entries(out), blob: blamed_file_blob, statistics: stats, blame_path, }) } /// Pass ownership of each unblamed hunk of `from` to `to`. /// /// This happens when `from` didn't actually change anything in the blamed file. fn pass_blame_from_to(from: ObjectId, to: ObjectId, hunks_to_blame: &mut Vec) { for unblamed_hunk in hunks_to_blame { unblamed_hunk.pass_blame(from, to); } } /// Convert each of the unblamed hunk in `hunks_to_blame` into a [`BlameEntry`], consuming them in the process. /// /// Return `true` if we are done because `hunks_to_blame` is empty. fn unblamed_to_out_is_done( hunks_to_blame: &mut Vec, out: &mut Vec, suspect: ObjectId, ) -> bool { let mut without_suspect = Vec::new(); out.extend(hunks_to_blame.drain(..).filter_map(|hunk| { BlameEntry::from_unblamed_hunk(&hunk, suspect).or_else(|| { without_suspect.push(hunk); None }) })); *hunks_to_blame = without_suspect; hunks_to_blame.is_empty() } /// This function merges adjacent blame entries. It merges entries that are adjacent both in the /// blamed file and in the source file that introduced them. This follows `git`’s /// behaviour. `libgit2`, as of 2024-09-19, only checks whether two entries are adjacent in the /// blamed file which can result in different blames in certain edge cases. See [the commit][1] /// that introduced the extra check into `git` for context. See [this commit][2] for a way to test /// for this behaviour in `git`. /// /// [1]: https://github.com/git/git/commit/c2ebaa27d63bfb7c50cbbdaba90aee4efdd45d0a /// [2]: https://github.com/git/git/commit/6dbf0c7bebd1c71c44d786ebac0f2b3f226a0131 fn coalesce_blame_entries(lines_blamed: Vec) -> Vec { let len = lines_blamed.len(); lines_blamed .into_iter() .fold(Vec::with_capacity(len), |mut acc, entry| { let previous_entry = acc.last(); if let Some(previous_entry) = previous_entry { let previous_blamed_range = previous_entry.range_in_blamed_file(); let current_blamed_range = entry.range_in_blamed_file(); let previous_source_range = previous_entry.range_in_source_file(); let current_source_range = entry.range_in_source_file(); if previous_entry.commit_id == entry.commit_id && previous_blamed_range.end == current_blamed_range.start // As of 2024-09-19, the check below only is in `git`, but not in `libgit2`. && previous_source_range.end == current_source_range.start { let coalesced_entry = BlameEntry { start_in_blamed_file: previous_blamed_range.start as u32, start_in_source_file: previous_source_range.start as u32, len: NonZeroU32::new((current_source_range.end - previous_source_range.start) as u32) .expect("BUG: hunks are never zero-sized"), commit_id: previous_entry.commit_id, source_file_name: previous_entry.source_file_name.clone(), }; acc.pop(); acc.push(coalesced_entry); } else { acc.push(entry); } acc } else { acc.push(entry); acc } }) } /// The union of [`gix_diff::tree::recorder::Change`] and [`gix_diff::tree_with_rewrites::Change`], /// keeping only the blame-relevant information. enum TreeDiffChange { Addition { id: ObjectId, }, Deletion, Modification { previous_id: ObjectId, id: ObjectId, }, Rewrite { source_location: BString, source_id: ObjectId, id: ObjectId, }, } impl From for TreeDiffChange { fn from(value: gix_diff::tree::recorder::Change) -> Self { use gix_diff::tree::recorder::Change; match value { Change::Addition { oid, .. } => Self::Addition { id: oid }, Change::Deletion { .. } => Self::Deletion, Change::Modification { previous_oid, oid, .. } => Self::Modification { previous_id: previous_oid, id: oid, }, } } } impl From for TreeDiffChange { fn from(value: gix_diff::tree_with_rewrites::Change) -> Self { use gix_diff::tree_with_rewrites::Change; match value { Change::Addition { id, .. } => Self::Addition { id }, Change::Deletion { .. } => Self::Deletion, Change::Modification { previous_id, id, .. } => Self::Modification { previous_id, id }, Change::Rewrite { source_location, source_id, id, .. } => Self::Rewrite { source_location, source_id, id, }, } } } #[allow(clippy::too_many_arguments)] fn tree_diff_at_file_path( odb: impl gix_object::Find + gix_object::FindHeader, file_path: &BStr, id: ObjectId, parent_id: ObjectId, cache: Option<&gix_commitgraph::Graph>, stats: &mut Statistics, state: &mut gix_diff::tree::State, resource_cache: &mut gix_diff::blob::Platform, commit_buf: &mut Vec, lhs_tree_buf: &mut Vec, rhs_tree_buf: &mut Vec, rewrites: Option, ) -> Result, Error> { let parent_tree_id = find_commit(cache, &odb, &parent_id, commit_buf)?.tree_id()?; let parent_tree_iter = odb.find_tree_iter(&parent_tree_id, lhs_tree_buf)?; stats.trees_decoded += 1; let tree_id = find_commit(cache, &odb, &id, commit_buf)?.tree_id()?; let tree_iter = odb.find_tree_iter(&tree_id, rhs_tree_buf)?; stats.trees_decoded += 1; let result = tree_diff_without_rewrites_at_file_path(&odb, file_path, stats, state, parent_tree_iter, tree_iter)?; // Here, we follow git’s behaviour. We return when we’ve found a `Modification`. We try a // second time with rename tracking when the change is either an `Addition` or a `Deletion` // because those can turn out to have been a `Rewrite`. // TODO(perf): renames are usually rare enough to not care about the work duplication done here. // But in theory, a rename tracker could be used by us, on demand, and we could stuff the // changes in there and have it find renames, without repeating the diff. if matches!(result, Some(TreeDiffChange::Modification { .. })) { return Ok(result); } let Some(rewrites) = rewrites else { return Ok(result); }; let result = tree_diff_with_rewrites_at_file_path( &odb, file_path, stats, state, resource_cache, parent_tree_iter, tree_iter, rewrites, )?; Ok(result) } #[allow(clippy::too_many_arguments)] fn tree_diff_without_rewrites_at_file_path( odb: impl gix_object::Find + gix_object::FindHeader, file_path: &BStr, stats: &mut Statistics, state: &mut gix_diff::tree::State, parent_tree_iter: gix_object::TreeRefIter<'_>, tree_iter: gix_object::TreeRefIter<'_>, ) -> Result, Error> { struct FindChangeToPath { inner: gix_diff::tree::Recorder, interesting_path: BString, change: Option, } impl FindChangeToPath { fn new(interesting_path: BString) -> Self { let inner = gix_diff::tree::Recorder::default().track_location(Some(gix_diff::tree::recorder::Location::Path)); FindChangeToPath { inner, interesting_path, change: None, } } } impl Visit for FindChangeToPath { fn pop_front_tracked_path_and_set_current(&mut self) { self.inner.pop_front_tracked_path_and_set_current(); } fn push_back_tracked_path_component(&mut self, component: &BStr) { self.inner.push_back_tracked_path_component(component); } fn push_path_component(&mut self, component: &BStr) { self.inner.push_path_component(component); } fn pop_path_component(&mut self) { self.inner.pop_path_component(); } fn visit(&mut self, change: gix_diff::tree::visit::Change) -> gix_diff::tree::visit::Action { use gix_diff::tree::visit::Change::*; if self.inner.path() == self.interesting_path { self.change = Some(match change { Deletion { entry_mode, oid, relation, } => gix_diff::tree::recorder::Change::Deletion { entry_mode, oid, path: self.inner.path_clone(), relation, }, Addition { entry_mode, oid, relation, } => gix_diff::tree::recorder::Change::Addition { entry_mode, oid, path: self.inner.path_clone(), relation, }, Modification { previous_entry_mode, previous_oid, entry_mode, oid, } => gix_diff::tree::recorder::Change::Modification { previous_entry_mode, previous_oid, entry_mode, oid, path: self.inner.path_clone(), }, }); std::ops::ControlFlow::Break(()) } else { std::ops::ControlFlow::Continue(()) } } } let mut recorder = FindChangeToPath::new(file_path.into()); let result = gix_diff::tree(parent_tree_iter, tree_iter, state, &odb, &mut recorder); stats.trees_diffed += 1; match result { Ok(_) | Err(gix_diff::tree::Error::Cancelled) => Ok(recorder.change.map(Into::into)), Err(error) => Err(Error::DiffTree(error)), } } #[allow(clippy::too_many_arguments)] fn tree_diff_with_rewrites_at_file_path( odb: impl gix_object::Find + gix_object::FindHeader, file_path: &BStr, stats: &mut Statistics, state: &mut gix_diff::tree::State, resource_cache: &mut gix_diff::blob::Platform, parent_tree_iter: gix_object::TreeRefIter<'_>, tree_iter: gix_object::TreeRefIter<'_>, rewrites: gix_diff::Rewrites, ) -> Result, Error> { let mut change: Option = None; let options: gix_diff::tree_with_rewrites::Options = gix_diff::tree_with_rewrites::Options { location: Some(gix_diff::tree::recorder::Location::Path), rewrites: Some(rewrites), }; let result = gix_diff::tree_with_rewrites( parent_tree_iter, tree_iter, resource_cache, state, &odb, |change_ref| -> Result<_, std::convert::Infallible> { if change_ref.location() == file_path { change = Some(change_ref.into_owned()); Ok(std::ops::ControlFlow::Break(())) } else { Ok(std::ops::ControlFlow::Continue(())) } }, options, ); stats.trees_diffed_with_rewrites += 1; match result { Ok(_) | Err(gix_diff::tree_with_rewrites::Error::Diff(gix_diff::tree::Error::Cancelled)) => { Ok(change.map(Into::into)) } Err(error) => Err(Error::DiffTreeWithRewrites(error)), } } #[allow(clippy::too_many_arguments)] fn blob_changes( odb: impl gix_object::Find + gix_object::FindHeader, resource_cache: &mut gix_diff::blob::Platform, oid: ObjectId, previous_oid: ObjectId, file_path: &BStr, previous_file_path: &BStr, diff_algorithm: gix_diff::blob::Algorithm, stats: &mut Statistics, ) -> Result, Error> { use gix_diff::blob::Hunk; resource_cache.set_resource( previous_oid, gix_object::tree::EntryKind::Blob, previous_file_path, gix_diff::blob::ResourceKind::OldOrSource, &odb, )?; resource_cache.set_resource( oid, gix_object::tree::EntryKind::Blob, file_path, gix_diff::blob::ResourceKind::NewOrDestination, &odb, )?; let outcome = resource_cache.prepare_diff()?; let input = gix_diff::blob::InternedInput::new( outcome.old.data.as_slice().unwrap_or_default(), outcome.new.data.as_slice().unwrap_or_default(), ); let mut diff = gix_diff::blob::Diff::compute(diff_algorithm, &input); diff.postprocess_lines(&input); let mut last_seen_after_end = 0; let mut changes = diff.hunks().fold(Vec::new(), |mut hunks, hunk| { let Hunk { before, after } = hunk; // This checks for unchanged hunks. if after.start > last_seen_after_end { hunks.push(Change::Unchanged(last_seen_after_end..after.start)); } match (!before.is_empty(), !after.is_empty()) { (_, true) => { hunks.push(Change::AddedOrReplaced( after.start..after.end, before.end - before.start, )); } (true, false) => { hunks.push(Change::Deleted(after.start, before.end - before.start)); } (false, false) => unreachable!("BUG: imara-diff provided a non-change"), } last_seen_after_end = after.end; hunks }); let total_number_of_lines = input.after.len() as u32; if input.after.len() > last_seen_after_end as usize { changes.push(Change::Unchanged(last_seen_after_end..total_number_of_lines)); } stats.blobs_diffed += 1; Ok(changes) } fn find_path_entry_in_commit( odb: &impl gix_object::Find, commit: &gix_hash::oid, file_path: &BStr, cache: Option<&gix_commitgraph::Graph>, buf: &mut Vec, buf2: &mut Vec, stats: &mut Statistics, ) -> Result, Error> { let tree_id = find_commit(cache, odb, commit, buf)?.tree_id()?; let tree_iter = odb.find_tree_iter(&tree_id, buf)?; stats.trees_decoded += 1; let res = tree_iter.lookup_entry( odb, buf2, file_path.split(|b| *b == b'/').inspect(|_| stats.trees_decoded += 1), )?; stats.trees_decoded -= 1; Ok(res.map(|e| e.oid)) } type ParentIds = SmallVec<[(gix_hash::ObjectId, i64); 2]>; fn collect_parents( commit: gix_traverse::commit::Either<'_, '_>, odb: &impl gix_object::Find, cache: Option<&gix_commitgraph::Graph>, buf: &mut Vec, ) -> Result { let mut parent_ids: ParentIds = Default::default(); match commit { gix_traverse::commit::Either::CachedCommit(commit) => { let cache = cache .as_ref() .expect("find returned a cached commit, so we expect cache to be present"); for parent_pos in commit.iter_parents() { let parent = cache.commit_at(parent_pos?); parent_ids.push((parent.id().to_owned(), parent.committer_timestamp() as i64)); } } gix_traverse::commit::Either::CommitRefIter(commit_ref_iter) => { for id in commit_ref_iter.parent_ids() { let parent = odb.find_commit_iter(id.as_ref(), buf).ok(); let parent_commit_time = parent .and_then(|parent| parent.committer().ok().map(|committer| committer.seconds())) .unwrap_or_default(); parent_ids.push((id, parent_commit_time)); } } } Ok(parent_ids) } /// Return an iterator over tokens for use in diffing. These are usually lines, but it's important /// to unify them so the later access shows the right thing. pub(crate) fn tokens_for_diffing(data: &[u8]) -> impl TokenSource { gix_diff::blob::sources::byte_lines(data) } gix-blame-0.13.0/src/file/mod.rs000064400000000000000000000466531046102023000144230ustar 00000000000000//! A module with low-level types and functions. use std::{num::NonZeroU32, ops::Range}; use gix_hash::ObjectId; use crate::types::{BlameEntry, Change, Either, LineRange, Offset, UnblamedHunk}; pub(super) mod function; /// Compare a section from a potential *Source File* (`hunk`) with a change from a diff and see if /// there is an intersection with `change`. Based on that intersection, we may generate a /// [`BlameEntry`] for `out` and/or split the `hunk` into multiple. /// /// This is the core of the blame implementation as it matches regions in *Blamed File* to /// corresponding regions in one or more than one *Source File*. fn process_change( new_hunks_to_blame: &mut Vec, offset: &mut Offset, suspect: ObjectId, parent: ObjectId, hunk: Option, change: Option, ) -> (Option, Option) { /// Since `range_with_end` is a range that is not inclusive at the end, /// `range_with_end.end` is not part of `range_with_end`. /// The first line that is `range_with_end.end - 1`. fn actual_end_in_range(test: &Range, containing_range: &Range) -> bool { (test.end - 1) >= containing_range.start && test.end <= containing_range.end } // # General Rules // 1. If there is no suspect, immediately reschedule `hunk` and redo processing of `change`. // // # Detailed Rules // 1. whenever we do *not* return `hunk`, it must be added to `new_hunks_to_blame`, shifted with `offset` // 2. return `hunk` if it is not fully covered by changes yet. // 3. `change` *must* be returned if it is not fully included in `hunk`. match (hunk, change) { (Some(hunk), Some(Change::Unchanged(unchanged))) => { let Some(range_in_suspect) = hunk.get_range(&suspect) else { // We don’t clone blame to `parent` as `suspect` has nothing to do with this // `hunk`. new_hunks_to_blame.push(hunk); return (None, Some(Change::Unchanged(unchanged))); }; match ( range_in_suspect.contains(&unchanged.start), actual_end_in_range(&unchanged, range_in_suspect), ) { (_, true) => { // <------> (hunk) // <-------> (unchanged) // // <----------> (hunk) // <---> (unchanged) // skip over unchanged - there will be changes right after. (Some(hunk), None) } (true, false) => { // <--------> (hunk) // <-------> (unchanged) // Nothing to do with `hunk` except shifting it, // but `unchanged` needs to be checked against the next hunk to catch up. new_hunks_to_blame.push(hunk.passed_blame(suspect, parent).shift_by(parent, *offset)); (None, Some(Change::Unchanged(unchanged))) } (false, false) => { // Any of the following cases are handled by this branch: // <---> (hunk) // <----------> (unchanged) // // <----> (hunk) // <--> (unchanged) // // <--> (hunk) // <----> (unchanged) if unchanged.end <= range_in_suspect.start { // <----> (hunk) // <--> (unchanged) // Let changes catch up with us. (Some(hunk), None) } else { // <--> (hunk) // <----> (unchanged) // // <---> (hunk) // <----------> (unchanged) // Nothing to do with `hunk` except shifting it, // but `unchanged` needs to be checked against the next hunk to catch up. new_hunks_to_blame.push(hunk.passed_blame(suspect, parent).shift_by(parent, *offset)); (None, Some(Change::Unchanged(unchanged))) } } } } (Some(hunk), Some(Change::AddedOrReplaced(added, number_of_lines_deleted))) => { let Some(range_in_suspect) = hunk.get_range(&suspect).cloned() else { new_hunks_to_blame.push(hunk); return (None, Some(Change::AddedOrReplaced(added, number_of_lines_deleted))); }; let suspect_contains_added_start = range_in_suspect.contains(&added.start); let suspect_contains_added_end = actual_end_in_range(&added, &range_in_suspect); match (suspect_contains_added_start, suspect_contains_added_end) { (true, true) => { // A perfect match of lines to take out of the unblamed portion. // <----------> (hunk) // <---> (added) // <---> (blamed) // <--> <-> (new hunk) // Split hunk at the start of added. let hunk_starting_at_added = match hunk.split_at(suspect, added.start) { Either::Left(hunk) => { // `added` starts with `hunk`, nothing to split. hunk } Either::Right((before, after)) => { // requeue the left side `before` after offsetting it… new_hunks_to_blame.push(before.passed_blame(suspect, parent).shift_by(parent, *offset)); // …and treat `after` as `new_hunk`, which contains the `added` range. after } }; *offset += added.end - added.start; *offset -= number_of_lines_deleted; // The overlapping `added` section was successfully located. // Re-split at the end of `added` to continue with what's after. match hunk_starting_at_added.split_at(suspect, added.end) { Either::Left(hunk) => { new_hunks_to_blame.push(hunk); // Nothing to split, so we are done with this hunk. (None, None) } Either::Right((hunk, after)) => { new_hunks_to_blame.push(hunk); // Keep processing the unblamed range after `added` (Some(after), None) } } } (true, false) => { // Added overlaps towards the end of `hunk`. // <--------> (hunk) // <-------> (added) // <----> (blamed) // <--> (new hunk) let hunk_starting_at_added = match hunk.split_at(suspect, added.start) { Either::Left(hunk) => hunk, Either::Right((before, after)) => { // Keep looking for the left side of the unblamed portion. new_hunks_to_blame.push(before.passed_blame(suspect, parent).shift_by(parent, *offset)); after } }; // We can 'blame' the overlapping area of `added` and `hunk`. new_hunks_to_blame.push(hunk_starting_at_added); // Keep processing `added`, it's portion past `hunk` may still contribute. (None, Some(Change::AddedOrReplaced(added, number_of_lines_deleted))) } (false, true) => { // Added reaches into the hunk, so we blame only the overlapping portion of it. // <-------> (hunk) // <------> (added) // <---> (blamed) // <--> (new hunk) *offset += added.end - added.start; *offset -= number_of_lines_deleted; match hunk.split_at(suspect, added.end) { Either::Left(hunk) => { new_hunks_to_blame.push(hunk); (None, None) } Either::Right((before, after)) => { new_hunks_to_blame.push(before); (Some(after), None) } } } (false, false) => { // Any of the following cases are handled by this branch: // <---> (hunk) // <----------> (added) // // <----> (hunk) // <--> (added) // // <--> (hunk) // <----> (added) if added.end <= range_in_suspect.start { // <----> (hunk) // <--> (added) *offset += added.end - added.start; *offset -= number_of_lines_deleted; // Let changes catchup with `hunk` after letting `added` contribute to the offset. (Some(hunk), None) } else if range_in_suspect.end <= added.start { // <--> (hunk) // <----> (added) // Retry `hunk` once there is overlapping changes to process. new_hunks_to_blame.push(hunk.passed_blame(suspect, parent).shift_by(parent, *offset)); // Let hunks catchup with this change. ( None, Some(Change::AddedOrReplaced(added.clone(), number_of_lines_deleted)), ) } else { // Discard the left side of `added`, keep track of `blamed`, and continue with the // right side of added that is going past `hunk`. // <---> (hunk) // <----------> (added) // <---> (blamed) // Successfully blame the whole range. new_hunks_to_blame.push(hunk); // And keep processing `added` with future `hunks` that might be affected by it. ( None, Some(Change::AddedOrReplaced(added.clone(), number_of_lines_deleted)), ) } } } } (Some(hunk), Some(Change::Deleted(line_number_in_destination, number_of_lines_deleted))) => { let Some(range_in_suspect) = hunk.get_range(&suspect) else { new_hunks_to_blame.push(hunk); return ( None, Some(Change::Deleted(line_number_in_destination, number_of_lines_deleted)), ); }; if line_number_in_destination < range_in_suspect.start { // <---> (hunk) // | (line_number_in_destination) // Track the shift to `hunk` as it affects us, and keep catching up with changes. *offset -= number_of_lines_deleted; (Some(hunk), None) } else if line_number_in_destination < range_in_suspect.end { // <-----> (hunk) // | (line_number_in_destination) let new_hunk = match hunk.split_at(suspect, line_number_in_destination) { Either::Left(hunk) => { // Nothing to split as `line_number_in_destination` is directly at start of `hunk` hunk } Either::Right((before, after)) => { // `before` isn't affected by deletion, so keep it for later. new_hunks_to_blame.push(before.passed_blame(suspect, parent).shift_by(parent, *offset)); // after will be affected by offset, and we will see if there are more changes affecting it. after } }; *offset -= number_of_lines_deleted; (Some(new_hunk), None) } else { // <---> (hunk) // | (line_number_in_destination) // Catchup with changes. new_hunks_to_blame.push(hunk.passed_blame(suspect, parent).shift_by(parent, *offset)); ( None, Some(Change::Deleted(line_number_in_destination, number_of_lines_deleted)), ) } } (Some(hunk), None) => { // nothing to do - changes are exhausted, re-evaluate `hunk`. new_hunks_to_blame.push(hunk.passed_blame(suspect, parent).shift_by(parent, *offset)); (None, None) } (None, Some(Change::Unchanged(_))) => { // Nothing changed past the blamed range - do nothing. (None, None) } (None, Some(Change::AddedOrReplaced(added, number_of_lines_deleted))) => { // Keep track of the shift to apply to hunks in the future. *offset += added.len() as u32; *offset -= number_of_lines_deleted; (None, None) } (None, Some(Change::Deleted(_, number_of_lines_deleted))) => { // Keep track of the shift to apply to hunks in the future. *offset -= number_of_lines_deleted; (None, None) } (None, None) => { // Noop, caller shouldn't do that, but not our problem. (None, None) } } } /// Consume `hunks_to_blame` and `changes` to pair up matches ranges (also overlapping) with each other. /// Once a match is found, it's pushed onto `out`. /// /// `process_changes` assumes that ranges coming from the same *Source File* can and do /// occasionally overlap. If it were a desirable property of the blame algorithm as a whole to /// never have two different lines from a *Blamed File* mapped to the same line in a *Source File*, /// this property would need to be enforced at a higher level than `process_changes`. /// Then the nested loops could potentially be flattened into one. fn process_changes( hunks_to_blame: Vec, changes: Vec, suspect: ObjectId, parent: ObjectId, ) -> Vec { let mut new_hunks_to_blame = Vec::new(); for mut hunk in hunks_to_blame.into_iter().map(Some) { let mut offset_in_destination = Offset::Added(0); let mut changes_iter = changes.iter().cloned(); let mut change = changes_iter.next(); loop { (hunk, change) = process_change( &mut new_hunks_to_blame, &mut offset_in_destination, suspect, parent, hunk, change, ); change = change.or_else(|| changes_iter.next()); if hunk.is_none() { break; } } } new_hunks_to_blame } impl UnblamedHunk { fn shift_by(mut self, suspect: ObjectId, offset: Offset) -> Self { if let Some(entry) = self.suspects.iter_mut().find(|entry| entry.0 == suspect) { entry.1 = entry.1.shift_by(offset); } self } fn split_at(self, suspect: ObjectId, line_number_in_destination: u32) -> Either { match self.get_range(&suspect) { None => Either::Left(self), Some(range_in_suspect) => { if !range_in_suspect.contains(&line_number_in_destination) { return Either::Left(self); } let split_at_from_start = line_number_in_destination - range_in_suspect.start; if split_at_from_start > 0 { let new_suspects_before = self .suspects .iter() .map(|(suspect, range)| (*suspect, range.start..(range.start + split_at_from_start))); let new_suspects_after = self .suspects .iter() .map(|(suspect, range)| (*suspect, (range.start + split_at_from_start)..range.end)); let new_hunk_before = Self { range_in_blamed_file: self.range_in_blamed_file.start ..(self.range_in_blamed_file.start + split_at_from_start), suspects: new_suspects_before.collect(), source_file_name: self.source_file_name.clone(), }; let new_hunk_after = Self { range_in_blamed_file: (self.range_in_blamed_file.start + split_at_from_start) ..(self.range_in_blamed_file.end), suspects: new_suspects_after.collect(), source_file_name: self.source_file_name, }; Either::Right((new_hunk_before, new_hunk_after)) } else { Either::Left(self) } } } } /// This is like [`Self::pass_blame()`], but easier to use in places where the 'passing' is /// done 'inline'. fn passed_blame(mut self, from: ObjectId, to: ObjectId) -> Self { if let Some(entry) = self.suspects.iter_mut().find(|entry| entry.0 == from) { entry.0 = to; } self } /// Transfer all ranges from the commit at `from` to the commit at `to`. fn pass_blame(&mut self, from: ObjectId, to: ObjectId) { if let Some(entry) = self.suspects.iter_mut().find(|entry| entry.0 == from) { entry.0 = to; } } fn clone_blame(&mut self, from: ObjectId, to: ObjectId) { if let Some(range_in_suspect) = self.get_range(&from) { self.suspects.push((to, range_in_suspect.clone())); } } fn remove_blame(&mut self, suspect: ObjectId) { self.suspects.retain(|entry| entry.0 != suspect); } } impl BlameEntry { /// Create an offset from a portion of the *Blamed File*. fn from_unblamed_hunk(unblamed_hunk: &UnblamedHunk, commit_id: ObjectId) -> Option { let range_in_source_file = unblamed_hunk.get_range(&commit_id)?; Some(Self { start_in_blamed_file: unblamed_hunk.range_in_blamed_file.start, start_in_source_file: range_in_source_file.start, len: force_non_zero(range_in_source_file.len() as u32), commit_id, source_file_name: unblamed_hunk.source_file_name.clone(), }) } } fn force_non_zero(n: u32) -> NonZeroU32 { NonZeroU32::new(n).expect("BUG: hunks are never empty") } #[cfg(test)] mod tests; gix-blame-0.13.0/src/file/tests.rs000064400000000000000000001050001046102023000147640ustar 00000000000000use std::ops::Range; use gix_hash::ObjectId; use crate::file::UnblamedHunk; impl From<(Range, ObjectId)> for UnblamedHunk { fn from(value: (Range, ObjectId)) -> Self { let (range_in_blamed_file, suspect) = value; let range_in_destination = range_in_blamed_file.clone(); (range_in_blamed_file, suspect, range_in_destination).into() } } impl From<(Range, ObjectId, Range)> for UnblamedHunk { fn from(value: (Range, ObjectId, Range)) -> Self { let (range_in_blamed_file, suspect, range_in_destination) = value; assert!( range_in_blamed_file.end > range_in_blamed_file.start, "{range_in_blamed_file:?}" ); assert!( range_in_destination.end > range_in_destination.start, "{range_in_destination:?}" ); assert_eq!(range_in_blamed_file.len(), range_in_destination.len()); UnblamedHunk { range_in_blamed_file, suspects: [(suspect, range_in_destination)].into(), source_file_name: None, } } } fn zero_sha() -> ObjectId { use std::str::FromStr; ObjectId::from_str("0000000000000000000000000000000000000000").unwrap() } fn one_sha() -> ObjectId { use std::str::FromStr; ObjectId::from_str("1111111111111111111111111111111111111111").unwrap() } mod process_change { use super::*; use crate::file::{process_change, Change, Offset}; #[test] fn nothing() { let mut new_hunks_to_blame = Vec::new(); let mut offset_in_destination: Offset = Offset::Added(0); let suspect = zero_sha(); let parent = one_sha(); let (hunk, change) = process_change( &mut new_hunks_to_blame, &mut offset_in_destination, suspect, parent, None, None, ); assert_eq!(hunk, None); assert_eq!(change, None); assert_eq!(offset_in_destination, Offset::Added(0)); } #[test] fn added_hunk() { let mut new_hunks_to_blame = Vec::new(); let mut offset_in_destination: Offset = Offset::Added(0); let suspect = zero_sha(); let parent = one_sha(); let (hunk, change) = process_change( &mut new_hunks_to_blame, &mut offset_in_destination, suspect, parent, Some((0..5, suspect).into()), Some(Change::AddedOrReplaced(0..3, 0)), ); assert_eq!(hunk, Some((3..5, suspect).into())); assert_eq!(change, None); assert_eq!(new_hunks_to_blame, [(0..3, suspect).into()]); assert_eq!(offset_in_destination, Offset::Added(3)); } #[test] fn added_hunk_2() { let mut new_hunks_to_blame = Vec::new(); let mut offset_in_destination: Offset = Offset::Added(0); let suspect = zero_sha(); let parent = one_sha(); let (hunk, change) = process_change( &mut new_hunks_to_blame, &mut offset_in_destination, suspect, parent, Some((0..5, suspect).into()), Some(Change::AddedOrReplaced(2..3, 0)), ); assert_eq!(hunk, Some((3..5, suspect).into())); assert_eq!(change, None); assert_eq!(new_hunks_to_blame, [(0..2, parent).into(), (2..3, suspect).into()]); assert_eq!(offset_in_destination, Offset::Added(1)); } #[test] fn added_hunk_3() { let mut new_hunks_to_blame = Vec::new(); let mut offset_in_destination: Offset = Offset::Added(5); let suspect = zero_sha(); let parent = one_sha(); let (hunk, change) = process_change( &mut new_hunks_to_blame, &mut offset_in_destination, suspect, parent, Some((10..15, suspect).into()), Some(Change::AddedOrReplaced(12..13, 0)), ); assert_eq!(hunk, Some((13..15, suspect).into())); assert_eq!(change, None); assert_eq!( new_hunks_to_blame, [(10..12, parent, 5..7).into(), (12..13, suspect).into()] ); assert_eq!(offset_in_destination, Offset::Added(6)); } #[test] fn added_hunk_4() { let mut new_hunks_to_blame = Vec::new(); let mut offset_in_destination: Offset = Offset::Added(0); let suspect = zero_sha(); let parent = one_sha(); let (hunk, change) = process_change( &mut new_hunks_to_blame, &mut offset_in_destination, suspect, parent, Some((12..17, suspect, 7..12).into()), Some(Change::AddedOrReplaced(9..10, 0)), ); assert_eq!(hunk, Some((15..17, suspect, 10..12).into())); assert_eq!(change, None); assert_eq!( new_hunks_to_blame, [(12..14, parent, 7..9).into(), (14..15, suspect, 9..10).into()] ); assert_eq!(offset_in_destination, Offset::Added(1)); } #[test] fn added_hunk_5() { let mut new_hunks_to_blame = Vec::new(); let mut offset_in_destination: Offset = Offset::Added(0); let suspect = zero_sha(); let parent = one_sha(); let (hunk, change) = process_change( &mut new_hunks_to_blame, &mut offset_in_destination, suspect, parent, Some((0..5, suspect).into()), Some(Change::AddedOrReplaced(0..3, 1)), ); assert_eq!(hunk, Some((3..5, suspect).into())); assert_eq!(change, None); assert_eq!(new_hunks_to_blame, [(0..3, suspect).into()]); assert_eq!(offset_in_destination, Offset::Added(2)); } #[test] fn added_hunk_6() { let mut new_hunks_to_blame = Vec::new(); let mut offset_in_destination: Offset = Offset::Added(0); let suspect = zero_sha(); let parent = one_sha(); let (hunk, change) = process_change( &mut new_hunks_to_blame, &mut offset_in_destination, suspect, parent, Some((1..5, suspect, 0..4).into()), Some(Change::AddedOrReplaced(0..3, 1)), ); assert_eq!(hunk, Some((4..5, suspect, 3..4).into())); assert_eq!(change, None); assert_eq!(new_hunks_to_blame, [(1..4, suspect, 0..3).into()]); assert_eq!(offset_in_destination, Offset::Added(2)); } #[test] fn added_hunk_7() { let mut new_hunks_to_blame = Vec::new(); let mut offset_in_destination: Offset = Offset::Added(2); let suspect = zero_sha(); let parent = one_sha(); let (hunk, change) = process_change( &mut new_hunks_to_blame, &mut offset_in_destination, suspect, parent, Some((3..7, suspect, 2..6).into()), Some(Change::AddedOrReplaced(3..5, 1)), ); assert_eq!(hunk, Some((6..7, suspect, 5..6).into())); assert_eq!(change, None); assert_eq!( new_hunks_to_blame, [(3..4, parent, 0..1).into(), (4..6, suspect, 3..5).into()] ); assert_eq!(offset_in_destination, Offset::Added(3)); } #[test] fn added_hunk_8() { let mut new_hunks_to_blame = Vec::new(); let mut offset_in_destination: Offset = Offset::Added(1); let suspect = zero_sha(); let parent = one_sha(); let (hunk, change) = process_change( &mut new_hunks_to_blame, &mut offset_in_destination, suspect, parent, Some((23..24, suspect, 25..26).into()), Some(Change::AddedOrReplaced(25..27, 1)), ); assert_eq!(hunk, None); assert_eq!(change, Some(Change::AddedOrReplaced(25..27, 1))); assert_eq!(new_hunks_to_blame, [(23..24, suspect, 25..26).into()]); assert_eq!(offset_in_destination, Offset::Added(1)); } #[test] fn added_hunk_9() { let mut new_hunks_to_blame = Vec::new(); let mut offset_in_destination: Offset = Offset::Added(0); let suspect = zero_sha(); let parent = one_sha(); let (hunk, change) = process_change( &mut new_hunks_to_blame, &mut offset_in_destination, suspect, parent, Some((23..24, suspect, 21..22).into()), Some(Change::AddedOrReplaced(18..22, 3)), ); assert_eq!(hunk, None); assert_eq!(change, None); assert_eq!(new_hunks_to_blame, [(23..24, suspect, 21..22).into()]); assert_eq!(offset_in_destination, Offset::Added(1)); } #[test] fn added_hunk_10() { let mut new_hunks_to_blame = Vec::new(); let mut offset_in_destination: Offset = Offset::Added(0); let suspect = zero_sha(); let parent = one_sha(); let (hunk, change) = process_change( &mut new_hunks_to_blame, &mut offset_in_destination, suspect, parent, Some((71..109, suspect, 70..108).into()), Some(Change::AddedOrReplaced(106..109, 0)), ); assert_eq!(hunk, None); assert_eq!(change, Some(Change::AddedOrReplaced(106..109, 0))); assert_eq!( new_hunks_to_blame, [(71..107, parent, 70..106).into(), (107..109, suspect, 106..108).into()] ); assert_eq!(offset_in_destination, Offset::Added(0)); } #[test] fn added_hunk_11() { let mut new_hunks_to_blame = Vec::new(); let mut offset_in_destination: Offset = Offset::Added(0); let suspect = zero_sha(); let parent = one_sha(); let (hunk, change) = process_change( &mut new_hunks_to_blame, &mut offset_in_destination, suspect, parent, Some((149..156, suspect, 137..144).into()), Some(Change::AddedOrReplaced(143..146, 0)), ); assert_eq!(hunk, None); assert_eq!(change, Some(Change::AddedOrReplaced(143..146, 0))); assert_eq!( new_hunks_to_blame, [ (149..155, parent, 137..143).into(), (155..156, suspect, 143..144).into() ] ); assert_eq!(offset_in_destination, Offset::Added(0)); } #[test] fn no_overlap() { let mut new_hunks_to_blame = Vec::new(); let mut offset_in_destination: Offset = Offset::Deleted(3); let suspect = zero_sha(); let parent = one_sha(); let (hunk, change) = process_change( &mut new_hunks_to_blame, &mut offset_in_destination, suspect, parent, Some((3..6, suspect, 2..5).into()), Some(Change::AddedOrReplaced(7..10, 1)), ); assert_eq!(hunk, None); assert_eq!(change, Some(Change::AddedOrReplaced(7..10, 1))); assert_eq!(new_hunks_to_blame, [(3..6, parent, 5..8).into()]); assert_eq!(offset_in_destination, Offset::Deleted(3)); } #[test] fn no_overlap_2() { let mut new_hunks_to_blame = Vec::new(); let mut offset_in_destination: Offset = Offset::Added(0); let suspect = zero_sha(); let parent = one_sha(); let (hunk, change) = process_change( &mut new_hunks_to_blame, &mut offset_in_destination, suspect, parent, Some((9..11, suspect, 6..8).into()), Some(Change::AddedOrReplaced(2..5, 0)), ); assert_eq!(hunk, Some((9..11, suspect, 6..8).into())); assert_eq!(change, None); assert_eq!(new_hunks_to_blame, []); assert_eq!(offset_in_destination, Offset::Added(3)); } #[test] fn no_overlap_3() { let mut new_hunks_to_blame = Vec::new(); let mut offset_in_destination: Offset = Offset::Added(0); let suspect = zero_sha(); let parent = one_sha(); let (hunk, change) = process_change( &mut new_hunks_to_blame, &mut offset_in_destination, suspect, parent, Some((4..15, suspect, 5..16).into()), Some(Change::AddedOrReplaced(4..5, 1)), ); assert_eq!(hunk, Some((4..15, suspect, 5..16).into())); assert_eq!(change, None); assert_eq!(new_hunks_to_blame, []); assert_eq!(offset_in_destination, Offset::Added(0)); } #[test] fn no_overlap_4() { let mut new_hunks_to_blame = Vec::new(); let mut offset_in_destination: Offset = Offset::Added(1); let suspect = zero_sha(); let parent = one_sha(); let (hunk, change) = process_change( &mut new_hunks_to_blame, &mut offset_in_destination, suspect, parent, Some((23..25, suspect, 25..27).into()), Some(Change::Unchanged(21..22)), ); assert_eq!(hunk, Some((23..25, suspect, 25..27).into())); assert_eq!(change, None); assert_eq!(new_hunks_to_blame, []); assert_eq!(offset_in_destination, Offset::Added(1)); } #[test] fn no_overlap_5() { let mut new_hunks_to_blame = Vec::new(); let mut offset_in_destination: Offset = Offset::Added(1); let suspect = zero_sha(); let parent = one_sha(); let (hunk, change) = process_change( &mut new_hunks_to_blame, &mut offset_in_destination, suspect, parent, Some((15..16, suspect, 17..18).into()), Some(Change::Deleted(20, 1)), ); assert_eq!(hunk, None); assert_eq!(change, Some(Change::Deleted(20, 1))); assert_eq!(new_hunks_to_blame, [(15..16, parent, 16..17).into()]); assert_eq!(offset_in_destination, Offset::Added(1)); } #[test] fn no_overlap_6() { let mut new_hunks_to_blame = Vec::new(); let mut offset_in_destination: Offset = Offset::Added(0); let suspect = zero_sha(); let parent = one_sha(); let (hunk, change) = process_change( &mut new_hunks_to_blame, &mut offset_in_destination, suspect, parent, Some((23..25, suspect, 22..24).into()), Some(Change::Deleted(20, 1)), ); assert_eq!(hunk, Some((23..25, suspect, 22..24).into())); assert_eq!(change, None); assert_eq!(new_hunks_to_blame, []); assert_eq!(offset_in_destination, Offset::Deleted(1)); } #[test] fn enclosing_addition() { let mut new_hunks_to_blame = Vec::new(); let mut offset_in_destination: Offset = Offset::Added(3); let suspect = zero_sha(); let parent = one_sha(); let (hunk, change) = process_change( &mut new_hunks_to_blame, &mut offset_in_destination, suspect, parent, Some((2..5, suspect, 5..8).into()), Some(Change::AddedOrReplaced(3..12, 2)), ); assert_eq!(hunk, None); assert_eq!(change, Some(Change::AddedOrReplaced(3..12, 2))); assert_eq!(new_hunks_to_blame, [(2..5, suspect, 5..8).into()]); assert_eq!(offset_in_destination, Offset::Added(3)); } #[test] fn enclosing_deletion() { let mut new_hunks_to_blame = Vec::new(); let mut offset_in_destination: Offset = Offset::Added(3); let suspect = zero_sha(); let parent = one_sha(); let (hunk, change) = process_change( &mut new_hunks_to_blame, &mut offset_in_destination, suspect, parent, Some((12..19, suspect, 13..20).into()), Some(Change::Deleted(15, 2)), ); assert_eq!(hunk, Some((14..19, suspect, 15..20).into())); assert_eq!(change, None); assert_eq!(new_hunks_to_blame, [(12..14, parent, 10..12).into()]); assert_eq!(offset_in_destination, Offset::Added(1)); } #[test] fn enclosing_unchanged_lines() { let mut new_hunks_to_blame = Vec::new(); let mut offset_in_destination: Offset = Offset::Added(3); let suspect = zero_sha(); let parent = one_sha(); let (hunk, change) = process_change( &mut new_hunks_to_blame, &mut offset_in_destination, suspect, parent, Some((110..114, suspect, 109..113).into()), Some(Change::Unchanged(109..172)), ); assert_eq!(hunk, None); assert_eq!(change, Some(Change::Unchanged(109..172))); assert_eq!(new_hunks_to_blame, [(110..114, parent, 106..110).into()]); assert_eq!(offset_in_destination, Offset::Added(3)); } #[test] fn unchanged_hunk() { let mut new_hunks_to_blame = Vec::new(); let mut offset_in_destination: Offset = Offset::Added(0); let suspect = zero_sha(); let parent = one_sha(); let (hunk, change) = process_change( &mut new_hunks_to_blame, &mut offset_in_destination, suspect, parent, Some((0..5, suspect).into()), Some(Change::Unchanged(0..3)), ); assert_eq!(hunk, Some((0..5, suspect).into())); assert_eq!(change, None); assert_eq!(new_hunks_to_blame, []); assert_eq!(offset_in_destination, Offset::Added(0)); } #[test] fn unchanged_hunk_2() { let mut new_hunks_to_blame = Vec::new(); let mut offset_in_destination: Offset = Offset::Added(0); let suspect = zero_sha(); let parent = one_sha(); let (hunk, change) = process_change( &mut new_hunks_to_blame, &mut offset_in_destination, suspect, parent, Some((0..5, suspect).into()), Some(Change::Unchanged(0..7)), ); assert_eq!(hunk, None); assert_eq!(change, Some(Change::Unchanged(0..7))); assert_eq!(new_hunks_to_blame, [(0..5, parent).into()]); assert_eq!(offset_in_destination, Offset::Added(0)); } #[test] fn unchanged_hunk_3() { let mut new_hunks_to_blame = Vec::new(); let mut offset_in_destination: Offset = Offset::Deleted(2); let suspect = zero_sha(); let parent = one_sha(); let (hunk, change) = process_change( &mut new_hunks_to_blame, &mut offset_in_destination, suspect, parent, Some((22..30, suspect, 21..29).into()), Some(Change::Unchanged(21..23)), ); assert_eq!(hunk, Some((22..30, suspect, 21..29).into())); assert_eq!(change, None); assert_eq!(new_hunks_to_blame, []); assert_eq!(offset_in_destination, Offset::Deleted(2)); } #[test] fn deleted_hunk() { let mut new_hunks_to_blame = Vec::new(); let mut offset_in_destination: Offset = Offset::Added(0); let suspect = zero_sha(); let parent = one_sha(); let (hunk, change) = process_change( &mut new_hunks_to_blame, &mut offset_in_destination, suspect, parent, Some((0..5, suspect).into()), Some(Change::Deleted(5, 3)), ); assert_eq!(hunk, None); assert_eq!(change, Some(Change::Deleted(5, 3))); assert_eq!(new_hunks_to_blame, [(0..5, parent).into()]); assert_eq!(offset_in_destination, Offset::Added(0)); } #[test] fn deleted_hunk_2() { let mut new_hunks_to_blame = Vec::new(); let mut offset_in_destination: Offset = Offset::Added(0); let suspect = zero_sha(); let parent = one_sha(); let (hunk, change) = process_change( &mut new_hunks_to_blame, &mut offset_in_destination, suspect, parent, Some((2..16, suspect).into()), Some(Change::Deleted(0, 4)), ); assert_eq!(hunk, Some((2..16, suspect).into())); assert_eq!(change, None); assert_eq!(new_hunks_to_blame, []); assert_eq!(offset_in_destination, Offset::Deleted(4)); } #[test] fn deleted_hunk_3() { let mut new_hunks_to_blame = Vec::new(); let mut offset_in_destination: Offset = Offset::Added(0); let suspect = zero_sha(); let parent = one_sha(); let (hunk, change) = process_change( &mut new_hunks_to_blame, &mut offset_in_destination, suspect, parent, Some((2..16, suspect).into()), Some(Change::Deleted(14, 4)), ); assert_eq!(hunk, Some((14..16, suspect).into())); assert_eq!(change, None); assert_eq!(new_hunks_to_blame, [(2..14, parent).into()]); assert_eq!(offset_in_destination, Offset::Deleted(4)); } #[test] fn addition_only() { let mut new_hunks_to_blame = Vec::new(); let mut offset_in_destination: Offset = Offset::Added(1); let suspect = zero_sha(); let parent = one_sha(); let (hunk, change) = process_change( &mut new_hunks_to_blame, &mut offset_in_destination, suspect, parent, None, Some(Change::AddedOrReplaced(22..25, 1)), ); assert_eq!(hunk, None); assert_eq!(change, None); assert_eq!(new_hunks_to_blame, []); assert_eq!(offset_in_destination, Offset::Added(3)); } #[test] fn deletion_only() { let mut new_hunks_to_blame = Vec::new(); let mut offset_in_destination: Offset = Offset::Added(1); let suspect = zero_sha(); let parent = one_sha(); let (hunk, change) = process_change( &mut new_hunks_to_blame, &mut offset_in_destination, suspect, parent, None, Some(Change::Deleted(11, 5)), ); assert_eq!(hunk, None); assert_eq!(change, None); assert_eq!(new_hunks_to_blame, []); assert_eq!(offset_in_destination, Offset::Deleted(4)); } #[test] fn unchanged_only() { let mut new_hunks_to_blame = Vec::new(); let mut offset_in_destination: Offset = Offset::Added(1); let suspect = zero_sha(); let parent = one_sha(); let (hunk, change) = process_change( &mut new_hunks_to_blame, &mut offset_in_destination, suspect, parent, None, Some(Change::Unchanged(11..13)), ); assert_eq!(hunk, None); assert_eq!(change, None); assert_eq!(new_hunks_to_blame, []); assert_eq!(offset_in_destination, Offset::Added(1)); } } mod process_changes { use pretty_assertions::assert_eq; use crate::file::{ process_changes, tests::{one_sha, zero_sha}, Change, }; #[test] fn nothing() { let suspect = zero_sha(); let parent = one_sha(); let new_hunks_to_blame = process_changes(vec![], vec![], suspect, parent); assert_eq!(new_hunks_to_blame, []); } #[test] fn added_hunk() { let suspect = zero_sha(); let parent = one_sha(); let hunks_to_blame = vec![(0..4, suspect).into()]; let changes = vec![Change::AddedOrReplaced(0..4, 0)]; let new_hunks_to_blame = process_changes(hunks_to_blame, changes, suspect, parent); assert_eq!(new_hunks_to_blame, [(0..4, suspect).into()]); } #[test] fn added_hunk_2() { let suspect = zero_sha(); let parent = one_sha(); let hunks_to_blame = vec![(0..6, suspect).into()]; let changes = vec![Change::AddedOrReplaced(0..4, 0), Change::Unchanged(4..6)]; let new_hunks_to_blame = process_changes(hunks_to_blame, changes, suspect, parent); assert_eq!( new_hunks_to_blame, [(0..4, suspect).into(), (4..6, parent, 0..2).into(),] ); } #[test] fn added_hunk_3() { let suspect = zero_sha(); let parent = one_sha(); let hunks_to_blame = vec![(0..6, suspect).into()]; let changes = vec![ Change::Unchanged(0..2), Change::AddedOrReplaced(2..4, 0), Change::Unchanged(4..6), ]; let new_hunks_to_blame = process_changes(hunks_to_blame, changes, suspect, parent); assert_eq!( new_hunks_to_blame, [ (0..2, parent).into(), (2..4, suspect).into(), (4..6, parent, 2..4).into(), ] ); } #[test] fn added_hunk_4_0() { let suspect = zero_sha(); let parent = one_sha(); let hunks_to_blame = vec![(0..6, suspect).into()]; let changes = vec![ Change::AddedOrReplaced(0..1, 0), Change::AddedOrReplaced(1..4, 0), Change::Unchanged(4..6), ]; let new_hunks_to_blame = process_changes(hunks_to_blame, changes, suspect, parent); assert_eq!( new_hunks_to_blame, [ (0..1, suspect).into(), (1..4, suspect).into(), (4..6, parent, 0..2).into() ] ); } #[test] fn added_hunk_4_1() { let suspect = zero_sha(); let parent = one_sha(); let hunks_to_blame = vec![(0..6, suspect).into()]; let changes = vec![Change::AddedOrReplaced(0..1, 0)]; let new_hunks_to_blame = process_changes(hunks_to_blame, changes, suspect, parent); assert_eq!( new_hunks_to_blame, [(0..1, suspect).into(), (1..6, parent, 0..5).into()] ); } #[test] fn added_hunk_4_2() { let suspect = zero_sha(); let parent = one_sha(); let hunks_to_blame = vec![(2..6, suspect, 0..4).into()]; let changes = vec![Change::AddedOrReplaced(0..1, 0)]; let new_hunks_to_blame = process_changes(hunks_to_blame, changes, suspect, parent); assert_eq!( new_hunks_to_blame, [(2..3, suspect, 0..1).into(), (3..6, parent, 0..3).into()] ); } #[test] fn added_hunk_5() { let suspect = zero_sha(); let parent = one_sha(); let hunks_to_blame = vec![(0..6, suspect).into()]; let changes = vec![Change::AddedOrReplaced(0..4, 3), Change::Unchanged(4..6)]; let new_hunks_to_blame = process_changes(hunks_to_blame, changes, suspect, parent); assert_eq!( new_hunks_to_blame, [(0..4, suspect).into(), (4..6, parent, 3..5).into()] ); } #[test] fn added_hunk_6() { let suspect = zero_sha(); let parent = one_sha(); let hunks_to_blame = vec![(4..6, suspect, 3..5).into()]; let changes = vec![Change::AddedOrReplaced(0..3, 0), Change::Unchanged(3..5)]; let new_hunks_to_blame = process_changes(hunks_to_blame, changes, suspect, parent); assert_eq!(new_hunks_to_blame, [(4..6, parent, 0..2).into()]); } #[test] fn added_hunk_7() { let suspect = zero_sha(); let parent = one_sha(); let hunks_to_blame = vec![(1..3, suspect, 0..2).into()]; let changes = vec![Change::AddedOrReplaced(0..1, 2)]; let new_hunks_to_blame = process_changes(hunks_to_blame, changes, suspect, parent); assert_eq!( new_hunks_to_blame, [(1..2, suspect, 0..1).into(), (2..3, parent).into()] ); } #[test] fn added_hunk_8() { let suspect = zero_sha(); let parent = one_sha(); let hunks_to_blame = vec![(0..4, suspect).into()]; let changes = vec![ Change::AddedOrReplaced(0..2, 0), Change::Unchanged(2..3), Change::AddedOrReplaced(3..4, 0), ]; let new_hunks_to_blame = process_changes(hunks_to_blame, changes, suspect, parent); assert_eq!( new_hunks_to_blame, [ (0..2, suspect).into(), (2..3, parent, 0..1).into(), (3..4, suspect).into(), ] ); } #[test] fn added_hunk_9() { let suspect = zero_sha(); let parent = one_sha(); let hunks_to_blame = vec![(0..30, suspect).into(), (31..37, suspect).into()]; let changes = vec![ Change::Unchanged(0..16), Change::AddedOrReplaced(16..17, 0), Change::Unchanged(17..37), ]; let new_hunks_to_blame = process_changes(hunks_to_blame, changes, suspect, parent); assert_eq!( new_hunks_to_blame, [ (0..16, parent).into(), (16..17, suspect).into(), (17..30, parent, 16..29).into(), (31..37, parent, 30..36).into() ] ); } #[test] fn added_hunk_10() { let suspect = zero_sha(); let parent = one_sha(); let hunks_to_blame = vec![(1..3, suspect).into(), (5..7, suspect).into(), (8..10, suspect).into()]; let changes = vec![ Change::Unchanged(0..6), Change::AddedOrReplaced(6..9, 0), Change::Unchanged(9..11), ]; let new_hunks_to_blame = process_changes(hunks_to_blame, changes, suspect, parent); assert_eq!( new_hunks_to_blame, [ (1..3, parent).into(), (5..6, parent).into(), (6..7, suspect).into(), (8..9, suspect).into(), (9..10, parent, 6..7).into(), ] ); } #[test] fn deleted_hunk() { let suspect = zero_sha(); let parent = one_sha(); let hunks_to_blame = vec![(0..4, suspect).into(), (4..7, suspect).into()]; let changes = vec![Change::Deleted(0, 3), Change::AddedOrReplaced(0..4, 0)]; let new_hunks_to_blame = process_changes(hunks_to_blame, changes, suspect, parent); assert_eq!( new_hunks_to_blame, [(0..4, suspect).into(), (4..7, parent, 3..6).into()] ); } #[test] fn subsequent_hunks_overlapping_end_of_addition() { let suspect = zero_sha(); let parent = one_sha(); let hunks_to_blame = vec![(13..16, suspect).into(), (10..17, suspect).into()]; let changes = vec![Change::AddedOrReplaced(10..14, 0)]; let new_hunks_to_blame = process_changes(hunks_to_blame, changes, suspect, parent); assert_eq!( new_hunks_to_blame, [ (13..14, suspect).into(), (14..16, parent, 10..12).into(), (10..14, suspect).into(), (14..17, parent, 10..13).into(), ] ); } } mod blame_ranges { use crate::{BlameRanges, Error}; #[test] fn create_with_invalid_range() { let ranges = BlameRanges::from_one_based_inclusive_range(0..=10); assert!(matches!(ranges, Err(Error::InvalidOneBasedLineRange))); } #[test] fn create_from_single_range() { let ranges = BlameRanges::from_one_based_inclusive_range(20..=40).unwrap(); assert_eq!(ranges.to_zero_based_exclusive_ranges(100), vec![19..40]); } #[test] fn create_from_multiple_ranges() { let ranges = BlameRanges::from_one_based_inclusive_ranges(vec![1..=4, 10..=14]).unwrap(); assert_eq!(ranges.to_zero_based_exclusive_ranges(100), vec![0..4, 9..14]); } #[test] fn create_with_empty_ranges() { let ranges = BlameRanges::from_one_based_inclusive_ranges(vec![]).unwrap(); assert_eq!(ranges.to_zero_based_exclusive_ranges(100), vec![0..100]); } #[test] fn add_range_merges_overlapping() { let mut ranges = BlameRanges::from_one_based_inclusive_range(1..=5).unwrap(); ranges.add_one_based_inclusive_range(3..=7).unwrap(); assert_eq!(ranges.to_zero_based_exclusive_ranges(100), vec![0..7]); } #[test] fn add_range_merges_overlapping_both() { let mut ranges = BlameRanges::from_one_based_inclusive_range(1..=3).unwrap(); ranges.add_one_based_inclusive_range(5..=7).unwrap(); ranges.add_one_based_inclusive_range(2..=6).unwrap(); assert_eq!(ranges.to_zero_based_exclusive_ranges(100), vec![0..7]); } #[test] fn add_range_non_sorted() { let mut ranges = BlameRanges::from_one_based_inclusive_range(5..=7).unwrap(); ranges.add_one_based_inclusive_range(1..=3).unwrap(); assert_eq!(ranges.to_zero_based_exclusive_ranges(100), vec![0..3, 4..7]); } #[test] fn add_range_merges_adjacent() { let mut ranges = BlameRanges::from_one_based_inclusive_range(1..=5).unwrap(); ranges.add_one_based_inclusive_range(6..=10).unwrap(); assert_eq!(ranges.to_zero_based_exclusive_ranges(100), vec![0..10]); } #[test] fn non_sorted_ranges() { let ranges = BlameRanges::from_one_based_inclusive_ranges(vec![10..=15, 1..=5]).unwrap(); assert_eq!(ranges.to_zero_based_exclusive_ranges(100), vec![0..5, 9..15]); } #[test] fn convert_to_zero_based_exclusive() { let ranges = BlameRanges::from_one_based_inclusive_ranges(vec![1..=5, 10..=15]).unwrap(); assert_eq!(ranges.to_zero_based_exclusive_ranges(100), vec![0..5, 9..15]); } #[test] fn convert_full_file_to_zero_based() { let ranges = BlameRanges::WholeFile; assert_eq!(ranges.to_zero_based_exclusive_ranges(100), vec![0..100]); } #[test] fn adding_a_range_turns_whole_file_into_partial_file() { let mut ranges = BlameRanges::default(); ranges.add_one_based_inclusive_range(1..=10).unwrap(); assert_eq!(ranges.to_zero_based_exclusive_ranges(100), vec![0..10]); } #[test] fn to_zero_based_exclusive_ignores_range_past_max_lines() { let mut ranges = BlameRanges::from_one_based_inclusive_range(1..=5).unwrap(); ranges.add_one_based_inclusive_range(16..=20).unwrap(); assert_eq!(ranges.to_zero_based_exclusive_ranges(7), vec![0..5]); } #[test] fn to_zero_based_exclusive_range_doesnt_exceed_max_lines() { let mut ranges = BlameRanges::from_one_based_inclusive_range(1..=5).unwrap(); ranges.add_one_based_inclusive_range(6..=10).unwrap(); assert_eq!(ranges.to_zero_based_exclusive_ranges(7), vec![0..7]); } #[test] fn to_zero_based_exclusive_merged_ranges_dont_exceed_max_lines() { let mut ranges = BlameRanges::from_one_based_inclusive_range(1..=4).unwrap(); ranges.add_one_based_inclusive_range(6..=10).unwrap(); assert_eq!(ranges.to_zero_based_exclusive_ranges(7), vec![0..4, 5..7]); } #[test] fn default_is_full_file() { let ranges = BlameRanges::default(); assert!(matches!(ranges, BlameRanges::WholeFile)); } } gix-blame-0.13.0/src/lib.rs000064400000000000000000000016621046102023000134620ustar 00000000000000//! A crate to implement an algorithm to annotate lines in tracked files with the commits that changed them. //! //! ### Terminology //! //! * **Blamed File** //! - The file as it exists in `HEAD`. //! - the initial state with all lines that we need to associate with a *Source File*. //! * **Source File** //! - A file at a version (i.e., commit) that introduces hunks into the final 'image' of the *Blamed File*. //! * **Suspects** //! - The versions of the files that can contain hunks that we could use in the final 'image' //! - multiple at the same time as the commit-graph may split up. //! - They turn into a *Source File* once we have found an association into the *Blamed File*. #![deny(rust_2018_idioms, missing_docs)] #![forbid(unsafe_code)] mod error; pub use error::Error; mod types; pub use types::{BlameEntry, BlamePathEntry, BlameRanges, Options, Outcome, Statistics}; mod file; pub use file::function::file; gix-blame-0.13.0/src/types.rs000064400000000000000000000375011046102023000140610ustar 00000000000000use gix_hash::ObjectId; use gix_object::bstr::BString; use smallvec::SmallVec; use std::ops::RangeInclusive; use std::{ num::NonZeroU32, ops::{AddAssign, Range, SubAssign}, }; use crate::file::function::tokens_for_diffing; use crate::Error; /// A type to represent one or more line ranges to blame in a file. /// /// It handles the conversion between git's 1-based inclusive ranges and the internal /// 0-based exclusive ranges used by the blame algorithm. /// /// # Examples /// /// ```rust /// use gix_blame::BlameRanges; /// /// // Blame lines 20 through 40 (inclusive) /// let range = BlameRanges::from_one_based_inclusive_range(20..=40); /// /// // Blame multiple ranges /// let mut ranges = BlameRanges::from_one_based_inclusive_ranges(vec![ /// 1..=4, // Lines 1-4 /// 10..=14, // Lines 10-14 /// ] /// ); /// ``` /// /// # Line Number Representation /// /// This type uses 1-based inclusive ranges to mirror `git`'s behaviour: /// - A range of `20..=40` represents 21 lines, spanning from line 20 up to and including line 40 /// - This will be converted to `19..40` internally as the algorithm uses 0-based ranges that are exclusive at the end /// /// # Empty Ranges /// You can blame the entire file by calling `BlameRanges::default()`, or by passing an empty vector to `from_one_based_inclusive_ranges`. #[derive(Debug, Clone, Default)] pub enum BlameRanges { /// Blame the entire file. #[default] WholeFile, /// Blame ranges in 0-based exclusive format. PartialFile(Vec>), } /// Lifecycle impl BlameRanges { /// Create from a single 0-based range. /// /// Note that the input range is 1-based inclusive, as used by git, and /// the output is a zero-based `BlameRanges` instance. pub fn from_one_based_inclusive_range(range: RangeInclusive) -> Result { let zero_based_range = Self::inclusive_to_zero_based_exclusive(range)?; Ok(Self::PartialFile(vec![zero_based_range])) } /// Create from multiple 0-based ranges. /// /// Note that the input ranges are 1-based inclusive, as used by git, and /// the output is a zero-based `BlameRanges` instance. /// /// If the input vector is empty, the result will be `WholeFile`. pub fn from_one_based_inclusive_ranges(ranges: Vec>) -> Result { if ranges.is_empty() { return Ok(Self::WholeFile); } let zero_based_ranges = ranges .into_iter() .map(Self::inclusive_to_zero_based_exclusive) .collect::>(); let mut result = Self::PartialFile(vec![]); for range in zero_based_ranges { result.merge_zero_based_exclusive_range(range?); } Ok(result) } /// Convert a 1-based inclusive range to a 0-based exclusive range. fn inclusive_to_zero_based_exclusive(range: RangeInclusive) -> Result, Error> { if range.start() == &0 { return Err(Error::InvalidOneBasedLineRange); } let start = range.start() - 1; let end = *range.end(); Ok(start..end) } } impl BlameRanges { /// Add a single range to blame. /// /// The new range will be merged with any overlapping existing ranges. pub fn add_one_based_inclusive_range(&mut self, new_range: RangeInclusive) -> Result<(), Error> { let zero_based_range = Self::inclusive_to_zero_based_exclusive(new_range)?; self.merge_zero_based_exclusive_range(zero_based_range); Ok(()) } /// Adds a new ranges, merging it with any existing overlapping ranges. fn merge_zero_based_exclusive_range(&mut self, new_range: Range) { match self { Self::PartialFile(ref mut ranges) => { // Partition ranges into those that don't overlap and those that do. let (mut non_overlapping, overlapping): (Vec<_>, Vec<_>) = ranges .drain(..) .partition(|range| new_range.end < range.start || range.end < new_range.start); let merged_range = overlapping.into_iter().fold(new_range, |acc, range| { acc.start.min(range.start)..acc.end.max(range.end) }); non_overlapping.push(merged_range); *ranges = non_overlapping; ranges.sort_by_key(|a| a.start); } Self::WholeFile => *self = Self::PartialFile(vec![new_range]), } } /// Gets zero-based exclusive ranges. pub fn to_zero_based_exclusive_ranges(&self, max_lines: u32) -> Vec> { match self { Self::WholeFile => { let full_range = 0..max_lines; vec![full_range] } Self::PartialFile(ranges) => ranges .iter() .filter_map(|range| { if range.end < max_lines { return Some(range.clone()); } if range.start < max_lines { Some(range.start..max_lines) } else { None } }) .collect(), } } } /// Options to be passed to [`file()`](crate::file()). #[derive(Default, Debug, Clone)] pub struct Options { /// The algorithm to use for diffing. pub diff_algorithm: gix_diff::blob::Algorithm, /// The ranges to blame in the file. pub ranges: BlameRanges, /// Don't consider commits before the given date. pub since: Option, /// Determine if rename tracking should be performed, and how. pub rewrites: Option, /// Collect debug information whenever there's a diff or rename that affects the outcome of a /// blame. pub debug_track_path: bool, } /// Represents a change during history traversal for blame. It is supposed to capture enough /// information to allow reconstruction of the way a blame was performed, i. e. the path the /// history traversal, combined with repeated diffing of two subsequent states in this history, has /// taken. /// /// This is intended for debugging purposes. #[derive(Clone, Debug)] pub struct BlamePathEntry { /// The path to the *Source File* in the blob after the change. pub source_file_path: BString, /// The path to the *Source File* in the blob before the change. Allows /// detection of renames. `None` for root commits. pub previous_source_file_path: Option, /// The commit id associated with the state after the change. pub commit_id: ObjectId, /// The blob id associated with the state after the change. pub blob_id: ObjectId, /// The blob id associated with the state before the change. pub previous_blob_id: ObjectId, /// When there is more than one `BlamePathEntry` for a commit, this indicates to which parent /// commit the change is related. pub parent_index: usize, } /// The outcome of [`file()`](crate::file()). #[derive(Debug, Default, Clone)] pub struct Outcome { /// One entry in sequential order, to associate a hunk in the blamed file with the source commit (and its lines) /// that introduced it. pub entries: Vec, /// A buffer with the file content of the *Blamed File*, ready for tokenization. pub blob: Vec, /// Additional information about the amount of work performed to produce the blame. pub statistics: Statistics, /// Contains a log of all changes that affected the outcome of this blame. pub blame_path: Option>, } /// Additional information about the performed operations. #[derive(Debug, Default, Copy, Clone)] pub struct Statistics { /// The amount of commits it traversed until the blame was complete. pub commits_traversed: usize, /// The amount of trees that were decoded to find the entry of the file to blame. pub trees_decoded: usize, /// The amount of tree-diffs to see if the filepath was added, deleted or modified. These diffs /// are likely partial as they are cancelled as soon as a change to the blamed file is /// detected. pub trees_diffed: usize, /// The amount of tree-diffs to see if the file was moved (or rewritten, in git terminology). /// These diffs are likely partial as they are cancelled as soon as a change to the blamed file /// is detected. pub trees_diffed_with_rewrites: usize, /// The amount of blobs there were compared to each other to learn what changed between commits. /// Note that in order to diff a blob, one needs to load both versions from the database. pub blobs_diffed: usize, } impl Outcome { /// Return an iterator over each entry in [`Self::entries`], along with its lines, line by line. /// /// Note that [`Self::blob`] must be tokenized in exactly the same way as the tokenizer that was used /// to perform the diffs, which is what this method assures. pub fn entries_with_lines(&self) -> impl Iterator)> + '_ { use gix_diff::blob::TokenSource; let mut interner = gix_diff::blob::Interner::new(self.blob.len() / 100); let lines_as_tokens: Vec<_> = tokens_for_diffing(&self.blob) .tokenize() .map(|token| interner.intern(token)) .collect(); self.entries.iter().map(move |e| { ( e.clone(), lines_as_tokens[e.range_in_blamed_file()] .iter() .map(|token| BString::new(interner[*token].into())) .collect(), ) }) } } /// Describes the offset of a particular hunk relative to the *Blamed File*. #[derive(Clone, Copy, Debug, PartialEq)] pub enum Offset { /// The amount of lines to add. Added(u32), /// The amount of lines to remove. Deleted(u32), } impl Offset { /// Shift the given `range` according to our offset. pub fn shifted_range(&self, range: &Range) -> Range { match self { Offset::Added(added) => { debug_assert!(range.start >= *added, "{self:?} {range:?}"); Range { start: range.start - added, end: range.end - added, } } Offset::Deleted(deleted) => Range { start: range.start + deleted, end: range.end + deleted, }, } } } impl AddAssign for Offset { fn add_assign(&mut self, rhs: u32) { match self { Self::Added(added) => *self = Self::Added(*added + rhs), Self::Deleted(deleted) => { if rhs > *deleted { *self = Self::Added(rhs - *deleted); } else { *self = Self::Deleted(*deleted - rhs); } } } } } impl SubAssign for Offset { fn sub_assign(&mut self, rhs: u32) { match self { Self::Added(added) => { if rhs > *added { *self = Self::Deleted(rhs - *added); } else { *self = Self::Added(*added - rhs); } } Self::Deleted(deleted) => *self = Self::Deleted(*deleted + rhs), } } } /// A mapping of a section of the *Blamed File* to the section in a *Source File* that introduced it. /// /// Both ranges are of the same size, but may use different [starting points](Range::start). Naturally, /// they have the same content, which is the reason they are in what is returned by [`file()`](crate::file()). #[derive(Clone, Debug, PartialEq)] pub struct BlameEntry { /// The index of the token in the *Blamed File* (typically lines) where this entry begins. pub start_in_blamed_file: u32, /// The index of the token in the *Source File* (typically lines) where this entry begins. /// /// This is possibly offset compared to `start_in_blamed_file`. pub start_in_source_file: u32, /// The amount of lines the hunk is spanning. pub len: NonZeroU32, /// The commit that introduced the section into the *Source File*. pub commit_id: ObjectId, /// The *Source File*'s name, in case it differs from *Blamed File*'s name. /// This happens when the file was renamed. pub source_file_name: Option, } impl BlameEntry { /// Create a new instance. pub fn new( range_in_blamed_file: Range, range_in_source_file: Range, commit_id: ObjectId, source_file_name: Option, ) -> Self { debug_assert!( range_in_blamed_file.end > range_in_blamed_file.start, "{range_in_blamed_file:?}" ); debug_assert!( range_in_source_file.end > range_in_source_file.start, "{range_in_source_file:?}" ); debug_assert_eq!(range_in_source_file.len(), range_in_blamed_file.len()); Self { start_in_blamed_file: range_in_blamed_file.start, start_in_source_file: range_in_source_file.start, len: NonZeroU32::new(range_in_blamed_file.len() as u32).expect("BUG: hunks are never empty"), commit_id, source_file_name, } } } impl BlameEntry { /// Return the range of tokens this entry spans in the *Blamed File*. pub fn range_in_blamed_file(&self) -> Range { let start = self.start_in_blamed_file as usize; start..start + self.len.get() as usize } /// Return the range of tokens this entry spans in the *Source File*. pub fn range_in_source_file(&self) -> Range { let start = self.start_in_source_file as usize; start..start + self.len.get() as usize } } pub(crate) trait LineRange { fn shift_by(&self, offset: Offset) -> Self; } impl LineRange for Range { fn shift_by(&self, offset: Offset) -> Self { offset.shifted_range(self) } } /// Tracks the hunks in the *Blamed File* that are not yet associated with the commit that introduced them. #[derive(Debug, PartialEq)] pub struct UnblamedHunk { /// The range in the file that is being blamed that this hunk represents. pub range_in_blamed_file: Range, /// Maps a commit to the range in a source file (i.e. *Blamed File* at a revision) that is /// equal to `range_in_blamed_file`. Since `suspects` rarely contains more than 1 item, it can /// efficiently be stored as a `SmallVec`. pub suspects: SmallVec<[(ObjectId, Range); 1]>, /// The *Source File*'s name, in case it differs from *Blamed File*'s name. pub source_file_name: Option, } impl UnblamedHunk { pub(crate) fn new(from_range_in_blamed_file: Range, suspect: ObjectId) -> Self { let range_start = from_range_in_blamed_file.start; let range_end = from_range_in_blamed_file.end; UnblamedHunk { range_in_blamed_file: range_start..range_end, suspects: [(suspect, range_start..range_end)].into(), source_file_name: None, } } pub(crate) fn has_suspect(&self, suspect: &ObjectId) -> bool { self.suspects.iter().any(|entry| entry.0 == *suspect) } pub(crate) fn get_range(&self, suspect: &ObjectId) -> Option<&Range> { self.suspects .iter() .find(|entry| entry.0 == *suspect) .map(|entry| &entry.1) } } #[derive(Debug)] pub(crate) enum Either { Left(T), Right(U), } /// A single change between two blobs, or an unchanged region. /// /// Line numbers refer to the file that is referred to as `after` or `NewOrDestination`, depending /// on the context. #[derive(Clone, Debug, PartialEq)] pub enum Change { /// A range of tokens that wasn't changed. Unchanged(Range), /// `(added_line_range, num_deleted_in_before)` AddedOrReplaced(Range, u32), /// `(line_to_start_deletion_at, num_deleted_in_before)` Deleted(u32, u32), }