diff --git a/Cargo.lock b/Cargo.lock index b49524bad..92b647628 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -41,9 +41,9 @@ dependencies = [ [[package]] name = "allocator-api2" -version = "0.2.18" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "android-tzdata" @@ -77,9 +77,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.17" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23a1e53f0f5d86382dafe1cf314783b2044280f406e7e1506368220ad11b1338" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" dependencies = [ "anstyle", "anstyle-parse", @@ -92,9 +92,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8365de52b16c035ff4fcafe0092ba9390540e3e352870ac09933bebcaa2c8c56" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] name = "anstyle-parse" @@ -116,19 +116,20 @@ dependencies = [ [[package]] name = "anstyle-wincon" -version = "3.0.6" +version = "3.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" dependencies = [ "anstyle", + "once_cell", "windows-sys 0.59.0", ] [[package]] name = "anyhow" -version = "1.0.95" +version = "1.0.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" +checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" [[package]] name = "arrayvec" @@ -163,13 +164,13 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.86" +version = "0.1.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "644dd749086bf3771a2fbc5f256fdb982d53f011c7d5d560304eafeecebce79d" +checksum = "d556ec1359574147ec0c4fc5eb525f3f23263a592b1a9c07e0a75b427de55c97" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.99", ] [[package]] @@ -248,9 +249,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.6.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" [[package]] name = "block-buffer" @@ -268,7 +269,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0" dependencies = [ "memchr", - "regex-automata 0.4.8", + "regex-automata 0.4.9", "serde", ] @@ -285,9 +286,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.16.0" +version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" [[package]] name = "byteorder" @@ -297,9 +298,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.8.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" dependencies = [ "serde", ] @@ -327,9 +328,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.13" +version = "1.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7777341816418c02e033934a09f20dc0ccaf65a5201ef8a450ae0105a573fda" +checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" dependencies = [ "jobserver", "libc", @@ -354,16 +355,16 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.39" +version = "0.4.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" +checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" dependencies = [ "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "wasm-bindgen", - "windows-targets 0.52.6", + "windows-link", ] [[package]] @@ -430,9 +431,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.28" +version = "4.5.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e77c3243bd94243c03672cb5154667347c457ca271254724f9f393aee1c05ff" +checksum = "027bb0d98429ae334a8698531da7077bdf906419543a35a55c2cb1b66437d767" dependencies = [ "clap_builder", "clap_derive", @@ -440,9 +441,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.27" +version = "4.5.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7" +checksum = "5589e0cba072e0f3d23791efac0fd8627b49c829c196a492e88168e6a669d863" dependencies = [ "anstream", "anstyle", @@ -459,7 +460,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.99", ] [[package]] @@ -483,7 +484,7 @@ version = "0.2.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "724842fa9b144f9b89b3f3d371a89f3455eea660361d13a554f68f8ae5d6c13a" dependencies = [ - "clap 4.5.28", + "clap 4.5.31", "roff 0.2.2", ] @@ -569,9 +570,9 @@ dependencies = [ [[package]] name = "console" -version = "0.15.10" +version = "0.15.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea3c6ecd8059b57859df5c69830340ed3c41d30e3da0c1cbed90a96ac853041b" +checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" dependencies = [ "encode_unicode", "libc", @@ -588,9 +589,9 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" -version = "0.2.14" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ "libc", ] @@ -604,7 +605,7 @@ dependencies = [ "anes", "cast", "ciborium", - "clap 4.5.28", + "clap 4.5.31", "criterion-plot", "is-terminal", "itertools 0.10.5", @@ -646,18 +647,18 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.13" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" +checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-deque" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -674,18 +675,18 @@ dependencies = [ [[package]] name = "crossbeam-queue" -version = "0.3.11" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.20" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crossterm" @@ -709,7 +710,7 @@ version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.0", "crossterm_winapi", "libc", "mio 0.8.11", @@ -725,7 +726,7 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.0", "crossterm_winapi", "mio 1.0.3", "parking_lot 0.12.3", @@ -746,9 +747,9 @@ dependencies = [ [[package]] name = "crunchy" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" [[package]] name = "crypto-common" @@ -788,7 +789,7 @@ dependencies = [ "cursive_core", "enumset", "log", - "smallvec 1.13.2", + "smallvec 1.14.0", "unicode-segmentation", "unicode-width 0.2.0", ] @@ -858,7 +859,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.96", + "syn 2.0.99", ] [[package]] @@ -880,7 +881,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core 0.20.10", "quote", - "syn 2.0.96", + "syn 2.0.99", ] [[package]] @@ -947,9 +948,9 @@ checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" [[package]] name = "diffy" -version = "0.4.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d3041965b7a63e70447ec818a46b1e5297f7fcae3058356d226c02750c4e6cb" +checksum = "b545b8c50194bdd008283985ab0b31dba153cfd5b3066a92770634fbc0d7d291" dependencies = [ "nu-ansi-term", ] @@ -985,6 +986,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.99", +] + [[package]] name = "doc-comment" version = "0.3.3" @@ -999,9 +1011,9 @@ checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" [[package]] name = "either" -version = "1.13.0" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +checksum = "b7914353092ddf589ad78f25c5c1c21b7f80b0ff8621e7c814c3485b5306da9d" [[package]] name = "ena" @@ -1035,7 +1047,7 @@ checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.99", ] [[package]] @@ -1056,7 +1068,7 @@ dependencies = [ "darling 0.20.10", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.99", ] [[package]] @@ -1074,18 +1086,18 @@ dependencies = [ [[package]] name = "equivalent" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1243,15 +1255,15 @@ checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" [[package]] name = "fastrand" -version = "2.1.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "filedescriptor" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7199d965852c3bac31f779ef99cbb4537f80e952e2d6aa0ffeb30cce00f4f46e" +checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" dependencies = [ "libc", "thiserror 1.0.69", @@ -1272,9 +1284,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foldhash" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2" +checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" [[package]] name = "form_urlencoded" @@ -1367,7 +1379,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.99", ] [[package]] @@ -1486,6 +1498,7 @@ dependencies = [ "git-branchless-submit", "git-branchless-test", "git-branchless-undo", + "git2-ext", "insta", "itertools 0.14.0", "lazy_static", @@ -1497,7 +1510,7 @@ dependencies = [ "regex", "rusqlite", "scm-diff-editor", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", "tracing-chrome", "tracing-error", @@ -1539,7 +1552,7 @@ dependencies = [ name = "git-branchless-invoke" version = "0.10.0" dependencies = [ - "clap 4.5.28", + "clap 4.5.31", "color-eyre", "cursive_core", "eyre", @@ -1572,6 +1585,7 @@ dependencies = [ "eyre", "futures", "git2", + "git2-ext", "indicatif", "insta", "itertools 0.14.0", @@ -1586,7 +1600,7 @@ dependencies = [ "shell-words", "tempfile", "textwrap", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", "tracing-chrome", "tracing-error", @@ -1628,7 +1642,7 @@ dependencies = [ name = "git-branchless-opts" version = "0.10.0" dependencies = [ - "clap 4.5.28", + "clap 4.5.31", "clap_mangen", "git-branchless-lib", "itertools 0.14.0", @@ -1691,7 +1705,7 @@ dependencies = [ "rayon", "regex", "serde_json", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", ] @@ -1732,7 +1746,7 @@ dependencies = [ name = "git-branchless-submit" version = "0.10.0" dependencies = [ - "clap 4.5.28", + "clap 4.5.31", "cursive_core", "esl01-dag", "eyre", @@ -1750,7 +1764,7 @@ dependencies = [ "serde", "serde_json", "tempfile", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", ] @@ -1760,7 +1774,7 @@ version = "0.10.0" dependencies = [ "assert_cmd", "bstr", - "clap 4.5.28", + "clap 4.5.31", "crossbeam", "cursive", "esl01-dag", @@ -1781,7 +1795,7 @@ dependencies = [ "serde", "serde_json", "tempfile", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", ] @@ -1817,22 +1831,37 @@ checksum = "53010ccb100b96a67bc32c0175f0ed1426b31b655d562898e57325f81c023ac0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.99", ] [[package]] name = "git2" -version = "0.20.0" +version = "0.18.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fda788993cc341f69012feba8bf45c0ba4f3291fcc08e214b4d5a7332d88aff" +checksum = "232e6a7bfe35766bf715e55a88b39a700596c0ccfd88cd3680b4cdb40d66ef70" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.0", "libc", "libgit2-sys", "log", "url", ] +[[package]] +name = "git2-ext" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9afba1d6a6625c5bbc1719eba68578e087296c5776956b4fa3af5c29b949c2" +dependencies = [ + "bstr", + "git2", + "itertools 0.13.0", + "log", + "shlex", + "tempfile", + "which", +] + [[package]] name = "glob" version = "0.3.2" @@ -1867,9 +1896,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.0" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" dependencies = [ "allocator-api2", "equivalent", @@ -1918,6 +1947,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "humantime" version = "2.1.0" @@ -1947,6 +1985,124 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec 1.14.0", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.99", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -1955,12 +2111,23 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "0.5.0" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec 1.14.0", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" dependencies = [ - "unicode-bidi", - "unicode-normalization", + "icu_normalizer", + "icu_properties", ] [[package]] @@ -1986,7 +2153,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" dependencies = [ "equivalent", - "hashbrown 0.15.0", + "hashbrown 0.15.2", ] [[package]] @@ -2005,15 +2172,15 @@ dependencies = [ [[package]] name = "indoc" -version = "2.0.5" +version = "2.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" +checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" [[package]] name = "insta" -version = "1.42.1" +version = "1.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71c1b125e30d93896b365e156c33dadfffab45ee8400afcbba4752f59de08a86" +checksum = "50259abbaa67d11d2bcafc7ba1d094ed7a0c70e3ce893f0d0997f73558cb3084" dependencies = [ "console", "linked-hash-map", @@ -2024,15 +2191,15 @@ dependencies = [ [[package]] name = "instability" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "894813a444908c0c8c0e221b041771d107c4a21de1d317dc49bcc66e3c9e5b3f" +checksum = "0bf9fed6d91cfb734e7476a06bde8300a1b94e217e1b523b6f0cd1a01998c71d" dependencies = [ "darling 0.20.10", "indoc", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.99", ] [[package]] @@ -2046,13 +2213,13 @@ dependencies = [ [[package]] name = "is-terminal" -version = "0.4.13" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" +checksum = "e19b23d53f35ce9f56aebc7d1bb4e6ac1e9c0db7ac85c8d1760c04379edced37" dependencies = [ "hermit-abi 0.4.0", "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2090,9 +2257,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.11" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jobserver" @@ -2105,10 +2272,11 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.72" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" dependencies = [ + "once_cell", "wasm-bindgen", ] @@ -2157,15 +2325,15 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.169" +version = "0.2.170" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" +checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828" [[package]] name = "libgit2-sys" -version = "0.18.0+1.9.0" +version = "0.16.2+1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1a117465e7e1597e8febea8bb0c410f1c7fb93b1e1cddf34363f8390367ffec" +checksum = "ee4126d8b4ee5c9d9ea891dd875cfdc1e9d0950437179104b183d7d8a74d24e8" dependencies = [ "cc", "libc", @@ -2179,7 +2347,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.0", "libc", ] @@ -2196,9 +2364,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.20" +version = "1.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2d16453e800a8cf6dd2fc3eb4bc99b786a9b90c663b8559a5b1a041bf89e472" +checksum = "df9b68e50e6e0b26f672573834882eb57759f6db9b3be2ea3c35c91188bb4eaa" dependencies = [ "cc", "libc", @@ -2214,9 +2382,15 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "litemap" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" [[package]] name = "lock_api" @@ -2230,9 +2404,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.22" +version = "0.4.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" [[package]] name = "lru" @@ -2240,7 +2414,7 @@ version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" dependencies = [ - "hashbrown 0.15.0", + "hashbrown 0.15.2", ] [[package]] @@ -2564,7 +2738,7 @@ dependencies = [ "cfg-if", "libc", "redox_syscall", - "smallvec 1.13.2", + "smallvec 1.14.0", "windows-targets 0.52.6", ] @@ -2598,38 +2772,38 @@ dependencies = [ [[package]] name = "phf_shared" -version = "0.10.0" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" dependencies = [ "siphasher", ] [[package]] name = "pin-project" -version = "1.1.8" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e2ec53ad785f4d35dac0adea7f7dc6f1bb277ad84a680c7afefeae05d1f5916" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.8" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56a66c0c55993aa927429d0f8a0abfd74f084e4d9c192cffed01e418d83eefb" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.99", ] [[package]] name = "pin-project-lite" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" @@ -2639,9 +2813,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "plotters" @@ -2673,9 +2847,9 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.9.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2" +checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" [[package]] name = "portable-pty" @@ -2721,9 +2895,9 @@ checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" [[package]] name = "predicates" -version = "3.1.2" +version = "3.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e9086cc7640c29a356d1a29fd134380bee9d8f79a17410aa76e7ad295f42c97" +checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" dependencies = [ "anstyle", "difflib", @@ -2732,15 +2906,15 @@ dependencies = [ [[package]] name = "predicates-core" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae8177bee8e75d6846599c6b9ff679ed51e882816914eec639944d7c9aa11931" +checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" [[package]] name = "predicates-tree" -version = "1.0.11" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41b740d195ed3166cd147c8047ec98db0e22ec019eb8eeb76d343b795304fb13" +checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" dependencies = [ "predicates-core", "termtree", @@ -2748,9 +2922,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.92" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" dependencies = [ "unicode-ident", ] @@ -2763,7 +2937,7 @@ checksum = "14cae93065090804185d3b75f0bf93b8eeda30c7a9b4a33d3bdb3988d6229e50" dependencies = [ "bit-set 0.8.0", "bit-vec 0.8.0", - "bitflags 2.6.0", + "bitflags 2.9.0", "lazy_static", "num-traits", "rand 0.8.5", @@ -2783,9 +2957,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quote" -version = "1.0.37" +version = "1.0.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +checksum = "c1f1914ce909e1658d9907913b4b91947430c7d9be598b15a1912935b8c04801" dependencies = [ "proc-macro2", ] @@ -2904,7 +3078,7 @@ version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d16546c5b5962abf8ce6e2881e722b4e0ae3b6f1a08a26ae3573c55853ca68d3" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.0", "cassowary", "compact_str 0.7.1", "crossterm 0.27.0", @@ -2925,7 +3099,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.0", "cassowary", "compact_str 0.8.1", "crossterm 0.28.1", @@ -2971,11 +3145,11 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.7" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.0", ] [[package]] @@ -2997,7 +3171,7 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.8", + "regex-automata 0.4.9", "regex-syntax 0.8.5", ] @@ -3012,9 +3186,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", @@ -3051,12 +3225,12 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "549b9d036d571d42e6e85d1c1425e2ac83491075078ca9a15be021c56b1641f2" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.0", "fallible-iterator", "fallible-streaming-iterator", "hashlink", "libsqlite3-sys", - "smallvec 1.13.2", + "smallvec 1.14.0", ] [[package]] @@ -3067,22 +3241,22 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustix" -version = "0.38.40" +version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99e4ea3e1cdc4b559b8e5650f9c8e5998e3e5c1343b4eaf034565f32318d63c0" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.0", "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "rustversion" -version = "1.0.18" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" [[package]] name = "rusty-fork" @@ -3098,9 +3272,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.18" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "same-file" @@ -3126,7 +3300,7 @@ dependencies = [ "itertools 0.14.0", "maplit", "proptest", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", ] @@ -3136,7 +3310,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adfb2974d3500342f111c2642357b8eda61977b8d4f2994274aa07a0ad834069" dependencies = [ - "clap 4.5.28", + "clap 4.5.31", "diffy", "scm-record 0.4.0", "sha1", @@ -3174,7 +3348,7 @@ dependencies = [ "ratatui 0.29.0", "serde", "serde_json", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", "unicode-width 0.2.0", ] @@ -3187,29 +3361,29 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.217" +version = "1.0.218" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.217" +version = "1.0.218" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.99", ] [[package]] name = "serde_json" -version = "1.0.138" +version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ "itoa", "memchr", @@ -3340,15 +3514,15 @@ dependencies = [ [[package]] name = "similar" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1de1d4f81173b03af4c0cbed3c898f6bff5b870e4a7f5d6f4057d62a7a4b686e" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" [[package]] name = "siphasher" -version = "0.3.11" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] name = "skim" @@ -3399,9 +3573,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.13.2" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" [[package]] name = "smawk" @@ -3416,7 +3590,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d904e7009df136af5297832a3ace3370cd14ff1546a232f4f185036c2736fcac" dependencies = [ "quote", - "syn 2.0.96", + "syn 2.0.99", ] [[package]] @@ -3433,12 +3607,11 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "string_cache" -version = "0.8.7" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" +checksum = "938d512196766101d333398efde81bc1f37b00cb42c2f8350e5df639f040bbbe" dependencies = [ "new_debug_unreachable", - "once_cell", "parking_lot 0.12.3", "phf_shared", "precomputed-hash", @@ -3475,7 +3648,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.96", + "syn 2.0.99", ] [[package]] @@ -3491,15 +3664,26 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.96" +version = "2.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" +checksum = "e02e925281e18ffd9d640e234264753c43edc62d64b2d4cf898f1bc5e75f3fc2" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.99", +] + [[package]] name = "sysinfo" version = "0.33.1" @@ -3516,9 +3700,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.16.0" +version = "3.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38c246215d7d24f48ae091a2902398798e05d978b24315d6efbc00ede9a8bb91" +checksum = "22e5a0acb1f3f55f65cc4a866c361b2fb2a0ff6366785ae6fbb5f85df07ba230" dependencies = [ "cfg-if", "fastrand", @@ -3559,19 +3743,19 @@ dependencies = [ [[package]] name = "termtree" -version = "0.4.1" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" [[package]] name = "textwrap" -version = "0.16.1" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" dependencies = [ "smawk", "unicode-linebreak", - "unicode-width 0.1.14", + "unicode-width 0.2.0", ] [[package]] @@ -3585,11 +3769,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.11" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" dependencies = [ - "thiserror-impl 2.0.11", + "thiserror-impl 2.0.12", ] [[package]] @@ -3600,18 +3784,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.99", ] [[package]] name = "thiserror-impl" -version = "2.0.11" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.99", ] [[package]] @@ -3626,9 +3810,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.36" +version = "0.3.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +checksum = "bb041120f25f8fbe8fd2dbe4671c7c2ed74d83be2e7a77529bf7e0790ae3f472" dependencies = [ "deranged", "itoa", @@ -3643,15 +3827,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" +checksum = "765c97a5b985b7c11d7bc27fa927dc4fe6af3a6dfb021d28deb60d3bf51e76ef" [[package]] name = "time-macros" -version = "0.2.18" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +checksum = "e8093bc3e81c3bc5f7879de09619d06c9a5a5e45ca44dfeeb7225bae38005c5c" dependencies = [ "num-conv", "time-core", @@ -3676,30 +3860,25 @@ dependencies = [ ] [[package]] -name = "tinytemplate" -version = "1.2.1" +name = "tinystr" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" dependencies = [ - "serde", - "serde_json", + "displaydoc", + "zerovec", ] [[package]] -name = "tinyvec" -version = "1.8.0" +name = "tinytemplate" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" dependencies = [ - "tinyvec_macros", + "serde", + "serde_json", ] -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - [[package]] name = "tracing" version = "0.1.41" @@ -3719,7 +3898,7 @@ checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.99", ] [[package]] @@ -3776,7 +3955,7 @@ dependencies = [ "matchers", "regex", "sharded-slab", - "smallvec 1.13.2", + "smallvec 1.14.0", "thread_local", "tracing", "tracing-core", @@ -3810,9 +3989,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.17.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" [[package]] name = "unarray" @@ -3820,17 +3999,11 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" -[[package]] -name = "unicode-bidi" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" - [[package]] name = "unicode-ident" -version = "1.0.13" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "unicode-linebreak" @@ -3838,15 +4011,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" -[[package]] -name = "unicode-normalization" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" -dependencies = [ - "tinyvec", -] - [[package]] name = "unicode-segmentation" version = "1.12.0" @@ -3884,15 +4048,27 @@ checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "url" -version = "2.5.2" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", "idna", "percent-encoding", ] +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" @@ -3901,9 +4077,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "valuable" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "vcpkg" @@ -3952,9 +4128,9 @@ dependencies = [ [[package]] name = "wait-timeout" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" dependencies = [ "libc", ] @@ -3992,35 +4168,35 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.95" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", "once_cell", + "rustversion", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.95" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.99", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.95" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4028,28 +4204,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.95" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.99", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.95" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] [[package]] name = "web-sys" -version = "0.3.72" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" dependencies = [ "js-sys", "wasm-bindgen", @@ -4065,6 +4244,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "which" +version = "6.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ee928febd44d98f2f459a4a79bd4d928591333a494a10a868418ac1b39cf1f" +dependencies = [ + "either", + "home", + "rustix", + "winsafe", +] + [[package]] name = "winapi" version = "0.3.9" @@ -4135,7 +4326,7 @@ checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.99", ] [[package]] @@ -4146,9 +4337,15 @@ checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.99", ] +[[package]] +name = "windows-link" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3" + [[package]] name = "windows-result" version = "0.1.2" @@ -4381,21 +4578,63 @@ dependencies = [ "winapi", ] +[[package]] +name = "winsafe" +version = "0.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" + [[package]] name = "wit-bindgen-rt" version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.0", ] +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + [[package]] name = "xi-unicode" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a67300977d3dc3f8034dae89778f502b6ba20b269527b3223ba59c0cf393bb8a" +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.99", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.7.35" @@ -4414,5 +4653,48 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.99", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.99", + "synstructure", +] + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.99", ] diff --git a/Cargo.toml b/Cargo.toml index d55794454..4ab27dca6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,7 +69,7 @@ git-branchless-smartlog = { version = "0.10.0", path = "git-branchless-smartlog" git-branchless-submit = { version = "0.10.0", path = "git-branchless-submit" } git-branchless-test = { version = "0.10.0", path = "git-branchless-test" } git-branchless-undo = { version = "0.10.0", path = "git-branchless-undo" } -git2 = { version = "0.20.0", default-features = false } +git2 = { version = "0.18.0", default-features = false } glob = "0.3.2" indexmap = "2.7.1" indicatif = { version = "0.17.11", features = ["improved_unicode"] } diff --git a/GPG-SIGNING-GUIDE.md b/GPG-SIGNING-GUIDE.md new file mode 100644 index 000000000..bc37627c8 --- /dev/null +++ b/GPG-SIGNING-GUIDE.md @@ -0,0 +1,267 @@ +# GPG Signing Functionality Guide + +This guide explains how to test that the GPG signing functionality in this git-branchless fork works correctly. + +## Automated Testing + +### Basic Test Script + +You can run the provided basic test script to automatically test GPG signing functionality: + +```bash +./test-gpg-signing.sh +``` + +This script will: + +1. Create a test repository +2. Configure GPG signing +3. Create signed commits +4. Test `git move` and `git restack` operations +5. Verify signatures are preserved + +### Advanced Subtree Test Script + +For testing more complex scenarios with commit subtrees and divergent development: + +```bash +./test-advanced-gpg-subtrees.sh +``` + +This script tests: + +1. Moving entire subtrees of commits while preserving signatures +2. Restacking multiple divergent branches simultaneously +3. Using advanced revset operations to manipulate specific commit groups +4. Verifying signatures are maintained across all operations + +## Manual Testing + +If you prefer to test the functionality manually, follow these steps: + +### 1. Setup Test Repository + +```bash +# Create a test repository +mkdir test-gpg-branchless +cd test-gpg-branchless +git init + +# Configure git for the test +git config user.name "Your Name" +git config user.email "your.email@example.com" +git config user.signingkey YOUR_GPG_KEY_ID +git config commit.gpgsign true + +# Create initial commit +echo "# Test Repository" > README.md +git add README.md +git commit -S -m "Initial commit" + +# Initialize git-branchless +git-branchless init +``` + +### 2. Test Case 1: Moving Signed Commits + +```bash +# Create a series of signed commits +echo "Feature A" > featureA.txt +git add featureA.txt +git commit -S -m "Add Feature A" + +echo "Feature B" > featureB.txt +git add featureB.txt +git commit -S -m "Add Feature B" + +# Create a branch to move later +git checkout HEAD~1 +echo "Feature C" > featureC.txt +git add featureC.txt +git commit -S -m "Add Feature C" + +# View the commit graph +git branchless smartlog + +# Move the Feature C commit on top of Feature B +git branchless move -d "Add Feature C" -s "@" + +# Check the commit graph +git branchless smartlog + +# Verify signatures of the moved commits +git checkout @ +git verify-commit HEAD # Should show a valid signature for Feature C +git verify-commit HEAD~1 # Should show a valid signature for Feature B +``` + +### 3. Test Case 2: Restacking Signed Commits + +```bash +# Create a stack of signed commits +git checkout master +echo "Base feature" > base.txt +git add base.txt +git commit -S -m "Add base feature" + +echo "Dependent feature 1" > dep1.txt +git add dep1.txt +git commit -S -m "Add dependent feature 1" + +echo "Dependent feature 2" > dep2.txt +git add dep2.txt +git commit -S -m "Add dependent feature 2" + +# Amend a commit in the middle of the stack +git checkout HEAD~1 +echo "Modified content" >> dep1.txt +git add dep1.txt +git commit --amend -S -m "Modified dependent feature 1" + +# View the broken commit graph +git branchless smartlog +# Should show that "Add dependent feature 2" is abandoned + +# Restack the commits +git branchless restack + +# View the fixed commit graph +git branchless smartlog + +# Verify signatures +git checkout @ +git log -n 3 --format="%H %s" +# For each commit hash, verify the signature: +git verify-commit +``` + +### 4. Test Case 3: Interactive Record with Signing + +```bash +# Create some changes +echo "New content" > new_file.txt +echo "More content" >> README.md + +# Record the changes with signing +git branchless record -m "Record with signing" + +# Verify the commit has a signature +git verify-commit HEAD +``` + +### 5. Test Case 4: Advanced Subtree Operations + +```bash +# Create a complex branching structure with multiple feature branches +git checkout master +git checkout -b feature1 +echo "Feature 1" > feature1.txt +git add feature1.txt +git commit -S -m "Add Feature 1" + +# Add a child commit to feature1 +echo "Feature 1 update" >> feature1.txt +git add feature1.txt +git commit -S -m "Update Feature 1" + +# Create another branch from master +git checkout master +git checkout -b feature2 +echo "Feature 2" > feature2.txt +git add feature2.txt +git commit -S -m "Add Feature 2" + +# Create a child branch from feature2 +git checkout -b feature2-child +echo "Feature 2 child" > feature2-child.txt +git add feature2-child.txt +git commit -S -m "Add Feature 2 child" + +# View the structure +git branchless smartlog + +# Store commit hashes for reference +FEATURE1_HEAD=$(git rev-parse feature1) +FEATURE2_HEAD=$(git rev-parse feature2) +FEATURE2_CHILD_HEAD=$(git rev-parse feature2-child) + +# Move the entire feature2 subtree (including its child) onto feature1 +git branchless move -d "ancestors($FEATURE2_HEAD) & descendants($FEATURE2_HEAD, $FEATURE2_CHILD_HEAD)" -s "$FEATURE1_HEAD" + +# View the new structure +git branchless smartlog + +# Verify the signatures on all moved commits +git checkout feature2 +git verify-commit HEAD +git checkout feature2-child +git verify-commit HEAD +``` + +## Expected Results + +For all operations, the expected result is that commits maintain or receive proper GPG signatures. You should see: + +1. When using `git move`, the moved commit should maintain its signature +2. When using `git restack`, restacked commits should maintain their signatures +3. When using `git record`, new commits should be signed if signing is enabled +4. When moving subtrees of commits, all commits in the subtree should maintain their signatures +5. When restacking divergent branches, all commits should maintain their signatures + +If any of these operations result in unsigned commits when they should be signed, then there's an issue with the GPG signing functionality in the fork. + +## Troubleshooting + +If you encounter issues with GPG signing: + +1. Verify your GPG key is properly configured: + + ```bash + git config --get user.signingkey + ``` + +2. Test basic Git signing: + + ```bash + echo "test" > test.txt + git add test.txt + git commit -S -m "Test signing" + git verify-commit HEAD + ``` + +3. Check if the global signing option is enabled: + + ```bash + git config --get commit.gpgsign + ``` + +4. If you're using a different signing program (like SSH keys), ensure it's properly configured: + + ```bash + git config --get gpg.format + ``` + +5. For debugging, you can enable more verbose GPG output: + ```bash + export GPG_TTY=$(tty) + ``` + +## Advanced Revset Usage + +git-branchless provides powerful revset expressions for selecting specific groups of commits: + +```bash +# Move all commits that are descendants of commit A but not descendants of commit B +git branchless move -d "descendants(A) - descendants(B)" -s "target" + +# Move all commits that are between A and B (inclusive) +git branchless move -d "A..B" -s "target" + +# Move all commits that are ancestors of A but also descendants of B +git branchless move -d "ancestors(A) & descendants(B)" -s "target" + +# Move a commit and all its children +git branchless move -d "A | descendants(A)" -s "target" +``` + +You can use these expressions to precisely control which commits are affected by your operations, while the GPG signing support in this fork ensures that signatures are preserved. diff --git a/git-branchless-invoke/Cargo.toml b/git-branchless-invoke/Cargo.toml index 46a9901a7..83eac3a3b 100644 --- a/git-branchless-invoke/Cargo.toml +++ b/git-branchless-invoke/Cargo.toml @@ -14,7 +14,7 @@ color-eyre = { workspace = true } cursive_core = { workspace = true } eyre = { workspace = true } git-branchless-opts = { workspace = true } -git2 = { workspace = true } +git2 = { version = "0.18.0", default-features = false } lib = { workspace = true } tracing = { workspace = true } tracing-chrome = { workspace = true } diff --git a/git-branchless-invoke/src/lib.rs b/git-branchless-invoke/src/lib.rs index a6cd9738f..eee43ffaa 100644 --- a/git-branchless-invoke/src/lib.rs +++ b/git-branchless-invoke/src/lib.rs @@ -117,12 +117,12 @@ fn install_tracing(effects: Effects) -> eyre::Result { #[instrument] fn install_libgit2_tracing() { - fn git_trace(level: git2::TraceLevel, msg: &[u8]) { - info!("[{:?}]: {}", level, String::from_utf8_lossy(msg)); + fn git_trace(level: git2::TraceLevel, msg: &str) { + info!("[{:?}]: {}", level, msg); } - if let Err(err) = git2::trace_set(git2::TraceLevel::Trace, git_trace) { - warn!("Failed to install libgit2 tracing: {err}"); + if !git2::trace_set(git2::TraceLevel::Trace, git_trace) { + warn!("Failed to install libgit2 tracing"); } } diff --git a/git-branchless-lib/Cargo.toml b/git-branchless-lib/Cargo.toml index 15d8ee590..fd3fdebac 100644 --- a/git-branchless-lib/Cargo.toml +++ b/git-branchless-lib/Cargo.toml @@ -55,7 +55,8 @@ cursive = { workspace = true } eden_dag = { workspace = true } eyre = { workspace = true } futures = { workspace = true } -git2 = { workspace = true } +git2 = { version = "0.18.0", default-features = false } +git2-ext = "0.6.2" indicatif = { workspace = true } itertools = { workspace = true } lazy_static = { workspace = true } diff --git a/git-branchless-lib/src/core/node_descriptors.rs b/git-branchless-lib/src/core/node_descriptors.rs index bc3b083ff..410caeda9 100644 --- a/git-branchless-lib/src/core/node_descriptors.rs +++ b/git-branchless-lib/src/core/node_descriptors.rs @@ -516,6 +516,79 @@ impl NodeDescriptor for RelativeTimeDescriptor { } } +/// Display the GPG signature status for a commit. +#[derive(Debug)] +pub struct SignatureStatusDescriptor { + is_enabled: bool, + repo_path: String, +} + +impl SignatureStatusDescriptor { + /// Constructor. + pub fn new(repo: &Repo, is_enabled: bool) -> eyre::Result { + let repo_path = repo.get_path().to_string_lossy().to_string(); + Ok(Self { + is_enabled, + repo_path, + }) + } +} + +impl NodeDescriptor for SignatureStatusDescriptor { + #[instrument] + fn describe_node( + &mut self, + _glyphs: &Glyphs, + object: &NodeObject, + ) -> eyre::Result> { + if !self.is_enabled { + return Ok(None); + } + + let oid = object.get_oid().to_string(); + + // Use git command-line to get signature information as git2 doesn't + // expose GPG signature verification directly + let output = std::process::Command::new("git") + .args(&["-C", &self.repo_path, "verify-commit", &oid, "--raw"]) + .output(); + + match output { + Ok(output) => { + if output.status.success() { + // Good signature + let mut result = StyledString::new(); + result.append_styled("[G]", BaseColor::Green.light()); + Ok(Some(result)) + } else { + // Bad signature + let stderr = String::from_utf8_lossy(&output.stderr); + if stderr.contains("BAD signature") { + let mut result = StyledString::new(); + result.append_styled("[B]", BaseColor::Red.light()); + Ok(Some(result)) + } else if stderr.contains("No signature") { + let mut result = StyledString::new(); + result.append_styled("[N]", BaseColor::Yellow.light()); + Ok(Some(result)) + } else { + // Unknown error + let mut result = StyledString::new(); + result.append_styled("[?]", BaseColor::Magenta.light()); + Ok(Some(result)) + } + } + } + Err(_) => { + // Error running command + let mut result = StyledString::new(); + result.append_styled("[E]", BaseColor::Red.light()); + Ok(Some(result)) + } + } + } +} + #[cfg(test)] mod tests { use std::ops::{Add, Sub}; @@ -577,4 +650,33 @@ Differential Revision: phabricator.com/D123"; Ok(()) } + + #[test] + fn test_signature_status_descriptor() -> eyre::Result<()> { + // Create a mock where is_enabled is false + let mut descriptor = SignatureStatusDescriptor { + is_enabled: false, + repo_path: String::from("/tmp"), + }; + + // Create a mock object - doesn't matter what it contains since is_enabled is false + let object = NodeObject::GarbageCollected { + oid: NonZeroOid::from_str("1234567890123456789012345678901234567890").unwrap(), + }; + + // With is_enabled = false, it should return None + let glyphs = Glyphs::ascii(); + let result = descriptor.describe_node(&glyphs, &object)?; + assert!(result.is_none()); + + // Now test with is_enabled = true + descriptor.is_enabled = true; + + // We can't easily test the actual git command execution in a unit test, + // but we can verify that the method doesn't panic and returns a result + let result = descriptor.describe_node(&glyphs, &object); + assert!(result.is_ok()); + + Ok(()) + } } diff --git a/git-branchless-lib/src/core/rewrite/execute.rs b/git-branchless-lib/src/core/rewrite/execute.rs index 876a6dd1f..d9df466af 100644 --- a/git-branchless-lib/src/core/rewrite/execute.rs +++ b/git-branchless-lib/src/core/rewrite/execute.rs @@ -15,7 +15,7 @@ use crate::core::formatting::Pluralize; use crate::core::repo_ext::RepoExt; use crate::git::{ BranchType, CategorizedReferenceName, GitRunInfo, MaybeZeroOid, NonZeroOid, ReferenceName, - Repo, ResolvedReferenceInfo, + Repo, ResolvedReferenceInfo, SignOption, }; use crate::util::{ExitCode, EyreExitOr}; @@ -436,8 +436,14 @@ mod in_memory { use crate::core::rewrite::move_branches; use crate::core::rewrite::plan::{OidOrLabel, RebaseCommand, RebasePlan}; use crate::git::{ - AmendFastOptions, CherryPickFastOptions, CreateCommitFastError, GitRunInfo, MaybeZeroOid, - NonZeroOid, Repo, + self, // from the incoming changes + AmendFastOptions, // from HEAD side + CreateCommitFastError, // from incoming + CherryPickFastOptions, // from both sides + GitRunInfo, // from both + MaybeZeroOid, // from both + NonZeroOid, // from both + Repo, // from both }; use crate::util::EyreExitOr; @@ -500,6 +506,7 @@ mod in_memory { force_on_disk: _, resolve_merge_conflicts: _, // May be needed once we can resolve merge conflicts in memory. check_out_commit_options: _, // Caller is responsible for checking out to new HEAD. + sign_option, } = options; let mut current_oid = rebase_plan.first_dest_oid; @@ -537,6 +544,8 @@ mod in_memory { .count(); let (effects, progress) = effects.start_operation(OperationType::RebaseCommits); + let signer = git::get_signer(&repo, sign_option)?; + for command in rebase_plan.commands.iter() { match command { RebaseCommand::CreateLabel { label_name } => { @@ -599,8 +608,16 @@ mod in_memory { } else { original_commit.get_committer().update_timestamp(*now)? }; - let mut rebased_commit_oid = None; - let mut rebased_commit = None; + let mut rebased_commit_oid = repo + .create_commit( + &original_commit.get_author(), + &committer_signature, + commit_message, + ¤t_commit.get_tree()?, + vec![¤t_commit], + signer.as_deref(), + ) + .wrap_err("Applying rebased commit")?; for commit_oid in commits_to_apply_oids.iter() { let commit_to_apply = repo @@ -632,7 +649,9 @@ mod in_memory { // Is it even possible to repeatedly amend a tree and then commit // it once at the end? - let maybe_tree = if rebased_commit.is_none() { + let have_rebased_commit = true; + + let maybe_tree = if !have_rebased_commit { repo.cherry_pick_fast( &commit_to_apply, ¤t_commit, @@ -641,8 +660,10 @@ mod in_memory { }, ) } else { + // Find the commit from the oid + let rebased_commit = repo.find_commit_or_fail(rebased_commit_oid)?; repo.amend_fast( - &rebased_commit.expect("rebased commit should not be None"), + &rebased_commit, &AmendFastOptions::FromCommit { commit: commit_to_apply, }, @@ -658,7 +679,10 @@ mod in_memory { }, )) } - Err(other) => eyre::bail!(other), + Err(other) => { + let err: eyre::Report = other.into(); + return Err(err); + } }; // this is the description of each fixup commit @@ -668,23 +692,17 @@ mod in_memory { OperationIcon::InProgress, format!("Committing to repository: {commit_description}"), ); - rebased_commit_oid = Some( - repo.create_commit( - None, - &commit_author, - &committer_signature, - commit_message, - &commit_tree, - vec![¤t_commit], - ) - .wrap_err("Applying rebased commit")?, - ); - - rebased_commit = repo.find_commit(rebased_commit_oid.unwrap())?; + rebased_commit_oid = repo.create_commit( + &commit_author, + &committer_signature, + commit_message, + &commit_tree, + vec![¤t_commit], + signer.as_deref(), + ) + .wrap_err("Applying rebased commit")?; } - let rebased_commit_oid = - rebased_commit_oid.expect("rebased oid should not be None"); let commit_description = effects .get_glyphs() @@ -693,9 +711,9 @@ mod in_memory { rebased_commit_oid, )?)?; - if rebased_commit - .expect("rebased commit should not be None") - .is_empty() + // Get the rebased commit to check if it's empty + let rebased_commit = repo.find_commit_or_fail(rebased_commit_oid)?; + if rebased_commit.is_empty() { rewritten_oids.insert(*original_commit_oid, MaybeZeroOid::Zero); maybe_set_skipped_head_new_oid(*original_commit_oid, current_oid); @@ -802,12 +820,12 @@ mod in_memory { }; let rebased_commit_oid = repo .create_commit( - None, &replacement_commit.get_author(), &committer_signature, replacement_commit_message, &replacement_tree, parents.iter().collect(), + signer.as_deref(), ) .wrap_err("Applying rebased commit")?; @@ -911,6 +929,7 @@ mod in_memory { force_on_disk: _, resolve_merge_conflicts: _, check_out_commit_options, + sign_option: _, } = options; for new_oid in rewritten_oids.values() { @@ -996,6 +1015,7 @@ mod on_disk { force_on_disk: _, resolve_merge_conflicts: _, check_out_commit_options: _, // Checkout happens after rebase has concluded. + sign_option, } = options; let (effects, _progress) = effects.start_operation(OperationType::InitializeRebase); @@ -1113,6 +1133,16 @@ mod on_disk { ) })?; + let gpg_sign_opt_file = rebase_state_dir.join("gpg_sign_opt"); + if let Some(sign_flag) = sign_option.as_rebase_flag(repo)? { + std::fs::write(&gpg_sign_opt_file, sign_flag).wrap_err_with(|| { + format!( + "Writing `gpg_sign_opt` to: {:?}", + gpg_sign_opt_file.as_path() + ) + })?; + } + let end_file_path = rebase_state_dir.join("end"); std::fs::write( end_file_path.as_path(), @@ -1172,6 +1202,7 @@ mod on_disk { force_on_disk: _, resolve_merge_conflicts: _, check_out_commit_options: _, // Checkout happens after rebase has concluded. + sign_option: _, } = options; match write_rebase_state_to_disk(effects, git_run_info, repo, rebase_plan, options)? { @@ -1216,6 +1247,9 @@ pub struct ExecuteRebasePlanOptions { /// If `HEAD` was moved, the options for checking out the new `HEAD` commit. pub check_out_commit_options: CheckOutCommitOptions, + + /// GPG-sign commits. + pub sign_option: SignOption, } /// The result of executing a rebase plan. @@ -1261,6 +1295,7 @@ pub fn execute_rebase_plan( force_on_disk, resolve_merge_conflicts, check_out_commit_options: _, + sign_option: _, } = options; if !force_on_disk { diff --git a/git-branchless-lib/src/git/mod.rs b/git-branchless-lib/src/git/mod.rs index 8b6a09754..8e0dcd02d 100644 --- a/git-branchless-lib/src/git/mod.rs +++ b/git-branchless-lib/src/git/mod.rs @@ -8,6 +8,7 @@ mod oid; mod reference; mod repo; mod run; +mod sign; mod snapshot; mod status; mod test; @@ -27,6 +28,7 @@ pub use repo::{ Result as RepoResult, Time, }; pub use run::{GitRunInfo, GitRunOpts, GitRunResult}; +pub use sign::{get_signer, SignOption}; pub use snapshot::{WorkingCopyChangesType, WorkingCopySnapshot}; pub use status::{FileMode, FileStatus, StatusEntry}; pub use test::{ diff --git a/git-branchless-lib/src/git/repo.rs b/git-branchless-lib/src/git/repo.rs index 387f5a2c1..606f50d82 100644 --- a/git-branchless-lib/src/git/repo.rs +++ b/git-branchless-lib/src/git/repo.rs @@ -23,6 +23,10 @@ use chrono::{DateTime, Utc}; use cursive::theme::BaseColor; use cursive::utils::markup::StyledString; use git2::DiffOptions; +#[allow(unused_attributes)] +#[path = ""] +extern crate git2_ext; +use git2_ext::ops::Sign; use itertools::Itertools; use thiserror::Error; use tracing::{instrument, warn}; @@ -1254,31 +1258,81 @@ impl Repo { } /// Create a new commit. - #[instrument] + #[instrument(skip(signer))] pub fn create_commit( &self, - update_ref: Option<&str>, author: &Signature, committer: &Signature, message: &str, tree: &Tree, parents: Vec<&Commit>, + signer: Option<&dyn Sign>, ) -> Result { let parents = parents .iter() .map(|commit| &commit.inner) .collect::>(); - let oid = self - .inner - .commit( - update_ref, - &author.inner, - &committer.inner, - message, - &tree.inner, - parents.as_slice(), - ) - .map_err(Error::CreateCommit)?; + let oid = git2_ext::ops::commit( + &self.inner, + &author.inner, + &committer.inner, + message, + &tree.inner, + parents.as_slice(), + signer, + ) + .map_err(Error::CreateCommit)?; + Ok(make_non_zero_oid(oid)) + } + + /// Amend a commit with all non-`None` values + #[instrument(skip(signer))] + pub fn amend_commit( + &self, + commit_to_amend: &Commit, + author: Option<&Signature>, + committer: Option<&Signature>, + message: Option<&str>, + tree: Option<&Tree>, + signer: Option<&dyn Sign>, + ) -> Result { + macro_rules! owning_unwrap_or { + ($name:ident, $value_source:expr) => { + let owned_value; + let $name = if let Some(value) = $name { + value + } else { + owned_value = $value_source; + &owned_value + }; + }; + } + owning_unwrap_or!(author, commit_to_amend.get_author()); + owning_unwrap_or!(committer, commit_to_amend.get_committer()); + owning_unwrap_or!( + message, + commit_to_amend + .inner + .message_raw() + .ok_or(Error::DecodeUtf8 { + item: "raw message", + })? + ); + owning_unwrap_or!(tree, commit_to_amend.get_tree()?); + + let parents = commit_to_amend.get_parents(); + let parents = parents.iter().map(|parent| &parent.inner).collect_vec(); + + let oid = git2_ext::ops::commit( + &self.inner, + &author.inner, + &committer.inner, + message, + &tree.inner, + parents.as_slice(), + signer, + ) + .map_err(Error::Amend)?; Ok(make_non_zero_oid(oid)) } @@ -1464,12 +1518,12 @@ impl Repo { vec![] }; let dehydrated_commit_oid = self.create_commit( - None, &signature, &signature, &message, &dehydrated_tree, parents.iter().collect_vec(), + None, )?; let dehydrated_commit = self.find_commit_or_fail(dehydrated_commit_oid)?; Ok(dehydrated_commit) diff --git a/git-branchless-lib/src/git/sign.rs b/git-branchless-lib/src/git/sign.rs new file mode 100644 index 000000000..c56efa830 --- /dev/null +++ b/git-branchless-lib/src/git/sign.rs @@ -0,0 +1,112 @@ +use tracing::instrument; + +#[allow(unused_attributes)] +#[path = ""] +extern crate git2_ext; + +use super::{repo::Result, Repo, RepoError}; + +/// GPG-signing option. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SignOption { + /// Sign commits conditionally based on the `commit.gpgsign` configuration and + /// and the key `user.signingkey`. + UseConfig, + /// Sign commits using the key from `user.signingkey` configuration. + UseConfigKey, + /// Sign commits using the provided signing key. + KeyOverride(String), + /// Do not sign commits. + Disable, +} + +impl SignOption { + /// GPG-signing flag to pass to Git. + pub fn as_git_flag(&self) -> Option { + match self { + Self::UseConfig => None, + Self::UseConfigKey => Some("--gpg-sign".to_string()), + Self::KeyOverride(keyid) => Some(format!("--gpg-sign={}", keyid)), + Self::Disable => Some("--no-gpg-sign".to_string()), + } + } + + /// GPG-signing flag to use for interactive rebase + pub fn as_rebase_flag(&self, repo: &Repo) -> Result> { + Ok(match self { + Self::UseConfig => { + let config = repo.inner.config().map_err(RepoError::ReadConfig)?; + match config.get_bool("commit.gpgsign").ok() { + Some(true) => Some("-S".to_string()), + Some(false) | None => None, + } + } + Self::UseConfigKey => Some("-S".to_string()), + Self::KeyOverride(keyid) => Some(format!("-S{}", keyid)), + Self::Disable => None, + }) + } +} + +/// Get commit signer configured from CLI arguments and repository configurations. +#[instrument] +pub fn get_signer( + repo: &Repo, + option: &SignOption, +) -> Result>> { + match option { + SignOption::UseConfig | SignOption::UseConfigKey => { + let config = repo.inner.config().map_err(RepoError::ReadConfig)?; + if *option == SignOption::UseConfig { + if config.get_bool("commit.gpgsign").ok() == Some(false) { + return Ok(None); + } + } + let signer = git2_ext::ops::UserSign::from_config(&repo.inner, &config) + .map_err(RepoError::ReadConfig)?; + Ok(Some(Box::new(signer) as Box)) + } + SignOption::KeyOverride(keyid) => { + let config = repo.inner.config().map_err(RepoError::ReadConfig)?; + let format = config + .get_string("gpg.format") + .unwrap_or_else(|_| "openpgp".to_owned()); + let signer = match format.as_str() { + "openpgp" => { + let program = config + .get_string("gpg.openpgp.program") + .or_else(|_| config.get_string("gpg.program")) + .unwrap_or_else(|_| "gpg".to_owned()); + + Box::new(git2_ext::ops::GpgSign::new(program, keyid.to_string())) + as Box + } + "x509" => { + let program = config + .get_string("gpg.x509.program") + .unwrap_or_else(|_| "gpgsm".to_owned()); + + Box::new(git2_ext::ops::GpgSign::new(program, keyid.to_string())) + as Box + } + "ssh" => { + let program = config + .get_string("gpg.ssh.program") + .unwrap_or_else(|_| "ssh-keygen".to_owned()); + + Box::new(git2_ext::ops::SshSign::new(program, keyid.to_string())) + as Box + } + format => { + return Err(RepoError::ReadConfig(git2::Error::new( + git2::ErrorCode::Invalid, + git2::ErrorClass::Config, + format!("invalid value for gpg.format: {}", format), + ))) + } + }; + Ok(Some(signer)) + } + SignOption::Disable => Ok(None), + } +} diff --git a/git-branchless-lib/src/git/snapshot.rs b/git-branchless-lib/src/git/snapshot.rs index ca958e5d2..80760bbec 100644 --- a/git-branchless-lib/src/git/snapshot.rs +++ b/git-branchless-lib/src/git/snapshot.rs @@ -213,7 +213,7 @@ branchless: automated working copy snapshot parents }; let commit_oid = - repo.create_commit(None, &signature, &signature, &message, &tree, parents)?; + repo.create_commit(&signature, &signature, &message, &tree, parents, None)?; Ok(WorkingCopySnapshot { base_commit: repo.find_commit_or_fail(commit_oid)?, @@ -364,12 +364,12 @@ branchless: automated working copy snapshot } ); let commit = repo.create_commit( - None, &signature, &signature, &message, &tree_unstaged, Vec::from_iter(head_commit), + None, )?; Ok(commit) } @@ -451,7 +451,6 @@ branchless: automated working copy snapshot } ); let commit_oid = repo.create_commit( - None, &signature, &signature, &message, @@ -460,6 +459,7 @@ branchless: automated working copy snapshot Some(parent_commit) => vec![parent_commit], None => vec![], }, + None, )?; Ok(commit_oid) } diff --git a/git-branchless-lib/tests/test_rewrite_plan.rs b/git-branchless-lib/tests/test_rewrite_plan.rs index 8adc2c335..62a61ee60 100644 --- a/git-branchless-lib/tests/test_rewrite_plan.rs +++ b/git-branchless-lib/tests/test_rewrite_plan.rs @@ -16,6 +16,7 @@ use branchless::core::rewrite::{ RebasePlan, RebasePlanBuilder, RepoResource, }; use branchless::testing::{make_git, Git}; +use branchless::git::SignOption; #[test] fn test_cache_shared_between_builders() -> eyre::Result<()> { @@ -765,6 +766,7 @@ fn create_and_execute_plan( reset: false, render_smartlog: false, }, + sign_option: SignOption::Disable, }; let git_run_info = git.get_git_run_info(); let result = execute_rebase_plan( diff --git a/git-branchless-move/src/lib.rs b/git-branchless-move/src/lib.rs index 86b081dea..e3fbde964 100644 --- a/git-branchless-move/src/lib.rs +++ b/git-branchless-move/src/lib.rs @@ -261,6 +261,7 @@ pub fn r#move( resolve_merge_conflicts, dump_rebase_constraints, dump_rebase_plan, + ref sign_options, } = *move_options; let now = SystemTime::now(); let event_tx_id = event_log_db.make_transaction_id(now, "move")?; @@ -486,6 +487,7 @@ pub fn r#move( force_on_disk, resolve_merge_conflicts, check_out_commit_options: Default::default(), + sign_option: sign_options.to_owned().into(), }; execute_rebase_plan( effects, diff --git a/git-branchless-opts/src/lib.rs b/git-branchless-opts/src/lib.rs index 702ec1cbc..ee75bff30 100644 --- a/git-branchless-opts/src/lib.rs +++ b/git-branchless-opts/src/lib.rs @@ -94,6 +94,10 @@ pub struct MoveOptions { /// executing it. #[clap(action, long = "debug-dump-rebase-plan")] pub dump_rebase_plan: bool, + + /// GPG-sign commits. + #[clap(flatten)] + pub sign_options: SignOptions, } /// Options for traversing commits. @@ -183,6 +187,46 @@ pub struct SwitchOptions { pub target: Option, } +/// Options for signing commits +#[derive(Args, Debug, Clone)] +pub struct SignOptions { + /// GPG-sign commits. The `keyid` argument is optional and defaults to the committer + /// identity. + #[clap( + short = 'S', + long = "gpg-sign", + value_name = "keyid", + conflicts_with = "no_gpg_sign", + num_args(0..=1), + default_missing_value(""), + )] + pub gpg_sign: Option, + + /// Countermand `commit.gpgSign` configuration variable. + #[clap(long = "no-gpg-sign", conflicts_with = "gpg_sign")] + pub no_gpg_sign: bool, +} + +impl From for lib::git::SignOption { + fn from(value: SignOptions) -> Self { + let SignOptions { + gpg_sign, + no_gpg_sign, + } = value; + if no_gpg_sign { + Self::Disable + } else if let Some(keyid) = gpg_sign { + if keyid.as_str() != "" { + Self::KeyOverride(keyid) + } else { + Self::UseConfigKey + } + } else { + Self::UseConfig + } + } +} + /// Internal use. #[derive(Debug, Parser)] pub enum HookSubcommand { @@ -329,6 +373,9 @@ pub struct RecordArgs { /// After making the new commit, switch back to the previous commit. #[clap(action, short = 's', long = "stash", conflicts_with_all(&["create", "detach"]))] pub stash: bool, + /// Options for signing commits. + #[clap(flatten)] + pub sign_options: SignOptions, } /// Display a nice graph of the commits you've recently worked on. @@ -358,6 +405,10 @@ pub struct SmartlogArgs { /// Options for resolving revset expressions. #[clap(flatten)] pub resolve_revset_options: ResolveRevsetOptions, + + /// Show GPG signature status for each commit. + #[clap(action, long = "show-signature")] + pub show_signature: bool, } /// The Git hosting provider to use, called a "forge". @@ -639,6 +690,10 @@ pub enum Command { /// use with `git rebase --autosquash`) targeting the supplied commit. #[clap(value_parser, long = "fixup", conflicts_with_all(&["messages", "discard"]))] commit_to_fixup: Option, + + /// GPG-sign commits. + #[clap(flatten)] + sign_options: SignOptions, }, /// `smartlog` command. diff --git a/git-branchless-record/src/lib.rs b/git-branchless-record/src/lib.rs index 166f6539b..be3de2453 100644 --- a/git-branchless-record/src/lib.rs +++ b/git-branchless-record/src/lib.rs @@ -32,7 +32,7 @@ use lib::core::rewrite::{ }; use lib::git::{ process_diff_for_record, update_index, CategorizedReferenceName, FileMode, GitRunInfo, - MaybeZeroOid, NonZeroOid, Repo, ResolvedReferenceInfo, Stage, UpdateIndexCommand, + MaybeZeroOid, NonZeroOid, Repo, ResolvedReferenceInfo, SignOption, Stage, UpdateIndexCommand, WorkingCopyChangesType, WorkingCopySnapshot, }; use lib::try_exit_code; @@ -58,6 +58,7 @@ pub fn command_main(ctx: CommandContext, args: RecordArgs) -> EyreExitOr<()> { detach, insert, stash, + sign_options, } = args; record( &effects, @@ -68,6 +69,7 @@ pub fn command_main(ctx: CommandContext, args: RecordArgs) -> EyreExitOr<()> { detach, insert, stash, + &sign_options.into(), ) } @@ -81,6 +83,7 @@ fn record( detach: bool, insert: bool, stash: bool, + sign_option: &SignOption, ) -> EyreExitOr<()> { let now = SystemTime::now(); let repo = Repo::from_dir(&git_run_info.working_directory)?; @@ -153,18 +156,25 @@ fn record( &repo, &snapshot, event_tx_id, - messages, + messages.clone(), + sign_option, )?); } } else { - let args = { - let mut args = vec!["commit"]; - args.extend(messages.iter().flat_map(|message| ["--message", message])); - if working_copy_changes_type == WorkingCopyChangesType::Unstaged { - args.push("--all"); - } - args - }; + let message = messages.first().map_or_else(|| String::new(), |m| m.clone()); + let mut args = vec!["commit".to_string()]; + if !message.is_empty() { + args.push("--message".to_string()); + args.push(message); + } + if working_copy_changes_type == WorkingCopyChangesType::Unstaged { + args.push("--all".to_string()); + } + let sign_flag = sign_option.as_git_flag(); + if let Some(flag) = sign_flag { + args.push(flag); + } + try_exit_code!(git_run_info.run_direct_no_wrapping(Some(event_tx_id), &args)?); } @@ -239,7 +249,8 @@ fn record( effects, git_run_info, now, - event_tx_id + event_tx_id, + sign_option, )?); } @@ -254,6 +265,7 @@ fn record_interactive( snapshot: &WorkingCopySnapshot, event_tx_id: EventTransactionId, messages: Vec, + sign_option: &SignOption, ) -> EyreExitOr<()> { let old_tree = snapshot.commit_stage0.get_tree()?; let new_tree = snapshot.commit_unstaged.get_tree()?; @@ -337,7 +349,7 @@ fn record_interactive( return Ok(Err(ExitCode(1))); } }; - let message = commits[0].message.clone().unwrap_or_default(); + let _message = commits[0].message.clone().unwrap_or_default(); let update_index_script: Vec = result .into_iter() @@ -405,13 +417,19 @@ fn record_interactive( &update_index_script, )?; - let args = { - let mut args = vec!["commit"]; - if !message.is_empty() { - args.extend(["--message", &message]); - } - args - }; + let message = messages.first().map_or_else(|| String::new(), |m| m.clone()); + + let mut args = vec!["commit".to_string()]; + if !message.is_empty() { + args.push("--message".to_string()); + args.push(message); + } + + let sign_flag = sign_option.as_git_flag(); + if let Some(flag) = sign_flag { + args.push(flag); + } + git_run_info.run_direct_no_wrapping(Some(event_tx_id), &args) } @@ -421,6 +439,7 @@ fn insert_before_siblings( git_run_info: &GitRunInfo, now: SystemTime, event_tx_id: EventTransactionId, + sign_option: &SignOption, ) -> EyreExitOr<()> { // Reopen the repository since references may have changed. let repo = Repo::from_dir(&git_run_info.working_directory)?; @@ -548,6 +567,7 @@ To proceed anyways, run: git move -f -s 'siblings(.)", force_on_disk: false, resolve_merge_conflicts: false, check_out_commit_options: Default::default(), + sign_option: sign_option.to_owned(), }; let result = execute_rebase_plan( effects, diff --git a/git-branchless-reword/src/lib.rs b/git-branchless-reword/src/lib.rs index ef53ad4d3..3005656b8 100644 --- a/git-branchless-reword/src/lib.rs +++ b/git-branchless-reword/src/lib.rs @@ -42,7 +42,7 @@ use lib::core::rewrite::{ }; use lib::git::{message_prettify, Commit, GitRunInfo, MaybeZeroOid, NonZeroOid, Repo}; -use git_branchless_opts::{ResolveRevsetOptions, Revset}; +use git_branchless_opts::{ResolveRevsetOptions, Revset, SignOptions}; use git_branchless_revset::resolve_commits; /// The commit message(s) provided by the user. @@ -90,6 +90,7 @@ pub fn reword( messages: InitialCommitMessages, git_run_info: &GitRunInfo, force_rewrite_public_commits: bool, + sign_options: SignOptions, ) -> EyreExitOr<()> { let repo = Repo::from_current_dir()?; let references_snapshot = repo.get_references_snapshot()?; @@ -295,6 +296,7 @@ pub fn reword( reset: false, render_smartlog: false, }, + sign_option: sign_options.into(), }; let result = execute_rebase_plan( effects, diff --git a/git-branchless-smartlog/src/lib.rs b/git-branchless-smartlog/src/lib.rs index e3b3bdc3f..9c532946c 100644 --- a/git-branchless-smartlog/src/lib.rs +++ b/git-branchless-smartlog/src/lib.rs @@ -34,7 +34,7 @@ use lib::core::formatting::Pluralize; use lib::core::node_descriptors::{ BranchesDescriptor, CommitMessageDescriptor, CommitOidDescriptor, DifferentialRevisionDescriptor, ObsolescenceExplanationDescriptor, Redactor, - RelativeTimeDescriptor, + RelativeTimeDescriptor, SignatureStatusDescriptor, }; use lib::git::{GitRunInfo, Repo}; @@ -753,6 +753,9 @@ mod render { /// Normally HEAD and the main branch are included. Set this to exclude them. pub exact: bool, + + /// Show GPG signature status for each commit. + pub show_signature: bool, } } @@ -769,6 +772,7 @@ pub fn smartlog( resolve_revset_options, reverse, exact, + show_signature, } = options; let repo = Repo::from_dir(&git_run_info.working_directory)?; @@ -845,6 +849,7 @@ pub fn smartlog( &Redactor::Disabled, )?, &mut DifferentialRevisionDescriptor::new(&repo, &Redactor::Disabled)?, + &mut SignatureStatusDescriptor::new(&repo, show_signature)?, &mut CommitMessageDescriptor::new(&Redactor::Disabled)?, ], )? @@ -916,6 +921,7 @@ pub fn command_main(ctx: CommandContext, args: SmartlogArgs) -> EyreExitOr<()> { resolve_revset_options, reverse, exact, + show_signature, } = args; smartlog( @@ -927,6 +933,7 @@ pub fn command_main(ctx: CommandContext, args: SmartlogArgs) -> EyreExitOr<()> { resolve_revset_options, reverse, exact, + show_signature, }, ) } diff --git a/git-branchless-smartlog/tests/test_smartlog.rs b/git-branchless-smartlog/tests/test_smartlog.rs index 2b53bd6e3..30f7e4412 100644 --- a/git-branchless-smartlog/tests/test_smartlog.rs +++ b/git-branchless-smartlog/tests/test_smartlog.rs @@ -812,3 +812,84 @@ fn test_exact() -> eyre::Result<()> { Ok(()) } + +#[test] +fn test_show_signature_flag() -> eyre::Result<()> { + let git = make_git()?; + + git.init_repo()?; + + // Just create a regular commit without trying to sign it + git.commit_file("signed_file", 1)?; + git.commit_file("unsigned_file", 2)?; + + // Run smartlog without --show-signature flag + { + let stdout = git.smartlog()?; + // Without the flag, the signature indicators shouldn't be visible + // However, we can't reliably check for absence of brackets since they might be used elsewhere + // So we'll just ensure the command runs successfully + assert!(!stdout.is_empty(), "Smartlog output should not be empty"); + } + + // Run smartlog with --show-signature flag + { + let (stdout, _) = git.branchless("smartlog", &["--show-signature"])?; + // With the flag, the command should run successfully + assert!(!stdout.is_empty(), "Smartlog output should not be empty"); + // The signature status indicators should be present in some form, + // but we don't validate exactly which status since that depends on the environment + } + + Ok(()) +} + +#[test] +fn test_show_signature_snapshot() -> eyre::Result<()> { + let git = make_git()?; + + git.init_repo()?; + + // Create regular commits without trying to sign them + git.commit_file("signed_file", 1)?; + + // Create a branch and make another commit + git.run(&["checkout", "-b", "branch1", "master"])?; + git.commit_file("another_file", 2)?; + + // Run smartlog with --show-signature flag + let (stdout, _) = git.branchless("smartlog", &["--show-signature"])?; + + // Simply verify the command runs without error + assert!(!stdout.is_empty(), "Smartlog output should not be empty"); + + Ok(()) +} + +#[test] +fn test_show_signature_argument_parsing() -> eyre::Result<()> { + let git = make_git()?; + git.init_repo()?; + + // Test that invalid flag combination reports error + // For example, combine with a flag that doesn't make sense (if any) + // If there are no incompatible flags, we can at least test that the flag is recognized + + // Test help output includes the flag + let (help_stdout, _) = git.branchless("smartlog", &["--help"])?; + assert!(help_stdout.contains("--show-signature"), + "Help output should include --show-signature option"); + + // Test that the flag is recognized (should execute without error) + let result = git.branchless("smartlog", &["--show-signature"]); + assert!(result.is_ok(), "The --show-signature flag should be recognized"); + + // Test that the flag works when combined with other flags + let result = git.branchless("smartlog", &["--show-signature", "--exact"]); + assert!(result.is_ok(), "The --show-signature flag should work with --exact"); + + let result = git.branchless("smartlog", &["--show-signature", "--reverse"]); + assert!(result.is_ok(), "The --show-signature flag should work with --reverse"); + + Ok(()) +} diff --git a/git-branchless-submit/src/phabricator.rs b/git-branchless-submit/src/phabricator.rs index b21fb3c89..ede4f56a8 100644 --- a/git-branchless-submit/src/phabricator.rs +++ b/git-branchless-submit/src/phabricator.rs @@ -26,7 +26,9 @@ use lib::core::rewrite::{ execute_rebase_plan, BuildRebasePlanError, BuildRebasePlanOptions, ExecuteRebasePlanOptions, ExecuteRebasePlanResult, RebasePlanBuilder, RebasePlanPermissions, RepoResource, }; -use lib::git::{Commit, GitRunInfo, MaybeZeroOid, NonZeroOid, Repo, RepoError, TestCommand}; +use lib::git::{ + Commit, GitRunInfo, MaybeZeroOid, NonZeroOid, Repo, RepoError, SignOption, TestCommand, +}; use lib::try_exit_code; use lib::util::{ExitCode, EyreExitOr}; use rayon::ThreadPoolBuilder; @@ -359,6 +361,7 @@ impl Forge for PhabricatorForge<'_> { render_smartlog: false, ..Default::default() }, + sign_option: SignOption::Disable, }; let permissions = RebasePlanPermissions::verify_rewrite_set(self.dag, build_options, &commit_set) @@ -607,6 +610,7 @@ Differential Revision: https://phabricator.example.com/D000$(git rev-list --coun render_smartlog: false, ..Default::default() }, + sign_option: SignOption::Disable, }; let permissions = RebasePlanPermissions::verify_rewrite_set(self.dag, build_options, &commit_set) diff --git a/git-branchless-test/src/lib.rs b/git-branchless-test/src/lib.rs index add7cd898..3605f880f 100644 --- a/git-branchless-test/src/lib.rs +++ b/git-branchless-test/src/lib.rs @@ -52,8 +52,9 @@ use lib::core::rewrite::{ use lib::git::{ get_latest_test_command_path, get_test_locks_dir, get_test_tree_dir, get_test_worktrees_dir, make_test_command_slug, Commit, ConfigRead, GitRunInfo, GitRunResult, MaybeZeroOid, NonZeroOid, - Repo, SerializedNonZeroOid, SerializedTestResult, TestCommand, WorkingCopyChangesType, - TEST_ABORT_EXIT_CODE, TEST_INDETERMINATE_EXIT_CODE, TEST_SUCCESS_EXIT_CODE, + Repo, SerializedNonZeroOid, SerializedTestResult, SignOption, TestCommand, + WorkingCopyChangesType, TEST_ABORT_EXIT_CODE, TEST_INDETERMINATE_EXIT_CODE, + TEST_SUCCESS_EXIT_CODE, }; use lib::try_exit_code; use lib::util::{get_sh, ExitCode, EyreExitOr}; @@ -388,6 +389,7 @@ BUG: Expected resolved_interactive ({resolved_interactive:?}) to match interacti resolve_merge_conflicts, dump_rebase_constraints, dump_rebase_plan, + sign_options, } = move_options; let force_in_memory = true; @@ -416,6 +418,7 @@ BUG: Expected resolved_interactive ({resolved_interactive:?}) to match interacti render_smartlog: false, ..Default::default() }, + sign_option: sign_options.to_owned().into(), }; let permissions = match RebasePlanPermissions::verify_rewrite_set(dag, build_options, commits)? { @@ -731,6 +734,7 @@ fn set_abort_trap( render_smartlog: false, ..Default::default() }, + sign_option: SignOption::Disable, }, )? { ExecuteRebasePlanResult::Succeeded { rewritten_oids: _ } => { @@ -1983,12 +1987,12 @@ fn apply_fixes( .try_collect()?; let fixed_tree = repo.find_tree_or_fail(fixed_tree_oid)?; let fixed_commit_oid = repo.create_commit( - None, &original_commit.get_author(), &original_commit.get_committer(), commit_message, &fixed_tree, parents.iter().collect(), + None, )?; if original_commit_oid == fixed_commit_oid { continue; diff --git a/git-branchless/Cargo.toml b/git-branchless/Cargo.toml index 830f42cf7..cca0a51df 100644 --- a/git-branchless/Cargo.toml +++ b/git-branchless/Cargo.toml @@ -56,6 +56,8 @@ tracing-chrome = { workspace = true } tracing-error = { workspace = true } tracing-subscriber = { workspace = true } +git2-ext = "0.6.2" + [dev-dependencies] insta = { workspace = true } diff --git a/git-branchless/src/commands/amend.rs b/git-branchless/src/commands/amend.rs index f20fa1251..19462aa1b 100644 --- a/git-branchless/src/commands/amend.rs +++ b/git-branchless/src/commands/amend.rs @@ -26,6 +26,7 @@ use lib::core::rewrite::{ execute_rebase_plan, move_branches, BuildRebasePlanOptions, ExecuteRebasePlanOptions, ExecuteRebasePlanResult, RebasePlanBuilder, RebasePlanPermissions, RepoResource, }; +use lib::git::get_signer; use lib::git::{AmendFastOptions, GitRunInfo, MaybeZeroOid, Repo, ResolvedReferenceInfo}; use lib::try_exit_code; use lib::util::{ExitCode, EyreExitOr}; @@ -155,12 +156,16 @@ pub fn amend( ) }; - let amended_commit_oid = head_commit.amend_commit( - None, + let sign_option = move_options.sign_options.to_owned().into(); + let signer = get_signer(&repo, &sign_option)?; + + let amended_commit_oid = repo.amend_commit( + &head_commit, Some(&author), Some(&committer), None, Some(&amended_tree), + signer.as_deref(), )?; // Switch to the new commit and move any branches. This is kind of a hack: @@ -265,12 +270,12 @@ pub fn amend( ) })?; let reparented_descendant_oid = repo.create_commit( - None, &descendant_commit.get_author(), &descendant_commit.get_committer(), descendant_message, &descendant_commit.get_tree()?, parents.iter().collect(), + signer.as_deref(), )?; builder.replace_commit(descendant_oid, reparented_descendant_oid)?; } @@ -300,6 +305,7 @@ pub fn amend( reset: false, render_smartlog: false, }, + sign_option, }; match execute_rebase_plan( effects, diff --git a/git-branchless/src/commands/mod.rs b/git-branchless/src/commands/mod.rs index d0ff79eaa..ee7d8c1d2 100644 --- a/git-branchless/src/commands/mod.rs +++ b/git-branchless/src/commands/mod.rs @@ -152,6 +152,7 @@ fn command_main(ctx: CommandContext, opts: Opts) -> EyreExitOr<()> { force_rewrite_public_commits, discard, commit_to_fixup, + sign_options, } => { let messages = if discard { git_branchless_reword::InitialCommitMessages::Discard @@ -167,6 +168,7 @@ fn command_main(ctx: CommandContext, opts: Opts) -> EyreExitOr<()> { messages, &git_run_info, force_rewrite_public_commits, + sign_options, )? } diff --git a/git-branchless/src/commands/restack.rs b/git-branchless/src/commands/restack.rs index 0b6c72839..802564b1c 100644 --- a/git-branchless/src/commands/restack.rs +++ b/git-branchless/src/commands/restack.rs @@ -312,6 +312,7 @@ pub fn restack( resolve_merge_conflicts, dump_rebase_constraints, dump_rebase_plan, + ref sign_options, } = *move_options; let build_options = BuildRebasePlanOptions { force_rewrite_public_commits, @@ -331,6 +332,7 @@ pub fn restack( reset: false, render_smartlog: false, }, + sign_option: sign_options.to_owned().into(), }; let pool = ThreadPoolBuilder::new().build()?; let repo_pool = RepoResource::new_pool(&repo)?; diff --git a/git-branchless/src/commands/sync.rs b/git-branchless/src/commands/sync.rs index 5106c1841..756d019e1 100644 --- a/git-branchless/src/commands/sync.rs +++ b/git-branchless/src/commands/sync.rs @@ -76,6 +76,7 @@ pub fn sync( resolve_merge_conflicts, dump_rebase_constraints, dump_rebase_plan, + ref sign_options, } = *move_options; let build_options = BuildRebasePlanOptions { force_rewrite_public_commits, @@ -97,6 +98,7 @@ pub fn sync( reset: false, render_smartlog: false, }, + sign_option: sign_options.to_owned().into(), }; let thread_pool = ThreadPoolBuilder::new().build()?; let repo_pool = RepoResource::new_pool(&repo)?; diff --git a/install.sh b/install.sh new file mode 100755 index 000000000..aa3e8301a --- /dev/null +++ b/install.sh @@ -0,0 +1,84 @@ +#!/bin/bash +set -eo pipefail + +# Colors for output +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +echo -e "${GREEN}git-branchless GPG fork installer${NC}" +echo "This script will install git-branchless with enhanced GPG signing support" +echo + +# Check if Rust/Cargo is installed +if ! command -v cargo &> /dev/null; then + echo -e "${RED}Error: Cargo is not installed.${NC}" + echo "Please install Rust and Cargo first: https://rustup.rs/" + exit 1 +fi + +# Check if Git is installed +if ! command -v git &> /dev/null; then + echo -e "${RED}Error: Git is not installed.${NC}" + echo "Please install Git first" + exit 1 +fi + +# Check if GPG is installed +if ! command -v gpg &> /dev/null; then + echo -e "${YELLOW}Warning: GPG is not installed.${NC}" + echo "The GPG signing features will not work without GPG installed." + read -p "Continue anyway? [y/N] " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo "Installation aborted." + exit 1 + fi +fi + +echo "Building git-branchless with GPG support..." +cargo build --release + +echo "Installing git-branchless..." +cargo install --path git-branchless + +# Check if installation was successful +if command -v git-branchless &> /dev/null; then + echo -e "${GREEN}git-branchless has been successfully installed!${NC}" +else + echo -e "${YELLOW}git-branchless was built but may not be in your PATH.${NC}" + + # Detect shell + SHELL_NAME=$(basename "$SHELL") + SHELL_RC="" + + if [[ "$SHELL_NAME" == "bash" ]]; then + SHELL_RC="$HOME/.bashrc" + elif [[ "$SHELL_NAME" == "zsh" ]]; then + SHELL_RC="$HOME/.zshrc" + fi + + if [[ -n "$SHELL_RC" ]]; then + echo "You may need to add the Cargo bin directory to your PATH:" + echo "echo 'export PATH=\$PATH:\$HOME/.cargo/bin' >> $SHELL_RC" + echo "source $SHELL_RC" + else + echo "Add the following to your shell configuration:" + echo "export PATH=\$PATH:\$HOME/.cargo/bin" + fi +fi + +echo +echo -e "${GREEN}Installation complete!${NC}" +echo +echo "To use git-branchless in a repository:" +echo " 1. Navigate to your repository: cd /path/to/your/repo" +echo " 2. Initialize git-branchless: git-branchless init" +echo " 3. Use the commands: git branchless smartlog" +echo +echo "To configure GPG signing:" +echo " git config --global user.signingkey YOUR_GPG_KEY_ID" +echo " git config --global commit.gpgsign true # Optional: sign all commits by default" +echo +echo "For more information, see the README.md" diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 94ed87190..a69d8bf11 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] # Current minimum-supported Rust version -channel = "1.74" +channel = "stable" profile = "default" diff --git a/test-advanced-gpg-subtrees.sh b/test-advanced-gpg-subtrees.sh new file mode 100755 index 000000000..3647c188d --- /dev/null +++ b/test-advanced-gpg-subtrees.sh @@ -0,0 +1,179 @@ +#!/bin/bash +set -eo pipefail + +# Colors for output +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +echo -e "${GREEN}Testing git-branchless advanced operations with GPG signing${NC}" +echo + +# Check if GPG is installed +if ! command -v gpg &> /dev/null; then + echo -e "${RED}Error: GPG is not installed.${NC}" + echo "Please install GPG to run this test." + exit 1 +fi + +# Check if git-branchless is installed +if ! command -v git-branchless &> /dev/null; then + echo -e "${RED}Error: git-branchless is not installed or not in PATH.${NC}" + echo "Please install git-branchless first." + exit 1 +fi + +# Create a temporary test directory +TEST_DIR=$(mktemp -d) +echo "Creating test repository in: $TEST_DIR" +cd "$TEST_DIR" + +# Initialize a git repository +git init +echo "# Test Repository" > README.md +git add README.md +git commit -m "Initial commit" + +# Configure GPG signing for this test repository +echo +echo -e "${YELLOW}GPG Configuration${NC}" +echo "We'll now set up GPG signing for this test repository." +echo "If you have a GPG key already, please enter the key ID." +echo "If not, press Enter and we'll try to use your default key." +echo + +read -p "Enter your GPG key ID (or press Enter for default): " GPG_KEY_ID + +if [ -z "$GPG_KEY_ID" ]; then + # Try to get the default key + GPG_KEY_ID=$(gpg --list-secret-keys --keyid-format=long | grep sec | head -n 1 | awk '{print $2}' | cut -d'/' -f2) + + if [ -z "$GPG_KEY_ID" ]; then + echo -e "${RED}No GPG key found. Please create a GPG key first.${NC}" + echo "You can create a key with: gpg --full-generate-key" + exit 1 + fi + + echo "Using default GPG key: $GPG_KEY_ID" +else + echo "Using provided GPG key: $GPG_KEY_ID" +fi + +# Configure Git to use the GPG key +git config user.signingkey "$GPG_KEY_ID" +git config commit.gpgsign true +git config user.name "Test User" +git config user.email "test@example.com" + +# Initialize git-branchless +echo +echo "Initializing git-branchless..." +git-branchless init + +echo -e "${BLUE}Creating a commit graph for testing...${NC}" + +# Main branch +echo "Main feature" > main.txt +git add main.txt +git commit -S -m "Add main feature" + +# First feature branch with two commits +git checkout -b feature1 +echo "Feature 1 commit" > feature1.txt +git add feature1.txt +git commit -S -m "Add feature1" + +# Second feature branch based on main +git checkout main +git checkout -b feature2 +echo "Feature 2 commit" > feature2.txt +git add feature2.txt +git commit -S -m "Add feature2" + +# Create a third branch from main +git checkout main +git checkout -b feature3 +echo "Feature 3 commit" > feature3.txt +git add feature3.txt +git commit -S -m "Add feature3" + +# Show the commit graph +echo -e "${BLUE}Commit graph before operations:${NC}" +git branchless smartlog + +# SCENARIO 1: Moving a commit to change the branch structure +echo -e "${GREEN}Scenario 1: Moving a commit to change branch structure${NC}" +echo "Moving feature2 to be based on feature1..." + +git branchless move -d feature2 -s feature1 + +# Show the updated commit graph +echo -e "${BLUE}Commit graph after moving feature2:${NC}" +git branchless smartlog + +# Verify signatures of the moved commit +echo -e "${GREEN}Verifying signatures in moved branch:${NC}" +git checkout feature2 +echo "Checking signature of feature2 tip:" +git verify-commit HEAD + +# SCENARIO 2: Test restacking after base modification +echo -e "${GREEN}Scenario 2: Testing restack with divergent branches${NC}" +echo "Modifying feature1 to cause feature2 to need restacking..." + +# Modify feature1 +git checkout feature1 +echo "Modified feature1" >> feature1.txt +git add feature1.txt +git commit --amend -S -m "Modified feature1 (amended)" + +# Show the broken commit graph +echo -e "${BLUE}Commit graph with abandoned branch:${NC}" +git branchless smartlog + +# Restack the commits +echo "Restacking all abandoned commits..." +git branchless restack + +# Show the fixed commit graph +echo -e "${BLUE}Commit graph after restacking:${NC}" +git branchless smartlog + +# Verify signatures of restacked commits +echo -e "${GREEN}Verifying signatures after restack:${NC}" +git checkout feature2 +echo "Checking signature of feature2 tip after restack:" +git verify-commit HEAD + +# SCENARIO 3: Test interactive record with signing +echo -e "${GREEN}Scenario 3: Testing record with signing${NC}" + +git checkout feature3 +echo "Additional content" >> feature3.txt +git branchless record -m "Update feature3 with record command" + +# Check signature of the new commit +echo "Checking signature of commit created with record command:" +git verify-commit HEAD + +# SCENARIO 4: Testing complex move operation +echo -e "${GREEN}Scenario 4: Testing a more complex move operation${NC}" +echo "Moving feature3 to be based on feature2..." + +git branchless move -d feature3 -s feature2 + +# Show the updated commit graph +echo -e "${BLUE}Final commit graph:${NC}" +git branchless smartlog + +# Verify signature +echo "Checking signature of feature3 tip after move:" +git verify-commit HEAD + +echo +echo -e "${GREEN}Advanced GPG testing completed!${NC}" +echo "The test repository is at: $TEST_DIR" +echo "You can explore the commit graph with: cd $TEST_DIR && git branchless smartlog" +echo "You can delete it when you're done with: rm -rf $TEST_DIR" diff --git a/test-gpg-signing.sh b/test-gpg-signing.sh new file mode 100755 index 000000000..250518a87 --- /dev/null +++ b/test-gpg-signing.sh @@ -0,0 +1,171 @@ +#!/bin/bash +set -eo pipefail + +# Colors for output +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +echo -e "${GREEN}Testing git-branchless GPG signing functionality${NC}" +echo + +# Check if GPG is installed +if ! command -v gpg &> /dev/null; then + echo -e "${RED}Error: GPG is not installed.${NC}" + echo "Please install GPG to run this test." + exit 1 +fi + +# Check if git-branchless is installed +if ! command -v git-branchless &> /dev/null; then + echo -e "${RED}Error: git-branchless is not installed or not in PATH.${NC}" + echo "Please install git-branchless first." + exit 1 +fi + +# Create a temporary test directory +TEST_DIR=$(mktemp -d) +echo "Creating test repository in: $TEST_DIR" +cd "$TEST_DIR" + +# Initialize a git repository +git init +echo "# Test Repository" > README.md +git add README.md +git commit -m "Initial commit" + +# Create a branch to work with +git checkout -b feature + +# Configure GPG signing for this test repository +echo +echo -e "${YELLOW}GPG Configuration${NC}" +echo "We'll now set up GPG signing for this test repository." +echo "If you have a GPG key already, please enter the key ID." +echo "If not, press Enter and we'll try to use your default key." +echo + +read -p "Enter your GPG key ID (or press Enter for default): " GPG_KEY_ID + +if [ -z "$GPG_KEY_ID" ]; then + # Try to get the default key + GPG_KEY_ID=$(gpg --list-secret-keys --keyid-format=long | grep sec | head -n 1 | awk '{print $2}' | cut -d'/' -f2) + + if [ -z "$GPG_KEY_ID" ]; then + echo -e "${RED}No GPG key found. Please create a GPG key first.${NC}" + echo "You can create a key with: gpg --full-generate-key" + exit 1 + fi + + echo "Using default GPG key: $GPG_KEY_ID" +else + echo "Using provided GPG key: $GPG_KEY_ID" +fi + +# Configure Git to use the GPG key +git config user.signingkey "$GPG_KEY_ID" +git config commit.gpgsign true +git config user.name "Test User" +git config user.email "test@example.com" + +echo +echo "Creating a series of commits to test with..." + +# Create some test files and commits +echo "First content" > file1.txt +git add file1.txt +git commit -S -m "Add file1.txt (signed)" + +echo "Second content" > file2.txt +git add file2.txt +git commit -S -m "Add file2.txt (signed)" + +echo "Third content" > file3.txt +git add file3.txt +git commit -S -m "Add file3.txt (signed)" + +# Save the hash of the latest commit +LATEST_COMMIT=$(git rev-parse HEAD) + +# Initialize git-branchless +echo +echo "Initializing git-branchless..." +git-branchless init + +# Show current state +echo +echo "Current commit stack:" +git branchless smartlog + +# Create a commit to be moved or restacked +git checkout HEAD~1 +echo "Feature content" > feature.txt +git add feature.txt +git commit -S -m "Add feature.txt (signed, to be moved)" + +# Get the hash of the new feature commit +FEATURE_COMMIT=$(git rev-parse HEAD) + +# Show current state again +echo +echo "Commit stack with a commit to be moved:" +git branchless smartlog + +# Test git move +echo +echo -e "${GREEN}Testing git move with signed commits...${NC}" +echo "Moving the 'feature.txt' commit to be on top of the latest commit..." +git branchless move -d "$FEATURE_COMMIT" -s "$LATEST_COMMIT" + +# Show the result +echo +echo "Commit stack after move:" +git branchless smartlog + +# Verify signatures +echo +echo -e "${GREEN}Verifying signatures...${NC}" +git checkout @ # Go to the tip of the stack +echo "Checking signature of the moved commit:" +git verify-commit HEAD + +echo +echo "Checking signature of the previous commit:" +git verify-commit HEAD~1 + +# Test git restack +echo +echo -e "${GREEN}Testing git restack with signed commits...${NC}" +# Create a situation that would require restacking +git checkout HEAD~2 +echo "Updated content" >> file1.txt +git add file1.txt +git commit --amend -S -m "Update file1.txt (amended, signed)" + +echo +echo "Commit stack after amending a commit (should show abandoned commits):" +git branchless smartlog + +echo +echo "Restacking commits..." +git branchless restack + +echo +echo "Commit stack after restacking:" +git branchless smartlog + +# Verify signatures again +echo +echo -e "${GREEN}Verifying signatures after restack...${NC}" +echo "Checking signatures of restacked commits:" +for commit in $(git log --format=%H -n 3); do + echo "Commit: $(git log -1 --format=%s $commit)" + git verify-commit $commit || echo "Signature verification failed!" + echo +done + +echo +echo -e "${GREEN}Test completed!${NC}" +echo "The test repository is at: $TEST_DIR" +echo "You can delete it when you're done with: rm -rf $TEST_DIR"