diff --git a/packages/worktree/.gitignore b/packages/worktree/.gitignore deleted file mode 100644 index b83d22266..000000000 --- a/packages/worktree/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/target/ diff --git a/packages/worktree/Cargo.lock b/packages/worktree/Cargo.lock deleted file mode 100644 index f446253de..000000000 --- a/packages/worktree/Cargo.lock +++ /dev/null @@ -1,992 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "anstream" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" - -[[package]] -name = "anstyle-parse" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" -dependencies = [ - "windows-sys", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" -dependencies = [ - "anstyle", - "once_cell_polyfill", - "windows-sys", -] - -[[package]] -name = "anyhow" -version = "1.0.102" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" - -[[package]] -name = "bitflags" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" - -[[package]] -name = "bumpalo" -version = "3.20.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" - -[[package]] -name = "cc" -version = "1.2.63" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" -dependencies = [ - "find-msvc-tools", - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "clap" -version = "4.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_derive" -version = "4.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "clap_lex" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" - -[[package]] -name = "colorchoice" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" - -[[package]] -name = "dirs" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" -dependencies = [ - "dirs-sys", -] - -[[package]] -name = "dirs-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" -dependencies = [ - "libc", - "option-ext", - "redox_users", - "windows-sys", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys", -] - -[[package]] -name = "fallible-iterator" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" - -[[package]] -name = "fallible-streaming-iterator" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" - -[[package]] -name = "fastrand" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" - -[[package]] -name = "filetime" -version = "0.2.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" -dependencies = [ - "cfg-if", - "libc", -] - -[[package]] -name = "find-msvc-tools" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" - -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[package]] -name = "futures-core" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" - -[[package]] -name = "futures-task" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" - -[[package]] -name = "futures-util" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" -dependencies = [ - "futures-core", - "futures-task", - "pin-project-lite", - "slab", -] - -[[package]] -name = "getrandom" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" -dependencies = [ - "cfg-if", - "libc", - "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 5.3.0", - "wasip2", -] - -[[package]] -name = "getrandom" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" -dependencies = [ - "cfg-if", - "libc", - "r-efi 6.0.0", - "wasip2", - "wasip3", -] - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash", -] - -[[package]] -name = "hashbrown" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" - -[[package]] -name = "hashlink" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" -dependencies = [ - "hashbrown 0.15.5", -] - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - -[[package]] -name = "indexmap" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" -dependencies = [ - "equivalent", - "hashbrown 0.17.1", - "serde", - "serde_core", -] - -[[package]] -name = "is_terminal_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "js-sys" -version = "0.3.99" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" -dependencies = [ - "cfg-if", - "futures-util", - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - -[[package]] -name = "libc" -version = "0.2.186" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" - -[[package]] -name = "libredox" -version = "0.1.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" -dependencies = [ - "libc", -] - -[[package]] -name = "libsqlite3-sys" -version = "0.33.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "947e6816f7825b2b45027c2c32e7085da9934defa535de4a6a46b10a4d5257fa" -dependencies = [ - "cc", - "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 = "log" -version = "0.4.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" - -[[package]] -name = "memchr" -version = "2.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "once_cell_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" - -[[package]] -name = "option-ext" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" - -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[package]] -name = "pkg-config" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn", -] - -[[package]] -name = "proc-macro2" -version = "1.0.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" -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 = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - -[[package]] -name = "rand" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" -dependencies = [ - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" -dependencies = [ - "getrandom 0.3.4", -] - -[[package]] -name = "redox_users" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" -dependencies = [ - "getrandom 0.2.17", - "libredox", - "thiserror", -] - -[[package]] -name = "rusqlite" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a22715a5d6deef63c637207afbe68d0c72c3f8d0022d7cf9714c442d6157606b" -dependencies = [ - "bitflags", - "fallible-iterator", - "fallible-streaming-iterator", - "hashlink", - "libsqlite3-sys", - "smallvec", -] - -[[package]] -name = "rustix" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "semver" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.150" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "shlex" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" - -[[package]] -name = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "syn" -version = "2.0.117" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "tempfile" -version = "3.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" -dependencies = [ - "fastrand", - "getrandom 0.4.2", - "once_cell", - "rustix", - "windows-sys", -] - -[[package]] -name = "thiserror" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "ulid" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "470dbf6591da1b39d43c14523b2b469c86879a53e8b758c8e090a470fe7b1fbe" -dependencies = [ - "rand", - "web-time", -] - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - -[[package]] -name = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "wasi" -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.3+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" -dependencies = [ - "wit-bindgen 0.57.1", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen 0.51.0", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.122" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.122" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.122" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.122" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen" -version = "0.57.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" - -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - -[[package]] -name = "worktree" -version = "0.1.0" -dependencies = [ - "dirs", - "filetime", - "libc", - "rusqlite", - "tempfile", - "thiserror", - "ulid", - "walkdir", -] - -[[package]] -name = "worktree-cli" -version = "0.1.0" -dependencies = [ - "clap", - "worktree", -] - -[[package]] -name = "zerocopy" -version = "0.8.50" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.50" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zmij" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/packages/worktree/Cargo.toml b/packages/worktree/Cargo.toml deleted file mode 100644 index e30a9d16b..000000000 --- a/packages/worktree/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -[workspace] -resolver = "2" -members = ["crates/core", "crates/cli"] - -[workspace.package] -edition = "2024" -license = "MIT" -version = "0.1.0" - -[workspace.dependencies] -clap = { version = "4.5", features = ["derive"] } -dirs = "6.0" -filetime = "0.2" -libc = "0.2" -rusqlite = { version = "0.35", features = ["bundled"] } -tempfile = "3.20" -thiserror = "2.0" -ulid = "1.2" -walkdir = "2.5" diff --git a/packages/worktree/crates/cli/Cargo.toml b/packages/worktree/crates/cli/Cargo.toml deleted file mode 100644 index e19bd5089..000000000 --- a/packages/worktree/crates/cli/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "worktree-cli" -version.workspace = true -edition.workspace = true -license.workspace = true - -[[bin]] -name = "worktree" -path = "src/main.rs" - -[dependencies] -clap.workspace = true -worktree = { path = "../core" } diff --git a/packages/worktree/crates/cli/src/main.rs b/packages/worktree/crates/cli/src/main.rs deleted file mode 100644 index 03acc84b7..000000000 --- a/packages/worktree/crates/cli/src/main.rs +++ /dev/null @@ -1,79 +0,0 @@ -use clap::{Parser, Subcommand}; -use std::path::PathBuf; -use worktree::{Create, Link, Manager}; - -#[derive(Parser)] -#[command(name = "worktree")] -struct Cli { - #[arg(long, hide = true)] - database: Option, - #[command(subcommand)] - command: Command, -} - -#[derive(Subcommand)] -enum Command { - Create { - from: Option, - #[arg(long)] - name: Option, - #[arg(long)] - into: Option, - }, - Remove { - at: PathBuf, - }, - Link { - at: PathBuf, - #[arg(long)] - to: Option, - }, - Children { - of: PathBuf, - }, - Ancestors { - of: PathBuf, - }, -} - -fn main() { - if let Err(error) = run() { - eprintln!("worktree: {error}"); - std::process::exit(1); - } -} - -fn run() -> worktree::Result<()> { - let cli = Cli::parse(); - let mut manager = match cli.database { - Some(path) => Manager::open(path)?, - None => Manager::open_default()?, - }; - match cli.command { - Command::Create { from, name, into } => { - println!( - "{}", - manager - .create(Create { - from: from.unwrap_or(std::env::current_dir()?), - name, - into, - })? - .display() - ); - } - Command::Remove { at } => manager.remove(at)?, - Command::Link { at, to } => manager.link(Link { at, to })?, - Command::Children { of } => { - for path in manager.children(of)? { - println!("{}", path.display()); - } - } - Command::Ancestors { of } => { - for path in manager.ancestors(of)? { - println!("{}", path.display()); - } - } - } - Ok(()) -} diff --git a/packages/worktree/crates/core/Cargo.toml b/packages/worktree/crates/core/Cargo.toml deleted file mode 100644 index be96fc46a..000000000 --- a/packages/worktree/crates/core/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[package] -name = "worktree" -version.workspace = true -edition.workspace = true -license.workspace = true - -[dependencies] -dirs.workspace = true -filetime.workspace = true -libc.workspace = true -rusqlite.workspace = true -thiserror.workspace = true -ulid.workspace = true -walkdir.workspace = true - -[dev-dependencies] -tempfile.workspace = true diff --git a/packages/worktree/crates/core/src/copy.rs b/packages/worktree/crates/core/src/copy.rs deleted file mode 100644 index fa08c747c..000000000 --- a/packages/worktree/crates/core/src/copy.rs +++ /dev/null @@ -1,195 +0,0 @@ -use crate::{Error, Result}; -#[cfg(target_os = "linux")] -use filetime::{FileTime, set_file_times}; -use std::fs; -#[cfg(target_os = "linux")] -use std::fs::{File, OpenOptions}; -use std::path::Path; -#[cfg(any(target_os = "linux", test))] -use walkdir::WalkDir; - -pub(crate) trait CopyStrategy { - fn copy_directory(&self, from: &Path, to: &Path) -> Result<()>; -} - -pub(crate) struct CowStrategy; - -impl CopyStrategy for CowStrategy { - fn copy_directory(&self, from: &Path, to: &Path) -> Result<()> { - #[cfg(target_os = "linux")] - return copy_directory_linux(from, to); - - #[cfg(target_os = "macos")] - return copy_directory_macos(from, to); - - #[cfg(not(any(target_os = "linux", target_os = "macos")))] - { - let _ = (from, to); - Err(Error::CowUnavailable( - "no copy-on-write strategy has been implemented for this platform".into(), - )) - } - } -} - -#[cfg(target_os = "macos")] -fn copy_directory_macos(from: &Path, to: &Path) -> Result<()> { - use std::ffi::CString; - use std::os::unix::ffi::OsStrExt; - - let source = CString::new(from.as_os_str().as_bytes()) - .map_err(|_| Error::Path(format!("path contains a null byte: {}", from.display())))?; - let destination = CString::new(to.as_os_str().as_bytes()) - .map_err(|_| Error::Path(format!("path contains a null byte: {}", to.display())))?; - let result = unsafe { libc::clonefile(source.as_ptr(), destination.as_ptr(), 0) }; - if result == 0 { - return Ok(()); - } - Err(Error::CowUnavailable(format!( - "failed to clone {}: {}", - from.display(), - std::io::Error::last_os_error() - ))) -} - -#[cfg(target_os = "linux")] -fn copy_directory_linux(from: &Path, to: &Path) -> Result<()> { - fs::create_dir(to)?; - fs::set_permissions(to, fs::metadata(from)?.permissions())?; - - let entries = WalkDir::new(from) - .min_depth(1) - .follow_links(false) - .into_iter() - .collect::, _>>()?; - - for entry in &entries { - let relative = entry - .path() - .strip_prefix(from) - .map_err(|error| Error::Path(error.to_string()))?; - let destination = to.join(relative); - let metadata = fs::symlink_metadata(entry.path())?; - if metadata.is_dir() { - fs::create_dir(&destination)?; - fs::set_permissions(&destination, metadata.permissions())?; - continue; - } - if metadata.is_symlink() { - copy_symlink(entry.path(), &destination)?; - continue; - } - if !metadata.is_file() { - return Err(Error::UnsupportedEntry(entry.path().to_path_buf())); - } - reflink_file(entry.path(), &destination)?; - fs::set_permissions(&destination, metadata.permissions())?; - set_file_times( - &destination, - FileTime::from_last_access_time(&metadata), - FileTime::from_last_modification_time(&metadata), - )?; - } - - for entry in entries - .iter() - .rev() - .filter(|entry| entry.file_type().is_dir()) - { - let destination = to.join( - entry - .path() - .strip_prefix(from) - .map_err(|error| Error::Path(error.to_string()))?, - ); - let metadata = fs::metadata(entry.path())?; - set_file_times( - &destination, - FileTime::from_last_access_time(&metadata), - FileTime::from_last_modification_time(&metadata), - )?; - } - - let metadata = fs::metadata(from)?; - set_file_times( - to, - FileTime::from_last_access_time(&metadata), - FileTime::from_last_modification_time(&metadata), - )?; - Ok(()) -} - -#[cfg(target_os = "linux")] -fn reflink_file(from: &Path, to: &Path) -> Result<()> { - use std::os::fd::AsRawFd; - - const FICLONE: libc::c_ulong = 0x4004_9409; - let source = File::open(from)?; - let destination = OpenOptions::new().write(true).create_new(true).open(to)?; - let result = unsafe { libc::ioctl(destination.as_raw_fd(), FICLONE, source.as_raw_fd()) }; - if result == 0 { - return Ok(()); - } - let error = std::io::Error::last_os_error(); - Err(Error::CowUnavailable(format!( - "failed to reflink {}: {}", - from.display(), - error - ))) -} - -#[cfg(unix)] -fn copy_symlink(from: &Path, to: &Path) -> Result<()> { - std::os::unix::fs::symlink(fs::read_link(from)?, to)?; - Ok(()) -} - -#[cfg(windows)] -fn copy_symlink(from: &Path, to: &Path) -> Result<()> { - let target = fs::read_link(from)?; - if fs::metadata(from)?.is_dir() { - std::os::windows::fs::symlink_dir(target, to)?; - return Ok(()); - } - std::os::windows::fs::symlink_file(target, to)?; - Ok(()) -} - -#[cfg(test)] -pub(crate) struct TestStrategy; - -#[cfg(test)] -impl CopyStrategy for TestStrategy { - fn copy_directory(&self, from: &Path, to: &Path) -> Result<()> { - fs::create_dir(to)?; - for entry in WalkDir::new(from).min_depth(1).follow_links(false) { - let entry = entry?; - let destination = to.join( - entry - .path() - .strip_prefix(from) - .map_err(|error| Error::Path(error.to_string()))?, - ); - if entry.file_type().is_dir() { - fs::create_dir(&destination)?; - continue; - } - if entry.file_type().is_symlink() { - copy_symlink(entry.path(), &destination)?; - continue; - } - fs::copy(entry.path(), destination)?; - } - Ok(()) - } -} - -#[cfg(test)] -pub(crate) struct FailureStrategy; - -#[cfg(test)] -impl CopyStrategy for FailureStrategy { - fn copy_directory(&self, _from: &Path, _to: &Path) -> Result<()> { - Err(Error::CowUnavailable("test failure".into())) - } -} diff --git a/packages/worktree/crates/core/src/git.rs b/packages/worktree/crates/core/src/git.rs deleted file mode 100644 index ed6769caa..000000000 --- a/packages/worktree/crates/core/src/git.rs +++ /dev/null @@ -1,75 +0,0 @@ -use crate::{Error, Result}; -use std::fs; -use std::path::Path; -use std::process::Command; - -pub(crate) fn check_source(path: &Path) -> Result { - let git = path.join(".git"); - if !git.exists() { - return Ok(false); - } - if !git.is_dir() { - return Err(Error::UnsafeGit( - "linked Git worktree sources are not supported".into(), - )); - } - - for state in [ - "MERGE_HEAD", - "CHERRY_PICK_HEAD", - "REVERT_HEAD", - "BISECT_LOG", - "rebase-merge", - "rebase-apply", - "index.lock", - "HEAD.lock", - ] { - if git.join(state).exists() { - return Err(Error::UnsafeGit(format!("Git state in progress: {state}"))); - } - } - Ok(true) -} - -pub(crate) fn hide_marker(path: &Path) -> Result<()> { - let info = path.join(".git").join("info"); - fs::create_dir_all(&info)?; - let exclude = info.join("exclude"); - let existing = if exclude.exists() { - fs::read_to_string(&exclude)? - } else { - String::new() - }; - if existing.lines().any(|line| line.trim() == "/.worktree") { - return Ok(()); - } - let separator = if existing.is_empty() || existing.ends_with('\n') { - "" - } else { - "\n" - }; - fs::write(exclude, format!("{existing}{separator}/.worktree\n"))?; - Ok(()) -} - -pub(crate) fn detach_destination(path: &Path) -> Result<()> { - let head = Command::new("git") - .arg("-C") - .arg(path) - .args(["rev-parse", "--verify", "HEAD^{commit}"]) - .output()?; - if !head.status.success() { - return Ok(()); - } - let output = Command::new("git") - .arg("-C") - .arg(path) - .args(["switch", "--detach", "--quiet", "HEAD"]) - .output()?; - if output.status.success() { - return Ok(()); - } - Err(Error::UnsafeGit( - String::from_utf8_lossy(&output.stderr).trim().to_owned(), - )) -} diff --git a/packages/worktree/crates/core/src/lib.rs b/packages/worktree/crates/core/src/lib.rs deleted file mode 100644 index 9914a14bd..000000000 --- a/packages/worktree/crates/core/src/lib.rs +++ /dev/null @@ -1,712 +0,0 @@ -mod copy; -mod git; - -use copy::{CopyStrategy, CowStrategy}; -use rusqlite::{Connection, OptionalExtension, params}; -use std::fs; -use std::path::{Path, PathBuf}; -use thiserror::Error; -use ulid::Ulid; - -pub type Result = std::result::Result; - -#[derive(Debug, Error)] -pub enum Error { - #[error("{0}")] - Io(#[from] std::io::Error), - #[error("{0}")] - Database(#[from] rusqlite::Error), - #[error("{0}")] - Walk(#[from] walkdir::Error), - #[error("invalid path: {0}")] - Path(String), - #[error("copy-on-write cloning unavailable: {0}")] - CowUnavailable(String), - #[error("unsupported filesystem entry: {0}")] - UnsupportedEntry(PathBuf), - #[error("unsafe Git source: {0}")] - UnsafeGit(String), - #[error("worktree is not managed: {0}")] - NotManaged(PathBuf), - #[error("worktree marker does not match the registry at: {0}")] - MarkerMismatch(PathBuf), - #[error("worktree marker belongs to an unknown registry entry at: {0}")] - UnknownMarker(PathBuf), - #[error("worktree already exists: {0}")] - AlreadyExists(PathBuf), - #[error("cannot remove the original registered workspace: {0}")] - CannotRemoveRoot(PathBuf), - #[error("cannot reparent the original registered workspace: {0}")] - CannotLinkRoot(PathBuf), - #[error("cannot remove subtree while a recorded worktree path is missing: {0}")] - MissingWorktree(PathBuf), - #[error("cannot link a worktree to itself or its descendant")] - Cycle, - #[error("cannot copy a workspace into itself: {0}")] - InsideSource(PathBuf), -} - -pub struct Create { - pub from: PathBuf, - pub name: Option, - pub into: Option, -} - -pub struct Link { - pub at: PathBuf, - pub to: Option, -} - -#[derive(Clone)] -struct Record { - id: String, - parent_id: Option, - path: PathBuf, -} - -pub struct Manager { - database: Connection, - copier: Box, -} - -impl Manager { - pub fn open_default() -> Result { - let path = default_database_path()?; - if let Some(parent) = path.parent() { - fs::create_dir_all(parent)?; - } - Self::open(path) - } - - pub fn open(path: impl AsRef) -> Result { - Self::with_copier(path, Box::new(CowStrategy)) - } - - fn with_copier(path: impl AsRef, copier: Box) -> Result { - let database = Connection::open(path)?; - database.execute_batch( - "PRAGMA foreign_keys = ON; - CREATE TABLE IF NOT EXISTS worktree ( - id TEXT PRIMARY KEY, - parent_id TEXT REFERENCES worktree(id) ON DELETE CASCADE, - path TEXT NOT NULL UNIQUE, - created_at INTEGER NOT NULL - ); - CREATE INDEX IF NOT EXISTS worktree_parent_id_idx ON worktree(parent_id);", - )?; - Ok(Self { database, copier }) - } - - pub fn create(&mut self, input: Create) -> Result { - let from = existing_directory(&input.from)?; - let git = git::check_source(&from)?; - let (source, register_source) = self.source(&from)?; - let root = self.root(&source)?; - let id = Ulid::new().to_string(); - let destination_parent = match input.into { - Some(path) => absolute_path(&path)?, - None => default_storage(&root.path)?, - }; - let name = destination_name(input.name, &id)?; - if destination_parent.join(&name).starts_with(&from) { - return Err(Error::InsideSource(destination_parent.join(name))); - } - fs::create_dir_all(&destination_parent)?; - let destination_parent = fs::canonicalize(destination_parent)?; - let destination = destination_parent.join(name); - if destination.starts_with(&from) { - return Err(Error::InsideSource(destination)); - } - if destination.exists() { - return Err(Error::AlreadyExists(destination)); - } - - if let Err(error) = self.copier.copy_directory(&from, &destination) { - let _ = fs::remove_dir_all(&destination); - return Err(error); - } - - let result = (|| { - write_marker(&destination, &id)?; - if git { - git::hide_marker(&destination)?; - git::detach_destination(&destination)?; - } - if register_source { - write_marker(&from, &source.id)?; - self.database.execute( - "INSERT INTO worktree (id, parent_id, path, created_at) VALUES (?1, NULL, ?2, ?3)", - params![source.id, path_text(&from)?, timestamp()], - )?; - } - if git { - git::hide_marker(&from)?; - } - self.database.execute( - "INSERT INTO worktree (id, parent_id, path, created_at) VALUES (?1, ?2, ?3, ?4)", - params![id, source.id, path_text(&destination)?, timestamp()], - )?; - Ok(destination.clone()) - })(); - if result.is_err() { - let _ = fs::remove_dir_all(&destination); - } - result - } - - pub fn remove(&mut self, at: impl AsRef) -> Result<()> { - let at = existing_directory(at.as_ref())?; - let record = self.record_at(&at)?; - if record.parent_id.is_none() { - return Err(Error::CannotRemoveRoot(at)); - } - verify_marker(&record)?; - let mut statement = self.database.prepare( - "WITH RECURSIVE subtree(id, path, depth) AS ( - SELECT id, path, 0 FROM worktree WHERE id = ?1 - UNION ALL - SELECT worktree.id, worktree.path, subtree.depth + 1 - FROM worktree JOIN subtree ON worktree.parent_id = subtree.id - ) SELECT id, path, depth FROM subtree ORDER BY depth DESC", - )?; - let rows = statement - .query_map([&record.id], |row| { - Ok(( - row.get::<_, String>(0)?, - PathBuf::from(row.get::<_, String>(1)?), - row.get::<_, i64>(2)?, - )) - })? - .collect::, _>>()?; - drop(statement); - for (id, path, _) in &rows { - if !path.exists() { - return Err(Error::MissingWorktree(path.clone())); - } - verify_marker(&Record { - id: id.clone(), - parent_id: None, - path: path.clone(), - })?; - } - for (id, path, _) in &rows { - fs::remove_dir_all(path)?; - self.database - .execute("DELETE FROM worktree WHERE id = ?1", [id])?; - } - Ok(()) - } - - pub fn link(&mut self, input: Link) -> Result<()> { - let at = existing_directory(&input.at)?; - let record = match read_marker(&at)? { - Some(id) => { - let record = self - .record_id(&id)? - .ok_or_else(|| Error::UnknownMarker(at.clone()))?; - if record.path != at { - if record.path.exists() { - return Err(Error::MarkerMismatch(at)); - } - self.database.execute( - "UPDATE worktree SET path = ?1 WHERE id = ?2", - params![path_text(&at)?, record.id], - )?; - } - Record { - path: at.clone(), - ..record - } - } - None => { - let record = self.record_at(&at)?; - write_marker(&at, &record.id)?; - record - } - }; - if at.join(".git").is_dir() { - git::hide_marker(&at)?; - } - let Some(to) = input.to else { - return Ok(()); - }; - if record.parent_id.is_none() { - return Err(Error::CannotLinkRoot(at)); - } - let parent = self.record_at(&existing_directory(&to)?)?; - if parent.id == record.id || self.is_descendant(&parent.id, &record.id)? { - return Err(Error::Cycle); - } - self.database.execute( - "UPDATE worktree SET parent_id = ?1 WHERE id = ?2", - params![parent.id, record.id], - )?; - Ok(()) - } - - pub fn children(&self, of: impl AsRef) -> Result> { - let record = self.record_at(&existing_directory(of.as_ref())?)?; - let mut statement = self - .database - .prepare("SELECT path FROM worktree WHERE parent_id = ?1 ORDER BY created_at, id")?; - Ok(statement - .query_map([record.id], |row| { - Ok(PathBuf::from(row.get::<_, String>(0)?)) - })? - .collect::, _>>()?) - } - - pub fn ancestors(&self, of: impl AsRef) -> Result> { - let record = self.record_at(&existing_directory(of.as_ref())?)?; - let mut paths = Vec::new(); - let mut parent_id = record.parent_id; - while let Some(id) = parent_id { - let parent = self - .record_id(&id)? - .ok_or_else(|| Error::NotManaged(record.path.clone()))?; - paths.push(parent.path); - parent_id = parent.parent_id; - } - Ok(paths) - } - - fn source(&self, path: &Path) -> Result<(Record, bool)> { - if let Some(id) = read_marker(path)? { - let record = self - .record_id(&id)? - .ok_or_else(|| Error::UnknownMarker(path.to_path_buf()))?; - if record.path != path { - return Err(Error::MarkerMismatch(path.to_path_buf())); - } - return Ok((record, false)); - } - if self.record_at_optional(path)?.is_some() { - return Err(Error::MarkerMismatch(path.to_path_buf())); - } - let id = Ulid::new().to_string(); - Ok(( - Record { - id, - parent_id: None, - path: path.to_path_buf(), - }, - true, - )) - } - - fn root(&self, record: &Record) -> Result { - let mut current = record.clone(); - while let Some(id) = current.parent_id.clone() { - current = self - .record_id(&id)? - .ok_or_else(|| Error::NotManaged(record.path.clone()))?; - } - Ok(current) - } - - fn record_at(&self, path: &Path) -> Result { - self.record_at_optional(path)? - .ok_or_else(|| Error::NotManaged(path.to_path_buf())) - } - - fn record_at_optional(&self, path: &Path) -> Result> { - self.database - .query_row( - "SELECT id, parent_id, path FROM worktree WHERE path = ?1", - [path_text(path)?], - |row| { - Ok(Record { - id: row.get(0)?, - parent_id: row.get(1)?, - path: PathBuf::from(row.get::<_, String>(2)?), - }) - }, - ) - .optional() - .map_err(Error::from) - } - - fn record_id(&self, id: &str) -> Result> { - self.database - .query_row( - "SELECT id, parent_id, path FROM worktree WHERE id = ?1", - [id], - |row| { - Ok(Record { - id: row.get(0)?, - parent_id: row.get(1)?, - path: PathBuf::from(row.get::<_, String>(2)?), - }) - }, - ) - .optional() - .map_err(Error::from) - } - - fn is_descendant(&self, candidate: &str, of: &str) -> Result { - Ok(self.database.query_row( - "WITH RECURSIVE descendants(id) AS ( - SELECT id FROM worktree WHERE parent_id = ?1 - UNION ALL - SELECT worktree.id FROM worktree JOIN descendants ON worktree.parent_id = descendants.id - ) SELECT EXISTS(SELECT 1 FROM descendants WHERE id = ?2)", - params![of, candidate], - |row| row.get(0), - )?) - } -} - -fn default_database_path() -> Result { - let base = dirs::data_local_dir() - .ok_or_else(|| Error::Path("user data directory is unavailable".into()))?; - Ok(base.join("worktree").join("worktree.sqlite")) -} - -fn existing_directory(path: &Path) -> Result { - let path = fs::canonicalize(path)?; - if !path.is_dir() { - return Err(Error::Path(format!("not a directory: {}", path.display()))); - } - Ok(path) -} - -fn absolute_path(path: &Path) -> Result { - if path.is_absolute() { - return Ok(path.to_path_buf()); - } - Ok(std::env::current_dir()?.join(path)) -} - -fn default_storage(root: &Path) -> Result { - let parent = root - .parent() - .ok_or_else(|| Error::Path(format!("workspace has no parent: {}", root.display())))?; - let name = root - .file_name() - .ok_or_else(|| Error::Path(format!("workspace has no name: {}", root.display())))?; - Ok(parent.join(".worktrees").join(name)) -} - -fn destination_name(name: Option, id: &str) -> Result { - let name = name.unwrap_or_else(|| id.to_owned()); - if name.is_empty() || name == "." || name == ".." || Path::new(&name).components().count() != 1 - { - return Err(Error::Path(format!("invalid worktree name: {name}"))); - } - Ok(name) -} - -fn marker(path: &Path) -> PathBuf { - path.join(".worktree") -} - -fn write_marker(path: &Path, id: &str) -> Result<()> { - fs::write(marker(path), format!("{id}\n"))?; - Ok(()) -} - -fn read_marker(path: &Path) -> Result> { - let marker = marker(path); - if !marker.exists() { - return Ok(None); - } - Ok(Some(fs::read_to_string(marker)?.trim().to_owned())) -} - -fn verify_marker(record: &Record) -> Result<()> { - if read_marker(&record.path)?.as_deref() == Some(&record.id) { - return Ok(()); - } - Err(Error::MarkerMismatch(record.path.clone())) -} - -fn path_text(path: &Path) -> Result { - path.to_str() - .map(ToOwned::to_owned) - .ok_or_else(|| Error::Path(format!("path is not valid UTF-8: {}", path.display()))) -} - -fn timestamp() -> i64 { - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_millis() as i64 -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::copy::{FailureStrategy, TestStrategy}; - use std::process::Command; - use tempfile::TempDir; - - fn manager(temp: &TempDir) -> Manager { - Manager::with_copier(temp.path().join("registry.sqlite"), Box::new(TestStrategy)).unwrap() - } - - fn source(temp: &TempDir) -> PathBuf { - let source = temp.path().join("app"); - fs::create_dir(&source).unwrap(); - fs::write(source.join("file.txt"), "hello").unwrap(); - source - } - - #[test] - fn create_tracks_parentage_and_default_storage() { - let temp = TempDir::new().unwrap(); - let source = source(&temp); - let mut manager = manager(&temp); - let first = manager - .create(Create { - from: source.clone(), - name: Some("first".into()), - into: None, - }) - .unwrap(); - let second = manager - .create(Create { - from: first.clone(), - name: Some("second".into()), - into: None, - }) - .unwrap(); - - assert_eq!(first, temp.path().join(".worktrees/app/first")); - assert_eq!(second, temp.path().join(".worktrees/app/second")); - assert_ne!( - fs::read_to_string(source.join(".worktree")).unwrap(), - fs::read_to_string(first.join(".worktree")).unwrap() - ); - assert_eq!(manager.children(&source).unwrap(), vec![first.clone()]); - assert_eq!(manager.ancestors(&second).unwrap(), vec![first, source]); - } - - #[test] - fn remove_deletes_a_full_subtree() { - let temp = TempDir::new().unwrap(); - let source = source(&temp); - let mut manager = manager(&temp); - let first = manager - .create(Create { - from: source.clone(), - name: Some("first".into()), - into: None, - }) - .unwrap(); - let second = manager - .create(Create { - from: first.clone(), - name: Some("second".into()), - into: None, - }) - .unwrap(); - - manager.remove(&first).unwrap(); - - assert!(!first.exists()); - assert!(!second.exists()); - assert!(manager.children(&source).unwrap().is_empty()); - assert!(matches!( - manager.remove(&source), - Err(Error::CannotRemoveRoot(_)) - )); - } - - #[test] - fn remove_refuses_a_subtree_with_an_unlinked_move() { - let temp = TempDir::new().unwrap(); - let source = source(&temp); - let mut manager = manager(&temp); - let first = manager - .create(Create { - from: source, - name: Some("first".into()), - into: None, - }) - .unwrap(); - let second = manager - .create(Create { - from: first.clone(), - name: Some("second".into()), - into: None, - }) - .unwrap(); - fs::rename(&second, temp.path().join("moved")).unwrap(); - - assert!(matches!( - manager.remove(&first), - Err(Error::MissingWorktree(_)) - )); - assert!(first.exists()); - } - - #[test] - fn link_restores_moves_markers_and_reparents() { - let temp = TempDir::new().unwrap(); - let source = source(&temp); - let mut manager = manager(&temp); - let first = manager - .create(Create { - from: source.clone(), - name: Some("first".into()), - into: None, - }) - .unwrap(); - let second = manager - .create(Create { - from: source.clone(), - name: Some("second".into()), - into: None, - }) - .unwrap(); - let moved = temp.path().join("moved"); - fs::rename(&second, &moved).unwrap(); - - manager - .link(Link { - at: moved.clone(), - to: Some(first.clone()), - }) - .unwrap(); - assert_eq!( - manager.ancestors(&moved).unwrap(), - vec![first, source.clone()] - ); - - fs::remove_file(source.join(".worktree")).unwrap(); - manager - .link(Link { - at: source.clone(), - to: None, - }) - .unwrap(); - assert!(source.join(".worktree").exists()); - } - - #[test] - fn link_does_not_reparent_a_registered_source() { - let temp = TempDir::new().unwrap(); - let source = source(&temp); - let mut manager = manager(&temp); - let child = manager - .create(Create { - from: source.clone(), - name: Some("child".into()), - into: None, - }) - .unwrap(); - - assert!(matches!( - manager.link(Link { - at: source.clone(), - to: Some(child), - }), - Err(Error::CannotLinkRoot(_)) - )); - assert!(matches!( - manager.remove(&source), - Err(Error::CannotRemoveRoot(_)) - )); - } - - #[test] - fn git_copy_detaches_head_and_preserves_dirty_state() { - let temp = TempDir::new().unwrap(); - let source = source(&temp); - run(&source, &["init"]); - run(&source, &["config", "user.email", "test@example.com"]); - run(&source, &["config", "user.name", "Test"]); - run(&source, &["add", "file.txt"]); - run(&source, &["commit", "-m", "initial"]); - fs::write(source.join("file.txt"), "changed").unwrap(); - run(&source, &["add", "file.txt"]); - fs::write(source.join("untracked.txt"), "new").unwrap(); - let mut manager = manager(&temp); - - let destination = manager - .create(Create { - from: source.clone(), - name: Some("git".into()), - into: None, - }) - .unwrap(); - - assert!( - !Command::new("git") - .arg("-C") - .arg(&destination) - .args(["symbolic-ref", "-q", "HEAD"]) - .status() - .unwrap() - .success() - ); - let staged = Command::new("git") - .arg("-C") - .arg(&destination) - .args(["diff", "--cached", "--name-only"]) - .output() - .unwrap(); - assert!(String::from_utf8_lossy(&staged.stdout).contains("file.txt")); - assert!(destination.join("untracked.txt").exists()); - let status = Command::new("git") - .arg("-C") - .arg(&destination) - .args(["status", "--porcelain", "--", ".worktree"]) - .output() - .unwrap(); - assert!(status.stdout.is_empty()); - } - - #[test] - fn unsafe_git_source_is_rejected_without_registering_it() { - let temp = TempDir::new().unwrap(); - let source = source(&temp); - run(&source, &["init"]); - fs::write(source.join(".git/MERGE_HEAD"), "commit").unwrap(); - let mut manager = manager(&temp); - - assert!(matches!( - manager.create(Create { - from: source.clone(), - name: Some("unsafe".into()), - into: None, - }), - Err(Error::UnsafeGit(_)) - )); - assert!(!source.join(".worktree").exists()); - } - - #[test] - fn unavailable_cow_does_not_register_the_source() { - let temp = TempDir::new().unwrap(); - let source = source(&temp); - let mut manager = Manager::with_copier( - temp.path().join("registry.sqlite"), - Box::new(FailureStrategy), - ) - .unwrap(); - - assert!(matches!( - manager.create(Create { - from: source.clone(), - name: Some("failure".into()), - into: None, - }), - Err(Error::CowUnavailable(_)) - )); - assert!(!source.join(".worktree").exists()); - assert!(manager.record_at_optional(&source).unwrap().is_none()); - } - - fn run(path: &Path, args: &[&str]) { - assert!( - Command::new("git") - .arg("-C") - .arg(path) - .args(args) - .status() - .unwrap() - .success() - ); - } -} diff --git a/packages/worktree/specs.md b/packages/worktree/specs.md deleted file mode 100644 index 61c3758b0..000000000 --- a/packages/worktree/specs.md +++ /dev/null @@ -1,182 +0,0 @@ -# Worktree Specs - -## Requirement - -`worktree` must be cross-platform as far as practical. Core semantics should work across macOS, Linux, and Windows. Copy-on-write is a platform/filesystem acceleration and must not define the product model. - -## API - -### `create` - -```ts -create(input: { - from: AbsolutePath - name?: string - into?: AbsolutePath -}): AbsolutePath -``` - -Default behavior: - -- Source is `from`. -- `name` defaults to a generated directory name. -- `into` defaults to the managed worktree directory. -- Copy the whole workspace, including dirty, staged, untracked, and ignored files. -- Detach `HEAD` in the new workspace. -- Return the path of the new workspace. - -If `from` is already a managed worktree, create copies that exact worktree. Do not resolve back to an earlier workspace. Metadata should record the immediate source worktree as its parent. - -Default storage is a hidden sibling directory of the original registered workspace: - -```text -/projects/app/ original workspace -/projects/.worktrees/app/task-a/ created worktree -/projects/.worktrees/app/task-b/ created worktree -``` - -- Created worktrees must not be stored inside the workspace being copied, because an exact copy would recursively contain existing worktrees. -- If `from` is an original unregistered workspace, its sibling `.worktrees//` directory becomes the default destination directory. -- If `from` is already managed, descendants use the default destination directory associated with the original workspace rather than nesting storage beside each descendant. -- If `into` is provided, use it instead of the default destination directory. -- If the original workspace is itself a filesystem mount root, its sibling default destination may not support copy-on-write with it; provide `into` on the same filesystem in that case. - -### `remove` - -```ts -remove(input: { - at: AbsolutePath -}): void -``` - -`remove` deletes a managed worktree and its full descendant subtree. - -- `at` must identify a worktree created by this tool; the registered source root cannot be removed. -- Resolve all descendants through `parent_id` and remove their directories deepest-first. -- Verify each existing directory's `.worktree` marker before deleting it. -- Refuse removal if any descendant path is missing, because it may be a moved workspace that has not been linked yet. -- After successful filesystem removal, delete the subtree records from the database. - -### `link` - -```ts -link(input: { - at: AbsolutePath - to?: AbsolutePath -}): void -``` - -`link` reconnects a moved managed worktree to its registry record and can change its parent. - -- Read the ULID from `.worktree` at `at`. -- Look up the existing worktree record by ULID. -- If its recorded path is `at`, leave its location unchanged. -- If its recorded path is different and missing, update it to `at`. -- If its recorded path is different and still exists, fail because this is a duplicate identity, not a move. -- If the ULID is unknown to the database, fail; `.worktree` alone does not include the ancestry needed to rebuild the record. -- If `.worktree` is missing, look up `at` by its absolute path. If it matches an existing record, recreate the marker with that record's ULID. -- If `.worktree` is missing and `at` does not match an existing record, fail. A moved workspace without its marker cannot be identified safely. -- If `to` is provided, set the worktree's parent to the managed worktree at `to`. -- Refuse `to` for an original registered workspace; only worktrees created by this tool can be reparented. -- Refuse `to` if it is `at` or a descendant of `at`, because reparenting must not create a cycle. - -### `children` - -```ts -children(input: { - of: AbsolutePath -}): AbsolutePath[] -``` - -`children` returns the direct managed children created from `of`. - -### `ancestors` - -```ts -ancestors(input: { - of: AbsolutePath -}): AbsolutePath[] -``` - -`ancestors` returns the managed ancestry of `of`, ordered from its immediate parent to the root workspace. - -## Metadata - -Metadata is stored in a central SQLite database in the platform-appropriate user data directory. - -SQLite is not overkill: multiple processes and agents may create, inspect, or remove worktrees concurrently. It provides cross-platform transactions and locking without building a safe JSON registry protocol. - -Start with one table: - -```sql -CREATE TABLE worktree ( - id TEXT PRIMARY KEY, - parent_id TEXT REFERENCES worktree(id) ON DELETE CASCADE, - path TEXT NOT NULL UNIQUE, - created_at INTEGER NOT NULL -); - -CREATE INDEX worktree_parent_id_idx ON worktree(parent_id); -``` - -- Every managed worktree has a stable generated `id`. -- `id` is a ULID generated when the workspace is first registered or created. -- `id` is stored in the central database and in a `.worktree` marker file at the root of the workspace. -- `.worktree` contains the worktree ULID and allows a moved workspace to be rediscovered and verified against the database. -- When a managed workspace is copied, the copied `.worktree` marker is replaced with the new workspace's ULID. -- The original registered workspace has `parent_id = NULL`. -- A created worktree has `parent_id` set to the source worktree `id`. -- `path` is its current location, not its identity. -- Provenance is a rooted tree. Descendants of any worktree can be listed through recursive queries over `parent_id`. -- `remove` deletes a whole subtree, so no surviving record depends on deleted ancestry. - -### Moved Worktrees - -If a worktree is moved outside the tool, its recorded path becomes missing. The tool cannot discover an arbitrary new location without being given a path or scanning a configured directory. - -When `link` is run against a directory containing `.worktree`, the tool reads its ULID and reconciles the database path if the recorded path no longer exists. - -If both the recorded path and the provided path exist with the same ULID, the tool must refuse automatic reconciliation because the directory was copied without assigning a new identity. - -## Git Integration - -Git support is an integration for directories that contain repositories; it does not define the core worktree model. - -When registering or creating from a Git repository: - -- Add `/.worktree` to `.git/info/exclude` so the identity marker does not appear in local Git status. -- Copy the directory with its staged, unstaged, untracked, ignored, and cached state intact. -- If `HEAD` resolves to a commit, detach `HEAD` in the created destination at that same commit. -- Preserve the copied index and working tree state while detaching. -- If the repository has no commits yet, leave its unborn branch state unchanged because there is no commit to detach to. - -Refuse creation from a Git repository when: - -- It is a linked Git worktree whose `.git` is not an independent directory. -- A merge, rebase, cherry-pick, revert, or bisect is in progress. -- Git lock or inconsistent index state makes an exact safe copy unclear. - -The tool does not create branches, commit changes, or otherwise replace normal Git commands. - -## Copy Strategies - -Copying is implemented behind a strategy boundary so platform-specific copy-on-write backends can be added independently. - -- The production strategy on Linux uses reflink cloning. -- The production strategy on macOS uses APFS `clonefile` directory cloning. -- If no implemented copy-on-write strategy succeeds, `create` fails. -- Full byte copying is not implemented as a fallback. -- Future strategies may add Windows copy-on-write support without changing the API. - -## Packaging - -The project ships four interfaces backed by the same implementation and metadata model: - -1. Native library containing the core API and implementation. -2. CLI package providing the `worktree` executable. -3. Bun FFI package for use from Bun applications. -4. Node FFI package for use from Node.js applications. - -The CLI and language bindings should remain thin and expose the same API semantics as the native library. - -For CLI ergonomics, `worktree create` defaults `from` to the current working directory when no source path is provided.