├── .github └── workflows │ └── rust.yml ├── .gitignore ├── .vscode └── settings.json ├── Cargo.lock ├── Cargo.toml ├── Justfile ├── README.md ├── build.rs ├── scripts ├── demo.tape └── replace.awk └── src ├── lib.rs └── main.rs /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Install latest rust toolchain 18 | uses: actions-rs/toolchain@v1 19 | with: 20 | toolchain: stable 21 | default: true 22 | override: true 23 | 24 | - name: Build 25 | run: cargo build --verbose 26 | 27 | - name: Run tests 28 | run: cargo test --verbose 29 | 30 | - name: Run Multigit help 31 | run: cargo run -- --help 32 | 33 | clippy_check: 34 | runs-on: ubuntu-latest 35 | steps: 36 | - uses: actions/checkout@v2 37 | - name: Run Clippy 38 | run: cargo clippy -- -D warnings 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Config 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.enableFiletypes": [ 3 | "!rust" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.24.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler2" 16 | version = "2.0.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 19 | 20 | [[package]] 21 | name = "aho-corasick" 22 | version = "1.1.3" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 25 | dependencies = [ 26 | "memchr", 27 | ] 28 | 29 | [[package]] 30 | name = "android_system_properties" 31 | version = "0.1.5" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 34 | dependencies = [ 35 | "libc", 36 | ] 37 | 38 | [[package]] 39 | name = "anstream" 40 | version = "0.6.18" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 43 | dependencies = [ 44 | "anstyle", 45 | "anstyle-parse", 46 | "anstyle-query", 47 | "anstyle-wincon", 48 | "colorchoice", 49 | "is_terminal_polyfill", 50 | "utf8parse", 51 | ] 52 | 53 | [[package]] 54 | name = "anstyle" 55 | version = "1.0.10" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 58 | 59 | [[package]] 60 | name = "anstyle-parse" 61 | version = "0.2.6" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 64 | dependencies = [ 65 | "utf8parse", 66 | ] 67 | 68 | [[package]] 69 | name = "anstyle-query" 70 | version = "1.1.2" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 73 | dependencies = [ 74 | "windows-sys 0.59.0", 75 | ] 76 | 77 | [[package]] 78 | name = "anstyle-wincon" 79 | version = "3.0.7" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" 82 | dependencies = [ 83 | "anstyle", 84 | "once_cell", 85 | "windows-sys 0.59.0", 86 | ] 87 | 88 | [[package]] 89 | name = "anyhow" 90 | version = "1.0.97" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" 93 | 94 | [[package]] 95 | name = "assert_cmd" 96 | version = "2.0.16" 97 | source = "registry+https://github.com/rust-lang/crates.io-index" 98 | checksum = "dc1835b7f27878de8525dc71410b5a31cdcc5f230aed5ba5df968e09c201b23d" 99 | dependencies = [ 100 | "anstyle", 101 | "bstr", 102 | "doc-comment", 103 | "libc", 104 | "predicates", 105 | "predicates-core", 106 | "predicates-tree", 107 | "wait-timeout", 108 | ] 109 | 110 | [[package]] 111 | name = "autocfg" 112 | version = "1.4.0" 113 | source = "registry+https://github.com/rust-lang/crates.io-index" 114 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 115 | 116 | [[package]] 117 | name = "backtrace" 118 | version = "0.3.74" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" 121 | dependencies = [ 122 | "addr2line", 123 | "cfg-if", 124 | "libc", 125 | "miniz_oxide", 126 | "object", 127 | "rustc-demangle", 128 | "windows-targets 0.52.6", 129 | ] 130 | 131 | [[package]] 132 | name = "better-panic" 133 | version = "0.3.0" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "6fa9e1d11a268684cbd90ed36370d7577afb6c62d912ddff5c15fc34343e5036" 136 | dependencies = [ 137 | "backtrace", 138 | "console", 139 | ] 140 | 141 | [[package]] 142 | name = "bitflags" 143 | version = "1.3.2" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 146 | 147 | [[package]] 148 | name = "bitflags" 149 | version = "2.9.0" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" 152 | 153 | [[package]] 154 | name = "bstr" 155 | version = "1.11.1" 156 | source = "registry+https://github.com/rust-lang/crates.io-index" 157 | checksum = "786a307d683a5bf92e6fd5fd69a7eb613751668d1d8d67d802846dfe367c62c8" 158 | dependencies = [ 159 | "memchr", 160 | "regex-automata", 161 | "serde", 162 | ] 163 | 164 | [[package]] 165 | name = "bumpalo" 166 | version = "3.17.0" 167 | source = "registry+https://github.com/rust-lang/crates.io-index" 168 | checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" 169 | 170 | [[package]] 171 | name = "bytecount" 172 | version = "0.6.8" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | checksum = "5ce89b21cab1437276d2650d57e971f9d548a2d9037cc231abdc0562b97498ce" 175 | 176 | [[package]] 177 | name = "byteorder" 178 | version = "1.5.0" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 181 | 182 | [[package]] 183 | name = "cc" 184 | version = "1.2.18" 185 | source = "registry+https://github.com/rust-lang/crates.io-index" 186 | checksum = "525046617d8376e3db1deffb079e91cef90a89fc3ca5c185bbf8c9ecdd15cd5c" 187 | dependencies = [ 188 | "jobserver", 189 | "libc", 190 | "shlex", 191 | ] 192 | 193 | [[package]] 194 | name = "cfg-if" 195 | version = "1.0.0" 196 | source = "registry+https://github.com/rust-lang/crates.io-index" 197 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 198 | 199 | [[package]] 200 | name = "clap" 201 | version = "4.5.35" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "d8aa86934b44c19c50f87cc2790e19f54f7a67aedb64101c2e1a2e5ecfb73944" 204 | dependencies = [ 205 | "clap_builder", 206 | "clap_derive", 207 | ] 208 | 209 | [[package]] 210 | name = "clap-verbosity-flag" 211 | version = "2.2.3" 212 | source = "registry+https://github.com/rust-lang/crates.io-index" 213 | checksum = "34c77f67047557f62582784fd7482884697731b2932c7d37ced54bce2312e1e2" 214 | dependencies = [ 215 | "clap", 216 | "log", 217 | ] 218 | 219 | [[package]] 220 | name = "clap_builder" 221 | version = "4.5.35" 222 | source = "registry+https://github.com/rust-lang/crates.io-index" 223 | checksum = "2414dbb2dd0695280da6ea9261e327479e9d37b0630f6b53ba2a11c60c679fd9" 224 | dependencies = [ 225 | "anstream", 226 | "anstyle", 227 | "clap_lex", 228 | "strsim", 229 | ] 230 | 231 | [[package]] 232 | name = "clap_complete" 233 | version = "4.5.47" 234 | source = "registry+https://github.com/rust-lang/crates.io-index" 235 | checksum = "c06f5378ea264ad4f82bbc826628b5aad714a75abf6ece087e923010eb937fb6" 236 | dependencies = [ 237 | "clap", 238 | ] 239 | 240 | [[package]] 241 | name = "clap_derive" 242 | version = "4.5.32" 243 | source = "registry+https://github.com/rust-lang/crates.io-index" 244 | checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" 245 | dependencies = [ 246 | "heck 0.5.0", 247 | "proc-macro2", 248 | "quote", 249 | "syn 2.0.100", 250 | ] 251 | 252 | [[package]] 253 | name = "clap_lex" 254 | version = "0.7.4" 255 | source = "registry+https://github.com/rust-lang/crates.io-index" 256 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 257 | 258 | [[package]] 259 | name = "colorchoice" 260 | version = "1.0.3" 261 | source = "registry+https://github.com/rust-lang/crates.io-index" 262 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 263 | 264 | [[package]] 265 | name = "colored" 266 | version = "1.9.4" 267 | source = "registry+https://github.com/rust-lang/crates.io-index" 268 | checksum = "5a5f741c91823341bebf717d4c71bda820630ce065443b58bd1b7451af008355" 269 | dependencies = [ 270 | "is-terminal", 271 | "lazy_static", 272 | "winapi", 273 | ] 274 | 275 | [[package]] 276 | name = "colored" 277 | version = "2.2.0" 278 | source = "registry+https://github.com/rust-lang/crates.io-index" 279 | checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" 280 | dependencies = [ 281 | "lazy_static", 282 | "windows-sys 0.48.0", 283 | ] 284 | 285 | [[package]] 286 | name = "colored_markup" 287 | version = "0.1.1" 288 | source = "registry+https://github.com/rust-lang/crates.io-index" 289 | checksum = "957f60a0329d7aff2b8557e7f4966c0014a1e19a9a35acb79df279500d4ba257" 290 | dependencies = [ 291 | "anyhow", 292 | "colored 2.2.0", 293 | "lazy_static", 294 | "nom", 295 | "regex", 296 | ] 297 | 298 | [[package]] 299 | name = "console" 300 | version = "0.15.10" 301 | source = "registry+https://github.com/rust-lang/crates.io-index" 302 | checksum = "ea3c6ecd8059b57859df5c69830340ed3c41d30e3da0c1cbed90a96ac853041b" 303 | dependencies = [ 304 | "encode_unicode", 305 | "libc", 306 | "once_cell", 307 | "windows-sys 0.59.0", 308 | ] 309 | 310 | [[package]] 311 | name = "const_fn" 312 | version = "0.4.11" 313 | source = "registry+https://github.com/rust-lang/crates.io-index" 314 | checksum = "2f8a2ca5ac02d09563609681103aada9e1777d54fc57a5acd7a41404f9c93b6e" 315 | 316 | [[package]] 317 | name = "const_format" 318 | version = "0.2.34" 319 | source = "registry+https://github.com/rust-lang/crates.io-index" 320 | checksum = "126f97965c8ad46d6d9163268ff28432e8f6a1196a55578867832e3049df63dd" 321 | dependencies = [ 322 | "const_format_proc_macros", 323 | ] 324 | 325 | [[package]] 326 | name = "const_format_proc_macros" 327 | version = "0.2.34" 328 | source = "registry+https://github.com/rust-lang/crates.io-index" 329 | checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" 330 | dependencies = [ 331 | "proc-macro2", 332 | "quote", 333 | "unicode-xid", 334 | ] 335 | 336 | [[package]] 337 | name = "core-foundation-sys" 338 | version = "0.8.7" 339 | source = "registry+https://github.com/rust-lang/crates.io-index" 340 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 341 | 342 | [[package]] 343 | name = "crossterm" 344 | version = "0.25.0" 345 | source = "registry+https://github.com/rust-lang/crates.io-index" 346 | checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" 347 | dependencies = [ 348 | "bitflags 1.3.2", 349 | "crossterm_winapi", 350 | "libc", 351 | "mio", 352 | "parking_lot", 353 | "signal-hook", 354 | "signal-hook-mio", 355 | "winapi", 356 | ] 357 | 358 | [[package]] 359 | name = "crossterm_winapi" 360 | version = "0.9.1" 361 | source = "registry+https://github.com/rust-lang/crates.io-index" 362 | checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" 363 | dependencies = [ 364 | "winapi", 365 | ] 366 | 367 | [[package]] 368 | name = "csv" 369 | version = "1.3.1" 370 | source = "registry+https://github.com/rust-lang/crates.io-index" 371 | checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" 372 | dependencies = [ 373 | "csv-core", 374 | "itoa", 375 | "ryu", 376 | "serde", 377 | ] 378 | 379 | [[package]] 380 | name = "csv-core" 381 | version = "0.1.11" 382 | source = "registry+https://github.com/rust-lang/crates.io-index" 383 | checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70" 384 | dependencies = [ 385 | "memchr", 386 | ] 387 | 388 | [[package]] 389 | name = "deranged" 390 | version = "0.4.0" 391 | source = "registry+https://github.com/rust-lang/crates.io-index" 392 | checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" 393 | dependencies = [ 394 | "powerfmt", 395 | ] 396 | 397 | [[package]] 398 | name = "difflib" 399 | version = "0.4.0" 400 | source = "registry+https://github.com/rust-lang/crates.io-index" 401 | checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" 402 | 403 | [[package]] 404 | name = "dirs" 405 | version = "5.0.1" 406 | source = "registry+https://github.com/rust-lang/crates.io-index" 407 | checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" 408 | dependencies = [ 409 | "dirs-sys", 410 | ] 411 | 412 | [[package]] 413 | name = "dirs-sys" 414 | version = "0.4.1" 415 | source = "registry+https://github.com/rust-lang/crates.io-index" 416 | checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" 417 | dependencies = [ 418 | "libc", 419 | "option-ext", 420 | "redox_users", 421 | "windows-sys 0.48.0", 422 | ] 423 | 424 | [[package]] 425 | name = "displaydoc" 426 | version = "0.2.5" 427 | source = "registry+https://github.com/rust-lang/crates.io-index" 428 | checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 429 | dependencies = [ 430 | "proc-macro2", 431 | "quote", 432 | "syn 2.0.100", 433 | ] 434 | 435 | [[package]] 436 | name = "doc-comment" 437 | version = "0.3.3" 438 | source = "registry+https://github.com/rust-lang/crates.io-index" 439 | checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" 440 | 441 | [[package]] 442 | name = "dyn-clone" 443 | version = "1.0.17" 444 | source = "registry+https://github.com/rust-lang/crates.io-index" 445 | checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" 446 | 447 | [[package]] 448 | name = "either" 449 | version = "1.13.0" 450 | source = "registry+https://github.com/rust-lang/crates.io-index" 451 | checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" 452 | 453 | [[package]] 454 | name = "encode_unicode" 455 | version = "1.0.0" 456 | source = "registry+https://github.com/rust-lang/crates.io-index" 457 | checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" 458 | 459 | [[package]] 460 | name = "equivalent" 461 | version = "1.0.2" 462 | source = "registry+https://github.com/rust-lang/crates.io-index" 463 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 464 | 465 | [[package]] 466 | name = "fern" 467 | version = "0.6.2" 468 | source = "registry+https://github.com/rust-lang/crates.io-index" 469 | checksum = "d9f0c14694cbd524c8720dd69b0e3179344f04ebb5f90f2e4a440c6ea3b2f1ee" 470 | dependencies = [ 471 | "colored 1.9.4", 472 | "log", 473 | ] 474 | 475 | [[package]] 476 | name = "fnv" 477 | version = "1.0.7" 478 | source = "registry+https://github.com/rust-lang/crates.io-index" 479 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 480 | 481 | [[package]] 482 | name = "form_urlencoded" 483 | version = "1.2.1" 484 | source = "registry+https://github.com/rust-lang/crates.io-index" 485 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 486 | dependencies = [ 487 | "percent-encoding", 488 | ] 489 | 490 | [[package]] 491 | name = "fuzzy-matcher" 492 | version = "0.3.7" 493 | source = "registry+https://github.com/rust-lang/crates.io-index" 494 | checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" 495 | dependencies = [ 496 | "thread_local", 497 | ] 498 | 499 | [[package]] 500 | name = "fxhash" 501 | version = "0.2.1" 502 | source = "registry+https://github.com/rust-lang/crates.io-index" 503 | checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" 504 | dependencies = [ 505 | "byteorder", 506 | ] 507 | 508 | [[package]] 509 | name = "getrandom" 510 | version = "0.2.15" 511 | source = "registry+https://github.com/rust-lang/crates.io-index" 512 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 513 | dependencies = [ 514 | "cfg-if", 515 | "libc", 516 | "wasi 0.11.0+wasi-snapshot-preview1", 517 | ] 518 | 519 | [[package]] 520 | name = "getrandom" 521 | version = "0.3.2" 522 | source = "registry+https://github.com/rust-lang/crates.io-index" 523 | checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" 524 | dependencies = [ 525 | "cfg-if", 526 | "libc", 527 | "r-efi", 528 | "wasi 0.14.2+wasi-0.2.4", 529 | ] 530 | 531 | [[package]] 532 | name = "gimli" 533 | version = "0.31.1" 534 | source = "registry+https://github.com/rust-lang/crates.io-index" 535 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 536 | 537 | [[package]] 538 | name = "git2" 539 | version = "0.19.0" 540 | source = "registry+https://github.com/rust-lang/crates.io-index" 541 | checksum = "b903b73e45dc0c6c596f2d37eccece7c1c8bb6e4407b001096387c63d0d93724" 542 | dependencies = [ 543 | "bitflags 2.9.0", 544 | "libc", 545 | "libgit2-sys", 546 | "log", 547 | "openssl-probe", 548 | "openssl-sys", 549 | "url", 550 | ] 551 | 552 | [[package]] 553 | name = "hashbrown" 554 | version = "0.15.2" 555 | source = "registry+https://github.com/rust-lang/crates.io-index" 556 | checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" 557 | 558 | [[package]] 559 | name = "heck" 560 | version = "0.4.1" 561 | source = "registry+https://github.com/rust-lang/crates.io-index" 562 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 563 | 564 | [[package]] 565 | name = "heck" 566 | version = "0.5.0" 567 | source = "registry+https://github.com/rust-lang/crates.io-index" 568 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 569 | 570 | [[package]] 571 | name = "hermit-abi" 572 | version = "0.4.0" 573 | source = "registry+https://github.com/rust-lang/crates.io-index" 574 | checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" 575 | 576 | [[package]] 577 | name = "iana-time-zone" 578 | version = "0.1.63" 579 | source = "registry+https://github.com/rust-lang/crates.io-index" 580 | checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" 581 | dependencies = [ 582 | "android_system_properties", 583 | "core-foundation-sys", 584 | "iana-time-zone-haiku", 585 | "js-sys", 586 | "log", 587 | "wasm-bindgen", 588 | "windows-core", 589 | ] 590 | 591 | [[package]] 592 | name = "iana-time-zone-haiku" 593 | version = "0.1.2" 594 | source = "registry+https://github.com/rust-lang/crates.io-index" 595 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 596 | dependencies = [ 597 | "cc", 598 | ] 599 | 600 | [[package]] 601 | name = "icu_collections" 602 | version = "1.5.0" 603 | source = "registry+https://github.com/rust-lang/crates.io-index" 604 | checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" 605 | dependencies = [ 606 | "displaydoc", 607 | "yoke", 608 | "zerofrom", 609 | "zerovec", 610 | ] 611 | 612 | [[package]] 613 | name = "icu_locid" 614 | version = "1.5.0" 615 | source = "registry+https://github.com/rust-lang/crates.io-index" 616 | checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" 617 | dependencies = [ 618 | "displaydoc", 619 | "litemap", 620 | "tinystr", 621 | "writeable", 622 | "zerovec", 623 | ] 624 | 625 | [[package]] 626 | name = "icu_locid_transform" 627 | version = "1.5.0" 628 | source = "registry+https://github.com/rust-lang/crates.io-index" 629 | checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" 630 | dependencies = [ 631 | "displaydoc", 632 | "icu_locid", 633 | "icu_locid_transform_data", 634 | "icu_provider", 635 | "tinystr", 636 | "zerovec", 637 | ] 638 | 639 | [[package]] 640 | name = "icu_locid_transform_data" 641 | version = "1.5.1" 642 | source = "registry+https://github.com/rust-lang/crates.io-index" 643 | checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d" 644 | 645 | [[package]] 646 | name = "icu_normalizer" 647 | version = "1.5.0" 648 | source = "registry+https://github.com/rust-lang/crates.io-index" 649 | checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" 650 | dependencies = [ 651 | "displaydoc", 652 | "icu_collections", 653 | "icu_normalizer_data", 654 | "icu_properties", 655 | "icu_provider", 656 | "smallvec", 657 | "utf16_iter", 658 | "utf8_iter", 659 | "write16", 660 | "zerovec", 661 | ] 662 | 663 | [[package]] 664 | name = "icu_normalizer_data" 665 | version = "1.5.1" 666 | source = "registry+https://github.com/rust-lang/crates.io-index" 667 | checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7" 668 | 669 | [[package]] 670 | name = "icu_properties" 671 | version = "1.5.1" 672 | source = "registry+https://github.com/rust-lang/crates.io-index" 673 | checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" 674 | dependencies = [ 675 | "displaydoc", 676 | "icu_collections", 677 | "icu_locid_transform", 678 | "icu_properties_data", 679 | "icu_provider", 680 | "tinystr", 681 | "zerovec", 682 | ] 683 | 684 | [[package]] 685 | name = "icu_properties_data" 686 | version = "1.5.1" 687 | source = "registry+https://github.com/rust-lang/crates.io-index" 688 | checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2" 689 | 690 | [[package]] 691 | name = "icu_provider" 692 | version = "1.5.0" 693 | source = "registry+https://github.com/rust-lang/crates.io-index" 694 | checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" 695 | dependencies = [ 696 | "displaydoc", 697 | "icu_locid", 698 | "icu_provider_macros", 699 | "stable_deref_trait", 700 | "tinystr", 701 | "writeable", 702 | "yoke", 703 | "zerofrom", 704 | "zerovec", 705 | ] 706 | 707 | [[package]] 708 | name = "icu_provider_macros" 709 | version = "1.5.0" 710 | source = "registry+https://github.com/rust-lang/crates.io-index" 711 | checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" 712 | dependencies = [ 713 | "proc-macro2", 714 | "quote", 715 | "syn 2.0.100", 716 | ] 717 | 718 | [[package]] 719 | name = "idna" 720 | version = "1.0.3" 721 | source = "registry+https://github.com/rust-lang/crates.io-index" 722 | checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" 723 | dependencies = [ 724 | "idna_adapter", 725 | "smallvec", 726 | "utf8_iter", 727 | ] 728 | 729 | [[package]] 730 | name = "idna_adapter" 731 | version = "1.2.0" 732 | source = "registry+https://github.com/rust-lang/crates.io-index" 733 | checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" 734 | dependencies = [ 735 | "icu_normalizer", 736 | "icu_properties", 737 | ] 738 | 739 | [[package]] 740 | name = "indexmap" 741 | version = "2.9.0" 742 | source = "registry+https://github.com/rust-lang/crates.io-index" 743 | checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" 744 | dependencies = [ 745 | "equivalent", 746 | "hashbrown", 747 | ] 748 | 749 | [[package]] 750 | name = "inquire" 751 | version = "0.7.5" 752 | source = "registry+https://github.com/rust-lang/crates.io-index" 753 | checksum = "0fddf93031af70e75410a2511ec04d49e758ed2f26dad3404a934e0fb45cc12a" 754 | dependencies = [ 755 | "bitflags 2.9.0", 756 | "crossterm", 757 | "dyn-clone", 758 | "fuzzy-matcher", 759 | "fxhash", 760 | "newline-converter", 761 | "once_cell", 762 | "unicode-segmentation", 763 | "unicode-width", 764 | ] 765 | 766 | [[package]] 767 | name = "is-terminal" 768 | version = "0.4.13" 769 | source = "registry+https://github.com/rust-lang/crates.io-index" 770 | checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" 771 | dependencies = [ 772 | "hermit-abi", 773 | "libc", 774 | "windows-sys 0.52.0", 775 | ] 776 | 777 | [[package]] 778 | name = "is_debug" 779 | version = "1.0.2" 780 | source = "registry+https://github.com/rust-lang/crates.io-index" 781 | checksum = "e8ea828c9d6638a5bd3d8b14e37502b4d56cae910ccf8a5b7f51c7a0eb1d0508" 782 | 783 | [[package]] 784 | name = "is_terminal_polyfill" 785 | version = "1.70.1" 786 | source = "registry+https://github.com/rust-lang/crates.io-index" 787 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 788 | 789 | [[package]] 790 | name = "itoa" 791 | version = "1.0.15" 792 | source = "registry+https://github.com/rust-lang/crates.io-index" 793 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 794 | 795 | [[package]] 796 | name = "jobserver" 797 | version = "0.1.33" 798 | source = "registry+https://github.com/rust-lang/crates.io-index" 799 | checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" 800 | dependencies = [ 801 | "getrandom 0.3.2", 802 | "libc", 803 | ] 804 | 805 | [[package]] 806 | name = "js-sys" 807 | version = "0.3.77" 808 | source = "registry+https://github.com/rust-lang/crates.io-index" 809 | checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" 810 | dependencies = [ 811 | "once_cell", 812 | "wasm-bindgen", 813 | ] 814 | 815 | [[package]] 816 | name = "lazy_static" 817 | version = "1.5.0" 818 | source = "registry+https://github.com/rust-lang/crates.io-index" 819 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 820 | 821 | [[package]] 822 | name = "libc" 823 | version = "0.2.171" 824 | source = "registry+https://github.com/rust-lang/crates.io-index" 825 | checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" 826 | 827 | [[package]] 828 | name = "libgit2-sys" 829 | version = "0.17.0+1.8.1" 830 | source = "registry+https://github.com/rust-lang/crates.io-index" 831 | checksum = "10472326a8a6477c3c20a64547b0059e4b0d086869eee31e6d7da728a8eb7224" 832 | dependencies = [ 833 | "cc", 834 | "libc", 835 | "libssh2-sys", 836 | "libz-sys", 837 | "openssl-sys", 838 | "pkg-config", 839 | ] 840 | 841 | [[package]] 842 | name = "libredox" 843 | version = "0.1.3" 844 | source = "registry+https://github.com/rust-lang/crates.io-index" 845 | checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" 846 | dependencies = [ 847 | "bitflags 2.9.0", 848 | "libc", 849 | ] 850 | 851 | [[package]] 852 | name = "libssh2-sys" 853 | version = "0.3.1" 854 | source = "registry+https://github.com/rust-lang/crates.io-index" 855 | checksum = "220e4f05ad4a218192533b300327f5150e809b54c4ec83b5a1d91833601811b9" 856 | dependencies = [ 857 | "cc", 858 | "libc", 859 | "libz-sys", 860 | "openssl-sys", 861 | "pkg-config", 862 | "vcpkg", 863 | ] 864 | 865 | [[package]] 866 | name = "libz-sys" 867 | version = "1.1.22" 868 | source = "registry+https://github.com/rust-lang/crates.io-index" 869 | checksum = "8b70e7a7df205e92a1a4cd9aaae7898dac0aa555503cc0a649494d0d60e7651d" 870 | dependencies = [ 871 | "cc", 872 | "libc", 873 | "pkg-config", 874 | "vcpkg", 875 | ] 876 | 877 | [[package]] 878 | name = "litemap" 879 | version = "0.7.5" 880 | source = "registry+https://github.com/rust-lang/crates.io-index" 881 | checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" 882 | 883 | [[package]] 884 | name = "lock_api" 885 | version = "0.4.12" 886 | source = "registry+https://github.com/rust-lang/crates.io-index" 887 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 888 | dependencies = [ 889 | "autocfg", 890 | "scopeguard", 891 | ] 892 | 893 | [[package]] 894 | name = "log" 895 | version = "0.4.27" 896 | source = "registry+https://github.com/rust-lang/crates.io-index" 897 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 898 | 899 | [[package]] 900 | name = "memchr" 901 | version = "2.7.4" 902 | source = "registry+https://github.com/rust-lang/crates.io-index" 903 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 904 | 905 | [[package]] 906 | name = "minimal-lexical" 907 | version = "0.2.1" 908 | source = "registry+https://github.com/rust-lang/crates.io-index" 909 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 910 | 911 | [[package]] 912 | name = "miniz_oxide" 913 | version = "0.8.2" 914 | source = "registry+https://github.com/rust-lang/crates.io-index" 915 | checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" 916 | dependencies = [ 917 | "adler2", 918 | ] 919 | 920 | [[package]] 921 | name = "mio" 922 | version = "0.8.11" 923 | source = "registry+https://github.com/rust-lang/crates.io-index" 924 | checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" 925 | dependencies = [ 926 | "libc", 927 | "log", 928 | "wasi 0.11.0+wasi-snapshot-preview1", 929 | "windows-sys 0.48.0", 930 | ] 931 | 932 | [[package]] 933 | name = "multigit" 934 | version = "0.1.3" 935 | dependencies = [ 936 | "anyhow", 937 | "assert_cmd", 938 | "better-panic", 939 | "clap", 940 | "clap-verbosity-flag", 941 | "clap_complete", 942 | "colored_markup", 943 | "csv", 944 | "fern", 945 | "git2", 946 | "inquire", 947 | "log", 948 | "path-absolutize", 949 | "patharg", 950 | "serde", 951 | "shadow-rs", 952 | "shell-words", 953 | "shellexpand", 954 | "tabled", 955 | "termsize", 956 | "time", 957 | "toml", 958 | "walkdir", 959 | ] 960 | 961 | [[package]] 962 | name = "newline-converter" 963 | version = "0.3.0" 964 | source = "registry+https://github.com/rust-lang/crates.io-index" 965 | checksum = "47b6b097ecb1cbfed438542d16e84fd7ad9b0c76c8a65b7f9039212a3d14dc7f" 966 | dependencies = [ 967 | "unicode-segmentation", 968 | ] 969 | 970 | [[package]] 971 | name = "nom" 972 | version = "7.1.3" 973 | source = "registry+https://github.com/rust-lang/crates.io-index" 974 | checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" 975 | dependencies = [ 976 | "memchr", 977 | "minimal-lexical", 978 | ] 979 | 980 | [[package]] 981 | name = "num-conv" 982 | version = "0.1.0" 983 | source = "registry+https://github.com/rust-lang/crates.io-index" 984 | checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 985 | 986 | [[package]] 987 | name = "num_threads" 988 | version = "0.1.7" 989 | source = "registry+https://github.com/rust-lang/crates.io-index" 990 | checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" 991 | dependencies = [ 992 | "libc", 993 | ] 994 | 995 | [[package]] 996 | name = "object" 997 | version = "0.36.5" 998 | source = "registry+https://github.com/rust-lang/crates.io-index" 999 | checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" 1000 | dependencies = [ 1001 | "memchr", 1002 | ] 1003 | 1004 | [[package]] 1005 | name = "once_cell" 1006 | version = "1.21.3" 1007 | source = "registry+https://github.com/rust-lang/crates.io-index" 1008 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 1009 | 1010 | [[package]] 1011 | name = "openssl-probe" 1012 | version = "0.1.5" 1013 | source = "registry+https://github.com/rust-lang/crates.io-index" 1014 | checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" 1015 | 1016 | [[package]] 1017 | name = "openssl-sys" 1018 | version = "0.9.107" 1019 | source = "registry+https://github.com/rust-lang/crates.io-index" 1020 | checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07" 1021 | dependencies = [ 1022 | "cc", 1023 | "libc", 1024 | "pkg-config", 1025 | "vcpkg", 1026 | ] 1027 | 1028 | [[package]] 1029 | name = "option-ext" 1030 | version = "0.2.0" 1031 | source = "registry+https://github.com/rust-lang/crates.io-index" 1032 | checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" 1033 | 1034 | [[package]] 1035 | name = "papergrid" 1036 | version = "0.12.0" 1037 | source = "registry+https://github.com/rust-lang/crates.io-index" 1038 | checksum = "c7419ad52a7de9b60d33e11085a0fe3df1fbd5926aa3f93d3dd53afbc9e86725" 1039 | dependencies = [ 1040 | "bytecount", 1041 | "fnv", 1042 | "unicode-width", 1043 | ] 1044 | 1045 | [[package]] 1046 | name = "parking_lot" 1047 | version = "0.12.3" 1048 | source = "registry+https://github.com/rust-lang/crates.io-index" 1049 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 1050 | dependencies = [ 1051 | "lock_api", 1052 | "parking_lot_core", 1053 | ] 1054 | 1055 | [[package]] 1056 | name = "parking_lot_core" 1057 | version = "0.9.10" 1058 | source = "registry+https://github.com/rust-lang/crates.io-index" 1059 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 1060 | dependencies = [ 1061 | "cfg-if", 1062 | "libc", 1063 | "redox_syscall", 1064 | "smallvec", 1065 | "windows-targets 0.52.6", 1066 | ] 1067 | 1068 | [[package]] 1069 | name = "path-absolutize" 1070 | version = "3.1.1" 1071 | source = "registry+https://github.com/rust-lang/crates.io-index" 1072 | checksum = "e4af381fe79fa195b4909485d99f73a80792331df0625188e707854f0b3383f5" 1073 | dependencies = [ 1074 | "path-dedot", 1075 | ] 1076 | 1077 | [[package]] 1078 | name = "path-dedot" 1079 | version = "3.1.1" 1080 | source = "registry+https://github.com/rust-lang/crates.io-index" 1081 | checksum = "07ba0ad7e047712414213ff67533e6dd477af0a4e1d14fb52343e53d30ea9397" 1082 | dependencies = [ 1083 | "once_cell", 1084 | ] 1085 | 1086 | [[package]] 1087 | name = "patharg" 1088 | version = "0.4.0" 1089 | source = "registry+https://github.com/rust-lang/crates.io-index" 1090 | checksum = "1ada2bee7a186eb0d859bb985e47991d4c5b6887c95fc28c44ebc0e7d52b526a" 1091 | dependencies = [ 1092 | "cfg-if", 1093 | "either", 1094 | ] 1095 | 1096 | [[package]] 1097 | name = "percent-encoding" 1098 | version = "2.3.1" 1099 | source = "registry+https://github.com/rust-lang/crates.io-index" 1100 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 1101 | 1102 | [[package]] 1103 | name = "pkg-config" 1104 | version = "0.3.32" 1105 | source = "registry+https://github.com/rust-lang/crates.io-index" 1106 | checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 1107 | 1108 | [[package]] 1109 | name = "powerfmt" 1110 | version = "0.2.0" 1111 | source = "registry+https://github.com/rust-lang/crates.io-index" 1112 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 1113 | 1114 | [[package]] 1115 | name = "predicates" 1116 | version = "3.1.2" 1117 | source = "registry+https://github.com/rust-lang/crates.io-index" 1118 | checksum = "7e9086cc7640c29a356d1a29fd134380bee9d8f79a17410aa76e7ad295f42c97" 1119 | dependencies = [ 1120 | "anstyle", 1121 | "difflib", 1122 | "predicates-core", 1123 | ] 1124 | 1125 | [[package]] 1126 | name = "predicates-core" 1127 | version = "1.0.8" 1128 | source = "registry+https://github.com/rust-lang/crates.io-index" 1129 | checksum = "ae8177bee8e75d6846599c6b9ff679ed51e882816914eec639944d7c9aa11931" 1130 | 1131 | [[package]] 1132 | name = "predicates-tree" 1133 | version = "1.0.11" 1134 | source = "registry+https://github.com/rust-lang/crates.io-index" 1135 | checksum = "41b740d195ed3166cd147c8047ec98db0e22ec019eb8eeb76d343b795304fb13" 1136 | dependencies = [ 1137 | "predicates-core", 1138 | "termtree", 1139 | ] 1140 | 1141 | [[package]] 1142 | name = "proc-macro-error" 1143 | version = "1.0.4" 1144 | source = "registry+https://github.com/rust-lang/crates.io-index" 1145 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 1146 | dependencies = [ 1147 | "proc-macro-error-attr", 1148 | "proc-macro2", 1149 | "quote", 1150 | "syn 1.0.109", 1151 | "version_check", 1152 | ] 1153 | 1154 | [[package]] 1155 | name = "proc-macro-error-attr" 1156 | version = "1.0.4" 1157 | source = "registry+https://github.com/rust-lang/crates.io-index" 1158 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 1159 | dependencies = [ 1160 | "proc-macro2", 1161 | "quote", 1162 | "version_check", 1163 | ] 1164 | 1165 | [[package]] 1166 | name = "proc-macro2" 1167 | version = "1.0.94" 1168 | source = "registry+https://github.com/rust-lang/crates.io-index" 1169 | checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" 1170 | dependencies = [ 1171 | "unicode-ident", 1172 | ] 1173 | 1174 | [[package]] 1175 | name = "quote" 1176 | version = "1.0.40" 1177 | source = "registry+https://github.com/rust-lang/crates.io-index" 1178 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 1179 | dependencies = [ 1180 | "proc-macro2", 1181 | ] 1182 | 1183 | [[package]] 1184 | name = "r-efi" 1185 | version = "5.2.0" 1186 | source = "registry+https://github.com/rust-lang/crates.io-index" 1187 | checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" 1188 | 1189 | [[package]] 1190 | name = "redox_syscall" 1191 | version = "0.5.8" 1192 | source = "registry+https://github.com/rust-lang/crates.io-index" 1193 | checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" 1194 | dependencies = [ 1195 | "bitflags 2.9.0", 1196 | ] 1197 | 1198 | [[package]] 1199 | name = "redox_users" 1200 | version = "0.4.6" 1201 | source = "registry+https://github.com/rust-lang/crates.io-index" 1202 | checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" 1203 | dependencies = [ 1204 | "getrandom 0.2.15", 1205 | "libredox", 1206 | "thiserror", 1207 | ] 1208 | 1209 | [[package]] 1210 | name = "regex" 1211 | version = "1.11.1" 1212 | source = "registry+https://github.com/rust-lang/crates.io-index" 1213 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 1214 | dependencies = [ 1215 | "aho-corasick", 1216 | "memchr", 1217 | "regex-automata", 1218 | "regex-syntax", 1219 | ] 1220 | 1221 | [[package]] 1222 | name = "regex-automata" 1223 | version = "0.4.9" 1224 | source = "registry+https://github.com/rust-lang/crates.io-index" 1225 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 1226 | dependencies = [ 1227 | "aho-corasick", 1228 | "memchr", 1229 | "regex-syntax", 1230 | ] 1231 | 1232 | [[package]] 1233 | name = "regex-syntax" 1234 | version = "0.8.5" 1235 | source = "registry+https://github.com/rust-lang/crates.io-index" 1236 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 1237 | 1238 | [[package]] 1239 | name = "rustc-demangle" 1240 | version = "0.1.24" 1241 | source = "registry+https://github.com/rust-lang/crates.io-index" 1242 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 1243 | 1244 | [[package]] 1245 | name = "rustversion" 1246 | version = "1.0.18" 1247 | source = "registry+https://github.com/rust-lang/crates.io-index" 1248 | checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" 1249 | 1250 | [[package]] 1251 | name = "ryu" 1252 | version = "1.0.20" 1253 | source = "registry+https://github.com/rust-lang/crates.io-index" 1254 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 1255 | 1256 | [[package]] 1257 | name = "same-file" 1258 | version = "1.0.6" 1259 | source = "registry+https://github.com/rust-lang/crates.io-index" 1260 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 1261 | dependencies = [ 1262 | "winapi-util", 1263 | ] 1264 | 1265 | [[package]] 1266 | name = "scopeguard" 1267 | version = "1.2.0" 1268 | source = "registry+https://github.com/rust-lang/crates.io-index" 1269 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 1270 | 1271 | [[package]] 1272 | name = "serde" 1273 | version = "1.0.219" 1274 | source = "registry+https://github.com/rust-lang/crates.io-index" 1275 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 1276 | dependencies = [ 1277 | "serde_derive", 1278 | ] 1279 | 1280 | [[package]] 1281 | name = "serde_derive" 1282 | version = "1.0.219" 1283 | source = "registry+https://github.com/rust-lang/crates.io-index" 1284 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 1285 | dependencies = [ 1286 | "proc-macro2", 1287 | "quote", 1288 | "syn 2.0.100", 1289 | ] 1290 | 1291 | [[package]] 1292 | name = "serde_spanned" 1293 | version = "0.6.8" 1294 | source = "registry+https://github.com/rust-lang/crates.io-index" 1295 | checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" 1296 | dependencies = [ 1297 | "serde", 1298 | ] 1299 | 1300 | [[package]] 1301 | name = "shadow-rs" 1302 | version = "0.35.2" 1303 | source = "registry+https://github.com/rust-lang/crates.io-index" 1304 | checksum = "f1b2328fb3ec0d5302f95915e7e77cfc2ff943714d9970bc4b66e9eacf318687" 1305 | dependencies = [ 1306 | "const_format", 1307 | "git2", 1308 | "is_debug", 1309 | "time", 1310 | "tzdb", 1311 | ] 1312 | 1313 | [[package]] 1314 | name = "shell-words" 1315 | version = "1.1.0" 1316 | source = "registry+https://github.com/rust-lang/crates.io-index" 1317 | checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" 1318 | 1319 | [[package]] 1320 | name = "shellexpand" 1321 | version = "3.1.0" 1322 | source = "registry+https://github.com/rust-lang/crates.io-index" 1323 | checksum = "da03fa3b94cc19e3ebfc88c4229c49d8f08cdbd1228870a45f0ffdf84988e14b" 1324 | dependencies = [ 1325 | "dirs", 1326 | ] 1327 | 1328 | [[package]] 1329 | name = "shlex" 1330 | version = "1.3.0" 1331 | source = "registry+https://github.com/rust-lang/crates.io-index" 1332 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 1333 | 1334 | [[package]] 1335 | name = "signal-hook" 1336 | version = "0.3.17" 1337 | source = "registry+https://github.com/rust-lang/crates.io-index" 1338 | checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" 1339 | dependencies = [ 1340 | "libc", 1341 | "signal-hook-registry", 1342 | ] 1343 | 1344 | [[package]] 1345 | name = "signal-hook-mio" 1346 | version = "0.2.4" 1347 | source = "registry+https://github.com/rust-lang/crates.io-index" 1348 | checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" 1349 | dependencies = [ 1350 | "libc", 1351 | "mio", 1352 | "signal-hook", 1353 | ] 1354 | 1355 | [[package]] 1356 | name = "signal-hook-registry" 1357 | version = "1.4.2" 1358 | source = "registry+https://github.com/rust-lang/crates.io-index" 1359 | checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" 1360 | dependencies = [ 1361 | "libc", 1362 | ] 1363 | 1364 | [[package]] 1365 | name = "smallvec" 1366 | version = "1.15.0" 1367 | source = "registry+https://github.com/rust-lang/crates.io-index" 1368 | checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" 1369 | 1370 | [[package]] 1371 | name = "stable_deref_trait" 1372 | version = "1.2.0" 1373 | source = "registry+https://github.com/rust-lang/crates.io-index" 1374 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 1375 | 1376 | [[package]] 1377 | name = "strsim" 1378 | version = "0.11.1" 1379 | source = "registry+https://github.com/rust-lang/crates.io-index" 1380 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 1381 | 1382 | [[package]] 1383 | name = "syn" 1384 | version = "1.0.109" 1385 | source = "registry+https://github.com/rust-lang/crates.io-index" 1386 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 1387 | dependencies = [ 1388 | "proc-macro2", 1389 | "quote", 1390 | "unicode-ident", 1391 | ] 1392 | 1393 | [[package]] 1394 | name = "syn" 1395 | version = "2.0.100" 1396 | source = "registry+https://github.com/rust-lang/crates.io-index" 1397 | checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" 1398 | dependencies = [ 1399 | "proc-macro2", 1400 | "quote", 1401 | "unicode-ident", 1402 | ] 1403 | 1404 | [[package]] 1405 | name = "synstructure" 1406 | version = "0.13.1" 1407 | source = "registry+https://github.com/rust-lang/crates.io-index" 1408 | checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" 1409 | dependencies = [ 1410 | "proc-macro2", 1411 | "quote", 1412 | "syn 2.0.100", 1413 | ] 1414 | 1415 | [[package]] 1416 | name = "tabled" 1417 | version = "0.16.0" 1418 | source = "registry+https://github.com/rust-lang/crates.io-index" 1419 | checksum = "77c9303ee60b9bedf722012ea29ae3711ba13a67c9b9ae28993838b63057cb1b" 1420 | dependencies = [ 1421 | "papergrid", 1422 | "tabled_derive", 1423 | ] 1424 | 1425 | [[package]] 1426 | name = "tabled_derive" 1427 | version = "0.8.0" 1428 | source = "registry+https://github.com/rust-lang/crates.io-index" 1429 | checksum = "bf0fb8bfdc709786c154e24a66777493fb63ae97e3036d914c8666774c477069" 1430 | dependencies = [ 1431 | "heck 0.4.1", 1432 | "proc-macro-error", 1433 | "proc-macro2", 1434 | "quote", 1435 | "syn 1.0.109", 1436 | ] 1437 | 1438 | [[package]] 1439 | name = "termsize" 1440 | version = "0.1.9" 1441 | source = "registry+https://github.com/rust-lang/crates.io-index" 1442 | checksum = "6f11ff5c25c172608d5b85e2fb43ee9a6d683a7f4ab7f96ae07b3d8b590368fd" 1443 | dependencies = [ 1444 | "libc", 1445 | "winapi", 1446 | ] 1447 | 1448 | [[package]] 1449 | name = "termtree" 1450 | version = "0.4.1" 1451 | source = "registry+https://github.com/rust-lang/crates.io-index" 1452 | checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" 1453 | 1454 | [[package]] 1455 | name = "thiserror" 1456 | version = "1.0.69" 1457 | source = "registry+https://github.com/rust-lang/crates.io-index" 1458 | checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 1459 | dependencies = [ 1460 | "thiserror-impl", 1461 | ] 1462 | 1463 | [[package]] 1464 | name = "thiserror-impl" 1465 | version = "1.0.69" 1466 | source = "registry+https://github.com/rust-lang/crates.io-index" 1467 | checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 1468 | dependencies = [ 1469 | "proc-macro2", 1470 | "quote", 1471 | "syn 2.0.100", 1472 | ] 1473 | 1474 | [[package]] 1475 | name = "thread_local" 1476 | version = "1.1.8" 1477 | source = "registry+https://github.com/rust-lang/crates.io-index" 1478 | checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" 1479 | dependencies = [ 1480 | "cfg-if", 1481 | "once_cell", 1482 | ] 1483 | 1484 | [[package]] 1485 | name = "time" 1486 | version = "0.3.41" 1487 | source = "registry+https://github.com/rust-lang/crates.io-index" 1488 | checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" 1489 | dependencies = [ 1490 | "deranged", 1491 | "itoa", 1492 | "libc", 1493 | "num-conv", 1494 | "num_threads", 1495 | "powerfmt", 1496 | "serde", 1497 | "time-core", 1498 | "time-macros", 1499 | ] 1500 | 1501 | [[package]] 1502 | name = "time-core" 1503 | version = "0.1.4" 1504 | source = "registry+https://github.com/rust-lang/crates.io-index" 1505 | checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" 1506 | 1507 | [[package]] 1508 | name = "time-macros" 1509 | version = "0.2.22" 1510 | source = "registry+https://github.com/rust-lang/crates.io-index" 1511 | checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" 1512 | dependencies = [ 1513 | "num-conv", 1514 | "time-core", 1515 | ] 1516 | 1517 | [[package]] 1518 | name = "tinystr" 1519 | version = "0.7.6" 1520 | source = "registry+https://github.com/rust-lang/crates.io-index" 1521 | checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" 1522 | dependencies = [ 1523 | "displaydoc", 1524 | "zerovec", 1525 | ] 1526 | 1527 | [[package]] 1528 | name = "toml" 1529 | version = "0.8.20" 1530 | source = "registry+https://github.com/rust-lang/crates.io-index" 1531 | checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" 1532 | dependencies = [ 1533 | "serde", 1534 | "serde_spanned", 1535 | "toml_datetime", 1536 | "toml_edit", 1537 | ] 1538 | 1539 | [[package]] 1540 | name = "toml_datetime" 1541 | version = "0.6.8" 1542 | source = "registry+https://github.com/rust-lang/crates.io-index" 1543 | checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" 1544 | dependencies = [ 1545 | "serde", 1546 | ] 1547 | 1548 | [[package]] 1549 | name = "toml_edit" 1550 | version = "0.22.24" 1551 | source = "registry+https://github.com/rust-lang/crates.io-index" 1552 | checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" 1553 | dependencies = [ 1554 | "indexmap", 1555 | "serde", 1556 | "serde_spanned", 1557 | "toml_datetime", 1558 | "winnow", 1559 | ] 1560 | 1561 | [[package]] 1562 | name = "tz-rs" 1563 | version = "0.6.14" 1564 | source = "registry+https://github.com/rust-lang/crates.io-index" 1565 | checksum = "33851b15c848fad2cf4b105c6bb66eb9512b6f6c44a4b13f57c53c73c707e2b4" 1566 | dependencies = [ 1567 | "const_fn", 1568 | ] 1569 | 1570 | [[package]] 1571 | name = "tzdb" 1572 | version = "0.6.1" 1573 | source = "registry+https://github.com/rust-lang/crates.io-index" 1574 | checksum = "1b580f6b365fa89f5767cdb619a55d534d04a4e14c2d7e5b9a31e94598687fb1" 1575 | dependencies = [ 1576 | "iana-time-zone", 1577 | "tz-rs", 1578 | "tzdb_data", 1579 | ] 1580 | 1581 | [[package]] 1582 | name = "tzdb_data" 1583 | version = "0.1.3" 1584 | source = "registry+https://github.com/rust-lang/crates.io-index" 1585 | checksum = "654c1ec546942ce0594e8d220e6b8e3899e0a0a8fe70ddd54d32a376dfefe3f8" 1586 | dependencies = [ 1587 | "tz-rs", 1588 | ] 1589 | 1590 | [[package]] 1591 | name = "unicode-ident" 1592 | version = "1.0.18" 1593 | source = "registry+https://github.com/rust-lang/crates.io-index" 1594 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 1595 | 1596 | [[package]] 1597 | name = "unicode-segmentation" 1598 | version = "1.12.0" 1599 | source = "registry+https://github.com/rust-lang/crates.io-index" 1600 | checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" 1601 | 1602 | [[package]] 1603 | name = "unicode-width" 1604 | version = "0.1.11" 1605 | source = "registry+https://github.com/rust-lang/crates.io-index" 1606 | checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" 1607 | 1608 | [[package]] 1609 | name = "unicode-xid" 1610 | version = "0.2.6" 1611 | source = "registry+https://github.com/rust-lang/crates.io-index" 1612 | checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" 1613 | 1614 | [[package]] 1615 | name = "url" 1616 | version = "2.5.4" 1617 | source = "registry+https://github.com/rust-lang/crates.io-index" 1618 | checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" 1619 | dependencies = [ 1620 | "form_urlencoded", 1621 | "idna", 1622 | "percent-encoding", 1623 | ] 1624 | 1625 | [[package]] 1626 | name = "utf16_iter" 1627 | version = "1.0.5" 1628 | source = "registry+https://github.com/rust-lang/crates.io-index" 1629 | checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" 1630 | 1631 | [[package]] 1632 | name = "utf8_iter" 1633 | version = "1.0.4" 1634 | source = "registry+https://github.com/rust-lang/crates.io-index" 1635 | checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 1636 | 1637 | [[package]] 1638 | name = "utf8parse" 1639 | version = "0.2.2" 1640 | source = "registry+https://github.com/rust-lang/crates.io-index" 1641 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 1642 | 1643 | [[package]] 1644 | name = "vcpkg" 1645 | version = "0.2.15" 1646 | source = "registry+https://github.com/rust-lang/crates.io-index" 1647 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 1648 | 1649 | [[package]] 1650 | name = "version_check" 1651 | version = "0.9.5" 1652 | source = "registry+https://github.com/rust-lang/crates.io-index" 1653 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 1654 | 1655 | [[package]] 1656 | name = "wait-timeout" 1657 | version = "0.2.0" 1658 | source = "registry+https://github.com/rust-lang/crates.io-index" 1659 | checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" 1660 | dependencies = [ 1661 | "libc", 1662 | ] 1663 | 1664 | [[package]] 1665 | name = "walkdir" 1666 | version = "2.5.0" 1667 | source = "registry+https://github.com/rust-lang/crates.io-index" 1668 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 1669 | dependencies = [ 1670 | "same-file", 1671 | "winapi-util", 1672 | ] 1673 | 1674 | [[package]] 1675 | name = "wasi" 1676 | version = "0.11.0+wasi-snapshot-preview1" 1677 | source = "registry+https://github.com/rust-lang/crates.io-index" 1678 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1679 | 1680 | [[package]] 1681 | name = "wasi" 1682 | version = "0.14.2+wasi-0.2.4" 1683 | source = "registry+https://github.com/rust-lang/crates.io-index" 1684 | checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" 1685 | dependencies = [ 1686 | "wit-bindgen-rt", 1687 | ] 1688 | 1689 | [[package]] 1690 | name = "wasm-bindgen" 1691 | version = "0.2.100" 1692 | source = "registry+https://github.com/rust-lang/crates.io-index" 1693 | checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" 1694 | dependencies = [ 1695 | "cfg-if", 1696 | "once_cell", 1697 | "rustversion", 1698 | "wasm-bindgen-macro", 1699 | ] 1700 | 1701 | [[package]] 1702 | name = "wasm-bindgen-backend" 1703 | version = "0.2.100" 1704 | source = "registry+https://github.com/rust-lang/crates.io-index" 1705 | checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" 1706 | dependencies = [ 1707 | "bumpalo", 1708 | "log", 1709 | "proc-macro2", 1710 | "quote", 1711 | "syn 2.0.100", 1712 | "wasm-bindgen-shared", 1713 | ] 1714 | 1715 | [[package]] 1716 | name = "wasm-bindgen-macro" 1717 | version = "0.2.100" 1718 | source = "registry+https://github.com/rust-lang/crates.io-index" 1719 | checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" 1720 | dependencies = [ 1721 | "quote", 1722 | "wasm-bindgen-macro-support", 1723 | ] 1724 | 1725 | [[package]] 1726 | name = "wasm-bindgen-macro-support" 1727 | version = "0.2.100" 1728 | source = "registry+https://github.com/rust-lang/crates.io-index" 1729 | checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" 1730 | dependencies = [ 1731 | "proc-macro2", 1732 | "quote", 1733 | "syn 2.0.100", 1734 | "wasm-bindgen-backend", 1735 | "wasm-bindgen-shared", 1736 | ] 1737 | 1738 | [[package]] 1739 | name = "wasm-bindgen-shared" 1740 | version = "0.2.100" 1741 | source = "registry+https://github.com/rust-lang/crates.io-index" 1742 | checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" 1743 | dependencies = [ 1744 | "unicode-ident", 1745 | ] 1746 | 1747 | [[package]] 1748 | name = "winapi" 1749 | version = "0.3.9" 1750 | source = "registry+https://github.com/rust-lang/crates.io-index" 1751 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1752 | dependencies = [ 1753 | "winapi-i686-pc-windows-gnu", 1754 | "winapi-x86_64-pc-windows-gnu", 1755 | ] 1756 | 1757 | [[package]] 1758 | name = "winapi-i686-pc-windows-gnu" 1759 | version = "0.4.0" 1760 | source = "registry+https://github.com/rust-lang/crates.io-index" 1761 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1762 | 1763 | [[package]] 1764 | name = "winapi-util" 1765 | version = "0.1.9" 1766 | source = "registry+https://github.com/rust-lang/crates.io-index" 1767 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 1768 | dependencies = [ 1769 | "windows-sys 0.48.0", 1770 | ] 1771 | 1772 | [[package]] 1773 | name = "winapi-x86_64-pc-windows-gnu" 1774 | version = "0.4.0" 1775 | source = "registry+https://github.com/rust-lang/crates.io-index" 1776 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1777 | 1778 | [[package]] 1779 | name = "windows-core" 1780 | version = "0.58.0" 1781 | source = "registry+https://github.com/rust-lang/crates.io-index" 1782 | checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" 1783 | dependencies = [ 1784 | "windows-implement", 1785 | "windows-interface", 1786 | "windows-result", 1787 | "windows-strings", 1788 | "windows-targets 0.52.6", 1789 | ] 1790 | 1791 | [[package]] 1792 | name = "windows-implement" 1793 | version = "0.58.0" 1794 | source = "registry+https://github.com/rust-lang/crates.io-index" 1795 | checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" 1796 | dependencies = [ 1797 | "proc-macro2", 1798 | "quote", 1799 | "syn 2.0.100", 1800 | ] 1801 | 1802 | [[package]] 1803 | name = "windows-interface" 1804 | version = "0.58.0" 1805 | source = "registry+https://github.com/rust-lang/crates.io-index" 1806 | checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" 1807 | dependencies = [ 1808 | "proc-macro2", 1809 | "quote", 1810 | "syn 2.0.100", 1811 | ] 1812 | 1813 | [[package]] 1814 | name = "windows-result" 1815 | version = "0.2.0" 1816 | source = "registry+https://github.com/rust-lang/crates.io-index" 1817 | checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" 1818 | dependencies = [ 1819 | "windows-targets 0.52.6", 1820 | ] 1821 | 1822 | [[package]] 1823 | name = "windows-strings" 1824 | version = "0.1.0" 1825 | source = "registry+https://github.com/rust-lang/crates.io-index" 1826 | checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" 1827 | dependencies = [ 1828 | "windows-result", 1829 | "windows-targets 0.52.6", 1830 | ] 1831 | 1832 | [[package]] 1833 | name = "windows-sys" 1834 | version = "0.48.0" 1835 | source = "registry+https://github.com/rust-lang/crates.io-index" 1836 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 1837 | dependencies = [ 1838 | "windows-targets 0.48.5", 1839 | ] 1840 | 1841 | [[package]] 1842 | name = "windows-sys" 1843 | version = "0.52.0" 1844 | source = "registry+https://github.com/rust-lang/crates.io-index" 1845 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1846 | dependencies = [ 1847 | "windows-targets 0.52.6", 1848 | ] 1849 | 1850 | [[package]] 1851 | name = "windows-sys" 1852 | version = "0.59.0" 1853 | source = "registry+https://github.com/rust-lang/crates.io-index" 1854 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1855 | dependencies = [ 1856 | "windows-targets 0.52.6", 1857 | ] 1858 | 1859 | [[package]] 1860 | name = "windows-targets" 1861 | version = "0.48.5" 1862 | source = "registry+https://github.com/rust-lang/crates.io-index" 1863 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 1864 | dependencies = [ 1865 | "windows_aarch64_gnullvm 0.48.5", 1866 | "windows_aarch64_msvc 0.48.5", 1867 | "windows_i686_gnu 0.48.5", 1868 | "windows_i686_msvc 0.48.5", 1869 | "windows_x86_64_gnu 0.48.5", 1870 | "windows_x86_64_gnullvm 0.48.5", 1871 | "windows_x86_64_msvc 0.48.5", 1872 | ] 1873 | 1874 | [[package]] 1875 | name = "windows-targets" 1876 | version = "0.52.6" 1877 | source = "registry+https://github.com/rust-lang/crates.io-index" 1878 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1879 | dependencies = [ 1880 | "windows_aarch64_gnullvm 0.52.6", 1881 | "windows_aarch64_msvc 0.52.6", 1882 | "windows_i686_gnu 0.52.6", 1883 | "windows_i686_gnullvm", 1884 | "windows_i686_msvc 0.52.6", 1885 | "windows_x86_64_gnu 0.52.6", 1886 | "windows_x86_64_gnullvm 0.52.6", 1887 | "windows_x86_64_msvc 0.52.6", 1888 | ] 1889 | 1890 | [[package]] 1891 | name = "windows_aarch64_gnullvm" 1892 | version = "0.48.5" 1893 | source = "registry+https://github.com/rust-lang/crates.io-index" 1894 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 1895 | 1896 | [[package]] 1897 | name = "windows_aarch64_gnullvm" 1898 | version = "0.52.6" 1899 | source = "registry+https://github.com/rust-lang/crates.io-index" 1900 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1901 | 1902 | [[package]] 1903 | name = "windows_aarch64_msvc" 1904 | version = "0.48.5" 1905 | source = "registry+https://github.com/rust-lang/crates.io-index" 1906 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 1907 | 1908 | [[package]] 1909 | name = "windows_aarch64_msvc" 1910 | version = "0.52.6" 1911 | source = "registry+https://github.com/rust-lang/crates.io-index" 1912 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1913 | 1914 | [[package]] 1915 | name = "windows_i686_gnu" 1916 | version = "0.48.5" 1917 | source = "registry+https://github.com/rust-lang/crates.io-index" 1918 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 1919 | 1920 | [[package]] 1921 | name = "windows_i686_gnu" 1922 | version = "0.52.6" 1923 | source = "registry+https://github.com/rust-lang/crates.io-index" 1924 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1925 | 1926 | [[package]] 1927 | name = "windows_i686_gnullvm" 1928 | version = "0.52.6" 1929 | source = "registry+https://github.com/rust-lang/crates.io-index" 1930 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1931 | 1932 | [[package]] 1933 | name = "windows_i686_msvc" 1934 | version = "0.48.5" 1935 | source = "registry+https://github.com/rust-lang/crates.io-index" 1936 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 1937 | 1938 | [[package]] 1939 | name = "windows_i686_msvc" 1940 | version = "0.52.6" 1941 | source = "registry+https://github.com/rust-lang/crates.io-index" 1942 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1943 | 1944 | [[package]] 1945 | name = "windows_x86_64_gnu" 1946 | version = "0.48.5" 1947 | source = "registry+https://github.com/rust-lang/crates.io-index" 1948 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 1949 | 1950 | [[package]] 1951 | name = "windows_x86_64_gnu" 1952 | version = "0.52.6" 1953 | source = "registry+https://github.com/rust-lang/crates.io-index" 1954 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1955 | 1956 | [[package]] 1957 | name = "windows_x86_64_gnullvm" 1958 | version = "0.48.5" 1959 | source = "registry+https://github.com/rust-lang/crates.io-index" 1960 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 1961 | 1962 | [[package]] 1963 | name = "windows_x86_64_gnullvm" 1964 | version = "0.52.6" 1965 | source = "registry+https://github.com/rust-lang/crates.io-index" 1966 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1967 | 1968 | [[package]] 1969 | name = "windows_x86_64_msvc" 1970 | version = "0.48.5" 1971 | source = "registry+https://github.com/rust-lang/crates.io-index" 1972 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 1973 | 1974 | [[package]] 1975 | name = "windows_x86_64_msvc" 1976 | version = "0.52.6" 1977 | source = "registry+https://github.com/rust-lang/crates.io-index" 1978 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1979 | 1980 | [[package]] 1981 | name = "winnow" 1982 | version = "0.7.6" 1983 | source = "registry+https://github.com/rust-lang/crates.io-index" 1984 | checksum = "63d3fcd9bba44b03821e7d699eeee959f3126dcc4aa8e4ae18ec617c2a5cea10" 1985 | dependencies = [ 1986 | "memchr", 1987 | ] 1988 | 1989 | [[package]] 1990 | name = "wit-bindgen-rt" 1991 | version = "0.39.0" 1992 | source = "registry+https://github.com/rust-lang/crates.io-index" 1993 | checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" 1994 | dependencies = [ 1995 | "bitflags 2.9.0", 1996 | ] 1997 | 1998 | [[package]] 1999 | name = "write16" 2000 | version = "1.0.0" 2001 | source = "registry+https://github.com/rust-lang/crates.io-index" 2002 | checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" 2003 | 2004 | [[package]] 2005 | name = "writeable" 2006 | version = "0.5.5" 2007 | source = "registry+https://github.com/rust-lang/crates.io-index" 2008 | checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" 2009 | 2010 | [[package]] 2011 | name = "yoke" 2012 | version = "0.7.5" 2013 | source = "registry+https://github.com/rust-lang/crates.io-index" 2014 | checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" 2015 | dependencies = [ 2016 | "serde", 2017 | "stable_deref_trait", 2018 | "yoke-derive", 2019 | "zerofrom", 2020 | ] 2021 | 2022 | [[package]] 2023 | name = "yoke-derive" 2024 | version = "0.7.5" 2025 | source = "registry+https://github.com/rust-lang/crates.io-index" 2026 | checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" 2027 | dependencies = [ 2028 | "proc-macro2", 2029 | "quote", 2030 | "syn 2.0.100", 2031 | "synstructure", 2032 | ] 2033 | 2034 | [[package]] 2035 | name = "zerofrom" 2036 | version = "0.1.6" 2037 | source = "registry+https://github.com/rust-lang/crates.io-index" 2038 | checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" 2039 | dependencies = [ 2040 | "zerofrom-derive", 2041 | ] 2042 | 2043 | [[package]] 2044 | name = "zerofrom-derive" 2045 | version = "0.1.6" 2046 | source = "registry+https://github.com/rust-lang/crates.io-index" 2047 | checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" 2048 | dependencies = [ 2049 | "proc-macro2", 2050 | "quote", 2051 | "syn 2.0.100", 2052 | "synstructure", 2053 | ] 2054 | 2055 | [[package]] 2056 | name = "zerovec" 2057 | version = "0.10.4" 2058 | source = "registry+https://github.com/rust-lang/crates.io-index" 2059 | checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" 2060 | dependencies = [ 2061 | "yoke", 2062 | "zerofrom", 2063 | "zerovec-derive", 2064 | ] 2065 | 2066 | [[package]] 2067 | name = "zerovec-derive" 2068 | version = "0.10.3" 2069 | source = "registry+https://github.com/rust-lang/crates.io-index" 2070 | checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" 2071 | dependencies = [ 2072 | "proc-macro2", 2073 | "quote", 2074 | "syn 2.0.100", 2075 | ] 2076 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "multigit" 3 | version = "0.1.3" 4 | authors = ["Jonathan Wight "] 5 | edition = "2021" 6 | description = "A CLI tool to manage multiple Git repositories simultaneously" 7 | readme = "README.md" 8 | homepage = "https://github.com/schwa/multigit-rs" 9 | repository = "https://github.com/schwa/multigit-rs" 10 | license = "MIT" 11 | keywords = ["git", "cli", "multiple", "repositories", "management"] 12 | categories = ["command-line-utilities", "development-tools"] 13 | build = "build.rs" 14 | 15 | [dependencies] 16 | anyhow = "1.0.97" 17 | better-panic = "0.3.0" 18 | clap = { version = "4.5.35", features = ["derive"] } 19 | clap-verbosity-flag = "2.2.3" 20 | clap_complete = "4.5.47" 21 | colored_markup = "0.1.1" 22 | csv = "1.3.1" 23 | fern = { version = "0.6.2", features = ["colored"] } 24 | git2 = "0.19.0" 25 | inquire = "0.7.5" 26 | log = "0.4.27" 27 | path-absolutize = "3.1.1" 28 | patharg = "0.4.0" 29 | serde = { version = "1.0.219", features = ["derive"] } 30 | shadow-rs = "0.35.2" 31 | shell-words = "1.1.0" 32 | shellexpand = "3.1.0" 33 | tabled = "0.16.0" 34 | termsize = "0.1.9" 35 | time = "0.3.41" 36 | toml = "0.8.20" 37 | walkdir = "2.5.0" 38 | 39 | [build-dependencies] 40 | shadow-rs = "0.35.2" 41 | 42 | [dev-dependencies] 43 | assert_cmd = "2.0.16" 44 | -------------------------------------------------------------------------------- /Justfile: -------------------------------------------------------------------------------- 1 | set shell := ["fish", "-c"] 2 | 3 | PROJECT_NAME := "multigit" 4 | 5 | _default: list 6 | 7 | list: 8 | just --list 9 | 10 | update-gif: 11 | cargo install --path . 12 | vhs scripts/demo.tape -o docs/out.gif 13 | 14 | publish: 15 | #!/usr/bin/env fish 16 | set CURRENT_BRANCH (git symbolic-ref --short HEAD) 17 | if [ $CURRENT_BRANCH != main ] 18 | echo "Not on main branch. Please switch to main before publishing." 19 | exit 1 20 | end 21 | set NEXT_VERSION (just _next-version) 22 | gum confirm "Confirm next version: '$NEXT_VERSION'?"; or exit 1 23 | just _check-repo; or exit 1 24 | cargo clippy --fix 25 | just _check-repo; or exit 1 26 | cargo test; or exit 1 27 | # just update-usage 28 | echo "Updating Cargo.toml to version $NEXT_VERSION" 29 | toml set Cargo.toml package.version $NEXT_VERSION| sponge Cargo.toml 30 | # gum confirm "Update gif?"; and just update-gif; git add docs/out.gif 31 | gum confirm "git commit -a"; and git commit -a 32 | gum confirm "git tag?"; and git tag $NEXT_VERSION 33 | gum confirm "git push?"; and git push --tags origin main 34 | gum confirm "Rust publish"; and cargo publish 35 | 36 | gum confirm "Update homebrew?"; and just homebrew-release $NEXT_VERSION 37 | 38 | _check-repo: 39 | #!/usr/bin/env fish 40 | set is_dirty (git status --porcelain) 41 | if test -n "$is_dirty" 42 | echo "Repo is dirty. Please commit all changes before publishing." 43 | exit 1 44 | end 45 | 46 | update-usage: 47 | #!/usr/bin/env fish 48 | awk -f scripts/replace.awk -v INDEX=3 -v "REPLACEMENT=cargo run -- --help 2> /dev/null" README.md | sponge README.md 49 | 50 | _next-version: 51 | #!/usr/bin/env fish 52 | set LATEST_TAG (git describe --tags --abbrev=0) 53 | set PARTS (string split . $LATEST_TAG) 54 | set MAJOR $PARTS[1] 55 | set MINOR $PARTS[2] 56 | set PATCH $PARTS[3] 57 | set NEXT_PATCH (math $PATCH + 1) 58 | echo "$MAJOR.$MINOR.$NEXT_PATCH" 59 | 60 | bonnieplusplus: 61 | bonnie++ | bon_csv2html > bonnie++.html 62 | 63 | cargo-analytics: 64 | cargo tree > docs/tree.txt 65 | cargo bloat --release --crates -n 10000 > docs/bloat.txt 66 | cargo report future-incompatibilities > docs/future-incompatibilities.txt; or true 67 | unused-features analyze 68 | unused-features build-report --input report.json 69 | mv report.json docs/unused-features.json 70 | mv report.html docs/unused-features.html 71 | unused-features prune --input docs/unused-features.json 72 | 73 | cargo-installs: 74 | brew install cargo-udeps 75 | cargo install cargo-bloat 76 | cargo install cargo-edit 77 | cargo install cargo-machete 78 | cargo install cargo-unused-features 79 | cargo install grcov 80 | cargo install toml-cli 81 | rustup component add llvm-tools-preview 82 | 83 | linux-setup machine_name: 84 | orb create -a arm64 ubuntu {{machine_name}} 85 | orb run --machine {{machine_name}} sudo apt install -y gcc 86 | orb run --machine {{machine_name}} --shell curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh 87 | orb run --machine {{machine_name}} --shell ". $HOME/.cargo/env" 88 | 89 | linux-run_ machine_name: (linux-setup machine_name) 90 | orb run --machine {{machine_name}} cargo clean 91 | orb run --machine {{machine_name}} cargo build 92 | 93 | linux-run: (linux-run_ "ubuntu") 94 | 95 | windows-deps: 96 | rustup target add x86_64-pc-windows-gnu 97 | 98 | windows-build: 99 | # TODO: use cross 100 | cargo build --target x86_64-pc-windows-gnu 101 | 102 | coverage: 103 | set -x RUSTFLAGS "-C instrument-coverage"; cargo build 104 | cargo test 105 | 106 | homebrew-release VERSION: 107 | #!/usr/bin/env fish 108 | # https://federicoterzi.com/blog/how-to-publish-your-rust-project-on-homebrew/ 109 | 110 | # Build release, create tarball and calculate sha256 111 | cargo build --release 112 | pushd target/release/ 113 | tar -czf {{PROJECT_NAME}}.tar.gz {{PROJECT_NAME}} 114 | set SHA (shasum -a 256 {{PROJECT_NAME}}.tar.gz | cut -d " " -f 1) 115 | echo $SHA 116 | popd 117 | 118 | # Create release on GitHub 119 | gh release create {{VERSION}} target/release/{{PROJECT_NAME}}.tar.gz --title "{{PROJECT_NAME}} {{VERSION}}" 120 | 121 | # Update homebrew formula 122 | pushd $HOME/Projects/homebrew-schwa 123 | git pull 124 | sed -i '' -e "s/sha256 \".*\"/sha256 \"$SHA\"/g" Formula/{{PROJECT_NAME}}.rb 125 | sed -i '' -e "s/version \".*\"/version \"{{VERSION}}\"/g" Formula/{{PROJECT_NAME}}.rb 126 | git commit --all --message "{{PROJECT_NAME}} {{VERSION}}" 127 | git push 128 | popd 129 | 130 | test-homebrew: 131 | brew tap schwa/schwa 132 | brew update 133 | brew install {{PROJECT_NAME}} 134 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Multigit 2 | 3 | Multigit is a powerful command-line interface (CLI) tool designed to manage multiple Git repositories simultaneously. It streamlines common Git operations across multiple projects, saving time and effort for developers working with multiple repositories. 4 | 5 | **Note: This project is currently in progress and may not work properly. Use at your own risk.** 6 | 7 | ## Features 8 | 9 | - Perform Git operations (status, add, commit, push, pull) on multiple repositories at once 10 | - Execute custom commands across selected repositories 11 | - List and manage registered repositories 12 | - Filter repositories for specific operations 13 | - Open the configured Git UI program for selected repositories 14 | - Easy registration and unregistration of repositories 15 | 16 | ## Installation 17 | 18 | Multigit is a Rust project that can be installed using Cargo, the Rust package manager. Follow these steps to install Multigit: 19 | 20 | 1. If you don't have Rust and Cargo installed, first install them by following the instructions at [https://www.rust-lang.org/tools/install](https://www.rust-lang.org/tools/install). 21 | 22 | 2. Once Rust and Cargo are installed, you can install Multigit directly from the GitHub repository: 23 | 24 | ```sh 25 | cargo install --git https://github.com/schwa/multigit-rs 26 | ``` 27 | 28 | This command will download the source code, compile it, and install the `multigit` binary in your Cargo bin directory (usually `~/.cargo/bin/`). 29 | 30 | 3. Ensure that your Cargo bin directory is in your system's PATH. 31 | 32 | Alternatively, if you want to contribute or modify the code: 33 | 34 | 1. Clone the repository: 35 | 36 | ```sh 37 | git clone https://github.com/schwa/multigit-rs.git 38 | cd multigit-rs 39 | ``` 40 | 41 | 2. Build and install from the local source: 42 | 43 | ```sh 44 | cargo install --path . 45 | ``` 46 | 47 | After installation, you can run `multigit --version` to verify that it's installed correctly. 48 | 49 | ## Repository Management 50 | 51 | ### Registering Repositories 52 | 53 | To start using Multigit, you first need to register your Git repositories: 54 | 55 | ```sh 56 | multigit register 57 | ``` 58 | 59 | The `` can be: 60 | 61 | - A direct path to a Git repository 62 | - A path to a directory containing one or more Git repositories 63 | 64 | If you provide a directory path, Multigit will recursively search for all Git repositories within that directory and register them. 65 | 66 | Example: 67 | ```sh 68 | multigit register ~/projects/repo1 ~/projects/repo2 ~/all-projects 69 | ``` 70 | 71 | ### Unregistering Repositories 72 | 73 | To remove repositories from Multigit's management: 74 | 75 | ```sh 76 | multigit unregister 77 | ``` 78 | 79 | Similar to the register command, `` can be a direct path to a Git repository or a directory. If a directory is provided, all registered repositories within that directory will be unregistered. 80 | 81 | To unregister all repositories at once: 82 | 83 | ```sh 84 | multigit unregister --all 85 | ``` 86 | 87 | ### Listing Repositories 88 | 89 | To confirm whether repositories are registered or to see all managed repositories: 90 | 91 | ```sh 92 | multigit list 93 | ``` 94 | 95 | This command is useful to verify if a repository was successfully registered or unregistered. 96 | 97 | ## Common Git Operations 98 | 99 | Multigit provides the following commands for managing your repositories: 100 | 101 | ```sh 102 | multigit [COMMAND] [OPTIONS] 103 | ``` 104 | 105 | ### Commands: 106 | 107 | - `status`: Show the status of repositories 108 | - `add`: Add files to the staging area in selected repositories 109 | - `commit`: Commit changes in selected repositories 110 | - `push`: Push changes to remote repositories 111 | - `pull`: Pull changes from remote repositories 112 | - `exec`: Execute a custom command in selected repositories 113 | - `ui`: Open the configured Git UI program for selected repositories 114 | 115 | ### Options: 116 | 117 | Most commands support the following option: 118 | 119 | - `--filter `: Apply filters to select specific repositories 120 | 121 | ### Examples: 122 | 123 | 1. Check status of all repositories: 124 | ```sh 125 | multigit status 126 | ``` 127 | 128 | 2. Check status of dirty repositories: 129 | ```sh 130 | multigit status --filter dirty 131 | ``` 132 | 133 | 3. Commit changes in dirty repositories: 134 | ```sh 135 | multigit commit --filter dirty -m "Update documentation" 136 | ``` 137 | 138 | 4. Pull changes in dirty repositories: 139 | ```sh 140 | multigit pull --filter dirty 141 | ``` 142 | 143 | 5. Execute a custom command in dirty repositories: 144 | ```sh 145 | multigit exec --filter dirty -- git log --oneline -n 5 146 | ``` 147 | 148 | ## Configuration 149 | 150 | Multigit can be configured by editing the TOML file located at `~/.config/multigit/config.toml`. 151 | 152 | [Add more details about configuration options and their effects] 153 | 154 | ## Contributing 155 | 156 | Contributions are welcome! Please feel free to submit a Pull Request. 157 | 158 | ## License 159 | 160 | This project is licensed under the MIT License. 161 | 162 | ## Author 163 | 164 | Jonathan Wight 165 | 166 | ## Version 167 | 168 | 0.1 169 | 170 | For more detailed information on each command and its options, use the `--help` flag: 171 | 172 | ```sh 173 | multigit --help 174 | multigit [COMMAND] --help 175 | ``` 176 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | fn main() -> shadow_rs::SdResult<()> { 2 | shadow_rs::new() 3 | } 4 | -------------------------------------------------------------------------------- /scripts/demo.tape: -------------------------------------------------------------------------------- 1 | Set Shell bash 2 | Set FontSize 12 3 | Set Width 960 4 | Set Height 480 5 | Type "multigit list" 6 | Enter 7 | Sleep 8 8 | -------------------------------------------------------------------------------- /scripts/replace.awk: -------------------------------------------------------------------------------- 1 | # Awk script to replace a code block (at INDEX) in a markdown file with the output of a command (REPLACEMENT). 2 | # 3 | # awk -f replace.awk -v INDEX=2 -v "REPLACEMENT=cargo run -- --help 2> /dev/null" README.md | sponge README.md 4 | 5 | match($0, "```[a-z]+$") { 6 | count += 1; 7 | if (count == INDEX) { 8 | skip = 1; 9 | } 10 | } 11 | skip == 0 { print } 12 | match($0, "```$") { 13 | skip = 0; 14 | if (count == INDEX) { 15 | print("```sh") 16 | system(REPLACEMENT) 17 | print("```") 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A library for managing multiple Git repositories. 2 | //! 3 | //! This library provides functionalities to register, unregister, list, and perform Git operations on multiple repositories. 4 | //! It supports filtering repositories based on their state and provides utilities to execute commands across repositories. 5 | 6 | use anyhow::{anyhow, Context, Result}; 7 | use colored_markup::{println_markup, StyleSheet}; 8 | use csv::WriterBuilder; 9 | use fern::colors::{Color, ColoredLevelConfig}; 10 | use inquire::Confirm; 11 | use path_absolutize::Absolutize; 12 | use patharg::InputArg; 13 | use serde::{Deserialize, Serialize}; 14 | use std::collections::{HashMap, HashSet}; 15 | use std::env; 16 | use std::fmt; 17 | use std::fs; 18 | use std::io; 19 | use std::io::Read; 20 | use std::path::{Path, PathBuf}; 21 | use std::process::Command; 22 | use std::time::SystemTime; 23 | use tabled::{Table, Tabled}; 24 | use walkdir::WalkDir; 25 | 26 | /// Represents an entry for a single Git repository. 27 | #[derive(Debug, Deserialize, Serialize)] 28 | pub struct RepositoryEntry { 29 | /// The path to the repository. 30 | pub path: PathBuf, 31 | } 32 | 33 | impl RepositoryEntry { 34 | fn current_branch(&self) -> Result { 35 | let repo = git2::Repository::open(&self.path)?; 36 | let head = repo.head()?; 37 | let branch = head.shorthand().unwrap(); 38 | Ok(branch.to_string()) 39 | } 40 | 41 | fn has_tracking_branch(&self) -> Result { 42 | let repo = git2::Repository::open(&self.path)?; 43 | let has_upstream = repo 44 | .head() 45 | .ok() 46 | .and_then(|head| head.shorthand().map(|s| s.to_owned())) 47 | .and_then(|branch_name| repo.find_branch(&branch_name, git2::BranchType::Local).ok()) 48 | .map(|branch| branch.upstream().is_ok()) 49 | .unwrap_or(false); 50 | 51 | Ok(has_upstream) 52 | } 53 | 54 | fn behind_remote(&self) -> Result> { 55 | let repo = git2::Repository::open(&self.path)?; 56 | let head = repo.head()?; 57 | let branch = head.shorthand().unwrap(); 58 | let branch = repo.find_branch(branch, git2::BranchType::Local)?; 59 | if branch.upstream().is_err() { 60 | return Ok(None); 61 | } 62 | let upstream = branch.upstream()?; 63 | let (_, behind) = repo.graph_ahead_behind( 64 | branch.get().target().unwrap(), 65 | upstream.get().target().unwrap(), 66 | )?; 67 | Ok(Some(behind > 0)) 68 | } 69 | 70 | fn ahead_remote(&self) -> Result> { 71 | let repo = git2::Repository::open(&self.path)?; 72 | let head = repo.head()?; 73 | let branch = head.shorthand().unwrap(); 74 | let branch = repo.find_branch(branch, git2::BranchType::Local)?; 75 | // if no upstream is set, return None 76 | if branch.upstream().is_err() { 77 | return Ok(None); 78 | } 79 | let upstream = branch.upstream()?; 80 | let (ahead, _) = repo.graph_ahead_behind( 81 | branch.get().target().unwrap(), 82 | upstream.get().target().unwrap(), 83 | )?; 84 | Ok(Some(ahead > 0)) 85 | } 86 | 87 | fn has_stashes(&self) -> Result { 88 | let mut repo = git2::Repository::open(&self.path)?; 89 | let mut has_stashes = false; 90 | repo.stash_foreach(|_, _, _| { 91 | has_stashes = true; 92 | false 93 | })?; 94 | Ok(has_stashes) 95 | } 96 | } 97 | 98 | /// Represents an entry for a directory containing Git repositories. 99 | #[derive(Debug, Deserialize, Serialize)] 100 | pub struct DirectoryEntry { 101 | /// The path to the directory. 102 | pub path: PathBuf, 103 | } 104 | 105 | impl RepositoryEntry { 106 | /// Retrieves the state of the repository. 107 | /// 108 | /// Returns a `RepositoryState` containing information about the repository's status. 109 | pub fn state(&self) -> Result { 110 | let mut state = RepositoryState { 111 | entries: HashSet::new(), 112 | }; 113 | 114 | let git_repo = git2::Repository::open(&self.path)?; 115 | let mut status_options = git2::StatusOptions::new(); 116 | status_options.include_untracked(true); 117 | status_options.include_ignored(false); 118 | let statuses = git_repo.statuses(Some(&mut status_options))?; 119 | for status in statuses.into_iter() { 120 | match status.status() { 121 | git2::Status::INDEX_NEW 122 | | git2::Status::INDEX_MODIFIED 123 | | git2::Status::INDEX_DELETED 124 | | git2::Status::INDEX_RENAMED 125 | | git2::Status::INDEX_TYPECHANGE 126 | | git2::Status::WT_NEW 127 | | git2::Status::WT_MODIFIED 128 | | git2::Status::WT_DELETED 129 | | git2::Status::WT_TYPECHANGE 130 | | git2::Status::WT_RENAMED 131 | | git2::Status::CONFLICTED => { 132 | state.entries.insert(EntryState::Dirty); 133 | } 134 | _ => {} 135 | } 136 | } 137 | anyhow::Ok(state) 138 | } 139 | 140 | #[allow(dead_code)] 141 | fn is_dirty(&self) -> bool { 142 | let state = self.state().unwrap(); 143 | state.entries.contains(&EntryState::Dirty) 144 | } 145 | } 146 | 147 | /// Configuration data for the application, including registered repositories and directories. 148 | #[derive(Debug, Deserialize, Serialize, Default)] 149 | pub struct Config { 150 | /// A map of repository names to their entries. 151 | #[serde(default = "HashMap::new")] 152 | pub repositories: HashMap, 153 | 154 | /// A map of directory names to their entries. 155 | #[serde(default = "HashMap::new")] 156 | pub directories: HashMap, 157 | } 158 | 159 | impl Config { 160 | /// Loads the configuration from the default config file. 161 | pub fn load(path: InputArg) -> Result { 162 | log::debug!("{:?}", path); 163 | // if file at path is missing, return default config 164 | if let InputArg::Path(path) = &path { 165 | let expanded_path = shellexpand::tilde(path.to_str().unwrap()); 166 | let path = PathBuf::from(expanded_path.to_string()); 167 | if !path.exists() { 168 | log::info!( 169 | "Config file not found at '{:?}'. Using default configuration.", 170 | path 171 | ); 172 | return Ok(Config::default()); 173 | } 174 | } 175 | 176 | let content = match path { 177 | InputArg::Stdin => { 178 | let mut buffer = String::new(); 179 | io::stdin().read_to_string(&mut buffer)?; 180 | buffer 181 | } 182 | InputArg::Path(path) => { 183 | let expanded_path = shellexpand::tilde(path.to_str().unwrap()); 184 | let config_path = PathBuf::from(expanded_path.to_string()); 185 | fs::read_to_string(config_path) 186 | .map_err(|e| anyhow!("Failed to read config file: {}", e))? 187 | } 188 | }; 189 | 190 | toml::from_str(&content) 191 | .map_err(|e| anyhow!("Failed to parse config: {}", e)) 192 | .or_else(|_| Ok(Config::default())) 193 | } 194 | 195 | /// Saves the current configuration to the default config file. 196 | pub fn save(&self) -> Result<()> { 197 | let config_path = "~/.config/multigit/config.toml"; 198 | 199 | // if file doesn't exist, create it and intermediate paths 200 | let expanded_path = shellexpand::tilde(config_path); 201 | let path = PathBuf::from(expanded_path.to_string()); 202 | if !path.exists() { 203 | if let Some(parent) = path.parent() { 204 | fs::create_dir_all(parent)?; 205 | } 206 | } 207 | 208 | let config_path = shellexpand::tilde(config_path); 209 | let config_path = config_path.to_string(); 210 | let config_content = toml::to_string(&self)?; 211 | std::fs::write(config_path, config_content)?; 212 | anyhow::Ok(()) 213 | } 214 | 215 | /// Registers a path as a repository or directory. 216 | /// 217 | /// If the path is a Git repository, it is added to the repositories map. 218 | /// If the path is a directory containing repositories, it is added to the directories map. 219 | pub fn register(&mut self, path: &Path) -> Result<()> { 220 | let absolute_path = path.absolutize().context("Failed to get absolute path")?; 221 | let name = absolute_path 222 | .to_str() 223 | .context("Failed to convert path to string")?; 224 | 225 | if !is_git_repository(path) { 226 | let entry = DirectoryEntry { 227 | path: path.to_path_buf(), 228 | }; 229 | self.directories.insert(name.to_string(), entry); 230 | } else { 231 | let entry = RepositoryEntry { 232 | path: path.to_path_buf(), 233 | }; 234 | self.repositories.insert(name.to_string(), entry); 235 | } 236 | self.save()?; 237 | anyhow::Ok(()) 238 | } 239 | 240 | /// Unregisters a repository or directory. 241 | pub fn unregister(&mut self, path: &PathBuf) -> Result<()> { 242 | let absolute_path = path.absolutize().context("Failed to get absolute path")?; 243 | let name = absolute_path 244 | .to_str() 245 | .context("Failed to convert path to string")?; 246 | self.directories.remove(name); 247 | self.repositories.remove(name); 248 | self.save()?; 249 | anyhow::Ok(()) 250 | } 251 | } 252 | 253 | /// Represents the main application handling multiple repositories. 254 | #[derive(Debug)] 255 | pub struct Multigit { 256 | /// The configuration containing repositories and directories. 257 | pub config: Config, 258 | 259 | pub directory: Option, 260 | 261 | /// The stylesheet used for colored output. 262 | pub style_sheet: StyleSheet<'static>, 263 | } 264 | 265 | impl Multigit { 266 | /// Creates a new instance of `Multigit`. 267 | pub fn new(config: Config, directory: Option) -> Result { 268 | let style_sheet = StyleSheet::parse( 269 | " 270 | repository { foreground: cyan; } 271 | status { foreground: yellow; } 272 | command { foreground: green; } 273 | divider { foreground: red; } 274 | ", 275 | ) 276 | .unwrap(); 277 | 278 | anyhow::Ok(Self { 279 | config, 280 | directory, 281 | style_sheet, 282 | }) 283 | } 284 | 285 | /// Retrieves all repositories, optionally filtering them. 286 | fn all_repositories(&self, filter: Option<&Vec>) -> Result> { 287 | let mut repositories: Vec = Vec::new(); 288 | 289 | if self.directory.is_some() { 290 | let directory = self.directory.as_ref().unwrap(); 291 | let directory_repositories = find_repositories(directory)?; 292 | let mut repositories: Vec = Vec::new(); 293 | for repository in directory_repositories { 294 | let repository = RepositoryEntry { path: repository }; 295 | repositories.push(repository); 296 | } 297 | return Ok(repositories); 298 | } else { 299 | for (_, repository) in self.config.repositories.iter() { 300 | repositories.push(RepositoryEntry { 301 | path: repository.path.clone(), 302 | }); 303 | } 304 | for (_, directory) in self.config.directories.iter() { 305 | let directory_repositories = find_repositories(&directory.path)?; 306 | for repository in directory_repositories { 307 | let repository = RepositoryEntry { path: repository }; 308 | repositories.push(repository); 309 | } 310 | } 311 | } 312 | 313 | if let Some(filter) = filter { 314 | if !filter.is_empty() { 315 | repositories.retain(|repository| { 316 | for f in filter { 317 | match f { 318 | Filter::Dirty => { 319 | if repository 320 | .state() 321 | .unwrap() 322 | .entries 323 | .contains(&EntryState::Dirty) 324 | { 325 | return true; 326 | } 327 | } 328 | Filter::Tracking => { 329 | if repository.has_tracking_branch().unwrap() { 330 | return true; 331 | } 332 | } 333 | } 334 | } 335 | false 336 | }); 337 | } 338 | } 339 | repositories.sort_by(|a, b| a.path.cmp(&b.path)); 340 | anyhow::Ok(repositories) 341 | } 342 | 343 | #[allow(dead_code)] 344 | fn iter_repositories( 345 | &self, 346 | filter: Option<&Vec>, 347 | ) -> Result> { 348 | let repositories = self.all_repositories(filter)?; 349 | Ok(repositories.into_iter()) 350 | } 351 | 352 | fn process_repositories( 353 | &self, 354 | repositories: &[RepositoryEntry], 355 | mut process: F, 356 | ) -> Result<()> 357 | where 358 | F: FnMut(&RepositoryEntry) -> Result<()>, 359 | { 360 | let mut errors = Vec::new(); 361 | 362 | for repository in repositories { 363 | if let Err(e) = process(repository) { 364 | eprintln!("Error processing repository {:?}: {}", repository.path, e); 365 | errors.push(RepositoryError { 366 | path: repository.path.clone(), 367 | error: e, 368 | }); 369 | } 370 | } 371 | 372 | if errors.is_empty() { 373 | anyhow::Ok(()) 374 | } else { 375 | Err(anyhow!("Errors occurred in {} repositories", errors.len())) 376 | } 377 | } 378 | 379 | /// Registers paths as repositories or directories. 380 | pub fn register(&mut self, paths: &Vec) -> Result<()> { 381 | if paths.is_empty() { 382 | self.config.register(&std::env::current_dir()?)?; 383 | } else { 384 | for path in paths { 385 | self.config.register(path)?; 386 | } 387 | } 388 | self.config.save()?; 389 | anyhow::Ok(()) 390 | } 391 | 392 | /// Unregisters repositories or directories. 393 | pub fn unregister(&mut self, paths: &Vec, all: &bool) -> Result<()> { 394 | if *all { 395 | let ans = Confirm::new("Unregister all repositories and directories??") 396 | .with_default(false) 397 | .prompt()?; 398 | match ans { 399 | true => { 400 | self.config.repositories.clear(); 401 | self.config.directories.clear(); 402 | } 403 | false => { 404 | return anyhow::Ok(()); 405 | } 406 | } 407 | } else if paths.is_empty() { 408 | self.config.unregister(&std::env::current_dir()?)?; 409 | } else { 410 | for path in paths { 411 | self.config.unregister(path)?; 412 | } 413 | } 414 | self.config.save()?; 415 | anyhow::Ok(()) 416 | } 417 | 418 | /// Lists all registered repositories. 419 | pub fn list(&self, filter: Option<&Vec>, detailed: &bool, csv: &bool) -> Result<()> { 420 | let repositories = self.all_repositories(filter)?; 421 | 422 | #[derive(Tabled, Debug, Serialize)] 423 | struct Row { 424 | name: String, 425 | 426 | #[tabled(skip)] 427 | path: PathBuf, 428 | #[serde(skip)] 429 | state: RepositoryState, 430 | current_branch: String, 431 | #[tabled(display_with = "display_option")] 432 | behind_remote: Option, 433 | #[tabled(display_with = "display_option")] 434 | ahead_remote: Option, 435 | has_stashes: bool, 436 | } 437 | 438 | let rows = repositories.iter().map(|repository| { 439 | let name = repository 440 | .path 441 | .file_name() 442 | .unwrap() 443 | .to_str() 444 | .unwrap() 445 | .to_string(); 446 | // let path = repository.path.display(); 447 | Row { 448 | name, 449 | path: repository.path.clone(), 450 | state: repository.state().unwrap(), 451 | current_branch: repository.current_branch().unwrap(), 452 | behind_remote: repository.behind_remote().ok().flatten(), 453 | ahead_remote: repository.ahead_remote().ok().flatten(), 454 | has_stashes: repository.has_stashes().unwrap(), 455 | } 456 | }); 457 | 458 | if !detailed { 459 | for row in rows { 460 | println_markup!( 461 | &self.style_sheet, 462 | "{}", 463 | row.path.display() 464 | ); 465 | } 466 | } else if *csv { 467 | let mut wtr = WriterBuilder::new().from_writer(vec![]); 468 | 469 | for row in rows { 470 | wtr.serialize(row)?; 471 | } 472 | let data = String::from_utf8(wtr.into_inner()?)?; 473 | println!("{}", data); 474 | } else { 475 | let table = Table::new(rows).to_string(); 476 | println!("{}", table); 477 | } 478 | 479 | Ok(()) 480 | } 481 | 482 | /// Shows the status of all repositories. 483 | pub fn status(&self, filter: Option<&Vec>) -> Result<()> { 484 | let repositories = self.all_repositories(filter)?; 485 | self.process_repositories(&repositories, |repository| { 486 | let mut status_options = git2::StatusOptions::new(); 487 | status_options.include_untracked(true); 488 | status_options.include_ignored(false); 489 | let repo = git2::Repository::open(&repository.path)?; 490 | let status = repo.statuses(Some(&mut status_options))?; 491 | if !status.is_empty() { 492 | let mut index_new: bool = false; 493 | let mut index_modified: bool = false; 494 | let mut index_deleted: bool = false; 495 | let mut index_renamed: bool = false; 496 | let mut index_typechange: bool = false; 497 | let mut wt_new: bool = false; 498 | let mut wt_modified: bool = false; 499 | let mut wt_deleted: bool = false; 500 | let mut wt_typechange: bool = false; 501 | let mut wt_renamed: bool = false; 502 | let mut ignored: bool = false; 503 | let mut conflicted: bool = false; 504 | 505 | for entry in status.iter() { 506 | match entry.status() { 507 | git2::Status::INDEX_NEW => index_new = true, 508 | git2::Status::INDEX_MODIFIED => index_modified = true, 509 | git2::Status::INDEX_DELETED => index_deleted = true, 510 | git2::Status::INDEX_RENAMED => index_renamed = true, 511 | git2::Status::INDEX_TYPECHANGE => index_typechange = true, 512 | git2::Status::WT_NEW => wt_new = true, 513 | git2::Status::WT_MODIFIED => wt_modified = true, 514 | git2::Status::WT_DELETED => wt_deleted = true, 515 | git2::Status::WT_TYPECHANGE => wt_typechange = true, 516 | git2::Status::WT_RENAMED => wt_renamed = true, 517 | git2::Status::IGNORED => ignored = true, 518 | git2::Status::CONFLICTED => conflicted = true, 519 | _ => {} 520 | } 521 | } 522 | 523 | let mut status_string = String::new(); 524 | 525 | if index_new { 526 | status_string.push_str(" [new]"); 527 | } 528 | if index_modified { 529 | status_string.push_str(" [modified]"); 530 | } 531 | if index_deleted { 532 | status_string.push_str(" [deleted]"); 533 | } 534 | if index_renamed { 535 | status_string.push_str(" [renamed]"); 536 | } 537 | if index_typechange { 538 | status_string.push_str(" [typechange]"); 539 | } 540 | if wt_new { 541 | status_string.push_str(" [wt-new]"); 542 | } 543 | if wt_modified { 544 | status_string.push_str(" [wt-modified]"); 545 | } 546 | if wt_deleted { 547 | status_string.push_str(" [wt-deleted]"); 548 | } 549 | if wt_typechange { 550 | status_string.push_str(" [wt-typechange]"); 551 | } 552 | if wt_renamed { 553 | status_string.push_str(" [wt-renamed]"); 554 | } 555 | if ignored { 556 | status_string.push_str(" [ignored]"); 557 | } 558 | if conflicted { 559 | status_string.push_str(" [conflicted]"); 560 | } 561 | 562 | println_markup!( 563 | &self.style_sheet, 564 | "{}{}", 565 | repository.path.to_str().unwrap(), 566 | status_string 567 | ); 568 | } 569 | anyhow::Ok(()) 570 | }) 571 | } 572 | 573 | /// Opens the configured Git UI for the selected repositories. 574 | pub fn ui(&self, filter: Option<&Vec>) -> Result<()> { 575 | let paths_to_open = self.all_repositories(filter)?; 576 | if paths_to_open.len() > 1 { 577 | let ans = Confirm::new(format!("Open {} repositories?", paths_to_open.len()).as_str()) 578 | .with_default(false) 579 | .prompt()?; 580 | if !ans { 581 | return anyhow::Ok(()); 582 | } 583 | } 584 | for repository in paths_to_open.iter() { 585 | println_markup!( 586 | &self.style_sheet, 587 | "Opening git ui for {}", 588 | repository.path.to_str().unwrap() 589 | ); 590 | open_in_git_ui(&repository.path)?; 591 | } 592 | anyhow::Ok(()) 593 | } 594 | 595 | /// Executes a custom command in the selected repositories. 596 | pub fn exec(&self, filter: Option<&Vec>, commands: &[String]) -> Result<()> { 597 | let repositories = self.all_repositories(filter)?; 598 | self.process_repositories(&repositories, |repository| { 599 | let mut command = std::process::Command::new(&commands[0]); 600 | command.args(&commands[1..]); 601 | command.current_dir(&repository.path); 602 | let status = command.status()?; 603 | if !status.success() { 604 | return Err(anyhow!("Failed to execute command")); 605 | } 606 | Ok(()) 607 | }) 608 | } 609 | 610 | /// Executes a Git command with optional arguments in the selected repositories. 611 | pub fn git_command( 612 | &self, 613 | git_command: &str, 614 | repositories: &[RepositoryEntry], 615 | passthrough: &[String], 616 | ) -> Result<()> { 617 | let width = termsize::get().unwrap().cols as usize; 618 | 619 | let divider = "#".repeat(width); 620 | 621 | let mut first_repository = true; 622 | 623 | self.process_repositories(repositories, |repository| { 624 | if !first_repository { 625 | println_markup!(&self.style_sheet, "\n{}\n", divider); 626 | } 627 | first_repository = false; 628 | println_markup!( 629 | &self.style_sheet, 630 | "Running `{}` in {}\n", 631 | git_command, 632 | repository.path.to_str().unwrap() 633 | ); 634 | let mut args = vec![git_command]; 635 | args.extend(passthrough.iter().map(|s| s.as_str())); 636 | let mut command = std::process::Command::new("git"); 637 | command.args(&args); 638 | command.current_dir(&repository.path); 639 | 640 | // Execute the command and capture the status 641 | let status = command.status()?; 642 | 643 | // Check if the command was successful 644 | if !status.success() { 645 | return Err(anyhow!( 646 | "Git command {} failed in repository `{}` with exit code {:?}", 647 | git_command, 648 | repository.path.display(), 649 | status.code() 650 | )); 651 | } 652 | Ok(()) 653 | }) 654 | } 655 | 656 | /// Commits changes in the selected repositories. 657 | pub fn commit(&self, filter: Option<&Vec>, passthrough: &[String]) -> Result<()> { 658 | let repositories = self.all_repositories(filter)?; 659 | self.git_command("commit", &repositories, passthrough) 660 | } 661 | 662 | /// Adds files to the staging area in the selected repositories. 663 | pub fn add(&self, filter: Option<&Vec>, passthrough: &[String]) -> Result<()> { 664 | let repositories = self.all_repositories(filter)?; 665 | self.git_command("add", &repositories, passthrough) 666 | } 667 | 668 | /// Pushes changes to remote repositories. 669 | pub fn push(&self, filter: Option<&Vec>, passthrough: &[String]) -> Result<()> { 670 | let repositories = self.all_repositories(filter)?; 671 | 672 | self.git_command("push", &repositories, passthrough) 673 | } 674 | 675 | /// Pulls changes from remote repositories. 676 | pub fn pull(&self, filter: Option<&Vec>, passthrough: &[String]) -> Result<()> { 677 | let repositories = self 678 | .all_repositories(filter)? 679 | .into_iter() 680 | .filter(|repo| repo.has_tracking_branch().unwrap()) 681 | .collect::>(); 682 | // let repositories = self.all_repositories(filter)?; 683 | 684 | self.git_command("pull", &repositories, passthrough) 685 | } 686 | 687 | /// Fetchs changes from remote repositories. 688 | pub fn fetch(&self, filter: Option<&Vec>, passthrough: &[String]) -> Result<()> { 689 | let repositories = self.all_repositories(filter)?; 690 | self.git_command("fetch", &repositories, passthrough) 691 | } 692 | 693 | pub fn config(&self) -> Result<()> { 694 | let editor = env::var("EDITOR").unwrap_or_else(|_| "vi".to_string()); 695 | let config_path = "~/.config/multigit/config.toml"; 696 | let config_path = shellexpand::tilde(config_path); 697 | let full_command = format!("{} {}", editor, config_path); 698 | let args = shell_words::split(&full_command)?; 699 | let (cmd, args) = args.split_first().ok_or("Empty command").unwrap(); 700 | let status = Command::new(cmd).args(args).status()?; 701 | if !status.success() { 702 | return Err(anyhow!("Failed to execute command")); 703 | } 704 | Ok(()) 705 | } 706 | } 707 | 708 | /// Enum representing possible filters for repositories. 709 | #[derive(clap::ValueEnum, Clone, Debug, Serialize)] 710 | pub enum Filter { 711 | /// Filter repositories that have uncommitted changes. 712 | Dirty, 713 | /// Filter where current branch is tracking remote 714 | Tracking, 715 | } 716 | 717 | /// Enum representing the state of repository entries. 718 | #[derive(Clone, Debug, Hash, PartialEq, Eq, Serialize)] 719 | pub enum EntryState { 720 | /// Indicates that the repository has uncommitted changes. 721 | Dirty, 722 | } 723 | 724 | /// Represents the state of a repository. 725 | #[derive(Debug, PartialEq, Eq, Serialize)] 726 | pub struct RepositoryState { 727 | /// A set of entry states. 728 | pub entries: HashSet, 729 | } 730 | 731 | /// Opens the configured Git UI for a given repository path. 732 | pub fn open_in_git_ui(path: &Path) -> Result<()> { 733 | let editor = "gitup"; 734 | let status = std::process::Command::new(editor) 735 | .current_dir(path) 736 | .status()?; 737 | if !status.success() { 738 | return Err(anyhow!("Failed to open git ui")); 739 | } 740 | Ok(()) 741 | } 742 | 743 | /// Finds all Git repositories within a given path. 744 | pub fn find_repositories(path: &Path) -> Result> { 745 | let mut repositories = Vec::new(); 746 | let walker = WalkDir::new(path).into_iter().filter_entry(|e| { 747 | e.file_type().is_dir() && !is_hidden(e.path()) && e.path().file_name().unwrap() != ".git" 748 | }); 749 | for entry in walker { 750 | let entry = entry?; 751 | if is_git_repository(entry.path()) { 752 | let path = entry.path(); 753 | repositories.push(path.to_path_buf()); 754 | } 755 | } 756 | Ok(repositories) 757 | } 758 | 759 | /// Checks if a path is a Git repository. 760 | pub fn is_git_repository(path: &Path) -> bool { 761 | path.join(".git").exists() 762 | } 763 | 764 | /// Checks if a path is hidden (starts with a dot). 765 | pub fn is_hidden(path: &Path) -> bool { 766 | path.file_name().unwrap().to_str().unwrap().starts_with('.') 767 | } 768 | 769 | /// Returns `None` if the vector is empty, otherwise returns `Some(&Vec)`. 770 | pub fn noneify(v: &Vec) -> Option<&Vec> { 771 | if v.is_empty() { 772 | None 773 | } else { 774 | Some(v) 775 | } 776 | } 777 | 778 | #[allow(dead_code)] 779 | struct RepositoryError { 780 | path: PathBuf, 781 | error: anyhow::Error, 782 | } 783 | 784 | impl fmt::Display for EntryState { 785 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 786 | match self { 787 | EntryState::Dirty => write!(f, "Dirty"), 788 | } 789 | } 790 | } 791 | 792 | impl fmt::Display for RepositoryState { 793 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 794 | if self.entries.is_empty() { 795 | write!(f, "Clean") 796 | } else { 797 | let states: Vec = self.entries.iter().map(|state| state.to_string()).collect(); 798 | write!(f, "{}", states.join(", ")) 799 | } 800 | } 801 | } 802 | 803 | fn display_option(o: &Option) -> String { 804 | match o { 805 | Some(s) => format!("{}", s), 806 | None => "".to_string(), 807 | } 808 | } 809 | 810 | pub fn setup_logger( 811 | level_filter: log::LevelFilter, 812 | //log_path: &Option, 813 | start_time: SystemTime, 814 | ) -> Result<()> { 815 | let colors = ColoredLevelConfig::new() 816 | .info(Color::Green) 817 | .debug(Color::Magenta); 818 | let mut base_logger = fern::Dispatch::new(); 819 | let console_logger = fern::Dispatch::new() 820 | .level(level_filter) 821 | .format(move |out, message, record| { 822 | let duration = SystemTime::now().duration_since(start_time).unwrap(); 823 | let duration_string = format!("{:10.3}", duration.as_secs_f64()); 824 | out.finish(format_args!( 825 | "{} {:8.8} {:24.24} | {}", 826 | duration_string, 827 | colors.color(record.level()), 828 | record.target(), 829 | message 830 | )) 831 | }) 832 | .chain(std::io::stdout()); 833 | base_logger = base_logger.chain(console_logger); 834 | 835 | // if let Some(log_path) = log_path { 836 | // let file_logger = fern::Dispatch::new() 837 | // .format(move |out, message, record| { 838 | // out.finish(format_args!( 839 | // "[{} {} {}] {}", 840 | // humantime::format_rfc3339_seconds(SystemTime::now()), 841 | // record.level(), 842 | // record.target(), 843 | // message 844 | // )) 845 | // }) 846 | // .chain(fern::log_file(log_path)?); 847 | // base_logger = base_logger.chain(file_logger); 848 | // } 849 | 850 | base_logger.apply()?; 851 | 852 | Ok(()) 853 | } 854 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | //! A multi-command CLI program to manage multiple Git repositories. 2 | //! 3 | //! This program allows users to perform Git operations across multiple repositories simultaneously. 4 | //! It supports commands like `add`, `commit`, `push`, `pull`, `exec`, `list`, `register`, `status`, `ui`, and `unregister`. 5 | 6 | use anyhow::Result; 7 | use clap::{CommandFactory, Parser, Subcommand}; 8 | use clap_complete::{generate, Shell}; 9 | use clap_verbosity_flag::{Verbosity, WarnLevel}; 10 | use multigit::*; 11 | use patharg::InputArg; 12 | use shadow_rs::shadow; 13 | use std::io; 14 | use std::path::PathBuf; 15 | use std::time::SystemTime; 16 | 17 | shadow!(build); 18 | 19 | /// The main CLI struct that defines the command-line interface. 20 | #[derive(Parser)] 21 | #[clap(name = build::PROJECT_NAME)] 22 | #[clap(author = "Jonathan Wight")] 23 | #[clap(version = build::PKG_VERSION)] 24 | #[clap(about = "A multi-command CLI example", long_about = None)] 25 | #[derive(Debug)] 26 | 27 | struct Cli { 28 | /// The configuration file to use. 29 | #[arg(short, long)] 30 | #[clap(default_value = "~/.config/multigit/config.toml")] 31 | config: InputArg, 32 | 33 | /// Directory to use instead of registering directories/repositories. 34 | #[arg(short, long)] 35 | directory: Option, 36 | 37 | /// Set the log level. 38 | #[clap(flatten)] 39 | verbose: Verbosity, 40 | 41 | /// The subcommand to execute. 42 | #[clap(subcommand)] 43 | command: Commands, 44 | } 45 | 46 | /// Enum representing the possible commands. 47 | #[derive(Subcommand, Debug)] 48 | 49 | enum Commands { 50 | /// Register git repositories or directories of git repositories. 51 | /// 52 | /// Adds new repositories to be managed by the tool. 53 | Register { 54 | /// Paths to repositories or directories containing repositories. 55 | paths: Vec, 56 | }, 57 | 58 | /// Unregister git repositories or directories of git repositories. 59 | /// 60 | /// Removes repositories from being managed by the tool. 61 | Unregister { 62 | /// Unregister all repositories. 63 | #[arg(short, long)] 64 | all: bool, 65 | 66 | /// Paths to repositories or directories to unregister. 67 | paths: Vec, 68 | }, 69 | 70 | /// List registered repositories. 71 | /// 72 | /// Shows the list of repositories currently managed by the tool. 73 | List { 74 | /// Filters to select specific repositories. 75 | #[arg(short, long)] 76 | filter: Vec, 77 | 78 | #[arg(short, long)] 79 | #[clap(default_value = "false")] 80 | detailed: bool, 81 | 82 | #[arg(short, long)] 83 | #[clap(default_value = "false")] 84 | csv: bool, 85 | }, 86 | 87 | /// Add files to the staging area in the selected repositories. 88 | Add { 89 | /// Filters to select specific repositories. 90 | #[arg(short, long)] 91 | filter: Vec, 92 | 93 | /// Additional arguments to pass through to the `git add` command. 94 | passthrough: Vec, 95 | }, 96 | /// Commit changes in the selected repositories. 97 | Commit { 98 | /// Filters to select specific repositories. 99 | #[arg(short, long)] 100 | filter: Vec, 101 | 102 | /// Additional arguments to pass through to the `git commit` command. 103 | #[clap(trailing_var_arg = true, allow_hyphen_values = true)] 104 | passthrough: Vec, 105 | }, 106 | /// Push changes to remote repositories. 107 | Push { 108 | /// Filters to select specific repositories. 109 | #[arg(short, long)] 110 | filter: Vec, 111 | 112 | /// Additional arguments to pass through to the `git push` command. 113 | #[clap(trailing_var_arg = true, allow_hyphen_values = true)] 114 | passthrough: Vec, 115 | }, 116 | /// Fetch changes from remote repositories. 117 | Fetch { 118 | /// Filters to select specific repositories. 119 | #[arg(short, long)] 120 | filter: Vec, 121 | 122 | /// Additional arguments to pass through to the `git fetch` command. 123 | #[clap(trailing_var_arg = true, allow_hyphen_values = true)] 124 | passthrough: Vec, 125 | }, 126 | 127 | /// Pull changes from remote repositories. 128 | Pull { 129 | /// Filters to select specific repositories. 130 | #[arg(short, long)] 131 | filter: Vec, 132 | 133 | /// Additional arguments to pass through to the `git pull` command. 134 | #[clap(trailing_var_arg = true, allow_hyphen_values = true)] 135 | passthrough: Vec, 136 | }, 137 | /// Execute a custom command in the selected repositories. 138 | Exec { 139 | /// Filters to select specific repositories. 140 | #[arg(short, long)] 141 | filter: Vec, 142 | 143 | /// The command to execute. 144 | #[clap(trailing_var_arg = true, allow_hyphen_values = true)] 145 | command: Vec, 146 | }, 147 | /// Show the status of repositories. 148 | Status { 149 | /// Filters to select specific repositories. 150 | #[arg(short, long)] 151 | filter: Vec, 152 | }, 153 | /// Open the configured git UI program for the selected repositories. 154 | UI { 155 | /// Filters to select specific repositories. 156 | #[arg(short, long)] 157 | filter: Vec, 158 | }, 159 | /// Edit the configuration file. 160 | Config {}, 161 | /// Generate shell completions. 162 | Completions { 163 | #[arg(short, long)] 164 | shell: String, 165 | }, 166 | } 167 | 168 | /// The main entry point of the program. 169 | fn main() -> Result<()> { 170 | better_panic::install(); 171 | 172 | let start_time = SystemTime::now(); 173 | 174 | // Parse command-line arguments into the `Cli` struct. 175 | let args = Cli::parse(); 176 | 177 | setup_logger( 178 | args.verbose.log_level_filter(), 179 | // &args.export_log, 180 | start_time, 181 | )?; 182 | 183 | log::debug!("{:?}", args); 184 | 185 | let config = Config::load(args.config)?; 186 | 187 | // Create a new instance of `Multigit`. 188 | let mut multigit = Multigit::new(config, args.directory).unwrap(); 189 | 190 | // Match the provided command and execute the corresponding action. 191 | match &args.command { 192 | Commands::List { 193 | filter, 194 | detailed, 195 | csv, 196 | } => multigit.list(noneify(filter), detailed, csv), 197 | Commands::Register { paths } => multigit.register(paths), 198 | Commands::Status { filter } => multigit.status(noneify(filter)), 199 | Commands::Unregister { paths, all } => multigit.unregister(paths, all), 200 | Commands::UI { filter } => multigit.ui(noneify(filter)), 201 | Commands::Exec { filter, command } => multigit.exec(noneify(filter), command), 202 | Commands::Add { 203 | filter, 204 | passthrough, 205 | } => multigit.add(noneify(filter), passthrough), 206 | Commands::Commit { 207 | filter, 208 | passthrough, 209 | } => multigit.commit(noneify(filter), passthrough), 210 | Commands::Push { 211 | filter, 212 | passthrough, 213 | } => multigit.push(noneify(filter), passthrough), 214 | Commands::Pull { 215 | filter, 216 | passthrough, 217 | } => multigit.pull(noneify(filter), passthrough), 218 | Commands::Fetch { 219 | filter, 220 | passthrough, 221 | } => multigit.fetch(noneify(filter), passthrough), 222 | Commands::Config {} => multigit.config(), 223 | Commands::Completions { shell } => { 224 | let shell: Shell = shell.parse().unwrap_or(Shell::Bash); 225 | let mut cmd = Cli::command(); 226 | generate(shell, &mut cmd, "myapp", &mut io::stdout()); 227 | Ok(()) 228 | } 229 | } 230 | } 231 | 232 | #[cfg(test)] 233 | mod tests { 234 | use assert_cmd::prelude::*; 235 | use std::process::Command; 236 | 237 | #[ignore] // Temporarily ignored because cargo_bin is not building the binary. 238 | #[test] 239 | fn run_empty() { 240 | // This will fail because no arguments are provided. 241 | let mut cmd = Command::cargo_bin("multigit").unwrap(); 242 | cmd.assert().failure(); 243 | } 244 | 245 | #[ignore] // Temporarily ignored because cargo_bin is not building the binary. 246 | #[test] 247 | fn run_help() { 248 | let mut cmd = Command::cargo_bin("multigit").unwrap(); 249 | cmd.args(["--help"]); 250 | cmd.assert().success(); 251 | } 252 | } 253 | --------------------------------------------------------------------------------