feat(native): add libgit2-backed git read operations for dispatch hotpath (#388)
This commit is contained in:
parent
ea439b99e4
commit
33cf0dcabd
7 changed files with 805 additions and 39 deletions
362
native/Cargo.lock
generated
362
native/Cargo.lock
generated
|
|
@ -149,6 +149,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"jobserver",
|
||||
"libc",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
|
|
@ -188,7 +190,7 @@ version = "0.1.16"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"getrandom 0.2.17",
|
||||
"once_cell",
|
||||
"tiny-keccak",
|
||||
]
|
||||
|
|
@ -276,6 +278,17 @@ dependencies = [
|
|||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "displaydoc"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.15.0"
|
||||
|
|
@ -396,6 +409,15 @@ version = "0.2.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gethostname"
|
||||
version = "1.1.0"
|
||||
|
|
@ -417,6 +439,18 @@ dependencies = [
|
|||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasip2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gif"
|
||||
version = "0.14.1"
|
||||
|
|
@ -427,6 +461,19 @@ dependencies = [
|
|||
"weezl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "git2"
|
||||
version = "0.20.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b88256088d75a56f8ecfa070513a775dd9107f6530ef14919dac831af9cfe2b"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"libc",
|
||||
"libgit2-sys",
|
||||
"log",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "globset"
|
||||
version = "0.4.18"
|
||||
|
|
@ -533,6 +580,7 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"arboard",
|
||||
"dashmap",
|
||||
"git2",
|
||||
"globset",
|
||||
"gsd-ast",
|
||||
"gsd-grep",
|
||||
|
|
@ -629,6 +677,108 @@ dependencies = [
|
|||
"markup5ever",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_collections"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"potential_utf",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_locale_core"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"litemap",
|
||||
"tinystr",
|
||||
"writeable",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_normalizer"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599"
|
||||
dependencies = [
|
||||
"icu_collections",
|
||||
"icu_normalizer_data",
|
||||
"icu_properties",
|
||||
"icu_provider",
|
||||
"smallvec",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_normalizer_data"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a"
|
||||
|
||||
[[package]]
|
||||
name = "icu_properties"
|
||||
version = "2.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec"
|
||||
dependencies = [
|
||||
"icu_collections",
|
||||
"icu_locale_core",
|
||||
"icu_properties_data",
|
||||
"icu_provider",
|
||||
"zerotrie",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_properties_data"
|
||||
version = "2.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af"
|
||||
|
||||
[[package]]
|
||||
name = "icu_provider"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"icu_locale_core",
|
||||
"writeable",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerotrie",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
|
||||
dependencies = [
|
||||
"idna_adapter",
|
||||
"smallvec",
|
||||
"utf8_iter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna_adapter"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
|
||||
dependencies = [
|
||||
"icu_normalizer",
|
||||
"icu_properties",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ignore"
|
||||
version = "0.4.25"
|
||||
|
|
@ -690,12 +840,34 @@ version = "1.0.17"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
||||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
version = "0.1.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
|
||||
dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.183"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
|
||||
|
||||
[[package]]
|
||||
name = "libgit2-sys"
|
||||
version = "0.18.3+1.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c9b3acc4b91781bb0b3386669d325163746af5f6e4f73e6d2d630e09a35f3487"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"libz-sys",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libloading"
|
||||
version = "0.8.9"
|
||||
|
|
@ -706,12 +878,30 @@ dependencies = [
|
|||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libz-sys"
|
||||
version = "1.1.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d52f4c29e2a68ac30c9087e1b772dc9f44a2b66ed44edf2266cf2be9b03dafc1"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||
|
||||
[[package]]
|
||||
name = "litemap"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.14"
|
||||
|
|
@ -1015,6 +1205,12 @@ dependencies = [
|
|||
"siphasher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
||||
|
||||
[[package]]
|
||||
name = "png"
|
||||
version = "0.18.1"
|
||||
|
|
@ -1028,6 +1224,15 @@ dependencies = [
|
|||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "potential_utf"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77"
|
||||
dependencies = [
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "precomputed-hash"
|
||||
version = "0.1.1"
|
||||
|
|
@ -1064,6 +1269,12 @@ dependencies = [
|
|||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "5.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||
|
||||
[[package]]
|
||||
name = "rayon"
|
||||
version = "1.11.0"
|
||||
|
|
@ -1229,6 +1440,12 @@ version = "1.15.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
|
||||
|
||||
[[package]]
|
||||
name = "streaming-iterator"
|
||||
version = "0.1.9"
|
||||
|
|
@ -1270,6 +1487,17 @@ dependencies = [
|
|||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "synstructure"
|
||||
version = "0.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syntect"
|
||||
version = "5.3.0"
|
||||
|
|
@ -1341,6 +1569,16 @@ dependencies = [
|
|||
"crunchy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinystr"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter"
|
||||
version = "0.25.10"
|
||||
|
|
@ -1749,6 +1987,18 @@ version = "0.2.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"idna",
|
||||
"percent-encoding",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utf-8"
|
||||
version = "0.7.6"
|
||||
|
|
@ -1761,6 +2011,18 @@ version = "0.1.8"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1292c0d970b54115d14f2492fe0170adf21d68a1de108eebc51c1df4f346a091"
|
||||
|
||||
[[package]]
|
||||
name = "utf8_iter"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
|
|
@ -1783,6 +2045,15 @@ version = "0.11.1+wasi-snapshot-preview1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "wasip2"
|
||||
version = "1.0.2+wasi-0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
|
||||
dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web_atoms"
|
||||
version = "0.2.3"
|
||||
|
|
@ -1899,6 +2170,18 @@ version = "0.53.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
|
||||
|
||||
[[package]]
|
||||
name = "writeable"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
|
||||
|
||||
[[package]]
|
||||
name = "x11rb"
|
||||
version = "0.13.2"
|
||||
|
|
@ -1922,6 +2205,29 @@ version = "0.8.15"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3"
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954"
|
||||
dependencies = [
|
||||
"stable_deref_trait",
|
||||
"yoke-derive",
|
||||
"zerofrom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yoke-derive"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.42"
|
||||
|
|
@ -1942,6 +2248,60 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerofrom"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
|
||||
dependencies = [
|
||||
"zerofrom-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerofrom-derive"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerotrie"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerovec"
|
||||
version = "0.11.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002"
|
||||
dependencies = [
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerovec-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerovec-derive"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.21"
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ syntect = { version = "5", default-features = false, features = ["default-syntax
|
|||
unicode-segmentation = "1"
|
||||
unicode-width = "0.2"
|
||||
xxhash-rust = { version = "0.8", features = ["xxh32"] }
|
||||
git2 = { version = "0.20", default-features = false, features = ["vendored-libgit2"] }
|
||||
|
||||
[build-dependencies]
|
||||
napi-build = "2"
|
||||
|
|
|
|||
236
native/crates/engine/src/git.rs
Normal file
236
native/crates/engine/src/git.rs
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
//! Native git operations via libgit2.
|
||||
//!
|
||||
//! Provides fast READ-ONLY git queries for the GSD dispatch hotpath,
|
||||
//! eliminating the need to spawn 25-40 `git` child processes per dispatch.
|
||||
//!
|
||||
//! WRITE operations (commit, merge, checkout, push) remain as execSync
|
||||
//! calls in TypeScript — only status queries are native.
|
||||
|
||||
use git2::{Repository, StatusOptions};
|
||||
use napi::bindgen_prelude::*;
|
||||
use napi_derive::napi;
|
||||
|
||||
/// Open a git repository at the given path.
|
||||
fn open_repo(repo_path: &str) -> Result<Repository> {
|
||||
Repository::open(repo_path).map_err(|e| {
|
||||
Error::new(
|
||||
Status::GenericFailure,
|
||||
format!("Failed to open git repository at {repo_path}: {e}"),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the current branch name (HEAD symbolic ref).
|
||||
/// Returns None if HEAD is detached.
|
||||
#[napi]
|
||||
pub fn git_current_branch(repo_path: String) -> Result<Option<String>> {
|
||||
let repo = open_repo(&repo_path)?;
|
||||
let head = repo.head().map_err(|e| {
|
||||
Error::new(
|
||||
Status::GenericFailure,
|
||||
format!("Failed to read HEAD: {e}"),
|
||||
)
|
||||
})?;
|
||||
|
||||
if head.is_branch() {
|
||||
Ok(head.shorthand().map(String::from))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Detect the main/integration branch for a repository.
|
||||
///
|
||||
/// Resolution order:
|
||||
/// 1. refs/remotes/origin/HEAD → extract branch name
|
||||
/// 2. refs/heads/main exists → "main"
|
||||
/// 3. refs/heads/master exists → "master"
|
||||
/// 4. Fall back to current branch
|
||||
///
|
||||
/// Note: milestone integration branch and worktree detection are handled
|
||||
/// in TypeScript — this function covers the repo-level default detection
|
||||
/// that previously spawned 4 `git show-ref` / `git symbolic-ref` calls.
|
||||
#[napi]
|
||||
pub fn git_main_branch(repo_path: String) -> Result<String> {
|
||||
let repo = open_repo(&repo_path)?;
|
||||
|
||||
// Check origin/HEAD symbolic ref
|
||||
if let Ok(reference) = repo.find_reference("refs/remotes/origin/HEAD") {
|
||||
if let Ok(resolved) = reference.resolve() {
|
||||
if let Some(name) = resolved.name() {
|
||||
if let Some(branch) = name.strip_prefix("refs/remotes/origin/") {
|
||||
return Ok(branch.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check refs/heads/main
|
||||
if repo.find_reference("refs/heads/main").is_ok() {
|
||||
return Ok("main".to_string());
|
||||
}
|
||||
|
||||
// Check refs/heads/master
|
||||
if repo.find_reference("refs/heads/master").is_ok() {
|
||||
return Ok("master".to_string());
|
||||
}
|
||||
|
||||
// Fall back to current branch
|
||||
let head = repo.head().map_err(|e| {
|
||||
Error::new(
|
||||
Status::GenericFailure,
|
||||
format!("Failed to read HEAD: {e}"),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(head.shorthand().unwrap_or("HEAD").to_string())
|
||||
}
|
||||
|
||||
/// Check if a local branch exists (refs/heads/<name>).
|
||||
#[napi]
|
||||
pub fn git_branch_exists(repo_path: String, branch: String) -> Result<bool> {
|
||||
let repo = open_repo(&repo_path)?;
|
||||
let refname = format!("refs/heads/{branch}");
|
||||
let exists = repo.find_reference(&refname).is_ok();
|
||||
Ok(exists)
|
||||
}
|
||||
|
||||
/// Check if the repository index has unmerged entries (merge conflicts).
|
||||
#[napi]
|
||||
pub fn git_has_merge_conflicts(repo_path: String) -> Result<bool> {
|
||||
let repo = open_repo(&repo_path)?;
|
||||
let index = repo.index().map_err(|e| {
|
||||
Error::new(
|
||||
Status::GenericFailure,
|
||||
format!("Failed to read index: {e}"),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(index.has_conflicts())
|
||||
}
|
||||
|
||||
/// Get working tree status in porcelain format.
|
||||
/// Returns a string where each line is "XY path" (git status --porcelain).
|
||||
#[napi]
|
||||
pub fn git_working_tree_status(repo_path: String) -> Result<String> {
|
||||
let repo = open_repo(&repo_path)?;
|
||||
let mut opts = StatusOptions::new();
|
||||
opts.include_untracked(true)
|
||||
.recurse_untracked_dirs(true);
|
||||
|
||||
let statuses = repo.statuses(Some(&mut opts)).map_err(|e| {
|
||||
Error::new(
|
||||
Status::GenericFailure,
|
||||
format!("Failed to get status: {e}"),
|
||||
)
|
||||
})?;
|
||||
|
||||
let mut lines = Vec::with_capacity(statuses.len());
|
||||
for entry in statuses.iter() {
|
||||
let status = entry.status();
|
||||
let path = entry.path().unwrap_or("?");
|
||||
|
||||
let index_char = if status.is_index_new() {
|
||||
'A'
|
||||
} else if status.is_index_modified() {
|
||||
'M'
|
||||
} else if status.is_index_deleted() {
|
||||
'D'
|
||||
} else if status.is_index_renamed() {
|
||||
'R'
|
||||
} else if status.is_index_typechange() {
|
||||
'T'
|
||||
} else {
|
||||
' '
|
||||
};
|
||||
|
||||
let wt_char = if status.is_wt_new() {
|
||||
'?'
|
||||
} else if status.is_wt_modified() {
|
||||
'M'
|
||||
} else if status.is_wt_deleted() {
|
||||
'D'
|
||||
} else if status.is_wt_renamed() {
|
||||
'R'
|
||||
} else if status.is_wt_typechange() {
|
||||
'T'
|
||||
} else {
|
||||
' '
|
||||
};
|
||||
|
||||
lines.push(format!("{index_char}{wt_char} {path}"));
|
||||
}
|
||||
|
||||
Ok(lines.join("\n"))
|
||||
}
|
||||
|
||||
/// Quick check: are there any staged or unstaged changes in the working tree?
|
||||
#[napi]
|
||||
pub fn git_has_changes(repo_path: String) -> Result<bool> {
|
||||
let repo = open_repo(&repo_path)?;
|
||||
let mut opts = StatusOptions::new();
|
||||
opts.include_untracked(true);
|
||||
|
||||
let statuses = repo.statuses(Some(&mut opts)).map_err(|e| {
|
||||
Error::new(
|
||||
Status::GenericFailure,
|
||||
format!("Failed to get status: {e}"),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(!statuses.is_empty())
|
||||
}
|
||||
|
||||
/// Count commits between two refs (equivalent to `git rev-list --count from..to`).
|
||||
#[napi]
|
||||
pub fn git_commit_count_between(
|
||||
repo_path: String,
|
||||
from_ref: String,
|
||||
to_ref: String,
|
||||
) -> Result<u32> {
|
||||
let repo = open_repo(&repo_path)?;
|
||||
|
||||
let from_oid = repo
|
||||
.revparse_single(&from_ref)
|
||||
.map_err(|e| {
|
||||
Error::new(
|
||||
Status::GenericFailure,
|
||||
format!("Failed to resolve ref '{from_ref}': {e}"),
|
||||
)
|
||||
})?
|
||||
.id();
|
||||
|
||||
let to_oid = repo
|
||||
.revparse_single(&to_ref)
|
||||
.map_err(|e| {
|
||||
Error::new(
|
||||
Status::GenericFailure,
|
||||
format!("Failed to resolve ref '{to_ref}': {e}"),
|
||||
)
|
||||
})?
|
||||
.id();
|
||||
|
||||
let mut revwalk = repo.revwalk().map_err(|e| {
|
||||
Error::new(
|
||||
Status::GenericFailure,
|
||||
format!("Failed to create revwalk: {e}"),
|
||||
)
|
||||
})?;
|
||||
|
||||
revwalk.push(to_oid).map_err(|e| {
|
||||
Error::new(
|
||||
Status::GenericFailure,
|
||||
format!("Failed to push to_ref: {e}"),
|
||||
)
|
||||
})?;
|
||||
|
||||
revwalk.hide(from_oid).map_err(|e| {
|
||||
Error::new(
|
||||
Status::GenericFailure,
|
||||
format!("Failed to hide from_ref: {e}"),
|
||||
)
|
||||
})?;
|
||||
|
||||
let count = revwalk.count() as u32;
|
||||
Ok(count)
|
||||
}
|
||||
|
|
@ -27,3 +27,4 @@ mod truncate;
|
|||
mod json_parse;
|
||||
mod stream_process;
|
||||
mod xxhash;
|
||||
mod git;
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ import {
|
|||
mergeSliceToMain,
|
||||
} from "./worktree.js";
|
||||
import { GitServiceImpl, runGit } from "./git-service.js";
|
||||
import { nativeCommitCountBetween } from "./native-git-bridge.js";
|
||||
import { getPriorSliceCompletionBlocker } from "./dispatch-guard.js";
|
||||
import type { GitPreferences } from "./git-service.js";
|
||||
import { truncateToWidth, visibleWidth } from "@gsd/pi-tui";
|
||||
|
|
@ -473,12 +474,8 @@ async function mergeOrphanedSliceBranches(
|
|||
|
||||
// Skip if already merged (no commits ahead of main)
|
||||
const mainBranch = getMainBranch(base);
|
||||
const aheadCount = runGit(
|
||||
base,
|
||||
["rev-list", "--count", `${mainBranch}..${branch}`],
|
||||
{ allowFailure: true },
|
||||
);
|
||||
if (!aheadCount || aheadCount === "0") continue;
|
||||
const aheadCount = nativeCommitCountBetween(base, mainBranch, branch);
|
||||
if (aheadCount === 0) continue;
|
||||
|
||||
// Read the roadmap from the slice branch to check if the slice is done.
|
||||
// relMilestoneFile resolves the actual directory name on disk (handles
|
||||
|
|
|
|||
|
|
@ -17,6 +17,14 @@ import {
|
|||
getSliceBranchName,
|
||||
SLICE_BRANCH_RE,
|
||||
} from "./worktree.js";
|
||||
import {
|
||||
nativeGetCurrentBranch,
|
||||
nativeDetectMainBranch,
|
||||
nativeBranchExists,
|
||||
nativeHasMergeConflicts,
|
||||
nativeHasChanges,
|
||||
nativeCommitCountBetween,
|
||||
} from "./native-git-bridge.js";
|
||||
|
||||
// ─── Types ─────────────────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -356,8 +364,8 @@ export class GitServiceImpl {
|
|||
*/
|
||||
autoCommit(unitType: string, unitId: string, extraExclusions: readonly string[] = []): string | null {
|
||||
// Quick check: is there anything dirty at all?
|
||||
const status = this.git(["status", "--short"], { allowFailure: true });
|
||||
if (!status) return null;
|
||||
// Native path uses libgit2 (single syscall), fallback spawns git.
|
||||
if (!nativeHasChanges(this.basePath)) return null;
|
||||
|
||||
this.smartStage(extraExclusions);
|
||||
|
||||
|
|
@ -400,37 +408,25 @@ export class GitServiceImpl {
|
|||
const integrationBranch = readIntegrationBranch(this.basePath, this._milestoneId);
|
||||
if (integrationBranch) {
|
||||
// Verify the branch still exists locally (could have been deleted)
|
||||
const exists = this.git(["show-ref", "--verify", `refs/heads/${integrationBranch}`], { allowFailure: true });
|
||||
if (exists) return integrationBranch;
|
||||
if (nativeBranchExists(this.basePath, integrationBranch)) return integrationBranch;
|
||||
}
|
||||
}
|
||||
|
||||
const wtName = detectWorktreeName(this.basePath);
|
||||
if (wtName) {
|
||||
const wtBranch = `worktree/${wtName}`;
|
||||
const exists = this.git(["show-ref", "--verify", `refs/heads/${wtBranch}`], { allowFailure: true });
|
||||
if (exists) return wtBranch;
|
||||
return this.git(["branch", "--show-current"]);
|
||||
if (nativeBranchExists(this.basePath, wtBranch)) return wtBranch;
|
||||
return nativeGetCurrentBranch(this.basePath);
|
||||
}
|
||||
|
||||
const symbolic = this.git(["symbolic-ref", "refs/remotes/origin/HEAD"], { allowFailure: true });
|
||||
if (symbolic) {
|
||||
const match = symbolic.match(/refs\/remotes\/origin\/(.+)$/);
|
||||
if (match) return match[1]!;
|
||||
}
|
||||
|
||||
const mainExists = this.git(["show-ref", "--verify", "refs/heads/main"], { allowFailure: true });
|
||||
if (mainExists) return "main";
|
||||
|
||||
const masterExists = this.git(["show-ref", "--verify", "refs/heads/master"], { allowFailure: true });
|
||||
if (masterExists) return "master";
|
||||
|
||||
return this.git(["branch", "--show-current"]);
|
||||
// Repo-level default detection: origin/HEAD → main → master → current branch.
|
||||
// Native path uses libgit2 (single call), fallback spawns multiple git processes.
|
||||
return nativeDetectMainBranch(this.basePath);
|
||||
}
|
||||
|
||||
/** Get the current branch name. */
|
||||
/** Get the current branch name. Native libgit2 when available, execSync fallback. */
|
||||
getCurrentBranch(): string {
|
||||
return this.git(["branch", "--show-current"]);
|
||||
return nativeGetCurrentBranch(this.basePath);
|
||||
}
|
||||
|
||||
/** True if currently on a GSD slice branch. */
|
||||
|
|
@ -452,15 +448,10 @@ export class GitServiceImpl {
|
|||
// ─── Branch Lifecycle ──────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Check if a local branch exists.
|
||||
* Check if a local branch exists. Native libgit2 when available, execSync fallback.
|
||||
*/
|
||||
private branchExists(branch: string): boolean {
|
||||
try {
|
||||
this.git(["show-ref", "--verify", "--quiet", `refs/heads/${branch}`]);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return nativeBranchExists(this.basePath, branch);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -715,9 +706,9 @@ export class GitServiceImpl {
|
|||
);
|
||||
}
|
||||
|
||||
// Check commits ahead
|
||||
const aheadCount = this.git(["rev-list", "--count", `${mainBranch}..${branch}`]);
|
||||
if (aheadCount === "0") {
|
||||
// Check commits ahead — native libgit2 revwalk when available
|
||||
const aheadCount = nativeCommitCountBetween(this.basePath, mainBranch, branch);
|
||||
if (aheadCount === 0) {
|
||||
throw new Error(
|
||||
`Slice branch "${branch}" has no commits ahead of "${mainBranch}". Nothing to merge.`,
|
||||
);
|
||||
|
|
|
|||
180
src/resources/extensions/gsd/native-git-bridge.ts
Normal file
180
src/resources/extensions/gsd/native-git-bridge.ts
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
// Native Git Bridge
|
||||
// Provides fast READ-ONLY git operations backed by libgit2 via the Rust native module.
|
||||
// Falls back to execSync git commands when the native module is unavailable.
|
||||
//
|
||||
// Only READ operations are native — WRITE operations (commit, merge, checkout, push)
|
||||
// remain as execSync calls in git-service.ts.
|
||||
|
||||
import { execSync } from "node:child_process";
|
||||
|
||||
/** Env overlay that suppresses all interactive git credential prompts. */
|
||||
const GIT_NO_PROMPT_ENV = {
|
||||
...process.env,
|
||||
GIT_TERMINAL_PROMPT: "0",
|
||||
GIT_ASKPASS: "",
|
||||
};
|
||||
|
||||
let nativeModule: {
|
||||
gitCurrentBranch: (repoPath: string) => string | null;
|
||||
gitMainBranch: (repoPath: string) => string;
|
||||
gitBranchExists: (repoPath: string, branch: string) => boolean;
|
||||
gitHasMergeConflicts: (repoPath: string) => boolean;
|
||||
gitWorkingTreeStatus: (repoPath: string) => string;
|
||||
gitHasChanges: (repoPath: string) => boolean;
|
||||
gitCommitCountBetween: (repoPath: string, fromRef: string, toRef: string) => number;
|
||||
} | null = null;
|
||||
|
||||
let loadAttempted = false;
|
||||
|
||||
function loadNative(): typeof nativeModule {
|
||||
if (loadAttempted) return nativeModule;
|
||||
loadAttempted = true;
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const mod = require("@gsd/native");
|
||||
if (mod.gitCurrentBranch && mod.gitHasChanges) {
|
||||
nativeModule = mod;
|
||||
}
|
||||
} catch {
|
||||
// Native module not available — all functions fall back to git CLI
|
||||
}
|
||||
|
||||
return nativeModule;
|
||||
}
|
||||
|
||||
/** Run a git command via execSync. Returns trimmed stdout. */
|
||||
function gitExec(basePath: string, args: string[], allowFailure = false): string {
|
||||
try {
|
||||
return execSync(`git ${args.join(" ")}`, {
|
||||
cwd: basePath,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
encoding: "utf-8",
|
||||
env: GIT_NO_PROMPT_ENV,
|
||||
}).trim();
|
||||
} catch {
|
||||
if (allowFailure) return "";
|
||||
throw new Error(`git ${args.join(" ")} failed in ${basePath}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current branch name.
|
||||
* Native: reads HEAD symbolic ref via libgit2.
|
||||
* Fallback: `git branch --show-current`.
|
||||
*/
|
||||
export function nativeGetCurrentBranch(basePath: string): string {
|
||||
const native = loadNative();
|
||||
if (native) {
|
||||
const branch = native.gitCurrentBranch(basePath);
|
||||
return branch ?? "";
|
||||
}
|
||||
return gitExec(basePath, ["branch", "--show-current"]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect the repo-level main branch (origin/HEAD → main → master → current).
|
||||
* Native: checks refs via libgit2.
|
||||
* Fallback: `git symbolic-ref` + `git show-ref` chain.
|
||||
*
|
||||
* Note: milestone integration branch and worktree detection are handled
|
||||
* by the caller (GitServiceImpl.getMainBranch) — this only covers the
|
||||
* repo-level default detection that spawned multiple git processes.
|
||||
*/
|
||||
export function nativeDetectMainBranch(basePath: string): string {
|
||||
const native = loadNative();
|
||||
if (native) {
|
||||
return native.gitMainBranch(basePath);
|
||||
}
|
||||
|
||||
// Fallback: same logic as GitServiceImpl.getMainBranch() repo-level detection
|
||||
const symbolic = gitExec(basePath, ["symbolic-ref", "refs/remotes/origin/HEAD"], true);
|
||||
if (symbolic) {
|
||||
const match = symbolic.match(/refs\/remotes\/origin\/(.+)$/);
|
||||
if (match) return match[1]!;
|
||||
}
|
||||
|
||||
const mainExists = gitExec(basePath, ["show-ref", "--verify", "refs/heads/main"], true);
|
||||
if (mainExists) return "main";
|
||||
|
||||
const masterExists = gitExec(basePath, ["show-ref", "--verify", "refs/heads/master"], true);
|
||||
if (masterExists) return "master";
|
||||
|
||||
return gitExec(basePath, ["branch", "--show-current"]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a local branch exists.
|
||||
* Native: checks refs/heads/<name> via libgit2.
|
||||
* Fallback: `git show-ref --verify`.
|
||||
*/
|
||||
export function nativeBranchExists(basePath: string, branch: string): boolean {
|
||||
const native = loadNative();
|
||||
if (native) {
|
||||
return native.gitBranchExists(basePath, branch);
|
||||
}
|
||||
const result = gitExec(basePath, ["show-ref", "--verify", `refs/heads/${branch}`], true);
|
||||
return result !== "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the index has unmerged entries (merge conflicts).
|
||||
* Native: reads index conflict state via libgit2.
|
||||
* Fallback: `git diff --name-only --diff-filter=U`.
|
||||
*/
|
||||
export function nativeHasMergeConflicts(basePath: string): boolean {
|
||||
const native = loadNative();
|
||||
if (native) {
|
||||
return native.gitHasMergeConflicts(basePath);
|
||||
}
|
||||
const result = gitExec(basePath, ["diff", "--name-only", "--diff-filter=U"], true);
|
||||
return result !== "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get working tree status (porcelain format).
|
||||
* Native: reads status via libgit2.
|
||||
* Fallback: `git status --porcelain`.
|
||||
*/
|
||||
export function nativeWorkingTreeStatus(basePath: string): string {
|
||||
const native = loadNative();
|
||||
if (native) {
|
||||
return native.gitWorkingTreeStatus(basePath);
|
||||
}
|
||||
return gitExec(basePath, ["status", "--porcelain"], true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Quick check: any staged or unstaged changes?
|
||||
* Native: libgit2 status check (single syscall).
|
||||
* Fallback: `git status --short`.
|
||||
*/
|
||||
export function nativeHasChanges(basePath: string): boolean {
|
||||
const native = loadNative();
|
||||
if (native) {
|
||||
return native.gitHasChanges(basePath);
|
||||
}
|
||||
const result = gitExec(basePath, ["status", "--short"], true);
|
||||
return result !== "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Count commits between two refs (from..to).
|
||||
* Native: libgit2 revwalk.
|
||||
* Fallback: `git rev-list --count from..to`.
|
||||
*/
|
||||
export function nativeCommitCountBetween(basePath: string, fromRef: string, toRef: string): number {
|
||||
const native = loadNative();
|
||||
if (native) {
|
||||
return native.gitCommitCountBetween(basePath, fromRef, toRef);
|
||||
}
|
||||
const result = gitExec(basePath, ["rev-list", "--count", `${fromRef}..${toRef}`], true);
|
||||
return parseInt(result, 10) || 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the native git module is available.
|
||||
*/
|
||||
export function isNativeGitAvailable(): boolean {
|
||||
return loadNative() !== null;
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue