├── .github └── workflows │ └── rust.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md └── src ├── common.rs ├── git-delete.rs ├── git-fork.rs ├── git-push2.rs └── git-try-merge.rs /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | schedule: 9 | - cron: 0 0 1 * * 10 | 11 | env: 12 | CARGO_TERM_COLOR: always 13 | 14 | jobs: 15 | build: 16 | 17 | runs-on: ubuntu-latest 18 | 19 | outputs: 20 | size: ${{ steps.build.outputs.size }} 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | - run: cargo test --all-features --verbose 25 | - run: cargo doc --verbose --all-features 26 | 27 | checks: 28 | 29 | runs-on: ubuntu-latest 30 | if: ${{ github.event_name == 'pull_request' }} 31 | 32 | steps: 33 | - uses: actions/checkout@v2 34 | - run: cargo fmt -- --check 35 | - uses: actions-rs/clippy-check@v1 36 | with: 37 | token: ${{ secrets.GITHUB_TOKEN }} 38 | args: --all-features -- -D warnings 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "ansi_term" 16 | version = "0.12.1" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" 19 | dependencies = [ 20 | "winapi", 21 | ] 22 | 23 | [[package]] 24 | name = "anyhow" 25 | version = "1.0.79" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" 28 | 29 | [[package]] 30 | name = "atty" 31 | version = "0.2.14" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 34 | dependencies = [ 35 | "hermit-abi", 36 | "libc", 37 | "winapi", 38 | ] 39 | 40 | [[package]] 41 | name = "bitflags" 42 | version = "1.3.2" 43 | source = "registry+https://github.com/rust-lang/crates.io-index" 44 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 45 | 46 | [[package]] 47 | name = "bitflags" 48 | version = "2.4.2" 49 | source = "registry+https://github.com/rust-lang/crates.io-index" 50 | checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" 51 | 52 | [[package]] 53 | name = "bitvec" 54 | version = "0.17.4" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "41262f11d771fd4a61aa3ce019fca363b4b6c282fca9da2a31186d3965a47a5c" 57 | dependencies = [ 58 | "either", 59 | "radium", 60 | ] 61 | 62 | [[package]] 63 | name = "block-buffer" 64 | version = "0.10.4" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 67 | dependencies = [ 68 | "generic-array", 69 | ] 70 | 71 | [[package]] 72 | name = "bstr" 73 | version = "1.9.0" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "c48f0051a4b4c5e0b6d365cd04af53aeaa209e3cc15ec2cdb69e73cc87fbd0dc" 76 | dependencies = [ 77 | "memchr", 78 | "serde", 79 | ] 80 | 81 | [[package]] 82 | name = "cc" 83 | version = "1.0.83" 84 | source = "registry+https://github.com/rust-lang/crates.io-index" 85 | checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" 86 | dependencies = [ 87 | "jobserver", 88 | "libc", 89 | ] 90 | 91 | [[package]] 92 | name = "cfg-if" 93 | version = "1.0.0" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 96 | 97 | [[package]] 98 | name = "clap" 99 | version = "2.34.0" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" 102 | dependencies = [ 103 | "ansi_term", 104 | "atty", 105 | "bitflags 1.3.2", 106 | "strsim", 107 | "textwrap", 108 | "unicode-width", 109 | "vec_map", 110 | ] 111 | 112 | [[package]] 113 | name = "console" 114 | version = "0.15.8" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" 117 | dependencies = [ 118 | "encode_unicode", 119 | "lazy_static", 120 | "libc", 121 | "unicode-width", 122 | "windows-sys 0.52.0", 123 | ] 124 | 125 | [[package]] 126 | name = "cpufeatures" 127 | version = "0.2.12" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" 130 | dependencies = [ 131 | "libc", 132 | ] 133 | 134 | [[package]] 135 | name = "crypto-common" 136 | version = "0.1.6" 137 | source = "registry+https://github.com/rust-lang/crates.io-index" 138 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 139 | dependencies = [ 140 | "generic-array", 141 | "typenum", 142 | ] 143 | 144 | [[package]] 145 | name = "dialoguer" 146 | version = "0.10.4" 147 | source = "registry+https://github.com/rust-lang/crates.io-index" 148 | checksum = "59c6f2989294b9a498d3ad5491a79c6deb604617378e1cdc4bfc1c1361fe2f87" 149 | dependencies = [ 150 | "console", 151 | "shell-words", 152 | "tempfile", 153 | "zeroize", 154 | ] 155 | 156 | [[package]] 157 | name = "digest" 158 | version = "0.10.7" 159 | source = "registry+https://github.com/rust-lang/crates.io-index" 160 | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 161 | dependencies = [ 162 | "block-buffer", 163 | "crypto-common", 164 | ] 165 | 166 | [[package]] 167 | name = "dirs" 168 | version = "3.0.2" 169 | source = "registry+https://github.com/rust-lang/crates.io-index" 170 | checksum = "30baa043103c9d0c2a57cf537cc2f35623889dc0d405e6c3cccfadbc81c71309" 171 | dependencies = [ 172 | "dirs-sys 0.3.7", 173 | ] 174 | 175 | [[package]] 176 | name = "dirs" 177 | version = "5.0.1" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" 180 | dependencies = [ 181 | "dirs-sys 0.4.1", 182 | ] 183 | 184 | [[package]] 185 | name = "dirs-sys" 186 | version = "0.3.7" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" 189 | dependencies = [ 190 | "libc", 191 | "redox_users", 192 | "winapi", 193 | ] 194 | 195 | [[package]] 196 | name = "dirs-sys" 197 | version = "0.4.1" 198 | source = "registry+https://github.com/rust-lang/crates.io-index" 199 | checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" 200 | dependencies = [ 201 | "libc", 202 | "option-ext", 203 | "redox_users", 204 | "windows-sys 0.48.0", 205 | ] 206 | 207 | [[package]] 208 | name = "either" 209 | version = "1.9.0" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" 212 | 213 | [[package]] 214 | name = "encode_unicode" 215 | version = "0.3.6" 216 | source = "registry+https://github.com/rust-lang/crates.io-index" 217 | checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" 218 | 219 | [[package]] 220 | name = "errno" 221 | version = "0.3.8" 222 | source = "registry+https://github.com/rust-lang/crates.io-index" 223 | checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" 224 | dependencies = [ 225 | "libc", 226 | "windows-sys 0.52.0", 227 | ] 228 | 229 | [[package]] 230 | name = "fastrand" 231 | version = "2.0.1" 232 | source = "registry+https://github.com/rust-lang/crates.io-index" 233 | checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" 234 | 235 | [[package]] 236 | name = "form_urlencoded" 237 | version = "1.2.1" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 240 | dependencies = [ 241 | "percent-encoding", 242 | ] 243 | 244 | [[package]] 245 | name = "generic-array" 246 | version = "0.14.7" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 249 | dependencies = [ 250 | "typenum", 251 | "version_check", 252 | ] 253 | 254 | [[package]] 255 | name = "getrandom" 256 | version = "0.2.12" 257 | source = "registry+https://github.com/rust-lang/crates.io-index" 258 | checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" 259 | dependencies = [ 260 | "cfg-if", 261 | "libc", 262 | "wasi", 263 | ] 264 | 265 | [[package]] 266 | name = "git-tools" 267 | version = "0.1.3" 268 | dependencies = [ 269 | "anyhow", 270 | "bitvec", 271 | "dirs 3.0.2", 272 | "git2", 273 | "git2_credentials", 274 | "globset", 275 | "structopt", 276 | "users", 277 | ] 278 | 279 | [[package]] 280 | name = "git2" 281 | version = "0.18.1" 282 | source = "registry+https://github.com/rust-lang/crates.io-index" 283 | checksum = "fbf97ba92db08df386e10c8ede66a2a0369bd277090afd8710e19e38de9ec0cd" 284 | dependencies = [ 285 | "bitflags 2.4.2", 286 | "libc", 287 | "libgit2-sys", 288 | "log", 289 | "openssl-probe", 290 | "openssl-sys", 291 | "url", 292 | ] 293 | 294 | [[package]] 295 | name = "git2_credentials" 296 | version = "0.13.0" 297 | source = "registry+https://github.com/rust-lang/crates.io-index" 298 | checksum = "a297fe29addafaf3c774fdbb8f410e487b819555f067ed94c44c5c2ae15e3702" 299 | dependencies = [ 300 | "dialoguer", 301 | "dirs 5.0.1", 302 | "git2", 303 | "pest", 304 | "pest_derive", 305 | "regex", 306 | ] 307 | 308 | [[package]] 309 | name = "globset" 310 | version = "0.4.14" 311 | source = "registry+https://github.com/rust-lang/crates.io-index" 312 | checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1" 313 | dependencies = [ 314 | "aho-corasick", 315 | "bstr", 316 | "log", 317 | "regex-automata", 318 | "regex-syntax", 319 | ] 320 | 321 | [[package]] 322 | name = "heck" 323 | version = "0.3.3" 324 | source = "registry+https://github.com/rust-lang/crates.io-index" 325 | checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" 326 | dependencies = [ 327 | "unicode-segmentation", 328 | ] 329 | 330 | [[package]] 331 | name = "hermit-abi" 332 | version = "0.1.19" 333 | source = "registry+https://github.com/rust-lang/crates.io-index" 334 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 335 | dependencies = [ 336 | "libc", 337 | ] 338 | 339 | [[package]] 340 | name = "idna" 341 | version = "0.5.0" 342 | source = "registry+https://github.com/rust-lang/crates.io-index" 343 | checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" 344 | dependencies = [ 345 | "unicode-bidi", 346 | "unicode-normalization", 347 | ] 348 | 349 | [[package]] 350 | name = "jobserver" 351 | version = "0.1.27" 352 | source = "registry+https://github.com/rust-lang/crates.io-index" 353 | checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d" 354 | dependencies = [ 355 | "libc", 356 | ] 357 | 358 | [[package]] 359 | name = "lazy_static" 360 | version = "1.4.0" 361 | source = "registry+https://github.com/rust-lang/crates.io-index" 362 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 363 | 364 | [[package]] 365 | name = "libc" 366 | version = "0.2.153" 367 | source = "registry+https://github.com/rust-lang/crates.io-index" 368 | checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" 369 | 370 | [[package]] 371 | name = "libgit2-sys" 372 | version = "0.16.1+1.7.1" 373 | source = "registry+https://github.com/rust-lang/crates.io-index" 374 | checksum = "f2a2bb3680b094add03bb3732ec520ece34da31a8cd2d633d1389d0f0fb60d0c" 375 | dependencies = [ 376 | "cc", 377 | "libc", 378 | "libssh2-sys", 379 | "libz-sys", 380 | "openssl-sys", 381 | "pkg-config", 382 | ] 383 | 384 | [[package]] 385 | name = "libredox" 386 | version = "0.0.1" 387 | source = "registry+https://github.com/rust-lang/crates.io-index" 388 | checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" 389 | dependencies = [ 390 | "bitflags 2.4.2", 391 | "libc", 392 | "redox_syscall", 393 | ] 394 | 395 | [[package]] 396 | name = "libssh2-sys" 397 | version = "0.3.0" 398 | source = "registry+https://github.com/rust-lang/crates.io-index" 399 | checksum = "2dc8a030b787e2119a731f1951d6a773e2280c660f8ec4b0f5e1505a386e71ee" 400 | dependencies = [ 401 | "cc", 402 | "libc", 403 | "libz-sys", 404 | "openssl-sys", 405 | "pkg-config", 406 | "vcpkg", 407 | ] 408 | 409 | [[package]] 410 | name = "libz-sys" 411 | version = "1.1.15" 412 | source = "registry+https://github.com/rust-lang/crates.io-index" 413 | checksum = "037731f5d3aaa87a5675e895b63ddff1a87624bc29f77004ea829809654e48f6" 414 | dependencies = [ 415 | "cc", 416 | "libc", 417 | "pkg-config", 418 | "vcpkg", 419 | ] 420 | 421 | [[package]] 422 | name = "linux-raw-sys" 423 | version = "0.4.13" 424 | source = "registry+https://github.com/rust-lang/crates.io-index" 425 | checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" 426 | 427 | [[package]] 428 | name = "log" 429 | version = "0.4.20" 430 | source = "registry+https://github.com/rust-lang/crates.io-index" 431 | checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" 432 | 433 | [[package]] 434 | name = "memchr" 435 | version = "2.7.1" 436 | source = "registry+https://github.com/rust-lang/crates.io-index" 437 | checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" 438 | 439 | [[package]] 440 | name = "once_cell" 441 | version = "1.19.0" 442 | source = "registry+https://github.com/rust-lang/crates.io-index" 443 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 444 | 445 | [[package]] 446 | name = "openssl-probe" 447 | version = "0.1.5" 448 | source = "registry+https://github.com/rust-lang/crates.io-index" 449 | checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" 450 | 451 | [[package]] 452 | name = "openssl-sys" 453 | version = "0.9.99" 454 | source = "registry+https://github.com/rust-lang/crates.io-index" 455 | checksum = "22e1bf214306098e4832460f797824c05d25aacdf896f64a985fb0fd992454ae" 456 | dependencies = [ 457 | "cc", 458 | "libc", 459 | "pkg-config", 460 | "vcpkg", 461 | ] 462 | 463 | [[package]] 464 | name = "option-ext" 465 | version = "0.2.0" 466 | source = "registry+https://github.com/rust-lang/crates.io-index" 467 | checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" 468 | 469 | [[package]] 470 | name = "percent-encoding" 471 | version = "2.3.1" 472 | source = "registry+https://github.com/rust-lang/crates.io-index" 473 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 474 | 475 | [[package]] 476 | name = "pest" 477 | version = "2.7.7" 478 | source = "registry+https://github.com/rust-lang/crates.io-index" 479 | checksum = "219c0dcc30b6a27553f9cc242972b67f75b60eb0db71f0b5462f38b058c41546" 480 | dependencies = [ 481 | "memchr", 482 | "thiserror", 483 | "ucd-trie", 484 | ] 485 | 486 | [[package]] 487 | name = "pest_derive" 488 | version = "2.7.7" 489 | source = "registry+https://github.com/rust-lang/crates.io-index" 490 | checksum = "22e1288dbd7786462961e69bfd4df7848c1e37e8b74303dbdab82c3a9cdd2809" 491 | dependencies = [ 492 | "pest", 493 | "pest_generator", 494 | ] 495 | 496 | [[package]] 497 | name = "pest_generator" 498 | version = "2.7.7" 499 | source = "registry+https://github.com/rust-lang/crates.io-index" 500 | checksum = "1381c29a877c6d34b8c176e734f35d7f7f5b3adaefe940cb4d1bb7af94678e2e" 501 | dependencies = [ 502 | "pest", 503 | "pest_meta", 504 | "proc-macro2", 505 | "quote", 506 | "syn 2.0.48", 507 | ] 508 | 509 | [[package]] 510 | name = "pest_meta" 511 | version = "2.7.7" 512 | source = "registry+https://github.com/rust-lang/crates.io-index" 513 | checksum = "d0934d6907f148c22a3acbda520c7eed243ad7487a30f51f6ce52b58b7077a8a" 514 | dependencies = [ 515 | "once_cell", 516 | "pest", 517 | "sha2", 518 | ] 519 | 520 | [[package]] 521 | name = "pkg-config" 522 | version = "0.3.29" 523 | source = "registry+https://github.com/rust-lang/crates.io-index" 524 | checksum = "2900ede94e305130c13ddd391e0ab7cbaeb783945ae07a279c268cb05109c6cb" 525 | 526 | [[package]] 527 | name = "proc-macro-error" 528 | version = "1.0.4" 529 | source = "registry+https://github.com/rust-lang/crates.io-index" 530 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 531 | dependencies = [ 532 | "proc-macro-error-attr", 533 | "proc-macro2", 534 | "quote", 535 | "syn 1.0.109", 536 | "version_check", 537 | ] 538 | 539 | [[package]] 540 | name = "proc-macro-error-attr" 541 | version = "1.0.4" 542 | source = "registry+https://github.com/rust-lang/crates.io-index" 543 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 544 | dependencies = [ 545 | "proc-macro2", 546 | "quote", 547 | "version_check", 548 | ] 549 | 550 | [[package]] 551 | name = "proc-macro2" 552 | version = "1.0.78" 553 | source = "registry+https://github.com/rust-lang/crates.io-index" 554 | checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" 555 | dependencies = [ 556 | "unicode-ident", 557 | ] 558 | 559 | [[package]] 560 | name = "quote" 561 | version = "1.0.35" 562 | source = "registry+https://github.com/rust-lang/crates.io-index" 563 | checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" 564 | dependencies = [ 565 | "proc-macro2", 566 | ] 567 | 568 | [[package]] 569 | name = "radium" 570 | version = "0.3.0" 571 | source = "registry+https://github.com/rust-lang/crates.io-index" 572 | checksum = "def50a86306165861203e7f84ecffbbdfdea79f0e51039b33de1e952358c47ac" 573 | 574 | [[package]] 575 | name = "redox_syscall" 576 | version = "0.4.1" 577 | source = "registry+https://github.com/rust-lang/crates.io-index" 578 | checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" 579 | dependencies = [ 580 | "bitflags 1.3.2", 581 | ] 582 | 583 | [[package]] 584 | name = "redox_users" 585 | version = "0.4.4" 586 | source = "registry+https://github.com/rust-lang/crates.io-index" 587 | checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" 588 | dependencies = [ 589 | "getrandom", 590 | "libredox", 591 | "thiserror", 592 | ] 593 | 594 | [[package]] 595 | name = "regex" 596 | version = "1.10.3" 597 | source = "registry+https://github.com/rust-lang/crates.io-index" 598 | checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" 599 | dependencies = [ 600 | "aho-corasick", 601 | "memchr", 602 | "regex-automata", 603 | "regex-syntax", 604 | ] 605 | 606 | [[package]] 607 | name = "regex-automata" 608 | version = "0.4.5" 609 | source = "registry+https://github.com/rust-lang/crates.io-index" 610 | checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" 611 | dependencies = [ 612 | "aho-corasick", 613 | "memchr", 614 | "regex-syntax", 615 | ] 616 | 617 | [[package]] 618 | name = "regex-syntax" 619 | version = "0.8.2" 620 | source = "registry+https://github.com/rust-lang/crates.io-index" 621 | checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" 622 | 623 | [[package]] 624 | name = "rustix" 625 | version = "0.38.31" 626 | source = "registry+https://github.com/rust-lang/crates.io-index" 627 | checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" 628 | dependencies = [ 629 | "bitflags 2.4.2", 630 | "errno", 631 | "libc", 632 | "linux-raw-sys", 633 | "windows-sys 0.52.0", 634 | ] 635 | 636 | [[package]] 637 | name = "serde" 638 | version = "1.0.196" 639 | source = "registry+https://github.com/rust-lang/crates.io-index" 640 | checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" 641 | dependencies = [ 642 | "serde_derive", 643 | ] 644 | 645 | [[package]] 646 | name = "serde_derive" 647 | version = "1.0.196" 648 | source = "registry+https://github.com/rust-lang/crates.io-index" 649 | checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" 650 | dependencies = [ 651 | "proc-macro2", 652 | "quote", 653 | "syn 2.0.48", 654 | ] 655 | 656 | [[package]] 657 | name = "sha2" 658 | version = "0.10.8" 659 | source = "registry+https://github.com/rust-lang/crates.io-index" 660 | checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" 661 | dependencies = [ 662 | "cfg-if", 663 | "cpufeatures", 664 | "digest", 665 | ] 666 | 667 | [[package]] 668 | name = "shell-words" 669 | version = "1.1.0" 670 | source = "registry+https://github.com/rust-lang/crates.io-index" 671 | checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" 672 | 673 | [[package]] 674 | name = "strsim" 675 | version = "0.8.0" 676 | source = "registry+https://github.com/rust-lang/crates.io-index" 677 | checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" 678 | 679 | [[package]] 680 | name = "structopt" 681 | version = "0.3.26" 682 | source = "registry+https://github.com/rust-lang/crates.io-index" 683 | checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10" 684 | dependencies = [ 685 | "clap", 686 | "lazy_static", 687 | "structopt-derive", 688 | ] 689 | 690 | [[package]] 691 | name = "structopt-derive" 692 | version = "0.4.18" 693 | source = "registry+https://github.com/rust-lang/crates.io-index" 694 | checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" 695 | dependencies = [ 696 | "heck", 697 | "proc-macro-error", 698 | "proc-macro2", 699 | "quote", 700 | "syn 1.0.109", 701 | ] 702 | 703 | [[package]] 704 | name = "syn" 705 | version = "1.0.109" 706 | source = "registry+https://github.com/rust-lang/crates.io-index" 707 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 708 | dependencies = [ 709 | "proc-macro2", 710 | "quote", 711 | "unicode-ident", 712 | ] 713 | 714 | [[package]] 715 | name = "syn" 716 | version = "2.0.48" 717 | source = "registry+https://github.com/rust-lang/crates.io-index" 718 | checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" 719 | dependencies = [ 720 | "proc-macro2", 721 | "quote", 722 | "unicode-ident", 723 | ] 724 | 725 | [[package]] 726 | name = "tempfile" 727 | version = "3.10.0" 728 | source = "registry+https://github.com/rust-lang/crates.io-index" 729 | checksum = "a365e8cd18e44762ef95d87f284f4b5cd04107fec2ff3052bd6a3e6069669e67" 730 | dependencies = [ 731 | "cfg-if", 732 | "fastrand", 733 | "rustix", 734 | "windows-sys 0.52.0", 735 | ] 736 | 737 | [[package]] 738 | name = "textwrap" 739 | version = "0.11.0" 740 | source = "registry+https://github.com/rust-lang/crates.io-index" 741 | checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" 742 | dependencies = [ 743 | "unicode-width", 744 | ] 745 | 746 | [[package]] 747 | name = "thiserror" 748 | version = "1.0.56" 749 | source = "registry+https://github.com/rust-lang/crates.io-index" 750 | checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" 751 | dependencies = [ 752 | "thiserror-impl", 753 | ] 754 | 755 | [[package]] 756 | name = "thiserror-impl" 757 | version = "1.0.56" 758 | source = "registry+https://github.com/rust-lang/crates.io-index" 759 | checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" 760 | dependencies = [ 761 | "proc-macro2", 762 | "quote", 763 | "syn 2.0.48", 764 | ] 765 | 766 | [[package]] 767 | name = "tinyvec" 768 | version = "1.6.0" 769 | source = "registry+https://github.com/rust-lang/crates.io-index" 770 | checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" 771 | dependencies = [ 772 | "tinyvec_macros", 773 | ] 774 | 775 | [[package]] 776 | name = "tinyvec_macros" 777 | version = "0.1.1" 778 | source = "registry+https://github.com/rust-lang/crates.io-index" 779 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 780 | 781 | [[package]] 782 | name = "typenum" 783 | version = "1.17.0" 784 | source = "registry+https://github.com/rust-lang/crates.io-index" 785 | checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" 786 | 787 | [[package]] 788 | name = "ucd-trie" 789 | version = "0.1.6" 790 | source = "registry+https://github.com/rust-lang/crates.io-index" 791 | checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" 792 | 793 | [[package]] 794 | name = "unicode-bidi" 795 | version = "0.3.15" 796 | source = "registry+https://github.com/rust-lang/crates.io-index" 797 | checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" 798 | 799 | [[package]] 800 | name = "unicode-ident" 801 | version = "1.0.12" 802 | source = "registry+https://github.com/rust-lang/crates.io-index" 803 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 804 | 805 | [[package]] 806 | name = "unicode-normalization" 807 | version = "0.1.22" 808 | source = "registry+https://github.com/rust-lang/crates.io-index" 809 | checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" 810 | dependencies = [ 811 | "tinyvec", 812 | ] 813 | 814 | [[package]] 815 | name = "unicode-segmentation" 816 | version = "1.10.1" 817 | source = "registry+https://github.com/rust-lang/crates.io-index" 818 | checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" 819 | 820 | [[package]] 821 | name = "unicode-width" 822 | version = "0.1.11" 823 | source = "registry+https://github.com/rust-lang/crates.io-index" 824 | checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" 825 | 826 | [[package]] 827 | name = "url" 828 | version = "2.5.0" 829 | source = "registry+https://github.com/rust-lang/crates.io-index" 830 | checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" 831 | dependencies = [ 832 | "form_urlencoded", 833 | "idna", 834 | "percent-encoding", 835 | ] 836 | 837 | [[package]] 838 | name = "users" 839 | version = "0.11.0" 840 | source = "registry+https://github.com/rust-lang/crates.io-index" 841 | checksum = "24cc0f6d6f267b73e5a2cadf007ba8f9bc39c6a6f9666f8cf25ea809a153b032" 842 | dependencies = [ 843 | "libc", 844 | "log", 845 | ] 846 | 847 | [[package]] 848 | name = "vcpkg" 849 | version = "0.2.15" 850 | source = "registry+https://github.com/rust-lang/crates.io-index" 851 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 852 | 853 | [[package]] 854 | name = "vec_map" 855 | version = "0.8.2" 856 | source = "registry+https://github.com/rust-lang/crates.io-index" 857 | checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" 858 | 859 | [[package]] 860 | name = "version_check" 861 | version = "0.9.4" 862 | source = "registry+https://github.com/rust-lang/crates.io-index" 863 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 864 | 865 | [[package]] 866 | name = "wasi" 867 | version = "0.11.0+wasi-snapshot-preview1" 868 | source = "registry+https://github.com/rust-lang/crates.io-index" 869 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 870 | 871 | [[package]] 872 | name = "winapi" 873 | version = "0.3.9" 874 | source = "registry+https://github.com/rust-lang/crates.io-index" 875 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 876 | dependencies = [ 877 | "winapi-i686-pc-windows-gnu", 878 | "winapi-x86_64-pc-windows-gnu", 879 | ] 880 | 881 | [[package]] 882 | name = "winapi-i686-pc-windows-gnu" 883 | version = "0.4.0" 884 | source = "registry+https://github.com/rust-lang/crates.io-index" 885 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 886 | 887 | [[package]] 888 | name = "winapi-x86_64-pc-windows-gnu" 889 | version = "0.4.0" 890 | source = "registry+https://github.com/rust-lang/crates.io-index" 891 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 892 | 893 | [[package]] 894 | name = "windows-sys" 895 | version = "0.48.0" 896 | source = "registry+https://github.com/rust-lang/crates.io-index" 897 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 898 | dependencies = [ 899 | "windows-targets 0.48.5", 900 | ] 901 | 902 | [[package]] 903 | name = "windows-sys" 904 | version = "0.52.0" 905 | source = "registry+https://github.com/rust-lang/crates.io-index" 906 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 907 | dependencies = [ 908 | "windows-targets 0.52.0", 909 | ] 910 | 911 | [[package]] 912 | name = "windows-targets" 913 | version = "0.48.5" 914 | source = "registry+https://github.com/rust-lang/crates.io-index" 915 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 916 | dependencies = [ 917 | "windows_aarch64_gnullvm 0.48.5", 918 | "windows_aarch64_msvc 0.48.5", 919 | "windows_i686_gnu 0.48.5", 920 | "windows_i686_msvc 0.48.5", 921 | "windows_x86_64_gnu 0.48.5", 922 | "windows_x86_64_gnullvm 0.48.5", 923 | "windows_x86_64_msvc 0.48.5", 924 | ] 925 | 926 | [[package]] 927 | name = "windows-targets" 928 | version = "0.52.0" 929 | source = "registry+https://github.com/rust-lang/crates.io-index" 930 | checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" 931 | dependencies = [ 932 | "windows_aarch64_gnullvm 0.52.0", 933 | "windows_aarch64_msvc 0.52.0", 934 | "windows_i686_gnu 0.52.0", 935 | "windows_i686_msvc 0.52.0", 936 | "windows_x86_64_gnu 0.52.0", 937 | "windows_x86_64_gnullvm 0.52.0", 938 | "windows_x86_64_msvc 0.52.0", 939 | ] 940 | 941 | [[package]] 942 | name = "windows_aarch64_gnullvm" 943 | version = "0.48.5" 944 | source = "registry+https://github.com/rust-lang/crates.io-index" 945 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 946 | 947 | [[package]] 948 | name = "windows_aarch64_gnullvm" 949 | version = "0.52.0" 950 | source = "registry+https://github.com/rust-lang/crates.io-index" 951 | checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" 952 | 953 | [[package]] 954 | name = "windows_aarch64_msvc" 955 | version = "0.48.5" 956 | source = "registry+https://github.com/rust-lang/crates.io-index" 957 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 958 | 959 | [[package]] 960 | name = "windows_aarch64_msvc" 961 | version = "0.52.0" 962 | source = "registry+https://github.com/rust-lang/crates.io-index" 963 | checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" 964 | 965 | [[package]] 966 | name = "windows_i686_gnu" 967 | version = "0.48.5" 968 | source = "registry+https://github.com/rust-lang/crates.io-index" 969 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 970 | 971 | [[package]] 972 | name = "windows_i686_gnu" 973 | version = "0.52.0" 974 | source = "registry+https://github.com/rust-lang/crates.io-index" 975 | checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" 976 | 977 | [[package]] 978 | name = "windows_i686_msvc" 979 | version = "0.48.5" 980 | source = "registry+https://github.com/rust-lang/crates.io-index" 981 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 982 | 983 | [[package]] 984 | name = "windows_i686_msvc" 985 | version = "0.52.0" 986 | source = "registry+https://github.com/rust-lang/crates.io-index" 987 | checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" 988 | 989 | [[package]] 990 | name = "windows_x86_64_gnu" 991 | version = "0.48.5" 992 | source = "registry+https://github.com/rust-lang/crates.io-index" 993 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 994 | 995 | [[package]] 996 | name = "windows_x86_64_gnu" 997 | version = "0.52.0" 998 | source = "registry+https://github.com/rust-lang/crates.io-index" 999 | checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" 1000 | 1001 | [[package]] 1002 | name = "windows_x86_64_gnullvm" 1003 | version = "0.48.5" 1004 | source = "registry+https://github.com/rust-lang/crates.io-index" 1005 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 1006 | 1007 | [[package]] 1008 | name = "windows_x86_64_gnullvm" 1009 | version = "0.52.0" 1010 | source = "registry+https://github.com/rust-lang/crates.io-index" 1011 | checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" 1012 | 1013 | [[package]] 1014 | name = "windows_x86_64_msvc" 1015 | version = "0.48.5" 1016 | source = "registry+https://github.com/rust-lang/crates.io-index" 1017 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 1018 | 1019 | [[package]] 1020 | name = "windows_x86_64_msvc" 1021 | version = "0.52.0" 1022 | source = "registry+https://github.com/rust-lang/crates.io-index" 1023 | checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" 1024 | 1025 | [[package]] 1026 | name = "zeroize" 1027 | version = "1.7.0" 1028 | source = "registry+https://github.com/rust-lang/crates.io-index" 1029 | checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" 1030 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "git-tools" 3 | version = "0.1.3" 4 | authors = ["Cecile Tonglet "] 5 | edition = "2018" 6 | license = "MIT" 7 | description = "Git subcommands to help with your workflow." 8 | repository = "https://github.com/cecton/git-tools" 9 | homepage = "https://github.com/cecton/git-tools" 10 | readme = "README.md" 11 | keywords = ["git", "workflow", "merge", "rebase", "try-merge"] 12 | categories = ["command-line-utilities"] 13 | 14 | [[bin]] 15 | name = "git-try-merge" 16 | path = "src/git-try-merge.rs" 17 | doc = false 18 | 19 | [[bin]] 20 | name = "git-fork" 21 | path = "src/git-fork.rs" 22 | doc = false 23 | 24 | [[bin]] 25 | name = "git-push2" 26 | path = "src/git-push2.rs" 27 | doc = false 28 | 29 | [[bin]] 30 | name = "git-delete" 31 | path = "src/git-delete.rs" 32 | doc = false 33 | 34 | [dependencies] 35 | anyhow = "1" 36 | git2 = "0.18.1" 37 | dirs = "3.0.1" 38 | users = "0.11.0" 39 | git2_credentials = "0.13.0" 40 | bitvec = "0.17.4" 41 | structopt = "0.3.17" 42 | globset = "0.4.6" 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Cecile Tonglet 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Rust](https://github.com/cecton/git-tools/workflows/Rust/badge.svg) 2 | [![Latest Version](https://img.shields.io/crates/v/git-tools.svg)](https://crates.io/crates/git-tools) 3 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](http://opensource.org/licenses/MIT) 4 | [![Docs.rs](https://docs.rs/git-tools/badge.svg)](https://docs.rs/git-tools) 5 | 6 | ##### Table of Contents 7 | 8 | * [`git delete`](#git-delete) 9 | 10 | Delete a local branch and its upstream branch altogether. 11 | 12 | * [`git fork`](#git-fork) 13 | 14 | Create a new branch based on the default branch (usually `origin/main`). 15 | 16 | * [`git push2`](#git-push2) 17 | 18 | Push a branch and set the upstream if not already set. 19 | 20 | * [`git try-merge`](#git-try-merge) 21 | 22 | Does like a `git merge origin/main` but helps you resolve the conflicting 23 | commits one by one instead than having to solve them altogether like 24 | `git merge`. 25 | 26 | git-try-merge 27 | ============= 28 | 29 | Does like a `git merge origin/main` but helps you resolve the conflicting 30 | commits one by one instead than having to solve them altogether like 31 | `git merge`. 32 | 33 | Synopsis 34 | -------- 35 | 36 | ```bash 37 | git try-merge 38 | # 1. Merge as many non-conflicting commits as possible under one merge commit 39 | # (if any) 40 | # 2. Merge the first conflicting commit alone 41 | # (if any) 42 | # 43 | # Then you need to repeat the command `git try-merge` until your branch is 44 | # fully updated. 45 | # 46 | # The point: all the conflicting commits will be merged one-by-one which will 47 | # allow you to fully understand the reason of the conflict and solve them 48 | # separately. (A bit like `git rebase` would do.) 49 | ``` 50 | 51 | There is no real equivalent purely with Git's CLI. This is the closest: 52 | 53 | ```bash 54 | git fetch 55 | git merge origin/main 56 | # Then you will solve all the conflicts of all the commits in one commit, 57 | # no matter how many commits are conflicting. 58 | ``` 59 | 60 | Installation 61 | ------------ 62 | 63 | ```bash 64 | cargo install git-tools --bin git-try-merge 65 | ``` 66 | 67 | git-fork 68 | ======== 69 | 70 | Create a new branch based on the default branch (usually `origin/main`). 71 | 72 | Synopsis 73 | -------- 74 | 75 | ```bash 76 | git fork new-branch 77 | 78 | # This command will: 79 | # - make sure there is no uncommitted changes (clean state) 80 | # - fetch (update) origin/main (or your default branch) 81 | # - create a new branch "new-branch" that will be based on origin/main 82 | # - checkout on this new branch 83 | ``` 84 | 85 | More or less equivalent to: 86 | 87 | ```bash 88 | git checkout main 89 | git pull --ff-only 90 | git checkout -b new-branch 91 | ``` 92 | 93 | ### Implementation notes 94 | 95 | The local branch main will not be updated. In fact, you don't even 96 | need a local branch main. The exact equivalent with Git would be more 97 | something like this: 98 | 99 | ```bash 100 | git fetch origin main 101 | # 102 | git branch -f new-branch origin/main 103 | git checkout new-branch 104 | ``` 105 | 106 | Installation 107 | ------------ 108 | 109 | ```bash 110 | cargo install git-tools --bin git-fork 111 | ``` 112 | 113 | git-push2 114 | ========= 115 | 116 | Push a branch and set the upstream if not already set. 117 | 118 | Synopsis 119 | -------- 120 | 121 | ```bash 122 | git push2 123 | ``` 124 | 125 | This is the equivalent of: 126 | 127 | ```bash 128 | git push 129 | # if it fails: 130 | git push --set-upstream origin new-branch 131 | ``` 132 | 133 | Installation 134 | ------------ 135 | 136 | ```bash 137 | cargo install git-tools --bin git-push2 138 | ``` 139 | 140 | git-delete 141 | ========== 142 | 143 | Delete a local branch and its upstream branch altogether. 144 | 145 | Synopsis 146 | -------- 147 | 148 | ```bash 149 | git delete new-branch 150 | ``` 151 | 152 | This is the equivalent of: 153 | 154 | ``` 155 | git branch -d new-branch 156 | git push origin :new-branch 157 | ``` 158 | 159 | Installation 160 | ------------ 161 | 162 | ```bash 163 | cargo install git-tools --bin git-delete 164 | ``` 165 | -------------------------------------------------------------------------------- /src/common.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use globset::GlobSet; 4 | use std::env::{current_dir, set_current_dir}; 5 | use std::path::{Path, PathBuf}; 6 | 7 | use git2::{ 8 | Branch, BranchType, Commit, Config, Cred, CredentialType, Error, ErrorCode, FetchOptions, 9 | MergeOptions, PushOptions, RemoteCallbacks, Sort, StatusOptions, 10 | }; 11 | pub use git2::{Oid, Repository}; 12 | 13 | pub struct Git { 14 | pub repo: Repository, 15 | pub head_message: String, 16 | pub head_hash: String, 17 | pub branch_name: Option, 18 | pub upstream: Option, 19 | pub config: Config, 20 | } 21 | 22 | impl Git { 23 | pub fn open() -> Result { 24 | if let Some(path) = find_git_repository()? { 25 | set_current_dir(path).map_err(|e| Error::from_str(&e.to_string()))?; 26 | } 27 | 28 | let repo = Repository::open(".")?; 29 | let head_message; 30 | let head_hash; 31 | let branch_name; 32 | let upstream; 33 | 34 | { 35 | let (object, maybe_ref) = repo.revparse_ext("HEAD")?; 36 | let commit = object.as_commit().unwrap(); 37 | head_message = commit.message().unwrap().to_string(); 38 | head_hash = format!("{}", object.id()); 39 | branch_name = maybe_ref.and_then(|x| { 40 | x.shorthand() 41 | .filter(|&x| x != "HEAD") 42 | .map(|x| x.to_string()) 43 | }); 44 | upstream = if let Some(name) = branch_name.as_ref() { 45 | if let Ok(remote_branch) = repo 46 | .find_branch(name, BranchType::Local) 47 | .and_then(|x| x.upstream()) 48 | { 49 | remote_branch.name()?.map(|x| x.to_string()) 50 | } else { 51 | None 52 | } 53 | } else { 54 | None 55 | }; 56 | } 57 | 58 | let config = repo.config()?.snapshot()?; 59 | 60 | Ok(Git { 61 | repo, 62 | head_message, 63 | head_hash, 64 | branch_name, 65 | upstream, 66 | config, 67 | }) 68 | } 69 | 70 | pub fn get_staged_and_unstaged_files(&self) -> Result, Error> { 71 | let mut files = Vec::new(); 72 | let mut options = StatusOptions::new(); 73 | options.include_untracked(true); 74 | options.include_ignored(false); 75 | 76 | for entry in self.repo.statuses(Some(&mut options))?.iter() { 77 | files.push(entry.path().unwrap().to_string()); 78 | } 79 | 80 | Ok(files) 81 | } 82 | 83 | pub fn branch(&self, name: &str, from: Option<&str>) -> Result { 84 | let object = self.repo.revparse_single(from.unwrap_or("HEAD"))?; 85 | let commit = object.as_commit().unwrap(); 86 | let branch = self.repo.branch(name, &commit, false)?; 87 | 88 | Ok(branch.get().name().unwrap().to_string()) 89 | } 90 | 91 | pub fn get_branch_hash(&self, branch_name: &str) -> Result, Error> { 92 | if let (_, Some(reference)) = self.repo.revparse_ext(branch_name)? { 93 | Ok(Some(format!("{}", reference.target().unwrap()))) 94 | } else { 95 | Ok(None) 96 | } 97 | } 98 | 99 | pub fn get_default_branch(&self, remote: &str) -> Result { 100 | let reference = match self 101 | .repo 102 | .find_reference(format!("refs/remotes/{}/HEAD", remote).as_str()) 103 | { 104 | Ok(x) => x, 105 | Err(err) if err.code() == ErrorCode::NotFound => { 106 | return Ok(format!("{}/master", remote)) 107 | } 108 | Err(err) => return Err(err), 109 | }; 110 | 111 | Ok(reference 112 | .symbolic_target() 113 | .expect("reference HEAD is not symbolic") 114 | .strip_prefix("refs/remotes/") 115 | .expect("invalid target") 116 | .to_string()) 117 | } 118 | 119 | pub fn switch_branch(&mut self, branch_name: &str) -> Result<(), Error> { 120 | let branch = self.repo.find_branch(branch_name, BranchType::Local)?; 121 | let object = self.repo.revparse_single(branch_name)?; 122 | 123 | self.repo.checkout_tree(&object, None)?; 124 | self.repo.set_head(branch.get().name().unwrap())?; 125 | 126 | self.branch_name = Some(branch_name.to_string()); 127 | self.head_hash = format!("{}", object.id()); 128 | if let Ok(upstream) = branch.upstream() { 129 | self.upstream = upstream.name()?.map(|x| x.to_string()); 130 | } 131 | 132 | Ok(()) 133 | } 134 | 135 | pub fn commit_files(&mut self, message: &str, files: &[&str]) -> Result { 136 | let object = self.repo.revparse_single("HEAD")?; 137 | let commit = object.as_commit().unwrap(); 138 | let old_tree = commit.tree()?; 139 | 140 | let mut treebuilder = self.repo.treebuilder(Some(&old_tree))?; 141 | for file in files { 142 | let oid = self.repo.blob_path(Path::new(file))?; 143 | treebuilder.insert(file, oid, 0o100644)?; 144 | } 145 | let tree_oid = treebuilder.write()?; 146 | let tree = self.repo.find_tree(tree_oid)?; 147 | 148 | let signature = self.repo.signature()?; 149 | let oid = self.repo.commit( 150 | Some("HEAD"), 151 | &signature, 152 | &signature, 153 | message, 154 | &tree, 155 | &[&commit], 156 | )?; 157 | 158 | let mut index = self.repo.index()?; 159 | index.update_all(files, None)?; 160 | self.repo.checkout_index(Some(&mut index), None)?; 161 | 162 | self.head_hash = format!("{}", oid); 163 | 164 | Ok(oid) 165 | } 166 | 167 | pub fn has_file_changes(&self) -> Result { 168 | let tree = self.repo.head()?.peel_to_tree()?; 169 | 170 | Ok(self 171 | .repo 172 | .diff_tree_to_workdir_with_index(Some(&tree), None)? 173 | .stats()? 174 | .files_changed() 175 | > 0) 176 | } 177 | 178 | pub fn check_no_conflict(&mut self, branch_name: &str) -> Result, Error> { 179 | let mut cargo_lock_conflict = false; 180 | let our_object = self.repo.revparse_single("HEAD")?; 181 | let our = our_object.as_commit().expect("our is a commit"); 182 | let their_object = self.repo.revparse_single(branch_name)?; 183 | let their = their_object.as_commit().expect("their is a commit"); 184 | 185 | let mut options = MergeOptions::new(); 186 | options.fail_on_conflict(false); 187 | 188 | let index = self.repo.merge_commits(&our, &their, Some(&options))?; 189 | let conflicts = index.conflicts()?.collect::, _>>()?; 190 | for conflict in conflicts { 191 | let their = conflict.their.expect("an index entry for their exist"); 192 | let path = std::str::from_utf8(their.path.as_slice()).expect("valid UTF-8"); 193 | 194 | if path == "Cargo.lock" { 195 | cargo_lock_conflict = true; 196 | } else { 197 | return Ok(None); 198 | } 199 | } 200 | 201 | Ok(Some(cargo_lock_conflict)) 202 | } 203 | 204 | pub fn merge_no_conflict( 205 | &mut self, 206 | branch_name: &str, 207 | message: &str, 208 | ignore_conflict_globs: &GlobSet, 209 | ) -> Result)>, Error> { 210 | let our_object = self.repo.revparse_single("HEAD")?; 211 | let our = our_object.as_commit().expect("our is a commit"); 212 | let their_object = self.repo.revparse_single(branch_name)?; 213 | let their = their_object.as_commit().expect("their is a commit"); 214 | 215 | let mut options = MergeOptions::new(); 216 | options.fail_on_conflict(false); 217 | 218 | let mut index = self.repo.merge_commits(&our, &their, Some(&mut options))?; 219 | let conflicts = index.conflicts()?.collect::, _>>()?; 220 | let mut ignored_conflicts = Vec::new(); 221 | for conflict in conflicts { 222 | let their = match conflict.their { 223 | Some(x) => x, 224 | None => return Ok(None), 225 | }; 226 | 227 | let path = std::str::from_utf8(their.path.as_slice()).expect("valid UTF-8"); 228 | 229 | if ignore_conflict_globs.matches(path).is_empty() { 230 | return Ok(None); 231 | } else { 232 | use bitvec::prelude::*; 233 | 234 | let path = path.to_owned(); 235 | ignored_conflicts.push(path.clone()); 236 | 237 | let mut flags = BitVec::::from_element(their.flags); 238 | // NOTE: Reset stage flags 239 | // https://github.com/git/git/blob/master/Documentation/technical/index-format.txt 240 | flags[2..=3].set_all(false); 241 | let their = git2::IndexEntry { 242 | flags: flags.as_slice()[0], 243 | ..their 244 | }; 245 | index.remove_path(Path::new(&path)).unwrap(); 246 | index.add(&their)?; 247 | } 248 | } 249 | 250 | let oid = index.write_tree_to(&self.repo)?; 251 | let tree = self.repo.find_tree(oid)?; 252 | 253 | let signature = self.repo.signature()?; 254 | let oid = self.repo.commit( 255 | Some("HEAD"), 256 | &signature, 257 | &signature, 258 | message, 259 | &tree, 260 | &[&our, &their], 261 | )?; 262 | 263 | let mut checkout_builder = git2::build::CheckoutBuilder::new(); 264 | checkout_builder.force(); 265 | self.repo.checkout_head(Some(&mut checkout_builder))?; 266 | 267 | self.head_hash = format!("{}", oid); 268 | 269 | Ok(Some((self.head_hash.clone(), ignored_conflicts))) 270 | } 271 | 272 | pub fn rev_list(&self, from: &str, to: &str, reversed: bool) -> Result, Error> { 273 | let mut revwalk = self.repo.revwalk()?; 274 | if reversed { 275 | revwalk.set_sorting(Sort::TOPOLOGICAL | Sort::REVERSE); 276 | } else { 277 | revwalk.set_sorting(Sort::TOPOLOGICAL); 278 | } 279 | 280 | let from_object = self.repo.revparse_single(from)?; 281 | let to_object = self.repo.revparse_single(to)?; 282 | revwalk.hide(from_object.id())?; 283 | revwalk.push(to_object.id())?; 284 | 285 | revwalk 286 | .map(|x| x.map(|x| format!("{}", x))) 287 | .collect::, Error>>() 288 | } 289 | 290 | pub fn update_upstream(&self, branch_name: &str) -> Result<(), Error> { 291 | let branch = self.repo.find_branch(branch_name, BranchType::Remote)?; 292 | let (maybe_remote_name, branch_name) = get_remote_and_branch(&branch); 293 | 294 | // TODO: this method fails if branch_name is not a remote branch 295 | // this `if` statement makes no sense 296 | if let Some(remote_name) = maybe_remote_name { 297 | let mut remote_callbacks = RemoteCallbacks::new(); 298 | let mut handler = CredentialHandler::new(); 299 | remote_callbacks.credentials(move |x, y, z| handler.credentials_callback(x, y, z)); 300 | 301 | let mut fetch_options = FetchOptions::new(); 302 | fetch_options.remote_callbacks(remote_callbacks); 303 | 304 | self.repo.find_remote(remote_name)?.fetch( 305 | &[branch_name], 306 | Some(&mut fetch_options), 307 | None, 308 | )?; 309 | } 310 | 311 | Ok(()) 312 | } 313 | 314 | pub fn ancestors(&self, rev: &str) -> Result { 315 | let object = self.repo.revparse_single(rev)?; 316 | let commit = object.peel_to_commit()?; 317 | 318 | Ok(Ancestors { 319 | current: Some(commit), 320 | }) 321 | } 322 | 323 | pub fn squash( 324 | &mut self, 325 | parent_0: &str, 326 | parent_1: &str, 327 | message: &str, 328 | ) -> Result { 329 | let parent_0 = self.repo.revparse_single(parent_0)?.peel_to_commit()?; 330 | let parent_1 = self.repo.revparse_single(parent_1)?.peel_to_commit()?; 331 | let head = self.repo.revparse_single("HEAD")?.peel_to_commit()?; 332 | let tree = self.repo.find_tree(head.tree_id())?; 333 | 334 | // git reset --soft to the parent "0" commit 335 | if let (_, Some(mut reference)) = self 336 | .repo 337 | .revparse_ext(self.branch_name.as_ref().unwrap_or(&self.head_hash))? 338 | { 339 | reference.set_target(parent_0.id(), message)?; 340 | } else { 341 | self.repo.set_head_detached(parent_0.id())?; 342 | } 343 | 344 | // Make a commit with the current tree 345 | let signature = self.repo.signature()?; 346 | let oid = self.repo.commit( 347 | Some("HEAD"), 348 | &signature, 349 | &signature, 350 | message, 351 | &tree, 352 | &[&parent_0, &parent_1], 353 | )?; 354 | 355 | self.head_hash = format!("{}", oid); 356 | 357 | Ok(self.head_hash.clone()) 358 | } 359 | } 360 | 361 | fn find_git_repository() -> Result, Error> { 362 | let mut path = current_dir().map_err(|e| Error::from_str(&e.to_string()))?; 363 | 364 | loop { 365 | if path.join(".git").exists() { 366 | return Ok(Some(path)); 367 | } 368 | if !path.pop() { 369 | break; 370 | } 371 | } 372 | 373 | Ok(None) 374 | } 375 | 376 | fn get_remote_and_branch<'a>(branch: &'a Branch) -> (Option<&'a str>, &'a str) { 377 | let mut parts = branch 378 | .get() 379 | .shorthand() 380 | .expect("valid UTF-8") 381 | .rsplitn(2, '/'); 382 | let branch_name = parts.next().unwrap(); 383 | let maybe_remote_name = parts.next(); 384 | 385 | (maybe_remote_name, branch_name) 386 | } 387 | 388 | pub struct CredentialHandler { 389 | second_handler: git2_credentials::CredentialHandler, 390 | first_attempt_failed: bool, 391 | } 392 | 393 | impl CredentialHandler { 394 | pub fn new() -> CredentialHandler { 395 | let git_config = git2::Config::open_default().unwrap(); 396 | let second_handler = git2_credentials::CredentialHandler::new(git_config); 397 | 398 | CredentialHandler { 399 | second_handler, 400 | first_attempt_failed: false, 401 | } 402 | } 403 | 404 | pub fn credentials_callback( 405 | &mut self, 406 | url: &str, 407 | username_from_url: Option<&str>, 408 | allowed_types: CredentialType, 409 | ) -> Result { 410 | if !self.first_attempt_failed && allowed_types.contains(CredentialType::SSH_KEY) { 411 | self.first_attempt_failed = true; 412 | let user = users::get_current_username().expect("could not get username"); 413 | let home_dir = dirs::home_dir().expect("could not get home directory"); 414 | 415 | Cred::ssh_key( 416 | username_from_url.unwrap_or_else(|| user.to_str().unwrap()), 417 | Some(&home_dir.join(".ssh/id_rsa.pub")), 418 | &home_dir.join(".ssh/id_rsa"), 419 | None, 420 | ) 421 | } else { 422 | self.second_handler 423 | .try_next_credential(url, username_from_url, allowed_types) 424 | } 425 | } 426 | } 427 | 428 | pub struct Ancestors<'a> { 429 | current: Option>, 430 | } 431 | 432 | impl<'a> Iterator for Ancestors<'a> { 433 | type Item = Commit<'a>; 434 | 435 | fn next(&mut self) -> Option { 436 | self.current.take().map(|this| { 437 | self.current = this.parent(0).ok(); 438 | this 439 | }) 440 | } 441 | } 442 | -------------------------------------------------------------------------------- /src/git-delete.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | 3 | use anyhow::{bail, Context, Result}; 4 | use std::env; 5 | use std::io::Write; 6 | use structopt::StructOpt; 7 | 8 | #[derive(StructOpt, Debug)] 9 | #[structopt( 10 | bin_name = "git delete", 11 | about = env!("CARGO_PKG_DESCRIPTION") 12 | )] 13 | pub struct Delete { 14 | branch_name: String, 15 | } 16 | 17 | fn main() { 18 | let exit_status = execute(); 19 | std::process::exit(exit_status); 20 | } 21 | 22 | const SUCCESS: i32 = 0; 23 | const FAILURE: i32 = 1; 24 | 25 | fn execute() -> i32 { 26 | let opts = Delete::from_args(); 27 | 28 | if let Err(err) = run(opts) { 29 | eprintln!("{}", err); 30 | 31 | FAILURE 32 | } else { 33 | SUCCESS 34 | } 35 | } 36 | 37 | pub fn run(params: Delete) -> Result<()> { 38 | let branch_name = params.branch_name.as_str(); 39 | let repo = git2::Repository::open(".").context("Could not open repository")?; 40 | 41 | let mut branch = repo 42 | .find_branch(¶ms.branch_name, git2::BranchType::Local) 43 | .with_context(|| format!("Could not find local branch: {}", params.branch_name))?; 44 | let branch_name = branch 45 | .name() 46 | .context("Could not retrieve branch name")? 47 | .expect("not valid utf-8") 48 | .to_owned(); 49 | 50 | if branch.is_head() { 51 | bail!("Aborted: cannot delete branch currently pointed at by HEAD"); 52 | } 53 | 54 | // delete remote branch if any 55 | if let Ok(upstream) = branch.upstream() { 56 | let upstream_name = upstream.get().name().expect("not valid utf-8"); 57 | let remote_name = upstream_name 58 | .strip_suffix(&branch_name) 59 | .and_then(|x| x.strip_prefix("refs/remotes/")) 60 | .context("Could not find remote name")? 61 | .trim_end_matches('/'); 62 | 63 | let mut remote = repo 64 | .find_remote(remote_name) 65 | .with_context(|| format!("Could not find remote `{}`", remote_name))?; 66 | 67 | // this is a reference to the default branch if it exists 68 | let head_reference = repo.find_reference(&format!("refs/remotes/{}/HEAD", remote_name)); 69 | 70 | let default_branch_name = { 71 | match head_reference.as_ref() { 72 | Ok(reference) => reference 73 | .symbolic_target() 74 | .context("Invalid reference HEAD: not symbolic reference")? 75 | .to_string(), 76 | Err(err) if err.code() == git2::ErrorCode::NotFound => { 77 | format!("refs/remote/{}/master", remote_name) 78 | } 79 | Err(err) => bail!("Could not find default branch for this repository: {}", err), 80 | } 81 | }; 82 | 83 | if upstream_name == default_branch_name { 84 | bail!("Aborted: deleting default branch is forbidden"); 85 | } 86 | 87 | // TODO better handling for credentials using git2_credentials 88 | // make sure it works with ~/.ssh/id_rsa and ssh-agent 89 | let mut remote_callbacks = git2::RemoteCallbacks::new(); 90 | let mut handler = common::CredentialHandler::new(); 91 | remote_callbacks.credentials(move |x, y, z| handler.credentials_callback(x, y, z)); 92 | 93 | let mut push_options = git2::PushOptions::new(); 94 | push_options.remote_callbacks(remote_callbacks); 95 | 96 | remote.push( 97 | &[&format!("+:refs/heads/{}", branch_name)], 98 | Some(&mut push_options), 99 | )?; 100 | println!("Upstream deleted: {}", upstream_name); 101 | } 102 | 103 | branch.delete(); 104 | println!("Local branch deleted: {}", branch_name); 105 | 106 | Ok(()) 107 | } 108 | -------------------------------------------------------------------------------- /src/git-fork.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | 3 | use common::Git; 4 | 5 | use std::env; 6 | use std::io::Write; 7 | use structopt::StructOpt; 8 | 9 | #[derive(StructOpt, Debug)] 10 | #[structopt( 11 | bin_name = "git fork", 12 | about = env!("CARGO_PKG_DESCRIPTION") 13 | )] 14 | pub struct Fork { 15 | branch_name: String, 16 | from: Option, 17 | } 18 | 19 | fn main() { 20 | let exit_status = execute(); 21 | std::io::stdout().flush().unwrap(); 22 | std::process::exit(exit_status); 23 | } 24 | 25 | const SUCCESS: i32 = 0; 26 | const FAILURE: i32 = 1; 27 | 28 | fn execute() -> i32 { 29 | let opts = Fork::from_args(); 30 | 31 | if let Err(err) = run(opts) { 32 | eprintln!("{}", err); 33 | 34 | FAILURE 35 | } else { 36 | SUCCESS 37 | } 38 | } 39 | 40 | pub fn run(params: Fork) -> Result<(), Box> { 41 | let mut git = Git::open()?; 42 | 43 | if git.has_file_changes()? { 44 | return Err("The repository has not committed changes, aborting.".into()); 45 | } 46 | 47 | let branch_name = params.branch_name.as_str(); 48 | let default_branch = git.get_default_branch("origin")?; 49 | let name = params 50 | .from 51 | .as_deref() 52 | .unwrap_or_else(|| default_branch.as_str()); 53 | 54 | if name.contains('/') { 55 | git.update_upstream(name)?; 56 | } 57 | 58 | match git.get_branch_hash(name)? { 59 | // name is really a branch 60 | Some(hash) => git.branch(branch_name, Some(hash.as_str()))?, 61 | // name was not a branch 62 | None => git.branch(branch_name, Some(name))?, 63 | }; 64 | 65 | git.switch_branch(branch_name)?; 66 | 67 | println!("Branch {} created.", branch_name); 68 | 69 | Ok(()) 70 | } 71 | -------------------------------------------------------------------------------- /src/git-push2.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | 3 | use common::Git; 4 | 5 | use std::env; 6 | use std::io::Write; 7 | use std::os::unix::process::CommandExt; 8 | use std::process::Command; 9 | use structopt::{clap::AppSettings, StructOpt}; 10 | 11 | #[derive(StructOpt, Debug)] 12 | #[structopt( 13 | bin_name = "git push2", 14 | about = env!("CARGO_PKG_DESCRIPTION"), 15 | settings = &[AppSettings::TrailingVarArg, AppSettings::AllowLeadingHyphen], 16 | )] 17 | pub struct Params { 18 | args: Vec, 19 | } 20 | 21 | fn main() { 22 | let exit_status = execute(); 23 | std::io::stdout().flush().unwrap(); 24 | std::process::exit(exit_status); 25 | } 26 | 27 | const SUCCESS: i32 = 0; 28 | const FAILURE: i32 = 1; 29 | 30 | fn execute() -> i32 { 31 | let opts = Params::from_args(); 32 | 33 | if let Err(err) = run(opts) { 34 | eprintln!("{}", err); 35 | 36 | FAILURE 37 | } else { 38 | SUCCESS 39 | } 40 | } 41 | 42 | pub fn run(params: Params) -> Result<(), Box> { 43 | let git = Git::open()?; 44 | 45 | Err(match (git.branch_name.as_ref(), git.upstream.as_ref()) { 46 | (Some(name), None) => Command::new("git") 47 | .arg("push") 48 | .args(&["--set-upstream", "origin", name]) 49 | .args(params.args) 50 | .exec() 51 | .into(), 52 | _ => Command::new("git") 53 | .arg("push") 54 | .args(params.args) 55 | .exec() 56 | .into(), 57 | }) 58 | } 59 | -------------------------------------------------------------------------------- /src/git-try-merge.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | 3 | use common::Git; 4 | 5 | use globset::{Glob, GlobSetBuilder}; 6 | use std::collections::HashSet; 7 | use std::io::Write; 8 | use std::os::unix::process::CommandExt; 9 | use std::process::Command; 10 | use structopt::{clap::AppSettings, StructOpt}; 11 | 12 | #[derive(StructOpt, Debug)] 13 | #[structopt( 14 | bin_name = "git try-merge", 15 | about = env!("CARGO_PKG_DESCRIPTION"), 16 | settings = &[AppSettings::TrailingVarArg, AppSettings::AllowLeadingHyphen], 17 | )] 18 | pub struct TryMerge { 19 | /// Squash all the merge commits together at the end. 20 | /// 21 | /// You can make this behavior the default using the following command: 22 | /// 23 | /// git config --global try-merge.squash true 24 | /// 25 | /// Or for this repository only: 26 | /// 27 | /// git config try-merge.squash true 28 | #[structopt(long)] 29 | squash: bool, 30 | 31 | // NOTE: the long and short name for the parameters must not conflict with `git merge` 32 | /// Do not run `git merge` at the end. (Merge to the latest commit possible without conflict.) 33 | #[structopt(long, short = "u")] 34 | no_merge: bool, 35 | 36 | /// Revision for the update (default branch or origin/main by default). 37 | revision: Option, 38 | 39 | merge_args: Vec, 40 | } 41 | 42 | fn main() { 43 | let exit_status = execute(); 44 | std::io::stdout().flush().unwrap(); 45 | std::process::exit(exit_status); 46 | } 47 | 48 | const SUCCESS: i32 = 0; 49 | const FAILURE: i32 = 1; 50 | 51 | fn execute() -> i32 { 52 | let opts = TryMerge::from_args(); 53 | 54 | if let Err(err) = run(opts) { 55 | eprintln!("{}", err); 56 | 57 | FAILURE 58 | } else { 59 | SUCCESS 60 | } 61 | } 62 | 63 | pub fn run(params: TryMerge) -> Result<(), Box> { 64 | let git = Git::open()?; 65 | 66 | update_branch(git, params) 67 | } 68 | 69 | fn update_branch(mut git: Git, params: TryMerge) -> Result<(), Box> { 70 | let default_branch = git.get_default_branch("origin")?; 71 | let top_rev = params.revision.clone().unwrap_or_else(|| default_branch); 72 | 73 | if top_rev.contains('/') { 74 | git.update_upstream(top_rev.as_str())?; 75 | } 76 | 77 | if git.has_file_changes()? { 78 | return Err("The repository has not committed changes, aborting.".into()); 79 | } 80 | 81 | let mut rev_list = git.rev_list("HEAD", top_rev.as_str(), true)?; 82 | 83 | if rev_list.is_empty() { 84 | let default_squash = git.config.get_bool("try-merge.squash").ok(); 85 | if params.squash || default_squash.unwrap_or_default() { 86 | let commit = squash_all_merge_commits(&mut git, &top_rev)?; 87 | if commit.is_some() { 88 | println!("Your merge commits have been squashed."); 89 | return Ok(()); 90 | } 91 | } 92 | println!("Your branch is already up-to-date."); 93 | return Ok(()); 94 | } 95 | 96 | let ignore_conflict_set = { 97 | let mut builder = GlobSetBuilder::new(); 98 | let mut entries = git.config.multivar("try-merge.ignore-conflict", None)?; 99 | while let Some(entry) = entries.next().transpose()? { 100 | builder.add(Glob::new(entry.value().expect("invalid UTF-8"))?); 101 | } 102 | builder.build()? 103 | }; 104 | 105 | let mut skipped = 0; 106 | let mut last_failing_revision: Option = None; 107 | let mut all_ignored_conflicts = HashSet::new(); 108 | while let Some(revision) = rev_list.pop() { 109 | let message = format!("Merge commit {} (no conflict)\n\n", revision,); 110 | 111 | if let Some((_, ignored_conflicts)) = 112 | git.merge_no_conflict(revision.as_str(), message.as_str(), &ignore_conflict_set)? 113 | { 114 | println!( 115 | "All the commits to {} have been merged successfully without conflict", 116 | revision 117 | ); 118 | all_ignored_conflicts.extend(ignored_conflicts); 119 | 120 | break; 121 | } else { 122 | skipped += 1; 123 | last_failing_revision = Some(revision.clone()); 124 | } 125 | } 126 | 127 | if !all_ignored_conflicts.is_empty() { 128 | println!("The following files had conflicts but have been ignored:"); 129 | for file_path in all_ignored_conflicts { 130 | println!("{}", file_path); 131 | } 132 | } 133 | 134 | if params.no_merge { 135 | return Ok(()); 136 | } else if let Some(revision) = last_failing_revision { 137 | println!( 138 | "Your current branch is still behind '{}' by {} commit(s).", 139 | top_rev, skipped 140 | ); 141 | println!("First merge conflict detected on: {}", revision); 142 | 143 | let message = format!("Merge commit {} (conflicts)\n\n", revision,); 144 | 145 | return Err(Command::new("git") 146 | .args(&[ 147 | "merge", 148 | "--no-ff", 149 | revision.as_str(), 150 | "-m", 151 | message.as_str(), 152 | ]) 153 | .args(params.merge_args) 154 | .exec() 155 | .into()); 156 | } else { 157 | println!("Nothing more to merge. Your branch is up-to-date."); 158 | } 159 | 160 | Ok(()) 161 | } 162 | 163 | fn squash_all_merge_commits( 164 | git: &mut Git, 165 | top_rev: &str, 166 | ) -> Result, Box> { 167 | let merge_commits = git.ancestors("HEAD")?.take_while(|commit| { 168 | commit 169 | .message() 170 | .map(|msg| msg.starts_with("Merge commit")) 171 | .unwrap_or_default() 172 | }); 173 | if let Some(ancestor) = merge_commits 174 | // NOTE: we need to have more than 1 commit to make a squash 175 | .skip(1) 176 | .last() 177 | .map(|x| format!("{}", x.parent(0).unwrap().id())) 178 | { 179 | Ok(Some(git.squash( 180 | &ancestor, 181 | top_rev, 182 | &format!("Merge branch {}", top_rev), 183 | )?)) 184 | } else { 185 | Ok(None) 186 | } 187 | } 188 | --------------------------------------------------------------------------------