diff --git a/.github/config-schema.json b/.github/config-schema.json index 130a23d08..1a97e55d3 100644 --- a/.github/config-schema.json +++ b/.github/config-schema.json @@ -664,7 +664,8 @@ "style": "red bold", "typechanged": "", "untracked": "?", - "up_to_date": "" + "up_to_date": "", + "use_git_executable": false }, "allOf": [ { @@ -3649,6 +3650,10 @@ "default": false, "type": "boolean" }, + "use_git_executable": { + "default": false, + "type": "boolean" + }, "windows_starship": { "type": [ "string", diff --git a/Cargo.lock b/Cargo.lock index ed5138893..3d716a9fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -360,6 +360,12 @@ version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytesize" version = "1.3.3" @@ -607,6 +613,20 @@ dependencies = [ "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 = "deelevate" version = "0.2.0" @@ -846,10 +866,11 @@ dependencies = [ [[package]] name = "faster-hex" -version = "0.9.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2a2b11eda1d40935b26cf18f6833c526845ae8c41e58d09af6adeb6f0269183" +checksum = "7223ae2d2f179b803433d9c830478527e92b8117eab39460edae7f1614d9fb73" dependencies = [ + "heapless", "serde", ] @@ -980,27 +1001,33 @@ dependencies = [ [[package]] name = "gix" -version = "0.71.0" +version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a61e71ec6817fc3c9f12f812682cfe51ee6ea0d2e27e02fc3849c35524617435" +checksum = "01237e8d3d78581f71642be8b0c2ae8c0b2b5c251c9c5d9ebbea3c1ea280dce8" dependencies = [ "gix-actor", + "gix-attributes", + "gix-command", "gix-commitgraph", "gix-config", "gix-date", "gix-diff", + "gix-dir", "gix-discover", "gix-features", + "gix-filter", "gix-fs", "gix-glob", "gix-hash", "gix-hashtable", + "gix-ignore", "gix-index", "gix-lock", "gix-object", "gix-odb", "gix-pack", "gix-path", + "gix-pathspec", "gix-protocol", "gix-ref", "gix-refspec", @@ -1008,12 +1035,15 @@ dependencies = [ "gix-revwalk", "gix-sec", "gix-shallow", + "gix-status", + "gix-submodule", "gix-tempfile", "gix-trace", "gix-traverse", "gix-url", "gix-utils", "gix-validate", + "gix-worktree", "once_cell", "smallvec", "thiserror 2.0.12", @@ -1021,9 +1051,9 @@ dependencies = [ [[package]] name = "gix-actor" -version = "0.34.0" +version = "0.35.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f438c87d4028aca4b82f82ba8d8ab1569823cfb3e5bc5fa8456a71678b2a20e7" +checksum = "6b300e6e4f31f3f6bd2de5e2b0caab192ced00dc0fcd0f7cc56e28c575c8e1ff" dependencies = [ "bstr", "gix-date", @@ -1033,6 +1063,23 @@ dependencies = [ "winnow", ] +[[package]] +name = "gix-attributes" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e26b3ac280ddb25bb6980d34f4a82ee326f78bf2c6d4ea45eef2d940048b8e" +dependencies = [ + "bstr", + "gix-glob", + "gix-path", + "gix-quote", + "gix-trace", + "kstring", + "smallvec", + "thiserror 2.0.12", + "unicode-bom", +] + [[package]] name = "gix-bitmap" version = "0.2.14" @@ -1053,9 +1100,9 @@ dependencies = [ [[package]] name = "gix-command" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0378995847773a697f8e157fe2963ecf3462fe64be05b7b3da000b3b472def8" +checksum = "d2f47f3fb4ba33644061e8e0e1030ef2a937d42dc969553118c320a205a9fb28" dependencies = [ "bstr", "gix-path", @@ -1066,9 +1113,9 @@ dependencies = [ [[package]] name = "gix-commitgraph" -version = "0.27.0" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "043cbe49b7a7505150db975f3cb7c15833335ac1e26781f615454d9d640a28fe" +checksum = "e05050fd6caa6c731fe3bd7f9485b3b520be062d3d139cb2626e052d6c127951" dependencies = [ "bstr", "gix-chunk", @@ -1079,9 +1126,9 @@ dependencies = [ [[package]] name = "gix-config" -version = "0.44.0" +version = "0.45.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c6f830bf746604940261b49abf7f655d2c19cadc9f4142ae9379e3a316e8cfa" +checksum = "48f3c8f357ae049bfb77493c2ec9010f58cfc924ae485e1116c3718fc0f0d881" dependencies = [ "bstr", "gix-config-value", @@ -1100,9 +1147,9 @@ dependencies = [ [[package]] name = "gix-config-value" -version = "0.14.12" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8dc2c844c4cf141884678cabef736fd91dd73068b9146e6f004ba1a0457944b6" +checksum = "439d62e241dae2dffd55bfeeabe551275cf9d9f084c5ebc6b48bad49d03285b7" dependencies = [ "bitflags 2.9.0", "bstr", @@ -1113,33 +1160,66 @@ dependencies = [ [[package]] name = "gix-date" -version = "0.9.4" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daa30058ec7d3511fbc229e4f9e696a35abd07ec5b82e635eff864a2726217e4" +checksum = "3a98593f1f1e14b9fa15c5b921b2c465e904d698b9463e21bb377be8376c3c1a" dependencies = [ "bstr", "itoa", "jiff", + "smallvec", "thiserror 2.0.12", ] [[package]] name = "gix-diff" -version = "0.51.0" +version = "0.52.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2c975dad2afc85e4e233f444d1efbe436c3cdcf3a07173984509c436d00a3f8" +checksum = "5e9b43e95fe352da82a969f0c84ff860c2de3e724d93f6681fedbcd6c917f252" dependencies = [ "bstr", + "gix-attributes", + "gix-command", + "gix-filter", + "gix-fs", "gix-hash", + "gix-index", "gix-object", + "gix-path", + "gix-pathspec", + "gix-tempfile", + "gix-trace", + "gix-traverse", + "gix-worktree", + "imara-diff", + "thiserror 2.0.12", +] + +[[package]] +name = "gix-dir" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01e6e2dc5b8917142d0ffe272209d1671e45b771e433f90186bc71c016792e87" +dependencies = [ + "bstr", + "gix-discover", + "gix-fs", + "gix-ignore", + "gix-index", + "gix-object", + "gix-path", + "gix-pathspec", + "gix-trace", + "gix-utils", + "gix-worktree", "thiserror 2.0.12", ] [[package]] name = "gix-discover" -version = "0.39.0" +version = "0.40.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7fb8a4349b854506a3915de18d3341e5f1daa6b489c8affc9ca0d69efe86781" +checksum = "dccfe3e25b4ea46083916c56db3ba9d1e6ef6dce54da485f0463f9fc0fe1837c" dependencies = [ "bstr", "dunce", @@ -1153,9 +1233,9 @@ dependencies = [ [[package]] name = "gix-features" -version = "0.41.1" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016d6050219458d14520fe22bdfdeb9cb71631dec9bc2724767c983f60109634" +checksum = "56f4399af6ec4fd9db84dd4cf9656c5c785ab492ab40a7c27ea92b4241923fed" dependencies = [ "crc32fast", "crossbeam-channel", @@ -1172,10 +1252,31 @@ dependencies = [ ] [[package]] -name = "gix-fs" -version = "0.14.0" +name = "gix-filter" +version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "951e886120dc5fa8cac053e5e5c89443f12368ca36811b2e43d1539081f9c111" +checksum = "f90c21f0d61778f518bbb7c431b00247bf4534b2153c3e85bcf383876c55ca6c" +dependencies = [ + "bstr", + "encoding_rs", + "gix-attributes", + "gix-command", + "gix-hash", + "gix-object", + "gix-packetline-blocking", + "gix-path", + "gix-quote", + "gix-trace", + "gix-utils", + "smallvec", + "thiserror 2.0.12", +] + +[[package]] +name = "gix-fs" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a0637149b4ef24d3ea55f81f77231401c8463fae6da27331c987957eb597c7" dependencies = [ "bstr", "fastrand", @@ -1187,9 +1288,9 @@ dependencies = [ [[package]] name = "gix-glob" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20972499c03473e773a2099e5fd0c695b9b72465837797a51a43391a1635a030" +checksum = "2926b03666e83b8d01c10cf06e5733521aacbd2d97179a4c9b1fdddabb9e937d" dependencies = [ "bitflags 2.9.0", "bstr", @@ -1199,9 +1300,9 @@ dependencies = [ [[package]] name = "gix-hash" -version = "0.17.0" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "834e79722063958b03342edaa1e17595cd2939bb2b3306b3225d0815566dcb49" +checksum = "8d4900562c662852a6b42e2ef03442eccebf24f047d8eab4f23bc12ef0d785d8" dependencies = [ "faster-hex", "gix-features", @@ -1211,9 +1312,9 @@ dependencies = [ [[package]] name = "gix-hashtable" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f06066d8702a9186dc1fdc1ed751ff2d7e924ceca21cb5d51b8f990c9c2e014a" +checksum = "b5b5cb3c308b4144f2612ff64e32130e641279fcf1a84d8d40dad843b4f64904" dependencies = [ "gix-hash", "hashbrown 0.14.5", @@ -1221,10 +1322,23 @@ dependencies = [ ] [[package]] -name = "gix-index" -version = "0.39.0" +name = "gix-ignore" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "855bece2d4153453aa5d0a80d51deea1ce8cd6a3b4cf213da85ac344ccb908a7" +checksum = "ae358c3c96660b10abc7da63c06788dfded603e717edbd19e38c6477911b71c8" +dependencies = [ + "bstr", + "gix-glob", + "gix-path", + "gix-trace", + "unicode-bom", +] + +[[package]] +name = "gix-index" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d505aea7d7c4267a3153cb90c712a89970b4dd02a2cb3205be322891f530b5" dependencies = [ "bitflags 2.9.0", "bstr", @@ -1243,16 +1357,16 @@ dependencies = [ "itoa", "libc", "memmap2", - "rustix 0.38.44", + "rustix 1.0.5", "smallvec", "thiserror 2.0.12", ] [[package]] name = "gix-lock" -version = "17.0.0" +version = "17.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df47b8f11c34520db5541bc5fc9fbc8e4b0bdfcec3736af89ccb1a5728a0126f" +checksum = "570f8b034659f256366dc90f1a24924902f20acccd6a15be96d44d1269e7a796" dependencies = [ "gix-tempfile", "gix-utils", @@ -1261,9 +1375,9 @@ dependencies = [ [[package]] name = "gix-object" -version = "0.48.0" +version = "0.49.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4943fcdae6ffc135920c9ea71e0362ed539182924ab7a85dd9dac8d89b0dd69a" +checksum = "d957ca3640c555d48bb27f8278c67169fa1380ed94f6452c5590742524c40fbb" dependencies = [ "bstr", "gix-actor", @@ -1282,9 +1396,9 @@ dependencies = [ [[package]] name = "gix-odb" -version = "0.68.0" +version = "0.69.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50306d40dcc982eb6b7593103f066ea6289c7b094cb9db14f3cd2be0b9f5e610" +checksum = "868f703905fdbcfc1bd750942f82419903ecb7039f5288adb5206d6de405e0c9" dependencies = [ "arc-swap", "gix-date", @@ -1303,9 +1417,9 @@ dependencies = [ [[package]] name = "gix-pack" -version = "0.58.0" +version = "0.59.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b65fffb09393c26624ca408d32cfe8776fb94cd0a5cdf984905e1d2f39779cb" +checksum = "9d49c55d69c8449f2a0a5a77eb9cbacfebb6b0e2f1215f0fc23a4cb60528a450" dependencies = [ "clru", "gix-chunk", @@ -1322,9 +1436,21 @@ dependencies = [ [[package]] name = "gix-packetline" -version = "0.18.4" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "123844a70cf4d5352441dc06bab0da8aef61be94ec239cb631e0ba01dc6d3a04" +checksum = "8ddc034bc67c848e4ef7596ab5528cd8fd439d310858dbe1ce8b324f25deb91c" +dependencies = [ + "bstr", + "faster-hex", + "gix-trace", + "thiserror 2.0.12", +] + +[[package]] +name = "gix-packetline-blocking" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c44880f028ba46d6cf37a66d27a300310c6b51b8ed0e44918f93df061168e2f3" dependencies = [ "bstr", "faster-hex", @@ -1334,22 +1460,38 @@ dependencies = [ [[package]] name = "gix-path" -version = "0.10.15" +version = "0.10.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f910668e2f6b2a55ff35a1f04df88a1a049f7b868507f4cbeeaa220eaba7be87" +checksum = "c091d2e887e02c3462f52252c5ea61150270c0f2657b642e8d0d6df56c16e642" dependencies = [ "bstr", "gix-trace", + "gix-validate", "home", "once_cell", "thiserror 2.0.12", ] [[package]] -name = "gix-protocol" -version = "0.49.0" +name = "gix-pathspec" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5678ddae1d62880bc30e2200be1b9387af3372e0e88e21f81b4e7f8367355b5a" +checksum = "ce061c50e5f8f7c830cacb3da3e999ae935e283ce8522249f0ce2256d110979d" +dependencies = [ + "bitflags 2.9.0", + "bstr", + "gix-attributes", + "gix-config-value", + "gix-glob", + "gix-path", + "thiserror 2.0.12", +] + +[[package]] +name = "gix-protocol" +version = "0.50.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5c17d78bb0414f8d60b5f952196dc2e47ec320dca885de9128ecdb4a0e38401" dependencies = [ "bstr", "gix-date", @@ -1366,9 +1508,9 @@ dependencies = [ [[package]] name = "gix-quote" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b005c550bf84de3b24aa5e540a23e6146a1c01c7d30470e35d75a12f827f969" +checksum = "4a375a75b4d663e8bafe3bf4940a18a23755644c13582fa326e99f8f987d83fd" dependencies = [ "bstr", "gix-utils", @@ -1377,9 +1519,9 @@ dependencies = [ [[package]] name = "gix-ref" -version = "0.51.0" +version = "0.52.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2e1f7eb6b7ce82d2d19961f74bd637bab3ea79b1bc7bfb23dbefc67b0415d8b" +checksum = "d1b7985657029684d759f656b09abc3e2c73085596d5cdb494428823970a7762" dependencies = [ "gix-actor", "gix-features", @@ -1398,9 +1540,9 @@ dependencies = [ [[package]] name = "gix-refspec" -version = "0.29.0" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d8587b21e2264a6e8938d940c5c99662779c13a10741a5737b15fc85c252ffc" +checksum = "445ed14e3db78e8e79980085e3723df94e1c8163b3ae5bc8ed6a8fe6cf983b42" dependencies = [ "bstr", "gix-hash", @@ -1412,9 +1554,9 @@ dependencies = [ [[package]] name = "gix-revision" -version = "0.33.0" +version = "0.34.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "342caa4e158df3020cadf62f656307c3948fe4eacfdf67171d7212811860c3e9" +checksum = "78d0b8e5cbd1c329e25383e088cb8f17439414021a643b30afa5146b71e3c65d" dependencies = [ "bitflags 2.9.0", "bstr", @@ -1430,9 +1572,9 @@ dependencies = [ [[package]] name = "gix-revwalk" -version = "0.19.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dc7c3d7e5cdc1ab8d35130106e4af0a4f9f9eca0c81f4312b690780e92bde0d" +checksum = "1bc756b73225bf005ddeb871d1ca7b3c33e2417d0d53e56effa5a36765b52b28" dependencies = [ "gix-commitgraph", "gix-date", @@ -1445,21 +1587,21 @@ dependencies = [ [[package]] name = "gix-sec" -version = "0.10.12" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47aeb0f13de9ef2f3033f5ff218de30f44db827ac9f1286f9ef050aacddd5888" +checksum = "d0dabbc78c759ecc006b970339394951b2c8e1e38a37b072c105b80b84c308fd" dependencies = [ "bitflags 2.9.0", "gix-path", "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "gix-shallow" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc0598aacfe1d52575a21c9492fee086edbb21e228ec36c819c42ab923f434c3" +checksum = "6b9a6f6e34d6ede08f522d89e5c7990b4f60524b8ae6ebf8e850963828119ad4" dependencies = [ "bstr", "gix-hash", @@ -1468,11 +1610,50 @@ dependencies = [ ] [[package]] -name = "gix-tempfile" -version = "17.0.0" +name = "gix-status" +version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6de439bbb9a5d3550c9c7fab0e16d2d637d120fcbe0dfbc538772a187f099b" +checksum = "072099c2415cfa5397df7d47eacbcb6016d2cd17e0d674c74965e6ad1b17289f" dependencies = [ + "bstr", + "filetime", + "gix-diff", + "gix-dir", + "gix-features", + "gix-filter", + "gix-fs", + "gix-hash", + "gix-index", + "gix-object", + "gix-path", + "gix-pathspec", + "gix-worktree", + "portable-atomic", + "thiserror 2.0.12", +] + +[[package]] +name = "gix-submodule" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f51472f05a450cc61bc91ed2f62fb06e31e2bbb31c420bc4be8793f26c8b0c1" +dependencies = [ + "bstr", + "gix-config", + "gix-path", + "gix-pathspec", + "gix-refspec", + "gix-url", + "thiserror 2.0.12", +] + +[[package]] +name = "gix-tempfile" +version = "17.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c750e8c008453a2dba67a2b0d928b7716e05da31173a3f5e351d5457ad4470aa" +dependencies = [ + "dashmap", "gix-fs", "libc", "once_cell", @@ -1488,9 +1669,9 @@ checksum = "7c396a2036920c69695f760a65e7f2677267ccf483f25046977d87e4cb2665f7" [[package]] name = "gix-transport" -version = "0.46.0" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3f68c2870bfca8278389d2484a7f2215b67d0b0cc5277d3c72ad72acf41787e" +checksum = "edfe22ba26d4b65c17879f12b9882eafe65d3c8611c933b272fce2c10f546f59" dependencies = [ "bstr", "gix-command", @@ -1504,9 +1685,9 @@ dependencies = [ [[package]] name = "gix-traverse" -version = "0.45.0" +version = "0.46.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36c0b049f8bdb61b20016694102f7b507f2e1727e83e9c5e6dad4f7d84ff7384" +checksum = "39094185f6d9a4d81101130fbbf7f598a06441d774ae3b3ae7930a613bbe1157" dependencies = [ "bitflags 2.9.0", "gix-commitgraph", @@ -1521,9 +1702,9 @@ dependencies = [ [[package]] name = "gix-url" -version = "0.30.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48dfe23f93f1ddb84977d80bb0dd7aa09d1bf5d5afc0c9b6820cccacc25ae860" +checksum = "42a1ad0b04a5718b5cb233e6888e52a9b627846296161d81dcc5eb9203ec84b8" dependencies = [ "bstr", "gix-features", @@ -1535,24 +1716,44 @@ dependencies = [ [[package]] name = "gix-utils" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "189f8724cf903e7fd57cfe0b7bc209db255cacdcb22c781a022f52c3a774f8d0" +checksum = "5351af2b172caf41a3728eb4455326d84e0d70fe26fc4de74ab0bd37df4191c5" dependencies = [ + "bstr", "fastrand", "unicode-normalization", ] [[package]] name = "gix-validate" -version = "0.9.4" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34b5f1253109da6c79ed7cf6e1e38437080bb6d704c76af14c93e2f255234084" +checksum = "77b9e00cacde5b51388d28ed746c493b18a6add1f19b5e01d686b3b9ece66d4d" dependencies = [ "bstr", "thiserror 2.0.12", ] +[[package]] +name = "gix-worktree" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54f1916f8d928268300c977d773dd70a8746b646873b77add0a34876a8c847e9" +dependencies = [ + "bstr", + "gix-attributes", + "gix-features", + "gix-fs", + "gix-glob", + "gix-hash", + "gix-ignore", + "gix-index", + "gix-object", + "gix-path", + "gix-validate", +] + [[package]] name = "guess_host_triple" version = "0.1.4" @@ -1565,6 +1766,15 @@ dependencies = [ "winapi", ] +[[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.12.3" @@ -1599,6 +1809,16 @@ dependencies = [ "hashbrown 0.15.2", ] +[[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" @@ -1789,6 +2009,15 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "imara-diff" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17d34b7d42178945f775e84bc4c36dde7c1c6cdfea656d3354d009056f2bb3d2" +dependencies = [ + "hashbrown 0.15.2", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -1917,6 +2146,15 @@ dependencies = [ "serde_json", ] +[[package]] +name = "kstring" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "558bf9508a558512042d3095138b1f7b8fe90c5467d94f9f1da28b3731c5dbd1" +dependencies = [ + "static_assertions", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -3087,6 +3325,7 @@ dependencies = [ "nu-ansi-term", "open", "os_info", + "parking_lot", "path-slash", "pest", "pest_derive", diff --git a/Cargo.toml b/Cargo.toml index 628a9ddfd..a3e8c6fa9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,7 +42,7 @@ clap_complete_nushell = "4.5.5" dirs = "6.0.0" dunce = "1.0.5" # default feature restriction addresses https://github.com/starship/starship/issues/4251 -gix = { version = "0.71.0", default-features = false, features = ["max-performance-safe", "revision", "zlib-rs"] } +gix = { version = "0.72.1", default-features = false, features = ["max-performance-safe", "revision", "zlib-rs", "status"] } indexmap = { version = "2.9.0", features = ["serde"] } jsonc-parser = { version = "0.26.2", features = ["serde"] } log = { version = "0.4.27", features = ["std"] } @@ -53,6 +53,8 @@ nu-ansi-term = "0.50.1" open = "5.3.2" # update os module config and tests when upgrading os_info os_info = "3.10.0" +# for efficient shared state between `git_status` and `git_metrics`, allowing parallel printing. This is for poison-free locks. +parking_lot = "0.12.3" path-slash = "0.2.1" pest = "2.8.0" pest_derive = "2.8.0" @@ -124,6 +126,11 @@ codegen-units = 1 lto = true strip = true +[profile.bench] +codegen-units = 16 +lto = "thin" +strip = false + [[bin]] name = "starship" path = "src/main.rs" diff --git a/docs/config/README.md b/docs/config/README.md index c34d07c5a..b0630f328 100644 --- a/docs/config/README.md +++ b/docs/config/README.md @@ -1960,25 +1960,26 @@ You can disable the module or use the `windows_starship` option to use a Windows ### Options -| Option | Default | Description | -| ------------------- | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | -| `format` | `'([\[$all_status$ahead_behind\]]($style) )'` | The default format for `git_status` | -| `conflicted` | `'='` | This branch has merge conflicts. | -| `ahead` | `'⇡'` | The format of `ahead` | -| `behind` | `'⇣'` | The format of `behind` | -| `diverged` | `'⇕'` | The format of `diverged` | -| `up_to_date` | `''` | The format of `up_to_date` | -| `untracked` | `'?'` | The format of `untracked` | -| `stashed` | `'$'` | The format of `stashed` | -| `modified` | `'!'` | The format of `modified` | -| `staged` | `'+'` | The format of `staged` | -| `renamed` | `'»'` | The format of `renamed` | -| `deleted` | `'✘'` | The format of `deleted` | -| `typechanged` | `""` | The format of `typechanged` | -| `style` | `'bold red'` | The style for the module. | -| `ignore_submodules` | `false` | Ignore changes to submodules. | -| `disabled` | `false` | Disables the `git_status` module. | -| `windows_starship` | | Use this (Linux) path to a Windows Starship executable to render `git_status` when on Windows paths in WSL. | +| Option | Default | Description | +| -------------------- | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | +| `format` | `'([\[$all_status$ahead_behind\]]($style) )'` | The default format for `git_status` | +| `conflicted` | `'='` | This branch has merge conflicts. | +| `ahead` | `'⇡'` | The format of `ahead` | +| `behind` | `'⇣'` | The format of `behind` | +| `diverged` | `'⇕'` | The format of `diverged` | +| `up_to_date` | `''` | The format of `up_to_date` | +| `untracked` | `'?'` | The format of `untracked` | +| `stashed` | `'$'` | The format of `stashed` | +| `modified` | `'!'` | The format of `modified` | +| `staged` | `'+'` | The format of `staged` | +| `renamed` | `'»'` | The format of `renamed` | +| `deleted` | `'✘'` | The format of `deleted` | +| `typechanged` | `""` | The format of `typechanged` | +| `style` | `'bold red'` | The style for the module. | +| `ignore_submodules` | `false` | Ignore changes to submodules. | +| `disabled` | `false` | Disables the `git_status` module. | +| `windows_starship` | | Use this (Linux) path to a Windows Starship executable to render `git_status` when on Windows paths in WSL. | +| `use_git_executable` | `false` | Do not use `gitoxide` for computing the status, but use the `git` executable instead. | ### Variables diff --git a/src/configs/git_status.rs b/src/configs/git_status.rs index fb02a49f8..6dc334e73 100644 --- a/src/configs/git_status.rs +++ b/src/configs/git_status.rs @@ -24,6 +24,7 @@ pub struct GitStatusConfig<'a> { pub typechanged: &'a str, pub ignore_submodules: bool, pub disabled: bool, + pub use_git_executable: bool, #[serde(skip_serializing_if = "Option::is_none")] pub windows_starship: Option<&'a str>, } @@ -47,6 +48,7 @@ impl Default for GitStatusConfig<'_> { typechanged: "", ignore_submodules: false, disabled: false, + use_git_executable: false, windows_starship: None, } } diff --git a/src/context.rs b/src/context.rs index 47ce3cda2..87af2120b 100644 --- a/src/context.rs +++ b/src/context.rs @@ -294,12 +294,13 @@ impl<'a> Context<'a> { let mut git_open_opts_map = git_sec::trust::Mapping::::default(); - // don't use the global git configs + // Load all the configuration as it affects aspects of the + // `git_status` and `git_metrics` modules. let config = gix::open::permissions::Config { - git_binary: false, - system: false, - git: false, - user: false, + git_binary: true, + system: true, + git: true, + user: true, env: true, includes: true, }; @@ -652,7 +653,7 @@ pub struct Repo { /// Contains `true` if the value of `core.fsmonitor` is set to `true`. /// If not `true`, `fsmonitor` is explicitly disabled in git commands. - fs_monitor_value_is_true: bool, + pub(crate) fs_monitor_value_is_true: bool, // Kind of repository, work tree or bare pub kind: Kind, @@ -671,7 +672,7 @@ impl Repo { pub fn exec_git + Debug>( &self, context: &Context, - git_args: &[T], + git_args: impl IntoIterator, ) -> Option { let mut command = create_command("git").ok()?; diff --git a/src/lib.rs b/src/lib.rs index 505866377..21f1d91c5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,8 @@ #[macro_use] extern crate shadow_rs; +use std::thread::available_parallelism; + shadow!(shadow); // Lib is present to allow for benchmarking @@ -24,3 +26,18 @@ mod utils; #[cfg(test)] mod test; + +/// Return the number of threads starship should use, if configured. +pub fn num_configured_starship_threads() -> Option { + std::env::var("STARSHIP_NUM_THREADS") + .ok() + .and_then(|s| s.parse().ok()) +} + +/// Return the maximum number of threads for the global thread-pool. +pub fn num_rayon_threads() -> usize { + num_configured_starship_threads() + // Default to the number of logical cores, + // but restrict the number of threads to 8 + .unwrap_or_else(|| available_parallelism().map(usize::from).unwrap_or(1).min(8)) +} diff --git a/src/main.rs b/src/main.rs index 20832b50c..ffb5210e1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,6 @@ use clap::crate_authors; use std::io; use std::path::PathBuf; -use std::thread::available_parallelism; use std::time::SystemTime; use clap::{CommandFactory, Parser, Subcommand, ValueEnum}; @@ -281,16 +280,8 @@ fn main() { /// Initialize global `rayon` thread pool fn init_global_threadpool() { - // Allow overriding the number of threads - let num_threads = std::env::var("STARSHIP_NUM_THREADS") - .ok() - .and_then(|s| s.parse().ok()) - // Default to the number of logical cores, - // but restrict the number of threads to 8 - .unwrap_or_else(|| available_parallelism().map(usize::from).unwrap_or(1).min(8)); - rayon::ThreadPoolBuilder::new() - .num_threads(num_threads) + .num_threads(num_rayon_threads()) .build_global() .expect("Failed to initialize worker thread pool"); } diff --git a/src/modules/git_metrics.rs b/src/modules/git_metrics.rs index 3df96137b..3f9761d2b 100644 --- a/src/modules/git_metrics.rs +++ b/src/modules/git_metrics.rs @@ -1,12 +1,16 @@ +use gix::bstr::{BStr, ByteSlice}; +use gix::diff::blob::ResourceKind; +use gix::diff::blob::pipeline::WorktreeRoots; +use rayon::prelude::{IntoParallelRefIterator, ParallelIterator}; use regex::Regex; +use super::Context; +use crate::configs::git_status::GitStatusConfig; use crate::{ config::ModuleConfig, configs::git_metrics::GitMetricsConfig, formatter::StringFormatter, formatter::string_formatter::StringFormatterError, module::Module, }; -use super::Context; - /// Creates a module with the current added/deleted lines in the git repository at the /// current directory pub fn module<'a>(context: &'a Context) -> Option> { @@ -20,14 +24,227 @@ pub fn module<'a>(context: &'a Context) -> Option> { }; let repo = context.get_repo().ok()?; - let mut git_args = vec!["diff", "--shortstat"]; - if config.ignore_submodules { - git_args.push("--ignore-submodules"); + let gix_repo = repo.open(); + if gix_repo.is_bare() { + return None; } + // TODO: remove this special case once `gitoxide` can handle sparse indices for tree-index comparisons. + let stats = if gix_repo.index_or_empty().ok()?.is_sparse() || repo.fs_monitor_value_is_true { + let mut git_args = vec!["diff", "--shortstat"]; + if config.ignore_submodules { + git_args.push("--ignore-submodules"); + } - let diff = repo.exec_git(context, &git_args)?.stdout; + let diff = repo.exec_git(context, &git_args)?.stdout; - let stats = GitDiff::parse(&diff); + GitDiff::parse(&diff) + } else { + #[derive(Default)] + struct Diff { + added: usize, + deleted: usize, + } + impl Diff { + fn add(&mut self, c: Option>) { + let Some(c) = c else { return }; + self.added += c.insertions as usize; + self.deleted += c.removals as usize; + } + } + let status_module = context.new_module("git_status"); + let status_config = GitStatusConfig::try_load(status_module.config); + let status = super::git_status::get_static_repo_status(context, repo, &status_config)?; + let gix_repo = gix_repo.with_object_memory(); + gix_repo.write_blob([]).ok()?; /* create empty blob */ + let tree_index_cache = prevent_external_diff( + gix_repo + .diff_resource_cache(gix::diff::blob::pipeline::Mode::ToGit, Default::default()) + .ok()?, + ); + let index_worktree_cache = prevent_external_diff( + gix_repo + .diff_resource_cache( + gix::diff::blob::pipeline::Mode::ToGit, + WorktreeRoots { + old_root: None, + new_root: gix_repo.workdir().map(ToOwned::to_owned), + }, + ) + .ok()?, + ); + let diff = status + .changes + .par_iter() + .map_init( + { + let repo = gix_repo.into_sync(); + move || { + let repo = repo.to_thread_local(); + (repo, tree_index_cache.clone(), index_worktree_cache.clone()) + } + }, + |(repo, tree_index_cache, index_worktree_cache), change| { + use gix::status; + let mut diff = Diff::default(); + match change { + status::Item::TreeIndex(change) => { + use gix::diff::index::Change; + match change { + Change::Addition { + entry_mode, + location, + id, + .. + } => { + diff.added += count_lines( + location, + id.as_ref().into(), + *entry_mode, + tree_index_cache, + repo, + ); + } + Change::Deletion { + entry_mode, + location, + id, + .. + } => { + diff.deleted += count_lines( + location, + id.as_ref().into(), + *entry_mode, + tree_index_cache, + repo, + ); + } + Change::Modification { + location, + previous_entry_mode, + previous_id, + entry_mode, + id, + .. + } => { + let location = location.as_ref(); + diff.add(diff_two_opt( + location, + previous_id.as_ref().to_owned(), + *previous_entry_mode, + location, + id.as_ref().to_owned(), + *entry_mode, + tree_index_cache, + repo, + )); + } + Change::Rewrite { + source_location, + source_entry_mode, + source_id, + location, + entry_mode, + id, + copy, + .. + } => { + if *copy { + diff.added += count_lines( + location, + id.as_ref().into(), + *entry_mode, + tree_index_cache, + repo, + ); + } else { + diff.add(diff_two_opt( + source_location.as_ref(), + source_id.as_ref().to_owned(), + *source_entry_mode, + location, + id.as_ref().to_owned(), + *entry_mode, + tree_index_cache, + repo, + )); + } + } + } + } + status::Item::IndexWorktree(change) => { + use gix::status::index_worktree::Item; + use gix::status::plumbing::index_as_worktree::{Change, EntryStatus}; + match change { + Item::Modification { + rela_path, + entry, + status: EntryStatus::Change(Change::Removed), + .. + } => { + diff.deleted += count_lines( + rela_path.as_bstr(), + entry.id, + entry.mode, + tree_index_cache, + repo, + ); + } + Item::Modification { + rela_path, + entry, + status: + EntryStatus::Change(Change::Modification { + content_change: Some(_), + .. + }), + .. + } => { + let location = rela_path.as_bstr(); + diff.add(diff_two_opt( + location, + entry.id, + entry.mode, + location, + repo.object_hash().null(), + entry.mode, + index_worktree_cache, + repo, + )); + } + Item::Modification { + rela_path, + entry, + status: EntryStatus::IntentToAdd, + .. + } => { + diff.added += count_lines( + rela_path.as_bstr(), + repo.object_hash().null(), + entry.mode, + index_worktree_cache, + repo, + ); + } + Item::Rewrite { .. } => { + unreachable!("not activated") + } + _ => {} + } + } + }; + diff + }, + ) + .reduce(Diff::default, |a, b| Diff { + added: a.added + b.added, + deleted: a.deleted + b.deleted, + }); + + GitDiff { + added: diff.added.to_string(), + deleted: diff.deleted.to_string(), + } + }; let parsed = StringFormatter::new(config.format).and_then(|formatter| { formatter @@ -37,8 +254,8 @@ pub fn module<'a>(context: &'a Context) -> Option> { _ => None, }) .map(|variable| match variable { - "added" => GitDiff::get_variable(config.only_nonzero_diffs, stats.added), - "deleted" => GitDiff::get_variable(config.only_nonzero_diffs, stats.deleted), + "added" => GitDiff::get_variable(config.only_nonzero_diffs, &stats.added), + "deleted" => GitDiff::get_variable(config.only_nonzero_diffs, &stats.deleted), _ => None, }) .parse(None, Some(context)) @@ -55,16 +272,95 @@ pub fn module<'a>(context: &'a Context) -> Option> { Some(module) } -/// Represents the parsed output from a git diff. -struct GitDiff<'a> { - added: &'a str, - deleted: &'a str, +fn prevent_external_diff(mut cache: gix::diff::blob::Platform) -> gix::diff::blob::Platform { + cache.options.skip_internal_diff_if_external_is_configured = false; + cache } -impl<'a> GitDiff<'a> { +#[allow(clippy::too_many_arguments)] +fn diff_two_opt( + lhs_location: &BStr, + lhs_id: gix::ObjectId, + lhs_kind: gix::index::entry::Mode, + rhs_location: &BStr, + rhs_id: gix::ObjectId, + rhs_kind: gix::index::entry::Mode, + cache: &mut gix::diff::blob::Platform, + find: &impl gix::objs::FindObjectOrHeader, +) -> Option> { + cache + .set_resource( + lhs_id, + lhs_kind.to_tree_entry_mode()?.kind(), + lhs_location, + ResourceKind::OldOrSource, + find, + ) + .ok()?; + cache + .set_resource( + rhs_id, + rhs_kind.to_tree_entry_mode()?.kind(), + rhs_location, + ResourceKind::NewOrDestination, + find, + ) + .ok()?; + count_diff_lines(cache.prepare_diff().ok()?) +} + +fn count_lines( + location: &BStr, + id: gix::ObjectId, + kind: gix::index::entry::Mode, + cache: &mut gix::diff::blob::Platform, + find: &impl gix::objs::FindObjectOrHeader, +) -> usize { + diff_two_opt( + location, + id.kind().null(), + kind, + location, + id, + kind, + cache, + find, + ) + .map_or(0, |diff| diff.insertions as usize) +} + +fn count_diff_lines( + prep: gix::diff::blob::platform::prepare_diff::Outcome<'_>, +) -> Option> { + use gix::diff::blob::platform::prepare_diff::Operation; + match prep.operation { + Operation::InternalDiff { algorithm } => { + let tokens = prep.interned_input(); + let counter = gix::diff::blob::diff( + algorithm, + &tokens, + gix::diff::blob::sink::Counter::default(), + ); + Some(counter) + } + Operation::ExternalCommand { .. } => { + unreachable!("we disabled that") + } + Operation::SourceOrDestinationIsBinary => None, + } +} + +/// Represents the parsed output from a git diff. +#[derive(Default)] +struct GitDiff { + added: String, + deleted: String, +} + +impl GitDiff { /// Returns the first capture group given a regular expression and a string. /// If it fails to get the capture group it will return "0". - fn get_matched_str(diff: &'a str, re: &Regex) -> &'a str { + fn get_matched_str<'a>(diff: &'a str, re: &Regex) -> &'a str { match re.captures(diff) { Some(caps) => caps.get(1).unwrap().as_str(), _ => "0", @@ -72,13 +368,13 @@ impl<'a> GitDiff<'a> { } /// Parses the result of 'git diff --shortstat' as a `GitDiff` struct. - pub fn parse(diff: &'a str) -> Self { + pub fn parse(diff: &str) -> Self { let added_re = Regex::new(r"(\d+) \w+\(\+\)").unwrap(); let deleted_re = Regex::new(r"(\d+) \w+\(\-\)").unwrap(); Self { - added: GitDiff::get_matched_str(diff, &added_re), - deleted: GitDiff::get_matched_str(diff, &deleted_re), + added: GitDiff::get_matched_str(diff, &added_re).to_owned(), + deleted: GitDiff::get_matched_str(diff, &deleted_re).to_owned(), } } @@ -105,10 +401,10 @@ mod tests { use std::path::Path; use std::process::Stdio; + use crate::modules::git_status::tests::make_sparse; + use crate::test::{FixtureProvider, ModuleRenderer, fixture_repo}; use nu_ansi_term::Color; - use crate::test::ModuleRenderer; - #[test] fn shows_nothing_on_empty_dir() -> io::Result<()> { let repo_dir = tempfile::tempdir()?; @@ -140,6 +436,78 @@ mod tests { repo_dir.close() } + #[test] + fn shows_staged_addition() -> io::Result<()> { + let repo_dir = create_repo_with_commit()?; + let path = repo_dir.path(); + + std::fs::write(path.join("new-file"), "new line")?; + run_git_cmd(["add", "new-file"], Some(path), true)?; + + let actual = render_metrics(path); + + let expected = Some(format!("{} ", Color::Green.bold().paint("+1"),)); + + assert_eq!(expected, actual); + repo_dir.close() + } + + #[test] + fn shows_staged_rename_modification() -> io::Result<()> { + let repo_dir = create_repo_with_commit()?; + let path = repo_dir.path(); + + let the_file = path.join("the_file"); + let mut the_file = OpenOptions::new().append(true).open(the_file)?; + writeln!(the_file, "Added line")?; + the_file.sync_all()?; + run_git_cmd(["add", "the_file"], Some(path), true)?; + run_git_cmd(["mv", "the_file", "that_file"], Some(path), true)?; + + let actual = render_metrics(path); + + let expected = Some(format!("{} ", Color::Green.bold().paint("+1"),)); + + assert_eq!(expected, actual); + repo_dir.close() + } + + #[test] + fn shows_staged_addition_intended() -> io::Result<()> { + let repo_dir = create_repo_with_commit()?; + let path = repo_dir.path(); + + std::fs::write(path.join("new-file"), "new line")?; + run_git_cmd(["add", "-N", "new-file"], Some(path), true)?; + + let actual = render_metrics(path); + + let expected = Some(format!("{} ", Color::Green.bold().paint("+1"),)); + + assert_eq!(expected, actual); + repo_dir.close() + } + + #[test] + fn shows_staged_modification() -> io::Result<()> { + let repo_dir = create_repo_with_commit()?; + let path = repo_dir.path(); + + std::fs::write(path.join("the_file"), "modify all")?; + run_git_cmd(["add", "the_file"], Some(path), true)?; + + let actual = render_metrics(path); + + let expected = Some(format!( + "{} {} ", + Color::Green.bold().paint("+1"), + Color::Red.bold().paint("-3") + )); + + assert_eq!(expected, actual); + repo_dir.close() + } + #[test] fn shows_deleted_lines() -> io::Result<()> { let repo_dir = create_repo_with_commit()?; @@ -156,6 +524,36 @@ mod tests { repo_dir.close() } + #[test] + fn shows_deleted_lines_of_entire_file() -> io::Result<()> { + let repo_dir = create_repo_with_commit()?; + let path = repo_dir.path(); + + std::fs::remove_file(path.join("the_file"))?; + + let actual = render_metrics(path); + + let expected = Some(format!("{} ", Color::Red.bold().paint("-3"))); + + assert_eq!(expected, actual); + repo_dir.close() + } + + #[test] + fn shows_staged_deletion() -> io::Result<()> { + let repo_dir = create_repo_with_commit()?; + let path = repo_dir.path(); + + run_git_cmd(["rm", "the_file"], Some(path), true)?; + + let actual = render_metrics(path); + + let expected = Some(format!("{} ", Color::Red.bold().paint("-3"))); + + assert_eq!(expected, actual); + repo_dir.close() + } + #[test] fn shows_all_changes() -> io::Result<()> { let repo_dir = create_repo_with_commit()?; @@ -188,6 +586,32 @@ mod tests { repo_dir.close() } + #[test] + fn shows_nothing_on_untracked() -> io::Result<()> { + let repo_dir = create_repo_with_commit()?; + let path = repo_dir.path(); + std::fs::write(path.join("untracked"), "a line")?; + + let actual = render_metrics(path); + + let expected = None; + assert_eq!(expected, actual); + repo_dir.close() + } + + #[test] + fn shows_nothing_if_no_changes_sparse() -> io::Result<()> { + let repo_dir = create_repo_with_commit()?; + let path = repo_dir.path(); + + make_sparse(path)?; + let actual = render_metrics(path); + + let expected = None; + assert_eq!(expected, actual); + repo_dir.close() + } + #[test] fn shows_all_if_only_nonzero_diffs_is_false() -> io::Result<()> { let repo_dir = create_repo_with_commit()?; @@ -202,21 +626,27 @@ mod tests { .config(toml::toml! { [git_metrics] disabled = false - only_nonzero_diffs = false + only_nonzero_diffs = true }) .path(path) .collect(); - let expected = Some(format!( - "{} {} ", - Color::Green.bold().paint("+1"), - Color::Red.bold().paint("-0") - )); + let expected = Some(format!("{} ", Color::Green.bold().paint("+1"),)); assert_eq!(expected, actual); repo_dir.close() } + #[test] + fn doesnt_generate_git_metrics_for_bare_repo() -> io::Result<()> { + let repo_dir = fixture_repo(FixtureProvider::GitBare)?; + + let actual = render_metrics(repo_dir.path()); + assert_eq!(None, actual); + + repo_dir.close() + } + #[test] fn shows_all_changes_with_ignored_submodules() -> io::Result<()> { let repo_dir = create_repo_with_commit()?; diff --git a/src/modules/git_status.rs b/src/modules/git_status.rs index 38a781299..41a82ce82 100644 --- a/src/modules/git_status.rs +++ b/src/modules/git_status.rs @@ -1,13 +1,14 @@ -use regex::Regex; -use std::sync::OnceLock; - use super::{Context, Module, ModuleConfig}; - use crate::configs::git_status::GitStatusConfig; -use crate::context; use crate::formatter::StringFormatter; use crate::segment::Segment; -use std::sync::Arc; +use crate::{context, num_configured_starship_threads, num_rayon_threads}; +use gix::bstr::ByteVec; +use gix::status::Submodule; +use regex::Regex; +use std::path::PathBuf; +use std::sync::atomic::AtomicBool; +use std::sync::{Arc, OnceLock}; const ALL_STATUS_FORMAT: &str = "$conflicted$stashed$deleted$renamed$modified$typechanged$staged$untracked"; @@ -47,7 +48,7 @@ pub fn module<'a>(context: &'a Context) -> Option> { return Some(module); } - let info = Arc::new(GitStatusInfo::load(context, repo, config.clone())); + let info = GitStatusInfo::load(context, repo, config.clone()); let parsed = StringFormatter::new(config.format).and_then(|formatter| { formatter @@ -60,7 +61,6 @@ pub fn module<'a>(context: &'a Context) -> Option> { _ => None, }) .map_variables_to_segments(|variable: &str| { - let info = Arc::clone(&info); let segments = match variable { "stashed" => info.get_stashed().and_then(|count| { format_count(config.stashed, "git_status.stashed", context, count) @@ -135,7 +135,7 @@ struct GitStatusInfo<'a> { context: &'a Context<'a>, repo: &'a context::Repo, config: GitStatusConfig<'a>, - repo_status: OnceLock>, + repo_status: OnceLock>>, stashed_count: OnceLock>, } @@ -158,16 +158,19 @@ impl<'a> GitStatusInfo<'a> { self.get_repo_status().map(|data| (data.ahead, data.behind)) } - pub fn get_repo_status(&self) -> &Option { - self.repo_status.get_or_init(|| { - match get_repo_status(self.context, self.repo, &self.config) { - Some(repo_status) => Some(repo_status), - None => { - log::debug!("get_repo_status: git status execution failed"); - None - } - } - }) + pub fn get_repo_status(&self) -> Option<&RepoStatus> { + self.repo_status + .get_or_init( + || match get_static_repo_status(self.context, self.repo, &self.config) { + Some(repo_status) => Some(repo_status), + None => { + log::debug!("get_repo_status: git status execution failed"); + None + } + }, + ) + .as_ref() + .map(|repo_status| repo_status.as_ref()) } pub fn get_stashed(&self) -> &Option { @@ -210,6 +213,29 @@ impl<'a> GitStatusInfo<'a> { } } +/// Return a globally shared version the repository status so it can be reused. +/// It's shared so those who received a copy can keep it, even if the next call uses a different +/// path so the cache is trashed. +/// +/// The trashing is only expected when tests run though, as otherwise one path is used with a variety of modules. +pub(crate) fn get_static_repo_status( + context: &Context, + repo: &context::Repo, + config: &GitStatusConfig, +) -> Option> { + static REPO_STATUS: parking_lot::Mutex, PathBuf)>> = + parking_lot::Mutex::new(None); + let mut status = REPO_STATUS.lock(); + let needs_update = status + .as_ref() + .is_none_or(|(_status, status_path)| status_path != &context.current_dir); + if needs_update { + *status = get_repo_status(context, repo, config) + .map(|status| (Arc::new(status), context.current_dir.clone())); + } + status.as_ref().map(|(status, _)| Arc::clone(status)) +} + /// Gets the number of files in various git states (staged, modified, deleted, etc...) fn get_repo_status( context: &Context, @@ -219,40 +245,226 @@ fn get_repo_status( log::debug!("New repo status created"); let mut repo_status = RepoStatus::default(); - let mut args = vec!["status", "--porcelain=2"]; - - // for performance reasons, only pass flags if necessary... - let has_ahead_behind = !config.ahead.is_empty() || !config.behind.is_empty(); - let has_up_to_date_diverged = !config.up_to_date.is_empty() || !config.diverged.is_empty(); - if has_ahead_behind || has_up_to_date_diverged { - args.push("--branch"); - } - - // ... and add flags that omit information the user doesn't want + let gix_repo = repo.open(); + // TODO: remove this special case once `gitoxide` can handle sparse indices for tree-index comparisons. let has_untracked = !config.untracked.is_empty(); - if !has_untracked { - args.push("--untracked-files=no"); - } - if config.ignore_submodules { - args.push("--ignore-submodules=dirty"); - } else if !has_untracked { - args.push("--ignore-submodules=untracked"); - } + let git_config = gix_repo.config_snapshot(); + if config.use_git_executable + || gix_repo.index_or_empty().ok()?.is_sparse() + || repo.fs_monitor_value_is_true + { + let mut args = vec!["status", "--porcelain=2"]; - let status_output = repo.exec_git(context, &args)?; - let statuses = status_output.stdout.lines(); - - statuses.for_each(|status| { - if status.starts_with("# branch.ab ") { - repo_status.set_ahead_behind(status); - } else if !status.starts_with('#') { - repo_status.add(status); + // for performance reasons, only pass flags if necessary... + let has_ahead_behind = !config.ahead.is_empty() || !config.behind.is_empty(); + let has_up_to_date_diverged = !config.up_to_date.is_empty() || !config.diverged.is_empty(); + if has_ahead_behind || has_up_to_date_diverged { + args.push("--branch"); } - }); + + // ... and add flags that omit information the user doesn't want + if !has_untracked { + args.push("--untracked-files=no"); + } + if config.ignore_submodules { + args.push("--ignore-submodules=dirty"); + } else if !has_untracked { + args.push("--ignore-submodules=untracked"); + } + + let status_output = repo.exec_git(context, &args)?; + let statuses = status_output.stdout.lines(); + + statuses.for_each(|status| { + if status.starts_with("# branch.ab ") { + repo_status.set_ahead_behind(status); + } else if !status.starts_with('#') { + repo_status.add(status); + } + }); + } else { + let is_interrupted = Arc::new(AtomicBool::new(false)); + std::thread::Builder::new() + .name("starship timer".into()) + .stack_size(256 * 1024) + .spawn({ + let is_interrupted = is_interrupted.clone(); + let abort_after = + std::time::Duration::from_millis(context.root_config.command_timeout); + move || { + std::thread::sleep(abort_after); + is_interrupted.store(true, std::sync::atomic::Ordering::SeqCst); + } + }) + .expect("should be able to spawn timer thread"); + // We don't show details in submodules. + let check_dirty = true; + let status = gix_repo + .status(gix::features::progress::Discard) + .ok()? + .index_worktree_submodules(if config.ignore_submodules { + Submodule::Given { + ignore: gix::submodule::config::Ignore::Dirty, + check_dirty, + } + } else if !has_untracked { + Submodule::Given { + ignore: gix::submodule::config::Ignore::Untracked, + check_dirty, + } + } else { + Submodule::AsConfigured { check_dirty } + }) + .index_worktree_options_mut(|opts| { + opts.thread_limit = if cfg!(target_os = "macos") { + Some(num_configured_starship_threads().unwrap_or( + // TODO: figure out good defaults for other platforms, maybe make it configurable. + // Git uses everything (if repo-size permits), but that's not the best choice for MacOS. + 3, + )) + } else { + Some(num_rayon_threads()) + }; + if config.untracked.is_empty() { + opts.dirwalk_options.take(); + } else if let Some(opts) = opts.dirwalk_options.as_mut() { + opts.set_emit_untracked(gix::dir::walk::EmissionMode::Matching) + .set_emit_ignored(None) + .set_emit_pruned(false) + .set_emit_empty_directories(false); + } + }) + .tree_index_track_renames(if config.renamed.is_empty() { + gix::status::tree_index::TrackRenames::Disabled + } else { + gix::status::tree_index::TrackRenames::Given(sanitize_rename_tracking( + // Get configured diff-rename configuration, or use default settings. + gix::diff::new_rewrites(&git_config, true) + .unwrap_or_default() + .0 + .unwrap_or_default(), + )) + }) + .should_interrupt_owned(is_interrupted.clone()); + + // This will start the status machinery, collecting status items in the background. + // Thus, we can do some work in this thread without blocking, before starting to count status items. + let status = status.into_iter(None).ok()?; + + // for performance reasons, only pass flags if necessary... + let has_ahead_behind = !config.ahead.is_empty() || !config.behind.is_empty(); + let has_up_to_date_or_diverged = + !config.up_to_date.is_empty() || !config.diverged.is_empty(); + if has_ahead_behind || has_up_to_date_or_diverged { + if let Some(branch_name) = gix_repo.head_name().ok().flatten().and_then(|ref_name| { + Vec::from(gix::bstr::BString::from(ref_name)) + .into_string() + .ok() + }) { + let output = repo.exec_git( + context, + ["for-each-ref", "--format", "%(upstream:track)"] + .into_iter() + .map(ToOwned::to_owned) + .chain(Some(branch_name)), + )?; + if let Some(line) = output.stdout.lines().next() { + repo_status.set_ahead_behind_for_each_ref(line); + } + } + } + + for change in status.filter_map(Result::ok) { + use gix::status; + match &change { + status::Item::TreeIndex(change) => { + use gix::diff::index::Change; + match change { + Change::Addition { .. } => { + repo_status.staged += 1; + } + Change::Deletion { .. } => { + repo_status.deleted += 1; + } + Change::Modification { .. } => { + repo_status.staged += 1; + } + Change::Rewrite { .. } => { + repo_status.renamed += 1; + } + } + } + status::Item::IndexWorktree(change) => { + use gix::status::index_worktree::Item; + use gix::status::plumbing::index_as_worktree::{Change, EntryStatus}; + match change { + Item::Modification { + status: EntryStatus::Conflict(_), + .. + } => { + repo_status.conflicted += 1; + } + Item::Modification { + status: EntryStatus::Change(Change::Removed), + .. + } => { + repo_status.deleted += 1; + } + Item::Modification { + status: + EntryStatus::IntentToAdd + | EntryStatus::Change( + Change::Modification { .. } | Change::SubmoduleModification(_), + ), + .. + } => { + repo_status.modified += 1; + } + Item::Modification { + status: EntryStatus::Change(Change::Type { .. }), + .. + } => { + repo_status.typechanged += 1; + } + Item::DirectoryContents { + entry: + gix::dir::Entry { + status: gix::dir::entry::Status::Untracked, + .. + }, + .. + } => { + repo_status.untracked += 1; + } + Item::Rewrite { .. } => { + unreachable!( + "this kind of rename tracking isn't enabled by default and specific to gitoxide" + ) + } + _ => {} + } + } + } + // Keep it for potential reuse by `git_metrics` + repo_status.changes.push(change); + } + if is_interrupted.load(std::sync::atomic::Ordering::Relaxed) { + repo_status = RepoStatus { + ahead: repo_status.ahead, + behind: repo_status.behind, + ..Default::default() + }; + } + } Some(repo_status) } +fn sanitize_rename_tracking(mut config: gix::diff::Rewrites) -> gix::diff::Rewrites { + config.limit = 100; + config +} + fn get_stashed_count(repo: &context::Repo) -> Option { let repo = repo.open(); let reference = match repo.try_find_reference("refs/stash") { @@ -279,10 +491,11 @@ fn get_stashed_count(repo: &context::Repo) -> Option { } } -#[derive(Default, Debug, Copy, Clone)] -struct RepoStatus { +#[derive(Default, Debug, Clone)] +pub(crate) struct RepoStatus { ahead: Option, behind: Option, + pub(crate) changes: Vec, conflicted: usize, deleted: usize, renamed: usize, @@ -355,6 +568,27 @@ impl RepoStatus { self.behind = caps.get(2).unwrap().as_str().parse::().ok(); } } + + fn set_ahead_behind_for_each_ref(&mut self, mut s: &str) { + s = s.trim_matches(|c| c == '[' || c == ']'); + + for pair in s.split(',') { + let mut tokens = pair.trim().splitn(2, ' '); + if let (Some(name), Some(number)) = (tokens.next(), tokens.next()) { + let storage = match name { + "ahead" => &mut self.ahead, + "behind" => &mut self.behind, + _ => return, + }; + *storage = number.parse().ok(); + } + } + for field in [&mut self.ahead, &mut self.behind] { + if field.is_none() { + *field = Some(0); + } + } + } } fn format_text( @@ -514,16 +748,15 @@ fn git_status_wsl(_context: &Context, _conf: &GitStatusConfig) -> Option } #[cfg(test)] -mod tests { +pub(crate) mod tests { + use crate::test::{FixtureProvider, ModuleRenderer, fixture_repo}; + use crate::utils::create_command; use nu_ansi_term::{AnsiStrings, Color}; use std::ffi::OsStr; use std::fs::{self, File}; use std::io::{self, prelude::*}; use std::path::Path; - use crate::test::{FixtureProvider, ModuleRenderer, fixture_repo}; - use crate::utils::create_command; - #[allow(clippy::unnecessary_wraps)] fn format_output(symbols: &str) -> Option { Some(format!( @@ -880,6 +1113,21 @@ mod tests { repo_dir.close() } + #[test] + fn shows_typechanged_in_index() -> io::Result<()> { + let repo_dir = fixture_repo(FixtureProvider::Git)?; + + create_typechanged_in_index(repo_dir.path())?; + + let actual = ModuleRenderer::new("git_status") + .path(repo_dir.path()) + .collect(); + let expected = format_output("+"); + + assert_eq!(expected, actual); + repo_dir.close() + } + #[test] fn shows_modified() -> io::Result<()> { let repo_dir = fixture_repo(FixtureProvider::Git)?; @@ -914,6 +1162,27 @@ mod tests { repo_dir.close() } + #[test] + fn shows_modified_with_count_sparse() -> io::Result<()> { + let repo_dir = fixture_repo(FixtureProvider::Git)?; + + make_sparse(repo_dir.path())?; + create_modified(repo_dir.path())?; + + let actual = ModuleRenderer::new("git_status") + .config(toml::toml! { + [git_status] + modified = "!$count" + ahead = "" + }) + .path(repo_dir.path()) + .collect(); + let expected = format_output("!1"); + + assert_eq!(expected, actual); + repo_dir.close() + } + #[test] fn shows_added() -> io::Result<()> { let repo_dir = fixture_repo(FixtureProvider::Git)?; @@ -1075,6 +1344,21 @@ mod tests { repo_dir.close() } + #[test] + fn shows_deleted_file_in_index() -> io::Result<()> { + let repo_dir = fixture_repo(FixtureProvider::Git)?; + + create_deleted_in_index(repo_dir.path())?; + + let actual = ModuleRenderer::new("git_status") + .path(repo_dir.path()) + .collect(); + let expected = format_output("✘"); + + assert_eq!(expected, actual); + repo_dir.close() + } + #[test] fn shows_deleted_file_with_count() -> io::Result<()> { let repo_dir = fixture_repo(FixtureProvider::Git)?; @@ -1139,7 +1423,6 @@ mod tests { // but as untracked instead. The following test checks if manually deleted and manually renamed // files are tracked by git_status module in the same way 'git status' does. #[test] - #[ignore] fn ignore_manually_renamed() -> io::Result<()> { let repo_dir = fixture_repo(FixtureProvider::Git)?; File::create(repo_dir.path().join("a"))?.sync_all()?; @@ -1239,6 +1522,16 @@ mod tests { Ok(()) } + fn create_typechanged_in_index(repo_dir: &Path) -> io::Result<()> { + create_typechanged(repo_dir)?; + + create_command("git")? + .args(["add", "readme.md"]) + .current_dir(repo_dir) + .output()?; + Ok(()) + } + fn create_staged_typechange(repo_dir: &Path) -> io::Result<()> { create_typechanged(repo_dir)?; @@ -1320,6 +1613,33 @@ mod tests { Ok(()) } + pub(crate) fn make_sparse(repo_dir: &Path) -> io::Result<()> { + let sparse_dirname = "sparse-dir"; + let dir = repo_dir.join(sparse_dirname); + std::fs::create_dir(&dir)?; + File::create(dir.join("still-visible"))?.sync_all()?; + let subdir = dir.join("not-checked-out"); + std::fs::create_dir(&subdir)?; + File::create(subdir.join("invisible"))?.sync_all()?; + + create_command("git")? + .args(["add", "sparse-dir"]) + .current_dir(repo_dir) + .output()?; + + create_command("git")? + .args(["commit", "-m", "add new directory", "--no-gpg-sign"]) + .current_dir(repo_dir) + .output()?; + + create_command("git")? + .args(["sparse-checkout", "set", sparse_dirname, "--sparse-index"]) + .current_dir(repo_dir) + .output()?; + + Ok(()) + } + fn create_staged(repo_dir: &Path) -> io::Result<()> { File::create(repo_dir.join("license"))?.sync_all()?; @@ -1384,6 +1704,15 @@ mod tests { Ok(()) } + fn create_deleted_in_index(repo_dir: &Path) -> io::Result<()> { + create_command("git")? + .args(["rm", "readme.md"]) + .current_dir(repo_dir) + .output()?; + + Ok(()) + } + fn create_staged_and_ignored(repo_dir: &Path) -> io::Result<()> { let mut file = File::create(repo_dir.join(".gitignore"))?; writeln!(&mut file, "ignored.txt")?; diff --git a/src/modules/mod.rs b/src/modules/mod.rs index 6afe8a823..9eb16a4a0 100644 --- a/src/modules/mod.rs +++ b/src/modules/mod.rs @@ -32,7 +32,7 @@ mod git_branch; mod git_commit; mod git_metrics; mod git_state; -mod git_status; +pub(crate) mod git_status; mod gleam; mod golang; mod gradle; diff --git a/src/test/mod.rs b/src/test/mod.rs index 232313692..854143ae3 100644 --- a/src/test/mod.rs +++ b/src/test/mod.rs @@ -246,12 +246,13 @@ pub fn fixture_repo(provider: FixtureProvider) -> io::Result { } FixtureProvider::GitBare => { let path = tempfile::tempdir()?; - gix::ThreadSafeRepository::init( - &path, - gix::create::Kind::Bare, - gix::create::Options::default(), - ) - .map_err(|err| io::Error::new(io::ErrorKind::Other, err))?; + + create_command("git")? + .current_dir(path.path()) + .args(["clone", "-b", "master", "--bare"]) + .arg(GIT_FIXTURE.as_os_str()) + .arg(path.path()) + .output()?; Ok(path) } FixtureProvider::Hg => { diff --git a/typos.toml b/typos.toml index aec4f96eb..730fe44a4 100644 --- a/typos.toml +++ b/typos.toml @@ -9,5 +9,6 @@ afe = "afe" typ = "typ" extentions = "extentions" # TODO: should be extensions worl = "worl" # typo on purpose +rela = "rela" [files] extend-exclude = ["CHANGELOG.md", "docs/*"]