├── .github └── workflows │ └── rust.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE.txt ├── LICENSE-MIT.txt ├── README.md ├── src ├── args.rs ├── ci.rs ├── ci │ ├── github.rs │ └── gitlab.rs ├── config.rs └── main.rs └── tests ├── cli.rs ├── github_failing_integration_test.yml ├── github_parse_check.yml ├── github_parse_check_on_push_to_branch.yml ├── github_passing_integration_test.yml ├── gitlab_parse_check.yml └── gitlab_passing_integration_test.yml /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v1 12 | - name: Build 13 | run: cargo build 14 | - name: Run tests 15 | run: cargo test 16 | - name: Clippy 17 | run: cargo clippy --all-targets --all-features -- -D warnings 18 | - name: Format 19 | run: cargo fmt --all -- --check 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "aho-corasick" 5 | version = "0.7.9" 6 | source = "registry+https://github.com/rust-lang/crates.io-index" 7 | checksum = "d5e63fd144e18ba274ae7095c0197a870a7b9468abc801dd62f190d80817d2ec" 8 | dependencies = [ 9 | "memchr", 10 | ] 11 | 12 | [[package]] 13 | name = "ansi_term" 14 | version = "0.11.0" 15 | source = "registry+https://github.com/rust-lang/crates.io-index" 16 | checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" 17 | dependencies = [ 18 | "winapi", 19 | ] 20 | 21 | [[package]] 22 | name = "arrayref" 23 | version = "0.3.6" 24 | source = "registry+https://github.com/rust-lang/crates.io-index" 25 | checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" 26 | 27 | [[package]] 28 | name = "arrayvec" 29 | version = "0.5.1" 30 | source = "registry+https://github.com/rust-lang/crates.io-index" 31 | checksum = "cff77d8686867eceff3105329d4698d96c2391c176d5d03adc90c7389162b5b8" 32 | 33 | [[package]] 34 | name = "assert_cmd" 35 | version = "0.12.0" 36 | source = "registry+https://github.com/rust-lang/crates.io-index" 37 | checksum = "6283bac8dd7226470d491bc4737816fea4ca1fba7a2847f2e9097fd6bfb4624c" 38 | dependencies = [ 39 | "doc-comment", 40 | "escargot", 41 | "predicates", 42 | "predicates-core", 43 | "predicates-tree", 44 | ] 45 | 46 | [[package]] 47 | name = "assert_fs" 48 | version = "0.13.1" 49 | source = "registry+https://github.com/rust-lang/crates.io-index" 50 | checksum = "815b47a19f74a89ba726736cdac88c9fba28a655b16ea01225adae328b26f1bf" 51 | dependencies = [ 52 | "doc-comment", 53 | "globwalk", 54 | "predicates", 55 | "predicates-core", 56 | "predicates-tree", 57 | "tempfile", 58 | ] 59 | 60 | [[package]] 61 | name = "atty" 62 | version = "0.2.14" 63 | source = "registry+https://github.com/rust-lang/crates.io-index" 64 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 65 | dependencies = [ 66 | "hermit-abi", 67 | "libc", 68 | "winapi", 69 | ] 70 | 71 | [[package]] 72 | name = "autocfg" 73 | version = "1.0.0" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" 76 | 77 | [[package]] 78 | name = "base64" 79 | version = "0.11.0" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" 82 | 83 | [[package]] 84 | name = "belay" 85 | version = "0.5.0" 86 | dependencies = [ 87 | "assert_cmd", 88 | "assert_fs", 89 | "directories", 90 | "predicates", 91 | "serde", 92 | "serde_yaml", 93 | "structopt", 94 | "yaml-rust", 95 | ] 96 | 97 | [[package]] 98 | name = "bitflags" 99 | version = "1.2.1" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 102 | 103 | [[package]] 104 | name = "blake2b_simd" 105 | version = "0.5.10" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "d8fb2d74254a3a0b5cac33ac9f8ed0e44aa50378d9dbb2e5d83bd21ed1dc2c8a" 108 | dependencies = [ 109 | "arrayref", 110 | "arrayvec", 111 | "constant_time_eq", 112 | ] 113 | 114 | [[package]] 115 | name = "bstr" 116 | version = "0.2.11" 117 | source = "registry+https://github.com/rust-lang/crates.io-index" 118 | checksum = "502ae1441a0a5adb8fbd38a5955a6416b9493e92b465de5e4a9bde6a539c2c48" 119 | dependencies = [ 120 | "memchr", 121 | ] 122 | 123 | [[package]] 124 | name = "c2-chacha" 125 | version = "0.2.3" 126 | source = "registry+https://github.com/rust-lang/crates.io-index" 127 | checksum = "214238caa1bf3a496ec3392968969cab8549f96ff30652c9e56885329315f6bb" 128 | dependencies = [ 129 | "ppv-lite86", 130 | ] 131 | 132 | [[package]] 133 | name = "cfg-if" 134 | version = "0.1.10" 135 | source = "registry+https://github.com/rust-lang/crates.io-index" 136 | checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 137 | 138 | [[package]] 139 | name = "clap" 140 | version = "2.33.0" 141 | source = "registry+https://github.com/rust-lang/crates.io-index" 142 | checksum = "5067f5bb2d80ef5d68b4c87db81601f0b75bca627bc2ef76b141d7b846a3c6d9" 143 | dependencies = [ 144 | "ansi_term", 145 | "atty", 146 | "bitflags", 147 | "strsim", 148 | "textwrap", 149 | "unicode-width", 150 | "vec_map", 151 | ] 152 | 153 | [[package]] 154 | name = "constant_time_eq" 155 | version = "0.1.5" 156 | source = "registry+https://github.com/rust-lang/crates.io-index" 157 | checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" 158 | 159 | [[package]] 160 | name = "crossbeam-channel" 161 | version = "0.4.2" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "cced8691919c02aac3cb0a1bc2e9b73d89e832bf9a06fc579d4e71b68a2da061" 164 | dependencies = [ 165 | "crossbeam-utils", 166 | "maybe-uninit", 167 | ] 168 | 169 | [[package]] 170 | name = "crossbeam-utils" 171 | version = "0.7.2" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" 174 | dependencies = [ 175 | "autocfg", 176 | "cfg-if", 177 | "lazy_static", 178 | ] 179 | 180 | [[package]] 181 | name = "difference" 182 | version = "2.0.0" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" 185 | 186 | [[package]] 187 | name = "directories" 188 | version = "2.0.2" 189 | source = "registry+https://github.com/rust-lang/crates.io-index" 190 | checksum = "551a778172a450d7fc12e629ca3b0428d00f6afa9a43da1b630d54604e97371c" 191 | dependencies = [ 192 | "cfg-if", 193 | "dirs-sys", 194 | ] 195 | 196 | [[package]] 197 | name = "dirs-sys" 198 | version = "0.3.4" 199 | source = "registry+https://github.com/rust-lang/crates.io-index" 200 | checksum = "afa0b23de8fd801745c471deffa6e12d248f962c9fd4b4c33787b055599bde7b" 201 | dependencies = [ 202 | "cfg-if", 203 | "libc", 204 | "redox_users", 205 | "winapi", 206 | ] 207 | 208 | [[package]] 209 | name = "doc-comment" 210 | version = "0.3.1" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "923dea538cea0aa3025e8685b20d6ee21ef99c4f77e954a30febbaac5ec73a97" 213 | 214 | [[package]] 215 | name = "dtoa" 216 | version = "0.4.5" 217 | source = "registry+https://github.com/rust-lang/crates.io-index" 218 | checksum = "4358a9e11b9a09cf52383b451b49a169e8d797b68aa02301ff586d70d9661ea3" 219 | 220 | [[package]] 221 | name = "escargot" 222 | version = "0.5.0" 223 | source = "registry+https://github.com/rust-lang/crates.io-index" 224 | checksum = "74cf96bec282dcdb07099f7e31d9fed323bca9435a09aba7b6d99b7617bca96d" 225 | dependencies = [ 226 | "lazy_static", 227 | "log", 228 | "serde", 229 | "serde_json", 230 | ] 231 | 232 | [[package]] 233 | name = "float-cmp" 234 | version = "0.6.0" 235 | source = "registry+https://github.com/rust-lang/crates.io-index" 236 | checksum = "da62c4f1b81918835a8c6a484a397775fff5953fe83529afd51b05f5c6a6617d" 237 | dependencies = [ 238 | "num-traits", 239 | ] 240 | 241 | [[package]] 242 | name = "fnv" 243 | version = "1.0.6" 244 | source = "registry+https://github.com/rust-lang/crates.io-index" 245 | checksum = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3" 246 | 247 | [[package]] 248 | name = "getrandom" 249 | version = "0.1.14" 250 | source = "registry+https://github.com/rust-lang/crates.io-index" 251 | checksum = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb" 252 | dependencies = [ 253 | "cfg-if", 254 | "libc", 255 | "wasi", 256 | ] 257 | 258 | [[package]] 259 | name = "globset" 260 | version = "0.4.4" 261 | source = "registry+https://github.com/rust-lang/crates.io-index" 262 | checksum = "925aa2cac82d8834e2b2a4415b6f6879757fb5c0928fc445ae76461a12eed8f2" 263 | dependencies = [ 264 | "aho-corasick", 265 | "bstr", 266 | "fnv", 267 | "log", 268 | "regex", 269 | ] 270 | 271 | [[package]] 272 | name = "globwalk" 273 | version = "0.7.3" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | checksum = "d9db17aec586697a93219b19726b5b68307eba92898c34b170857343fe67c99d" 276 | dependencies = [ 277 | "ignore", 278 | "walkdir", 279 | ] 280 | 281 | [[package]] 282 | name = "heck" 283 | version = "0.3.1" 284 | source = "registry+https://github.com/rust-lang/crates.io-index" 285 | checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" 286 | dependencies = [ 287 | "unicode-segmentation", 288 | ] 289 | 290 | [[package]] 291 | name = "hermit-abi" 292 | version = "0.1.8" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "1010591b26bbfe835e9faeabeb11866061cc7dcebffd56ad7d0942d0e61aefd8" 295 | dependencies = [ 296 | "libc", 297 | ] 298 | 299 | [[package]] 300 | name = "ignore" 301 | version = "0.4.11" 302 | source = "registry+https://github.com/rust-lang/crates.io-index" 303 | checksum = "522daefc3b69036f80c7d2990b28ff9e0471c683bad05ca258e0a01dd22c5a1e" 304 | dependencies = [ 305 | "crossbeam-channel", 306 | "globset", 307 | "lazy_static", 308 | "log", 309 | "memchr", 310 | "regex", 311 | "same-file", 312 | "thread_local", 313 | "walkdir", 314 | "winapi-util", 315 | ] 316 | 317 | [[package]] 318 | name = "itoa" 319 | version = "0.4.5" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "b8b7a7c0c47db5545ed3fef7468ee7bb5b74691498139e4b3f6a20685dc6dd8e" 322 | 323 | [[package]] 324 | name = "lazy_static" 325 | version = "1.4.0" 326 | source = "registry+https://github.com/rust-lang/crates.io-index" 327 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 328 | 329 | [[package]] 330 | name = "libc" 331 | version = "0.2.67" 332 | source = "registry+https://github.com/rust-lang/crates.io-index" 333 | checksum = "eb147597cdf94ed43ab7a9038716637d2d1bf2bc571da995d0028dec06bd3018" 334 | 335 | [[package]] 336 | name = "linked-hash-map" 337 | version = "0.5.2" 338 | source = "registry+https://github.com/rust-lang/crates.io-index" 339 | checksum = "ae91b68aebc4ddb91978b11a1b02ddd8602a05ec19002801c5666000e05e0f83" 340 | 341 | [[package]] 342 | name = "log" 343 | version = "0.4.8" 344 | source = "registry+https://github.com/rust-lang/crates.io-index" 345 | checksum = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" 346 | dependencies = [ 347 | "cfg-if", 348 | ] 349 | 350 | [[package]] 351 | name = "maybe-uninit" 352 | version = "2.0.0" 353 | source = "registry+https://github.com/rust-lang/crates.io-index" 354 | checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" 355 | 356 | [[package]] 357 | name = "memchr" 358 | version = "2.3.3" 359 | source = "registry+https://github.com/rust-lang/crates.io-index" 360 | checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" 361 | 362 | [[package]] 363 | name = "normalize-line-endings" 364 | version = "0.3.0" 365 | source = "registry+https://github.com/rust-lang/crates.io-index" 366 | checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" 367 | 368 | [[package]] 369 | name = "num-traits" 370 | version = "0.2.11" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "c62be47e61d1842b9170f0fdeec8eba98e60e90e5446449a0545e5152acd7096" 373 | dependencies = [ 374 | "autocfg", 375 | ] 376 | 377 | [[package]] 378 | name = "ppv-lite86" 379 | version = "0.2.6" 380 | source = "registry+https://github.com/rust-lang/crates.io-index" 381 | checksum = "74490b50b9fbe561ac330df47c08f3f33073d2d00c150f719147d7c54522fa1b" 382 | 383 | [[package]] 384 | name = "predicates" 385 | version = "1.0.4" 386 | source = "registry+https://github.com/rust-lang/crates.io-index" 387 | checksum = "347a1b6f0b21e636bc9872fb60b83b8e185f6f5516298b8238699f7f9a531030" 388 | dependencies = [ 389 | "difference", 390 | "float-cmp", 391 | "normalize-line-endings", 392 | "predicates-core", 393 | "regex", 394 | ] 395 | 396 | [[package]] 397 | name = "predicates-core" 398 | version = "1.0.0" 399 | source = "registry+https://github.com/rust-lang/crates.io-index" 400 | checksum = "06075c3a3e92559ff8929e7a280684489ea27fe44805174c3ebd9328dcb37178" 401 | 402 | [[package]] 403 | name = "predicates-tree" 404 | version = "1.0.0" 405 | source = "registry+https://github.com/rust-lang/crates.io-index" 406 | checksum = "8e63c4859013b38a76eca2414c64911fba30def9e3202ac461a2d22831220124" 407 | dependencies = [ 408 | "predicates-core", 409 | "treeline", 410 | ] 411 | 412 | [[package]] 413 | name = "proc-macro-error" 414 | version = "0.4.11" 415 | source = "registry+https://github.com/rust-lang/crates.io-index" 416 | checksum = "e7959c6467d962050d639361f7703b2051c43036d03493c36f01d440fdd3138a" 417 | dependencies = [ 418 | "proc-macro-error-attr", 419 | "proc-macro2", 420 | "quote", 421 | "syn", 422 | "version_check", 423 | ] 424 | 425 | [[package]] 426 | name = "proc-macro-error-attr" 427 | version = "0.4.11" 428 | source = "registry+https://github.com/rust-lang/crates.io-index" 429 | checksum = "e4002d9f55991d5e019fb940a90e1a95eb80c24e77cb2462dd4dc869604d543a" 430 | dependencies = [ 431 | "proc-macro2", 432 | "quote", 433 | "syn", 434 | "syn-mid", 435 | "version_check", 436 | ] 437 | 438 | [[package]] 439 | name = "proc-macro2" 440 | version = "1.0.9" 441 | source = "registry+https://github.com/rust-lang/crates.io-index" 442 | checksum = "6c09721c6781493a2a492a96b5a5bf19b65917fe6728884e7c44dd0c60ca3435" 443 | dependencies = [ 444 | "unicode-xid", 445 | ] 446 | 447 | [[package]] 448 | name = "quote" 449 | version = "1.0.2" 450 | source = "registry+https://github.com/rust-lang/crates.io-index" 451 | checksum = "053a8c8bcc71fcce321828dc897a98ab9760bef03a4fc36693c231e5b3216cfe" 452 | dependencies = [ 453 | "proc-macro2", 454 | ] 455 | 456 | [[package]] 457 | name = "rand" 458 | version = "0.7.3" 459 | source = "registry+https://github.com/rust-lang/crates.io-index" 460 | checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" 461 | dependencies = [ 462 | "getrandom", 463 | "libc", 464 | "rand_chacha", 465 | "rand_core", 466 | "rand_hc", 467 | ] 468 | 469 | [[package]] 470 | name = "rand_chacha" 471 | version = "0.2.1" 472 | source = "registry+https://github.com/rust-lang/crates.io-index" 473 | checksum = "03a2a90da8c7523f554344f921aa97283eadf6ac484a6d2a7d0212fa7f8d6853" 474 | dependencies = [ 475 | "c2-chacha", 476 | "rand_core", 477 | ] 478 | 479 | [[package]] 480 | name = "rand_core" 481 | version = "0.5.1" 482 | source = "registry+https://github.com/rust-lang/crates.io-index" 483 | checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" 484 | dependencies = [ 485 | "getrandom", 486 | ] 487 | 488 | [[package]] 489 | name = "rand_hc" 490 | version = "0.2.0" 491 | source = "registry+https://github.com/rust-lang/crates.io-index" 492 | checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" 493 | dependencies = [ 494 | "rand_core", 495 | ] 496 | 497 | [[package]] 498 | name = "redox_syscall" 499 | version = "0.1.56" 500 | source = "registry+https://github.com/rust-lang/crates.io-index" 501 | checksum = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" 502 | 503 | [[package]] 504 | name = "redox_users" 505 | version = "0.3.4" 506 | source = "registry+https://github.com/rust-lang/crates.io-index" 507 | checksum = "09b23093265f8d200fa7b4c2c76297f47e681c655f6f1285a8780d6a022f7431" 508 | dependencies = [ 509 | "getrandom", 510 | "redox_syscall", 511 | "rust-argon2", 512 | ] 513 | 514 | [[package]] 515 | name = "regex" 516 | version = "1.3.4" 517 | source = "registry+https://github.com/rust-lang/crates.io-index" 518 | checksum = "322cf97724bea3ee221b78fe25ac9c46114ebb51747ad5babd51a2fc6a8235a8" 519 | dependencies = [ 520 | "aho-corasick", 521 | "memchr", 522 | "regex-syntax", 523 | "thread_local", 524 | ] 525 | 526 | [[package]] 527 | name = "regex-syntax" 528 | version = "0.6.16" 529 | source = "registry+https://github.com/rust-lang/crates.io-index" 530 | checksum = "1132f845907680735a84409c3bebc64d1364a5683ffbce899550cd09d5eaefc1" 531 | 532 | [[package]] 533 | name = "remove_dir_all" 534 | version = "0.5.2" 535 | source = "registry+https://github.com/rust-lang/crates.io-index" 536 | checksum = "4a83fa3702a688b9359eccba92d153ac33fd2e8462f9e0e3fdf155239ea7792e" 537 | dependencies = [ 538 | "winapi", 539 | ] 540 | 541 | [[package]] 542 | name = "rust-argon2" 543 | version = "0.7.0" 544 | source = "registry+https://github.com/rust-lang/crates.io-index" 545 | checksum = "2bc8af4bda8e1ff4932523b94d3dd20ee30a87232323eda55903ffd71d2fb017" 546 | dependencies = [ 547 | "base64", 548 | "blake2b_simd", 549 | "constant_time_eq", 550 | "crossbeam-utils", 551 | ] 552 | 553 | [[package]] 554 | name = "ryu" 555 | version = "1.0.2" 556 | source = "registry+https://github.com/rust-lang/crates.io-index" 557 | checksum = "bfa8506c1de11c9c4e4c38863ccbe02a305c8188e85a05a784c9e11e1c3910c8" 558 | 559 | [[package]] 560 | name = "same-file" 561 | version = "1.0.6" 562 | source = "registry+https://github.com/rust-lang/crates.io-index" 563 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 564 | dependencies = [ 565 | "winapi-util", 566 | ] 567 | 568 | [[package]] 569 | name = "serde" 570 | version = "1.0.104" 571 | source = "registry+https://github.com/rust-lang/crates.io-index" 572 | checksum = "414115f25f818d7dfccec8ee535d76949ae78584fc4f79a6f45a904bf8ab4449" 573 | dependencies = [ 574 | "serde_derive", 575 | ] 576 | 577 | [[package]] 578 | name = "serde_derive" 579 | version = "1.0.104" 580 | source = "registry+https://github.com/rust-lang/crates.io-index" 581 | checksum = "128f9e303a5a29922045a830221b8f78ec74a5f544944f3d5984f8ec3895ef64" 582 | dependencies = [ 583 | "proc-macro2", 584 | "quote", 585 | "syn", 586 | ] 587 | 588 | [[package]] 589 | name = "serde_json" 590 | version = "1.0.48" 591 | source = "registry+https://github.com/rust-lang/crates.io-index" 592 | checksum = "9371ade75d4c2d6cb154141b9752cf3781ec9c05e0e5cf35060e1e70ee7b9c25" 593 | dependencies = [ 594 | "itoa", 595 | "ryu", 596 | "serde", 597 | ] 598 | 599 | [[package]] 600 | name = "serde_yaml" 601 | version = "0.8.11" 602 | source = "registry+https://github.com/rust-lang/crates.io-index" 603 | checksum = "691b17f19fc1ec9d94ec0b5864859290dff279dbd7b03f017afda54eb36c3c35" 604 | dependencies = [ 605 | "dtoa", 606 | "linked-hash-map", 607 | "serde", 608 | "yaml-rust", 609 | ] 610 | 611 | [[package]] 612 | name = "strsim" 613 | version = "0.8.0" 614 | source = "registry+https://github.com/rust-lang/crates.io-index" 615 | checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" 616 | 617 | [[package]] 618 | name = "structopt" 619 | version = "0.3.11" 620 | source = "registry+https://github.com/rust-lang/crates.io-index" 621 | checksum = "3fe43617218c0805c6eb37160119dc3c548110a67786da7218d1c6555212f073" 622 | dependencies = [ 623 | "clap", 624 | "lazy_static", 625 | "structopt-derive", 626 | ] 627 | 628 | [[package]] 629 | name = "structopt-derive" 630 | version = "0.4.4" 631 | source = "registry+https://github.com/rust-lang/crates.io-index" 632 | checksum = "c6e79c80e0f4efd86ca960218d4e056249be189ff1c42824dcd9a7f51a56f0bd" 633 | dependencies = [ 634 | "heck", 635 | "proc-macro-error", 636 | "proc-macro2", 637 | "quote", 638 | "syn", 639 | ] 640 | 641 | [[package]] 642 | name = "syn" 643 | version = "1.0.16" 644 | source = "registry+https://github.com/rust-lang/crates.io-index" 645 | checksum = "123bd9499cfb380418d509322d7a6d52e5315f064fe4b3ad18a53d6b92c07859" 646 | dependencies = [ 647 | "proc-macro2", 648 | "quote", 649 | "unicode-xid", 650 | ] 651 | 652 | [[package]] 653 | name = "syn-mid" 654 | version = "0.5.0" 655 | source = "registry+https://github.com/rust-lang/crates.io-index" 656 | checksum = "7be3539f6c128a931cf19dcee741c1af532c7fd387baa739c03dd2e96479338a" 657 | dependencies = [ 658 | "proc-macro2", 659 | "quote", 660 | "syn", 661 | ] 662 | 663 | [[package]] 664 | name = "tempfile" 665 | version = "3.1.0" 666 | source = "registry+https://github.com/rust-lang/crates.io-index" 667 | checksum = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" 668 | dependencies = [ 669 | "cfg-if", 670 | "libc", 671 | "rand", 672 | "redox_syscall", 673 | "remove_dir_all", 674 | "winapi", 675 | ] 676 | 677 | [[package]] 678 | name = "textwrap" 679 | version = "0.11.0" 680 | source = "registry+https://github.com/rust-lang/crates.io-index" 681 | checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" 682 | dependencies = [ 683 | "unicode-width", 684 | ] 685 | 686 | [[package]] 687 | name = "thread_local" 688 | version = "1.0.1" 689 | source = "registry+https://github.com/rust-lang/crates.io-index" 690 | checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" 691 | dependencies = [ 692 | "lazy_static", 693 | ] 694 | 695 | [[package]] 696 | name = "treeline" 697 | version = "0.1.0" 698 | source = "registry+https://github.com/rust-lang/crates.io-index" 699 | checksum = "a7f741b240f1a48843f9b8e0444fb55fb2a4ff67293b50a9179dfd5ea67f8d41" 700 | 701 | [[package]] 702 | name = "unicode-segmentation" 703 | version = "1.6.0" 704 | source = "registry+https://github.com/rust-lang/crates.io-index" 705 | checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" 706 | 707 | [[package]] 708 | name = "unicode-width" 709 | version = "0.1.7" 710 | source = "registry+https://github.com/rust-lang/crates.io-index" 711 | checksum = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479" 712 | 713 | [[package]] 714 | name = "unicode-xid" 715 | version = "0.2.0" 716 | source = "registry+https://github.com/rust-lang/crates.io-index" 717 | checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" 718 | 719 | [[package]] 720 | name = "vec_map" 721 | version = "0.8.1" 722 | source = "registry+https://github.com/rust-lang/crates.io-index" 723 | checksum = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a" 724 | 725 | [[package]] 726 | name = "version_check" 727 | version = "0.9.1" 728 | source = "registry+https://github.com/rust-lang/crates.io-index" 729 | checksum = "078775d0255232fb988e6fccf26ddc9d1ac274299aaedcedce21c6f72cc533ce" 730 | 731 | [[package]] 732 | name = "walkdir" 733 | version = "2.3.1" 734 | source = "registry+https://github.com/rust-lang/crates.io-index" 735 | checksum = "777182bc735b6424e1a57516d35ed72cb8019d85c8c9bf536dccb3445c1a2f7d" 736 | dependencies = [ 737 | "same-file", 738 | "winapi", 739 | "winapi-util", 740 | ] 741 | 742 | [[package]] 743 | name = "wasi" 744 | version = "0.9.0+wasi-snapshot-preview1" 745 | source = "registry+https://github.com/rust-lang/crates.io-index" 746 | checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" 747 | 748 | [[package]] 749 | name = "winapi" 750 | version = "0.3.8" 751 | source = "registry+https://github.com/rust-lang/crates.io-index" 752 | checksum = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" 753 | dependencies = [ 754 | "winapi-i686-pc-windows-gnu", 755 | "winapi-x86_64-pc-windows-gnu", 756 | ] 757 | 758 | [[package]] 759 | name = "winapi-i686-pc-windows-gnu" 760 | version = "0.4.0" 761 | source = "registry+https://github.com/rust-lang/crates.io-index" 762 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 763 | 764 | [[package]] 765 | name = "winapi-util" 766 | version = "0.1.3" 767 | source = "registry+https://github.com/rust-lang/crates.io-index" 768 | checksum = "4ccfbf554c6ad11084fb7517daca16cfdcaccbdadba4fc336f032a8b12c2ad80" 769 | dependencies = [ 770 | "winapi", 771 | ] 772 | 773 | [[package]] 774 | name = "winapi-x86_64-pc-windows-gnu" 775 | version = "0.4.0" 776 | source = "registry+https://github.com/rust-lang/crates.io-index" 777 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 778 | 779 | [[package]] 780 | name = "yaml-rust" 781 | version = "0.4.3" 782 | source = "registry+https://github.com/rust-lang/crates.io-index" 783 | checksum = "65923dd1784f44da1d2c3dbbc5e822045628c590ba72123e1c73d3c230c4434d" 784 | dependencies = [ 785 | "linked-hash-map", 786 | ] 787 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "belay" 3 | description = "Run your CI checks locally to git push with confidence" 4 | license = "MIT/Apache-2.0" 5 | repository = "https://github.com/JoshMcguigan/belay" 6 | readme = "README.md" 7 | version = "0.5.0" 8 | authors = ["Josh Mcguigan"] 9 | edition = "2018" 10 | 11 | [dependencies] 12 | directories = "2.0" 13 | serde = { version = "1.0", features = ["derive"] } 14 | serde_yaml = "0.8" 15 | yaml-rust = "0.4" 16 | structopt = "0.3" 17 | 18 | [dev-dependencies] 19 | assert_cmd = "0.12" 20 | assert_fs = "0.13" 21 | predicates = "1" 22 | -------------------------------------------------------------------------------- /LICENSE-APACHE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2020 Josh Mcguigan 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2020 Josh Mcguigan 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Belay [![crates.io badge](https://img.shields.io/crates/v/belay.svg)](https://crates.io/crates/belay) [![github action badge](https://github.com/JoshMcguigan/belay/workflows/Rust/badge.svg)](https://github.com/JoshMcguigan/belay/actions) 2 | 3 | Belay makes it easy to run your CI checks locally, so you can `git push` with confidence. 4 | 5 | [![asciicast](https://asciinema.org/a/okeJVtb2YJFHneYnS9ZnObBmK.svg)](https://asciinema.org/a/okeJVtb2YJFHneYnS9ZnObBmK) 6 | 7 | ### Usage 8 | 9 | In a git repo with either Gitlab or GitHub CI configured, running `belay` with no arguments will parse your CI configuration and run your CI scripts on your local machine. 10 | 11 | ```bash 12 | $ belay 13 | Checking 'build': 14 | Success! 15 | Checking 'test': 16 | ... test output 17 | .. 18 | . 19 | Success! 20 | ``` 21 | 22 | Belay can also setup pre-commit or pre-push git hooks in your repo. 23 | 24 | ```bash 25 | # to create a pre-push hook 26 | $ belay hook push 27 | 28 | # to create a pre-commit hook 29 | $ belay hook commit 30 | ``` 31 | 32 | ### Install 33 | 34 | ```bash 35 | cargo install --force belay 36 | ``` 37 | 38 | ### Configuration 39 | 40 | Belay creates a configuration file in the following location: 41 | 42 | * Linux: /home/alice/.config/belay/config.yml 43 | * Windows: C:\Users\Alice\AppData\Roaming\cargo\belay\config.yml 44 | * macOS: /Users/Alice/Library/Preferences/com.cargo.belay/config.yml 45 | 46 | The keys of the config file are described below: 47 | 48 | * command\_blacklist 49 | * array of strings 50 | * belay will skip CI tasks which contain any string in the command blacklist 51 | 52 | ## License 53 | 54 | Licensed under either of 55 | 56 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 57 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 58 | 59 | at your option. 60 | 61 | ### Contribution 62 | 63 | Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. 64 | -------------------------------------------------------------------------------- /src/args.rs: -------------------------------------------------------------------------------- 1 | use structopt::StructOpt; 2 | 3 | #[derive(StructOpt)] 4 | pub struct Args { 5 | #[structopt(subcommand)] 6 | pub subcommand: Option, 7 | } 8 | 9 | #[derive(StructOpt)] 10 | pub enum Subcommand { 11 | Hook { 12 | #[structopt(subcommand)] 13 | hook_type: HookType, 14 | }, 15 | } 16 | 17 | #[derive(StructOpt)] 18 | pub enum HookType { 19 | Commit, 20 | Push, 21 | } 22 | 23 | impl HookType { 24 | pub fn filename(&self) -> String { 25 | match self { 26 | HookType::Commit => String::from("pre-commit"), 27 | HookType::Push => String::from("pre-push"), 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/ci.rs: -------------------------------------------------------------------------------- 1 | use crate::Config; 2 | 3 | pub mod github; 4 | pub mod gitlab; 5 | 6 | pub struct Task { 7 | pub name: Option, 8 | pub command: String, 9 | applicability: Vec, 10 | } 11 | 12 | /// Applicability represents the times when a task should be run. 13 | /// 14 | /// See Trigger for additional details. 15 | #[derive(Clone)] 16 | pub enum Applicability { 17 | /// This task should only be run on a push. If branches are 18 | /// specified it should only be run on a push to these particular 19 | /// branches. 20 | /// 21 | /// Belay will run these tasks even on pre-commit hooks (as well 22 | /// as pre-push hooks). 23 | Push { branches: Option> }, 24 | /// This tasks should be run on pull requests. 25 | PullRequest, 26 | /// This task is always applicable. 27 | Any, 28 | } 29 | 30 | /// Trigger represents the type of CI event we expect to happen. 31 | /// 32 | /// Of course we can't know whether the user has an open pull 33 | /// request (or will open a pull request), but we can assume this 34 | /// based on whether they have an upstream remote configured. 35 | pub enum Trigger { 36 | Push { branch: String }, 37 | PullRequest, 38 | } 39 | 40 | impl Applicability { 41 | fn is_triggered_by(&self, trigger: &Trigger) -> bool { 42 | match (self, trigger) { 43 | ( 44 | Applicability::Push { 45 | branches: Some(branches), 46 | }, 47 | Trigger::Push { branch }, 48 | ) => branches.contains(branch), 49 | (Applicability::Push { branches: None }, Trigger::Push { .. }) => true, 50 | (Applicability::PullRequest, Trigger::PullRequest) => true, 51 | (Applicability::Any, _) => true, 52 | (_, _) => false, 53 | } 54 | } 55 | } 56 | 57 | pub trait TaskList { 58 | /// Returns all CI tasks, including tasks which we 59 | /// would not want to execute in belay. 60 | fn all_tasks(&self) -> Vec; 61 | 62 | /// Returns the subset of CI tasks that we do 63 | /// want to execute in belay. 64 | fn tasks(&self, config: Config, triggers: Vec) -> Vec { 65 | fn is_applicable(applicabilities: &[Applicability], triggers: &[Trigger]) -> bool { 66 | for applicability in applicabilities { 67 | for trigger in triggers { 68 | if applicability.is_triggered_by(trigger) { 69 | return true; 70 | } 71 | } 72 | } 73 | 74 | false 75 | } 76 | 77 | self.all_tasks() 78 | .into_iter() 79 | .filter(|task| { 80 | for blacklisted_command in &config.command_blacklist { 81 | if task.command.contains(blacklisted_command) { 82 | return false; 83 | } 84 | } 85 | 86 | true 87 | }) 88 | .filter(|task| is_applicable(&task.applicability, &triggers)) 89 | .collect() 90 | } 91 | } 92 | 93 | impl TaskList for github::CiConfig { 94 | fn all_tasks(&self) -> Vec { 95 | self.jobs 96 | .values() 97 | .flat_map(|job| &job.steps) 98 | .map(|step| Task { 99 | name: step.name.clone(), 100 | command: step.run.clone(), 101 | applicability: self.on.clone(), 102 | }) 103 | .collect() 104 | } 105 | } 106 | 107 | impl TaskList for gitlab::CiConfig { 108 | fn all_tasks(&self) -> Vec { 109 | self.jobs 110 | .values() 111 | .filter_map(|job| job.script.as_ref()) 112 | .flat_map(|script: &Vec| script) 113 | .map(|cmd| Task { 114 | name: None, 115 | command: cmd.clone(), 116 | // For now restricted applicability is not supported 117 | // for gitlab within belay. 118 | applicability: vec![Applicability::Any], 119 | }) 120 | .collect() 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/ci/github.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashMap, 3 | convert::TryFrom, 4 | error::Error, 5 | fmt::{self, Display}, 6 | }; 7 | 8 | use yaml_rust::YamlLoader; 9 | 10 | use super::Applicability; 11 | 12 | pub struct CiConfig { 13 | #[allow(dead_code)] 14 | pub name: String, 15 | pub jobs: HashMap, 16 | pub on: Vec, 17 | } 18 | 19 | pub struct CiConfigJob { 20 | pub steps: Vec, 21 | } 22 | 23 | pub struct CiConfigJobStep { 24 | pub name: Option, 25 | pub run: String, 26 | } 27 | 28 | fn applicability_from(input: &str, branches: Option>) -> Result { 29 | match input { 30 | "push" => Ok(Applicability::Push { branches }), 31 | "pull_request" => Ok(Applicability::PullRequest), 32 | _ => Err(()), 33 | } 34 | } 35 | 36 | #[derive(Debug)] 37 | pub enum YamlParseError { 38 | ScanError(yaml_rust::scanner::ScanError), 39 | MissingDocument, 40 | MissingField, 41 | } 42 | 43 | impl Display for YamlParseError { 44 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { 45 | match self { 46 | YamlParseError::ScanError(e) => write!(f, "{}", e), 47 | YamlParseError::MissingDocument => write!(f, "No yaml document found"), 48 | YamlParseError::MissingField => write!(f, "Missing required field"), 49 | } 50 | } 51 | } 52 | 53 | impl Error for YamlParseError {} 54 | 55 | impl From for YamlParseError { 56 | fn from(input: yaml_rust::scanner::ScanError) -> Self { 57 | Self::ScanError(input) 58 | } 59 | } 60 | 61 | impl TryFrom<&str> for CiConfig { 62 | type Error = YamlParseError; 63 | 64 | fn try_from(input: &str) -> Result { 65 | let yaml = YamlLoader::load_from_str(input)? 66 | .pop() 67 | .ok_or(YamlParseError::MissingDocument)?; 68 | 69 | let name = &yaml["name"].as_str().ok_or(YamlParseError::MissingField)?; 70 | 71 | let jobs = &yaml["jobs"].as_hash().ok_or(YamlParseError::MissingField)?; 72 | 73 | // We attempt to parse the `on` field as both an array as well as a map, 74 | // then use whichever of the two worked. It would be possible to only 75 | // attempt to parse this as a map if parsing as an array fails, but 76 | // in practice there won't be any meaningful performance difference. 77 | let on_as_vec = yaml["on"].as_vec().map(|a| { 78 | a.iter() 79 | .filter_map(|item| { 80 | item.as_str() 81 | .iter() 82 | .filter_map(|&s| applicability_from(s, None).ok()) 83 | .next() 84 | }) 85 | .collect() 86 | }); 87 | let on_as_map = yaml["on"].as_hash().map(|hashmap| { 88 | hashmap 89 | .iter() 90 | .filter_map(|(k, v)| k.as_str().map(|k| (k, v))) 91 | .filter_map(|(k, v)| { 92 | let branches = v["branches"].as_vec().map(|branches| { 93 | branches 94 | .iter() 95 | .filter_map(|branch| branch.as_str().map(|s| s.to_string())) 96 | .collect() 97 | }); 98 | applicability_from(k, branches).ok() 99 | }) 100 | .collect() 101 | }); 102 | let on = match (on_as_vec, on_as_map) { 103 | (Some(x), _) => x, 104 | (_, Some(x)) => x, 105 | (None, None) => vec![], 106 | }; 107 | 108 | let mut ci_config = CiConfig { 109 | name: (*name).to_string(), 110 | jobs: HashMap::new(), 111 | on, 112 | }; 113 | 114 | for (job_name, job) in jobs.iter() { 115 | let job_name = job_name 116 | .as_str() 117 | .ok_or(YamlParseError::MissingField) 118 | .map(|s| (*s).to_string())?; 119 | 120 | let steps = &job["steps"].as_vec().ok_or(YamlParseError::MissingField)?; 121 | 122 | let mut parsed_steps = vec![]; 123 | 124 | for step in steps.iter() { 125 | let name = step["name"].as_str().map(|s| (*s).to_string()); 126 | let run = step["run"].as_str().map(|s| (*s).to_string()); 127 | 128 | // we skip steps without run 129 | if let Some(run) = run { 130 | let step = CiConfigJobStep { name, run }; 131 | 132 | parsed_steps.push(step); 133 | } 134 | } 135 | 136 | ci_config.jobs.insert( 137 | job_name, 138 | CiConfigJob { 139 | steps: parsed_steps, 140 | }, 141 | ); 142 | } 143 | 144 | Ok(ci_config) 145 | } 146 | } 147 | 148 | #[cfg(test)] 149 | mod tests { 150 | use super::*; 151 | 152 | type Result = std::result::Result>; 153 | 154 | #[test] 155 | fn parse_github_yaml() -> Result<()> { 156 | let github_yaml = include_str!("../../tests/github_parse_check.yml"); 157 | 158 | let github_ci_config = CiConfig::try_from(github_yaml)?; 159 | 160 | assert_eq!("Rust", &github_ci_config.name); 161 | 162 | assert_eq!(1, github_ci_config.jobs.len()); 163 | 164 | let job = &github_ci_config.jobs["build"]; 165 | 166 | // the `uses` step is skipped during parsing 167 | assert_eq!(5, job.steps.len()); 168 | 169 | assert_eq!(2, github_ci_config.on.len()); 170 | 171 | Ok(()) 172 | } 173 | 174 | #[test] 175 | fn parse_github_yaml_push_to_branch() -> Result<()> { 176 | let github_yaml = include_str!("../../tests/github_parse_check_on_push_to_branch.yml"); 177 | 178 | let github_ci_config = CiConfig::try_from(github_yaml)?; 179 | 180 | assert_eq!(2, github_ci_config.on.len()); 181 | 182 | Ok(()) 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/ci/gitlab.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use std::collections::HashMap; 3 | 4 | #[derive(Deserialize)] 5 | pub struct CiConfig { 6 | #[allow(dead_code)] 7 | image: Option, 8 | #[allow(dead_code)] 9 | stages: Option>, 10 | #[serde(flatten)] 11 | pub(super) jobs: HashMap, 12 | } 13 | 14 | /// All fields which aren't explicitly configured in this struct are 15 | /// parsed as `jobs`, since jobs can have (almost) any name. 16 | /// 17 | /// Although all actual jobs will have a script field, the field 18 | /// is marked as optional here to support parsing config files 19 | /// which have extra fields in the root (see the `cache` key 20 | /// in the example gitlab file). 21 | #[derive(Deserialize)] 22 | pub struct CiConfigJob { 23 | pub(super) script: Option>, 24 | } 25 | 26 | #[cfg(test)] 27 | mod tests { 28 | use super::*; 29 | 30 | type Result = std::result::Result>; 31 | 32 | #[test] 33 | fn parse_gitlab_yaml() -> Result<()> { 34 | let gitlab_yaml = include_str!("../../tests/gitlab_parse_check.yml"); 35 | 36 | let gitlab_ci_config = serde_yaml::from_str::(gitlab_yaml)?; 37 | 38 | assert_eq!( 39 | 5, 40 | gitlab_ci_config.jobs.values().fold(0, |mut acc, job| { 41 | acc += job 42 | .script 43 | .as_ref() 44 | .map(|scripts| scripts.len()) 45 | .unwrap_or(0); 46 | acc 47 | }) 48 | ); 49 | 50 | Ok(()) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | 3 | use directories::ProjectDirs; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | #[derive(Deserialize, Serialize)] 7 | pub struct Config { 8 | pub command_blacklist: Vec, 9 | } 10 | 11 | impl Config { 12 | pub fn read() -> Self { 13 | let project_dirs = 14 | ProjectDirs::from("com", "cargo", "belay").expect("failed to find home directory"); 15 | let config_path = project_dirs.config_dir().join("config.yml"); 16 | 17 | if config_path.is_file() { 18 | serde_yaml::from_str::( 19 | &fs::read_to_string(config_path).expect("failed to read config file"), 20 | ) 21 | .expect("failed to read config file as yaml") 22 | } else { 23 | fs::create_dir_all(config_path.parent().expect("config dir should have parent")) 24 | .expect("failed to create config directory"); 25 | let config = Self::default(); 26 | 27 | let config_as_string = 28 | serde_yaml::to_string(&config).expect("failed to convert default config to string"); 29 | 30 | fs::File::create(&config_path).expect("failed to create config file"); 31 | 32 | fs::write(&config_path, config_as_string.as_bytes()) 33 | .expect("failed to write config to file"); 34 | 35 | config 36 | } 37 | } 38 | 39 | /// Creates a default configuration. 40 | /// 41 | /// This is specifically not an implementation of the Default 42 | /// trait because we want it to be on accesible from this module. 43 | fn default() -> Self { 44 | Self { 45 | command_blacklist: vec![ 46 | "apt install".into(), 47 | "cargo install".into(), 48 | "chown".into(), 49 | "rustup component add".into(), 50 | ], 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashSet, 3 | convert::TryFrom, 4 | env::current_dir, 5 | fs::{read_dir, read_to_string, File}, 6 | io::Write, 7 | path::{Path, PathBuf}, 8 | process::Command, 9 | }; 10 | use structopt::StructOpt; 11 | 12 | #[cfg(not(windows))] 13 | use std::{fs::Permissions, os::unix::fs::PermissionsExt}; 14 | 15 | mod args; 16 | use args::{Args, Subcommand}; 17 | 18 | mod ci; 19 | use ci::{github, gitlab, Task, TaskList, Trigger}; 20 | 21 | mod config; 22 | use config::Config; 23 | 24 | type Result = std::result::Result>; 25 | 26 | fn main() -> Result<()> { 27 | let root_dir = find_git_root().ok_or("Failed to find git root")?; 28 | 29 | let args = Args::from_args(); 30 | 31 | if let Some(Subcommand::Hook { hook_type }) = args.subcommand { 32 | let hook_filename = hook_type.filename(); 33 | let mut hook_path = root_dir; 34 | hook_path.push(".git"); 35 | hook_path.push("hooks"); 36 | hook_path.push(&hook_filename); 37 | 38 | let mut file = File::create(&hook_path)?; 39 | 40 | #[cfg(not(windows))] 41 | file.set_permissions(Permissions::from_mode(0o755))?; 42 | 43 | file.write_all(b"#!/bin/sh\nbelay")?; 44 | 45 | println!("Created hook `.git/hooks/{}`", hook_filename); 46 | 47 | return Ok(()); 48 | } 49 | 50 | let ci_configs: Vec> = 51 | match (handle_github(&root_dir), handle_gitlab(&root_dir)) { 52 | (Ok(configs), _) => configs 53 | .into_iter() 54 | .map(|c| Box::new(c) as Box) 55 | .collect(), 56 | (_, Ok(config)) => vec![Box::new(config)], 57 | _ => return Err("Unable to find CI configuration".into()), 58 | }; 59 | 60 | let mut completed_commands = HashSet::new(); 61 | for ci_config in ci_configs { 62 | for task in ci_config.tasks(Config::read(), get_triggers()) { 63 | let Task { name, command, .. } = task; 64 | 65 | // we want to de-duplicate commands across CI configurations 66 | if completed_commands.contains(&command) { 67 | continue; 68 | } 69 | 70 | let task_name = name.unwrap_or_else(|| command.clone()); 71 | println!("Checking '{}':", task_name); 72 | 73 | #[cfg(not(windows))] 74 | let status = Command::new("sh").arg("-c").arg(&command).status()?; 75 | #[cfg(windows)] 76 | let status = Command::new("cmd").arg("/c").arg(&command).status()?; 77 | 78 | if status.success() { 79 | println!("Success!"); 80 | } else { 81 | return Err("Failed".into()); 82 | } 83 | completed_commands.insert(command); 84 | } 85 | } 86 | 87 | Ok(()) 88 | } 89 | 90 | fn handle_github(root_dir: &Path) -> Result> { 91 | let github_workflows_dir = { 92 | let mut gh = root_dir.to_path_buf(); 93 | gh.push(".github"); 94 | gh.push("workflows"); 95 | 96 | gh 97 | }; 98 | 99 | let mut paths = read_dir(github_workflows_dir) 100 | .map_err(|_| "Unable to find CI configuration")? 101 | .filter_map(std::result::Result::ok) 102 | .map(|entry| entry.path()) 103 | .collect::>(); 104 | // Sort the workflow files alphabetically, so they run 105 | // in deterministic order. 106 | paths.sort(); 107 | 108 | let configs = paths 109 | .into_iter() 110 | .map(|path| -> Result { 111 | Ok(github::CiConfig::try_from(read_to_string(path)?.as_str())?) 112 | }) 113 | .collect::>>()?; 114 | 115 | if configs.is_empty() { 116 | return Err("failed to find github config".into()); 117 | } 118 | 119 | Ok(configs) 120 | } 121 | 122 | fn handle_gitlab(root_dir: &Path) -> Result { 123 | let file_path = { 124 | let mut path = root_dir.to_path_buf(); 125 | path.push(".gitlab-ci.yml"); 126 | 127 | path 128 | }; 129 | 130 | Ok(serde_yaml::from_str::(&read_to_string( 131 | file_path, 132 | )?)?) 133 | } 134 | 135 | fn find_git_root() -> Option { 136 | let mut dir = current_dir().ok()?; 137 | 138 | loop { 139 | let mut git_dir = dir.clone(); 140 | git_dir.push(".git"); 141 | 142 | if git_dir.exists() { 143 | return Some(dir); 144 | } 145 | 146 | dir.push(".."); 147 | 148 | if !dir.exists() { 149 | return None; 150 | } 151 | } 152 | } 153 | 154 | /// Get the best estimate of the triggers for this CI run. 155 | /// 156 | /// We can't know for sure if this will turn into a pull 157 | /// request, but we assume if there is an upstream remote 158 | /// that it will. 159 | fn get_triggers() -> Vec { 160 | let mut triggers = vec![Trigger::Push { 161 | branch: current_branch(), 162 | }]; 163 | 164 | if has_upstream() { 165 | triggers.push(Trigger::PullRequest); 166 | } 167 | 168 | triggers 169 | } 170 | 171 | /// Used to guess if this will turn into a pull request. Has the 172 | /// limitation that it only works if the upstream repository is 173 | /// named 'upstream'. 174 | fn has_upstream() -> bool { 175 | let command = "git remote"; 176 | #[cfg(not(windows))] 177 | let output = Command::new("sh").arg("-c").arg(&command).output(); 178 | #[cfg(windows)] 179 | let output = Command::new("cmd").arg("/c").arg(&command).output(); 180 | 181 | let output = output.expect("failed to run git command"); 182 | 183 | assert!(output.status.success(), "command to get git remotes failed"); 184 | 185 | let remotes = String::from_utf8_lossy(&output.stdout).into_owned(); 186 | remotes.contains("upstream") 187 | } 188 | 189 | fn current_branch() -> String { 190 | let command = "git rev-parse --abbrev-ref HEAD"; 191 | #[cfg(not(windows))] 192 | let output = Command::new("sh").arg("-c").arg(&command).output(); 193 | #[cfg(windows)] 194 | let output = Command::new("cmd").arg("/c").arg(&command).output(); 195 | 196 | let output = output.expect("failed to run git command"); 197 | 198 | if output.status.success() { 199 | String::from_utf8_lossy(&output.stdout).into_owned() 200 | } else { 201 | // We assume that if this command fails it is because no commits exist in this 202 | // repository. In that case, use 'master' as a placeholder branch name. This is 203 | // unlikely to happen often in real-world usage, but it happens in integration tests. 204 | "master".into() 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /tests/cli.rs: -------------------------------------------------------------------------------- 1 | use assert_cmd::{crate_name, Command}; 2 | use assert_fs::{ 3 | fixture::{FileWriteStr, PathChild}, 4 | TempDir, 5 | }; 6 | use predicates::prelude::{predicate, PredicateStrExt}; 7 | 8 | use std::fs; 9 | 10 | type TestResult = Result<(), Box>; 11 | 12 | #[test] 13 | fn belay_in_non_git_dir() -> TestResult { 14 | let working_dir = TempDir::new()?; 15 | 16 | Command::cargo_bin(crate_name!())? 17 | .current_dir(working_dir.path()) 18 | .assert() 19 | .failure() 20 | .stderr(predicate::str::similar(r#"Error: "Failed to find git root""#).trim()); 21 | 22 | Ok(()) 23 | } 24 | 25 | #[test] 26 | fn belay_in_no_ci_dir() -> TestResult { 27 | let working_dir = TempDir::new()?; 28 | Command::new("git") 29 | .arg("init") 30 | .current_dir(working_dir.path()) 31 | .assert() 32 | .success(); 33 | 34 | Command::cargo_bin(crate_name!())? 35 | .current_dir(working_dir.path()) 36 | .assert() 37 | .failure() 38 | .stderr(predicate::str::similar(r#"Error: "Unable to find CI configuration""#).trim()); 39 | 40 | Ok(()) 41 | } 42 | 43 | #[test] 44 | fn belay_in_github_ci_dir_with_restricted_applicability() -> TestResult { 45 | let working_dir = TempDir::new()?; 46 | 47 | Command::new("git") 48 | .arg("init") 49 | .current_dir(working_dir.path()) 50 | .assert() 51 | .success(); 52 | fs::create_dir_all(working_dir.child(".github").child("workflows").path())?; 53 | let github_yaml = include_str!("./github_parse_check_on_push_to_branch.yml"); 54 | working_dir 55 | .child(".github") 56 | .child("workflows") 57 | .child("rust.yml") 58 | .write_str(github_yaml)?; 59 | 60 | // Should run while on master branch 61 | Command::cargo_bin(crate_name!())? 62 | .current_dir(working_dir.path()) 63 | .assert() 64 | .success() 65 | .stdout( 66 | predicate::str::similar( 67 | r#"Checking 'A Step': 68 | stepping 69 | Success! 70 | "#, 71 | ) 72 | .normalize(), 73 | ); 74 | 75 | // Should not run while on other branch 76 | Command::new("git") 77 | .arg("checkout") 78 | .arg("-b") 79 | .arg("develop") 80 | .current_dir(working_dir.path()) 81 | .assert() 82 | .success(); 83 | // add and commit here to allow our current branch name 84 | // lookup to work 85 | Command::new("git") 86 | .arg("add") 87 | .arg(".") 88 | .current_dir(working_dir.path()) 89 | .assert() 90 | .success(); 91 | Command::new("git") 92 | .arg("-c") 93 | .arg("user.name='Josh'") 94 | .arg("-c") 95 | .arg("user.email='Josh@email.com'") 96 | .arg("commit") 97 | .arg("-m") 98 | .arg("\"test commit\"") 99 | .current_dir(working_dir.path()) 100 | .assert() 101 | .success(); 102 | Command::cargo_bin(crate_name!())? 103 | .current_dir(working_dir.path()) 104 | .assert() 105 | .success() 106 | .stdout(predicate::str::similar("")); 107 | 108 | // should run if upstream is set, since the yml specifies it 109 | // is triggered on pull request 110 | Command::new("git") 111 | .arg("remote") 112 | .arg("add") 113 | .arg("upstream") 114 | .arg("test.com") 115 | .current_dir(working_dir.path()) 116 | .assert() 117 | .success(); 118 | Command::cargo_bin(crate_name!())? 119 | .current_dir(working_dir.path()) 120 | .assert() 121 | .success() 122 | .stdout( 123 | predicate::str::similar( 124 | r#"Checking 'A Step': 125 | stepping 126 | Success! 127 | "#, 128 | ) 129 | .normalize(), 130 | ); 131 | 132 | Ok(()) 133 | } 134 | #[test] 135 | fn belay_in_github_ci_dir() -> TestResult { 136 | let working_dir = TempDir::new()?; 137 | 138 | Command::new("git") 139 | .arg("init") 140 | .current_dir(working_dir.path()) 141 | .assert() 142 | .success(); 143 | fs::create_dir_all(working_dir.child(".github").child("workflows").path())?; 144 | let github_yaml = include_str!("./github_passing_integration_test.yml"); 145 | working_dir 146 | .child(".github") 147 | .child("workflows") 148 | .child("rust.yml") 149 | .write_str(github_yaml)?; 150 | 151 | Command::cargo_bin(crate_name!())? 152 | .current_dir(working_dir.path()) 153 | .assert() 154 | .success() 155 | .stdout( 156 | predicate::str::similar( 157 | r#"Checking 'Say hello': 158 | hello 159 | Success! 160 | Checking 'Say goodbye': 161 | goodbye 162 | Success! 163 | "#, 164 | ) 165 | .normalize(), 166 | ); 167 | 168 | Ok(()) 169 | } 170 | 171 | #[test] 172 | fn belay_in_github_ci_dir_with_multiple_workflows() -> TestResult { 173 | let working_dir = TempDir::new()?; 174 | 175 | Command::new("git") 176 | .arg("init") 177 | .current_dir(working_dir.path()) 178 | .assert() 179 | .success(); 180 | fs::create_dir_all(working_dir.child(".github").child("workflows").path())?; 181 | 182 | let github_yaml = include_str!("./github_passing_integration_test.yml"); 183 | working_dir 184 | .child(".github") 185 | .child("workflows") 186 | .child("rust.yml") 187 | .write_str(github_yaml)?; 188 | let github_yaml = include_str!("./github_failing_integration_test.yml"); 189 | working_dir 190 | .child(".github") 191 | .child("workflows") 192 | .child("rust2.yml") 193 | .write_str(github_yaml)?; 194 | 195 | // workflows should run in alphabetical order, and scripts which 196 | // are exactly the same should not be run again 197 | Command::cargo_bin(crate_name!())? 198 | .current_dir(working_dir.path()) 199 | .assert() 200 | .failure() 201 | .stdout( 202 | predicate::str::similar( 203 | r#"Checking 'Say hello': 204 | hello 205 | Success! 206 | Checking 'Say goodbye': 207 | goodbye 208 | Success! 209 | Checking 'tough test': 210 | "#, 211 | ) 212 | .normalize(), 213 | ) 214 | .stderr(predicate::str::similar("Error: \"Failed\"").trim()); 215 | 216 | Ok(()) 217 | } 218 | 219 | #[test] 220 | fn belay_in_gitlab_ci_dir() -> TestResult { 221 | let working_dir = TempDir::new()?; 222 | 223 | Command::new("git") 224 | .arg("init") 225 | .current_dir(working_dir.path()) 226 | .assert() 227 | .success(); 228 | let github_yaml = include_str!("./gitlab_passing_integration_test.yml"); 229 | working_dir.child(".gitlab-ci.yml").write_str(github_yaml)?; 230 | 231 | Command::cargo_bin(crate_name!())? 232 | .current_dir(working_dir.path()) 233 | .assert() 234 | .success() 235 | .stdout( 236 | predicate::str::similar( 237 | r#"Checking 'echo hello': 238 | hello 239 | Success! 240 | "#, 241 | ) 242 | .normalize(), 243 | ); 244 | 245 | Ok(()) 246 | } 247 | 248 | #[test] 249 | fn belay_in_github_ci_dir_fails() -> TestResult { 250 | let working_dir = TempDir::new()?; 251 | 252 | Command::new("git") 253 | .arg("init") 254 | .current_dir(working_dir.path()) 255 | .assert() 256 | .success(); 257 | fs::create_dir_all(working_dir.child(".github").child("workflows").path())?; 258 | let github_yaml = include_str!("./github_failing_integration_test.yml"); 259 | working_dir 260 | .child(".github") 261 | .child("workflows") 262 | .child("rust.yml") 263 | .write_str(github_yaml)?; 264 | 265 | Command::cargo_bin(crate_name!())? 266 | .current_dir(working_dir.path()) 267 | .assert() 268 | .failure() 269 | .stdout( 270 | predicate::str::similar( 271 | r#"Checking 'Say hello': 272 | hello 273 | Success! 274 | Checking 'tough test': 275 | "#, 276 | ) 277 | .normalize(), 278 | ) 279 | .stderr(predicate::str::similar("Error: \"Failed\"").trim()); 280 | 281 | Ok(()) 282 | } 283 | 284 | #[test] 285 | fn belay_hook_push() -> TestResult { 286 | let working_dir = TempDir::new()?; 287 | 288 | Command::new("git") 289 | .arg("init") 290 | .current_dir(working_dir.path()) 291 | .assert() 292 | .success(); 293 | 294 | Command::cargo_bin(crate_name!())? 295 | .arg("hook") 296 | .arg("push") 297 | .current_dir(working_dir.path()) 298 | .assert() 299 | .success() 300 | .stdout(predicate::str::similar("Created hook `.git/hooks/pre-push`").trim()); 301 | 302 | assert!(working_dir 303 | .child(".git") 304 | .child("hooks") 305 | .child("pre-push") 306 | .path() 307 | .exists()); 308 | 309 | Ok(()) 310 | } 311 | -------------------------------------------------------------------------------- /tests/github_failing_integration_test.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v1 12 | - name: Say hello 13 | run: echo hello 14 | - name: tough test 15 | run: exit 1 16 | -------------------------------------------------------------------------------- /tests/github_parse_check.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v1 12 | - name: Install dependencies 13 | run: sudo apt install -y nasm 14 | - name: Build 15 | run: cargo build 16 | - name: Run tests 17 | run: cargo test -- --nocapture 18 | - name: Clippy 19 | run: cargo clippy --all-targets --all-features -- -D warnings 20 | - name: Format 21 | run: cargo fmt --all -- --check 22 | -------------------------------------------------------------------------------- /tests/github_parse_check_on_push_to_branch.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: A Step 16 | run: echo stepping 17 | -------------------------------------------------------------------------------- /tests/github_passing_integration_test.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v1 12 | - name: Say hello 13 | run: echo hello 14 | - name: Say goodbye 15 | run: echo goodbye 16 | -------------------------------------------------------------------------------- /tests/gitlab_parse_check.yml: -------------------------------------------------------------------------------- 1 | image: rust:1.41 2 | 3 | stages: 4 | - main 5 | 6 | cache: 7 | paths: 8 | - target 9 | 10 | test: 11 | stage: main 12 | script: 13 | - rustup component add clippy 14 | - rustup component add rustfmt 15 | - cargo test 16 | - cargo clippy --all-targets --all-features -- -D warnings 17 | - cargo fmt --all -- --check 18 | -------------------------------------------------------------------------------- /tests/gitlab_passing_integration_test.yml: -------------------------------------------------------------------------------- 1 | image: rust:1.41 2 | 3 | stages: 4 | - main 5 | 6 | test: 7 | stage: main 8 | script: 9 | - echo hello 10 | --------------------------------------------------------------------------------