From 0a359ccaf1ac7e154b68e9464a87c6c82d397cae Mon Sep 17 00:00:00 2001 From: Joe Date: Sun, 1 Mar 2026 07:49:03 -0600 Subject: [PATCH] Update dependencies and refactor git integration - Added new dependencies in `Cargo.lock` and updated `Cargo.toml` to include `gix` for improved git functionality. - Refactored git-related functions to utilize the `gix` library, enhancing performance and reliability in handling git operations. - Simplified the process of initializing git repositories and committing changes within the CLI commands. - Updated tests to reflect changes in git integration and ensure proper functionality. These changes aim to modernize the git handling in the project and improve overall code quality. --- Cargo.lock | 1188 +++++++++++++++++ Cargo.toml | 1 + TODO.txt | 35 +- ...yncback_util__schema_roundtrip-stdout.snap | 5 +- ...roundtrip-src__init___init.meta.json5.snap | 10 + src/cli/clone.rs | 20 +- src/cli/init.rs | 117 +- src/git.rs | 1002 +++++++++----- src/lib.rs | 1 + tests/rojo_test/serve_util.rs | 26 +- tests/tests/git_sync_defaults.rs | 20 +- tests/tests/syncback.rs | 2 +- 12 files changed, 1954 insertions(+), 473 deletions(-) create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__schema_roundtrip-src__init___init.meta.json5.snap diff --git a/Cargo.lock b/Cargo.lock index 4758d89b..e6430aa2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -63,6 +63,12 @@ dependencies = [ "cc", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "anes" version = "0.1.6" @@ -134,6 +140,15 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "arc-swap" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f3647c145568cec02c42054e07bdf9a5a698e15b466fb2341bfc393cd24aa5" +dependencies = [ + "rustversion", +] + [[package]] name = "arraydeque" version = "0.5.1" @@ -189,6 +204,7 @@ dependencies = [ "env_logger 0.11.9", "fs-err", "futures", + "gix", "globset", "http-body-util", "humantime", @@ -441,6 +457,12 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +[[package]] +name = "bytesize" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bd91ee7b2422bcb158d90ef4d14f75ef67f340943fc4149891dcce8f8b972a3" + [[package]] name = "cast" version = "0.3.0" @@ -585,6 +607,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" +[[package]] +name = "clru" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbd0f76e066e64fdc5631e3bb46381254deab9ef1158292f27c8c57e3bf3fe59" + [[package]] name = "cmake" version = "0.1.57" @@ -817,6 +845,20 @@ dependencies = [ "memchr", ] +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if 1.0.4", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "data-encoding" version = "2.10.0" @@ -1011,6 +1053,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "faster-hex" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7223ae2d2f179b803433d9c830478527e92b8117eab39460edae7f1614d9fb73" +dependencies = [ + "heapless", + "serde", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -1026,6 +1078,17 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "filetime" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" +dependencies = [ + "cfg-if 1.0.4", + "libc", + "libredox", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -1042,6 +1105,12 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foldhash" version = "0.1.5" @@ -1258,6 +1327,907 @@ version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" +[[package]] +name = "gix" +version = "0.80.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa56fdbfe98258af2759818ddc3175cc581112660e74c3fd55669836d29a994" +dependencies = [ + "gix-actor", + "gix-archive", + "gix-attributes", + "gix-blame", + "gix-command", + "gix-commitgraph", + "gix-config", + "gix-credentials", + "gix-date", + "gix-diff", + "gix-dir", + "gix-discover", + "gix-error", + "gix-features", + "gix-filter", + "gix-fs", + "gix-glob", + "gix-hash", + "gix-hashtable", + "gix-ignore", + "gix-index", + "gix-lock", + "gix-mailmap", + "gix-negotiate", + "gix-object", + "gix-odb", + "gix-pack", + "gix-path", + "gix-pathspec", + "gix-prompt", + "gix-protocol", + "gix-ref", + "gix-refspec", + "gix-revision", + "gix-revwalk", + "gix-sec", + "gix-shallow", + "gix-status", + "gix-submodule", + "gix-tempfile", + "gix-trace", + "gix-transport", + "gix-traverse", + "gix-url", + "gix-utils", + "gix-validate", + "gix-worktree", + "gix-worktree-state", + "gix-worktree-stream", + "nonempty", + "parking_lot", + "regex", + "signal-hook", + "smallvec", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-actor" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e5e5b518339d5e6718af108fd064d4e9ba33caf728cf487352873d76411df35" +dependencies = [ + "bstr", + "gix-date", + "gix-error", + "winnow", +] + +[[package]] +name = "gix-archive" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12fee3b0b2a30d01ceebd1ff134b187019b5eaedd2901c2e980d7264ee8758dd" +dependencies = [ + "bstr", + "gix-date", + "gix-error", + "gix-object", + "gix-worktree-stream", +] + +[[package]] +name = "gix-attributes" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c233d6eaa098c0ca5ce03236fd7a96e27f1abe72fad74b46003fbd11fe49563c" +dependencies = [ + "bstr", + "gix-glob", + "gix-path", + "gix-quote", + "gix-trace", + "kstring", + "smallvec", + "thiserror 2.0.18", + "unicode-bom", +] + +[[package]] +name = "gix-bitmap" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7add20f40d060db8c9b1314d499bac6ed7480f33eb113ce3e1cf5d6ff85d989" +dependencies = [ + "gix-error", +] + +[[package]] +name = "gix-blame" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2093922a26722186a2ea36615d581639299ca7d68241d8116d8c441da6f96b1a" +dependencies = [ + "gix-commitgraph", + "gix-date", + "gix-diff", + "gix-error", + "gix-hash", + "gix-object", + "gix-revwalk", + "gix-trace", + "gix-traverse", + "gix-worktree", + "smallvec", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-chunk" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1096b6608fbe5d27fb4984e20f992b4e76fb8c613f6acb87d07c5831b53a6959" +dependencies = [ + "gix-error", +] + +[[package]] +name = "gix-command" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b849c65a609f50d02f8a2774fe371650b3384a743c79c2a070ce0da49b7fb7da" +dependencies = [ + "bstr", + "gix-path", + "gix-quote", + "gix-trace", + "shell-words", +] + +[[package]] +name = "gix-commitgraph" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aea2fcfa6bc7329cd094696ba76682b89bdb61cafc848d91b34abba1c1d7e040" +dependencies = [ + "bstr", + "gix-chunk", + "gix-error", + "gix-hash", + "memmap2", + "nonempty", +] + +[[package]] +name = "gix-config" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c24b190bd42b55724368c28ae750840b48e2038b9b5281202de6fca4ec1fce1" +dependencies = [ + "bstr", + "gix-config-value", + "gix-features", + "gix-glob", + "gix-path", + "gix-ref", + "gix-sec", + "memchr", + "smallvec", + "thiserror 2.0.18", + "unicode-bom", + "winnow", +] + +[[package]] +name = "gix-config-value" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "441a300bc3645a1f45cba495b9175f90f47256ce43f2ee161da0031e3ac77c92" +dependencies = [ + "bitflags 2.11.0", + "bstr", + "gix-path", + "libc", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-credentials" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604b2d440d293a0017cbe60ee87fe10337f6e1d224dd6a147619e849e2be4623" +dependencies = [ + "bstr", + "gix-command", + "gix-config-value", + "gix-date", + "gix-path", + "gix-prompt", + "gix-sec", + "gix-trace", + "gix-url", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-date" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c2f2155782090fd947c2f7904166b9f3c3da0d91358adb011f753ea3a55c0ff" +dependencies = [ + "bstr", + "gix-error", + "itoa", + "jiff", + "smallvec", +] + +[[package]] +name = "gix-diff" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60592771b104eda4e537c311e8239daef0df651d61e0e21855f7e6166416ff12" +dependencies = [ + "bstr", + "gix-attributes", + "gix-command", + "gix-filter", + "gix-fs", + "gix-hash", + "gix-index", + "gix-object", + "gix-path", + "gix-pathspec", + "gix-tempfile", + "gix-trace", + "gix-traverse", + "gix-worktree", + "imara-diff 0.1.8", + "imara-diff 0.2.0", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-dir" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b483ca64cc32d9e33fa617be153ec90525ad77db51106a5f725805a066dc001" +dependencies = [ + "bstr", + "gix-discover", + "gix-fs", + "gix-ignore", + "gix-index", + "gix-object", + "gix-path", + "gix-pathspec", + "gix-trace", + "gix-utils", + "gix-worktree", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-discover" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "810764b92e8cb95e4d91b7adfc5a14666434fd32ace02900dfb66aae71f845df" +dependencies = [ + "bstr", + "dunce", + "gix-fs", + "gix-path", + "gix-ref", + "gix-sec", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-error" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2dfe8025209bf2a72d97a6f2dff105b93e5ebcf131ffa3d3f1728ce4ac3767b" +dependencies = [ + "bstr", +] + +[[package]] +name = "gix-features" +version = "0.46.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a83a5fe8927de3bb02b0cfb87165dbfb49f04d4c297767443f2e1011ecc15bdd" +dependencies = [ + "bytes", + "bytesize", + "crc32fast", + "crossbeam-channel", + "gix-path", + "gix-trace", + "gix-utils", + "libc", + "once_cell", + "parking_lot", + "prodash", + "thiserror 2.0.18", + "walkdir", + "zlib-rs", +] + +[[package]] +name = "gix-filter" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eda328750accaac05ce7637298fd7d6ba0d5d7bdf49c21f899d0b97e3df822d" +dependencies = [ + "bstr", + "encoding_rs", + "gix-attributes", + "gix-command", + "gix-hash", + "gix-object", + "gix-packetline", + "gix-path", + "gix-quote", + "gix-trace", + "gix-utils", + "smallvec", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-fs" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de4bd0d8e6c6ef03485205f8eecc0359042a866d26dba569075db1ebcc005970" +dependencies = [ + "bstr", + "fastrand", + "gix-features", + "gix-path", + "gix-utils", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-glob" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b03e6cd88cc0dc1eafa1fddac0fb719e4e74b6ea58dd016e71125fde4a326bee" +dependencies = [ + "bitflags 2.11.0", + "bstr", + "gix-features", + "gix-path", +] + +[[package]] +name = "gix-hash" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8ced05d2d7b13bff08b2f7eb4e47cfeaf00b974c2ddce08377c4fe1f706b3eb" +dependencies = [ + "faster-hex", + "gix-features", + "sha1-checked", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-hashtable" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f1eecdd006390cbed81f105417dbf82a6fe40842022006550f2e32484101da" +dependencies = [ + "gix-hash", + "hashbrown 0.16.1", + "parking_lot", +] + +[[package]] +name = "gix-ignore" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8953d87c13267e296d547f0fc7eaa8aa8fa5b2a9a34ab1cd5857f25240c7d299" +dependencies = [ + "bstr", + "gix-glob", + "gix-path", + "gix-trace", + "unicode-bom", +] + +[[package]] +name = "gix-index" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b28482b86662c8b78160e0750b097a35fd61185803a960681351b3a07de07e" +dependencies = [ + "bitflags 2.11.0", + "bstr", + "filetime", + "fnv", + "gix-bitmap", + "gix-features", + "gix-fs", + "gix-hash", + "gix-lock", + "gix-object", + "gix-traverse", + "gix-utils", + "gix-validate", + "hashbrown 0.16.1", + "itoa", + "libc", + "memmap2", + "rustix", + "smallvec", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-lock" +version = "21.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbe09cf05ba7c679bba189acc29eeea137f643e7fff1b5dff879dfd45248be31" +dependencies = [ + "gix-tempfile", + "gix-utils", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-mailmap" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7b4818da522786ec7e32a00884ee8fc40fa4c215c3997c0b15f7b62684d1199" +dependencies = [ + "bstr", + "gix-actor", + "gix-date", + "gix-error", +] + +[[package]] +name = "gix-negotiate" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a925ec9bc3664eaff9c7dc49bc857fe0de7e90ece6e092cb66ba923812824db" +dependencies = [ + "bitflags 2.11.0", + "gix-commitgraph", + "gix-date", + "gix-hash", + "gix-object", + "gix-revwalk", +] + +[[package]] +name = "gix-object" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "013eae8e072c6155191ac266950dfbc8d162408642571b32e2c6b3e4b03740fb" +dependencies = [ + "bstr", + "gix-actor", + "gix-date", + "gix-features", + "gix-hash", + "gix-hashtable", + "gix-path", + "gix-utils", + "gix-validate", + "itoa", + "smallvec", + "thiserror 2.0.18", + "winnow", +] + +[[package]] +name = "gix-odb" +version = "0.77.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8901a182923799e8857ac01bff6d7c6fecea999abd79a86dab638aadbb843f3" +dependencies = [ + "arc-swap", + "gix-features", + "gix-fs", + "gix-hash", + "gix-hashtable", + "gix-object", + "gix-pack", + "gix-path", + "gix-quote", + "parking_lot", + "tempfile", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-pack" +version = "0.67.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a9f96f4058359d6874123f160e5b2044974829a29f3a71bb9c9218d1916c3" +dependencies = [ + "clru", + "gix-chunk", + "gix-error", + "gix-features", + "gix-hash", + "gix-hashtable", + "gix-object", + "gix-path", + "gix-tempfile", + "memmap2", + "parking_lot", + "smallvec", + "thiserror 2.0.18", + "uluru", +] + +[[package]] +name = "gix-packetline" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25429ee1ef792d9b653ee5de09bb525489fc8e6908334cfd5d5824269f0b7073" +dependencies = [ + "bstr", + "faster-hex", + "gix-trace", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-path" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7163b1633d35846a52ef8093f390cec240e2d55da99b60151883035e5169cd85" +dependencies = [ + "bstr", + "gix-trace", + "gix-validate", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-pathspec" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40e7636782b35bb1d3ade19ea7387278e96fd49f6963ab41bfca81cef4b61b20" +dependencies = [ + "bitflags 2.11.0", + "bstr", + "gix-attributes", + "gix-config-value", + "gix-glob", + "gix-path", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-prompt" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5b43e9c81bce4e35d90e32405649d47e9fae2ab861d0edbc913fde4df2c6cc7" +dependencies = [ + "gix-command", + "gix-config-value", + "parking_lot", + "rustix", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-protocol" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c64ec7b04c57df6e97a2ac4738a4a09897b88febd6ec4bd2c5d3ff3ad3849df" +dependencies = [ + "bstr", + "gix-credentials", + "gix-date", + "gix-features", + "gix-hash", + "gix-lock", + "gix-negotiate", + "gix-object", + "gix-ref", + "gix-refspec", + "gix-revwalk", + "gix-shallow", + "gix-trace", + "gix-transport", + "gix-utils", + "maybe-async", + "nonempty", + "thiserror 2.0.18", + "winnow", +] + +[[package]] +name = "gix-quote" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68533db71259c8776dd4e770d2b7b98696213ecdc1f5c9e3507119e274e0c578" +dependencies = [ + "bstr", + "gix-error", + "gix-utils", +] + +[[package]] +name = "gix-ref" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cc7b230945f02d706a49bcf823b671785ecd9e88e713b8bd2ca5db104c97add" +dependencies = [ + "gix-actor", + "gix-features", + "gix-fs", + "gix-hash", + "gix-lock", + "gix-object", + "gix-path", + "gix-tempfile", + "gix-utils", + "gix-validate", + "memmap2", + "thiserror 2.0.18", + "winnow", +] + +[[package]] +name = "gix-refspec" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb3dc194cdc1176fc20f39f233d0d516f83df843ea14a9eb758a2690f3e38d1e" +dependencies = [ + "bstr", + "gix-error", + "gix-glob", + "gix-hash", + "gix-revision", + "gix-validate", + "smallvec", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-revision" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9e31cd402edae08c3fdb67917b9fb75b0c9c9bd2fbed0c2dd9c0847039c556" +dependencies = [ + "bitflags 2.11.0", + "bstr", + "gix-commitgraph", + "gix-date", + "gix-error", + "gix-hash", + "gix-hashtable", + "gix-object", + "gix-revwalk", + "gix-trace", + "nonempty", +] + +[[package]] +name = "gix-revwalk" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "573f6e471d76c0796f0b8ed5a431521ea5d121a7860121a2a9703e9434ab1d52" +dependencies = [ + "gix-commitgraph", + "gix-date", + "gix-error", + "gix-hash", + "gix-hashtable", + "gix-object", + "smallvec", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-sec" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e014df75f3d7f5c98b18b45c202422da6236a1c0c0a50997c3f41e601f3ad511" +dependencies = [ + "bitflags 2.11.0", + "gix-path", + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "gix-shallow" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee51037c8a27ddb1c7a6d6db2553d01e501d5b1dae7dc65e41905a70960e658" +dependencies = [ + "bstr", + "gix-hash", + "gix-lock", + "nonempty", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-status" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d4b93da8aae2b5c4ec2aaa3663a0914789737ba17383c665e9270a74173e8f6" +dependencies = [ + "bstr", + "filetime", + "gix-diff", + "gix-dir", + "gix-features", + "gix-filter", + "gix-fs", + "gix-hash", + "gix-index", + "gix-object", + "gix-path", + "gix-pathspec", + "gix-worktree", + "portable-atomic", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-submodule" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cba2022599491d620fbc77b3729dba0120862ce9b4af6e3c47d19a9f2a5d884" +dependencies = [ + "bstr", + "gix-config", + "gix-path", + "gix-pathspec", + "gix-refspec", + "gix-url", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-tempfile" +version = "21.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d9ab2c89fe4bfd4f1d8700aa4516534c170d8a21ae2c554167374607c2eaf16" +dependencies = [ + "dashmap", + "gix-fs", + "libc", + "parking_lot", + "signal-hook", + "signal-hook-registry", + "tempfile", +] + +[[package]] +name = "gix-trace" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f69a13643b8437d4ca6845e08143e847a36ca82903eed13303475d0ae8b162e0" + +[[package]] +name = "gix-transport" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4d72f5094b9f851e348f2cbb840d026ffd8119fc28bc2bca1387eecd171c815" +dependencies = [ + "bstr", + "gix-command", + "gix-features", + "gix-packetline", + "gix-quote", + "gix-sec", + "gix-url", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-traverse" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c99b3cf9dc87c13f1404e7b0e8c5e4bff4975d6f788831c02d6c006f3c76b4a0" +dependencies = [ + "bitflags 2.11.0", + "gix-commitgraph", + "gix-date", + "gix-hash", + "gix-hashtable", + "gix-object", + "gix-revwalk", + "smallvec", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-url" +version = "0.35.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d28e8af3d42581190da884f013caf254d2fd4d6ab102408f08d21bfa11de6c8d" +dependencies = [ + "bstr", + "gix-path", + "percent-encoding", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-utils" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "befcdbdfb1238d2854591f760a48711bed85e72d80a10e8f2f93f656746ef7c5" +dependencies = [ + "bstr", + "fastrand", + "unicode-normalization", +] + +[[package]] +name = "gix-validate" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec1eff98d91941f47766367cba1be746bab662bad761d9891ae6f7882f7840b" +dependencies = [ + "bstr", +] + +[[package]] +name = "gix-worktree" +version = "0.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "005627fc149315f39473e3e94a50058dd5d345c490a23723f67f32ee9c505232" +dependencies = [ + "bstr", + "gix-attributes", + "gix-fs", + "gix-glob", + "gix-hash", + "gix-ignore", + "gix-index", + "gix-object", + "gix-path", + "gix-validate", +] + +[[package]] +name = "gix-worktree-state" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ffce16a83def3651ee4c9872960f4582652fbcc8bbee568c9bae6ffa23894" +dependencies = [ + "bstr", + "gix-features", + "gix-filter", + "gix-fs", + "gix-index", + "gix-object", + "gix-path", + "gix-worktree", + "io-close", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-worktree-stream" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "805ff4ecc1c8ac02e8d1969521d54cd446fae7397f5db3f4d06c5f0fbaa4dd64" +dependencies = [ + "gix-attributes", + "gix-error", + "gix-features", + "gix-filter", + "gix-fs", + "gix-hash", + "gix-object", + "gix-path", + "gix-traverse", + "parking_lot", +] + [[package]] name = "globset" version = "0.4.18" @@ -1282,6 +2252,21 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder 1.5.0", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + [[package]] name = "hashbrown" version = "0.15.5" @@ -1297,6 +2282,8 @@ version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ + "allocator-api2", + "equivalent", "foldhash 0.2.0", ] @@ -1309,6 +2296,16 @@ dependencies = [ "hashbrown 0.16.1", ] +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32", + "stable_deref_trait", +] + [[package]] name = "heck" version = "0.5.0" @@ -1381,6 +2378,12 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "human_format" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaec953f16e5bcf6b8a3cb3aa959b17e5577dbd2693e94554c462c08be22624b" + [[package]] name = "humantime" version = "2.3.0" @@ -1571,6 +2574,25 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "imara-diff" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17d34b7d42178945f775e84bc4c36dde7c1c6cdfea656d3354d009056f2bb3d2" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "imara-diff" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f01d462f766df78ab820dd06f5eb700233c51f0f4c2e846520eaf4ba6aa5c5c" +dependencies = [ + "hashbrown 0.15.5", + "memchr", +] + [[package]] name = "indexmap" version = "2.13.0" @@ -1618,6 +2640,16 @@ dependencies = [ "tempfile", ] +[[package]] +name = "io-close" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cadcf447f06744f8ce713d2d6239bb5bde2c357a452397a9ed90c625da390bc" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -1662,10 +2694,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "819b44bc7c87d9117eb522f14d46e918add69ff12713c475946b0a29363ed1c2" dependencies = [ "jiff-static", + "jiff-tzdb-platform", "log", "portable-atomic", "portable-atomic-util", "serde_core", + "windows-sys 0.61.2", ] [[package]] @@ -1679,6 +2713,21 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "jiff-tzdb" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68971ebff725b9e2ca27a601c5eb38a4c5d64422c4cbab0c535f248087eda5c2" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + [[package]] name = "jni" version = "0.21.1" @@ -1757,6 +2806,15 @@ dependencies = [ "libc", ] +[[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" @@ -1806,7 +2864,10 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" dependencies = [ + "bitflags 2.11.0", "libc", + "plain", + "redox_syscall 0.7.3", ] [[package]] @@ -1879,12 +2940,32 @@ dependencies = [ "regex-automata", ] +[[package]] +name = "maybe-async" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cf92c10c7e361d6b99666ec1c6f9805b0bea2c3bd8c78dc6fe98ac5bd78db11" +dependencies = [ + "proc-macro2 1.0.106", + "quote 1.0.44", + "syn 2.0.117", +] + [[package]] name = "memchr" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "memmap2" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" +dependencies = [ + "libc", +] + [[package]] name = "memofs" version = "0.3.1" @@ -1936,6 +3017,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "nonempty" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9737e026353e5cd0736f98eddae28665118eb6f6600902a7f50db585621fecb6" + [[package]] name = "normpath" version = "1.5.0" @@ -2181,6 +3268,12 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + [[package]] name = "plist" version = "1.8.0" @@ -2311,6 +3404,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "prodash" +version = "31.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "962200e2d7d551451297d9fdce85138374019ada198e30ea9ede38034e27604c" +dependencies = [ + "bytesize", + "human_format", + "parking_lot", +] + [[package]] name = "profiling" version = "1.0.17" @@ -2601,6 +3705,15 @@ dependencies = [ "bitflags 2.11.0", ] +[[package]] +name = "redox_syscall" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" +dependencies = [ + "bitflags 2.11.0", +] + [[package]] name = "redox_users" version = "0.3.5" @@ -3090,6 +4203,16 @@ dependencies = [ "digest", ] +[[package]] +name = "sha1-checked" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89f599ac0c323ebb1c6082821a54962b839832b03984598375bff3975b804423" +dependencies = [ + "digest", + "sha1 0.10.6", +] + [[package]] name = "sha1_smol" version = "1.0.1" @@ -3116,12 +4239,38 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shell-words" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" + [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b57709da74f9ff9f4a27dce9526eec25ca8407c45a7887243b031a58935fb8e" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + [[package]] name = "simd-adler32" version = "0.3.8" @@ -3180,6 +4329,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "stdweb" version = "0.4.20" @@ -3755,18 +4910,42 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" +[[package]] +name = "uluru" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c8a2469e56e6e5095c82ccd3afb98dad95f7af7929aab6d8ba8d6e0f73657da" +dependencies = [ + "arrayvec 0.7.6", +] + [[package]] name = "unicase" version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" +[[package]] +name = "unicode-bom" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eec5d1121208364f6793f7d2e222bf75a915c19557537745b195b253dd64217" + [[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-width" version = "0.1.14" @@ -4355,6 +5534,9 @@ name = "winnow" version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] [[package]] name = "winreg" @@ -4613,6 +5795,12 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "zlib-rs" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c745c48e1007337ed136dc99df34128b9faa6ed542d80a1c673cf55a6d7236c8" + [[package]] name = "zmij" version = "1.0.21" diff --git a/Cargo.toml b/Cargo.toml index 3be401ee..378a35c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -107,6 +107,7 @@ data-encoding = "2.10.0" pathdiff = "0.2.3" blake3 = "1.8.3" +gix = { version = "0.80", default-features = true, features = ["blocking-network-client"] } sha1 = "0.10" indexmap = { version = "2.13.0", features = ["serde"] } rmp-serde = "1.3.1" diff --git a/TODO.txt b/TODO.txt index a5a1d78f..0a7bac63 100644 --- a/TODO.txt +++ b/TODO.txt @@ -5,15 +5,6 @@ get the schemas for everything into cursor somehow setup nightly build suffix version for beta -[mcp] -* atlas_sync - - Basically equiv to git fast-forward merge - - Invokes Atlas sync from Roblox - - If there are NO unselected items in the tree (e.g. all were automatically selected), it just automatically hits accept - - Does nothing if we are running in connected-mode (already syncing) - - Agent should await the completion of this sync before proceeding - - If we are in connected-mode we will need to figure out a way to await syncing to finish to ensure state is good - [merging] we should make the property diffs have arrows next to eachother so you can like "undo" certain things need proper merging support in this UI. @@ -336,10 +327,24 @@ because have already loaded it, we can detect this. this is an optimization to avoid writing 100,000 files or something on larger projects when file bytes match perfectly, or directories verifiably already exist. make sure this is applied to plugin-based syncback, too. -[git] -I notice we actually use the git cli in a lot of places. is there a git library we can use instead? +[mcp] +* atlas_sync + - Basically equiv to git fast-forward merge + - Atlas plugin automatically connects (tries every 5s if nothing there, do not spam errors) via https://create.roblox.com/docs/reference/engine/classes/HttpService#CreateWebStreamClient + - Atlas serve automatically hosts a http based mcp server (NOT stdio based!) + - Decide whether it's easier to make a separate server from the api.rs file, or integrate it - integrating would be cleaner, but im not sure if it would support without a massive amount of changes + - When an agent invokes atlas_sync, it invokes Atlas sync from Roblox in the plugin as if the user clicked the sync button + - Must be on a mutex, so multiple agents cannot invoke it at the same time. Just refuses if it's currently happening + - Also it needs to refuse it if we're actively connected on the regular api layer too, not just the mcp based command. + - If we are connected already, respond to the agent with a proper notice. Make sure if we are in connected-mode where all things are syncing automatically all the time, we tell the agent. We need to tell the agent the configuration of the lua plugin probably? + - If we are in connected-mode we will need to figure out a way to await syncing to finish to ensure state is good? + - Upon the plugin connecting, if there are NO changes (Always/Initial) it should just immediately accept + - If all changes are pre-selected, e.g. from our git pre selection system, then we "fast-forward" by clicking accept immediately. + - If there are actually changes, the agent should hang while waiting for the user to either accept/deny them + - The output of the mcp request needs to conclude that it was a success and what changes were synced AND if it was rejected + what changes were presented to the user to sync. + - Let's just give a list of file paths on the actual disk (relative to project so like ReplicatedStorage/SomeFile.luau or ReplicatedStorage/SomeModel.json5 etc) ? + - Needs to include the DIRECTION, too. + - I'm not sure what is the most value on this, but we absolutely need to give the agent a response on what was actually synced, ideally what the content was(?), and the directions of sync. + - Does nothing if we are running in connected-mode (already syncing) + - Agent should await the completion of this sync before proceeding, this mcp command needs to yield and give the response as described above. -[cli] -make console tabbing support work -when i say console tabbing, like i can type "git c" and it will recommend "git checkout" -is there a rust crate to accomplish this? can we enable it on our current argument parser library being used? diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__schema_roundtrip-stdout.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__schema_roundtrip-stdout.snap index edf282d8..396b05ac 100644 --- a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__schema_roundtrip-stdout.snap +++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__schema_roundtrip-stdout.snap @@ -5,6 +5,7 @@ expression: normalized_stdout Writing default.project.json5 Writing src/adjacent.luau Writing src/adjacent.meta.json5 -Writing src/init/init.meta.json5 +Writing src/init_/init.meta.json5 Writing src/model.model.json5 -Writing src/init +Writing src/init_ +Removing src/init diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__schema_roundtrip-src__init___init.meta.json5.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__schema_roundtrip-src__init___init.meta.json5.snap new file mode 100644 index 00000000..d0e38919 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__schema_roundtrip-src__init___init.meta.json5.snap @@ -0,0 +1,10 @@ +--- +source: tests/tests/syncback.rs +expression: src/init_/init.meta.json5 +--- +{ + attributes: { + trigger_reserialization: true + }, + name: "init" +} diff --git a/src/cli/clone.rs b/src/cli/clone.rs index 1c7ac103..b5ee1fdd 100644 --- a/src/cli/clone.rs +++ b/src/cli/clone.rs @@ -1,5 +1,4 @@ use std::path::{Path, PathBuf}; -use std::process::{Command, Stdio}; use anyhow::{bail, Context}; use clap::Parser; @@ -95,25 +94,8 @@ impl CloneCommand { syncback.run(global)?; - // Commit syncback result if !skip_git { - log::info!("Staging syncback changes..."); - let _ = Command::new("git") - .args(["add", "."]) - .current_dir(&path) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status(); - - log::info!("Committing syncback changes..."); - let _ = Command::new("git") - .args(["commit", "--no-verify", "-m", "syncback"]) - .current_dir(&path) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status(); + crate::git::git_add_all_and_commit(&path, "syncback"); } if !self.skip_cd { diff --git a/src/cli/init.rs b/src/cli/init.rs index 47dad03a..41258685 100644 --- a/src/cli/init.rs +++ b/src/cli/init.rs @@ -1,5 +1,6 @@ -use std::process::{Command, Stdio}; +use std::num::NonZero; use std::str::FromStr; +use std::sync::atomic::AtomicBool; use std::{ collections::VecDeque, path::{Path, PathBuf}, @@ -24,7 +25,7 @@ static TEMPLATE_BINCODE: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/templ /// Initializes a new Rojo project. /// /// By default, this will attempt to initialize a 'git' repository in the -/// project directory if `git` is installed. To avoid this, pass `--skip-git`. +/// project directory. To avoid this, pass `--skip-git`. #[derive(Debug, Parser)] pub struct InitCommand { /// Path to the place to create the project. Defaults to the current directory. @@ -57,7 +58,6 @@ impl InitCommand { let base_path = resolve_path(&self.path); - // Check if directory exists and is non-empty if base_path.exists() { let is_empty = base_path.read_dir()?.next().is_none(); if !is_empty { @@ -122,31 +122,14 @@ impl InitCommand { let did_git_init = if !self.skip_git && should_git_init(&base_path) { log::debug!("Initializing Git repository..."); - let status = Command::new("git") - .arg("init") - .current_dir(&base_path) - .status()?; + let mut repo = gix::init(&base_path).context("Failed to initialize git repository")?; - if !status.success() { - bail!("git init failed: status code {:?}", status.code()); - } - - for (key, value) in [ - ("core.autocrlf", "false"), - ("core.eol", "lf"), - ("core.safecrlf", "false"), - // ("core.splitIndex", "true"), - // ("core.fsmonitor", "true"), - // ("core.untrackedcache", "true"), - // ("feature.manyFiles", "true"), - ] { - let _ = Command::new("git") - .args(["config", "--local", key, value]) - .current_dir(&base_path) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status(); + { + let mut config = repo.config_snapshot_mut(); + let _ = config.set_raw_value(&gix::config::tree::Core::AUTO_CRLF, "false"); + let _ = config.set_raw_value(&gix::config::tree::Core::EOL, "lf"); + let _ = config.set_raw_value(&gix::config::tree::Core::SAFE_CRLF, "false"); + let _ = config.commit_auto_rollback(); } true @@ -158,54 +141,37 @@ impl InitCommand { log::debug!("Cloning cursor rules..."); let cursor_dir = base_path.join(".cursor"); - let result = Command::new("git") - .args([ - "clone", - "--depth", - "1", + let result = (|| -> anyhow::Result<()> { + let prep = gix::prepare_clone( "https://github.com/jrmelsha/cursor-rules.git", - ".cursor", - ]) - .current_dir(&base_path) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .env("GIT_TERMINAL_PROMPT", "0") - .status(); + &cursor_dir, + )?; + let mut prep = prep.with_shallow(gix::remote::fetch::Shallow::DepthAtRemote( + NonZero::new(1).unwrap(), + )); + let (mut checkout, _) = + prep.fetch_then_checkout(gix::progress::Discard, &AtomicBool::new(false))?; + let (_repo, _) = + checkout.main_worktree(gix::progress::Discard, &AtomicBool::new(false))?; + Ok(()) + })(); match result { - Ok(status) if status.success() => { - // Remove .git so it's just files, not a submodule + Ok(()) => { let git_dir = cursor_dir.join(".git"); if git_dir.exists() { let _ = fs::remove_dir_all(&git_dir); } println!("Cloned cursor rules successfully."); } - _ => { + Err(_) => { log::debug!("Failed to clone cursor rules, skipping."); } } } if did_git_init { - log::info!("Staging initial project..."); - let _ = Command::new("git") - .args(["add", "."]) - .current_dir(&base_path) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status(); - - log::info!("Committing initial project..."); - let _ = Command::new("git") - .args(["commit", "--no-verify", "-m", "Initial commit"]) - .current_dir(&base_path) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status(); + crate::git::git_add_all_and_commit(&base_path, "Initial commit"); } println!("Created project successfully."); @@ -219,6 +185,13 @@ impl InitCommand { } } +/// Tells whether we should initialize a Git repository inside the given path. +/// +/// Returns true if the path is not already inside a Git repository. +fn should_git_init(path: &Path) -> bool { + gix::discover(path).is_err() +} + /// The templates we support for initializing a Rojo project. #[derive(Debug, Clone, Copy)] pub enum InitKind { @@ -275,14 +248,12 @@ impl FromStr for InitKind { } } -/// Contains parameters used in templates to create a project. struct ProjectParams { name: String, place_id: Option, } impl ProjectParams { - /// Render a template by replacing variables with project parameters. fn render_template(&self, template: &str) -> String { let place_id_str = self .place_id @@ -296,28 +267,6 @@ impl ProjectParams { } } -/// Tells whether we should initialize a Git repository inside the given path. -/// -/// Will return false if the user doesn't have Git installed or if the path is -/// already inside a Git repository. -fn should_git_init(path: &Path) -> bool { - let result = Command::new("git") - .args(["rev-parse", "--is-inside-work-tree"]) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .current_dir(path) - .status(); - - match result { - // If the command ran, but returned a non-zero exit code, we are not in - // a Git repo and we should initialize one. - Ok(status) => !status.success(), - - // If the command failed to run, we probably don't have Git installed. - Err(_) => false, - } -} - /// Write a file if it does not exist yet, otherwise, leave it alone. fn write_if_not_exists(path: &Path, contents: &str) -> Result<(), anyhow::Error> { let file_res = OpenOptions::new().write(true).create_new(true).open(path); diff --git a/src/git.rs b/src/git.rs index e31a9caa..6818e618 100644 --- a/src/git.rs +++ b/src/git.rs @@ -1,12 +1,10 @@ use std::{ collections::{HashMap, HashSet}, - io::{BufRead, BufReader, Write}, path::{Path, PathBuf}, - process::{Command, Stdio}, sync::{Arc, Mutex}, - thread, }; +use gix::bstr::ByteSlice; use rbx_dom_weak::types::Ref; use sha1::{Digest, Sha1}; @@ -15,45 +13,38 @@ use crate::{ web::interface::GitMetadata, }; +fn open_repo(path: &Path) -> Option { + gix::discover(path).ok() +} + +#[cfg(windows)] +fn strip_unc_prefix(path: &Path) -> PathBuf { + let s = path.to_string_lossy(); + if let Some(stripped) = s.strip_prefix(r"\\?\") { + PathBuf::from(stripped) + } else { + path.to_owned() + } +} + +#[cfg(not(windows))] +fn strip_unc_prefix(path: &Path) -> PathBuf { + path.to_owned() +} + pub fn git_repo_root(project_root: &Path) -> Option { - let output = Command::new("git") - .args([ - "-C", - &project_root.to_string_lossy(), - "rev-parse", - "--show-toplevel", - ]) - .output() - .ok()?; - - if !output.status.success() { - return None; - } - - let stdout = String::from_utf8_lossy(&output.stdout); - let path_str = stdout.trim(); - if path_str.is_empty() { - return None; - } - - Some(PathBuf::from(path_str)) + let repo = open_repo(project_root)?; + repo.workdir().map(|p| p.to_owned()) } /// Returns the current HEAD commit SHA, or `None` if there are no commits /// or the project is not in a git repo. pub fn git_head_commit(repo_root: &Path) -> Option { - let output = Command::new("git") - .args(["-C", &repo_root.to_string_lossy(), "rev-parse", "HEAD"]) - .output() - .ok()?; - - if !output.status.success() { - return None; - } - - let sha = String::from_utf8_lossy(&output.stdout).trim().to_string(); - if sha.len() == 40 && sha.bytes().all(|b| b.is_ascii_hexdigit()) { - Some(sha) + let repo = open_repo(repo_root)?; + let head_id = repo.head_id().ok()?; + let hex = head_id.to_hex().to_string(); + if hex.len() == 40 && hex.bytes().all(|b| b.is_ascii_hexdigit()) { + Some(hex) } else { None } @@ -74,75 +65,302 @@ impl ChangedFiles { } } -fn git_changed_files(repo_root: &Path, initial_head: Option<&str>) -> Option { +fn resolve_tree_id(repo: &gix::Repository, rev: &str) -> Option { + let id = repo.rev_parse_single(rev.as_bytes()).ok()?; + let tree = id.object().ok()?.peel_to_tree().ok()?; + Some(tree.id().detach()) +} + +fn git_changed_files_impl( + repo: &gix::Repository, + initial_head: Option<&str>, +) -> Option { let mut tracked = HashSet::new(); + let mut untracked = HashSet::new(); let diff_base = initial_head.unwrap_or("HEAD"); - let diff_output = Command::new("git") - .args([ - "-C", - &repo_root.to_string_lossy(), - "diff", - diff_base, - "--name-only", - ]) - .output() - .ok()?; + let base_tree_id = resolve_tree_id(repo, diff_base); + let head_tree_id = resolve_tree_id(repo, "HEAD"); - if diff_output.status.success() { - for line in String::from_utf8_lossy(&diff_output.stdout).lines() { - let trimmed = line.trim(); - if !trimmed.is_empty() { - tracked.insert(PathBuf::from(trimmed)); - } - } - } else { - log::debug!( - "git diff {} failed (possibly no commits yet), skipping diff", - diff_base - ); - } + let diff_failed = base_tree_id.is_none(); - let mut untracked = HashSet::new(); - let untracked_output = Command::new("git") - .args([ - "-C", - &repo_root.to_string_lossy(), - "ls-files", - "--others", - "--exclude-standard", - ]) - .output() - .ok()?; - - if untracked_output.status.success() { - for line in String::from_utf8_lossy(&untracked_output.stdout).lines() { - let trimmed = line.trim(); - if !trimmed.is_empty() { - untracked.insert(PathBuf::from(trimmed)); + // If initial_head differs from HEAD, diff those trees for committed-since-start changes + if let (Some(base_id), Some(head_id)) = (base_tree_id, head_tree_id) { + if base_id != head_id { + if let (Ok(base_obj), Ok(head_obj)) = + (repo.find_object(base_id), repo.find_object(head_id)) + { + if let (Ok(base_tree), Ok(head_tree)) = + (base_obj.peel_to_tree(), head_obj.peel_to_tree()) + { + collect_tree_diff_paths(repo, &base_tree, &head_tree, &mut tracked); + } } } } - if tracked.is_empty() && untracked.is_empty() && !diff_output.status.success() { + // Compare base tree vs index for staged changes, and index vs worktree for unstaged + if let Some(base_id) = base_tree_id.or(head_tree_id) { + if let Ok(index) = repo.open_index() { + collect_tree_index_changes(repo, base_id, &index, &mut tracked); + collect_index_worktree_changes(repo, &index, &mut tracked); + } + } else if let Ok(index) = repo.open_index() { + for entry in index.entries() { + tracked.insert(PathBuf::from(entry.path(&index).to_str_lossy().as_ref())); + } + collect_index_worktree_changes(repo, &index, &mut tracked); + } + + collect_untracked_files(repo, &mut untracked); + + if tracked.is_empty() && untracked.is_empty() && diff_failed { return None; } Some(ChangedFiles { tracked, untracked }) } -#[cfg(test)] -fn git_show(repo_root: &Path, object_ref: &str) -> Option { - let output = Command::new("git") - .args(["-C", &repo_root.to_string_lossy(), "show", object_ref]) - .output() - .ok()?; +fn collect_tree_diff_paths( + repo: &gix::Repository, + old_tree: &gix::Tree<'_>, + new_tree: &gix::Tree<'_>, + tracked: &mut HashSet, +) { + if let Ok(changes) = repo.diff_tree_to_tree(old_tree, Some(new_tree), None) { + for change in changes { + let path_str = change.location().to_str_lossy(); + tracked.insert(PathBuf::from(path_str.as_ref())); + } + } +} - if !output.status.success() { - return None; +fn collect_tree_index_changes( + repo: &gix::Repository, + tree_id: gix::ObjectId, + index: &gix::index::File, + tracked: &mut HashSet, +) { + let tree_obj = match repo.find_object(tree_id) { + Ok(o) => o, + Err(_) => return, + }; + let tree = match tree_obj.peel_to_tree() { + Ok(t) => t, + Err(_) => return, + }; + + for entry in index.entries() { + let path_bstr = entry.path(index); + let path_str = path_bstr.to_str_lossy(); + + let tree_entry_id = tree + .lookup_entry_by_path(path_str.as_ref()) + .ok() + .flatten() + .map(|e| e.object_id()); + + match tree_entry_id { + Some(tid) if tid == entry.id => {} + _ => { + tracked.insert(PathBuf::from(path_str.as_ref())); + } + } } - String::from_utf8(output.stdout).ok() + collect_tree_only_paths(repo, &tree, "", index, tracked); +} + +fn collect_tree_only_paths( + repo: &gix::Repository, + tree: &gix::Tree<'_>, + prefix: &str, + index: &gix::index::File, + tracked: &mut HashSet, +) { + for entry in tree.iter() { + let entry = match entry { + Ok(e) => e, + Err(_) => continue, + }; + let name = entry.filename().to_str_lossy(); + let full_path = if prefix.is_empty() { + name.to_string() + } else { + format!("{}/{}", prefix, name) + }; + + if entry.mode().is_tree() { + if let Ok(sub_obj) = entry.id().object() { + if let Ok(sub_tree) = sub_obj.peel_to_tree() { + collect_tree_only_paths(repo, &sub_tree, &full_path, index, tracked); + } + } + } else if entry.mode().is_blob() || entry.mode().is_executable() { + let path_as_bstr: &gix::bstr::BStr = full_path.as_bytes().as_bstr(); + if index.entry_by_path(path_as_bstr).is_none() { + tracked.insert(PathBuf::from(&full_path)); + } + } + } +} + +fn collect_index_worktree_changes( + repo: &gix::Repository, + index: &gix::index::File, + tracked: &mut HashSet, +) { + let work_dir = match repo.workdir() { + Some(d) => d.to_owned(), + None => return, + }; + + for entry in index.entries() { + let path_bstr = entry.path(index); + let path_str = path_bstr.to_str_lossy(); + let full_path = work_dir.join(path_str.as_ref()); + + match std::fs::metadata(&full_path) { + Ok(meta) => { + let size_changed = entry.stat.size as u64 != meta.len(); + if size_changed { + tracked.insert(PathBuf::from(path_str.as_ref())); + } else { + let content = match std::fs::read(&full_path) { + Ok(c) => c, + Err(_) => { + tracked.insert(PathBuf::from(path_str.as_ref())); + continue; + } + }; + let worktree_hash = match repo.write_blob_stream(std::io::Cursor::new(&content)) + { + Ok(id) => id.detach(), + Err(_) => { + tracked.insert(PathBuf::from(path_str.as_ref())); + continue; + } + }; + if worktree_hash != entry.id { + tracked.insert(PathBuf::from(path_str.as_ref())); + } + } + } + Err(_) => { + tracked.insert(PathBuf::from(path_str.as_ref())); + } + } + } +} + +fn collect_untracked_files(repo: &gix::Repository, untracked: &mut HashSet) { + let work_dir = match repo.workdir() { + Some(d) => d.to_owned(), + None => return, + }; + let index = repo.open_index().unwrap_or_else(|_| { + gix::index::File::from_state( + gix::index::State::new(repo.object_hash()), + repo.index_path(), + ) + }); + + fn walk_dir( + base: &Path, + dir: &Path, + index: &gix::index::File, + untracked: &mut HashSet, + ) { + let entries = match std::fs::read_dir(dir) { + Ok(e) => e, + Err(_) => return, + }; + + for entry in entries { + let entry = match entry { + Ok(e) => e, + Err(_) => continue, + }; + let path = entry.path(); + let name = match path.file_name().and_then(|n| n.to_str()) { + Some(n) => n, + None => continue, + }; + + if name == ".git" { + continue; + } + + let rel = match path.strip_prefix(base) { + Ok(r) => r, + Err(_) => continue, + }; + let rel_str = rel.to_string_lossy().replace('\\', "/"); + + if let Ok(meta) = entry.metadata() { + if meta.is_dir() { + walk_dir(base, &path, index, untracked); + } else if meta.is_file() { + let path_as_bstr: &gix::bstr::BStr = rel_str.as_bytes().as_bstr(); + let is_tracked = index.entry_by_path(path_as_bstr).is_some(); + if !is_tracked { + untracked.insert(PathBuf::from(&rel_str)); + } + } + } + } + } + + walk_dir(&work_dir, &work_dir, &index, untracked); +} + +fn resolve_object_ref(repo: &gix::Repository, object_ref: &str) -> Option { + if let Some(path) = object_ref.strip_prefix(":0:") { + let index = repo.open_index().ok()?; + let bstr_path: &gix::bstr::BStr = path.as_bytes().as_bstr(); + let entry = index.entry_by_path(bstr_path)?; + Some(entry.id.to_hex().to_string()) + } else if let Some((rev, path)) = object_ref.split_once(':') { + let id = repo.rev_parse_single(rev.as_bytes()).ok()?; + let tree = id.object().ok()?.peel_to_tree().ok()?; + let entry = tree.lookup_entry_by_path(path).ok()??; + Some(entry.object_id().to_hex().to_string()) + } else { + None + } +} + +fn git_batch_check_hashes_impl( + repo: &gix::Repository, + object_refs: &[String], +) -> Vec> { + object_refs + .iter() + .map(|r| resolve_object_ref(repo, r)) + .collect() +} + +#[cfg(test)] +fn git_show(repo_root: &Path, object_ref: &str) -> Option { + let repo = open_repo(repo_root)?; + if let Some(path) = object_ref.strip_prefix(":0:") { + let index = repo.open_index().ok()?; + let bstr_path: &gix::bstr::BStr = path.as_bytes().as_bstr(); + let entry = index.entry_by_path(bstr_path)?; + let obj = repo.find_object(entry.id).ok()?; + String::from_utf8(obj.data.to_vec()).ok() + } else if object_ref.contains(':') { + let (rev, path) = object_ref.split_once(':')?; + let id = repo.rev_parse_single(rev.as_bytes()).ok()?; + let tree = id.object().ok()?.peel_to_tree().ok()?; + let entry = tree.lookup_entry_by_path(path).ok()??; + let obj = entry.id().object().ok()?; + String::from_utf8(obj.data.to_vec()).ok() + } else { + let id = repo.rev_parse_single(object_ref.as_bytes()).ok()?; + let obj = id.object().ok()?; + String::from_utf8(obj.data.to_vec()).ok() + } } #[cfg(test)] @@ -157,85 +375,6 @@ fn git_show_staged(repo_root: &Path, rel_path: &Path) -> Option { git_show(repo_root, &object_ref) } -/// Retrieves git blob hashes for multiple object refs in a single subprocess. -/// Returns a Vec with the same length as `object_refs`, where each element is -/// `Some(sha1_hex)` if the object exists, or `None` if missing. -/// -/// Uses `git cat-file --batch-check` which outputs the stored blob hash -/// directly, avoiding the need to download and re-hash file contents. -fn git_batch_check_hashes(repo_root: &Path, object_refs: &[String]) -> Vec> { - if object_refs.is_empty() { - return Vec::new(); - } - - let mut child = match Command::new("git") - .args([ - "-C", - &repo_root.to_string_lossy(), - "cat-file", - "--batch-check", - ]) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::null()) - .spawn() - { - Ok(child) => child, - Err(err) => { - log::warn!("Failed to spawn git cat-file --batch-check: {}", err); - return vec![None; object_refs.len()]; - } - }; - - let stdin = child.stdin.take().unwrap(); - let refs_for_writer: Vec = object_refs.to_vec(); - let writer_thread = thread::spawn(move || { - let mut stdin = stdin; - for object_ref in &refs_for_writer { - if writeln!(stdin, "{}", object_ref).is_err() { - break; - } - } - }); - - let expected = object_refs.len(); - let mut results = Vec::with_capacity(expected); - - if let Some(stdout) = child.stdout.take() { - let reader = BufReader::new(stdout); - for line in reader.lines() { - let line = match line { - Ok(l) => l, - Err(_) => break, - }; - - if line.ends_with(" missing") { - results.push(None); - } else if let Some(sha1) = line.split(' ').next() { - if sha1.len() == 40 && sha1.bytes().all(|b| b.is_ascii_hexdigit()) { - results.push(Some(sha1.to_string())); - } else { - results.push(None); - } - } else { - results.push(None); - } - - if results.len() >= expected { - break; - } - } - } - - while results.len() < expected { - results.push(None); - } - - let _ = writer_thread.join(); - let _ = child.wait(); - results -} - pub fn compute_blob_sha1(content: &str) -> String { let header = format!("blob {}\0", content.len()); let mut hasher = Sha1::new(); @@ -249,22 +388,237 @@ pub fn git_add(repo_root: &Path, paths: &[PathBuf]) { return; } - let mut cmd = Command::new("git"); - cmd.args(["-C", &repo_root.to_string_lossy(), "add", "--"]); + let repo = match open_repo(repo_root) { + Some(r) => r, + None => return, + }; + let mut index = match repo.open_index() { + Ok(i) => i, + Err(_) => return, + }; + + let work_dir = match repo.workdir() { + Some(d) => d.to_owned(), + None => return, + }; + for path in paths { - cmd.arg(path); + let abs_path = if path.is_absolute() { + path.to_owned() + } else { + work_dir.join(path) + }; + let norm_abs = strip_unc_prefix(&abs_path); + let norm_work = strip_unc_prefix(&work_dir); + let rel_path = match norm_abs.strip_prefix(&norm_work) { + Ok(r) => r.to_owned(), + Err(_) => path.to_owned(), + }; + let content = match std::fs::read(&abs_path) { + Ok(c) => c, + Err(_) => continue, + }; + + let blob_id = match repo.write_blob(&content) { + Ok(id) => id.detach(), + Err(_) => continue, + }; + + let rel_str = rel_path.to_string_lossy().replace('\\', "/"); + let bstr_path: gix::bstr::BString = rel_str.into(); + + let gix_meta = match gix::index::fs::Metadata::from_path_no_follow(&abs_path) { + Ok(m) => m, + Err(_) => continue, + }; + let stat = match gix::index::entry::Stat::from_fs(&gix_meta) { + Ok(s) => s, + Err(_) => gix::index::entry::Stat::default(), + }; + + if let Some(existing) = index + .entry_mut_by_path_and_stage(bstr_path.as_ref(), gix::index::entry::Stage::Unconflicted) + { + existing.id = blob_id; + existing.stat = stat; + } else { + index.dangerously_push_entry( + stat, + blob_id, + gix::index::entry::Flags::empty(), + gix::index::entry::Mode::FILE, + bstr_path.as_ref(), + ); + } } - match cmd.output() { - Ok(output) => { - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - log::warn!("git add failed: {}", stderr.trim()); + index.sort_entries(); + if let Err(e) = index.write(gix::index::write::Options::default()) { + log::warn!("Failed to write git index: {}", e); + } +} + +/// Build a tree object from sorted index entries and write it to the ODB. +pub fn write_index_tree(repo: &gix::Repository, index: &gix::index::File) -> Option { + use gix::objs::tree; + use std::collections::BTreeMap; + + enum TreeNode { + Blob(gix::ObjectId, tree::EntryKind), + Tree(BTreeMap), + } + + let mut root: BTreeMap = BTreeMap::new(); + + for entry in index.entries() { + let path = entry.path(index).to_str_lossy(); + let parts: Vec<&str> = path.split('/').collect(); + + let mut current = &mut root; + for (i, part) in parts.iter().enumerate() { + if i == parts.len() - 1 { + let kind = if entry.mode == gix::index::entry::Mode::FILE_EXECUTABLE { + tree::EntryKind::BlobExecutable + } else { + tree::EntryKind::Blob + }; + current.insert(part.to_string(), TreeNode::Blob(entry.id, kind)); + } else { + current = match current + .entry(part.to_string()) + .or_insert_with(|| TreeNode::Tree(BTreeMap::new())) + { + TreeNode::Tree(ref mut map) => map, + _ => return None, + }; } } - Err(err) => { - log::warn!("Failed to run git add: {}", err); + } + + fn write_recursive( + repo: &gix::Repository, + nodes: &BTreeMap, + ) -> Option { + let mut tree = gix::objs::Tree::empty(); + for (name, node) in nodes { + match node { + TreeNode::Blob(id, kind) => { + tree.entries.push(tree::Entry { + mode: (*kind).into(), + filename: name.as_str().into(), + oid: *id, + }); + } + TreeNode::Tree(children) => { + let subtree_id = write_recursive(repo, children)?; + tree.entries.push(tree::Entry { + mode: tree::EntryKind::Tree.into(), + filename: name.as_str().into(), + oid: subtree_id, + }); + } + } } + repo.write_object(&tree).ok().map(|id| id.detach()) + } + + write_recursive(repo, &root) +} + +/// Stage all worktree files and create a commit. +pub fn git_add_all_and_commit(dir: &Path, message: &str) { + let repo = match open_repo(dir) { + Some(r) => r, + None => return, + }; + let work_dir = match repo.workdir() { + Some(d) => d.to_owned(), + None => return, + }; + + log::info!("Staging all files..."); + let mut index = gix::index::File::from_state( + gix::index::State::new(repo.object_hash()), + repo.index_path(), + ); + + fn add_dir(repo: &gix::Repository, base: &Path, dir: &Path, index: &mut gix::index::File) { + let entries = match std::fs::read_dir(dir) { + Ok(e) => e, + Err(_) => return, + }; + for entry in entries { + let entry = match entry { + Ok(e) => e, + Err(_) => continue, + }; + let path = entry.path(); + let name = match path.file_name().and_then(|n| n.to_str()) { + Some(n) => n, + None => continue, + }; + if name == ".git" { + continue; + } + if let Ok(ft) = entry.file_type() { + if ft.is_dir() { + add_dir(repo, base, &path, index); + } else if ft.is_file() { + let rel = match path.strip_prefix(base) { + Ok(r) => r, + Err(_) => continue, + }; + let rel_str = rel.to_string_lossy().replace('\\', "/"); + let content = match std::fs::read(&path) { + Ok(c) => c, + Err(_) => continue, + }; + let blob_id = match repo.write_blob(&content) { + Ok(id) => id.detach(), + Err(_) => continue, + }; + let gix_meta = match gix::index::fs::Metadata::from_path_no_follow(&path) { + Ok(m) => m, + Err(_) => continue, + }; + let stat = gix::index::entry::Stat::from_fs(&gix_meta).unwrap_or_default(); + let bstr_path: &gix::bstr::BStr = rel_str.as_bytes().as_bstr(); + index.dangerously_push_entry( + stat, + blob_id, + gix::index::entry::Flags::empty(), + gix::index::entry::Mode::FILE, + bstr_path, + ); + } + } + } + } + + add_dir(&repo, &work_dir, &work_dir, &mut index); + index.sort_entries(); + if index.write(gix::index::write::Options::default()).is_err() { + log::warn!("Failed to write git index"); + return; + } + + log::info!("Committing: {}", message); + let tree_id = match write_index_tree(&repo, &index) { + Some(id) => id, + None => { + log::warn!("Failed to write tree from index"); + return; + } + }; + + let parents: Vec = repo + .head_id() + .ok() + .map(|id| id.detach()) + .into_iter() + .collect(); + if let Err(e) = repo.commit("HEAD", message, tree_id, parents.iter().copied()) { + log::warn!("Failed to commit: {}", e); } } @@ -280,15 +634,26 @@ struct ResolvedInstance { /// cached at session start by `ServeSession`. /// /// Uses a three-phase approach to avoid holding the tree lock during I/O: -/// 1. Run git commands to get changed files (no lock) +/// 1. Use gix to get changed files (no lock) /// 2. Briefly lock tree to resolve file paths to instance Refs and class names -/// 3. Single `git cat-file --batch-check` call to get blob hashes (no lock) +/// 3. Direct object/index lookups to get blob hashes (no lock) pub fn compute_git_metadata( tree_handle: &Arc>, repo_root: &Path, initial_head: Option<&str>, ) -> GitMetadata { - let changed_files = match git_changed_files(repo_root, initial_head) { + let repo = match open_repo(repo_root) { + Some(r) => r, + None => { + return GitMetadata { + changed_ids: Vec::new(), + script_committed_hashes: HashMap::new(), + new_file_ids: Vec::new(), + }; + } + }; + + let changed_files = match git_changed_files_impl(&repo, initial_head) { Some(files) => files, None => { return GitMetadata { @@ -310,7 +675,6 @@ pub fn compute_git_metadata( let untracked_set = &changed_files.untracked; let all_changed = changed_files.all(); - // Brief lock: resolve changed file paths to instance Refs and class names let resolved: Vec = { let tree = tree_handle.lock().unwrap(); let mut result = Vec::new(); @@ -340,7 +704,7 @@ pub fn compute_git_metadata( } result - }; // Lock released + }; let changed_ids: Vec = resolved.iter().map(|ri| ri.id).collect(); let new_file_ids: Vec = resolved @@ -392,7 +756,7 @@ pub fn compute_git_metadata( } } - let batch_hashes = git_batch_check_hashes(repo_root, &object_refs); + let batch_hashes = git_batch_check_hashes_impl(&repo, &object_refs); for (i, info) in script_refs.iter().enumerate() { let sha1 = match batch_hashes.get(i).and_then(|h| h.as_ref()) { @@ -428,31 +792,50 @@ pub fn compute_git_metadata( /// This is useful because syncback may rewrite files with identical content, /// which can cause git to report them as modified due to timestamp changes. pub fn refresh_git_index(project_dir: &Path) { - let mut check_dir = Some(project_dir); - let mut is_git_repo = false; - while let Some(dir) = check_dir { - if dir.join(".git").exists() { - is_git_repo = true; - break; + let repo = match open_repo(project_dir) { + Some(r) => r, + None => { + log::debug!("Not a git repository, skipping index refresh."); + return; } - check_dir = dir.parent(); - } + }; - if is_git_repo { - log::info!("Refreshing git index..."); - match Command::new("git") - .args(["update-index", "--refresh", "-q"]) - .current_dir(project_dir) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status() - { - Ok(_) => log::info!("Git index refreshed."), - Err(e) => log::warn!("Failed to run git update-index --refresh: {}", e), + log::info!("Refreshing git index..."); + match repo.open_index() { + Ok(mut index) => { + let work_dir = match repo.workdir() { + Some(d) => d.to_owned(), + None => return, + }; + + let paths_and_stats: Vec<_> = index + .entries() + .iter() + .filter_map(|entry| { + let path_str = entry.path(&index).to_str_lossy(); + let full_path = work_dir.join(path_str.as_ref()); + let gix_meta = + gix::index::fs::Metadata::from_path_no_follow(&full_path).ok()?; + let stat = gix::index::entry::Stat::from_fs(&gix_meta).ok()?; + Some((path_str.to_string(), stat)) + }) + .collect(); + + for (path_str, stat) in paths_and_stats { + let bstr_path: &gix::bstr::BStr = path_str.as_bytes().as_bstr(); + if let Some(entry) = index + .entry_mut_by_path_and_stage(bstr_path, gix::index::entry::Stage::Unconflicted) + { + entry.stat = stat; + } + } + + match index.write(gix::index::write::Options::default()) { + Ok(_) => log::info!("Git index refreshed."), + Err(e) => log::warn!("Failed to write refreshed git index: {}", e), + } } - } else { - log::debug!("Not a git repository, skipping index refresh."); + Err(e) => log::warn!("Failed to open git index for refresh: {}", e), } } @@ -463,57 +846,51 @@ mod tests { use tempfile::tempdir; fn git_init(dir: &Path) { - Command::new("git") - .args(["-C", &dir.to_string_lossy(), "init"]) - .output() - .expect("git init failed"); - Command::new("git") - .args([ - "-C", - &dir.to_string_lossy(), - "config", - "user.email", - "test@test.com", - ]) - .output() - .unwrap(); - Command::new("git") - .args(["-C", &dir.to_string_lossy(), "config", "user.name", "Test"]) - .output() - .unwrap(); + let mut repo = gix::init(dir).expect("gix init failed"); + { + let mut config = repo.config_snapshot_mut(); + config + .set_raw_value(&gix::config::tree::User::NAME, "Test") + .unwrap(); + config + .set_raw_value(&gix::config::tree::User::EMAIL, "test@test.com") + .unwrap(); + let _ = config.commit_auto_rollback().unwrap(); + } } fn git_commit_all(dir: &Path, msg: &str) { - Command::new("git") - .args(["-C", &dir.to_string_lossy(), "add", "-A"]) - .output() - .unwrap(); - Command::new("git") - .args(["-C", &dir.to_string_lossy(), "commit", "-m", msg]) - .output() - .unwrap(); + git_add_all_and_commit(dir, msg); } fn git_stage(dir: &Path, file: &str) { - Command::new("git") - .args(["-C", &dir.to_string_lossy(), "add", file]) - .output() - .unwrap(); + let repo = open_repo(dir).expect("repo not found"); + git_add(repo.workdir().unwrap_or(dir), &[PathBuf::from(file)]); } fn git_is_staged(dir: &Path, file: &str) -> bool { - let output = Command::new("git") - .args([ - "-C", - &dir.to_string_lossy(), - "diff", - "--cached", - "--name-only", - ]) - .output() - .unwrap(); - let stdout = String::from_utf8_lossy(&output.stdout); - stdout.lines().any(|l| l.trim() == file) + let repo = open_repo(dir).expect("repo not found"); + let index = match repo.open_index() { + Ok(i) => i, + Err(_) => return false, + }; + let bstr_path: &gix::bstr::BStr = file.as_bytes().as_bstr(); + let index_entry = match index.entry_by_path(bstr_path) { + Some(e) => e, + None => return false, + }; + + let head_blob_id = repo + .head_commit() + .ok() + .and_then(|c| c.tree().ok()) + .and_then(|t| t.lookup_entry_by_path(file).ok().flatten()) + .map(|e| e.object_id()); + + match head_blob_id { + Some(head_id) => index_entry.id != head_id, + None => true, + } } // ----------------------------------------------------------------------- @@ -523,15 +900,11 @@ mod tests { #[test] fn blob_sha1_empty_content() { let hash = compute_blob_sha1(""); - // git hash-object -t blob --stdin <<< '' produces the hash for empty blob - // "blob 0\0" -> SHA1 = e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 assert_eq!(hash, "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"); } #[test] fn blob_sha1_hello_world() { - // Verify against `echo -n "hello world" | git hash-object --stdin` - // "blob 11\0hello world" -> SHA1 = 95d09f2b10159347eece71399a7e2e907ea3df4f let hash = compute_blob_sha1("hello world"); assert_eq!(hash, "95d09f2b10159347eece71399a7e2e907ea3df4f"); } @@ -541,7 +914,6 @@ mod tests { let content = "local foo = 1\nlocal bar = 2\nreturn foo + bar\n"; let hash = compute_blob_sha1(content); assert_eq!(hash.len(), 40); - // Same content must produce same hash assert_eq!(hash, compute_blob_sha1(content)); } @@ -553,26 +925,19 @@ mod tests { } #[test] - fn blob_sha1_matches_git_hash_object() { + fn blob_sha1_matches_gix_hash_object() { let dir = tempdir().unwrap(); git_init(dir.path()); let content = "print('test script')\n"; fs::write(dir.path().join("test.luau"), content).unwrap(); - let output = Command::new("git") - .args([ - "-C", - &dir.path().to_string_lossy(), - "hash-object", - "test.luau", - ]) - .output() - .unwrap(); - let git_hash = String::from_utf8_lossy(&output.stdout).trim().to_string(); + let repo = open_repo(dir.path()).unwrap(); + let blob_id = repo.write_blob(content.as_bytes()).unwrap(); + let gix_hash = blob_id.to_hex().to_string(); let our_hash = compute_blob_sha1(content); - assert_eq!(our_hash, git_hash); + assert_eq!(our_hash, gix_hash); } // ----------------------------------------------------------------------- @@ -600,7 +965,6 @@ mod tests { let sub = dir.path().join("sub").join("deep"); fs::create_dir_all(&sub).unwrap(); let root = git_repo_root(&sub).unwrap(); - // Subdirectory should resolve to the same repo root assert_eq!( root.canonicalize().unwrap(), git_repo_root(dir.path()).unwrap().canonicalize().unwrap() @@ -618,7 +982,8 @@ mod tests { fs::write(dir.path().join("file.luau"), "content").unwrap(); git_commit_all(dir.path(), "init"); - let changed = git_changed_files(dir.path(), None); + let repo = open_repo(dir.path()).unwrap(); + let changed = git_changed_files_impl(&repo, None); assert!(changed.is_some()); assert!(changed.unwrap().is_empty()); } @@ -631,7 +996,8 @@ mod tests { git_commit_all(dir.path(), "init"); fs::write(dir.path().join("script.luau"), "modified").unwrap(); - let changed = git_changed_files(dir.path(), None).unwrap(); + let repo = open_repo(dir.path()).unwrap(); + let changed = git_changed_files_impl(&repo, None).unwrap(); assert!(changed.tracked.contains(&PathBuf::from("script.luau"))); assert!(changed.untracked.is_empty()); } @@ -645,7 +1011,8 @@ mod tests { fs::write(dir.path().join("script.luau"), "staged version").unwrap(); git_stage(dir.path(), "script.luau"); - let changed = git_changed_files(dir.path(), None).unwrap(); + let repo = open_repo(dir.path()).unwrap(); + let changed = git_changed_files_impl(&repo, None).unwrap(); assert!(changed.tracked.contains(&PathBuf::from("script.luau"))); } @@ -657,7 +1024,8 @@ mod tests { git_commit_all(dir.path(), "init"); fs::write(dir.path().join("new_file.luau"), "new").unwrap(); - let changed = git_changed_files(dir.path(), None).unwrap(); + let repo = open_repo(dir.path()).unwrap(); + let changed = git_changed_files_impl(&repo, None).unwrap(); assert!(changed.untracked.contains(&PathBuf::from("new_file.luau"))); assert!(!changed.tracked.contains(&PathBuf::from("new_file.luau"))); } @@ -670,7 +1038,8 @@ mod tests { git_commit_all(dir.path(), "init"); fs::remove_file(dir.path().join("to_delete.luau")).unwrap(); - let changed = git_changed_files(dir.path(), None).unwrap(); + let repo = open_repo(dir.path()).unwrap(); + let changed = git_changed_files_impl(&repo, None).unwrap(); assert!(changed.tracked.contains(&PathBuf::from("to_delete.luau"))); } @@ -685,7 +1054,8 @@ mod tests { fs::write(dir.path().join("will_modify.luau"), "new").unwrap(); fs::write(dir.path().join("untracked.luau"), "brand new").unwrap(); - let changed = git_changed_files(dir.path(), None).unwrap(); + let repo = open_repo(dir.path()).unwrap(); + let changed = git_changed_files_impl(&repo, None).unwrap(); let all = changed.all(); assert!(all.contains(&PathBuf::from("will_modify.luau"))); assert!(all.contains(&PathBuf::from("untracked.luau"))); @@ -700,7 +1070,8 @@ mod tests { git_init(dir.path()); fs::write(dir.path().join("new.luau"), "content").unwrap(); - let changed = git_changed_files(dir.path(), None); + let repo = open_repo(dir.path()).unwrap(); + let changed = git_changed_files_impl(&repo, None); assert!(changed.is_some()); assert!(changed .unwrap() @@ -717,7 +1088,8 @@ mod tests { git_commit_all(dir.path(), "init"); fs::write(dir.path().join("src/module.luau"), "modified").unwrap(); - let changed = git_changed_files(dir.path(), None).unwrap(); + let repo = open_repo(dir.path()).unwrap(); + let changed = git_changed_files_impl(&repo, None).unwrap(); assert!(changed.tracked.contains(&PathBuf::from("src/module.luau"))); } @@ -733,13 +1105,15 @@ mod tests { fs::write(dir.path().join("script.luau"), "modified").unwrap(); git_commit_all(dir.path(), "edit"); - let without = git_changed_files(dir.path(), None).unwrap(); + let repo = open_repo(dir.path()).unwrap(); + + let without = git_changed_files_impl(&repo, None).unwrap(); assert!( !without.all().contains(&PathBuf::from("script.luau")), "without initial_head, committed file should NOT appear" ); - let with = git_changed_files(dir.path(), Some(&initial_head)).unwrap(); + let with = git_changed_files_impl(&repo, Some(&initial_head)).unwrap(); assert!( with.tracked.contains(&PathBuf::from("script.luau")), "with initial_head, committed file should appear" @@ -804,7 +1178,6 @@ mod tests { fs::write(dir.path().join("script.luau"), "v1").unwrap(); git_commit_all(dir.path(), "init"); - // File not staged, show :0: should return committed version (same as HEAD) let staged = git_show_staged(dir.path(), Path::new("script.luau")); let head = git_show_head(dir.path(), Path::new("script.luau")); assert_eq!(staged, head); @@ -839,14 +1212,15 @@ mod tests { } // ----------------------------------------------------------------------- - // git_batch_check_hashes + // git_batch_check_hashes (via resolve_object_ref) // ----------------------------------------------------------------------- #[test] fn batch_check_empty_refs() { let dir = tempdir().unwrap(); git_init(dir.path()); - let results = git_batch_check_hashes(dir.path(), &[]); + let repo = open_repo(dir.path()).unwrap(); + let results = git_batch_check_hashes_impl(&repo, &[]); assert!(results.is_empty()); } @@ -857,8 +1231,9 @@ mod tests { fs::write(dir.path().join("script.luau"), "local x = 42\nreturn x").unwrap(); git_commit_all(dir.path(), "init"); + let repo = open_repo(dir.path()).unwrap(); let refs = vec!["HEAD:script.luau".to_string()]; - let results = git_batch_check_hashes(dir.path(), &refs); + let results = git_batch_check_hashes_impl(&repo, &refs); assert_eq!(results.len(), 1); let batch_hash = results[0].as_ref().unwrap(); @@ -874,8 +1249,9 @@ mod tests { fs::write(dir.path().join("dummy.luau"), "x").unwrap(); git_commit_all(dir.path(), "init"); + let repo = open_repo(dir.path()).unwrap(); let refs = vec!["HEAD:nonexistent.luau".to_string()]; - let results = git_batch_check_hashes(dir.path(), &refs); + let results = git_batch_check_hashes_impl(&repo, &refs); assert_eq!(results.len(), 1); assert!(results[0].is_none()); } @@ -887,8 +1263,9 @@ mod tests { fs::write(dir.path().join("a.luau"), "content a").unwrap(); git_commit_all(dir.path(), "init"); + let repo = open_repo(dir.path()).unwrap(); let refs = vec!["HEAD:a.luau".to_string(), ":0:a.luau".to_string()]; - let results = git_batch_check_hashes(dir.path(), &refs); + let results = git_batch_check_hashes_impl(&repo, &refs); assert_eq!(results.len(), 2); assert!(results[0].is_some()); assert_eq!(results[0], results[1]); @@ -907,8 +1284,9 @@ mod tests { fs::write(dir.path().join("b.luau"), "staged version").unwrap(); git_stage(dir.path(), "b.luau"); + let repo = open_repo(dir.path()).unwrap(); let refs = vec!["HEAD:b.luau".to_string(), ":0:b.luau".to_string()]; - let results = git_batch_check_hashes(dir.path(), &refs); + let results = git_batch_check_hashes_impl(&repo, &refs); assert_eq!(results.len(), 2); assert!(results[0].is_some()); assert!(results[1].is_some()); @@ -930,6 +1308,7 @@ mod tests { fs::write(dir.path().join("b.luau"), "content b modified").unwrap(); git_stage(dir.path(), "b.luau"); + let repo = open_repo(dir.path()).unwrap(); let refs = vec![ "HEAD:a.luau".to_string(), ":0:a.luau".to_string(), @@ -937,7 +1316,7 @@ mod tests { ":0:b.luau".to_string(), "HEAD:nonexistent.luau".to_string(), ]; - let results = git_batch_check_hashes(dir.path(), &refs); + let results = git_batch_check_hashes_impl(&repo, &refs); assert_eq!(results.len(), 5); assert_eq!(results[0], results[1]); // a: HEAD == staged @@ -966,8 +1345,9 @@ mod tests { fs::write(dir.path().join("src/init.luau"), "init content").unwrap(); git_commit_all(dir.path(), "init"); + let repo = open_repo(dir.path()).unwrap(); let refs = vec!["HEAD:src/init.luau".to_string()]; - let results = git_batch_check_hashes(dir.path(), &refs); + let results = git_batch_check_hashes_impl(&repo, &refs); assert_eq!(results.len(), 1); assert_eq!( results[0].as_ref().unwrap(), @@ -1014,7 +1394,6 @@ mod tests { fn git_add_empty_paths_is_noop() { let dir = tempdir().unwrap(); git_init(dir.path()); - // Should not panic or error git_add(dir.path(), &[]); } @@ -1046,26 +1425,17 @@ mod tests { } // ----------------------------------------------------------------------- - // Hash consistency: our SHA1 matches git hash-object for various content + // Hash consistency: our SHA1 matches gix blob hashing // ----------------------------------------------------------------------- #[test] fn hash_consistency_empty_file() { let dir = tempdir().unwrap(); git_init(dir.path()); - fs::write(dir.path().join("empty.luau"), "").unwrap(); - let output = Command::new("git") - .args([ - "-C", - &dir.path().to_string_lossy(), - "hash-object", - "empty.luau", - ]) - .output() - .unwrap(); - let git_hash = String::from_utf8_lossy(&output.stdout).trim().to_string(); - assert_eq!(compute_blob_sha1(""), git_hash); + let repo = open_repo(dir.path()).unwrap(); + let gix_hash = repo.write_blob(b"").unwrap().to_hex().to_string(); + assert_eq!(compute_blob_sha1(""), gix_hash); } #[test] @@ -1073,19 +1443,14 @@ mod tests { let content = "-- Unicode: 日本語テスト\nlocal x = '🎮'\n"; let dir = tempdir().unwrap(); git_init(dir.path()); - fs::write(dir.path().join("unicode.luau"), content).unwrap(); - let output = Command::new("git") - .args([ - "-C", - &dir.path().to_string_lossy(), - "hash-object", - "unicode.luau", - ]) - .output() - .unwrap(); - let git_hash = String::from_utf8_lossy(&output.stdout).trim().to_string(); - assert_eq!(compute_blob_sha1(content), git_hash); + let repo = open_repo(dir.path()).unwrap(); + let gix_hash = repo + .write_blob(content.as_bytes()) + .unwrap() + .to_hex() + .to_string(); + assert_eq!(compute_blob_sha1(content), gix_hash); } #[test] @@ -1095,18 +1460,13 @@ mod tests { .collect(); let dir = tempdir().unwrap(); git_init(dir.path()); - fs::write(dir.path().join("large.luau"), &content).unwrap(); - let output = Command::new("git") - .args([ - "-C", - &dir.path().to_string_lossy(), - "hash-object", - "large.luau", - ]) - .output() - .unwrap(); - let git_hash = String::from_utf8_lossy(&output.stdout).trim().to_string(); - assert_eq!(compute_blob_sha1(&content), git_hash); + let repo = open_repo(dir.path()).unwrap(); + let gix_hash = repo + .write_blob(content.as_bytes()) + .unwrap() + .to_hex() + .to_string(); + assert_eq!(compute_blob_sha1(&content), gix_hash); } } diff --git a/src/lib.rs b/src/lib.rs index ed8541cf..a680ce92 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28,6 +28,7 @@ mod web; // TODO: Work out what we should expose publicly +pub use git::{git_add, git_add_all_and_commit}; pub use project::*; pub use rojo_ref::*; pub use serve_session::TreeFreshnessReport; diff --git a/tests/rojo_test/serve_util.rs b/tests/rojo_test/serve_util.rs index a7b93c42..7dea17ae 100644 --- a/tests/rojo_test/serve_util.rs +++ b/tests/rojo_test/serve_util.rs @@ -203,21 +203,17 @@ impl TestServeSession { .expect("Couldn't copy project to temporary directory"); // Initialize git repo and run setup callback - Command::new("git") - .args(["init"]) - .current_dir(&project_path) - .output() - .expect("git init failed"); - Command::new("git") - .args(["config", "user.email", "test@test.com"]) - .current_dir(&project_path) - .output() - .unwrap(); - Command::new("git") - .args(["config", "user.name", "Test"]) - .current_dir(&project_path) - .output() - .unwrap(); + let mut repo = gix::init(&project_path).expect("gix init failed"); + { + let mut config = repo.config_snapshot_mut(); + config + .set_raw_value(&gix::config::tree::User::NAME, "Test") + .unwrap(); + config + .set_raw_value(&gix::config::tree::User::EMAIL, "test@test.com") + .unwrap(); + let _ = config.commit_auto_rollback().unwrap(); + } setup(&project_path); diff --git a/tests/tests/git_sync_defaults.rs b/tests/tests/git_sync_defaults.rs index b8f196b7..ecfc93ab 100644 --- a/tests/tests/git_sync_defaults.rs +++ b/tests/tests/git_sync_defaults.rs @@ -12,7 +12,7 @@ //! - Non-script changes are in changedIds but not in scriptCommittedHashes use std::collections::HashMap; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::process::Command; use std::time::Duration; use std::{fs, thread}; @@ -24,27 +24,15 @@ use rbx_dom_weak::{ustr, UstrMap}; use crate::rojo_test::serve_util::TestServeSession; fn git_commit_all(dir: &Path, msg: &str) { - Command::new("git") - .args(["add", "-A"]) - .current_dir(dir) - .output() - .expect("git add failed"); - Command::new("git") - .args(["commit", "-m", msg]) - .current_dir(dir) - .output() - .expect("git commit failed"); + librojo::git::git_add_all_and_commit(dir, msg); } fn git_stage(dir: &Path, file: &str) { - Command::new("git") - .args(["add", file]) - .current_dir(dir) - .output() - .expect("git stage failed"); + librojo::git_add(dir, &[PathBuf::from(file)]); } fn git_is_staged(dir: &Path, file: &str) -> bool { + use std::process::Command; let output = Command::new("git") .args(["diff", "--cached", "--name-only"]) .current_dir(dir) diff --git a/tests/tests/syncback.rs b/tests/tests/syncback.rs index 437b2f0b..97317dcd 100644 --- a/tests/tests/syncback.rs +++ b/tests/tests/syncback.rs @@ -93,7 +93,7 @@ syncback_tests! { // do not leave any trace of themselves (new files get .json5) ref_properties_pruned => ["src/Pointer1.model.json5", "src/Pointer2.model.json5", "src/Pointer3.model.json5"], // Ensures that the `$schema` field roundtrips with syncback - schema_roundtrip => ["default.project.json5", "src/model.model.json5", "src/init/init.meta.json5", "src/adjacent.meta.json5"], + schema_roundtrip => ["default.project.json5", "src/model.model.json5", "src/init_/init.meta.json5", "src/adjacent.meta.json5"], // Ensures that StringValues inside project files are written to the // project file, but only if they don't have `$path` set string_value_project => ["default.project.json5"],