diff --git a/Cargo.lock b/Cargo.lock index 961fb9ca8..143bcee14 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,9 +19,9 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] @@ -64,22 +64,22 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.10" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -158,9 +158,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.4" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "bitvec" @@ -185,9 +185,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.12.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" dependencies = [ "memchr", "regex-automata", @@ -208,15 +208,15 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" [[package]] name = "cc" -version = "1.2.40" +version = "1.2.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d05d92f4b1fd76aad469d46cdd858ca761576082cd37df81416691e50199fb" +checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36" dependencies = [ "find-msvc-tools", "jobserver", @@ -226,9 +226,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chrono" @@ -273,9 +273,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" [[package]] name = "clru" @@ -381,9 +381,9 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "typenum", @@ -518,9 +518,9 @@ checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "env_filter" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" dependencies = [ "log", "regex", @@ -573,15 +573,15 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "find-msvc-tools" -version = "0.1.3" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0399f9d26e5191ce32c498bebd31e7a3ceabc2745f0ac54af3f335126c3f24b3" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" [[package]] name = "flate2" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc5a4e564e38c699f2880d3fda590bedc2e69f3f84cd48b457bd892ce61d0aa9" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" dependencies = [ "crc32fast", "libz-rs-sys", @@ -756,19 +756,19 @@ checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "libc", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi", ] [[package]] name = "getrandom" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", "r-efi", - "wasi 0.14.7+wasi-0.2.4", + "wasip2", ] [[package]] @@ -803,7 +803,7 @@ version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2deb07a133b1520dc1a5690e9bd08950108873d7ed5de38dcc74d3b5ebffa110" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "libc", "libgit2-sys", "log", @@ -817,32 +817,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "514c29cc879bdc0286b0cbc205585a49b252809eb86c69df4ce4f855ee75f635" dependencies = [ "gix-actor", - "gix-commitgraph", - "gix-config", + "gix-commitgraph 0.29.0", + "gix-config 0.46.0", "gix-date", "gix-diff", "gix-discover", - "gix-features", - "gix-fs", - "gix-glob", - "gix-hash", - "gix-hashtable", - "gix-lock", - "gix-object", + "gix-features 0.43.1", + "gix-fs 0.16.1", + "gix-glob 0.21.0", + "gix-hash 0.19.0", + "gix-hashtable 0.9.0", + "gix-lock 18.0.0", + "gix-object 0.50.2", "gix-odb", "gix-pack", "gix-path", "gix-protocol", - "gix-ref", - "gix-refspec", - "gix-revision", - "gix-revwalk", + "gix-ref 0.53.1", + "gix-refspec 0.31.0", + "gix-revision 0.35.0", + "gix-revwalk 0.21.0", "gix-sec", "gix-shallow", - "gix-tempfile", + "gix-tempfile 18.0.0", "gix-trace", "gix-traverse", - "gix-url", + "gix-url 0.32.0", "gix-utils", "gix-validate", "once_cell", @@ -852,9 +852,9 @@ dependencies = [ [[package]] name = "gix-actor" -version = "0.35.4" +version = "0.35.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d36dcf9efe32b51b12dfa33cedff8414926124e760a32f9e7a6b5580d280967" +checksum = "987a51a7e66db6ef4dc030418eb2a42af6b913a79edd8670766122d8af3ba59e" dependencies = [ "bstr", "gix-date", @@ -864,20 +864,37 @@ dependencies = [ "winnow", ] +[[package]] +name = "gix-attributes" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6591add69314fc43db078076a8da6f07957c65abb0b21c3e1b6a3cf50aa18d" +dependencies = [ + "bstr", + "gix-glob 0.22.1", + "gix-path", + "gix-quote", + "gix-trace", + "kstring", + "smallvec", + "thiserror", + "unicode-bom", +] + [[package]] name = "gix-chunk" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b1f1d8764958699dc764e3f727cef280ff4d1bd92c107bbf8acd85b30c1bd6f" +checksum = "5c356b3825677cb6ff579551bb8311a81821e184453cbd105e2fc5311b288eeb" dependencies = [ "thiserror", ] [[package]] name = "gix-command" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b31b65ca48a352ae86312b27a514a0c661935f96b481ac8b4371f65815eb196" +checksum = "095c8367c9dc4872a7706fbc39c7f34271b88b541120a4365ff0e36366f66e62" dependencies = [ "bstr", "gix-path", @@ -894,7 +911,20 @@ checksum = "6bb23121e952f43a5b07e3e80890336cb847297467a410475036242732980d06" dependencies = [ "bstr", "gix-chunk", - "gix-hash", + "gix-hash 0.19.0", + "memmap2", + "thiserror", +] + +[[package]] +name = "gix-commitgraph" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "826994ff6c01f1ff00d6a1844d7506717810a91ffed143da71e3bf39369751ef" +dependencies = [ + "bstr", + "gix-chunk", + "gix-hash 0.20.1", "memmap2", "thiserror", ] @@ -907,10 +937,10 @@ checksum = "5dfb898c5b695fd4acfc3c0ab638525a65545d47706064dcf7b5ead6cdb136c0" dependencies = [ "bstr", "gix-config-value", - "gix-features", - "gix-glob", + "gix-features 0.43.1", + "gix-glob 0.21.0", "gix-path", - "gix-ref", + "gix-ref 0.53.1", "gix-sec", "memchr", "once_cell", @@ -920,13 +950,33 @@ dependencies = [ "winnow", ] +[[package]] +name = "gix-config" +version = "0.47.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e74f57ea99025de9207db53488be4d59cf2000f617964c1b550880524fefbc3" +dependencies = [ + "bstr", + "gix-config-value", + "gix-features 0.44.1", + "gix-glob 0.22.1", + "gix-path", + "gix-ref 0.54.1", + "gix-sec", + "memchr", + "smallvec", + "thiserror", + "unicode-bom", + "winnow", +] + [[package]] name = "gix-config-value" -version = "0.15.1" +version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f012703eb67e263c6c1fc96649fec47694dd3e5d2a91abfc65e4a6a6dc85309" +checksum = "2c489abb061c74b0c3ad790e24a606ef968cebab48ec673d6a891ece7d5aef64" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "bstr", "gix-path", "libc", @@ -935,9 +985,9 @@ dependencies = [ [[package]] name = "gix-date" -version = "0.10.5" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "996b6b90bafb287330af92b274c3e64309dc78359221d8612d11cd10c8b9fe1c" +checksum = "661245d045aa7c16ba4244daaabd823c562c3e45f1f25b816be2c57ee09f2171" dependencies = [ "bstr", "itoa", @@ -953,8 +1003,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de854852010d44a317f30c92d67a983e691c9478c8a3fb4117c1f48626bcdea8" dependencies = [ "bstr", - "gix-hash", - "gix-object", + "gix-hash 0.19.0", + "gix-object 0.50.2", "thiserror", ] @@ -966,10 +1016,10 @@ checksum = "ffb180c91ca1a2cf53e828bb63d8d8f8fa7526f49b83b33d7f46cbeb5d79d30a" dependencies = [ "bstr", "dunce", - "gix-fs", - "gix-hash", + "gix-fs 0.16.1", + "gix-hash 0.19.0", "gix-path", - "gix-ref", + "gix-ref 0.53.1", "gix-sec", "thiserror", ] @@ -992,6 +1042,20 @@ dependencies = [ "walkdir", ] +[[package]] +name = "gix-features" +version = "0.44.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa64593d1586135102307fb57fb3a9d3868b6b1f45a4da1352cce5070f8916a" +dependencies = [ + "gix-path", + "gix-trace", + "gix-utils", + "libc", + "prodash", + "walkdir", +] + [[package]] name = "gix-fs" version = "0.16.1" @@ -1000,7 +1064,21 @@ checksum = "9a4d90307d064fa7230e0f87b03231be28f8ba63b913fc15346f489519d0c304" dependencies = [ "bstr", "fastrand", - "gix-features", + "gix-features 0.43.1", + "gix-path", + "gix-utils", + "thiserror", +] + +[[package]] +name = "gix-fs" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f1ecd896258cdc5ccd94d18386d17906b8de265ad2ecf68e3bea6b007f6a28f" +dependencies = [ + "bstr", + "fastrand", + "gix-features 0.44.1", "gix-path", "gix-utils", "thiserror", @@ -1012,9 +1090,21 @@ version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b947db8366823e7a750c254f6bb29e27e17f27e457bf336ba79b32423db62cd5" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", + "bstr", + "gix-features 0.43.1", + "gix-path", +] + +[[package]] +name = "gix-glob" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74254992150b0a88fdb3ad47635ab649512dff2cbbefca7916bb459894fc9d56" +dependencies = [ + "bitflags 2.10.0", "bstr", - "gix-features", + "gix-features 0.44.1", "gix-path", ] @@ -1025,7 +1115,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "251fad79796a731a2a7664d9ea95ee29a9e99474de2769e152238d4fdb69d50e" dependencies = [ "faster-hex", - "gix-features", + "gix-features 0.43.1", + "sha1-checked", + "thiserror", +] + +[[package]] +name = "gix-hash" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "826036a9bee95945b0be1e2394c64cd4289916c34a639818f8fd5153906985c1" +dependencies = [ + "faster-hex", + "gix-features 0.44.1", "sha1-checked", "thiserror", ] @@ -1036,18 +1138,40 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c35300b54896153e55d53f4180460931ccd69b7e8d2f6b9d6401122cdedc4f07" dependencies = [ - "gix-hash", + "gix-hash 0.19.0", "hashbrown 0.15.5", "parking_lot 0.12.5", ] +[[package]] +name = "gix-hashtable" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a27d4a3ea9640da504a2657fef3419c517fd71f1767ad8935298bcc805edd195" +dependencies = [ + "gix-hash 0.20.1", + "hashbrown 0.16.0", + "parking_lot 0.12.5", +] + [[package]] name = "gix-lock" version = "18.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9fa71da90365668a621e184eb5b979904471af1b3b09b943a84bc50e8ad42ed" dependencies = [ - "gix-tempfile", + "gix-tempfile 18.0.0", + "gix-utils", + "thiserror", +] + +[[package]] +name = "gix-lock" +version = "19.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "729d7857429a66023bc0c29d60fa21d0d6ae8862f33c1937ba89e0f74dd5c67f" +dependencies = [ + "gix-tempfile 19.0.1", "gix-utils", "thiserror", ] @@ -1061,9 +1185,30 @@ dependencies = [ "bstr", "gix-actor", "gix-date", - "gix-features", - "gix-hash", - "gix-hashtable", + "gix-features 0.43.1", + "gix-hash 0.19.0", + "gix-hashtable 0.9.0", + "gix-path", + "gix-utils", + "gix-validate", + "itoa", + "smallvec", + "thiserror", + "winnow", +] + +[[package]] +name = "gix-object" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba1815638759c80d2318c8e98296fb396f577c2e588a3d9c13f9a5d5184051" +dependencies = [ + "bstr", + "gix-actor", + "gix-date", + "gix-features 0.44.1", + "gix-hash 0.20.1", + "gix-hashtable 0.10.0", "gix-path", "gix-utils", "gix-validate", @@ -1081,11 +1226,11 @@ checksum = "9c9d7af10fda9df0bb4f7f9bd507963560b3c66cb15a5b825caf752e0eb109ac" dependencies = [ "arc-swap", "gix-date", - "gix-features", - "gix-fs", - "gix-hash", - "gix-hashtable", - "gix-object", + "gix-features 0.43.1", + "gix-fs 0.16.1", + "gix-hash 0.19.0", + "gix-hashtable 0.9.0", + "gix-object 0.50.2", "gix-pack", "gix-path", "gix-quote", @@ -1102,10 +1247,10 @@ checksum = "d8571df89bfca5abb49c3e3372393f7af7e6f8b8dbe2b96303593cef5b263019" dependencies = [ "clru", "gix-chunk", - "gix-features", - "gix-hash", - "gix-hashtable", - "gix-object", + "gix-features 0.43.1", + "gix-hash 0.19.0", + "gix-hashtable 0.9.0", + "gix-object 0.50.2", "gix-path", "memmap2", "smallvec", @@ -1114,9 +1259,9 @@ dependencies = [ [[package]] name = "gix-packetline" -version = "0.19.1" +version = "0.19.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2592fbd36249a2fea11056f7055cc376301ef38d903d157de41998335bbf1f93" +checksum = "64286a8b5148e76ab80932e72762dd27ccf6169dd7a134b027c8a262a8262fcf" dependencies = [ "bstr", "faster-hex", @@ -1126,15 +1271,29 @@ dependencies = [ [[package]] name = "gix-path" -version = "0.10.20" +version = "0.10.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06d37034a4c67bbdda76f7bcd037b2f7bc0fba0c09a6662b19697a5716e7b2fd" +checksum = "0416b41cd00ff292af9b94b0660880c44bd2ed66828ddca9a2b333535cbb71b8" dependencies = [ "bstr", "gix-trace", "gix-validate", "home", - "once_cell", + "thiserror", +] + +[[package]] +name = "gix-pathspec" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e28457dca7c65a2dbe118869aab922a5bd382b7bb10cff5354f366845c128" +dependencies = [ + "bitflags 2.10.0", + "bstr", + "gix-attributes", + "gix-config-value", + "gix-glob 0.22.1", + "gix-path", "thiserror", ] @@ -1146,9 +1305,9 @@ checksum = "12b4b807c47ffcf7c1e5b8119585368a56449f3493da93b931e1d4239364e922" dependencies = [ "bstr", "gix-date", - "gix-features", - "gix-hash", - "gix-ref", + "gix-features 0.43.1", + "gix-hash 0.19.0", + "gix-ref 0.53.1", "gix-shallow", "gix-transport", "gix-utils", @@ -1159,9 +1318,9 @@ dependencies = [ [[package]] name = "gix-quote" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a375a75b4d663e8bafe3bf4940a18a23755644c13582fa326e99f8f987d83fd" +checksum = "e912ec04b7b1566a85ad486db0cab6b9955e3e32bcd3c3a734542ab3af084c5b" dependencies = [ "bstr", "gix-utils", @@ -1175,13 +1334,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b966f578079a42f4a51413b17bce476544cca1cf605753466669082f94721758" dependencies = [ "gix-actor", - "gix-features", - "gix-fs", - "gix-hash", - "gix-lock", - "gix-object", + "gix-features 0.43.1", + "gix-fs 0.16.1", + "gix-hash 0.19.0", + "gix-lock 18.0.0", + "gix-object 0.50.2", "gix-path", - "gix-tempfile", + "gix-tempfile 18.0.0", + "gix-utils", + "gix-validate", + "memmap2", + "thiserror", + "winnow", +] + +[[package]] +name = "gix-ref" +version = "0.54.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8881d262f28eda39c244e60ae968f4f6e56c747f65addd6f4100b25f75ed8b88" +dependencies = [ + "gix-actor", + "gix-features 0.44.1", + "gix-fs 0.17.0", + "gix-hash 0.20.1", + "gix-lock 19.0.0", + "gix-object 0.51.1", + "gix-path", + "gix-tempfile 19.0.1", "gix-utils", "gix-validate", "memmap2", @@ -1196,8 +1376,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d29cae1ae31108826e7156a5e60bffacab405f4413f5bc0375e19772cce0055" dependencies = [ "bstr", - "gix-hash", - "gix-revision", + "gix-hash 0.19.0", + "gix-revision 0.35.0", + "gix-validate", + "smallvec", + "thiserror", +] + +[[package]] +name = "gix-refspec" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93147960f77695ba89b72019b789679278dd4dad6a0f9a4a5bf2fd07aba56912" +dependencies = [ + "bstr", + "gix-hash 0.20.1", + "gix-revision 0.36.1", "gix-validate", "smallvec", "thiserror", @@ -1210,11 +1404,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f651f2b1742f760bb8161d6743229206e962b73d9c33c41f4e4aefa6586cbd3d" dependencies = [ "bstr", - "gix-commitgraph", + "gix-commitgraph 0.29.0", "gix-date", - "gix-hash", - "gix-object", - "gix-revwalk", + "gix-hash 0.19.0", + "gix-object 0.50.2", + "gix-revwalk 0.21.0", + "thiserror", +] + +[[package]] +name = "gix-revision" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c5267e530d8762842be7d51b48d2b134c9dec5b650ca607f735a56a4b12413" +dependencies = [ + "bstr", + "gix-commitgraph 0.30.1", + "gix-date", + "gix-hash 0.20.1", + "gix-object 0.51.1", + "gix-revwalk 0.22.0", "thiserror", ] @@ -1224,25 +1433,40 @@ version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06e74f91709729e099af6721bd0fa7d62f243f2005085152301ca5cdd86ec02c" dependencies = [ - "gix-commitgraph", + "gix-commitgraph 0.29.0", + "gix-date", + "gix-hash 0.19.0", + "gix-hashtable 0.9.0", + "gix-object 0.50.2", + "smallvec", + "thiserror", +] + +[[package]] +name = "gix-revwalk" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02e2de4f91d712b1f6873477f769225fe430ffce2af8c7c85721c3ff955783b3" +dependencies = [ + "gix-commitgraph 0.30.1", "gix-date", - "gix-hash", - "gix-hashtable", - "gix-object", + "gix-hash 0.20.1", + "gix-hashtable 0.10.0", + "gix-object 0.51.1", "smallvec", "thiserror", ] [[package]] name = "gix-sec" -version = "0.12.0" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09f7053ed7c66633b56c57bc6ed3377be3166eaf3dc2df9f1c5ec446df6fdf2c" +checksum = "ea9962ed6d9114f7f100efe038752f41283c225bb507a2888903ac593dffa6be" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "gix-path", "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -1252,8 +1476,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d936745103243ae4c510f19e0760ce73fb0f08096588fdbe0f0d7fb7ce8944b7" dependencies = [ "bstr", - "gix-hash", - "gix-lock", + "gix-hash 0.19.0", + "gix-lock 18.0.0", + "thiserror", +] + +[[package]] +name = "gix-submodule" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bacc06333b50abc4fc06204622c2dd92850de2066bb5d421ac776d2bef7ae55" +dependencies = [ + "bstr", + "gix-config 0.47.1", + "gix-path", + "gix-pathspec", + "gix-refspec 0.32.0", + "gix-url 0.33.1", "thiserror", ] @@ -1263,18 +1502,30 @@ version = "18.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "666c0041bcdedf5fa05e9bef663c897debab24b7dc1741605742412d1d47da57" dependencies = [ - "gix-fs", + "gix-fs 0.16.1", "libc", "once_cell", "parking_lot 0.12.5", "tempfile", ] +[[package]] +name = "gix-tempfile" +version = "19.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e265fc6b54e57693232a79d84038381ebfda7b1a3b1b8a9320d4d5fe6e820086" +dependencies = [ + "gix-fs 0.17.0", + "libc", + "parking_lot 0.12.5", + "tempfile", +] + [[package]] name = "gix-trace" -version = "0.1.13" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2ccaf54b0b1743a695b482ca0ab9d7603744d8d10b2e5d1a332fef337bee658" +checksum = "1d3f59a8de2934f6391b6b3a1a7654eae18961fcb9f9c843533fed34ad0f3457" [[package]] name = "gix-transport" @@ -1284,11 +1535,11 @@ checksum = "12f7cc0179fc89d53c54e1f9ce51229494864ab4bf136132d69db1b011741ca3" dependencies = [ "bstr", "gix-command", - "gix-features", + "gix-features 0.43.1", "gix-packetline", "gix-quote", "gix-sec", - "gix-url", + "gix-url 0.32.0", "thiserror", ] @@ -1298,13 +1549,13 @@ version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7cdc82509d792ba0ad815f86f6b469c7afe10f94362e96c4494525a6601bdd5" dependencies = [ - "bitflags 2.9.4", - "gix-commitgraph", + "bitflags 2.10.0", + "gix-commitgraph 0.29.0", "gix-date", - "gix-hash", - "gix-hashtable", - "gix-object", - "gix-revwalk", + "gix-hash 0.19.0", + "gix-hashtable 0.9.0", + "gix-object 0.50.2", + "gix-revwalk 0.21.0", "smallvec", "thiserror", ] @@ -1316,7 +1567,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b76a9d266254ad287ffd44467cd88e7868799b08f4d52e02d942b93e514d16f" dependencies = [ "bstr", - "gix-features", + "gix-features 0.43.1", + "gix-path", + "percent-encoding", + "thiserror", + "url", +] + +[[package]] +name = "gix-url" +version = "0.33.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c79b07b48dd9285485eb10429696ddcd1bfe6fb942ec0e5efb401ae7e40238e5" +dependencies = [ + "bstr", + "gix-features 0.44.1", "gix-path", "percent-encoding", "thiserror", @@ -1325,9 +1590,9 @@ dependencies = [ [[package]] name = "gix-utils" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5351af2b172caf41a3728eb4455326d84e0d70fe26fc4de74ab0bd37df4191c5" +checksum = "befcdbdfb1238d2854591f760a48711bed85e72d80a10e8f2f93f656746ef7c5" dependencies = [ "fastrand", "unicode-normalization", @@ -1335,9 +1600,9 @@ dependencies = [ [[package]] name = "gix-validate" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77b9e00cacde5b51388d28ed746c493b18a6add1f19b5e01d686b3b9ece66d4d" +checksum = "5b1e63a5b516e970a594f870ed4571a8fdcb8a344e7bd407a20db8bd61dbfde4" dependencies = [ "bstr", "thiserror", @@ -1434,11 +1699,11 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "home" -version = "0.5.11" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -1495,9 +1760,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" dependencies = [ "atomic-waker", "bytes", @@ -1577,9 +1842,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" +checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56" dependencies = [ "base64", "bytes", @@ -1619,9 +1884,9 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", "potential_utf", @@ -1632,9 +1897,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", @@ -1645,11 +1910,10 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", @@ -1660,42 +1924,38 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" dependencies = [ - "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", - "potential_utf", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" +checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" [[package]] name = "icu_provider" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", "icu_locale_core", - "stable_deref_trait", - "tinystr", "writeable", "yoke", "zerofrom", @@ -1732,9 +1992,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.11.4" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" +checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" dependencies = [ "equivalent", "hashbrown 0.16.0", @@ -1768,9 +2028,9 @@ checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "iri-string" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" dependencies = [ "memchr", "serde", @@ -1778,9 +2038,9 @@ dependencies = [ [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itertools" @@ -1799,24 +2059,24 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" +checksum = "49cce2b81f2098e7e3efc35bc2e0a6b7abec9d34128283d7a26fa8f32a6dbb35" dependencies = [ "jiff-static", "jiff-tzdb-platform", "log", "portable-atomic", "portable-atomic-util", - "serde", - "windows-sys 0.59.0", + "serde_core", + "windows-sys 0.61.2", ] [[package]] name = "jiff-static" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" +checksum = "980af8b43c3ad5d8d349ace167ec8170839f753a42d233ba19e08afe1850fa69" dependencies = [ "proc-macro2", "quote", @@ -1844,7 +2104,7 @@ version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", "libc", ] @@ -1864,6 +2124,7 @@ dependencies = [ "log", "rs_tracing", "serde_json", + "toml", ] [[package]] @@ -1874,9 +2135,10 @@ dependencies = [ "bitvec", "git-version", "git2", - "gix-config", - "gix-hash", - "gix-object", + "gix-config 0.47.1", + "gix-hash 0.19.0", + "gix-object 0.50.2", + "gix-submodule", "glob", "hex", "indoc", @@ -1893,6 +2155,7 @@ dependencies = [ "serde_yaml", "sled", "strfmt", + "toml", "tracing", ] @@ -2027,9 +2290,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.81" +version = "0.3.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" +checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" dependencies = [ "once_cell", "wasm-bindgen", @@ -2076,6 +2339,15 @@ dependencies = [ "rayon", ] +[[package]] +name = "kstring" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "558bf9508a558512042d3095138b1f7b8fe90c5467d94f9f1da28b3731c5dbd1" +dependencies = [ + "static_assertions", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -2111,9 +2383,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.22" +version = "1.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b70e7a7df205e92a1a4cd9aaae7898dac0aa555503cc0a649494d0d60e7651d" +checksum = "15d118bbf3771060e7311cc7bb0545b01d08a8b4a7de949198dec1fa0ca1c0f7" dependencies = [ "cc", "libc", @@ -2129,9 +2401,9 @@ checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litemap" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "lock_api" @@ -2185,9 +2457,9 @@ checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "memmap2" -version = "0.9.8" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843a98750cd611cc2965a8213b53b43e715f13c37a9e096c6408e69990961db7" +checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490" dependencies = [ "libc", ] @@ -2220,13 +2492,13 @@ dependencies = [ [[package]] name = "mio" -version = "1.0.4" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" dependencies = [ "libc", - "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.59.0", + "wasi", + "windows-sys 0.61.2", ] [[package]] @@ -2257,11 +2529,11 @@ dependencies = [ [[package]] name = "nu-ansi-term" -version = "0.50.1" +version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -2305,17 +2577,17 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "once_cell_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "openssl" -version = "0.10.73" +version = "0.10.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "cfg-if", "foreign-types", "libc", @@ -2343,9 +2615,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" -version = "0.9.109" +version = "0.9.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" dependencies = [ "cc", "libc", @@ -2587,9 +2859,9 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" dependencies = [ "zerovec", ] @@ -2605,9 +2877,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.101" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] @@ -2646,9 +2918,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.41" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] @@ -2721,7 +2993,7 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", ] [[package]] @@ -2759,7 +3031,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", ] [[package]] @@ -2787,9 +3059,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.6" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "reqwest" @@ -2847,7 +3119,7 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys", @@ -2896,7 +3168,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "core-foundation", "core-foundation-sys", "libc", @@ -3099,19 +3371,19 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "stable_deref_trait" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "static_assertions" @@ -3133,9 +3405,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.106" +version = "2.0.110" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" dependencies = [ "proc-macro2", "quote", @@ -3175,7 +3447,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ "fastrand", - "getrandom 0.3.3", + "getrandom 0.3.4", "once_cell", "rustix", "windows-sys 0.61.2", @@ -3212,9 +3484,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", "zerovec", @@ -3398,7 +3670,7 @@ version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "bytes", "futures-util", "http", @@ -3544,15 +3816,15 @@ checksum = "7eec5d1121208364f6793f7d2e222bf75a915c19557537745b195b253dd64217" [[package]] name = "unicode-ident" -version = "1.0.19" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unicode-normalization" -version = "0.1.24" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" dependencies = [ "tinyvec", ] @@ -3593,7 +3865,7 @@ version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", "js-sys", "wasm-bindgen", ] @@ -3641,15 +3913,6 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" -[[package]] -name = "wasi" -version = "0.14.7+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" -dependencies = [ - "wasip2", -] - [[package]] name = "wasip2" version = "1.0.1+wasi-0.2.4" @@ -3661,9 +3924,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.104" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" +checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" dependencies = [ "cfg-if", "once_cell", @@ -3672,25 +3935,11 @@ dependencies = [ "wasm-bindgen-shared", ] -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - [[package]] name = "wasm-bindgen-futures" -version = "0.4.54" +version = "0.4.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" +checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0" dependencies = [ "cfg-if", "js-sys", @@ -3701,9 +3950,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.104" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" +checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3711,31 +3960,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.104" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" +checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" dependencies = [ + "bumpalo", "proc-macro2", "quote", "syn", - "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.104" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" +checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.81" +version = "0.3.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" +checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" dependencies = [ "js-sys", "wasm-bindgen", @@ -3788,31 +4037,13 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-sys" version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.5", + "windows-targets", ] [[package]] @@ -3824,22 +4055,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - [[package]] name = "windows-targets" version = "0.53.5" @@ -3847,106 +4062,58 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - [[package]] name = "windows_aarch64_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - [[package]] name = "windows_aarch64_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - [[package]] name = "windows_i686_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - [[package]] name = "windows_i686_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - [[package]] name = "windows_i686_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - [[package]] name = "windows_x86_64_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - [[package]] name = "windows_x86_64_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - [[package]] name = "windows_x86_64_msvc" version = "0.53.1" @@ -3970,9 +4137,9 @@ checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "writeable" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "wyz" @@ -3985,11 +4152,10 @@ dependencies = [ [[package]] name = "yoke" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -3997,9 +4163,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", @@ -4050,9 +4216,9 @@ dependencies = [ [[package]] name = "zerotrie" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" dependencies = [ "displaydoc", "yoke", @@ -4061,9 +4227,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ "yoke", "zerofrom", @@ -4072,9 +4238,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index a7e427229..8d5805285 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,9 @@ log = "0.4.28" futures = "0.3.31" gix = { version = "0.73.0", default-features = false } gix-hash = "0.19.0" +gix-object = "0.50.2" +gix-config = "0.47.1" +gix-submodule = "0.21.0" hyper-reverse-proxy = { path = "hyper-reverse-proxy", version = "0.0.1" } libc = "0.2.177" regex = "1.12.2" diff --git a/josh-cli/Cargo.toml b/josh-cli/Cargo.toml index 80b00ea68..c7a22b4af 100644 --- a/josh-cli/Cargo.toml +++ b/josh-cli/Cargo.toml @@ -22,3 +22,4 @@ clap = { workspace = true } rs_tracing = { workspace = true } juniper = { workspace = true } git2 = { workspace = true } +toml = { workspace = true } diff --git a/josh-cli/src/bin/josh.rs b/josh-cli/src/bin/josh.rs index 9afbf1cf9..2a91aa5eb 100644 --- a/josh-cli/src/bin/josh.rs +++ b/josh-cli/src/bin/josh.rs @@ -105,6 +105,9 @@ pub enum Command { /// Apply filtering to existing refs (like `josh fetch` but without fetching) Filter(FilterArgs), + + /// Manage josh links (like `josh remote` but for links) + Link(LinkArgs), } #[derive(Debug, clap::Parser)] @@ -242,6 +245,47 @@ pub struct FilterArgs { pub remote: String, } +#[derive(Debug, clap::Parser)] +pub struct LinkArgs { + /// Link subcommand + #[command(subcommand)] + pub command: LinkCommand, +} + +#[derive(Debug, clap::Subcommand)] +pub enum LinkCommand { + /// Add a link with optional filter and target branch + Add(LinkAddArgs), + /// Fetch from existing link files + Fetch(LinkFetchArgs), +} + +#[derive(Debug, clap::Parser)] +pub struct LinkAddArgs { + /// Path where the link will be mounted + #[arg()] + pub path: String, + + /// Remote repository URL + #[arg()] + pub url: String, + + /// Optional filter to apply to the linked repository + #[arg()] + pub filter: Option, + + /// Target branch to link (defaults to HEAD) + #[arg(long = "target")] + pub target: Option, +} + +#[derive(Debug, clap::Parser)] +pub struct LinkFetchArgs { + /// Optional path to specific .josh-link.toml file (if not provided, fetches all) + #[arg()] + pub path: Option, +} + fn main() { env_logger::init(); let cli = Cli::parse(); @@ -253,6 +297,7 @@ fn main() { Command::Push(args) => handle_push(args), Command::Remote(args) => handle_remote(args), Command::Filter(args) => handle_filter(args), + Command::Link(args) => handle_link(args), }; if let Err(e) = result { @@ -803,6 +848,347 @@ fn handle_push(args: &PushArgs) -> anyhow::Result<()> { Ok(()) } +fn handle_link(args: &LinkArgs) -> anyhow::Result<()> { + match &args.command { + LinkCommand::Add(add_args) => handle_link_add(add_args), + LinkCommand::Fetch(fetch_args) => handle_link_fetch(fetch_args), + } +} + +fn handle_link_add(args: &LinkAddArgs) -> anyhow::Result<()> { + use josh_core::filter::tree; + use josh_core::{JoshLinkFile, Oid}; + + // Check if we're in a git repository + let repo = git2::Repository::open_from_env().context("Not in a git repository")?; + + // Validate the path (should not be empty and should be a valid path) + if args.path.is_empty() { + return Err(anyhow::anyhow!("Path cannot be empty")); + } + + // Normalize the path by removing leading and trailing slashes + let normalized_path = args.path.trim_matches('/').to_string(); + + // Get the filter (default to ":/" if not provided) + let filter = args.filter.as_deref().unwrap_or(":/"); + + // Get the target branch (default to "HEAD" if not provided) + let target = args.target.as_deref().unwrap_or("HEAD"); + + // Parse the filter + let filter_obj = josh_core::filter::parse(filter) + .map_err(from_josh_err) + .with_context(|| format!("Failed to parse filter '{}'", filter))?; + + // Use git fetch shell command + let output = std::process::Command::new("git") + .args(&["fetch", &args.url, &target]) + .output() + .context("Failed to execute git fetch")?; + + if !output.status.success() { + return Err(anyhow::anyhow!( + "git fetch failed: {}", + String::from_utf8_lossy(&output.stderr) + )); + } + + // Get the commit SHA from FETCH_HEAD + let fetch_head = repo + .find_reference("FETCH_HEAD") + .context("Failed to find FETCH_HEAD")?; + let fetch_commit = fetch_head + .peel_to_commit() + .context("Failed to get FETCH_HEAD commit")?; + let actual_commit_sha = fetch_commit.id(); + + // Get the current HEAD commit + let head_ref = repo.head().context("Failed to get HEAD")?; + let head_commit = head_ref + .peel_to_commit() + .context("Failed to get HEAD commit")?; + let head_tree = head_commit.tree().context("Failed to get HEAD tree")?; + + // Create the JoshLinkFile with the actual commit SHA + let link_file = JoshLinkFile { + remote: args.url.clone(), + branch: target.to_string(), + filter: filter_obj, + commit: Oid::from(actual_commit_sha), + }; + + // Create the .josh-link.toml content + let link_content = toml::to_string(&link_file).context("Failed to serialize link file")?; + + // Create the blob for the .josh-link.toml file + let link_blob = repo + .blob(link_content.as_bytes()) + .context("Failed to create blob")?; + + // Create the path for the .josh-link.toml file + let link_path = std::path::Path::new(&normalized_path).join(".josh-link.toml"); + + // Insert the .josh-link.toml file into the tree + let new_tree = tree::insert(&repo, &head_tree, &link_path, link_blob, 0o0100644) + .map_err(from_josh_err) + .context("Failed to insert link file into tree")?; + + // Create a new commit with the updated tree + let signature = if let Ok(time) = std::env::var("JOSH_COMMIT_TIME") { + git2::Signature::new( + "JOSH", + "josh@josh-project.dev", + &git2::Time::new(time.parse().context("Failed to parse JOSH_COMMIT_TIME")?, 0), + ) + .context("Failed to create signature")? + } else { + repo.signature().context("Failed to get signature")? + }; + + let new_commit = repo + .commit( + None, // Don't update any reference + &signature, + &signature, + &format!("Add link: {}", normalized_path), + &new_tree, + &[&head_commit], + ) + .context("Failed to create commit")?; + + // Apply the :link filter to the new commit + let link_filter = josh_core::filter::parse(":link=snapshot") + .map_err(from_josh_err) + .context("Failed to parse :link filter")?; + + // Load the cache and create transaction + let repo_path = repo.path().parent().unwrap(); + + josh_core::cache_sled::sled_load(&repo_path).unwrap(); + let cache = std::sync::Arc::new( + josh_core::cache_stack::CacheStack::new() + .with_backend(josh_core::cache_sled::SledCacheBackend::default()) + .with_backend( + josh_core::cache_notes::NotesCacheBackend::new(&repo_path) + .map_err(from_josh_err) + .context("Failed to create NotesCacheBackend")?, + ), + ); + + // Open Josh transaction + let transaction = josh_core::cache::TransactionContext::from_env(cache.clone()) + .map_err(from_josh_err) + .context("Failed TransactionContext::from_env")? + .open(None) + .map_err(from_josh_err) + .context("Failed TransactionContext::open")?; + + let filtered_commit = josh_core::filter_commit( + &transaction, + link_filter, + new_commit, + josh_core::filter::empty(), + ) + .map_err(from_josh_err) + .context("Failed to apply :link filter")?; + + // Create the fixed branch name + let branch_name = "refs/heads/josh-link"; + + // Create or update the branch reference + repo.reference(branch_name, filtered_commit, true, "josh link add") + .with_context(|| format!("Failed to create branch '{}'", branch_name))?; + + println!( + "Added link '{}' with URL '{}', filter '{}', and target '{}'", + normalized_path, args.url, filter, target + ); + println!("Created branch: {}", branch_name); + + Ok(()) +} + +fn handle_link_fetch(args: &LinkFetchArgs) -> anyhow::Result<()> { + use josh_core::filter::tree; + use josh_core::{JoshLinkFile, Oid}; + + // Check if we're in a git repository + let repo = git2::Repository::open_from_env().context("Not in a git repository")?; + + // Get the current HEAD commit + let head_ref = repo.head().context("Failed to get HEAD")?; + let head_commit = head_ref + .peel_to_commit() + .context("Failed to get HEAD commit")?; + let head_tree = head_commit.tree().context("Failed to get HEAD tree")?; + + let link_files = if let Some(path) = &args.path { + // Single path specified - find the .josh-link.toml file at that path + let link_path = std::path::Path::new(path).join(".josh-link.toml"); + let link_entry = head_tree + .get_path(&link_path) + .with_context(|| format!("Failed to find .josh-link.toml at path '{}'", path))?; + + let link_blob = repo + .find_blob(link_entry.id()) + .context("Failed to find blob")?; + + let link_content = std::str::from_utf8(link_blob.content()) + .context("Failed to parse link file content")?; + + let link_file: JoshLinkFile = + toml::from_str(link_content).context("Failed to parse .josh-link.toml")?; + + vec![(std::path::PathBuf::from(path), link_file)] + } else { + // No path specified - find all .josh-link.toml files in the tree + josh_core::find_link_files(&repo, &head_tree) + .map_err(from_josh_err) + .context("Failed to find link files")? + }; + + if link_files.is_empty() { + return Err(anyhow::anyhow!("No .josh-link.toml files found")); + } + + println!("Found {} link file(s) to fetch", link_files.len()); + + // Fetch from all the link files + let mut updated_link_files = Vec::new(); + for (path, mut link_file) in link_files { + println!("Fetching from link at path: {}", path.display()); + + // Use git fetch shell command + let output = std::process::Command::new("git") + .args(&["fetch", &link_file.remote, &link_file.branch]) + .output() + .context("Failed to execute git fetch")?; + + if !output.status.success() { + return Err(anyhow::anyhow!( + "git fetch failed for path '{}': {}", + path.display(), + String::from_utf8_lossy(&output.stderr) + )); + } + + // Get the commit SHA from FETCH_HEAD + let fetch_head = repo + .find_reference("FETCH_HEAD") + .context("Failed to find FETCH_HEAD")?; + let fetch_commit = fetch_head + .peel_to_commit() + .context("Failed to get FETCH_HEAD commit")?; + let actual_commit_sha = fetch_commit.id(); + + // Update the link file with the new commit SHA + link_file.commit = Oid::from(actual_commit_sha); + updated_link_files.push((path, link_file)); + } + + // Create new tree with updated .josh-link.toml files + let mut new_tree = head_tree; + for (path, link_file) in &updated_link_files { + // Create the .josh-link.toml content + let link_content = toml::to_string(link_file).context("Failed to serialize link file")?; + + // Create the blob for the .josh-link.toml file + let link_blob = repo + .blob(link_content.as_bytes()) + .context("Failed to create blob")?; + + // Create the path for the .josh-link.toml file + let link_path = path.join(".josh-link.toml"); + + // Insert the updated .josh-link.toml file into the tree + new_tree = tree::insert(&repo, &new_tree, &link_path, link_blob, 0o0100644) + .map_err(from_josh_err) + .with_context(|| { + format!( + "Failed to insert link file into tree at path '{}'", + path.display() + ) + })?; + } + + // Create a new commit with the updated tree + let signature = if let Ok(time) = std::env::var("JOSH_COMMIT_TIME") { + git2::Signature::new( + "JOSH", + "josh@josh-project.dev", + &git2::Time::new(time.parse().context("Failed to parse JOSH_COMMIT_TIME")?, 0), + ) + .context("Failed to create signature")? + } else { + repo.signature().context("Failed to get signature")? + }; + + let new_commit = repo + .commit( + None, // Don't update any reference + &signature, + &signature, + &format!( + "Update links: {}", + updated_link_files + .iter() + .map(|(p, _)| p.display().to_string()) + .collect::>() + .join(", ") + ), + &new_tree, + &[&head_commit], + ) + .context("Failed to create commit")?; + + // Apply the :link filter to the new commit + let link_filter = josh_core::filter::parse(":link") + .map_err(from_josh_err) + .context("Failed to parse :link filter")?; + + // Load the cache and create transaction + let repo_path = repo.path().parent().unwrap(); + josh_core::cache_sled::sled_load(&repo_path).unwrap(); + let cache = std::sync::Arc::new( + josh_core::cache_stack::CacheStack::new() + .with_backend(josh_core::cache_sled::SledCacheBackend::default()) + .with_backend( + josh_core::cache_notes::NotesCacheBackend::new(&repo_path) + .map_err(from_josh_err) + .context("Failed to create NotesCacheBackend")?, + ), + ); + + // Open Josh transaction + let transaction = josh_core::cache::TransactionContext::from_env(cache.clone()) + .map_err(from_josh_err) + .context("Failed TransactionContext::from_env")? + .open(None) + .map_err(from_josh_err) + .context("Failed TransactionContext::open")?; + let filtered_commit = josh_core::filter_commit( + &transaction, + link_filter, + new_commit, + josh_core::filter::empty(), + ) + .map_err(from_josh_err) + .context("Failed to apply :link filter")?; + + // Create the fixed branch name + let branch_name = "refs/heads/josh-link"; + + // Create or update the branch reference + repo.reference(branch_name, filtered_commit, true, "josh link fetch") + .with_context(|| format!("Failed to create branch '{}'", branch_name))?; + + println!("Updated {} link file(s)", updated_link_files.len()); + println!("Created branch: {}", branch_name); + + Ok(()) +} + fn handle_remote(args: &RemoteArgs) -> anyhow::Result<()> { match &args.command { RemoteCommand::Add(add_args) => handle_remote_add(add_args), diff --git a/josh-core/Cargo.toml b/josh-core/Cargo.toml index 250e2e029..0ea0a81a3 100644 --- a/josh-core/Cargo.toml +++ b/josh-core/Cargo.toml @@ -14,8 +14,9 @@ backtrace = "0.3.76" bitvec = "1.0.1" git-version = "0.3.9" git2 = { workspace = true } -gix-object = "0.50.2" -gix-config = "0.46.0" +gix-object = { workspace = true } +gix-config = { workspace = true } +gix-submodule = { workspace = true } gix-hash = { workspace = true } glob = "0.3.1" hex = { workspace = true } @@ -34,3 +35,4 @@ serde_json = { workspace = true } serde_yaml = { workspace = true } sled = "0.34.7" tracing = { workspace = true } +toml = { workspace = true } diff --git a/josh-core/src/filter/grammar.pest b/josh-core/src/filter/grammar.pest index 531368e58..46ef818a0 100644 --- a/josh-core/src/filter/grammar.pest +++ b/josh-core/src/filter/grammar.pest @@ -26,6 +26,7 @@ filter_spec = { ( | filter_rev | filter_from | filter_concat + | filter_unapply | filter_join | filter_replace | filter_squash @@ -73,6 +74,15 @@ filter_concat = { ~ ")" } +filter_unapply = { + CMD_START ~ "unapply" ~ "(" + ~ NEWLINE* + ~ (rev ~ filter_spec)? + ~ (CMD_SEP+ ~ (rev ~ filter_spec))* + ~ NEWLINE* + ~ ")" +} + filter_join = { CMD_START ~ "join" ~ "(" ~ NEWLINE* diff --git a/josh-core/src/filter/mod.rs b/josh-core/src/filter/mod.rs index 4a6769947..b7ad2e48d 100644 --- a/josh-core/src/filter/mod.rs +++ b/josh-core/src/filter/mod.rs @@ -16,12 +16,16 @@ pub use parse::get_comments; pub use parse::parse; static FILTERS: LazyLock>> = - LazyLock::new(|| std::sync::Mutex::new(std::collections::HashMap::new())); + LazyLock::new(|| Default::default()); static WORKSPACES: LazyLock>> = - LazyLock::new(|| std::sync::Mutex::new(std::collections::HashMap::new())); + LazyLock::new(|| Default::default()); static ANCESTORS: LazyLock< std::sync::Mutex>>, -> = LazyLock::new(|| std::sync::Mutex::new(std::collections::HashMap::new())); +> = LazyLock::new(|| Default::default()); + +static LEGALIZED: LazyLock< + std::sync::Mutex>, +> = LazyLock::new(|| Default::default()); /// Match-all regex pattern used as the default for Op::Message when no regex is specified. /// The pattern `(?s)^.*$` matches any string (including newlines) from start to end. @@ -286,6 +290,11 @@ enum Op { Empty, Fold, Paths, + Adapt(String), + Link(String), + Unlink, + Export, + Embed(std::path::PathBuf), // We use BTreeMap rather than HashMap to guarantee deterministic results when // converting to Filter @@ -318,6 +327,7 @@ enum Op { Message(String, regex::Regex), HistoryConcat(LazyRef, Filter), + Unapply(LazyRef, Filter), Compose(Vec), Chain(Filter, Filter), @@ -442,7 +452,19 @@ fn lazy_refs2(op: &Op) -> Vec { av.append(&mut lazy_refs(*b)); av } - Op::Rev(filters) => lazy_refs2(&Op::Join(filters.clone())), + Op::Rev(filters) => { + let mut lr = lazy_refs2(&Op::Compose(filters.values().copied().collect())); + lr.extend(filters.keys().filter_map(|x| { + if let LazyRef::Lazy(s) = x { + Some(s.to_owned()) + } else { + None + } + })); + lr.sort(); + lr.dedup(); + lr + } Op::HistoryConcat(r, f) => { let mut lr = Vec::new(); if let LazyRef::Lazy(s) = r { @@ -451,6 +473,14 @@ fn lazy_refs2(op: &Op) -> Vec { lr.append(&mut lazy_refs(*f)); lr } + Op::Unapply(r, f) => { + let mut lr = Vec::new(); + if let LazyRef::Lazy(s) = r { + lr.push(s.to_owned()); + } + lr.append(&mut lazy_refs(*f)); + lr + } Op::Join(filters) => { let mut lr = lazy_refs2(&Op::Compose(filters.values().copied().collect())); lr.extend(filters.keys().filter_map(|x| { @@ -524,6 +554,19 @@ fn resolve_refs2(refs: &std::collections::HashMap, op: &Op) - }; Op::HistoryConcat(resolved_ref, f) } + Op::Unapply(r, f) => { + let f = resolve_refs(refs, *f); + let resolved_ref = if let LazyRef::Lazy(s) = r { + if let Some(res) = refs.get(s) { + LazyRef::Resolved(*res) + } else { + r.clone() + } + } else { + r.clone() + }; + Op::Unapply(resolved_ref, f) + } Op::Join(filters) => { let lr = filters .iter() @@ -650,6 +693,10 @@ fn spec2(op: &Op) -> String { } Op::Linear => ":linear".to_string(), Op::Unsign => ":unsign".to_string(), + Op::Adapt(adapter) => format!(":adapt={}", adapter), + Op::Link(mode) => format!(":link={}", mode), + Op::Export => ":export".to_string(), + Op::Unlink => ":unlink".to_string(), Op::Subdir(path) => format!(":/{}", parse::quote_if(&path.to_string_lossy())), Op::File(dest_path, source_path) => { if source_path == dest_path { @@ -665,6 +712,9 @@ fn spec2(op: &Op) -> String { Op::Prune => ":prune=trivial-merge".to_string(), Op::Prefix(path) => format!(":prefix={}", parse::quote_if(&path.to_string_lossy())), Op::Pattern(pattern) => format!("::{}", parse::quote_if(pattern)), + Op::Embed(path) => { + format!(":embed={}", parse::quote_if(&path.to_string_lossy()),) + } Op::Author(author, email) => { format!(":author={};{}", parse::quote(author), parse::quote(email)) } @@ -684,6 +734,9 @@ fn spec2(op: &Op) -> String { Op::HistoryConcat(r, filter) => { format!(":concat({}{})", r.to_string(), spec(*filter)) } + Op::Unapply(r, filter) => { + format!(":unapply({}{})", r.to_string(), spec(*filter)) + } Op::Hook(hook) => { format!(":hook={}", parse::quote(hook)) } @@ -757,53 +810,51 @@ fn resolve_workspace_redirect<'a>( } } -fn get_workspace<'a>(repo: &'a git2::Repository, tree: &'a git2::Tree<'a>, path: &Path) -> Filter { +fn get_workspace<'a>( + transaction: &cache::Transaction, + tree: &'a git2::Tree<'a>, + path: &Path, +) -> Filter { let wsj_file = file("workspace.josh"); let base = to_filter(Op::Subdir(path.to_owned())); let wsj_file = chain(base, wsj_file); - compose( + let ws = compose( wsj_file, - compose(get_filter(repo, tree, &path.join("workspace.josh")), base), - ) + compose( + get_filter(transaction, tree, &path.join("workspace.josh")), + base, + ), + ); + ws } -// Handle stored.josh files that contain ":stored=..." as their only filter as -// a "redirect" to that other stored. We chain an exclude of the redirecting stored -// in front to prevent infinite recursion. -fn resolve_stored_redirect<'a>( - repo: &'a git2::Repository, +fn get_stored<'a>( + transaction: &cache::Transaction, tree: &'a git2::Tree<'a>, path: &Path, -) -> Option<(Filter, std::path::PathBuf)> { - let stored_path = path.with_extension("josh"); - let f = parse::parse(&tree::get_blob(repo, tree, &stored_path)) - .unwrap_or_else(|_| to_filter(Op::Empty)); - - if let Op::Stored(p) = to_op(f) { - Some((chain(to_filter(Op::Exclude(file(stored_path))), f), p)) - } else { - None - } -} - -fn get_stored<'a>(repo: &'a git2::Repository, tree: &'a git2::Tree<'a>, path: &Path) -> Filter { +) -> Filter { let stored_path = path.with_extension("josh"); let sj_file = file(stored_path.clone()); - compose(sj_file, get_filter(repo, tree, &stored_path)) + compose(sj_file, get_filter(transaction, tree, &stored_path)) } -fn get_filter<'a>(repo: &'a git2::Repository, tree: &'a git2::Tree<'a>, path: &Path) -> Filter { +fn get_filter<'a>( + transaction: &cache::Transaction, + tree: &'a git2::Tree<'a>, + path: &Path, +) -> Filter { let ws_path = normalize_path(path); let ws_id = ok_or!(tree.get_path(&ws_path), { return to_filter(Op::Empty); }) .id(); - let ws_blob = tree::get_blob(repo, tree, &ws_path); + let ws_blob = tree::get_blob(transaction.repo(), tree, &ws_path); if let Some(f) = WORKSPACES.lock().unwrap().get(&ws_id) { *f } else { let f = parse::parse(&ws_blob).unwrap_or_else(|_| to_filter(Op::Empty)); + let f = legalize_stored(transaction, f, tree); let f = if invert(f).is_ok() { f @@ -1049,6 +1100,160 @@ fn apply_to_commit2( )) .transpose(); } + Op::Export => { + let filtered_parent_ids = { + commit + .parents() + .map(|x| transaction.get(filter, x.id())) + .collect::>() + }; + + let mut filtered_parent_ids: Vec = + some_or!(filtered_parent_ids, { return Ok(None) }); + + // TODO: remove all parents that don't have a .josh-link.toml + + // let mut ok = true; + // filtered_parent_ids.retain(|c| { + // if let Ok(c) = repo.find_commit(*c) { + // c.tree_id() != new_tree.id() + // } else { + // ok = false; + // false + // } + // }); + + // if !ok { + // return Err(josh_error("missing commit")); + // } + + if let Some(link_file) = read_josh_link( + repo, + &commit.tree()?, + &std::path::PathBuf::new(), + ".josh-link.toml", + ) { + if filtered_parent_ids.contains(&link_file.commit.0) { + while filtered_parent_ids[0] != link_file.commit.0 { + filtered_parent_ids.rotate_right(1); + } + } + } + + return Some(history::create_filtered_commit( + commit, + filtered_parent_ids, + apply(transaction, filter, Apply::from_commit(commit)?)?, + transaction, + filter, + )) + .transpose(); + } + Op::Unlink => { + let filtered_parent_ids = { + commit + .parents() + .map(|x| transaction.get(filter, x.id())) + .collect::>() + }; + + let mut filtered_parent_ids: Vec = + some_or!(filtered_parent_ids, { return Ok(None) }); + + let mut link_parents = vec![]; + for (link_path, link_file) in find_link_files(&repo, &commit.tree()?)?.into_iter() { + if let Some(cmt) = + transaction.get(to_filter(Op::Prefix(link_path)), link_file.commit.0) + { + link_parents.push(cmt); + } else { + return Ok(None); + } + } + + let new_tree = apply(transaction, filter, Apply::from_commit(commit)?)?; + + filtered_parent_ids.retain(|c| !link_parents.contains(c)); + + return Some(history::create_filtered_commit( + commit, + filtered_parent_ids, + new_tree, + transaction, + filter, + )) + .transpose(); + } + Op::Link(mode) if mode == "embedded" => { + let normal_parents = commit + .parent_ids() + .map(|parent| transaction.get(filter, parent)) + .collect::>>(); + + let normal_parents = some_or!(normal_parents, { return Ok(None) }); + + let mut roots = get_link_roots(repo, transaction, &commit.tree()?)?; + + if let Some(parent) = commit.parents().next() { + roots.retain(|root| { + if let (Ok(a), Ok(b)) = ( + commit.tree().and_then(|x| x.get_path(&root)), + parent.tree().and_then(|x| x.get_path(&root)), + ) && a.id() == b.id() + { + false + } else { + true + } + }); + }; + + let v = links_from_roots(repo, &commit.tree()?, roots)?; + + let extra_parents = { + let mut extra_parents = vec![]; + for (root, _link_file) in v { + let embeding = some_or!( + apply_to_commit2( + &Op::Chain(message("{commit}"), file(root.join(".josh-link.toml"))), + &commit, + transaction + )?, + { + return Ok(None); + } + ); + + let f = to_filter(Op::Embed(root)); + /* let f = filter::chain(link_file.filter, to_filter(Op::Prefix(root))); */ + /* let scommit = repo.find_commit(link_file.commit.0)?; */ + + let embeding = repo.find_commit(embeding)?; + let r = some_or!(apply_to_commit2(&to_op(f), &embeding, transaction)?, { + return Ok(None); + }); + + extra_parents.push(r); + } + + extra_parents + }; + + let filtered_tree = apply(transaction, filter, Apply::from_commit(commit)?)?; + let filtered_parent_ids = normal_parents + .into_iter() + .chain(extra_parents) + .collect::>(); + + return Some(history::create_filtered_commit( + commit, + filtered_parent_ids.clone(), + filtered_tree, + transaction, + filter, + )) + .transpose(); + } Op::Workspace(ws_path) => { if let Some((redirect, _)) = resolve_workspace_redirect(repo, &commit.tree()?, ws_path) { @@ -1060,14 +1265,14 @@ fn apply_to_commit2( } } - let commit_filter = get_workspace(repo, &commit.tree()?, &ws_path); + let commit_filter = get_workspace(transaction, &commit.tree()?, &ws_path); let parent_filters = commit .parents() .map(|parent| { rs_tracing::trace_scoped!("parent", "id": parent.id().to_string()); let pcw = get_workspace( - repo, + transaction, &parent.tree().unwrap_or_else(|_| tree::empty(repo)), &ws_path, ); @@ -1078,23 +1283,14 @@ fn apply_to_commit2( return per_rev_filter(transaction, commit, filter, commit_filter, parent_filters); } Op::Stored(s_path) => { - if let Some((redirect, _)) = resolve_stored_redirect(repo, &commit.tree()?, s_path) { - if let Some(r) = apply_to_commit2(&to_op(redirect), commit, transaction)? { - transaction.insert(filter, commit.id(), r, true); - return Ok(Some(r)); - } else { - return Ok(None); - } - } - - let commit_filter = get_stored(repo, &commit.tree()?, &s_path); + let commit_filter = get_stored(transaction, &commit.tree()?, &s_path); let parent_filters = commit .parents() .map(|parent| { rs_tracing::trace_scoped!("parent", "id": parent.id().to_string()); let pcs = get_stored( - repo, + transaction, &parent.tree().unwrap_or_else(|_| tree::empty(repo)), &s_path, ); @@ -1139,8 +1335,60 @@ fn apply_to_commit2( return per_rev_filter(transaction, commit, filter, commit_filter, parent_filters); } - Op::HistoryConcat(r, f) => { - if let LazyRef::Resolved(c) = r { + Op::Unapply(target, uf) => { + if let LazyRef::Resolved(target) = target { + /* dbg!(target); */ + let target = repo.find_commit(*target)?; + if let Some(parent) = target.parents().next() { + let ptree = apply(transaction, *uf, Apply::from_commit(&parent)?)?; + if let Some(link) = read_josh_link( + repo, + &ptree.tree(), + &std::path::PathBuf::new(), + ".josh-link.toml", + ) { + if commit.id() == link.commit.0 { + let unapply = + to_filter(Op::Unapply(LazyRef::Resolved(parent.id()), *uf)); + let r = some_or!(transaction.get(unapply, link.commit.0), { + return Ok(None); + }); + transaction.insert(filter, commit.id(), r, true); + return Ok(Some(r)); + } + } + } + } else { + return Err(josh_error("unresolved lazy ref")); + } + /* dbg!("FALLTHROUGH"); */ + apply( + transaction, + filter, + Apply::from_commit(commit)?, /* Apply::from_commit(commit)?.with_parents(filtered_parent_ids), */ + )? + /* Apply::from_commit(commit)? */ + } + Op::Embed(path) => { + let subdir = to_filter(Op::Subdir(path.clone())); + let unapply = to_filter(Op::Unapply(LazyRef::Resolved(commit.id()), subdir)); + + /* dbg!("embed"); */ + /* dbg!(&path); */ + if let Some(link) = read_josh_link(repo, &commit.tree()?, &path, ".josh-link.toml") { + /* dbg!(&link); */ + let r = some_or!(transaction.get(unapply, link.commit.0), { + return Ok(None); + }); + transaction.insert(filter, commit.id(), r, true); + return Ok(Some(r)); + } else { + return Ok(Some(git2::Oid::zero())); + } + } + + Op::HistoryConcat(c, f) => { + if let LazyRef::Resolved(c) = c { let a = apply_to_commit2(&to_op(*f), &repo.find_commit(*c)?, transaction)?; let a = some_or!(a, { return Ok(None) }); if commit.id() == a { @@ -1186,6 +1434,104 @@ pub fn apply<'a>( apply2(transaction, &to_op(filter), x) } +fn extract_submodule_commits<'a>( + repo: &'a git2::Repository, + tree: &git2::Tree<'a>, +) -> JoshResult> { + // Get .gitmodules blob from the tree + let gitmodules_content = tree::get_blob(repo, tree, std::path::Path::new(".gitmodules")); + + if gitmodules_content.is_empty() { + // No .gitmodules file, return empty map + return Ok(std::collections::BTreeMap::new()); + } + + // Parse submodule entries using parse_gitmodules + let submodule_entries = match parse_gitmodules(&gitmodules_content) { + Ok(entries) => entries, + Err(_) => { + // If parsing fails, return empty map + return Ok(std::collections::BTreeMap::new()); + } + }; + + let mut submodule_commits: std::collections::BTreeMap< + std::path::PathBuf, + (git2::Oid, ParsedSubmoduleEntry), + > = std::collections::BTreeMap::new(); + + for parsed in submodule_entries { + let submodule_path = parsed.path.clone(); + // Get the submodule entry from the tree + if let Ok(entry) = tree.get_path(&submodule_path) { + // Check if this is a commit (submodule) entry + if entry.kind() == Some(git2::ObjectType::Commit) { + // Get the commit OID stored in the tree entry + let commit_oid = entry.id(); + // Store OID and parsed entry metadata + submodule_commits.insert(submodule_path, (commit_oid, parsed)); + } + } + } + + Ok(submodule_commits) +} + +fn get_link_roots<'a>( + _repo: &'a git2::Repository, + transaction: &'a cache::Transaction, + tree: &'a git2::Tree<'a>, +) -> JoshResult> { + let link_filter = to_filter(Op::Pattern("**/.josh-link.toml".to_string())); + let link_tree = apply(transaction, link_filter, Apply::from_tree(tree.clone()))?; + + let mut roots = vec![]; + link_tree + .tree() + .walk(git2::TreeWalkMode::PreOrder, |root, entry| { + let root = root.trim_matches('/'); + let root = std::path::PathBuf::from(root); + if entry.name() == Some(".josh-link.toml") { + roots.push(root); + } + 0 + })?; + + Ok(roots) +} + +fn links_from_roots<'a>( + repo: &'a git2::Repository, + tree: &git2::Tree<'a>, + roots: Vec, +) -> JoshResult> { + let mut v = vec![]; + for root in roots { + if let Some(link_file) = read_josh_link(repo, tree, &root, ".josh-link.toml") { + v.push((root, link_file)); + } + } + Ok(v) +} + +fn read_josh_link<'a>( + repo: &'a git2::Repository, + tree: &git2::Tree<'a>, + root: &std::path::Path, + filename: &str, +) -> Option { + let link_path = root.join(filename); + let link_entry = tree.get_path(&link_path).ok()?; + let link_blob = repo.find_blob(link_entry.id()).ok()?; + let b = std::str::from_utf8(link_blob.content()) + .map_err(|e| josh_error(&format!("invalid utf8 in {}: {}", filename, e))) + .ok()?; + let link_file: JoshLinkFile = toml::from_str(b) + .map_err(|e| josh_error(&format!("invalid toml in {}: {}", filename, e))) + .ok()?; + Some(link_file) +} + fn apply2<'a>(transaction: &'a cache::Transaction, op: &Op, x: Apply<'a>) -> JoshResult> { let repo = transaction.repo(); match op { @@ -1220,6 +1566,111 @@ fn apply2<'a>(transaction: &'a cache::Transaction, op: &Op, x: Apply<'a>) -> Jos Op::Linear => Ok(x), Op::Prune => Ok(x), Op::Unsign => Ok(x), + Op::Adapt(adapter) => { + let mut result_tree = x.tree().clone(); + match adapter.as_ref() { + "submodules" => { + // Extract submodule commits + let submodule_commits = extract_submodule_commits(repo, &result_tree)?; + + // Process each submodule commit + for (submodule_path, (commit_oid, meta)) in submodule_commits { + let prefix_filter = to_filter(Op::Nop); + let link_file = JoshLinkFile { + remote: meta.url.clone(), + filter: prefix_filter, + branch: "HEAD".to_string(), + commit: Oid(commit_oid), + }; + result_tree = tree::insert( + repo, + &result_tree, + &submodule_path.join(".josh-link.toml"), + repo.blob(toml::to_string(&link_file)?.as_bytes())?, + 0o0100644, + )?; + } + + // Remove .gitmodules file by setting it to zero OID + result_tree = tree::insert( + repo, + &result_tree, + std::path::Path::new(".gitmodules"), + git2::Oid::zero(), + 0o0100644, + )?; + } + _ => return Err(josh_error(&format!("unknown adapter {:?}", adapter))), + } + + // Remove .gitmodules file by setting it to zero OID + result_tree = tree::insert( + repo, + &result_tree, + std::path::Path::new(".gitmodules"), + git2::Oid::zero(), + 0o0100644, + )?; + + Ok(x.with_tree(result_tree)) + } + Op::Export => { + let tree = x.tree().clone(); + Ok(x.with_tree(tree::insert( + repo, + &tree, + &std::path::Path::new(".josh-link.toml"), + git2::Oid::zero(), + 0o0100644, + )?)) + } + Op::Unlink => { + let mut result_tree = x.tree.clone(); + for (link_path, link_file) in find_link_files(&repo, &result_tree)?.iter() { + result_tree = + tree::insert(repo, &result_tree, &link_path, git2::Oid::zero(), 0o0100644)?; + result_tree = tree::insert( + repo, + &result_tree, + &link_path.join(".josh-link.toml"), + repo.blob(toml::to_string(&link_file)?.as_bytes())?, + 0o0100644, + )?; + } + Ok(x.with_tree(result_tree)) + } + Op::Link(_) => { + let roots = get_link_roots(repo, transaction, &x.tree())?; + let v = links_from_roots(repo, &x.tree(), roots)?; + let mut result_tree = x.tree().clone(); + + for (root, link_file) in v { + let submodule_tree = repo.find_commit(link_file.commit.0)?.tree()?; + let submodule_tree = apply( + transaction, + link_file.filter, + Apply::from_tree(submodule_tree), + ) + .unwrap(); + + result_tree = tree::insert( + repo, + &result_tree, + &root, + submodule_tree.tree().id(), + 0o0040000, // Tree mode + )?; + result_tree = tree::insert( + repo, + &result_tree, + &root.join(".josh-link.toml"), + repo.blob(toml::to_string(&link_file)?.as_bytes())?, + 0o0100644, + )?; + } + + Ok(x.with_tree(result_tree)) + } Op::Rev(_) => Err(josh_error("not applicable to tree")), Op::Join(_) => Err(josh_error("not applicable to tree")), Op::RegexReplace(replacements) => { @@ -1302,8 +1753,8 @@ fn apply2<'a>(transaction: &'a cache::Transaction, op: &Op, x: Apply<'a>) -> Jos .with_tree(tree::invert_paths(transaction, "", x.tree().clone())?)) } - Op::Workspace(path) => apply(transaction, get_workspace(repo, &x.tree(), &path), x), - Op::Stored(path) => apply(transaction, get_stored(repo, &x.tree(), &path), x), + Op::Workspace(path) => apply(transaction, get_workspace(transaction, &x.tree(), &path), x), + Op::Stored(path) => apply(transaction, get_stored(transaction, &x.tree(), &path), x), Op::Compose(filters) => { let filtered: Vec<_> = filters @@ -1322,6 +1773,23 @@ fn apply2<'a>(transaction: &'a cache::Transaction, op: &Op, x: Apply<'a>) -> Jos } Op::Hook(_) => Err(josh_error("not applicable to tree")), + Op::Embed(..) => Err(josh_error("not applicable to tree")), + Op::Unapply(target, uf) => { + if let LazyRef::Resolved(target) = target { + let target = repo.find_commit(*target)?; + let target = git2::Oid::from_str(target.message().unwrap())?; + let target = repo.find_commit(target)?; + /* dbg!(&uf); */ + Ok(Apply::from_tree(filter::unapply( + transaction, + *uf, + x.tree().clone(), + target.tree()?, + )?)) + } else { + return Err(josh_error("unresolved lazy ref")); + } + } Op::Pin(_) => Ok(x), } } @@ -1394,12 +1862,9 @@ fn unapply_workspace<'a>( match op { Op::Workspace(path) => { let tree = pre_process_tree(transaction.repo(), tree)?; - let workspace = get_filter(transaction.repo(), &tree, Path::new("workspace.josh")); - let original_workspace = get_filter( - transaction.repo(), - &parent_tree, - &path.join("workspace.josh"), - ); + let workspace = get_filter(transaction, &tree, Path::new("workspace.josh")); + let original_workspace = + get_filter(transaction, &parent_tree, &path.join("workspace.josh")); let root = to_filter(Op::Subdir(path.to_owned())); let wsj_file = to_filter(Op::File( @@ -1428,8 +1893,8 @@ fn unapply_workspace<'a>( } Op::Stored(path) => { let stored_path = path.with_extension("josh"); - let stored = get_filter(transaction.repo(), &tree, &stored_path); - let original_stored = get_filter(transaction.repo(), &parent_tree, &stored_path); + let stored = get_filter(transaction, &tree, &stored_path); + let original_stored = get_filter(transaction, &parent_tree, &stored_path); let sj_file = file(stored_path.clone()); let filter = compose(sj_file, stored); @@ -1628,6 +2093,58 @@ where } } +fn legalize_stored(t: &cache::Transaction, f: Filter, tree: &git2::Tree) -> Filter { + legalize_stored2(t, f, tree, &mut *LEGALIZED.lock().unwrap()).unwrap_or_else(|_| empty()) +} + +fn legalize_stored2( + t: &cache::Transaction, + f: Filter, + tree: &git2::Tree, + hm: &mut std::collections::HashMap<(Filter, git2::Oid), Filter>, +) -> JoshResult { + if let Some(f) = hm.get(&(f, tree.id())) { + return Ok(*f); + } + + // Put an entry into the hashtable to prevent infinite recursion. + // If we get called with the same arguments again before we return, + // Above check breaks the recursion. + hm.insert((f, tree.id()), empty()); + + let r = match to_op(f) { + Op::Compose(f) => { + let f = f + .into_iter() + .map(|f| legalize_stored2(t, f, tree, hm)) + .collect::>>()?; + to_filter(Op::Compose(f)) + } + Op::Chain(a, b) => { + let first = legalize_stored2(t, a, tree, hm)?; + let second = legalize_stored2( + t, + b, + &apply(t, first, Apply::from_tree(tree.clone()))?.tree, + hm, + )?; + to_filter(Op::Chain(first, second)) + } + Op::Subtract(a, b) => to_filter(Op::Subtract( + legalize_stored2(t, a, tree, hm)?, + legalize_stored2(t, b, tree, hm)?, + )), + Op::Exclude(f) => to_filter(Op::Exclude(legalize_stored2(t, f, tree, hm)?)), + Op::Pin(f) => to_filter(Op::Pin(legalize_stored2(t, f, tree, hm)?)), + Op::Stored(path) => get_stored(t, tree, &path), + _ => f, + }; + + hm.insert((f, tree.id()), r); + + Ok(r) +} + fn per_rev_filter( transaction: &cache::Transaction, commit: &git2::Commit, diff --git a/josh-core/src/filter/opt.rs b/josh-core/src/filter/opt.rs index ee330e7e4..8740845d9 100644 --- a/josh-core/src/filter/opt.rs +++ b/josh-core/src/filter/opt.rs @@ -498,8 +498,10 @@ pub fn invert(filter: Filter) -> JoshResult { Op::Message(..) => Some(Op::Nop), Op::Linear => Some(Op::Nop), Op::Prune => Some(Op::Prune), + Op::Export => Some(Op::Export), Op::Unsign => Some(Op::Unsign), Op::Empty => Some(Op::Empty), + Op::Link(..) => Some(Op::Unlink), Op::Subdir(path) => Some(Op::Prefix(path)), Op::File(dest_path, source_path) => Some(Op::File(source_path, dest_path)), Op::Prefix(path) => Some(Op::Subdir(path)), @@ -529,7 +531,7 @@ pub fn invert(filter: Filter) -> JoshResult { .collect::>>()?, ), Op::Exclude(filter) => Op::Exclude(invert(filter)?), - _ => return Err(josh_error("no invert")), + _ => return Err(josh_error(&format!("no invert {:?}", filter))), }); let result = optimize(result); diff --git a/josh-core/src/filter/parse.rs b/josh-core/src/filter/parse.rs index dae20a118..06b3eb347 100644 --- a/josh-core/src/filter/parse.rs +++ b/josh-core/src/filter/parse.rs @@ -52,6 +52,12 @@ fn make_op(args: &[&str]) -> JoshResult { "# ))), ["unsign"] => Ok(Op::Unsign), + ["unlink"] => Ok(Op::Unlink), + ["adapt", adapter] => Ok(Op::Adapt(adapter.to_string())), + ["link"] => Ok(Op::Link("embedded".to_string())), + ["link", mode] => Ok(Op::Link(mode.to_string())), + ["embed", path] => Ok(Op::Embed(Path::new(path).to_owned())), + ["export"] => Ok(Op::Export), ["PATHS"] => Ok(Op::Paths), ["INDEX"] => Ok(Op::Index), ["INVERT"] => Ok(Op::Invert), @@ -203,6 +209,17 @@ fn parse_item(pair: pest::iterators::Pair) -> JoshResult { Err(josh_error("wrong argument count for :concat")) } } + Rule::filter_unapply => { + let v: Vec<_> = pair.into_inner().map(|x| x.as_str()).collect(); + + if v.len() == 2 { + let oid = LazyRef::parse(v[0])?; + let filter = parse(v[1])?; + Ok(Op::Unapply(oid, filter)) + } else { + Err(josh_error("wrong argument count for :unapply")) + } + } Rule::filter_replace => { let replacements = pair .into_inner() diff --git a/josh-core/src/filter/persist.rs b/josh-core/src/filter/persist.rs index f34f46d85..05a487db5 100644 --- a/josh-core/src/filter/persist.rs +++ b/josh-core/src/filter/persist.rs @@ -190,47 +190,44 @@ impl InMemoryBuilder { push_tree_entries(&mut entries, [("chain", params_tree)]); } Op::Exclude(b) => { - let child = self.build_filter(*b)?; - push_tree_entries(&mut entries, [("exclude", child)]); + let params_tree = self.build_filter_params(&[*b])?; + push_tree_entries(&mut entries, [("exclude", params_tree)]); } Op::Pin(b) => { - let child = self.build_filter(*b)?; - push_tree_entries(&mut entries, [("pin", child)]); + let params_tree = self.build_filter_params(&[*b])?; + push_tree_entries(&mut entries, [("pin", params_tree)]); } Op::Subdir(path) => { - let blob = self.write_blob(path.to_string_lossy().as_bytes()); - push_blob_entries(&mut entries, [("subdir", blob)]); + let params_tree = self.build_str_params(&[path.to_string_lossy().as_ref()]); + push_tree_entries(&mut entries, [("subdir", params_tree)]); } Op::Prefix(path) => { - let blob = self.write_blob(path.to_string_lossy().as_bytes()); - push_blob_entries(&mut entries, [("prefix", blob)]); + let params_tree = self.build_str_params(&[path.to_string_lossy().as_ref()]); + push_tree_entries(&mut entries, [("prefix", params_tree)]); } Op::File(dest_path, source_path) => { - if source_path == dest_path { - // Backward compatibility: use blob format when source and dest are the same - let blob = self.write_blob(dest_path.to_string_lossy().as_bytes()); - push_blob_entries(&mut entries, [("file", blob)]); - } else { - // New format: use tree format when source and dest differ - // Store as (dest_path, source_path) to match enum order - let params_tree = self.build_str_params(&[ - dest_path.to_string_lossy().as_ref(), - source_path.to_string_lossy().as_ref(), - ]); - push_tree_entries(&mut entries, [("file", params_tree)]); - } + // Store as (dest_path, source_path) to match enum order + let params_tree = self.build_str_params(&[ + dest_path.to_string_lossy().as_ref(), + source_path.to_string_lossy().as_ref(), + ]); + push_tree_entries(&mut entries, [("file", params_tree)]); + } + Op::Embed(path) => { + let params_tree = self.build_str_params(&[path.to_string_lossy().as_ref()]); + push_tree_entries(&mut entries, [("embed", params_tree)]); } Op::Pattern(pattern) => { - let blob = self.write_blob(pattern.as_bytes()); - push_blob_entries(&mut entries, [("pattern", blob)]); + let params_tree = self.build_str_params(&[pattern.as_ref()]); + push_tree_entries(&mut entries, [("pattern", params_tree)]); } Op::Workspace(path) => { - let blob = self.write_blob(path.to_string_lossy().as_bytes()); - push_blob_entries(&mut entries, [("workspace", blob)]); + let params_tree = self.build_str_params(&[path.to_string_lossy().as_ref()]); + push_tree_entries(&mut entries, [("workspace", params_tree)]); } Op::Stored(path) => { - let blob = self.write_blob(path.to_string_lossy().as_bytes()); - push_blob_entries(&mut entries, [("stored", blob)]); + let params_tree = self.build_str_params(&[path.to_string_lossy().as_ref()]); + push_tree_entries(&mut entries, [("stored", params_tree)]); } Op::Nop => { let blob = self.write_blob(b""); @@ -240,10 +237,26 @@ impl InMemoryBuilder { let blob = self.write_blob(b""); push_blob_entries(&mut entries, [("empty", blob)]); } + Op::Export => { + let blob = self.write_blob(b""); + push_blob_entries(&mut entries, [("export", blob)]); + } Op::Paths => { let blob = self.write_blob(b""); push_blob_entries(&mut entries, [("paths", blob)]); } + Op::Link(mode) => { + let params_tree = self.build_str_params(&[mode.as_ref()]); + push_tree_entries(&mut entries, [("link", params_tree)]); + } + Op::Adapt(mode) => { + let params_tree = self.build_str_params(&[mode.as_ref()]); + push_tree_entries(&mut entries, [("adapt", params_tree)]); + } + Op::Unlink => { + let blob = self.write_blob(b""); + push_blob_entries(&mut entries, [("unlink", blob)]); + } Op::Invert => { let blob = self.write_blob(b""); push_blob_entries(&mut entries, [("invert", blob)]); @@ -294,6 +307,10 @@ impl InMemoryBuilder { let params_tree = self.build_rev_params(&[(lr.to_string(), *f)])?; push_tree_entries(&mut entries, [("concat", params_tree)]); } + Op::Unapply(lr, f) => { + let params_tree = self.build_rev_params(&[(lr.to_string(), *f)])?; + push_tree_entries(&mut entries, [("unapply", params_tree)]); + } Op::Squash(Some(ids)) => { let mut v = ids .iter() @@ -308,8 +325,8 @@ impl InMemoryBuilder { push_tree_entries(&mut entries, [("regex_replace", params_tree)]); } Op::Hook(hook) => { - let blob = self.write_blob(hook.as_bytes()); - push_blob_entries(&mut entries, [("hook", blob)]); + let params_tree = self.build_str_params(&[hook.as_ref()]); + push_tree_entries(&mut entries, [("hook", params_tree)]); } } @@ -372,6 +389,38 @@ fn from_tree2(repo: &git2::Repository, tree_oid: git2::Oid) -> JoshResult { let _ = repo.find_blob(entry.id())?; Ok(Op::Paths) } + "export" => { + let _ = repo.find_blob(entry.id())?; + Ok(Op::Export) + } + "link" => { + let inner = repo.find_tree(entry.id())?; + let mode_blob = repo.find_blob( + inner + .get_name("0") + .ok_or_else(|| josh_error("link: missing mode"))? + .id(), + )?; + Ok(Op::Link( + std::str::from_utf8(mode_blob.content())?.to_string(), + )) + } + "adapt" => { + let inner = repo.find_tree(entry.id())?; + let mode_blob = repo.find_blob( + inner + .get_name("0") + .ok_or_else(|| josh_error("adapt: missing mode"))? + .id(), + )?; + Ok(Op::Adapt( + std::str::from_utf8(mode_blob.content())?.to_string(), + )) + } + "unlink" => { + let _ = repo.find_blob(entry.id())?; + Ok(Op::Unlink) + } "invert" => { let _ = repo.find_blob(entry.id())?; Ok(Op::Invert) @@ -402,8 +451,14 @@ fn from_tree2(repo: &git2::Repository, tree_oid: git2::Oid) -> JoshResult { } } "hook" => { - let blob = repo.find_blob(entry.id())?; - let hook_name = std::str::from_utf8(blob.content())?.to_string(); + let inner = repo.find_tree(entry.id())?; + let hook_blob = repo.find_blob( + inner + .get_name("0") + .ok_or_else(|| josh_error("hook: missing hook name"))? + .id(), + )?; + let hook_name = std::str::from_utf8(hook_blob.content())?.to_string(); Ok(Op::Hook(hook_name)) } "author" => { @@ -463,61 +518,90 @@ fn from_tree2(repo: &git2::Repository, tree_oid: git2::Oid) -> JoshResult { Ok(Op::Message(fmt, regex)) } "subdir" => { - let blob = repo.find_blob(entry.id())?; - let path = std::str::from_utf8(blob.content())?; + let inner = repo.find_tree(entry.id())?; + let path_blob = repo.find_blob( + inner + .get_name("0") + .ok_or_else(|| josh_error("subdir: missing path"))? + .id(), + )?; + let path = std::str::from_utf8(path_blob.content())?; Ok(Op::Subdir(std::path::PathBuf::from(path))) } "prefix" => { - let blob = repo.find_blob(entry.id())?; - let path = std::str::from_utf8(blob.content())?; + let inner = repo.find_tree(entry.id())?; + let path_blob = repo.find_blob( + inner + .get_name("0") + .ok_or_else(|| josh_error("prefix: missing path"))? + .id(), + )?; + let path = std::str::from_utf8(path_blob.content())?; Ok(Op::Prefix(std::path::PathBuf::from(path))) } "file" => { - // Try to read as tree (new format with destination path) - if let Ok(inner) = repo.find_tree(entry.id()) { - let dest_blob = repo.find_blob( - inner - .get_name("0") - .ok_or_else(|| josh_error("file: missing destination path"))? - .id(), - )?; - let dest_path_str = std::str::from_utf8(dest_blob.content())?.to_string(); - let source_path = inner + let inner = repo.find_tree(entry.id())?; + let dest_blob = repo.find_blob( + inner + .get_name("0") + .ok_or_else(|| josh_error("file: missing destination path"))? + .id(), + )?; + let source_blob = repo.find_blob( + inner .get_name("1") - .and_then(|entry| repo.find_blob(entry.id()).ok()) - .and_then(|blob| { - std::str::from_utf8(blob.content()) - .ok() - .map(|s| s.to_string()) - }) - .map(|s| std::path::PathBuf::from(s)) - .unwrap_or_else(|| std::path::PathBuf::from(&dest_path_str)); - Ok(Op::File( - std::path::PathBuf::from(dest_path_str), - source_path, - )) - } else { - // Fall back to blob format (old format, backward compatibility) - let blob = repo.find_blob(entry.id())?; - let path_str = std::str::from_utf8(blob.content())?.to_string(); - let path_buf = std::path::PathBuf::from(&path_str); - // When reading from blob format, destination is the same as source - Ok(Op::File(path_buf.clone(), path_buf)) - } + .ok_or_else(|| josh_error("file: missing source path"))? + .id(), + )?; + let dest_path_str = std::str::from_utf8(dest_blob.content())?.to_string(); + let source_path_str = std::str::from_utf8(source_blob.content())?.to_string(); + Ok(Op::File( + std::path::PathBuf::from(dest_path_str), + std::path::PathBuf::from(source_path_str), + )) + } + "embed" => { + let inner = repo.find_tree(entry.id())?; + let path_blob = repo.find_blob( + inner + .get_name("0") + .ok_or_else(|| josh_error("embed: missing path"))? + .id(), + )?; + let path = std::str::from_utf8(path_blob.content())?; + Ok(Op::Embed(std::path::PathBuf::from(path))) } "pattern" => { - let blob = repo.find_blob(entry.id())?; - let pattern = std::str::from_utf8(blob.content())?.to_string(); + let inner = repo.find_tree(entry.id())?; + let pattern_blob = repo.find_blob( + inner + .get_name("0") + .ok_or_else(|| josh_error("pattern: missing pattern"))? + .id(), + )?; + let pattern = std::str::from_utf8(pattern_blob.content())?.to_string(); Ok(Op::Pattern(pattern)) } "workspace" => { - let blob = repo.find_blob(entry.id())?; - let path = std::str::from_utf8(blob.content())?; + let inner = repo.find_tree(entry.id())?; + let path_blob = repo.find_blob( + inner + .get_name("0") + .ok_or_else(|| josh_error("workspace: missing path"))? + .id(), + )?; + let path = std::str::from_utf8(path_blob.content())?; Ok(Op::Workspace(std::path::PathBuf::from(path))) } "stored" => { - let blob = repo.find_blob(entry.id())?; - let path = std::str::from_utf8(blob.content())?; + let inner = repo.find_tree(entry.id())?; + let path_blob = repo.find_blob( + inner + .get_name("0") + .ok_or_else(|| josh_error("stored: missing path"))? + .id(), + )?; + let path = std::str::from_utf8(path_blob.content())?; Ok(Op::Stored(std::path::PathBuf::from(path))) } "compose" => { @@ -579,13 +663,33 @@ fn from_tree2(repo: &git2::Repository, tree_oid: git2::Oid) -> JoshResult { } "exclude" => { let exclude_tree = repo.find_tree(entry.id())?; - let filter = from_tree2(repo, exclude_tree.id())?; - Ok(Op::Exclude(to_filter(filter))) + if exclude_tree.len() == 1 { + let filter_tree = repo.find_tree( + exclude_tree + .get_name("0") + .ok_or_else(|| josh_error("exclude: missing 0"))? + .id(), + )?; + let filter = from_tree2(repo, filter_tree.id())?; + Ok(Op::Exclude(to_filter(filter))) + } else { + Err(josh_error("exclude: expected 1 entry")) + } } "pin" => { let pin_tree = repo.find_tree(entry.id())?; - let filter = from_tree2(repo, pin_tree.id())?; - Ok(Op::Pin(to_filter(filter))) + if pin_tree.len() == 1 { + let filter_tree = repo.find_tree( + pin_tree + .get_name("0") + .ok_or_else(|| josh_error("pin: missing 0"))? + .id(), + )?; + let filter = from_tree2(repo, filter_tree.id())?; + Ok(Op::Pin(to_filter(filter))) + } else { + Err(josh_error("pin: expected 1 entry")) + } } "rev" => { let rev_tree = repo.find_tree(entry.id())?; @@ -661,6 +765,28 @@ fn from_tree2(repo: &git2::Repository, tree_oid: git2::Oid) -> JoshResult { let filter = from_tree2(repo, filter_tree.id())?; Ok(Op::HistoryConcat(LazyRef::parse(&key)?, to_filter(filter))) } + "unapply" => { + let concat_tree = repo.find_tree(entry.id())?; + let entry = concat_tree + .get(0) + .ok_or_else(|| josh_error("concat: missing entry"))?; + let inner_tree = repo.find_tree(entry.id())?; + let key_blob = repo.find_blob( + inner_tree + .get_name("o") + .ok_or_else(|| josh_error("concat: missing key"))? + .id(), + )?; + let filter_tree = repo.find_tree( + inner_tree + .get_name("f") + .ok_or_else(|| josh_error("concat: missing filter"))? + .id(), + )?; + let key = std::str::from_utf8(key_blob.content())?.to_string(); + let filter = from_tree2(repo, filter_tree.id())?; + Ok(Op::Unapply(LazyRef::parse(&key)?, to_filter(filter))) + } "squash" => { // blob -> Squash(None), tree -> Squash(Some(...)) if let Some(kind) = entry.kind() { diff --git a/josh-core/src/lib.rs b/josh-core/src/lib.rs index 6f9c08bf0..17affad3b 100644 --- a/josh-core/src/lib.rs +++ b/josh-core/src/lib.rs @@ -408,3 +408,368 @@ pub fn get_acl( }) .unwrap_or_else(|| Ok((filter::empty(), filter::nop()))) } + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct JoshLinkFile { + pub remote: String, + pub branch: String, + pub filter: filter::Filter, + pub commit: Oid, +} + +pub struct ParsedSubmoduleEntry { + pub path: std::path::PathBuf, + pub url: String, + pub branch: String, +} + +pub fn parse_gitmodules(gitmodules_content: &str) -> JoshResult> { + use gix_submodule::File; + + let submodules = File::from_bytes(gitmodules_content.as_bytes(), None, &Default::default()) + .map_err(|e| josh_error(&format!("Failed to parse .gitmodules: {}", e)))?; + + let mut entries: Vec = Vec::new(); + + for name in submodules.names() { + // path is required to consider an entry + if let Ok(path) = submodules.path(name) { + let path = std::path::PathBuf::from(path.to_string()); + + let url = submodules + .url(name) + .ok() + .map(|u| u.to_string()) + .unwrap_or_default(); + + // Default branch to "HEAD" if not configured + let branch = submodules + .branch(name) + .ok() + .and_then(|opt| { + opt.map(|b| match b { + gix_submodule::config::Branch::CurrentInSuperproject => ".".to_string(), + gix_submodule::config::Branch::Name(n) => n.to_string(), + }) + }) + .unwrap_or_else(|| "HEAD".to_string()); + + entries.push(ParsedSubmoduleEntry { path, url, branch }); + } + } + + Ok(entries) +} + +pub fn update_gitmodules( + gitmodules_content: &str, + entry: &ParsedSubmoduleEntry, +) -> JoshResult { + use gix_config::File as ConfigFile; + use gix_submodule::File as SubmoduleFile; + + // Parse the existing gitmodules content using gix_submodule + let submodule_file = SubmoduleFile::from_bytes( + gitmodules_content.as_bytes(), + None, + &ConfigFile::new(gix_config::file::Metadata::default()), + ) + .map_err(|e| josh_error(&format!("Failed to parse .gitmodules: {}", e)))?; + + // Get the underlying config file to modify it + let mut config = submodule_file.config().clone(); + + // Find the existing submodule by matching the path + let mut existing_submodule_name = None; + for name in submodule_file.names() { + if let Ok(path) = submodule_file.path(name) { + if path.to_string() == entry.path.to_string_lossy() { + existing_submodule_name = Some(name.to_string()); + break; + } + } + } + + let submodule_name = if let Some(name) = existing_submodule_name { + // Use the existing submodule name + name + } else { + // Create a new submodule name from path (fallback) + entry.path.to_string_lossy().replace('/', "_") + }; + + // Create or update the submodule section + let mut section = config + .section_mut_or_create_new("submodule", Some(submodule_name.as_str().into())) + .map_err(|e| josh_error(&format!("Failed to create submodule section: {}", e)))?; + + // Set the submodule properties using push method + section.push( + "path".try_into().unwrap(), + Some(entry.path.to_string_lossy().as_ref().into()), + ); + section.push("url".try_into().unwrap(), Some(entry.url.as_str().into())); + if entry.branch != "HEAD" { + section.push( + "branch".try_into().unwrap(), + Some(entry.branch.as_str().into()), + ); + } + + // Write the updated config back to string + let mut output = Vec::new(); + config + .write_to(&mut output) + .map_err(|e| josh_error(&format!("Failed to write gitmodules: {}", e)))?; + + String::from_utf8(output) + .map_err(|e| josh_error(&format!("Invalid UTF-8 in gitmodules: {}", e))) +} + +pub fn find_link_files( + repo: &git2::Repository, + tree: &git2::Tree, +) -> JoshResult> { + let mut link_files = Vec::new(); + + tree.walk(git2::TreeWalkMode::PreOrder, |root, entry| { + if let Some(name) = entry.name() { + if name == ".josh-link.toml" { + // Found a link file + let link_blob = match repo.find_blob(entry.id()) { + Ok(blob) => blob, + Err(e) => { + eprintln!("Failed to find blob: {}", e); + return git2::TreeWalkResult::Skip; + } + }; + + let link_content = match std::str::from_utf8(link_blob.content()) { + Ok(content) => content, + Err(e) => { + eprintln!("Failed to parse link file content: {}", e); + return git2::TreeWalkResult::Skip; + } + }; + + let link_file: JoshLinkFile = match toml::from_str(link_content) { + Ok(file) => file, + Err(e) => { + eprintln!("Failed to parse .josh-link.toml: {}", e); + return git2::TreeWalkResult::Skip; + } + }; + + let root = root.trim_matches('/'); + // Use root as the directory path where the .josh-link.toml file is located + let path = std::path::PathBuf::from(root); + + link_files.push((path, link_file)); + } + } + + git2::TreeWalkResult::Ok + }) + .map_err(|e| josh_error(&format!("Failed to walk tree: {}", e)))?; + + Ok(link_files) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::path::PathBuf; + + #[test] + fn test_parse_gitmodules_basic() { + let content = r#"[submodule "libs/foo"] + path = libs/foo + url = https://github.com/example/foo.git + branch = main + +[submodule "libs/bar"] + path = libs/bar + url = https://github.com/example/bar.git"#; + + let result = parse_gitmodules(content).unwrap(); + assert_eq!(result.len(), 2); + + assert_eq!(result[0].path, PathBuf::from("libs/foo")); + assert_eq!(result[0].url, "https://github.com/example/foo.git"); + assert_eq!(result[0].branch, "main"); + + assert_eq!(result[1].path, PathBuf::from("libs/bar")); + assert_eq!(result[1].url, "https://github.com/example/bar.git"); + assert_eq!(result[1].branch, "HEAD"); // default + } + + #[test] + fn test_parse_gitmodules_empty() { + let content = ""; + let result = parse_gitmodules(content).unwrap(); + assert_eq!(result.len(), 0); + } + + #[test] + fn test_parse_gitmodules_invalid() { + let content = "invalid gitmodules content"; + let result = parse_gitmodules(content); + assert!(result.is_err()); + } + + #[test] + fn test_update_gitmodules_add_new() { + let content = ""; + let entry = ParsedSubmoduleEntry { + path: PathBuf::from("libs/foo"), + url: "https://github.com/example/foo.git".to_string(), + branch: "main".to_string(), + }; + + let result = update_gitmodules(content, &entry).unwrap(); + + let expected = r#"[submodule "libs_foo"] + path = libs/foo + url = https://github.com/example/foo.git + branch = main +"#; + assert_eq!(result, expected); + } + + #[test] + fn test_update_gitmodules_add_new_with_default_branch() { + let content = ""; + let entry = ParsedSubmoduleEntry { + path: PathBuf::from("libs/bar"), + url: "https://github.com/example/bar.git".to_string(), + branch: "HEAD".to_string(), + }; + + let result = update_gitmodules(content, &entry).unwrap(); + + let expected = r#"[submodule "libs_bar"] + path = libs/bar + url = https://github.com/example/bar.git +"#; + assert_eq!(result, expected); + } + + #[test] + fn test_update_gitmodules_update_existing() { + let content = r#"[submodule "existing_foo"] + path = libs/foo + url = https://github.com/example/old-foo.git + branch = old-branch"#; + + let entry = ParsedSubmoduleEntry { + path: PathBuf::from("libs/foo"), + url: "https://github.com/example/new-foo.git".to_string(), + branch: "new-branch".to_string(), + }; + + let result = update_gitmodules(content, &entry).unwrap(); + + // The gix-config API appends values instead of replacing them + let expected = r#"[submodule "existing_foo"] + path = libs/foo + url = https://github.com/example/old-foo.git + branch = old-branch +path = libs/foo + url = https://github.com/example/new-foo.git + branch = new-branch +"#; + assert_eq!(result, expected); + } + + #[test] + fn test_update_gitmodules_update_existing_with_default_branch() { + let content = r#"[submodule "existing_bar"] + path = libs/bar + url = https://github.com/example/old-bar.git + branch = old-branch"#; + + let entry = ParsedSubmoduleEntry { + path: PathBuf::from("libs/bar"), + url: "https://github.com/example/new-bar.git".to_string(), + branch: "HEAD".to_string(), + }; + + let result = update_gitmodules(content, &entry).unwrap(); + + // The gix-config API appends values instead of replacing them + let expected = r#"[submodule "existing_bar"] + path = libs/bar + url = https://github.com/example/old-bar.git + branch = old-branch +path = libs/bar + url = https://github.com/example/new-bar.git +"#; + assert_eq!(result, expected); + } + + #[test] + fn test_update_gitmodules_multiple_submodules() { + let content = r#"[submodule "existing_foo"] + path = libs/foo + url = https://github.com/example/foo.git + +[submodule "existing_bar"] + path = libs/bar + url = https://github.com/example/bar.git"#; + + let entry = ParsedSubmoduleEntry { + path: PathBuf::from("libs/baz"), + url: "https://github.com/example/baz.git".to_string(), + branch: "develop".to_string(), + }; + + let result = update_gitmodules(content, &entry).unwrap(); + + // The gix-config API appends new sections at the end + let expected = r#"[submodule "existing_foo"] + path = libs/foo + url = https://github.com/example/foo.git + +[submodule "existing_bar"] + path = libs/bar + url = https://github.com/example/bar.git +[submodule "libs_baz"] + path = libs/baz + url = https://github.com/example/baz.git + branch = develop +"#; + assert_eq!(result, expected); + } + + #[test] + fn test_update_gitmodules_path_with_slashes() { + let content = ""; + let entry = ParsedSubmoduleEntry { + path: PathBuf::from("deep/nested/path/submodule"), + url: "https://github.com/example/deep-submodule.git".to_string(), + branch: "main".to_string(), + }; + + let result = update_gitmodules(content, &entry).unwrap(); + + let expected = r#"[submodule "deep_nested_path_submodule"] + path = deep/nested/path/submodule + url = https://github.com/example/deep-submodule.git + branch = main +"#; + assert_eq!(result, expected); + } + + #[test] + fn test_update_gitmodules_invalid_content() { + let content = "invalid gitmodules content"; + let entry = ParsedSubmoduleEntry { + path: PathBuf::from("libs/foo"), + url: "https://github.com/example/foo.git".to_string(), + branch: "main".to_string(), + }; + + let result = update_gitmodules(content, &entry); + assert!(result.is_err()); + } +} diff --git a/josh-filter/src/bin/josh-filter.rs b/josh-filter/src/bin/josh-filter.rs index 20cd5e682..73ddcdd92 100644 --- a/josh-filter/src/bin/josh-filter.rs +++ b/josh-filter/src/bin/josh-filter.rs @@ -171,16 +171,25 @@ fn run_filter(args: Vec) -> josh_core::JoshResult { return Ok(0); } let specstr = args.get_one::("filter").unwrap(); + let is_from_file = args.get_one::("file").is_some(); let specstr = args .get_one::("file") .and_then(|f| read_to_string(f).ok()) .unwrap_or(specstr.to_string()); - let mut filterobj = josh_core::filter::parse(&specstr)?; + let repo = git2::Repository::open_from_env()?; + let repo_path = repo.path().to_path_buf(); - let repo_path = { - let repo = git2::Repository::open_from_env()?; - repo.path().to_path_buf() + // If the filter spec doesn't contain a colon and it's not from a file, + // treat it as a SHA and read from tree + let mut filterobj = if specstr.contains(':') || is_from_file { + josh_core::filter::parse(&specstr)? + } else { + // Try to parse as SHA and read filter from tree + let tree_oid = git2::Oid::from_str(&specstr.trim()).map_err(|_| { + josh_core::josh_error(&format!("Invalid filter spec or SHA: {}", specstr)) + })?; + josh_core::filter::persist::from_tree(&repo, tree_oid)? }; if !args.get_flag("no-cache") { diff --git a/tests/cli/link-add.t b/tests/cli/link-add.t new file mode 100644 index 000000000..c3702d3c5 --- /dev/null +++ b/tests/cli/link-add.t @@ -0,0 +1,288 @@ + $ export TESTTMP=${PWD} + +# Create a test repository + $ mkdir -p remote + $ cd remote + $ git init -q + $ git config user.name "Test User" + $ git config user.email "test@example.com" + +# Create some content + $ mkdir -p libs utils docs + $ echo "library code" > libs/lib1.txt + $ echo "utility code" > utils/util1.txt + $ echo "documentation" > docs/readme.txt + $ git add . + $ git commit -m "Initial commit" + [master (root-commit) *] Initial commit (glob) + 3 files changed, 3 insertions(+) + create mode 100644 docs/readme.txt + create mode 100644 libs/lib1.txt + create mode 100644 utils/util1.txt + + $ cd .. + +# Create a bare repository for linking + $ git clone --bare remote remote.git + Cloning into bare repository 'remote.git'... + done. + $ cd ${TESTTMP} + +# Create a new repository to test link add + $ git init test-repo + Initialized empty Git repository in * (glob) + $ cd test-repo + $ git config user.name "Test User" + $ git config user.email "test@example.com" + + $ which git + /opt/git-install/bin/git + +# Create an initial commit so we have a HEAD + $ echo "initial content" > README.md + $ git add README.md + $ git commit -m "Initial commit" + [master (root-commit) 3eb5c75] Initial commit + 1 file changed, 1 insertion(+) + create mode 100644 README.md + +# Test basic link add with default filter and target + $ josh link add libs ../remote.git + Added link 'libs' with URL '*', filter ':/', and target 'HEAD' (glob) + Created branch: refs/heads/josh-link + +# Verify the branch was created + $ git show-ref | grep refs/heads/josh-link + d92220053f9cc43c012995d1ca1f691fb6a374fa refs/heads/josh-link + +# Verify HEAD was not updated + $ git log --oneline + * Initial commit (glob) + +# Check the content of the link branch + $ git checkout refs/heads/josh-link + Note: switching to 'refs/heads/josh-link'. + + You are in 'detached HEAD' state. You can look around, make experimental + changes and commit them, and you can discard any commits you make in this + state without impacting any branches by switching back to a branch. + + If you want to create a new branch to retain commits you create, you may + do so (now or later) by using -c with the switch command. Example: + + git switch -c + + Or undo this operation with: + + git switch - + + Turn off this advice by setting config variable advice.detachedHead to false + + HEAD is now at d922200 Add link: libs + $ git ls-tree -r HEAD + 100644 blob f2376e2bab6c5194410bd8a55630f83f933d2f34\tREADME.md (esc) + 100644 blob 206d76fad48424fec1fface3ad37d1c24e5eba3a\tlibs/.josh-link.toml (esc) + 100644 blob dfcaa10d372d874e1cab9c3ba8d0b683099c3826\tlibs/docs/readme.txt (esc) + 100644 blob abe06153eb1e2462265336768a6ecd1164f73ae2\tlibs/libs/lib1.txt (esc) + 100644 blob f03a884ed41c1a40b529001c0b429eed24c5e9e5\tlibs/utils/util1.txt (esc) + $ cat libs/.josh-link.toml + remote = "../remote.git" + branch = "HEAD" + filter = ":/" + commit = "d27fa3a10cc019e6aa55fc74c1f0893913380e2d" + + $ git checkout master + Previous HEAD position was d922200 Add link: libs + Switched to branch 'master' + +# Test link add with custom filter and target + $ josh link add utils ../remote.git :/utils --target master + Added link 'utils' with URL '*', filter ':/utils', and target 'master' (glob) + Created branch: refs/heads/josh-link + +# Verify the branch was created + $ git show-ref | grep refs/heads/josh-link + 361928abbc41f83a6f6ef33dc414dfcf9b257a96 refs/heads/josh-link + +# Check the content of the utils link branch + $ git checkout refs/heads/josh-link + Note: switching to 'refs/heads/josh-link'. + + You are in 'detached HEAD' state. You can look around, make experimental + changes and commit them, and you can discard any commits you make in this + state without impacting any branches by switching back to a branch. + + If you want to create a new branch to retain commits you create, you may + do so (now or later) by using -c with the switch command. Example: + + git switch -c + + Or undo this operation with: + + git switch - + + Turn off this advice by setting config variable advice.detachedHead to false + + HEAD is now at 361928a Add link: utils + $ cat utils/.josh-link.toml + remote = "../remote.git" + branch = "master" + filter = ":/utils" + commit = "d27fa3a10cc019e6aa55fc74c1f0893913380e2d" + + $ git checkout master + Previous HEAD position was 361928a Add link: utils + Switched to branch 'master' + +# Test path normalization (path with leading slash) + $ josh link add /docs ../remote.git :/docs + Added link 'docs' with URL '*', filter ':/docs', and target 'HEAD' (glob) + Created branch: refs/heads/josh-link + +# Verify path was normalized (no leading slash in branch name) + $ git show-ref | grep refs/heads/josh-link + 0f959dafbb6690a168f83f2b86fb7929a5391b85 refs/heads/josh-link + + +# Test error case - empty path + $ josh link add "" ../remote.git + Error: Path cannot be empty + Path cannot be empty + [1] + +# Test error case - not in a git repository + $ cd .. + $ josh link add test ../remote.git + Error: Not in a git repository + Not in a git repository + could not find repository at '.'; class=Repository (6); code=NotFound (-3) + [1] + + $ cd test-repo + +# Verify that no git remotes were created (josh link add should not create remotes) + $ git remote -v + +# Verify that no git config entries were created (josh link add should not modify .git/config) + $ git config --list | grep josh-link + [1] + +# Test help output + $ josh link --help + Manage josh links (like `josh remote` but for links) + + Usage: josh link + + Commands: + add Add a link with optional filter and target branch + fetch Fetch from existing link files + help Print this message or the help of the given subcommand(s) + + Options: + -h, --help Print help + + $ josh link add --help + Add a link with optional filter and target branch + + Usage: josh link add [OPTIONS] [FILTER] + + Arguments: + Path where the link will be mounted + Remote repository URL + [FILTER] Optional filter to apply to the linked repository + + Options: + --target Target branch to link (defaults to HEAD) + -h, --help Print help + +# Test josh link fetch command +# First, create a link file directly in the master branch for testing + $ mkdir -p test-link + $ echo 'remote = "../remote.git"' > test-link/.josh-link.toml + $ echo 'branch = "HEAD"' >> test-link/.josh-link.toml + $ echo 'filter = ":/test"' >> test-link/.josh-link.toml + $ echo 'commit = "d27fa3a10cc019e6aa55fc74c1f0893913380e2d"' >> test-link/.josh-link.toml + $ git add test-link/.josh-link.toml + $ git commit -m "Add test link file for fetch testing" + [master *] Add test link file for fetch testing (glob) + 1 file changed, 4 insertions(+) + create mode 100644 test-link/.josh-link.toml + +# Test fetch with specific path + $ josh link fetch test-link + Found 1 link file(s) to fetch + Fetching from link at path: test-link + Updated 1 link file(s) + Created branch: refs/heads/josh-link + +# Verify the branch was updated + $ git show-ref | grep refs/heads/josh-link + 82aa51e68542366219191a2a25fefbb6ed6e57a0 refs/heads/josh-link + +# Check the updated content + $ git checkout refs/heads/josh-link + Note: switching to 'refs/heads/josh-link'. + + You are in 'detached HEAD' state. You can look around, make experimental + changes and commit them, and you can discard any commits you make in this + state without impacting any branches by switching back to a branch. + + If you want to create a new branch to retain commits you create, you may + do so (now or later) by using -c with the switch command. Example: + + git switch -c + + Or undo this operation with: + + git switch - + + Turn off this advice by setting config variable advice.detachedHead to false + + HEAD is now at 82aa51e Update links: test-link + $ git ls-tree -r HEAD + 100644 blob f2376e2bab6c5194410bd8a55630f83f933d2f34 README.md (esc) + 100644 blob bd917a0bed306891ca07801e3d89b9140954434f test-link/.josh-link.toml (esc) + $ cat test-link/.josh-link.toml + remote = "../remote.git" + branch = "HEAD" + filter = ":/test" + commit = "d27fa3a10cc019e6aa55fc74c1f0893913380e2d" + + $ git checkout master + Previous HEAD position was 82aa51e Update links: test-link + Switched to branch 'master' + +# Test fetch with no path (should find all .josh-link.toml files) + $ josh link fetch + Found 1 link file(s) to fetch + Fetching from link at path: test-link + Updated 1 link file(s) + Created branch: refs/heads/josh-link + +# Test error case - path that doesn't exist + $ josh link fetch nonexistent + Error: Failed to find .josh-link.toml at path 'nonexistent' + Failed to find .josh-link.toml at path 'nonexistent' + the path 'nonexistent' does not exist in the given tree; class=Tree (14); code=NotFound (-3) + [1] + +# Test error case - no link files found + $ cd .. + $ git init empty-repo + Initialized empty Git repository in * (glob) + $ cd empty-repo + $ git config user.name "Test User" + $ git config user.email "test@example.com" + $ echo "initial content" > README.md + $ git add README.md + $ git commit -m "Initial commit" + [master (root-commit) 3eb5c75] Initial commit + 1 file changed, 1 insertion(+) + create mode 100644 README.md + + $ josh link fetch + Error: No .josh-link.toml files found + No .josh-link.toml files found + [1] + + $ cd .. diff --git a/tests/filter/filter_id.t b/tests/filter/filter_id.t index e7c85059c..535865520 100644 --- a/tests/filter/filter_id.t +++ b/tests/filter/filter_id.t @@ -3,36 +3,42 @@ $ git init -q $ git commit -q --allow-empty -m "empty" - $ josh-filter -i :[:/a,:/b] - 6777de6ebd97608a3c9fbd8e39ad50cb52b4ae89 - $ git read-tree --reset -u 046b1982b3fa906076d7fce31acf11f19ab4c4c3 + $ FILTER_HASH=$(josh-filter -i :[:/a,:/b]) + $ josh-filter -p ${FILTER_HASH} + :[ + :/a + :/b + ] + $ git read-tree --reset -u ${FILTER_HASH} $ find . -type f -not -path './.git/*' -exec echo "-- {}" \; -exec cat {} \; - -- ./subdir - a (no-eol) - $ git read-tree --reset -u 6777de6ebd97608a3c9fbd8e39ad50cb52b4ae89 + -- ./compose/0/subdir/0 + a-- ./compose/1/subdir/0 + b (no-eol) $ tree . `-- compose |-- 0 | `-- subdir + | `-- 0 `-- 1 `-- subdir + `-- 0 - 4 directories, 2 files - $ git diff ${EMPTY_TREE}..6777de6ebd97608a3c9fbd8e39ad50cb52b4ae89 - diff --git a/compose/0/subdir b/compose/0/subdir + 6 directories, 2 files + $ git diff ${EMPTY_TREE}..${FILTER_HASH} + diff --git a/compose/0/subdir/0 b/compose/0/subdir/0 new file mode 100644 index 0000000..2e65efe --- /dev/null - +++ b/compose/0/subdir + +++ b/compose/0/subdir/0 @@ -0,0 +1 @@ +a \ No newline at end of file - diff --git a/compose/1/subdir b/compose/1/subdir + diff --git a/compose/1/subdir/0 b/compose/1/subdir/0 new file mode 100644 index 0000000..63d8dbd --- /dev/null - +++ b/compose/1/subdir + +++ b/compose/1/subdir/0 @@ -0,0 +1 @@ +b \ No newline at end of file @@ -54,92 +60,103 @@ :/a $ josh-filter --reverse -p :[:empty,:/a] :prefix=a - $ josh-filter -i :[x=:/a:/b:/d,y=:/a:/c:/d] - 9cac689eff79c3a65966083286840d7ea913e918 - $ git read-tree --reset -u 9cac689eff79c3a65966083286840d7ea913e918 + $ FILTER_HASH=$(josh-filter -i :[x=:/a:/b:/d,y=:/a:/c:/d]) + $ josh-filter -p ${FILTER_HASH} + :/a:[ + x = :/b/d + y = :/c/d + ] + $ git read-tree --reset -u ${FILTER_HASH} $ tree . `-- chain |-- 0 | `-- subdir + | `-- 0 `-- 1 `-- compose |-- 0 | `-- chain | |-- 0 | | `-- subdir + | | `-- 0 | `-- 1 | `-- chain | |-- 0 | | `-- subdir + | | `-- 0 | `-- 1 | `-- prefix + | `-- 0 `-- 1 `-- chain |-- 0 | `-- subdir + | `-- 0 `-- 1 `-- chain |-- 0 | `-- subdir + | `-- 0 `-- 1 `-- prefix + `-- 0 - 19 directories, 7 files - $ git diff ${EMPTY_TREE}..9cac689eff79c3a65966083286840d7ea913e918 - diff --git a/chain/0/subdir b/chain/0/subdir + 26 directories, 7 files + $ git diff ${EMPTY_TREE}..${FILTER_HASH} + diff --git a/chain/0/subdir/0 b/chain/0/subdir/0 new file mode 100644 index 0000000..2e65efe --- /dev/null - +++ b/chain/0/subdir + +++ b/chain/0/subdir/0 @@ -0,0 +1 @@ +a \ No newline at end of file - diff --git a/chain/1/compose/0/chain/0/subdir b/chain/1/compose/0/chain/0/subdir + diff --git a/chain/1/compose/0/chain/0/subdir/0 b/chain/1/compose/0/chain/0/subdir/0 new file mode 100644 index 0000000..63d8dbd --- /dev/null - +++ b/chain/1/compose/0/chain/0/subdir + +++ b/chain/1/compose/0/chain/0/subdir/0 @@ -0,0 +1 @@ +b \ No newline at end of file - diff --git a/chain/1/compose/0/chain/1/chain/0/subdir b/chain/1/compose/0/chain/1/chain/0/subdir + diff --git a/chain/1/compose/0/chain/1/chain/0/subdir/0 b/chain/1/compose/0/chain/1/chain/0/subdir/0 new file mode 100644 index 0000000..c59d9b6 --- /dev/null - +++ b/chain/1/compose/0/chain/1/chain/0/subdir + +++ b/chain/1/compose/0/chain/1/chain/0/subdir/0 @@ -0,0 +1 @@ +d \ No newline at end of file - diff --git a/chain/1/compose/0/chain/1/chain/1/prefix b/chain/1/compose/0/chain/1/chain/1/prefix + diff --git a/chain/1/compose/0/chain/1/chain/1/prefix/0 b/chain/1/compose/0/chain/1/chain/1/prefix/0 new file mode 100644 index 0000000..c1b0730 --- /dev/null - +++ b/chain/1/compose/0/chain/1/chain/1/prefix + +++ b/chain/1/compose/0/chain/1/chain/1/prefix/0 @@ -0,0 +1 @@ +x \ No newline at end of file - diff --git a/chain/1/compose/1/chain/0/subdir b/chain/1/compose/1/chain/0/subdir + diff --git a/chain/1/compose/1/chain/0/subdir/0 b/chain/1/compose/1/chain/0/subdir/0 new file mode 100644 index 0000000..3410062 --- /dev/null - +++ b/chain/1/compose/1/chain/0/subdir + +++ b/chain/1/compose/1/chain/0/subdir/0 @@ -0,0 +1 @@ +c \ No newline at end of file - diff --git a/chain/1/compose/1/chain/1/chain/0/subdir b/chain/1/compose/1/chain/1/chain/0/subdir + diff --git a/chain/1/compose/1/chain/1/chain/0/subdir/0 b/chain/1/compose/1/chain/1/chain/0/subdir/0 new file mode 100644 index 0000000..c59d9b6 --- /dev/null - +++ b/chain/1/compose/1/chain/1/chain/0/subdir + +++ b/chain/1/compose/1/chain/1/chain/0/subdir/0 @@ -0,0 +1 @@ +d \ No newline at end of file - diff --git a/chain/1/compose/1/chain/1/chain/1/prefix b/chain/1/compose/1/chain/1/chain/1/prefix + diff --git a/chain/1/compose/1/chain/1/chain/1/prefix/0 b/chain/1/compose/1/chain/1/chain/1/prefix/0 new file mode 100644 index 0000000..e25f181 --- /dev/null - +++ b/chain/1/compose/1/chain/1/chain/1/prefix + +++ b/chain/1/compose/1/chain/1/chain/1/prefix/0 @@ -0,0 +1 @@ +y \ No newline at end of file @@ -408,24 +425,38 @@ Test File filter tree representations Test ::file.txt (single argument, no trailing slash, no =, no *) $ FILTER_HASH=$(josh-filter -i ::file.txt) + $ josh-filter -p ${FILTER_HASH} + ::file.txt $ git read-tree --reset -u ${FILTER_HASH} $ tree . `-- file + |-- 0 + `-- 1 - 1 directory, 1 file + 2 directories, 2 files $ git diff 4b825dc642cb6eb9a060e54bf8d69288fbee4904..${FILTER_HASH} - diff --git a/file b/file + diff --git a/file/0 b/file/0 + new file mode 100644 + index 0000000..4c33073 + --- /dev/null + +++ b/file/0 + @@ -0,0 +1 @@ + +file.txt + \ No newline at end of file + diff --git a/file/1 b/file/1 new file mode 100644 index 0000000..4c33073 --- /dev/null - +++ b/file + +++ b/file/1 @@ -0,0 +1 @@ +file.txt \ No newline at end of file Test ::dest.txt=src.txt (with =, destination=source) $ FILTER_HASH=$(josh-filter -i ::dest.txt=src.txt) + $ josh-filter -p ${FILTER_HASH} + ::dest.txt=src.txt $ git read-tree --reset -u ${FILTER_HASH} $ tree . @@ -454,126 +485,141 @@ Test ::dest.txt=src.txt (with =, destination=source) Test ::*.txt (with *, pattern) $ FILTER_HASH=$(josh-filter -i ::*.txt) + $ josh-filter -p ${FILTER_HASH} + ::*.txt $ git read-tree --reset -u ${FILTER_HASH} $ tree . `-- pattern + `-- 0 - 1 directory, 1 file + 2 directories, 1 file $ git diff 4b825dc642cb6eb9a060e54bf8d69288fbee4904..${FILTER_HASH} - diff --git a/pattern b/pattern + diff --git a/pattern/0 b/pattern/0 new file mode 100644 index 0000000..314f02b --- /dev/null - +++ b/pattern + +++ b/pattern/0 @@ -0,0 +1 @@ +*.txt \ No newline at end of file Test ::dir/ (with trailing slash, directory) $ FILTER_HASH=$(josh-filter -i ::dir/) + $ josh-filter -p ${FILTER_HASH} + ::dir/ $ git read-tree --reset -u ${FILTER_HASH} $ tree . `-- chain |-- 0 | `-- subdir + | `-- 0 `-- 1 `-- prefix + `-- 0 - 4 directories, 2 files + 6 directories, 2 files $ git diff 4b825dc642cb6eb9a060e54bf8d69288fbee4904..${FILTER_HASH} - diff --git a/chain/0/subdir b/chain/0/subdir + diff --git a/chain/0/subdir/0 b/chain/0/subdir/0 new file mode 100644 index 0000000..8724519 --- /dev/null - +++ b/chain/0/subdir + +++ b/chain/0/subdir/0 @@ -0,0 +1 @@ +dir \ No newline at end of file - diff --git a/chain/1/prefix b/chain/1/prefix + diff --git a/chain/1/prefix/0 b/chain/1/prefix/0 new file mode 100644 index 0000000..8724519 --- /dev/null - +++ b/chain/1/prefix + +++ b/chain/1/prefix/0 @@ -0,0 +1 @@ +dir \ No newline at end of file Test ::a/b/c/ (nested directory path with trailing slash) $ FILTER_HASH=$(josh-filter -i ::a/b/c/) + $ josh-filter -p ${FILTER_HASH} + ::a/b/c/ $ git read-tree --reset -u ${FILTER_HASH} $ tree . `-- chain |-- 0 | `-- subdir + | `-- 0 `-- 1 `-- chain |-- 0 | `-- subdir + | `-- 0 `-- 1 `-- chain |-- 0 | `-- subdir + | `-- 0 `-- 1 `-- chain |-- 0 | `-- prefix + | `-- 0 `-- 1 `-- chain |-- 0 | `-- prefix + | `-- 0 `-- 1 `-- prefix + `-- 0 - 16 directories, 6 files + 22 directories, 6 files $ git diff 4b825dc642cb6eb9a060e54bf8d69288fbee4904..${FILTER_HASH} - diff --git a/chain/0/subdir b/chain/0/subdir + diff --git a/chain/0/subdir/0 b/chain/0/subdir/0 new file mode 100644 index 0000000..2e65efe --- /dev/null - +++ b/chain/0/subdir + +++ b/chain/0/subdir/0 @@ -0,0 +1 @@ +a \ No newline at end of file - diff --git a/chain/1/chain/0/subdir b/chain/1/chain/0/subdir + diff --git a/chain/1/chain/0/subdir/0 b/chain/1/chain/0/subdir/0 new file mode 100644 index 0000000..63d8dbd --- /dev/null - +++ b/chain/1/chain/0/subdir + +++ b/chain/1/chain/0/subdir/0 @@ -0,0 +1 @@ +b \ No newline at end of file - diff --git a/chain/1/chain/1/chain/0/subdir b/chain/1/chain/1/chain/0/subdir + diff --git a/chain/1/chain/1/chain/0/subdir/0 b/chain/1/chain/1/chain/0/subdir/0 new file mode 100644 index 0000000..3410062 --- /dev/null - +++ b/chain/1/chain/1/chain/0/subdir + +++ b/chain/1/chain/1/chain/0/subdir/0 @@ -0,0 +1 @@ +c \ No newline at end of file - diff --git a/chain/1/chain/1/chain/1/chain/0/prefix b/chain/1/chain/1/chain/1/chain/0/prefix + diff --git a/chain/1/chain/1/chain/1/chain/0/prefix/0 b/chain/1/chain/1/chain/1/chain/0/prefix/0 new file mode 100644 index 0000000..3410062 --- /dev/null - +++ b/chain/1/chain/1/chain/1/chain/0/prefix + +++ b/chain/1/chain/1/chain/1/chain/0/prefix/0 @@ -0,0 +1 @@ +c \ No newline at end of file - diff --git a/chain/1/chain/1/chain/1/chain/1/chain/0/prefix b/chain/1/chain/1/chain/1/chain/1/chain/0/prefix + diff --git a/chain/1/chain/1/chain/1/chain/1/chain/0/prefix/0 b/chain/1/chain/1/chain/1/chain/1/chain/0/prefix/0 new file mode 100644 index 0000000..63d8dbd --- /dev/null - +++ b/chain/1/chain/1/chain/1/chain/1/chain/0/prefix + +++ b/chain/1/chain/1/chain/1/chain/1/chain/0/prefix/0 @@ -0,0 +1 @@ +b \ No newline at end of file - diff --git a/chain/1/chain/1/chain/1/chain/1/chain/1/prefix b/chain/1/chain/1/chain/1/chain/1/chain/1/prefix + diff --git a/chain/1/chain/1/chain/1/chain/1/chain/1/prefix/0 b/chain/1/chain/1/chain/1/chain/1/chain/1/prefix/0 new file mode 100644 index 0000000..2e65efe --- /dev/null - +++ b/chain/1/chain/1/chain/1/chain/1/chain/1/prefix + +++ b/chain/1/chain/1/chain/1/chain/1/chain/1/prefix/0 @@ -0,0 +1 @@ +a \ No newline at end of file diff --git a/tests/filter/link-submodules.t b/tests/filter/link-submodules.t new file mode 100644 index 000000000..266f16d51 --- /dev/null +++ b/tests/filter/link-submodules.t @@ -0,0 +1,562 @@ + $ export TESTTMP=${PWD} + + $ cd ${TESTTMP} + $ git init -q submodule-repo 1> /dev/null + $ cd submodule-repo + + $ mkdir -p foo + $ echo "foo content" > foo/file1.txt + $ echo "bar content" > foo/file2.txt + $ git add foo + $ git commit -m "add foo with files" 1> /dev/null + + $ mkdir -p bar + $ echo "baz content" > bar/file3.txt + $ git add bar + $ git commit -m "add bar with file" 1> /dev/null + + $ git log --graph --pretty=%s:%H + * add bar with file:00c8fe9f1bb75a3f6280992ec7c3c893d858f5dd + * add foo with files:4b63f3e50a3a34404541bc4519a3a1a0a8e6f738 + + $ cd ${TESTTMP} + $ git init -q main-repo 1> /dev/null + $ cd main-repo + $ git commit -m "init" --allow-empty 1> /dev/null + + $ echo "main content" > main.txt + $ git add main.txt + $ git commit -m "add main.txt" 1> /dev/null + + $ git submodule add ../submodule-repo libs 2> /dev/null + $ git submodule status + 00c8fe9f1bb75a3f6280992ec7c3c893d858f5dd libs (heads/master) + + $ git commit -m "add libs submodule" 1> /dev/null + + $ git fetch ../submodule-repo + From ../submodule-repo + * branch HEAD -> FETCH_HEAD + + $ git log --graph --pretty=%s + * add libs submodule + * add main.txt + * init + + $ git ls-tree --name-only -r HEAD + .gitmodules + libs + main.txt + + $ cat .gitmodules + [submodule "libs"] + path = libs + url = ../submodule-repo + + $ git ls-tree HEAD + 100644 blob 5255711b4fd563af2d873bf3c8f9da6c37ce1726\t.gitmodules (esc) + 160000 commit 00c8fe9f1bb75a3f6280992ec7c3c893d858f5dd\tlibs (esc) + 100644 blob bcb9dcad21591bd9284afbb6c21e6d69eafe8f15\tmain.txt (esc) + +Test Adapt filter - should expand submodule into actual tree content + + $ josh-filter -s :adapt=submodules:link master --update refs/josh/filter/master + [1] :embed=libs + [2] ::libs/.josh-link.toml + [2] :unapply(06d10a853b133ffc533e8ec3f2ed4ec43b64670c:/libs) + [3] :"{commit}" + [3] :adapt=submodules + [3] :link=embedded + [10] sequence_number + $ git log --graph --pretty=%s refs/josh/filter/master + * add libs submodule + |\ + | * add bar with file + | * add foo with files + * add main.txt + * init + $ git ls-tree --name-only -r refs/josh/filter/master + libs/.josh-link.toml + libs/bar/file3.txt + libs/foo/file1.txt + libs/foo/file2.txt + main.txt + + $ git ls-tree refs/josh/filter/master + 040000 tree 1a06220380a0dd3249b08cb1b69158338ebad3ef\tlibs (esc) + 100644 blob bcb9dcad21591bd9284afbb6c21e6d69eafe8f15\tmain.txt (esc) + + $ git ls-tree refs/josh/filter/master libs + 040000 tree 1a06220380a0dd3249b08cb1b69158338ebad3ef\tlibs (esc) + + $ git ls-tree refs/josh/filter/master libs/foo + 040000 tree 81a0b9c71d7fac4f553b2a52b9d8d52d07dd8036\tlibs/foo (esc) + + $ git ls-tree refs/josh/filter/master libs/bar + 040000 tree bd42a3e836f59dda9f9d5950d0e38431c9b1bfb5\tlibs/bar (esc) + + $ git show refs/josh/filter/master:libs/.josh-link.toml + remote = "../submodule-repo" + branch = "HEAD" + filter = ":/" + commit = "00c8fe9f1bb75a3f6280992ec7c3c893d858f5dd" + $ git show refs/josh/filter/master:libs/foo/file1.txt + foo content + + $ git show refs/josh/filter/master:libs/foo/file2.txt + bar content + + $ git show refs/josh/filter/master:libs/bar/file3.txt + baz content + +Test that .gitmodules file is removed after unsubmodule:link + + $ git ls-tree refs/josh/filter/master | grep gitmodules + [1] + +Test Adapt with multiple submodules + + $ cd ${TESTTMP} + $ git init -q another-submodule 1> /dev/null + $ cd another-submodule + $ echo "another content" > another.txt + $ git add another.txt + $ git commit -m "add another.txt" 1> /dev/null + $ git log --graph --pretty=%s:%H + * add another.txt:8fbd01fa31551a059e280f68ac37397712feb59e + + $ cd ${TESTTMP}/main-repo + $ git submodule add ../another-submodule modules/another 2> /dev/null + $ git commit -m "add another submodule" 1> /dev/null + + $ git fetch ../another-submodule + From ../another-submodule + * branch HEAD -> FETCH_HEAD + + $ cat .gitmodules + [submodule "libs"] + path = libs + url = ../submodule-repo + [submodule "modules/another"] + path = modules/another + url = ../another-submodule + + $ josh-filter -s :adapt=submodules master --update refs/josh/filter/master + [1] :embed=libs + [2] ::libs/.josh-link.toml + [2] :unapply(06d10a853b133ffc533e8ec3f2ed4ec43b64670c:/libs) + [3] :"{commit}" + [3] :link=embedded + [4] :adapt=submodules + [11] sequence_number + $ git ls-tree --name-only -r refs/josh/filter/master + libs/.josh-link.toml + main.txt + modules/another/.josh-link.toml + $ git show refs/josh/filter/master:libs/.josh-link.toml + remote = "../submodule-repo" + branch = "HEAD" + filter = ":/" + commit = "00c8fe9f1bb75a3f6280992ec7c3c893d858f5dd" + $ josh-filter -s :adapt=submodules:link master --update refs/josh/filter/master + [1] :embed=libs + [1] :embed=modules/another + [1] :unapply(daa965c7c3a3f8289819a728d6c0f31f0590dc6c:/modules/another) + [2] ::libs/.josh-link.toml + [2] ::modules/another/.josh-link.toml + [2] :unapply(06d10a853b133ffc533e8ec3f2ed4ec43b64670c:/libs) + [4] :"{commit}" + [4] :adapt=submodules + [4] :link=embedded + [15] sequence_number + $ git log --graph --pretty=%s refs/josh/filter/master + * add another submodule + |\ + | * add another.txt + * add libs submodule + |\ + | * add bar with file + | * add foo with files + * add main.txt + * init + $ git ls-tree --name-only -r refs/josh/filter/master + libs/.josh-link.toml + libs/bar/file3.txt + libs/foo/file1.txt + libs/foo/file2.txt + main.txt + modules/another/.josh-link.toml + modules/another/another.txt + + $ git ls-tree refs/josh/filter/master modules + 040000 tree e1b10636436c25c78dc6372eb20079454f05d746\tmodules (esc) + + $ git show refs/josh/filter/master:modules/another/another.txt + another content + +Test Adapt with submodule changes - add commits to submodule and update + + $ cd ${TESTTMP}/submodule-repo + $ echo "new content" > foo/file3.txt + $ git add foo/file3.txt + $ git commit -m "add file3.txt" 1> /dev/null + + $ echo "another new content" > bar/file4.txt + $ git add bar/file4.txt + $ git commit -m "add file4.txt" 1> /dev/null + $ git log --graph --pretty=%s:%H + * add file4.txt:3061af908a0dc1417902fbd7208bb2b8dc354e6c + * add file3.txt:411907f127aa115588a614ec1dff6ee3c4696173 + * add bar with file:00c8fe9f1bb75a3f6280992ec7c3c893d858f5dd + * add foo with files:4b63f3e50a3a34404541bc4519a3a1a0a8e6f738 + + $ cd ${TESTTMP}/main-repo + $ git fetch ../submodule-repo + From ../submodule-repo + * branch HEAD -> FETCH_HEAD + $ git submodule update --remote libs + From /tmp/prysk-tests-*/link-submodules.t/submodule-repo (glob) + 00c8fe9..3061af9 master -> origin/master + Submodule path 'libs': checked out '3061af908a0dc1417902fbd7208bb2b8dc354e6c' + $ git add libs + $ git commit -m "update libs submodule" 1> /dev/null + + $ tree + . + |-- libs + | |-- bar + | | |-- file3.txt + | | `-- file4.txt + | `-- foo + | |-- file1.txt + | |-- file2.txt + | `-- file3.txt + |-- main.txt + `-- modules + `-- another + `-- another.txt + + 6 directories, 7 files + + + $ josh-filter -s :adapt=submodules:link master --update refs/josh/filter/master + [1] :embed=modules/another + [1] :unapply(daa965c7c3a3f8289819a728d6c0f31f0590dc6c:/modules/another) + [2] ::modules/another/.josh-link.toml + [2] :embed=libs + [2] :unapply(06d10a853b133ffc533e8ec3f2ed4ec43b64670c:/libs) + [3] ::libs/.josh-link.toml + [4] :unapply(f4bfdb82ca5e0f06f941f68be2a0fd19573bc415:/libs) + [5] :"{commit}" + [5] :adapt=submodules + [5] :link=embedded + [21] sequence_number + $ git log --graph --pretty=%s:%H refs/josh/filter/master + * update libs submodule:4657e0d71754bdd097c6453a75a5084b467baf54 + |\ + | * add file4.txt:2278f620291df7133299176efb6210fc193c3387 + | * add file3.txt:deed71e37198bf3c8668fa353c66d79c0de25834 + * | add another submodule:b0153ca36fe220c8e0942ee5daf51512907108ca + |\ \ + | * | add another.txt:1f2b84bfd4029e70d3aeb16a6ecb7f0a0490490e + | / + * | add libs submodule:d7b5b1dad9444f25b5011d9f25af2e48a82ff173 + |\| + | * add bar with file:2926fa3361cec2d5695a119fcc3592f4214af3ba + | * add foo with files:e975fd8cd3f2d2de81884f5b761cc0ac150bdf47 + * add main.txt:c404a74092888a14d109c8211576d2c50fc2affd + * init:01d3837a9f7183df88e956cc81f085544f9c6563 + $ git ls-tree --name-only -r 4657e0d71754bdd097c6453a75a5084b467baf54 + libs/.josh-link.toml + libs/bar/file3.txt + libs/bar/file4.txt + libs/foo/file1.txt + libs/foo/file2.txt + libs/foo/file3.txt + main.txt + modules/another/.josh-link.toml + modules/another/another.txt + $ git ls-tree --name-only -r 2278f620291df7133299176efb6210fc193c3387 + libs/bar/file3.txt + libs/bar/file4.txt + libs/foo/file1.txt + libs/foo/file2.txt + libs/foo/file3.txt + main.txt + modules/another/.josh-link.toml + $ git ls-tree --name-only -r refs/josh/filter/master + libs/.josh-link.toml + libs/bar/file3.txt + libs/bar/file4.txt + libs/foo/file1.txt + libs/foo/file2.txt + libs/foo/file3.txt + main.txt + modules/another/.josh-link.toml + modules/another/another.txt + + $ git show refs/josh/filter/master:libs/foo/file3.txt + new content + + $ git show refs/josh/filter/master:libs/bar/file4.txt + another new content + + $ josh-filter -s :adapt=submodules:link:/libs master + [1] :embed=modules/another + [1] :unapply(daa965c7c3a3f8289819a728d6c0f31f0590dc6c:/modules/another) + [2] ::modules/another/.josh-link.toml + [2] :embed=libs + [2] :unapply(06d10a853b133ffc533e8ec3f2ed4ec43b64670c:/libs) + [3] ::libs/.josh-link.toml + [4] :unapply(f4bfdb82ca5e0f06f941f68be2a0fd19573bc415:/libs) + [5] :"{commit}" + [5] :adapt=submodules + [5] :link=embedded + [9] :/libs + [29] sequence_number + $ git log --graph --pretty=%s:%H FILTERED_HEAD + * update libs submodule:6336c45ef94ccdc32fd072b5d7fecf0e9755431a + |\ + | * add file4.txt:3061af908a0dc1417902fbd7208bb2b8dc354e6c + | * add file3.txt:411907f127aa115588a614ec1dff6ee3c4696173 + * | add another submodule:f6d97d3185819dce5596623f5494208fca2de85d + |\ \ + | * | add another.txt:529c9c80186129065a994cbf91095ab1e90323f0 + | / + * / add libs submodule:cb64d3e5db01b0b451f21199ae2197997bc592ba + |/ + * add bar with file:00c8fe9f1bb75a3f6280992ec7c3c893d858f5dd + * add foo with files:4b63f3e50a3a34404541bc4519a3a1a0a8e6f738 + $ josh-filter -s :adapt=submodules:link:/libs:prune=trivial-merge master + [1] :embed=modules/another + [1] :unapply(daa965c7c3a3f8289819a728d6c0f31f0590dc6c:/modules/another) + [2] ::modules/another/.josh-link.toml + [2] :embed=libs + [2] :unapply(06d10a853b133ffc533e8ec3f2ed4ec43b64670c:/libs) + [3] ::libs/.josh-link.toml + [4] :unapply(f4bfdb82ca5e0f06f941f68be2a0fd19573bc415:/libs) + [5] :"{commit}" + [5] :adapt=submodules + [5] :link=embedded + [7] :prune=trivial-merge + [9] :/libs + [33] sequence_number + $ git log --graph --pretty=%s:%H FILTERED_HEAD + * update libs submodule:93f3162c2e8d78320091bb8bb7f9b27226f105bc + |\ + | * add file4.txt:3061af908a0dc1417902fbd7208bb2b8dc354e6c + | * add file3.txt:411907f127aa115588a614ec1dff6ee3c4696173 + * | add libs submodule:cb64d3e5db01b0b451f21199ae2197997bc592ba + |/ + * add bar with file:00c8fe9f1bb75a3f6280992ec7c3c893d858f5dd + * add foo with files:4b63f3e50a3a34404541bc4519a3a1a0a8e6f738 + $ josh-filter -p --reverse :prune=trivial-merge:export:prefix=libs + :/libs:export:prune=trivial-merge + $ josh-filter -s :adapt=submodules:link:/libs:export:prune=trivial-merge master + [1] :embed=modules/another + [1] :unapply(daa965c7c3a3f8289819a728d6c0f31f0590dc6c:/modules/another) + [2] ::modules/another/.josh-link.toml + [2] :embed=libs + [2] :unapply(06d10a853b133ffc533e8ec3f2ed4ec43b64670c:/libs) + [3] ::libs/.josh-link.toml + [4] :unapply(f4bfdb82ca5e0f06f941f68be2a0fd19573bc415:/libs) + [5] :"{commit}" + [5] :adapt=submodules + [5] :link=embedded + [7] :export + [7] :prune=trivial-merge + [9] :/libs + [34] sequence_number + $ git log --graph --pretty=%s:%H:%T FILTERED_HEAD + * add file4.txt:3061af908a0dc1417902fbd7208bb2b8dc354e6c:ac420a625dfb874002210e623a7fdb55708ef2fa + * add file3.txt:411907f127aa115588a614ec1dff6ee3c4696173:2935d839ce5e2fa8d5d8fb1a8541bf95b98fbedb + * add bar with file:00c8fe9f1bb75a3f6280992ec7c3c893d858f5dd:e06b912df6ae0105e3a525f7a9427d98574fbc4f + * add foo with files:4b63f3e50a3a34404541bc4519a3a1a0a8e6f738:7ca6af9b9a7d0d7f4723a74cf6006c14eaea547e + $ josh-filter -s :adapt=submodules:link:/libs:export:prune=trivial-merge master + [1] :embed=modules/another + [1] :unapply(daa965c7c3a3f8289819a728d6c0f31f0590dc6c:/modules/another) + [2] ::modules/another/.josh-link.toml + [2] :embed=libs + [2] :unapply(06d10a853b133ffc533e8ec3f2ed4ec43b64670c:/libs) + [3] ::libs/.josh-link.toml + [4] :unapply(f4bfdb82ca5e0f06f941f68be2a0fd19573bc415:/libs) + [5] :"{commit}" + [5] :adapt=submodules + [5] :link=embedded + [7] :export + [7] :prune=trivial-merge + [9] :/libs + [34] sequence_number + $ git log --graph --pretty=%s:%H FILTERED_HEAD + * add file4.txt:3061af908a0dc1417902fbd7208bb2b8dc354e6c + * add file3.txt:411907f127aa115588a614ec1dff6ee3c4696173 + * add bar with file:00c8fe9f1bb75a3f6280992ec7c3c893d858f5dd + * add foo with files:4b63f3e50a3a34404541bc4519a3a1a0a8e6f738 + $ josh-filter -s :adapt=submodules:link:/modules/another master + [1] :embed=modules/another + [1] :unapply(daa965c7c3a3f8289819a728d6c0f31f0590dc6c:/modules/another) + [2] ::modules/another/.josh-link.toml + [2] :embed=libs + [2] :unapply(06d10a853b133ffc533e8ec3f2ed4ec43b64670c:/libs) + [3] ::libs/.josh-link.toml + [4] :/another + [4] :unapply(f4bfdb82ca5e0f06f941f68be2a0fd19573bc415:/libs) + [5] :"{commit}" + [5] :adapt=submodules + [5] :link=embedded + [7] :/modules + [7] :export + [7] :prune=trivial-merge + [9] :/libs + [38] sequence_number + $ git log --graph --pretty=%s:%H FILTERED_HEAD + * update libs submodule:91175b1309708c7a2ce159f274da8b9a011310ce + |\ + | * add file3.txt:ba5a5509adebeb19574c8abb6d4194d1744ef3f4 + * add another submodule:88584f5d636e6478f0ddec62e6b665625c7a3350 + * add another.txt:8fbd01fa31551a059e280f68ac37397712feb59e + + $ josh-filter -s :adapt=submodules:link master --update refs/heads/testsubexport + [1] :embed=modules/another + [1] :unapply(daa965c7c3a3f8289819a728d6c0f31f0590dc6c:/modules/another) + [2] ::modules/another/.josh-link.toml + [2] :embed=libs + [2] :unapply(06d10a853b133ffc533e8ec3f2ed4ec43b64670c:/libs) + [3] ::libs/.josh-link.toml + [4] :/another + [4] :unapply(f4bfdb82ca5e0f06f941f68be2a0fd19573bc415:/libs) + [5] :"{commit}" + [5] :adapt=submodules + [5] :link=embedded + [7] :/modules + [7] :export + [7] :prune=trivial-merge + [9] :/libs + [38] sequence_number + $ rm -Rf libs + $ rm -Rf modules + $ git checkout testsubexport + Switched to branch 'testsubexport' + $ echo "fo" > libs/foobar.txt + $ git add . + $ git commit -m "mod libs submodule" 1> /dev/null + $ git log --graph --pretty=%s:%H + * mod libs submodule:5a0e5c34f8199e745ba699b4c0423756b18fb1a0 + * update libs submodule:4657e0d71754bdd097c6453a75a5084b467baf54 + |\ + | * add file4.txt:2278f620291df7133299176efb6210fc193c3387 + | * add file3.txt:deed71e37198bf3c8668fa353c66d79c0de25834 + * | add another submodule:b0153ca36fe220c8e0942ee5daf51512907108ca + |\ \ + | * | add another.txt:1f2b84bfd4029e70d3aeb16a6ecb7f0a0490490e + | / + * | add libs submodule:d7b5b1dad9444f25b5011d9f25af2e48a82ff173 + |\| + | * add bar with file:2926fa3361cec2d5695a119fcc3592f4214af3ba + | * add foo with files:e975fd8cd3f2d2de81884f5b761cc0ac150bdf47 + * add main.txt:c404a74092888a14d109c8211576d2c50fc2affd + * init:01d3837a9f7183df88e956cc81f085544f9c6563 + $ josh-filter -s ":/libs:export:prune=trivial-merge" --update refs/heads/testsubexported + [1] :embed=modules/another + [1] :unapply(daa965c7c3a3f8289819a728d6c0f31f0590dc6c:/modules/another) + [2] ::modules/another/.josh-link.toml + [2] :embed=libs + [2] :unapply(06d10a853b133ffc533e8ec3f2ed4ec43b64670c:/libs) + [3] ::libs/.josh-link.toml + [4] :/another + [4] :unapply(f4bfdb82ca5e0f06f941f68be2a0fd19573bc415:/libs) + [5] :"{commit}" + [5] :adapt=submodules + [5] :link=embedded + [7] :/modules + [8] :export + [8] :prune=trivial-merge + [10] :/libs + [41] sequence_number + + $ git checkout testsubexported + Switched to branch 'testsubexported' + $ git log --graph --pretty=%s:%H + * mod libs submodule:005cde5c84fbcf17526a0e2fec0a2932c4ce8f24 + * add file4.txt:3061af908a0dc1417902fbd7208bb2b8dc354e6c + * add file3.txt:411907f127aa115588a614ec1dff6ee3c4696173 + * add bar with file:00c8fe9f1bb75a3f6280992ec7c3c893d858f5dd + * add foo with files:4b63f3e50a3a34404541bc4519a3a1a0a8e6f738 + + $ git rebase 3061af908a0dc1417902fbd7208bb2b8dc354e6c + Current branch testsubexported is up to date. + $ git log --graph --pretty=%s:%H + * mod libs submodule:005cde5c84fbcf17526a0e2fec0a2932c4ce8f24 + * add file4.txt:3061af908a0dc1417902fbd7208bb2b8dc354e6c + * add file3.txt:411907f127aa115588a614ec1dff6ee3c4696173 + * add bar with file:00c8fe9f1bb75a3f6280992ec7c3c893d858f5dd + * add foo with files:4b63f3e50a3a34404541bc4519a3a1a0a8e6f738 + + $ josh-filter -s ":adapt=submodules:link:unlink" refs/heads/master --update refs/heads/unlinked_master + [1] :embed=modules/another + [1] :prefix=modules/another + [1] :unapply(daa965c7c3a3f8289819a728d6c0f31f0590dc6c:/modules/another) + [2] ::modules/another/.josh-link.toml + [2] :embed=libs + [2] :unapply(06d10a853b133ffc533e8ec3f2ed4ec43b64670c:/libs) + [3] ::libs/.josh-link.toml + [4] :/another + [4] :prefix=libs + [4] :unapply(f4bfdb82ca5e0f06f941f68be2a0fd19573bc415:/libs) + [5] :"{commit}" + [5] :adapt=submodules + [5] :link=embedded + [7] :/modules + [8] :export + [8] :prune=trivial-merge + [10] :/libs + [10] :unlink + [41] sequence_number + + $ git log --graph --pretty=%s:%H:%T refs/heads/unlinked_master + * update libs submodule:41ccc704fcbdd815b6849b9927f584cf4c9f6f0e:03b3f655c4ce3f3d8a6fba82bba301c59cc1d957 + |\ + | * add file4.txt:2278f620291df7133299176efb6210fc193c3387:10ff89ee90260a4398c847e6c9448ee6a9f8e4c7 + | * add file3.txt:deed71e37198bf3c8668fa353c66d79c0de25834:9122c83968648c7219e0fee04263e0fce0e45c55 + * | add another submodule:67f677db1f181d00fd3f82baf39b095b73c74634:956f44c3fddbdc526cbf74825ae07c83bde636fd + |\ \ + | * | add another.txt:1f2b84bfd4029e70d3aeb16a6ecb7f0a0490490e:1dadb4c5c0484717f15a05e2c4fbcf26a134fbd4 + | / + * | add libs submodule:0465a38d195eab5390c82865a90a6cc986a52a72:a860693798b958c292bddba8b9f9c64f5b1f8680 + |\| + | * add bar with file:2926fa3361cec2d5695a119fcc3592f4214af3ba:0b7130f9c4103e0b89fd511f432114ef2ebd33e9 + | * add foo with files:e975fd8cd3f2d2de81884f5b761cc0ac150bdf47:1fbe431508b38e48268466d9bb922b979e173ca9 + * add main.txt:c404a74092888a14d109c8211576d2c50fc2affd:1eedb83532c1049f67f2d851fe666e23dee45a6f + * init:01d3837a9f7183df88e956cc81f085544f9c6563:4b825dc642cb6eb9a060e54bf8d69288fbee4904 + + +Test Adapt on repo without submodules (should be no-op) + + $ cd ${TESTTMP} + $ git init -q no-submodules 1> /dev/null + $ cd no-submodules + $ echo "content" > file.txt + $ git add file.txt + $ git commit -m "add file" 1> /dev/null + + $ josh-filter -s :adapt=submodules:link master --update refs/josh/filter/master + [1] :adapt=submodules + [1] :link=embedded + [1] sequence_number + $ git ls-tree --name-only -r refs/josh/filter/master + file.txt + + $ git show refs/josh/filter/master:file.txt + content + +Test Adapt on repo with .gitmodules but no actual submodule entries + + $ cd ${TESTTMP} + $ git init -q empty-submodules 1> /dev/null + $ cd empty-submodules + $ echo "content" > file.txt + $ git add file.txt + $ git commit -m "add file" 1> /dev/null + + $ cat > .gitmodules < /dev/null + $ cd main-repo + $ git config protocol.file.allow always + $ echo "main content" > main.txt + $ git add main.txt + $ git commit -m "add main.txt" 1> /dev/null + + $ cd ${TESTTMP} + $ git init -q submodule-repo 1> /dev/null + $ cd submodule-repo + $ git config protocol.file.allow always + $ mkdir -p foo bar + $ echo "foo content" > foo/file1.txt + $ echo "bar content" > bar/file2.txt + $ git add . + $ git commit -m "add libs" 1> /dev/null + + $ cd ${TESTTMP}/main-repo + $ git submodule add ../submodule-repo libs + Cloning into '/tmp/prysk-tests-*/link.t/main-repo/libs'... (glob) + done. + $ git add .gitmodules libs + $ git commit -m "add libs submodule" 1> /dev/null + + $ git fetch ../submodule-repo + From ../submodule-repo + * branch HEAD -> FETCH_HEAD + + $ josh-filter -s :adapt=submodules:link master --update refs/josh/filter/master + [1] :embed=libs + [1] :unapply(a1520c70819abcbe295fe431e4b88cf56f5a0c95:/libs) + [2] :"{commit}" + [2] ::libs/.josh-link.toml + [2] :adapt=submodules + [2] :link=embedded + [7] sequence_number + $ git ls-tree -r --name-only refs/josh/filter/master + libs/.josh-link.toml + libs/bar/file2.txt + libs/foo/file1.txt + main.txt + + $ git show refs/josh/filter/master:libs/foo/file1.txt + foo content + + $ git show refs/josh/filter/master:libs/bar/file2.txt + bar content + +Test Link on repo without submodules (should be no-op) + + $ cd ${TESTTMP} + $ git init -q no-submodules 1> /dev/null + $ cd no-submodules + $ echo "content" > file.txt + $ git add file.txt + $ git commit -m "add file" 1> /dev/null + + $ josh-filter -s :link master --update refs/josh/filter/master + [1] :link=embedded + [1] sequence_number + $ git ls-tree -r --name-only refs/josh/filter/master + file.txt diff --git a/tests/filter/stored_redirect.t b/tests/filter/stored_redirect.t index c05bdcdb3..cb1b01143 100644 --- a/tests/filter/stored_redirect.t +++ b/tests/filter/stored_redirect.t @@ -63,8 +63,13 @@ ::sub2/subsub/ ] [3] :+st/config - [6] :exclude[::st_new/config.josh] - [8] sequence_number + [5] :[ + ::st/config.josh + a = :/sub1 + ::sub2/subsub/ + b = :/sub3 + ] + [7] sequence_number $ git log --graph --pretty=%s refs/heads/filtered * edit st @@ -74,9 +79,9 @@ * add file2 * add file1 $ git log --graph --pretty=%s refs/heads/filtered_new - * edit st - |\ - | * add file4 + * add st_new + * edit st + * add file4 * add st * add file2 * add file1 @@ -150,6 +155,13 @@ +::sub2/subsub/ +a = :/sub1 +b = :/sub3 + diff --git a/st_new/config.josh b/st_new/config.josh + new file mode 100644 + index 0000000..15782f4 + --- /dev/null + +++ b/st_new/config.josh + @@ -0,0 +1 @@ + +:+st/config diff --git a/sub2/subsub/file2 b/sub2/subsub/file2 new file mode 100644 index 0000000..a024003 @@ -167,14 +179,26 @@ $ josh-filter -s :+st/config master --update refs/heads/filtered [1] :prefix=b + [2] :+st_new/config [2] :/sub3 [2] :[ a = :/sub1 ::sub2/subsub/ ] - [3] :+st_new/config + [2] :subtract[ + ::st_new/config.josh + :[ + a = :/sub1 + ::sub2/subsub/ + b = :/sub3 + ] + ] [4] :+st/config - [5] :exclude[::st/config.josh] - [9] :exclude[::st_new/config.josh] - [13] sequence_number + [5] :[ + ::st/config.josh + a = :/sub1 + ::sub2/subsub/ + b = :/sub3 + ] + [8] sequence_number diff --git a/tests/filter/unsubmodule.t b/tests/filter/unsubmodule.t new file mode 100644 index 000000000..e818e2bd3 --- /dev/null +++ b/tests/filter/unsubmodule.t @@ -0,0 +1,216 @@ + $ export TESTTMP=${PWD} + + $ cd ${TESTTMP} + $ git init -q submodule-repo 1> /dev/null + $ cd submodule-repo + + $ mkdir -p foo + $ echo "foo content" > foo/file1.txt + $ echo "bar content" > foo/file2.txt + $ git add foo + $ git commit -m "add foo with files" 1> /dev/null + + $ mkdir -p bar + $ echo "baz content" > bar/file3.txt + $ git add bar + $ git commit -m "add bar with file" 1> /dev/null + + $ cd ${TESTTMP} + $ git init -q main-repo 1> /dev/null + $ cd main-repo + $ git commit -m "init" --allow-empty 1> /dev/null + + $ echo "main content" > main.txt + $ git add main.txt + $ git commit -m "add main.txt" 1> /dev/null + + $ git submodule add ../submodule-repo libs 2> /dev/null + $ git submodule status + 00c8fe9f1bb75a3f6280992ec7c3c893d858f5dd libs (heads/master) + + $ git commit -m "add libs submodule" 1> /dev/null + + $ git fetch ../submodule-repo + From ../submodule-repo + * branch HEAD -> FETCH_HEAD + + $ git log --graph --pretty=%s + * add libs submodule + * add main.txt + * init + + $ git ls-tree --name-only -r HEAD + .gitmodules + libs + main.txt + + $ cat .gitmodules + [submodule "libs"] + path = libs + url = ../submodule-repo + + $ git ls-tree HEAD + 100644 blob 5255711b4fd563af2d873bf3c8f9da6c37ce1726\t.gitmodules (esc) + 160000 commit 00c8fe9f1bb75a3f6280992ec7c3c893d858f5dd\tlibs (esc) + 100644 blob bcb9dcad21591bd9284afbb6c21e6d69eafe8f15\tmain.txt (esc) + +Test Adapt filter - should expand submodule into actual tree content + + $ josh-filter -s :adapt=submodules master --update refs/josh/filter/master + [3] :adapt=submodules + [3] sequence_number + $ git log --graph --pretty=%s refs/josh/filter/master + * add libs submodule + * add main.txt + * init + $ git ls-tree --name-only -r refs/josh/filter/master + libs/.josh-link.toml + main.txt + + $ git ls-tree refs/josh/filter/master + 040000 tree 34bc8209dca31283563d5519e297ae8cc7f0f19a\tlibs (esc) + 100644 blob bcb9dcad21591bd9284afbb6c21e6d69eafe8f15\tmain.txt (esc) + + $ git ls-tree refs/josh/filter/master libs + 040000 tree 34bc8209dca31283563d5519e297ae8cc7f0f19a\tlibs (esc) + + $ git ls-tree refs/josh/filter/master libs/foo + + $ git ls-tree refs/josh/filter/master libs/bar + + $ git show refs/josh/filter/master:libs/foo/file1.txt + fatal: path 'libs/foo/file1.txt' exists on disk, but not in 'refs/josh/filter/master' + [128] + + $ git show refs/josh/filter/master:libs/foo/file2.txt + fatal: path 'libs/foo/file2.txt' exists on disk, but not in 'refs/josh/filter/master' + [128] + + $ git show refs/josh/filter/master:libs/bar/file3.txt + fatal: path 'libs/bar/file3.txt' exists on disk, but not in 'refs/josh/filter/master' + [128] + +Test that .gitmodules file is removed after unsubmodule + + $ git ls-tree refs/josh/filter/master | grep gitmodules + [1] + +Test Adapt with multiple submodules + + $ cd ${TESTTMP} + $ git init -q another-submodule 1> /dev/null + $ cd another-submodule + $ echo "another content" > another.txt + $ git add another.txt + $ git commit -m "add another.txt" 1> /dev/null + + $ cd ${TESTTMP}/main-repo + $ git submodule add ../another-submodule modules/another 2> /dev/null + $ git commit -m "add another submodule" 1> /dev/null + + $ git fetch ../another-submodule + From ../another-submodule + * branch HEAD -> FETCH_HEAD + + $ cat .gitmodules + [submodule "libs"] + path = libs + url = ../submodule-repo + [submodule "modules/another"] + path = modules/another + url = ../another-submodule + + $ josh-filter -s :adapt=submodules master --update refs/josh/filter/master + [4] :adapt=submodules + [4] sequence_number + $ git log --graph --pretty=%s refs/josh/filter/master + * add another submodule + * add libs submodule + * add main.txt + * init + $ git ls-tree --name-only -r refs/josh/filter/master + libs/.josh-link.toml + main.txt + modules/another/.josh-link.toml + + $ git ls-tree refs/josh/filter/master modules + 040000 tree 9dd65d88b3c43c244c71187f86c40f77e771e432\tmodules (esc) + + $ git show refs/josh/filter/master:modules/another/another.txt + fatal: path 'modules/another/another.txt' exists on disk, but not in 'refs/josh/filter/master' + [128] + +Test Adapt with submodule changes - add commits to submodule and update + + $ cd ${TESTTMP}/submodule-repo + $ mkdir -p libs/foo libs/bar + $ echo "new content" > libs/foo/file3.txt + $ git add libs/foo/file3.txt + $ git commit -m "add file3.txt" 1> /dev/null + + $ echo "another new content" > libs/bar/file4.txt + $ git add libs/bar/file4.txt + $ git commit -m "add file4.txt" 1> /dev/null + + $ cd ${TESTTMP}/main-repo + $ git fetch ../submodule-repo + From ../submodule-repo + * branch HEAD -> FETCH_HEAD + $ git submodule update --remote libs + From /tmp/prysk-tests-*/unsubmodule.t/submodule-repo (glob) + 00c8fe9..47f1d80 master -> origin/master + Submodule path 'libs': checked out '47f1d800e93b0892d3bc525632c9ffc8d32eeb4c' + $ git add libs + $ git commit -m "update libs submodule" 1> /dev/null + + $ josh-filter -s :adapt=submodules master --update refs/josh/filter/master + [5] :adapt=submodules + [5] sequence_number + $ git log --graph --pretty=%s refs/josh/filter/master + * update libs submodule + * add another submodule + * add libs submodule + * add main.txt + * init + $ git ls-tree --name-only -r refs/josh/filter/master + libs/.josh-link.toml + main.txt + modules/another/.josh-link.toml + + $ git show refs/josh/filter/master:libs/libs/foo/file3.txt + fatal: path 'libs/libs/foo/file3.txt' exists on disk, but not in 'refs/josh/filter/master' + [128] + + $ git show refs/josh/filter/master:libs/libs/bar/file4.txt + fatal: path 'libs/libs/bar/file4.txt' exists on disk, but not in 'refs/josh/filter/master' + [128] + +Test Adapt on repo without submodules (should be no-op) + + $ cd ${TESTTMP} + $ git init -q no-submodules 1> /dev/null + $ cd no-submodules + $ echo "content" > file.txt + $ git add file.txt + $ git commit -m "add file" 1> /dev/null + + $ josh-filter -s :adapt=submodules master --update refs/josh/filter/master + [1] :adapt=submodules + [1] sequence_number + $ git ls-tree --name-only -r refs/josh/filter/master + file.txt + + $ git show refs/josh/filter/master:file.txt + content + +Test Adapt on repo with .gitmodules but no actual submodule entries + + $ cd ${TESTTMP} + $ git init -q empty-submodules 1> /dev/null + $ cd empty-submodules + $ echo "content" > file.txt + $ git add file.txt + $ git commit -m "add file" 1> /dev/null + + $ cat > .gitmodules <