diff --git a/.github/workflows/CD.yml b/.github/workflows/CD.yml index 800a23b7..f507c48e 100644 --- a/.github/workflows/CD.yml +++ b/.github/workflows/CD.yml @@ -13,7 +13,7 @@ jobs: matrix: # This should work with only the `include`s, but it currently doesn't because of this bug: # https://github.community/t5/How-to-use-Git-and-GitHub/GitHub-Actions-Matrix-options-dont-work-as-documented/td-p/29558 - target: [x86_64-osx, x86_64-unknown-linux-musl, armv7-unknown-linux-musleabihf, armv7-linux-androideabi, aarch64-linux-android] + target: [x86_64-osx, x86_64-unknown-linux-musl, armv7-unknown-linux-musleabihf] include: - os: macos-latest target: x86_64-osx @@ -21,10 +21,6 @@ jobs: target: x86_64-unknown-linux-musl - os: ubuntu-latest target: armv7-unknown-linux-musleabihf - - os: ubuntu-latest - target: armv7-linux-androideabi - - os: ubuntu-latest - target: aarch64-linux-android steps: - name: Install libssl-dev diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c6ee9db..381644af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,49 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## [5.4.0](https://github.com/cocogitto/cocogitto/compare/5.3.1..5.4.0) - 2023-06-23 +#### Bug Fixes +- **(bump)** bump don't throw error on no bump types commits - ([4a6a8b3](https://github.com/cocogitto/cocogitto/commit/4a6a8b30aa4e9eddfe1b0a3fe0c7fd822e767447)) - Wassim Ahmed-Belkacem +- **(monorepo)** incorrect increment method used for package bumps - ([7bb3229](https://github.com/cocogitto/cocogitto/commit/7bb3229e356692dbf9eb932fdb5f42840414562e)) - [@oknozor](https://github.com/oknozor) +#### Continuous Integration +- **(formatting)** Apply cargo fmt - ([2710183](https://github.com/cocogitto/cocogitto/commit/2710183246d9f4bd43e3e6fa989f20f12cd57801)) - Mark S +- **(tests)** Add `no_coverage` support when using `llvm-cov` on nightly - ([97fd420](https://github.com/cocogitto/cocogitto/commit/97fd4208fc1dc483decfec6bc3ace85dc954c30b)) - Mark S +- remove android targets - ([1197d5f](https://github.com/cocogitto/cocogitto/commit/1197d5f98dc5e99f9bff82552b8dc813ab46ec33)) - [@oknozor](https://github.com/oknozor) +#### Documentation +- Update manpage generation docs - ([4a09837](https://github.com/cocogitto/cocogitto/commit/4a09837244e070ff6168cd247ed5621b41f4264e)) - [@tranzystorek-io](https://github.com/tranzystorek-io) +#### Features +- **(bump)** support annotated tags - ([363387d](https://github.com/cocogitto/cocogitto/commit/363387df62050d96bd2988a11e827cb720487395)) - [@bitfehler](https://github.com/bitfehler) +- **(check)** allow running check on commit range - ([5168f75](https://github.com/cocogitto/cocogitto/commit/5168f75323ed14d34f497a7d14c7f2fa71db1693)) - Sanchith Hegde +- **(cli)** add file parameter to verify command - ([5e02aef](https://github.com/cocogitto/cocogitto/commit/5e02aefc6a77302f6bbe505425f731ebbc5214c6)) - sebasnabas +- **(cli)** Added get-version feature (#248) - ([5670cd8](https://github.com/cocogitto/cocogitto/commit/5670cd81a4127f2a125fde4b4eb9d38da3e121ed)) - [@andre161292](https://github.com/andre161292) +- **(commit)** execute commit hooks when running `cog commit` - ([bf38fa6](https://github.com/cocogitto/cocogitto/commit/bf38fa6454d038c4d175943f7c47aeea2a433ec8)) - [@oknozor](https://github.com/oknozor) +- **(monorepo)** add config opt to disable global global-tag-opt (#264) - ([96aa3b6](https://github.com/cocogitto/cocogitto/commit/96aa3b678845548cb50ef56c40948a86450d0ad0)) - [@oknozor](https://github.com/oknozor) +- Add configurable changelog omission for custom commit types - ([88f8742](https://github.com/cocogitto/cocogitto/commit/88f874220e058709ad4a1f2f2c35eb4a0d67dc4c)) - Mark S +- add custom git-hook installation - ([39cba74](https://github.com/cocogitto/cocogitto/commit/39cba74ba21c9679a03667e0fa8cbc6057bdf967)) - [@oknozor](https://github.com/oknozor) +- reorganize manpages generation - ([1509583](https://github.com/cocogitto/cocogitto/commit/1509583ca058d8e43e4d02e603d10d13248723b0)) - [@tranzystorek-io](https://github.com/tranzystorek-io) +- add {{package}} to hook version dsl - ([af08a7e](https://github.com/cocogitto/cocogitto/commit/af08a7e86c714fcdd0977d283d4887f3e98b6aa2)) - [@oknozor](https://github.com/oknozor) +- add version_tag and latest_tag to hook version dsl - ([9eaee5a](https://github.com/cocogitto/cocogitto/commit/9eaee5abcc16bd8fa06c50b83e1892dde126f38f)) - [@oknozor](https://github.com/oknozor) +#### Miscellaneous Chores +- **(template)** remove deprecated usage of json_pointer - ([7266575](https://github.com/cocogitto/cocogitto/commit/72665753acb129d709df38f385aadef1b06e17b7)) - [@oknozor](https://github.com/oknozor) +- bump clap to v4.2.4 for v1.69 clippy lints - ([3c259b6](https://github.com/cocogitto/cocogitto/commit/3c259b6345691d4aa37f38e61d85f6a0441563af)) - sebasnabas +- fix clippy default impl for TemplateKind - ([808632a](https://github.com/cocogitto/cocogitto/commit/808632ae4405fec2e8365dfa89803a0e220cacd7)) - sebasnabas +- rustfmt - ([c2e7ab0](https://github.com/cocogitto/cocogitto/commit/c2e7ab01d89cdf454028e53453fcce5c1f98bc0c)) - [@oknozor](https://github.com/oknozor) +- add bitfehler to contributors list - ([b9d351d](https://github.com/cocogitto/cocogitto/commit/b9d351df3b69c77857b4a7ce1bf78c8f22b50b2e)) - [@oknozor](https://github.com/oknozor) +- rename codesee token - ([0b82758](https://github.com/cocogitto/cocogitto/commit/0b82758078370786f032ff2ca6cfeaefc5e5a20a)) - [@oknozor](https://github.com/oknozor) +- add codesee workflow - ([548c76e](https://github.com/cocogitto/cocogitto/commit/548c76ec764f633968b62ab6c115763a093bcaf6)) - [@oknozor](https://github.com/oknozor) +#### Refactoring +- **(cli)** adjust cog-verify args - ([8a12065](https://github.com/cocogitto/cocogitto/commit/8a120650dc3aeacf1c55e11429592074f19174ec)) - [@tranzystorek-io](https://github.com/tranzystorek-io) +#### Revert +- **(partial)** revert addition of 'no_coverage' support and attributes - ([93c9903](https://github.com/cocogitto/cocogitto/commit/93c990322cccb71f27cf97d6c6bfac70cfca613c)) - Mark S +#### Style +- remove unused `CommitConfig::{omit,include}` methods - ([3ad69eb](https://github.com/cocogitto/cocogitto/commit/3ad69ebd487968e17822442bb171476766184dff)) - Mark S +#### Tests +- **(check)** add CLI tests for running check on commit range - ([e276bfa](https://github.com/cocogitto/cocogitto/commit/e276bfaf60590f6fb8c3fa04227b76eba8c31c64)) - Sanchith Hegde +- **(check)** add tests for running check on commit range - ([754e54d](https://github.com/cocogitto/cocogitto/commit/754e54d5904a487edf746271e997e983c33b4d08)) - Sanchith Hegde +- **(ci)** add test for configurable changelog omission - ([c1b070c](https://github.com/cocogitto/cocogitto/commit/c1b070cffd0b7fe17c22f826c48c382dbcc69b80)) - Mark S +- **(coverage)** add test for CommitConfig::{omit, include} methods - ([dd4461d](https://github.com/cocogitto/cocogitto/commit/dd4461db52f4e607d070c4c7e1dab53641f768af)) - Mark S +- - - + ## [5.3.1](https://github.com/cocogitto/cocogitto/compare/5.3.0..5.3.1) - 2023-01-23 #### Bug Fixes - **(monorepo)** fix package tag parsing - ([cdff4a1](https://github.com/cocogitto/cocogitto/commit/cdff4a10332fb4caf4299f5f368e5a794f862228)) - [@oknozor](https://github.com/oknozor) diff --git a/Cargo.lock b/Cargo.lock index 21321364..e3c579b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20,6 +20,55 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is-terminal", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d" + +[[package]] +name = "anstyle-parse" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "anstyle-wincon" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" +dependencies = [ + "anstyle", + "windows-sys 0.48.0", +] + [[package]] name = "anyhow" version = "1.0.68" @@ -48,7 +97,7 @@ checksum = "eff18d764974428cf3a9328e23fc5c986f5fbed46e6cd4cdf42544df5d297ec1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -143,9 +192,9 @@ dependencies = [ [[package]] name = "chrono-tz" -version = "0.6.3" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c39203181991a7dd4343b8005bd804e7a9a37afb8ac070e43771e8c820bbde" +checksum = "58549f1842da3080ce63002102d5bc954c7bc843d4f47818e642abdc36253552" dependencies = [ "chrono", "chrono-tz-build", @@ -154,9 +203,9 @@ dependencies = [ [[package]] name = "chrono-tz-build" -version = "0.0.3" +version = "0.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f509c3a87b33437b05e2458750a0700e5bdd6956176773e6c7d6dd15a283a0c" +checksum = "db058d493fb2f65f41861bfed7e3fe6335264a9f0f92710cab5bdf01fef09069" dependencies = [ "parse-zoneinfo", "phf", @@ -165,17 +214,26 @@ dependencies = [ [[package]] name = "clap" -version = "4.1.1" +version = "4.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ec7a4128863c188deefe750ac1d1dfe66c236909f845af04beed823638dc1b2" +checksum = "8a1f23fa97e1d1641371b51f35535cb26959b8e27ab50d167a8b996b5bada819" dependencies = [ - "bitflags", + "clap_builder", "clap_derive", - "clap_lex", - "is-terminal", "once_cell", +] + +[[package]] +name = "clap_builder" +version = "4.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fdc5d93c358224b4d6867ef1356d740de2303e9892edc06c5340daeccd96bab" +dependencies = [ + "anstream", + "anstyle", + "bitflags", + "clap_lex", "strsim", - "termcolor", ] [[package]] @@ -199,25 +257,21 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.1.0" +version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "684a277d672e91966334af371f1a7b5833f9aa00b07c84e92fbce95e00208ce8" +checksum = "3f9644cd56d6b87dbe899ef8b053e331c0637664e9e21a33dfcdc36093f5c5c4" dependencies = [ "heck", - "proc-macro-error", "proc-macro2", "quote", - "syn", + "syn 2.0.15", ] [[package]] name = "clap_lex" -version = "0.3.1" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "783fe232adfca04f90f56201b26d79682d4cd2625e0bc7290b95123afe558ade" -dependencies = [ - "os_str_bytes", -] +checksum = "8a2dd5a6fe8c6e3502f568a6353e5273bbb15193ad9a89e457b9970798efbea1" [[package]] name = "clap_mangen" @@ -251,12 +305,12 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] name = "cocogitto" -version = "5.3.1" +version = "5.4.0" dependencies = [ "anyhow", "assert_cmd", @@ -303,6 +357,12 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + [[package]] name = "colored" version = "2.0.0" @@ -370,7 +430,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096" dependencies = [ "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -397,7 +457,7 @@ dependencies = [ "proc-macro2", "quote", "scratch", - "syn", + "syn 1.0.107", ] [[package]] @@ -414,7 +474,7 @@ checksum = "65e07508b90551e610910fa648a1878991d367064997a596135b86df30daf07e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -475,13 +535,13 @@ checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" [[package]] name = "errno" -version = "0.2.8" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" dependencies = [ "errno-dragonfly", "libc", - "winapi", + "windows-sys 0.48.0", ] [[package]] @@ -630,18 +690,18 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.2.6" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" -dependencies = [ - "libc", -] +checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" [[package]] name = "humansize" -version = "1.1.1" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02296996cb8796d7c6e3bc2d9211b7802812d36999a51bb754123ead7d37d026" +checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" +dependencies = [ + "libm", +] [[package]] name = "iana-time-zone" @@ -716,19 +776,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7d6c6f8c91b4b9ed43484ad1a938e393caf35960fce7f82a040497207bd8e9e" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.42.0", ] [[package]] name = "is-terminal" -version = "0.4.2" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28dfb6c8100ccc63462345b67d1bbc3679177c75ee4bf59bf29c8b1d110b8189" +checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" dependencies = [ - "hermit-abi 0.2.6", + "hermit-abi 0.3.1", "io-lifetimes", "rustix", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -788,6 +848,12 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "libm" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" + [[package]] name = "libz-sys" version = "1.1.8" @@ -811,9 +877,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.1.4" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" +checksum = "b64f40e5e03e0d54f03845c8197d0291253cdbedfb1cb46b13c2c117554a9f4c" [[package]] name = "log" @@ -944,12 +1010,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "os_str_bytes" -version = "6.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" - [[package]] name = "output_vt100" version = "0.1.3" @@ -982,9 +1042,9 @@ checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" [[package]] name = "pest" -version = "2.5.3" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4257b4a04d91f7e9e6290be5d3da4804dd5784fafde3a497d73eb2b4a158c30a" +checksum = "e68e84bfb01f0507134eac1e9b410a12ba379d064eab48c50ba4ce329a527b70" dependencies = [ "thiserror", "ucd-trie", @@ -992,9 +1052,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.5.3" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241cda393b0cdd65e62e07e12454f1f25d57017dcc514b1514cd3c4645e3a0a6" +checksum = "6b79d4c71c865a25a4322296122e3924d30bc8ee0834c8bfc8b95f7f054afbfb" dependencies = [ "pest", "pest_generator", @@ -1002,22 +1062,22 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.5.3" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46b53634d8c8196302953c74d5352f33d0c512a9499bd2ce468fc9f4128fa27c" +checksum = "6c435bf1076437b851ebc8edc3a18442796b30f1728ffea6262d59bbe28b077e" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn", + "syn 2.0.15", ] [[package]] name = "pest_meta" -version = "2.5.3" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ef4f1332a8d4678b41966bb4cc1d0676880e84183a1ecc3f4b69f03e99c7a51" +checksum = "745a452f8eb71e39ffd8ee32b3c5f51d03845f99786fa9b68db6ff509c505411" dependencies = [ "once_cell", "pest", @@ -1026,18 +1086,18 @@ dependencies = [ [[package]] name = "phf" -version = "0.11.1" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "928c6535de93548188ef63bb7c4036bd415cd8f36ad25af44b9789b2ee72a48c" +checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" dependencies = [ "phf_shared", ] [[package]] name = "phf_codegen" -version = "0.11.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56ac890c5e3ca598bbdeaa99964edb5b0258a583a9eb6ef4e89fc85d9224770" +checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" dependencies = [ "phf_generator", "phf_shared", @@ -1045,9 +1105,9 @@ dependencies = [ [[package]] name = "phf_generator" -version = "0.11.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1181c94580fa345f50f19d738aaa39c0ed30a600d95cb2d3e23f94266f14fbf" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" dependencies = [ "phf_shared", "rand 0.8.5", @@ -1055,9 +1115,9 @@ dependencies = [ [[package]] name = "phf_shared" -version = "0.11.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1fb5f6f826b772a8d4c0394209441e7d37cbbb967ae9c7e0e8134365c9ee676" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" dependencies = [ "siphasher", "uncased", @@ -1136,7 +1196,7 @@ dependencies = [ "proc-macro-error-attr", "proc-macro2", "quote", - "syn", + "syn 1.0.107", "version_check", ] @@ -1153,9 +1213,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.50" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ef7d57beacfaf2d8aee5937dab7b7f28de3cb8b1828479bb5de2a7106f2bae2" +checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" dependencies = [ "unicode-ident", ] @@ -1168,9 +1228,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quote" -version = "1.0.23" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" dependencies = [ "proc-macro2", ] @@ -1295,16 +1355,16 @@ checksum = "b833d8d034ea094b1ea68aa6d5c740e0d04bad9d16568d08ba6f76823a114316" [[package]] name = "rustix" -version = "0.36.7" +version = "0.37.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fdebc4b395b7fbb9ab11e462e20ed9051e7b16e42d24042c776eca0ac81b03" +checksum = "62b24138615de35e32031d041a09032ef3487a616d901ca4db224e7d557efae2" dependencies = [ "bitflags", "errno", "io-lifetimes", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.45.0", ] [[package]] @@ -1359,7 +1419,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b672e005ae58fef5da619d90b9f1c5b44b061890f4a371b3c96257a8a15e697" dependencies = [ "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -1385,7 +1445,7 @@ checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -1470,6 +1530,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "2.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "tempfile" version = "3.3.0" @@ -1486,9 +1557,9 @@ dependencies = [ [[package]] name = "tera" -version = "1.17.1" +version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3df578c295f9ec044ff1c829daf31bb7581d5b3c2a7a3d87419afe1f2531438c" +checksum = "95a665751302f22a03c56721e23094e4dc22b04a80f381e6737a07bf7a7c70c0" dependencies = [ "chrono", "chrono-tz", @@ -1503,6 +1574,7 @@ dependencies = [ "serde", "serde_json", "slug", + "thread_local", "unic-segment", ] @@ -1538,7 +1610,7 @@ checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -1694,6 +1766,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + [[package]] name = "vcpkg" version = "0.2.15" @@ -1765,7 +1843,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn", + "syn 1.0.107", "wasm-bindgen-shared", ] @@ -1787,7 +1865,7 @@ checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -1846,13 +1924,61 @@ version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.42.1", + "windows_aarch64_msvc 0.42.1", + "windows_i686_gnu 0.42.1", + "windows_i686_msvc 0.42.1", + "windows_x86_64_gnu 0.42.1", + "windows_x86_64_gnullvm 0.42.1", + "windows_x86_64_msvc 0.42.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.1", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.0", +] + +[[package]] +name = "windows-targets" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" +dependencies = [ + "windows_aarch64_gnullvm 0.42.1", + "windows_aarch64_msvc 0.42.1", + "windows_i686_gnu 0.42.1", + "windows_i686_msvc 0.42.1", + "windows_x86_64_gnu 0.42.1", + "windows_x86_64_gnullvm 0.42.1", + "windows_x86_64_msvc 0.42.1", +] + +[[package]] +name = "windows-targets" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +dependencies = [ + "windows_aarch64_gnullvm 0.48.0", + "windows_aarch64_msvc 0.48.0", + "windows_i686_gnu 0.48.0", + "windows_i686_msvc 0.48.0", + "windows_x86_64_gnu 0.48.0", + "windows_x86_64_gnullvm 0.48.0", + "windows_x86_64_msvc 0.48.0", ] [[package]] @@ -1861,42 +1987,84 @@ version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + [[package]] name = "windows_aarch64_msvc" version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + [[package]] name = "windows_i686_gnu" version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + [[package]] name = "windows_i686_msvc" version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + [[package]] name = "windows_x86_64_gnu" version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + [[package]] name = "windows_x86_64_msvc" version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" + [[package]] name = "yansi" version = "0.5.1" diff --git a/Cargo.toml b/Cargo.toml index 4a8f1876..6cb4b9f2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cocogitto" -version = "5.3.1" +version = "5.4.0" authors = ["Paul Delafosse "] edition = "2021" readme = "README.md" @@ -34,14 +34,14 @@ shell-words = "^1" which = "^4" once_cell = "^1" toml = "^0" -clap = { version = "4.0", optional = true, features = ["derive"] } +clap = { version = "4.2.4", optional = true, features = ["derive", "string"] } clap_complete = { version = "4.0", optional = true } clap_mangen = { version = "0.2", optional = true } clap_complete_nushell = { version = "0.1.8", optional = true } conventional_commit_parser = "0.9.4" pest = "2.1.3" pest_derive = "2.1.0" -tera = "1.15.0" +tera = "1.18.1" globset = "0.4.8" log = "0.4.16" stderrlog = "0.5.1" diff --git a/cog.toml b/cog.toml index aa8bdd05..6b0b779a 100644 --- a/cog.toml +++ b/cog.toml @@ -1,4 +1,4 @@ -branch_whitelist = [ "main" ] +branch_whitelist = ["main"] ignore_merge_commits = true # A list of command to run BEFORE creating a version. @@ -61,5 +61,7 @@ authors = [ { signature = "darlaam", username = "darlaam" }, { signature = "Stephen Connolly", username = "stephenc" }, { signature = "Luca Trevisani", username = "lucatrv" }, - { signature = "Racci", username = "DaRacci" } + { signature = "Racci", username = "DaRacci" }, + { signature = "Conrad Hoffmann", username = "bitfehler" }, + { signature = "andre161292", username = "andre161292" } ] diff --git a/docs/Packaging.md b/docs/Packaging.md index 7c55bbfc..e0c8176e 100644 --- a/docs/Packaging.md +++ b/docs/Packaging.md @@ -2,15 +2,11 @@ ## Generating manpages -`cog` has a hidden subcommand to print generated manpage to STDOUT. To generate the main manpage, run: +`cog` has a hidden subcommand to create manpages in a specified directory: +```console +cog generate-manpages "${PWD}/gen" ``` -cog generate-manpage cog > cog.1 -``` - -You can also generate manpages for subcommands by specifying the subcommand name as argument, e.g.: -``` -cog generate-manpage commit > cog-commit.1 -cog generate-manpage install-hook > cog-install-hook.1 -``` +This creates the directory if it doesn't exist already (like `mkdir -p` on Linux), +then outputs generated manpages for `cog` as well as its subcommands into files in that directory. diff --git a/src/bin/cog/commit.rs b/src/bin/cog/commit.rs index 7009a6d4..eda8acb2 100644 --- a/src/bin/cog/commit.rs +++ b/src/bin/cog/commit.rs @@ -1,4 +1,6 @@ use std::fmt::Write; +use std::path::Path; +use std::{fs, io}; use cocogitto::COMMITS_METADATA; @@ -15,14 +17,23 @@ pub fn commit_types() -> PossibleValuesParser { types.into() } -pub fn edit_message( +pub fn prepare_edit_message>( typ: &str, message: &str, scope: Option<&str>, breaking: bool, -) -> Result<(Option, Option, bool)> { + path: P, +) -> io::Result { let template = prepare_edit_template(typ, message, scope, breaking); + fs::write(path, &template)?; + Ok(template) +} +pub fn edit_message>( + path: P, + breaking: bool, +) -> Result<(Option, Option, bool)> { + let template = fs::read_to_string(path.as_ref())?; let edited = edit::edit(template)?; if edited.lines().all(|line| { @@ -75,10 +86,10 @@ fn prepare_header(typ: &str, message: &str, scope: Option<&str>) -> String { let mut header = typ.to_string(); if let Some(scope) = scope { - write!(&mut header, "({})", scope).unwrap(); + write!(&mut header, "({scope})").unwrap(); } - write!(&mut header, ": {}", message).unwrap(); + write!(&mut header, ": {message}").unwrap(); header } @@ -93,8 +104,7 @@ fn prepare_edit_template(typ: &str, message: &str, scope: Option<&str>, breaking write!( &mut template, - "{}\n\n# Message body\n\n\n# Message footer\n# For example, foo: bar\n\n\n", - header + "{header}\n\n# Message body\n\n\n# Message footer\n# For example, foo: bar\n\n\n" ) .unwrap(); diff --git a/src/bin/cog/main.rs b/src/bin/cog/main.rs index eae19274..f919eade 100644 --- a/src/bin/cog/main.rs +++ b/src/bin/cog/main.rs @@ -1,21 +1,24 @@ mod commit; +mod mangen; +use std::fs; use std::path::PathBuf; use cocogitto::conventional::changelog::template::{RemoteContext, Template}; use cocogitto::conventional::commit as conv_commit; use cocogitto::conventional::version::IncrementCommand; -use cocogitto::git::hook::HookKind; use cocogitto::git::revspec::RevspecPattern; use cocogitto::log::filter::{CommitFilter, CommitFilters}; use cocogitto::log::output::Output; -use cocogitto::{CocoGitto, SETTINGS}; +use cocogitto::{CocoGitto, CommitHook, SETTINGS}; -use anyhow::{Context, Result}; +use crate::commit::prepare_edit_message; +use anyhow::{bail, Context, Result}; use clap::builder::{PossibleValue, PossibleValuesParser}; use clap::{ArgAction, ArgGroup, Args, CommandFactory, Parser, Subcommand, ValueEnum}; use clap_complete::{shells, Generator}; use clap_complete_nushell::Nushell; +use cocogitto::settings::GitHookType; fn hook_profiles() -> PossibleValuesParser { let profiles = SETTINGS @@ -26,6 +29,15 @@ fn hook_profiles() -> PossibleValuesParser { profiles.into() } +fn git_hook_types() -> PossibleValuesParser { + let hooks = SETTINGS + .git_hooks + .keys() + .map(|hook_type| hook_type.to_string()); + + hooks.into() +} + fn packages() -> PossibleValuesParser { let profiles = SETTINGS.packages.keys().map(|profile| -> &str { profile }); @@ -124,12 +136,16 @@ enum Command { /// Verify all commit messages against the conventional commit specification Check { /// Check commit history, starting from the latest tag to HEAD - #[arg(short = 'l', long)] + #[arg(short = 'l', long, group = "commit_range")] from_latest_tag: bool, /// Ignore merge commits messages #[arg(short, long)] ignore_merge_commits: bool, + + /// Check commits in the specified range + #[arg(group = "commit_range")] + range: Option, }, /// Create a new conventional commit @@ -166,9 +182,15 @@ enum Command { }, /// Verify a single commit message + #[command(group = ArgGroup::new("verify_input").required(true))] Verify { /// The commit message - message: String, + #[arg(group = "verify_input")] + message: Option, + + /// Read message from the specified file + #[arg(short, long, group = "verify_input")] + file: Option, /// Ignore merge commit messages #[arg(short, long)] @@ -187,7 +209,7 @@ enum Command { /// Generate the changelog with the given template. /// - /// Possible values are 'remote', 'full_hash', 'default' or the path to your template. + /// Possible values are 'remote', 'full_hash', 'default' or the path to your template. /// If not specified cog will use cog.toml template config or fallback to 'default'. #[arg(long, short)] template: Option, @@ -205,6 +227,17 @@ enum Command { repository: Option, }, + /// Get current version + GetVersion { + /// Fallback version. Has to be semver compliant. + #[arg(short, long)] + fallback: Option, + + /// Specify which package to get the version for in a monorepo. + #[arg(long, value_parser = packages())] + package: Option, + }, + /// Commit changelog from latest tag to HEAD and create new tag #[command(group = ArgGroup::new("bump-spec").required(true))] Bump { @@ -240,6 +273,10 @@ enum Command { #[arg(long, value_parser = packages())] package: Option, + /// Annotate tag with given message + #[arg(short = 'A', long)] + annotated: Option, + /// Dry-run: print the target version. No action taken #[arg(short, long)] dry_run: bool, @@ -255,8 +292,11 @@ enum Command { /// Add git hooks to the repository InstallHook { /// Type of hook to install - #[arg(value_parser = ["commit-msg", "pre-push", "all"])] - hook_type: String, + #[arg(value_parser = git_hook_types(), group = "git-hooks")] + hook_type: Vec, + /// Install all git-hooks + #[arg(short, long, group = "git-hooks")] + all: bool, }, /// Generate shell completions @@ -268,7 +308,7 @@ enum Command { /// Generate manpage #[command(hide = true)] - GenerateManpage { cmd: String }, + GenerateManpages { output_dir: PathBuf }, } #[derive(Args)] @@ -302,6 +342,10 @@ fn main() -> Result<()> { init_logs(cli.verbose, cli.quiet); match cli.command { + Command::GetVersion { fallback, package } => { + let cocogitto = CocoGitto::get()?; + cocogitto.get_latest_version(fallback, package)? + } Command::Bump { version, auto, @@ -311,21 +355,30 @@ fn main() -> Result<()> { pre, hook_profile, package, + annotated, dry_run, } => { let mut cocogitto = CocoGitto::get()?; + let is_monorepo = !SETTINGS.packages.is_empty(); let increment = match version { Some(version) => IncrementCommand::Manual(version), - None if auto => IncrementCommand::Auto, + None if auto => match package.as_ref() { + Some(package) => { + if !is_monorepo { + bail!("Cannot create package version on non mono-repository config") + }; + + IncrementCommand::AutoPackage(package.to_owned()) + } + None => IncrementCommand::Auto, + }, None if major => IncrementCommand::Major, None if minor => IncrementCommand::Minor, None if patch => IncrementCommand::Patch, _ => unreachable!(), }; - let is_monorepo = !SETTINGS.packages.is_empty(); - if is_monorepo { match package { Some(package_name) => { @@ -336,6 +389,7 @@ fn main() -> Result<()> { increment, pre.as_deref(), hook_profile.as_deref(), + annotated, dry_run, )? } @@ -343,6 +397,7 @@ fn main() -> Result<()> { increment, pre.as_deref(), hook_profile.as_deref(), + annotated, dry_run, )?, } @@ -351,12 +406,14 @@ fn main() -> Result<()> { increment, pre.as_deref(), hook_profile.as_deref(), + annotated, dry_run, )? } } Command::Verify { message, + file, ignore_merge_commits, } => { let ignore_merge_commits = ignore_merge_commits || SETTINGS.ignore_merge_commits; @@ -364,16 +421,33 @@ fn main() -> Result<()> { .map(|cogito| cogito.get_committer().unwrap()) .ok(); - conv_commit::verify(author, &message, ignore_merge_commits)?; + let commit_message = match (message, file) { + (Some(message), None) => message, + (None, Some(file_path)) => { + if !file_path.exists() { + bail!("File {file_path:#?} does not exist"); + } + + match fs::read_to_string(file_path) { + Err(e) => bail!("Could not read the file ({e})"), + Ok(msg) => msg, + } + } + (None, None) => unreachable!(), + (Some(_), Some(_)) => unreachable!(), + }; + + conv_commit::verify(author, &commit_message, ignore_merge_commits)?; } Command::Check { from_latest_tag, ignore_merge_commits, + range, } => { let cocogitto = CocoGitto::get()?; let from_latest_tag = from_latest_tag || SETTINGS.from_latest_tag; let ignore_merge_commits = ignore_merge_commits || SETTINGS.ignore_merge_commits; - cocogitto.check(from_latest_tag, ignore_merge_commits)?; + cocogitto.check(from_latest_tag, ignore_merge_commits, range)?; } Command::Edit { from_latest_tag } => { let cocogitto = CocoGitto::get()?; @@ -458,35 +532,30 @@ fn main() -> Result<()> { changelog.into_markdown(template)? } }; - println!("{}", result); + println!("{result}"); } Command::Init { path } => { cocogitto::command::init::init(&path)?; } - Command::InstallHook { hook_type } => { + Command::InstallHook { + hook_type: hook_types, + all, + } => { let cocogitto = CocoGitto::get()?; - match hook_type.as_str() { - "commit-msg" => cocogitto.install_hook(HookKind::PrepareCommit)?, - "pre-push" => cocogitto.install_hook(HookKind::PrePush)?, - "all" => cocogitto.install_hook(HookKind::All)?, - _ => unreachable!(), - } + if all { + cocogitto.install_all_hooks()?; + return Ok(()); + }; + + let hook_types = hook_types.into_iter().map(GitHookType::from).collect(); + + cocogitto.install_git_hooks(hook_types)?; } Command::GenerateCompletions { shell } => { clap_complete::generate(shell, &mut Cli::command(), "cog", &mut std::io::stdout()); } - Command::GenerateManpage { cmd } => { - let mut cog_cmd = Cli::command(); - cog_cmd = cog_cmd.disable_help_subcommand(true); - let cmd = match cmd.as_str() { - "cog" => cog_cmd, - cmd => cog_cmd - .find_subcommand(cmd) - .expect("Requested non-existent subcommand") - .clone(), - }; - let man = clap_mangen::Man::new(cmd); - man.render(&mut std::io::stdout())?; + Command::GenerateManpages { output_dir } => { + mangen::generate_manpages(&output_dir)?; } Command::Commit(CommitArgs { typ, @@ -497,13 +566,24 @@ fn main() -> Result<()> { sign, }) => { let cocogitto = CocoGitto::get()?; + cocogitto.run_commit_hook(CommitHook::PreCommit)?; + let commit_message_path = cocogitto.prepare_edit_message_path(); + let template = prepare_edit_message( + &typ, + &message, + scope.as_deref(), + breaking_change, + &commit_message_path, + )?; + cocogitto.run_commit_hook(CommitHook::PrepareCommitMessage(template))?; + let (body, footer, breaking) = if edit { - commit::edit_message(&typ, &message, scope.as_deref(), breaking_change)? + commit::edit_message(&commit_message_path, breaking_change)? } else { (None, None, breaking_change) }; - cocogitto.conventional_commit(&typ, scope, message, body, footer, breaking, sign)?; + cocogitto.run_commit_hook(CommitHook::PostCommit)?; } } diff --git a/src/bin/cog/mangen.rs b/src/bin/cog/mangen.rs new file mode 100644 index 00000000..5b00b8bd --- /dev/null +++ b/src/bin/cog/mangen.rs @@ -0,0 +1,37 @@ +use std::io::Result as IoResult; +use std::path::Path; + +use clap::{Command, CommandFactory}; +use clap_mangen::Man; + +use crate::Cli; + +pub fn generate_manpages(out_dir: &Path) -> IoResult<()> { + std::fs::create_dir_all(out_dir)?; + + let cog = Cli::command(); + + for mut subcmd in cog.get_subcommands().filter(|c| !c.is_hide_set()).cloned() { + let name = subcmd.get_name(); + let full_name = format!("cog-{}", name); + let man_name = format!("{}.1", full_name); + + subcmd = subcmd.name(&full_name); + + render_command(&out_dir.join(&man_name), subcmd)?; + } + + render_command(&out_dir.join("cog.1"), cog)?; + + Ok(()) +} + +fn render_command(file: &Path, cmd: Command) -> IoResult<()> { + let man = Man::new(cmd); + let mut buffer = Vec::new(); + + man.render(&mut buffer)?; + std::fs::write(file, buffer)?; + + Ok(()) +} diff --git a/src/command/bump/mod.rs b/src/command/bump/mod.rs index f4685693..1dc54bae 100644 --- a/src/command/bump/mod.rs +++ b/src/command/bump/mod.rs @@ -1,11 +1,10 @@ use crate::conventional::changelog::release::Release; use crate::conventional::commit::Commit; use crate::git::error::TagError; -use crate::git::hook::Hooks; use crate::git::oid::OidOf; use crate::git::revspec::RevspecPattern; use crate::git::tag::Tag; -use crate::hook::{Hook, HookVersion}; +use crate::hook::{Hook, HookVersion, Hooks}; use crate::settings::{HookType, MonoRepoPackage, Settings}; use crate::BumpError; use crate::{CocoGitto, SETTINGS}; @@ -24,15 +23,77 @@ mod monorepo; mod package; mod standard; +struct HookRunOptions<'a> { + hook_type: HookType, + current_tag: Option<&'a HookVersion>, + next_version: Option<&'a HookVersion>, + hook_profile: Option<&'a str>, + package_name: Option<&'a str>, + package: Option<&'a MonoRepoPackage>, +} + +impl<'a> HookRunOptions<'a> { + pub fn post_bump() -> Self { + Self { + hook_type: HookType::PostBump, + current_tag: None, + next_version: None, + hook_profile: None, + package_name: None, + package: None, + } + } + + pub fn pre_bump() -> Self { + Self { + hook_type: HookType::PreBump, + current_tag: None, + next_version: None, + hook_profile: None, + package_name: None, + package: None, + } + } + + pub fn current_tag<'b>(mut self, version: Option<&'b HookVersion>) -> Self + where + 'b: 'a, + { + self.current_tag = version; + self + } + + pub fn next_version<'b>(mut self, version: &'b HookVersion) -> Self + where + 'b: 'a, + { + self.next_version = Some(version); + self + } + + pub fn hook_profile<'b>(mut self, profile: Option<&'b str>) -> Self + where + 'b: 'a, + { + self.hook_profile = profile; + self + } + + pub fn package<'b>(mut self, name: &'b str, package: &'b MonoRepoPackage) -> Self + where + 'b: 'a, + { + self.package_name = Some(name); + self.package = Some(package); + self + } +} + fn ensure_tag_is_greater_than_previous(current: &Tag, next: &Tag) -> Result<()> { - if next <= current { - let comparison = format!("{} <= {}", current, next).red(); + if next < current { + let comparison = format!("{current} <= {next}").red(); let cause_key = "cause:".red(); - let cause = format!( - "{} version MUST be greater than current one: {}", - cause_key, comparison - ); - + let cause = format!("{cause_key} version MUST be greater than current one: {comparison}"); bail!("{}:\n\t{}\n", "SemVer Error".red().to_string(), cause); }; @@ -151,33 +212,24 @@ impl CocoGitto { Ok(release) } - fn run_hooks( - &self, - hook_type: HookType, - current_tag: Option<&HookVersion>, - next_version: &HookVersion, - hook_profile: Option<&str>, - package_name: Option<&str>, - package: Option<&MonoRepoPackage>, - ) -> Result<()> { + fn run_hooks(&self, options: HookRunOptions) -> Result<()> { let settings = Settings::get(&self.repository)?; - let hooks: Vec = match (package, hook_profile) { + let hooks: Vec = match (options.package, options.hook_profile) { (None, Some(profile)) => settings - .get_profile_hooks(profile, hook_type) + .get_profile_hooks(profile, options.hook_type) .iter() .map(|s| s.parse()) .enumerate() .map(|(idx, result)| { result.context(format!( - "Cannot parse bump profile {} hook at index {}", - profile, idx + "Cannot parse bump profile {profile} hook at index {idx}" )) }) .try_collect()?, (Some(package), Some(profile)) => { - let hooks = package.get_profile_hooks(profile, hook_type); + let hooks = package.get_profile_hooks(profile, options.hook_type); hooks .iter() @@ -185,35 +237,34 @@ impl CocoGitto { .enumerate() .map(|(idx, result)| { result.context(format!( - "Cannot parse bump profile {} hook at index {}", - profile, idx + "Cannot parse bump profile {profile} hook at index {idx}" )) }) .try_collect()? } (Some(package), None) => package - .get_hooks(hook_type) + .get_hooks(options.hook_type) .iter() .map(|s| s.parse()) .enumerate() - .map(|(idx, result)| result.context(format!("Cannot parse hook at index {}", idx))) + .map(|(idx, result)| result.context(format!("Cannot parse hook at index {idx}"))) .try_collect()?, (None, None) => settings - .get_hooks(hook_type) + .get_hooks(options.hook_type) .iter() .map(|s| s.parse()) .enumerate() - .map(|(idx, result)| result.context(format!("Cannot parse hook at index {}", idx))) + .map(|(idx, result)| result.context(format!("Cannot parse hook at index {idx}"))) .try_collect()?, }; if !hooks.is_empty() { - let hook_type = match hook_type { + let hook_type = match options.hook_type { HookType::PreBump => "pre-bump", HookType::PostBump => "post-bump", }; - match package_name { + match options.package_name { None => { let msg = format!("[{hook_type}]").underline().white().bold(); info!("{msg}") @@ -229,7 +280,7 @@ impl CocoGitto { } for mut hook in hooks { - hook.insert_versions(current_tag, next_version)?; + hook.insert_versions(options.current_tag, options.next_version)?; let command = hook.to_string(); let command = if command.chars().count() > 78 { &command[0..command.len()] @@ -237,7 +288,7 @@ impl CocoGitto { &command }; info!("[{command}]"); - let package_path = package.map(|p| p.path.as_path()); + let package_path = options.package.map(|p| p.path.as_path()); hook.run(package_path).context(hook.to_string())?; println!(); } diff --git a/src/command/bump/monorepo.rs b/src/command/bump/monorepo.rs index a24d8595..4ca58c80 100644 --- a/src/command/bump/monorepo.rs +++ b/src/command/bump/monorepo.rs @@ -1,4 +1,6 @@ -use crate::command::bump::{ensure_tag_is_greater_than_previous, tag_or_fallback_to_zero}; +use crate::command::bump::{ + ensure_tag_is_greater_than_previous, tag_or_fallback_to_zero, HookRunOptions, +}; use crate::conventional::changelog::template::{ MonoRepoContext, PackageBumpContext, PackageContext, @@ -9,13 +11,13 @@ use crate::conventional::version::{Increment, IncrementCommand}; use crate::git::tag::Tag; use crate::hook::HookVersion; -use crate::settings::HookType; use crate::{settings, CocoGitto, SETTINGS}; use anyhow::Result; use colored::*; -use log::info; +use log::{info, warn}; use semver::Prerelease; +use tera::Tera; use crate::conventional::error::BumpError; use crate::git::oid::OidOf; @@ -41,26 +43,104 @@ impl CocoGitto { increment: IncrementCommand, pre_release: Option<&str>, hooks_config: Option<&str>, + annotated: Option, dry_run: bool, ) -> Result<()> { match increment { IncrementCommand::Auto => { - self.create_monorepo_version_auto(pre_release, hooks_config, dry_run) + if SETTINGS.generate_mono_repository_global_tag { + self.create_monorepo_version_auto(pre_release, hooks_config, annotated, dry_run) + } else { + if annotated.is_some() { + warn!("--annotated flag is not supported for package bumps without a global tag"); + } + self.create_all_package_version_auto(pre_release, hooks_config, dry_run) + } } - _ => self.create_monorepo_version_manual(increment, pre_release, hooks_config, dry_run), + _ => self.create_monorepo_version_manual( + increment, + pre_release, + hooks_config, + annotated, + dry_run, + ), + } + } + + pub fn create_all_package_version_auto( + &mut self, + pre_release: Option<&str>, + hooks_config: Option<&str>, + dry_run: bool, + ) -> Result<()> { + self.pre_bump_checks()?; + // Get package bumps + let bumps = self.get_packages_bumps(pre_release)?; + + if bumps.is_empty() { + print!("No conventional commits found for your packages that required a bump. Changelogs will be updated on the next bump.\nPre-Hooks and Post-Hooks have been skiped.\n"); + return Ok(()); + } + + if dry_run { + for bump in bumps { + println!("{}", bump.new_version.prefixed_tag) + } + return Ok(()); + } + + let hook_result = self.run_hooks(HookRunOptions::pre_bump().hook_profile(hooks_config)); + + self.repository.add_all()?; + self.unwrap_or_stash_and_exit(&Tag::default(), hook_result); + self.bump_packages(pre_release, hooks_config, &bumps)?; + + let sign = self.repository.gpg_sign(); + self.repository + .commit("chore(version): bump packages", sign)?; + + for bump in &bumps { + self.repository.create_tag(&bump.new_version.prefixed_tag)?; + } + + // Run per package post hooks + for bump in bumps { + let package = SETTINGS + .packages + .get(&bump.package_name) + .expect("package exists"); + + self.run_hooks( + HookRunOptions::post_bump() + .current_tag(bump.old_version.as_ref()) + .next_version(&bump.new_version) + .hook_profile(hooks_config) + .package(&bump.package_name, package), + )?; } + + // Run global post hooks + self.run_hooks(HookRunOptions::post_bump().hook_profile(hooks_config))?; + + Ok(()) } fn create_monorepo_version_auto( &mut self, pre_release: Option<&str>, hooks_config: Option<&str>, + annotated: Option, dry_run: bool, ) -> Result<()> { self.pre_bump_checks()?; // Get package bumps let bumps = self.get_packages_bumps(pre_release)?; + if bumps.is_empty() { + print!("No conventional commits found for your packages that required a bump. Changelogs will be updated on the next bump.\nPre-Hooks and Post-Hooks have been skiped.\n"); + return Ok(()); + } + // Get the greatest package increment among public api packages let increment_from_package_bumps = bumps .iter() @@ -87,7 +167,7 @@ impl CocoGitto { for bump in bumps { println!("{}", bump.new_version.prefixed_tag) } - print!("{}", tag); + print!("{tag}"); return Ok(()); } @@ -134,12 +214,10 @@ impl CocoGitto { let next_version = HookVersion::new(tag.clone()); let hook_result = self.run_hooks( - HookType::PreBump, - current.as_ref(), - &next_version, - hooks_config, - None, - None, + HookRunOptions::pre_bump() + .current_tag(current.as_ref()) + .next_version(&next_version) + .hook_profile(hooks_config), ); self.repository.add_all()?; @@ -158,7 +236,15 @@ impl CocoGitto { self.repository.create_tag(&bump.new_version.prefixed_tag)?; } - self.repository.create_tag(&tag)?; + if let Some(msg_tmpl) = annotated { + let mut context = tera::Context::new(); + context.insert("latest", &old.version.to_string()); + context.insert("version", &tag.version.to_string()); + let msg = Tera::one_off(&msg_tmpl, &context, false)?; + self.repository.create_annotated_tag(&tag, &msg)?; + } else { + self.repository.create_tag(&tag)?; + } // Run per package post hooks for bump in bumps { @@ -167,23 +253,20 @@ impl CocoGitto { .get(&bump.package_name) .expect("package exists"); self.run_hooks( - HookType::PostBump, - bump.old_version.as_ref(), - &bump.new_version, - hooks_config, - Some(&bump.package_name), - Some(package), + HookRunOptions::post_bump() + .current_tag(bump.old_version.as_ref()) + .next_version(&bump.new_version) + .hook_profile(hooks_config) + .package(&bump.package_name, package), )?; } // Run global post hooks self.run_hooks( - HookType::PostBump, - current.as_ref(), - &next_version, - hooks_config, - None, - None, + HookRunOptions::post_bump() + .current_tag(current.as_ref()) + .next_version(&next_version) + .hook_profile(hooks_config), )?; Ok(()) @@ -194,6 +277,7 @@ impl CocoGitto { increment: IncrementCommand, pre_release: Option<&str>, hooks_config: Option<&str>, + annotated: Option, dry_run: bool, ) -> Result<()> { self.pre_bump_checks()?; @@ -213,7 +297,7 @@ impl CocoGitto { let tag = Tag::create(tag.version, None); if dry_run { - print!("{}", tag); + print!("{tag}"); return Ok(()); } @@ -249,12 +333,10 @@ impl CocoGitto { let next_version = HookVersion::new(tag.clone()); let hook_result = self.run_hooks( - HookType::PreBump, - current.as_ref(), - &next_version, - hooks_config, - None, - None, + HookRunOptions::pre_bump() + .current_tag(current.as_ref()) + .next_version(&next_version) + .hook_profile(hooks_config), ); self.repository.add_all()?; @@ -266,16 +348,22 @@ impl CocoGitto { sign, )?; - self.repository.create_tag(&tag)?; + if let Some(msg_tmpl) = annotated { + let mut context = tera::Context::new(); + context.insert("latest", &old.version.to_string()); + context.insert("version", &tag.version.to_string()); + let msg = Tera::one_off(&msg_tmpl, &context, false)?; + self.repository.create_annotated_tag(&tag, &msg)?; + } else { + self.repository.create_tag(&tag)?; + } // Run global post hooks self.run_hooks( - HookType::PostBump, - current.as_ref(), - &next_version, - hooks_config, - None, - None, + HookRunOptions::post_bump() + .current_tag(current.as_ref()) + .next_version(&next_version) + .hook_profile(hooks_config), )?; Ok(()) @@ -314,6 +402,10 @@ impl CocoGitto { let mut next_version = next_version.unwrap(); + if next_version == old { + continue; + } + if let Some(pre_release) = pre_release { next_version.version.pre = Prerelease::new(pre_release)?; } @@ -406,12 +498,11 @@ impl CocoGitto { let new_version = HookVersion::new(tag.clone()); let hook_result = self.run_hooks( - HookType::PreBump, - old_version.as_ref(), - &new_version, - hooks_config, - Some(package_name), - Some(package), + HookRunOptions::pre_bump() + .current_tag(old_version.as_ref()) + .next_version(&new_version) + .hook_profile(hooks_config) + .package(package_name, package), ); self.repository.add_all()?; diff --git a/src/command/bump/package.rs b/src/command/bump/package.rs index 79ccbc66..b204907e 100644 --- a/src/command/bump/package.rs +++ b/src/command/bump/package.rs @@ -1,15 +1,18 @@ -use crate::command::bump::{ensure_tag_is_greater_than_previous, tag_or_fallback_to_zero}; +use crate::command::bump::{ + ensure_tag_is_greater_than_previous, tag_or_fallback_to_zero, HookRunOptions, +}; use crate::conventional::changelog::template::PackageContext; use crate::conventional::changelog::ReleaseType; use crate::conventional::version::IncrementCommand; use crate::git::tag::Tag; use crate::hook::HookVersion; -use crate::settings::{HookType, MonoRepoPackage}; +use crate::settings::MonoRepoPackage; use crate::{CocoGitto, SETTINGS}; use anyhow::Result; use colored::*; use log::info; use semver::Prerelease; +use tera::Tera; impl CocoGitto { pub fn create_package_version( @@ -18,6 +21,7 @@ impl CocoGitto { increment: IncrementCommand, pre_release: Option<&str>, hooks_config: Option<&str>, + annotated: Option, dry_run: bool, ) -> Result<()> { self.pre_bump_checks()?; @@ -25,7 +29,13 @@ impl CocoGitto { let current_tag = self.repository.get_latest_package_tag(package_name); let current_tag = tag_or_fallback_to_zero(current_tag)?; let mut next_version = current_tag.bump(increment, &self.repository)?; + if current_tag == next_version { + print!("No conventional commits found for {package_name} that required a bump. Changelog will be updated on the next bump.\nPre-Hooks and Post-Hooks have been skiped.\n"); + return Ok(()); + } + ensure_tag_is_greater_than_previous(¤t_tag, &next_version)?; + if let Some(pre_release) = pre_release { next_version.version.pre = Prerelease::new(pre_release)?; } @@ -33,7 +43,7 @@ impl CocoGitto { let tag = Tag::create(next_version.version.clone(), Some(package_name.to_string())); if dry_run { - print!("{}", tag); + print!("{tag}"); return Ok(()); } @@ -61,12 +71,11 @@ impl CocoGitto { )); let hook_result = self.run_hooks( - HookType::PreBump, - current.as_ref(), - &next_version, - hooks_config, - Some(package_name), - Some(package), + HookRunOptions::pre_bump() + .current_tag(current.as_ref()) + .next_version(&next_version) + .hook_profile(hooks_config) + .package(package_name, package), ); self.repository.add_all()?; @@ -74,17 +83,24 @@ impl CocoGitto { let sign = self.repository.gpg_sign(); self.repository - .commit(&format!("chore(version): {}", tag), sign)?; - - self.repository.create_tag(&tag)?; + .commit(&format!("chore(version): {tag}"), sign)?; + + if let Some(msg_tmpl) = annotated { + let mut context = tera::Context::new(); + context.insert("latest", ¤t_tag.version.to_string()); + context.insert("version", &tag.version.to_string()); + let msg = Tera::one_off(&msg_tmpl, &context, false)?; + self.repository.create_annotated_tag(&tag, &msg)?; + } else { + self.repository.create_tag(&tag)?; + } self.run_hooks( - HookType::PostBump, - current.as_ref(), - &next_version, - hooks_config, - Some(package_name), - Some(package), + HookRunOptions::post_bump() + .current_tag(current.as_ref()) + .next_version(&next_version) + .hook_profile(hooks_config) + .package(package_name, package), )?; let current = current diff --git a/src/command/bump/standard.rs b/src/command/bump/standard.rs index 9039aa7f..6427c596 100644 --- a/src/command/bump/standard.rs +++ b/src/command/bump/standard.rs @@ -1,15 +1,17 @@ -use crate::command::bump::{ensure_tag_is_greater_than_previous, tag_or_fallback_to_zero}; +use crate::command::bump::{ + ensure_tag_is_greater_than_previous, tag_or_fallback_to_zero, HookRunOptions, +}; use crate::conventional::changelog::ReleaseType; use crate::conventional::version::IncrementCommand; use crate::git::tag::Tag; use crate::hook::HookVersion; -use crate::settings::HookType; use crate::{settings, CocoGitto, SETTINGS}; use anyhow::Result; use colored::*; use log::info; use semver::Prerelease; +use tera::Tera; impl CocoGitto { pub fn create_version( @@ -17,6 +19,7 @@ impl CocoGitto { increment: IncrementCommand, pre_release: Option<&str>, hooks_config: Option<&str>, + annotated: Option, dry_run: bool, ) -> Result<()> { self.pre_bump_checks()?; @@ -24,6 +27,10 @@ impl CocoGitto { let current_tag = self.repository.get_latest_tag(); let current_tag = tag_or_fallback_to_zero(current_tag)?; let mut tag = current_tag.bump(increment, &self.repository)?; + if current_tag == tag { + print!("No conventional commits for your repository that required a bump. Changelogs will be updated on the next bump.\nPre-Hooks and Post-Hooks have been skiped.\n"); + return Ok(()); + } ensure_tag_is_greater_than_previous(¤t_tag, &tag)?; @@ -34,7 +41,7 @@ impl CocoGitto { let tag = Tag::create(tag.version, None); if dry_run { - print!("{}", tag); + print!("{tag}"); return Ok(()); } @@ -52,12 +59,10 @@ impl CocoGitto { let next_version = HookVersion::new(tag.clone()); let hook_result = self.run_hooks( - HookType::PreBump, - current.as_ref(), - &next_version, - hooks_config, - None, - None, + HookRunOptions::pre_bump() + .current_tag(current.as_ref()) + .next_version(&next_version) + .hook_profile(hooks_config), ); self.repository.add_all()?; @@ -70,15 +75,21 @@ impl CocoGitto { sign, )?; - self.repository.create_tag(&tag)?; + if let Some(msg_tmpl) = annotated { + let mut context = tera::Context::new(); + context.insert("latest", ¤t_tag.version.to_string()); + context.insert("version", &tag.version.to_string()); + let msg = Tera::one_off(&msg_tmpl, &context, false)?; + self.repository.create_annotated_tag(&tag, &msg)?; + } else { + self.repository.create_tag(&tag)?; + } self.run_hooks( - HookType::PostBump, - current.as_ref(), - &next_version, - hooks_config, - None, - None, + HookRunOptions::post_bump() + .current_tag(current.as_ref()) + .next_version(&next_version) + .hook_profile(hooks_config), )?; let current = current diff --git a/src/command/changelog.rs b/src/command/changelog.rs index d9fb05b4..a72fb11e 100644 --- a/src/command/changelog.rs +++ b/src/command/changelog.rs @@ -26,7 +26,7 @@ impl CocoGitto { } pub fn get_changelog_at_tag(&self, tag: &str, template: Template) -> Result { - let pattern = format!("..{}", tag); + let pattern = format!("..{tag}"); let pattern = RevspecPattern::from(pattern.as_str()); let changelog = self.get_changelog(pattern, false)?; diff --git a/src/command/check.rs b/src/command/check.rs index ead91acf..9fa30d59 100644 --- a/src/command/check.rs +++ b/src/command/check.rs @@ -8,8 +8,16 @@ use colored::*; use log::info; impl CocoGitto { - pub fn check(&self, check_from_latest_tag: bool, ignore_merge_commits: bool) -> Result<()> { - let commit_range = if check_from_latest_tag { + pub fn check( + &self, + check_from_latest_tag: bool, + ignore_merge_commits: bool, + range: Option, + ) -> Result<()> { + let commit_range = if let Some(range) = range { + self.repository + .get_commit_range(&RevspecPattern::from(range.as_str()))? + } else if check_from_latest_tag { self.repository .get_commit_range(&RevspecPattern::default())? } else { diff --git a/src/command/commit.rs b/src/command/commit.rs index 59c6b9b3..b19d7919 100644 --- a/src/command/commit.rs +++ b/src/command/commit.rs @@ -1,9 +1,11 @@ use crate::conventional::commit::Commit; use crate::CocoGitto; +use crate::CommitHook::CommitMessage; use anyhow::Result; use conventional_commit_parser::commit::{CommitType, ConventionalCommit}; use conventional_commit_parser::parse_footers; use log::info; +use std::fs; impl CocoGitto { #[allow(clippy::too_many_arguments)] @@ -41,6 +43,8 @@ impl CocoGitto { // Git commit let sign = sign || self.repository.gpg_sign(); + fs::write(self.prepare_edit_message_path(), &conventional_message)?; + self.run_commit_hook(CommitMessage)?; let oid = self.repository.commit(&conventional_message, sign)?; // Pretty print a conventional commit summary diff --git a/src/command/get_version.rs b/src/command/get_version.rs new file mode 100644 index 00000000..ae1f23a0 --- /dev/null +++ b/src/command/get_version.rs @@ -0,0 +1,44 @@ +use anyhow::bail; +use anyhow::Result; +use log::warn; +use semver::Version; + +use crate::git::error::TagError; +use crate::CocoGitto; + +impl CocoGitto { + pub fn get_latest_version( + &self, + fallback: Option, + package: Option, + ) -> Result<()> { + let fallback = match fallback { + Some(input) => match Version::parse(&input) { + Ok(version) => Some(version), + Err(err) => { + warn!("Invalid fallback: {}", input); + bail!("{}", err) + } + }, + None => None, + }; + + let current_tag = match package { + Some(pkg) => self.repository.get_latest_package_tag(&pkg), + None => self.repository.get_latest_tag(), + }; + + let current_version = match current_tag { + Ok(tag) => tag.version, + Err(TagError::NoTag) => match fallback { + Some(input) => input, + None => bail!("No version yet"), + }, + Err(err) => bail!("{}", err), + }; + + warn!("Current version:"); + print!("{current_version}"); + Ok(()) + } +} diff --git a/src/command/git_hooks.rs b/src/command/git_hooks.rs new file mode 100644 index 00000000..d2c5c69a --- /dev/null +++ b/src/command/git_hooks.rs @@ -0,0 +1,38 @@ +use crate::git::hook::install_git_hook; +use crate::settings::GitHookType; +use crate::{CocoGitto, SETTINGS}; +use anyhow::{anyhow, Result}; + +impl CocoGitto { + pub fn install_all_hooks(&self) -> Result<()> { + let repodir = &self + .repository + .get_repo_dir() + .ok_or_else(|| anyhow!("Repository root directory not found"))? + .to_path_buf(); + + for (hook_type, hook) in SETTINGS.git_hooks.iter() { + install_git_hook(repodir, hook_type, hook)?; + } + + Ok(()) + } + + pub fn install_git_hooks(&self, hook_types: Vec) -> Result<()> { + let repodir = &self + .repository + .get_repo_dir() + .ok_or_else(|| anyhow!("Repository root directory not found"))? + .to_path_buf(); + + for hook_type in hook_types { + let hook = SETTINGS + .git_hooks + .get(&hook_type) + .ok_or(anyhow!("git-hook {hook_type} was not found in cog.toml"))?; + install_git_hook(repodir, &hook_type, hook)? + } + + Ok(()) + } +} diff --git a/src/command/log.rs b/src/command/log.rs index e295fc2a..4bcaa3db 100644 --- a/src/command/log.rs +++ b/src/command/log.rs @@ -35,11 +35,11 @@ impl CocoGitto { let mut repo_tag_name = repo_path.to_str()?.to_string(); if let Some(branch_shorthand) = self.repository.get_branch_shorthand() { - write!(&mut repo_tag_name, " on {}", branch_shorthand).unwrap(); + write!(&mut repo_tag_name, " on {branch_shorthand}").unwrap(); } if let Ok(latest_tag) = self.repository.get_latest_tag() { - write!(&mut repo_tag_name, " {}", latest_tag).unwrap(); + write!(&mut repo_tag_name, " {latest_tag}").unwrap(); }; Some(repo_tag_name) diff --git a/src/command/mod.rs b/src/command/mod.rs index c5f89407..2c666615 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -3,5 +3,7 @@ pub mod changelog; pub mod check; pub mod commit; pub mod edit; +pub mod get_version; +pub mod git_hooks; pub mod init; pub mod log; diff --git a/src/conventional/bump.rs b/src/conventional/bump.rs index f1ec3a3c..1808c823 100644 --- a/src/conventional/bump.rs +++ b/src/conventional/bump.rs @@ -23,6 +23,7 @@ pub(crate) trait Bump { fn major_bump(&self) -> Self; fn minor_bump(&self) -> Self; fn patch_bump(&self) -> Self; + fn no_bump(&self) -> Self; fn auto_bump(&self, repository: &Repository) -> Result where Self: Sized; @@ -66,6 +67,11 @@ impl Bump for Tag { next.reset_metadata() } + fn no_bump(&self) -> Self { + let next = self.clone(); + next.reset_metadata() + } + fn auto_bump(&self, repository: &Repository) -> Result { self.get_version_from_commit_history(repository) } @@ -111,6 +117,7 @@ impl Tag { IncrementCommand::Major => Ok(self.major_bump()), IncrementCommand::Minor => Ok(self.minor_bump()), IncrementCommand::Patch => Ok(self.patch_bump()), + IncrementCommand::NoBump => Ok(self.no_bump()), IncrementCommand::Auto => self.auto_bump(repository), IncrementCommand::AutoPackage(package) => self.auto_package_bump(repository, &package), IncrementCommand::AutoMonoRepoGlobal(package_increment) => { @@ -136,7 +143,7 @@ impl Tag { let changelog_start_oid = Some(changelog_start_oid.as_str()); let pattern = changelog_start_oid - .map(|oid| format!("{}..", oid)) + .map(|oid| format!("{oid}..")) .unwrap_or_else(|| "..".to_string()); let pattern = pattern.as_str(); let pattern = RevspecPattern::from(pattern); @@ -160,6 +167,7 @@ impl Tag { Increment::Major => self.major_bump(), Increment::Minor => self.minor_bump(), Increment::Patch => self.patch_bump(), + Increment::NoBump => self.no_bump(), }) } @@ -178,7 +186,7 @@ impl Tag { let changelog_start_oid = Some(changelog_start_oid.as_str()); let pattern = changelog_start_oid - .map(|oid| format!("{}..", oid)) + .map(|oid| format!("{oid}..")) .unwrap_or_else(|| "..".to_string()); let pattern = pattern.as_str(); let pattern = RevspecPattern::from(pattern); @@ -201,6 +209,7 @@ impl Tag { Increment::Major => self.major_bump(), Increment::Minor => self.minor_bump(), Increment::Patch => self.patch_bump(), + Increment::NoBump => self.no_bump(), }) } @@ -217,7 +226,7 @@ impl Tag { let changelog_start_oid = Some(changelog_start_oid.as_str()); let pattern = changelog_start_oid - .map(|oid| format!("{}..", oid)) + .map(|oid| format!("{oid}..")) .unwrap_or_else(|| "..".to_string()); let pattern = pattern.as_str(); let pattern = RevspecPattern::from(pattern); @@ -241,6 +250,7 @@ impl Tag { Increment::Major => self.major_bump(), Increment::Minor => self.minor_bump(), Increment::Patch => self.patch_bump(), + Increment::NoBump => self.no_bump(), }) } @@ -267,12 +277,18 @@ impl Tag { .any(|commit| commit.message.commit_type == CommitType::BugFix) }; + // At this point, it is not a major, minor or patch bump but we might have found conventional commits + // -> Must be only chore, docs, refactor ... which means commits that don't require bump but shouldn't throw error + let no_bump_required = !commits.is_empty(); + if is_major_bump() { Ok(Increment::Major) } else if is_minor_bump() { Ok(Increment::Minor) } else if is_patch_bump() { Ok(Increment::Patch) + } else if no_bump_required { + Ok(Increment::NoBump) } else { Err(BumpError::NoCommitFound) } @@ -359,6 +375,20 @@ mod test { Ok(()) } + #[sealed_test] + fn no_bump() -> Result<()> { + // Arrange + let repository = Repository::init(".")?; + let base_version = Tag::from_str("1.0.0", None)?; + + // Act + let tag = base_version.bump(IncrementCommand::NoBump, &repository)?; + + // Assert + assert_that!(tag.version).is_equal_to(Version::new(1, 0, 0)); + Ok(()) + } + #[test] fn should_get_next_auto_version_patch() -> Result<()> { // Arrange @@ -376,6 +406,42 @@ mod test { Ok(()) } + #[test] + fn should_not_bump_versions_due_to_non_bump_commits() -> Result<()> { + // Arrange + let revert = Commit::commit_fixture(CommitType::Revert, false); + let perf = Commit::commit_fixture(CommitType::Performances, false); + let documentation = Commit::commit_fixture(CommitType::Documentation, false); + let chore = Commit::commit_fixture(CommitType::Chore, false); + let style = Commit::commit_fixture(CommitType::Style, false); + let refactor = Commit::commit_fixture(CommitType::Refactor, false); + let test = Commit::commit_fixture(CommitType::Test, false); + let build = Commit::commit_fixture(CommitType::Build, false); + let ci = Commit::commit_fixture(CommitType::Ci, false); + + let base_version = Tag::from_str("1.0.0", None)?; + + // Act + let increment = base_version.version_increment_from_commit_history(&[ + revert, + perf, + documentation, + chore, + style, + refactor, + test, + build, + ci, + ]); + + // Assert + assert_that!(increment) + .is_ok() + .is_equal_to(Increment::NoBump); + + Ok(()) + } + #[test] fn increment_minor_version_should_set_patch_to_zero() -> Result<()> { // Arrange @@ -472,7 +538,7 @@ mod test { } #[test] - fn should_fail_without_feature_bug_fix_or_breaking_change_commit() -> Result<()> { + fn should_not_fail_without_feature_bug_fix_or_breaking_change_commit() -> Result<()> { // Arrange let chore = Commit::commit_fixture(CommitType::Chore, false); let docs = Commit::commit_fixture(CommitType::Documentation, false); @@ -482,21 +548,7 @@ mod test { let version = base_version.version_increment_from_commit_history(&[chore, docs]); // Assert - let result = version.unwrap_err().to_string(); - let result = result.as_str(); - - assert_eq!( - result, - r#"failed to bump version - -cause: No conventional commit found to bump current version. - Only feature, bug fix and breaking change commits will trigger an automatic bump. - -suggestion: Please see https://conventionalcommits.org/en/v1.0.0/#summary for more information. - Alternatively consider using `cog bump <--version |--auto|--major|--minor>` - -"# - ); + assert_that!(version).is_ok().is_equal_to(Increment::NoBump); Ok(()) } diff --git a/src/conventional/changelog/error.rs b/src/conventional/changelog/error.rs index 5b159a47..e78f1afa 100644 --- a/src/conventional/changelog/error.rs +++ b/src/conventional/changelog/error.rs @@ -15,13 +15,13 @@ impl Display for ChangelogError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { ChangelogError::TemplateNotFound(path) => { - writeln!(f, "changelog template not found in {:?}", path) + writeln!(f, "changelog template not found in {path:?}") } ChangelogError::TeraError(err) => { - writeln!(f, "failed to render changelog: \n\t{:?}", err) + writeln!(f, "failed to render changelog: \n\t{err:?}") } ChangelogError::WriteError(err) => { - writeln!(f, "failed to write changelog: \n\t{}", err) + writeln!(f, "failed to write changelog: \n\t{err}") } ChangelogError::SeparatorNotFound(path) => writeln!( f, diff --git a/src/conventional/changelog/release.rs b/src/conventional/changelog/release.rs index fbe8ab0e..71dc0600 100644 --- a/src/conventional/changelog/release.rs +++ b/src/conventional/changelog/release.rs @@ -60,7 +60,11 @@ impl<'a> From> for Release<'a> { } match Commit::from_git_commit(&commit) { - Ok(commit) => commits.push(ChangelogCommit::from(commit)), + Ok(commit) => { + if !commit.should_omit() { + commits.push(ChangelogCommit::from(commit)) + } + } Err(err) => { let err = err.to_string().red(); warn!("{}", err); diff --git a/src/conventional/changelog/renderer.rs b/src/conventional/changelog/renderer.rs index c29e12fe..8973ba4c 100644 --- a/src/conventional/changelog/renderer.rs +++ b/src/conventional/changelog/renderer.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use tera::{get_json_pointer, to_value, try_get_value, Context, Tera, Value}; +use tera::{dotted_pointer, to_value, try_get_value, Context, Tera, Value}; use crate::conventional::changelog::release::Release; use crate::conventional::changelog::template::{ @@ -95,12 +95,10 @@ impl Renderer { let value = args.get("value").unwrap_or(&Value::Null); - let json_pointer = get_json_pointer("scope"); - arr = arr .into_iter() .filter(|v| { - let val = v.pointer(&json_pointer).unwrap_or(&Value::Null); + let val = dotted_pointer(v, "scope").unwrap_or(&Value::Null); if value.is_null() { val == value } else { diff --git a/src/conventional/changelog/template.rs b/src/conventional/changelog/template.rs index 7a96783a..27c91997 100644 --- a/src/conventional/changelog/template.rs +++ b/src/conventional/changelog/template.rs @@ -45,8 +45,9 @@ impl Template { } } -#[derive(Debug)] +#[derive(Debug, Default)] pub enum TemplateKind { + #[default] Default, FullHash, Remote, @@ -59,12 +60,6 @@ pub enum TemplateKind { Custom(PathBuf), } -impl Default for TemplateKind { - fn default() -> Self { - TemplateKind::Default - } -} - impl TemplateKind { /// Returns either a predefined template or a custom template fn from_arg(value: &str) -> Result { diff --git a/src/conventional/commit.rs b/src/conventional/commit.rs index acd49a4d..4be76fc1 100644 --- a/src/conventional/commit.rs +++ b/src/conventional/commit.rs @@ -21,12 +21,15 @@ pub struct Commit { #[derive(Debug, Deserialize, Serialize, Clone, Eq, PartialEq)] pub struct CommitConfig { pub changelog_title: String, + #[serde(default)] + pub omit_from_changelog: bool, } impl CommitConfig { pub(crate) fn new(changelog_title: &str) -> Self { CommitConfig { changelog_title: changelog_title.to_string(), + omit_from_changelog: false, } } } @@ -87,6 +90,13 @@ impl Commit { } } + pub(crate) fn should_omit(&self) -> bool { + SETTINGS + .commit_types() + .get(&self.message.commit_type) + .map_or(false, |config| config.omit_from_changelog) + } + pub fn get_log(&self) -> String { let summary = &self.message.summary; let message_display = Commit::short_summary_from_str(summary).yellow(); @@ -244,7 +254,7 @@ pub(crate) fn format_summary(commit: &ConventionalCommit) -> String { #[cfg(test)] mod test { - use crate::conventional::commit::{format_summary, verify, Commit}; + use crate::conventional::commit::{format_summary, verify, Commit, CommitConfig}; use chrono::NaiveDateTime; use cmd_lib::run_fun; @@ -440,6 +450,36 @@ mod test { assert_that!(summary).is_equal_to("fix: this is the message".to_string()); } + #[test] + fn should_toggle_changelog_omission() { + // Arrange + let mut config = CommitConfig::new("Omittable Changes"); + + // Assert + assert!( + !&config.omit_from_changelog, + "expected CommitConfig::omit_from_changelog to be falsy unless explicitly set" + ); + + // Act + config.omit_from_changelog = true; + + // Assert + assert!( + &config.omit_from_changelog, + "CommitConfig::omit_from_changelog should be truthy after calling CommitConfig::omit" + ); + + // Act + config.omit_from_changelog = false; + + // Assert + assert!( + !&config.omit_from_changelog, + "CommitConfig::omit_from_changelog should be falsy after calling CommitConfig::include" + ); + } + #[sealed_test] fn should_map_conventional_commit() { // Arrange diff --git a/src/conventional/error.rs b/src/conventional/error.rs index 9b80671d..f27474de 100644 --- a/src/conventional/error.rs +++ b/src/conventional/error.rs @@ -36,10 +36,10 @@ impl Display for BumpError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { writeln!(f, "failed to bump version\n")?; match self { - BumpError::Git2Error(err) => writeln!(f, "\t{}", err), - BumpError::TagError(err) => writeln!(f, "\t{}", err), - BumpError::SemVerError(err) => writeln!(f, "\t{}", err), - BumpError::FmtError(err) => writeln!(f, "\t{}", err), + BumpError::Git2Error(err) => writeln!(f, "\t{err}"), + BumpError::TagError(err) => writeln!(f, "\t{err}"), + BumpError::SemVerError(err) => writeln!(f, "\t{err}"), + BumpError::FmtError(err) => writeln!(f, "\t{err}"), BumpError::NoCommitFound => writeln!( f, r#"cause: No conventional commit found to bump current version. @@ -87,9 +87,9 @@ impl Display for ConventionalCommitError { cause, } => { let error_header = "Errored commit: ".bold().red(); - let author = format!("<{}>", author).blue(); + let author = format!("<{author}>").blue(); let cause = anyhow!(cause.clone()); - let cause = format!("{:?}", cause) + let cause = format!("{cause:?}") .lines() .collect::>() .join("\n\t"); @@ -113,7 +113,7 @@ impl Display for ConventionalCommitError { author, } => { let error_header = "Errored commit: ".bold().red(); - let author = format!("<{}>", author).blue(); + let author = format!("<{author}>").blue(); writeln!( f, "{}{} {}\n\t{message}'{summary}'\n\t{cause}Commit type `{commit_type}` not allowed", @@ -128,7 +128,7 @@ impl Display for ConventionalCommitError { } ConventionalCommitError::ParseError(err) => { let err = anyhow!(err.clone()); - writeln!(f, "{:?}", err) + writeln!(f, "{err:?}") } } } diff --git a/src/conventional/version.rs b/src/conventional/version.rs index 6281eb5e..fcafd654 100644 --- a/src/conventional/version.rs +++ b/src/conventional/version.rs @@ -6,6 +6,7 @@ pub enum IncrementCommand { Minor, Patch, Auto, + NoBump, AutoPackage(String), AutoMonoRepoGlobal(Option), Manual(String), @@ -16,6 +17,7 @@ pub enum Increment { Major, Minor, Patch, + NoBump, } impl From for IncrementCommand { @@ -24,6 +26,7 @@ impl From for IncrementCommand { Increment::Major => IncrementCommand::Major, Increment::Minor => IncrementCommand::Minor, Increment::Patch => IncrementCommand::Patch, + Increment::NoBump => IncrementCommand::NoBump, } } } @@ -43,6 +46,9 @@ impl PartialOrd for Increment { (Increment::Minor, _) => Some(Ordering::Greater), (_, Increment::Minor) => Some(Ordering::Less), (Increment::Patch, Increment::Patch) => Some(Ordering::Equal), + (Increment::NoBump, Increment::NoBump) => Some(Ordering::Equal), + (Increment::Patch, Increment::NoBump) => Some(Ordering::Greater), + (Increment::NoBump, Increment::Patch) => Some(Ordering::Less), } } } diff --git a/src/error.rs b/src/error.rs index f3a782f7..280cd139 100644 --- a/src/error.rs +++ b/src/error.rs @@ -21,12 +21,12 @@ impl Display for CogCheckReport { .red() .bold(); - writeln!(f, "{}", header)?; + writeln!(f, "{header}")?; for err in &self.errors { let underline = format!("{:>57}", " ").underline(); - writeln!(f, "{:>5}\n", underline)?; - write!(f, "{}", err)?; + writeln!(f, "{underline:>5}\n")?; + write!(f, "{err}")?; } Ok(()) } @@ -54,6 +54,6 @@ impl Display for BumpError { \tyou can run `git stash apply stash@{}` to restore these changes.", stash_ref, self.stash_number ); - write!(f, "{}\n{}", header, suggestion) + write!(f, "{header}\n{suggestion}") } } diff --git a/src/git/assets/commit-msg b/src/git/assets/commit-msg deleted file mode 100755 index 48177c53..00000000 --- a/src/git/assets/commit-msg +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/sh - -COMMIT_MSG_FILE=$1 - -MESSAGE=$(cat $COMMIT_MSG_FILE) - -if cog verify "$MESSAGE"; then - echo "Commit parse succeeded" -else - echo "See https://www.conventionalcommits.org/en/v1.0.0" - exit 1 -fi - diff --git a/src/git/assets/pre-push b/src/git/assets/pre-push deleted file mode 100755 index 0b4e6858..00000000 --- a/src/git/assets/pre-push +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/sh - -if cog check; then - exit 0 -fi - -echo "Invalid commits were found, force push with '--no-verify'" -exit 1 diff --git a/src/git/error.rs b/src/git/error.rs index 4fdafb49..6e871ff4 100644 --- a/src/git/error.rs +++ b/src/git/error.rs @@ -1,6 +1,5 @@ use crate::git::status::Statuses; use colored::Colorize; -use git2::Error; use serde::de::StdError; use std::fmt::{Display, Formatter}; use std::{fmt, io}; @@ -26,6 +25,7 @@ pub enum Git2Error { NoTagFound, CommitterNotFound, TagError(TagError), + GitHookNonZeroExit(i32), } #[derive(Debug)] @@ -78,11 +78,11 @@ impl Display for Git2Error { match self { Git2Error::NothingToCommit { branch, statuses } => { if let Some(branch) = branch { - writeln!(f, "On branch {}\n", branch)?; + writeln!(f, "On branch {branch}\n")?; } match statuses { - Some(statuses) if !statuses.0.is_empty() => write!(f, "{}", statuses), + Some(statuses) if !statuses.0.is_empty() => write!(f, "{statuses}"), _ => writeln!( f, "nothing to commit (create/copy files and use \"git add\" to track)" @@ -119,6 +119,9 @@ impl Display for Git2Error { Git2Error::TagError(_) => writeln!(f, "Tag error"), Git2Error::IOError(_) => writeln!(f, "IO Error"), Git2Error::GpgError(_) => writeln!(f, "failed to sign commit"), + Git2Error::GitHookNonZeroExit(status) => { + writeln!(f, "commit hook failed with exit code {status}") + } }?; match self { @@ -130,10 +133,10 @@ impl Display for Git2Error { | Git2Error::StashError(err) | Git2Error::StatusError(err) | Git2Error::Other(err) - | Git2Error::CommitNotFound(err) => writeln!(f, "\ncause: {}", err), - Git2Error::GpgError(err) => writeln!(f, "\ncause: {}", err), - Git2Error::TagError(err) => writeln!(f, "\ncause: {}", err), - Git2Error::IOError(err) => writeln!(f, "\ncause: {}", err), + | Git2Error::CommitNotFound(err) => writeln!(f, "\ncause: {err}"), + Git2Error::GpgError(err) => writeln!(f, "\ncause: {err}"), + Git2Error::TagError(err) => writeln!(f, "\ncause: {err}"), + Git2Error::IOError(err) => writeln!(f, "\ncause: {err}"), _ => fmt::Result::Ok(()), } } @@ -143,30 +146,30 @@ impl Display for TagError { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { TagError::SemVerError { tag, err } => { - writeln!(f, "tag `{}` is not SemVer compliant", tag)?; - writeln!(f, "\tcause: {}", err) + writeln!(f, "tag `{tag}` is not SemVer compliant")?; + writeln!(f, "\tcause: {err}") } TagError::InvalidPrefixError { prefix, tag } => { - writeln!(f, "Expected a tag with prefix {}, got {}", prefix, tag) + writeln!(f, "Expected a tag with prefix {prefix}, got {tag}") } TagError::NotFound { tag, err } => { - writeln!(f, "tag {} not found", tag)?; - writeln!(f, "\tcause: {}", err) + writeln!(f, "tag {tag} not found")?; + writeln!(f, "\tcause: {err}") } TagError::NoTag => writeln!(f, "unable to get any tag"), TagError::NoMatchFound { pattern, err } => { match pattern { None => writeln!(f, "no tag found")?, - Some(pattern) => writeln!(f, "no tag matching pattern {}", pattern)?, + Some(pattern) => writeln!(f, "no tag matching pattern {pattern}")?, } - writeln!(f, "\tcause: {}", err) + writeln!(f, "\tcause: {err}") } } } } impl From for Git2Error { - fn from(err: Error) -> Self { + fn from(err: git2::Error) -> Self { Git2Error::Other(err) } } diff --git a/src/git/hook.rs b/src/git/hook.rs index dcc3655d..f86030bd 100644 --- a/src/git/hook.rs +++ b/src/git/hook.rs @@ -1,84 +1,41 @@ -use std::collections::HashMap; use std::fs::{self, Permissions}; use std::io; #[cfg(target_family = "unix")] use std::os::unix::fs::PermissionsExt; use std::path::Path; -use crate::{CocoGitto, HookType}; +use crate::settings::{GitHook, GitHookType}; +use anyhow::Result; -use crate::settings::BumpProfile; -use anyhow::{anyhow, Result}; +pub fn install_git_hook(repodir: &Path, hook_type: &GitHookType, hook: &GitHook) -> Result<()> { + let hook_path = repodir.join(".git/hooks"); + let hook_path = hook_path.join::<&str>((*hook_type).into()); -pub(crate) static PRE_PUSH_HOOK: &[u8] = include_bytes!("assets/pre-push"); -pub(crate) static PREPARE_COMMIT_HOOK: &[u8] = include_bytes!("assets/commit-msg"); -const PRE_COMMIT_HOOK_PATH: &str = ".git/hooks/commit-msg"; -const PRE_PUSH_HOOK_PATH: &str = ".git/hooks/pre-push"; + if hook_path.exists() { + let mut answer = String::new(); + println!( + "Git hook `{}` exists. (Overwrite Y/n)", + hook_path.to_string_lossy() + ); + io::stdin().read_line(&mut answer)?; -pub trait Hooks { - fn bump_profiles(&self) -> &HashMap; - fn pre_bump_hooks(&self) -> &Vec; - fn post_bump_hooks(&self) -> &Vec; - - fn get_hooks(&self, hook_type: HookType) -> &Vec { - match hook_type { - HookType::PreBump => self.pre_bump_hooks(), - HookType::PostBump => self.post_bump_hooks(), + if !answer.trim().eq_ignore_ascii_case("y") { + println!("Aborting"); + return Ok(()); } } - fn get_profile_hooks(&self, profile: &str, hook_type: HookType) -> &Vec { - let profile = self - .bump_profiles() - .get(profile) - .expect("Bump profile not found"); - match hook_type { - HookType::PreBump => &profile.pre_bump_hooks, - HookType::PostBump => &profile.post_bump_hooks, + match hook { + GitHook::Script { script } => fs::write(&hook_path, script)?, + GitHook::File { path } => { + fs::copy(path, &hook_path)?; } - } -} - -pub enum HookKind { - PrepareCommit, - PrePush, - All, -} - -impl CocoGitto { - pub fn install_hook(&self, kind: HookKind) -> Result<()> { - let repodir = &self - .repository - .get_repo_dir() - .ok_or_else(|| anyhow!("Repository root directory not found"))? - .to_path_buf(); - - match kind { - HookKind::PrepareCommit => create_hook(repodir, HookKind::PrepareCommit)?, - HookKind::PrePush => create_hook(repodir, HookKind::PrePush)?, - HookKind::All => { - create_hook(repodir, HookKind::PrepareCommit)?; - create_hook(repodir, HookKind::PrePush)? - } - }; - - Ok(()) - } -} - -fn create_hook(path: &Path, kind: HookKind) -> io::Result<()> { - let (hook_path, hook_content) = match kind { - HookKind::PrepareCommit => (path.join(PRE_COMMIT_HOOK_PATH), PREPARE_COMMIT_HOOK), - HookKind::PrePush => (path.join(PRE_PUSH_HOOK_PATH), PRE_PUSH_HOOK), - HookKind::All => unreachable!(), }; - fs::write(&hook_path, hook_content)?; - #[cfg(target_family = "unix")] { let permissions = Permissions::from_mode(0o755); - fs::set_permissions(&hook_path, permissions)?; + fs::set_permissions(hook_path, permissions)?; } Ok(()) @@ -86,11 +43,12 @@ fn create_hook(path: &Path, kind: HookKind) -> io::Result<()> { #[cfg(test)] mod tests { - use std::fs::File; + use std::collections::HashMap; + use std::fs; - use crate::git::hook::HookKind; use crate::CocoGitto; + use crate::settings::{GitHook, GitHookType, Settings}; use anyhow::Result; use cmd_lib::run_cmd; use sealed_test::prelude::*; @@ -101,67 +59,95 @@ mod tests { fn add_pre_commit_hook() -> Result<()> { // Arrange run_cmd!(git init)?; + let mut git_hooks = HashMap::new(); + let hooks_script = r#" +if cog check; then + exit 0 +fi + +echo "Invalid commits were found, force push with '--no-verify'" +exit 1"# + .to_string(); + + git_hooks.insert( + GitHookType::CommitMsg, + GitHook::Script { + script: hooks_script.clone(), + }, + ); + + let settings = Settings { + git_hooks, + ..Default::default() + }; + + let settings = toml::to_string(&settings)?; + fs::write("cog.toml", settings)?; let cog = CocoGitto::get()?; // Act - cog.install_hook(HookKind::PrepareCommit)?; + cog.install_git_hooks(vec![GitHookType::CommitMsg])?; // Assert assert_that!(Path::new(".git/hooks/commit-msg")).exists(); + let hooks = fs::read_to_string(".git/hooks/commit-msg")?; + assert_that!(hooks).is_equal_to(&hooks_script); assert_that!(Path::new(".git/hooks/pre-push")).does_not_exist(); Ok(()) } - #[sealed_test] - fn add_pre_push_hook() -> Result<()> { - // Arrange - run_cmd!(git init)?; - - let cog = CocoGitto::get()?; - - // Act - cog.install_hook(HookKind::PrePush)?; - - // Assert - assert_that!(Path::new(".git/hooks/pre-push")).exists(); - assert_that!(Path::new(".git/hooks/pre-commit")).does_not_exist(); - Ok(()) - } - #[sealed_test] fn add_all() -> Result<()> { // Arrange run_cmd!(git init)?; + run_cmd!(echo "echo toto" > pre-push;)?; + + let mut git_hooks = HashMap::new(); + let hooks_script = r#" +if cog check; then + exit 0 +fi + +echo "Invalid commits were found, force push with '--no-verify'" +exit 1"# + .to_string(); + + git_hooks.insert( + GitHookType::CommitMsg, + GitHook::Script { + script: hooks_script.clone(), + }, + ); + + git_hooks.insert( + GitHookType::PrePush, + GitHook::File { + path: "pre-push".into(), + }, + ); + + let settings = Settings { + git_hooks, + ..Default::default() + }; + + let settings = toml::to_string(&settings)?; + fs::write("cog.toml", settings)?; let cog = CocoGitto::get()?; // Act - cog.install_hook(HookKind::All)?; + cog.install_all_hooks()?; // Assert - assert_that!(Path::new(".git/hooks/pre-push")).exists(); assert_that!(Path::new(".git/hooks/commit-msg")).exists(); - Ok(()) - } + let hook = fs::read_to_string(".git/hooks/commit-msg")?; + assert_that!(hook).is_equal_to(&hooks_script); - #[sealed_test] - #[cfg(target_family = "unix")] - fn should_have_perm_755_on_unix() -> Result<()> { - // Arrange - use std::os::unix::fs::PermissionsExt; - run_cmd!(git init)?; - - let cog = CocoGitto::get()?; - - // Act - cog.install_hook(HookKind::PrePush)?; - - // Assert - let prepush = File::open(".git/hooks/pre-push")?; - let metadata = prepush.metadata()?; assert_that!(Path::new(".git/hooks/pre-push")).exists(); - assert_that!(metadata.permissions().mode() & 0o777).is_equal_to(0o755); + let hook = fs::read_to_string(".git/hooks/pre-push")?; + assert_that!(hook).is_equal_to("echo toto\n".to_string()); Ok(()) } } diff --git a/src/git/oid.rs b/src/git/oid.rs index 5ccd83ac..273a9e0b 100644 --- a/src/git/oid.rs +++ b/src/git/oid.rs @@ -26,7 +26,7 @@ impl Display for OidOf { /// Print the oid according to it's type fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { - OidOf::Tag(tag) => write!(f, "{}", tag), + OidOf::Tag(tag) => write!(f, "{tag}"), OidOf::Head(_) => write!(f, "HEAD"), OidOf::Other(oid) => write!(f, "{}", &oid.to_string()[0..6]), } diff --git a/src/git/revspec.rs b/src/git/revspec.rs index 729e3635..92b6eb94 100644 --- a/src/git/revspec.rs +++ b/src/git/revspec.rs @@ -27,14 +27,14 @@ impl fmt::Display for RevspecPattern { fn fmt(&self, f: &mut Formatter) -> fmt::Result { let from = self.from.as_deref().unwrap_or(""); let to = self.to.as_deref().unwrap_or(""); - write!(f, "{}..{}", from, to) + write!(f, "{from}..{to}") } } impl From<&str> for RevspecPattern { fn from(value: &str) -> Self { if !value.contains("..") { - panic!("Invalid commit range pattern: '{}'", value); + panic!("Invalid commit range pattern: '{value}'"); } let split = value.split("..").collect::>(); @@ -200,7 +200,7 @@ impl Repository { }; // Resolve shorthands and tags - let spec = format!("{}..{}", from, to); + let spec = format!("{from}..{to}"); // Attempt to resolve tag names, fallback to oid let to = maybe_to_tag .map(OidOf::Tag) diff --git a/src/git/stash.rs b/src/git/stash.rs index 42da9c86..8ed0c890 100644 --- a/src/git/stash.rs +++ b/src/git/stash.rs @@ -5,7 +5,7 @@ use crate::Tag; impl Repository { pub(crate) fn stash_failed_version(&mut self, tag: Tag) -> Result<(), Git2Error> { let sig = self.0.signature()?; - let message = &format!("cog_bump_{}", tag); + let message = &format!("cog_bump_{tag}"); self.0 .stash_save(&sig, message, None) .map(|_| ()) diff --git a/src/git/tag.rs b/src/git/tag.rs index aceee0d9..0f33b12e 100644 --- a/src/git/tag.rs +++ b/src/git/tag.rs @@ -38,6 +38,20 @@ impl Repository { .map_err(Git2Error::from) } + pub(crate) fn create_annotated_tag(&self, tag: &Tag, msg: &str) -> Result<(), Git2Error> { + if self.get_diff(true).is_some() { + let statuses = self.get_statuses()?; + return Err(Git2Error::ChangesNeedToBeCommitted(statuses)); + } + + let head = self.get_head_commit().unwrap(); + let sig = self.0.signature()?; + self.0 + .tag(&tag.to_string(), &head.into_object(), &sig, msg, false) + .map(|_| ()) + .map_err(Git2Error::from) + } + /// Get the latest tag, will ignore package tag if on a monorepo pub(crate) fn get_latest_tag(&self) -> Result { let tags: Vec = self.all_tags()?; @@ -71,7 +85,7 @@ impl Repository { let pattern = SETTINGS .tag_prefix .as_ref() - .map(|prefix| format!("{}*", prefix)); + .map(|prefix| format!("{prefix}*")); // Collect non packages tags let mut tags: Vec = self @@ -142,6 +156,13 @@ impl Default for Tag { } impl Tag { + pub(crate) fn strip_metadata(&self) -> Self { + let mut copy_without_prefix = self.clone(); + copy_without_prefix.package = None; + copy_without_prefix.prefix = None; + copy_without_prefix + } + // Tag always contains an oid unless it was created before the tag exist. // The only case where we do that is while creating the changelog during `cog bump`. // In this situation we need a tag to generate the changelog but this tag does not exist in the @@ -230,12 +251,12 @@ impl fmt::Display for Tag { let version = self.version.to_string(); if let Some((package, prefix)) = self.package.as_ref().zip(self.prefix.as_ref()) { let separator = SETTINGS.monorepo_separator().unwrap_or_else(|| - panic!("Found a tag with monorepo package prefix but 'monorepo_version_separator' is not defined") + panic!("Found a tag with monorepo package prefix but there are no packages in cog.toml") ); write!(f, "{package}{separator}{prefix}{version}") } else if let Some(package) = self.package.as_ref() { let separator = SETTINGS.monorepo_separator().unwrap_or_else(|| - panic!("Found a tag with monorepo package prefix but 'monorepo_version_separator' is not defined") + panic!("Found a tag with monorepo package prefix but there are no packages in cog.toml") ); write!(f, "{package}{separator}{version}") diff --git a/src/hook/mod.rs b/src/hook/mod.rs index f9777475..d6c31dfb 100644 --- a/src/hook/mod.rs +++ b/src/hook/mod.rs @@ -1,7 +1,7 @@ mod error; mod parser; -use std::collections::VecDeque; +use std::collections::{HashMap, VecDeque}; use std::ops::Range; use std::process::Command; use std::str::FromStr; @@ -10,8 +10,33 @@ use std::{fmt, path}; use crate::Tag; use parser::Token; +use crate::settings::{BumpProfile, HookType}; use anyhow::{anyhow, ensure, Result}; +pub trait Hooks { + fn bump_profiles(&self) -> &HashMap; + fn pre_bump_hooks(&self) -> &Vec; + fn post_bump_hooks(&self) -> &Vec; + + fn get_hooks(&self, hook_type: HookType) -> &Vec { + match hook_type { + HookType::PreBump => self.pre_bump_hooks(), + HookType::PostBump => self.post_bump_hooks(), + } + } + + fn get_profile_hooks(&self, profile: &str, hook_type: HookType) -> &Vec { + let profile = self + .bump_profiles() + .get(profile) + .expect("Bump profile not found"); + match hook_type { + HookType::PreBump => &profile.pre_bump_hooks, + HookType::PostBump => &profile.post_bump_hooks, + } + } +} + #[derive(Debug, Eq, PartialEq)] pub struct VersionSpan { range: Range, @@ -31,18 +56,33 @@ impl HookVersion { impl VersionSpan { pub(crate) fn build_version_str( &mut self, - version: &HookVersion, + version: Option<&HookVersion>, latest: Option<&HookVersion>, ) -> Result { - let version = version.prefixed_tag.version.clone(); - let latest = latest.map(|version| version.prefixed_tag.version.clone()); - // According to the pest grammar, a `version` or `latest_version` token is expected first - let mut version = match self.tokens.pop_front() { - Some(Token::Version) => Ok(version), - Some(Token::LatestVersion) => { - latest.ok_or_else(|| anyhow!("No previous tag found to replace {{latest}} version")) + let mut tag = match self.tokens.pop_front() { + Some(Token::Version) => version + .map(|version| version.prefixed_tag.strip_metadata()) + .ok_or_else(|| anyhow!("No previous tag found to replace {{{{version}}}} version")), + Some(Token::LatestVersion) => latest + .map(|version| version.prefixed_tag.strip_metadata()) + .ok_or_else(|| anyhow!("No previous tag found to replace {{{{latest}}}} version")), + Some(Token::LatestVersionTag) => latest + .map(|version| version.prefixed_tag.clone()) + .ok_or_else(|| { + anyhow!("No previous tag found to replace {{{{latest_tag}}}} version") + }), + Some(Token::VersionTag) => version + .map(|version| version.prefixed_tag.clone()) + .ok_or_else(|| { + anyhow!("No previous tag found to replace {{{{version_tag}}}} version") + }), + Some(Token::Package) => { + return version + .and_then(|version| version.prefixed_tag.package.clone()) + .ok_or_else(|| anyhow!("Current tag as no {{{{package}}}} info")) } + _ => unreachable!("Unexpected parsing error"), }?; @@ -56,23 +96,23 @@ impl VersionSpan { Token::Amount(amt) => amount = amt, // increments ... Token::Major => { - version.major += amount; - version.minor = 0; - version.patch = 0; + tag.version.major += amount; + tag.version.minor = 0; + tag.version.patch = 0; } Token::Minor => { - version.minor += amount; - version.patch = 0; + tag.version.minor += amount; + tag.version.patch = 0; } - Token::Patch => version.patch += amount, + Token::Patch => tag.version.patch += amount, // set build metadata and prerelease - Token::PreRelease(pre_release) => version.pre = pre_release, - Token::BuildMetadata(build) => version.build = build, + Token::PreRelease(pre_release) => tag.version.pre = pre_release, + Token::BuildMetadata(build) => tag.version.build = build, _ => unreachable!("Unexpected parsing error"), } } - Ok(version.to_string()) + Ok(tag.to_string()) } } @@ -85,7 +125,7 @@ pub struct HookSpan { impl HookSpan { fn replace_versions( &mut self, - version: &HookVersion, + version: Option<&HookVersion>, latest: Option<&HookVersion>, ) -> Result { let mut output = self.content.clone(); @@ -122,7 +162,7 @@ impl Hook { pub(crate) fn insert_versions( &mut self, current_version: Option<&HookVersion>, - next_version: &HookVersion, + next_version: Option<&HookVersion>, ) -> Result<()> { let mut parts = parser::parse(&self.0)?; self.0 = parts.replace_versions(next_version, current_version)?; @@ -144,13 +184,17 @@ impl Hook { #[cfg(test)] mod test { + use cmd_lib::run_cmd; use git2::Repository; + use std::collections::HashMap; use std::str::FromStr; use crate::{Result, Tag}; use crate::hook::{Hook, HookVersion}; + use crate::settings::{MonoRepoPackage, Settings}; use sealed_test::prelude::*; + use semver::Version; use speculoos::prelude::*; #[test] @@ -166,20 +210,102 @@ mod test { Ok(()) } + #[test] + fn parse_current_version() -> Result<()> { + let hook = Hook::from_str("cargo bump {{version_tag}}")?; + assert_that!(hook.0.as_str()).is_equal_to("cargo bump {{version_tag}}"); + Ok(()) + } + + #[test] + fn parse_latest_tag() -> Result<()> { + let hook = Hook::from_str("cargo bump {{latest_tag}}")?; + assert_that!(hook.0.as_str()).is_equal_to("cargo bump {{latest_tag}}"); + Ok(()) + } + #[test] fn replace_version_cargo() -> Result<()> { let mut hook = Hook::from_str("cargo bump {{version}}")?; - hook.insert_versions(None, &HookVersion::new(Tag::from_str("1.0.0", None)?)) + hook.insert_versions(None, Some(&HookVersion::new(Tag::from_str("1.0.0", None)?))) .unwrap(); assert_that!(hook.0.as_str()).is_equal_to("cargo bump 1.0.0"); Ok(()) } + #[test] + fn replace_version_tag_cargo() -> Result<()> { + let mut hook = Hook::from_str("cargo bump {{version_tag}}")?; + let tag = Tag { + package: None, + prefix: Some("v".to_string()), + version: Version::new(1, 0, 0), + oid: None, + }; + + hook.insert_versions(None, Some(&HookVersion::new(tag))) + .unwrap(); + + assert_that!(hook.0.as_str()).is_equal_to("cargo bump v1.0.0"); + Ok(()) + } + + #[sealed_test] + fn replace_version_tag_with_package() -> Result<()> { + let mut packages = HashMap::new(); + packages.insert("cog".to_string(), MonoRepoPackage::default()); + let settings = Settings { + packages, + ..Default::default() + }; + + let settings = toml::to_string(&settings)?; + + run_cmd!( + git init; + echo $settings > cog.toml; + git add .; + git commit -m "first commit"; + )?; + + let mut hook = Hook::from_str("echo {{version_tag}}")?; + + let tag = Tag { + package: Some("cog".to_string()), + prefix: Some("v".to_string()), + version: Version::new(1, 0, 0), + oid: None, + }; + + hook.insert_versions(None, Some(&HookVersion::new(tag))) + .unwrap(); + + assert_that!(hook.0.as_str()).is_equal_to("echo cog-v1.0.0"); + Ok(()) + } + + #[test] + fn replace_latest_tag() -> Result<()> { + let mut hook = Hook::from_str("echo {{latest_tag}}")?; + let tag = Tag { + package: None, + prefix: Some("v".to_string()), + version: Version::new(1, 0, 0), + oid: None, + }; + + hook.insert_versions(Some(&HookVersion::new(tag)), None) + .unwrap(); + + assert_that!(hook.0.as_str()).is_equal_to("echo v1.0.0"); + Ok(()) + } + #[test] fn replace_maven_version() -> Result<()> { let mut hook = Hook::from_str("mvn versions:set -DnewVersion={{version}}")?; - hook.insert_versions(None, &HookVersion::new(Tag::from_str("1.0.0", None)?)) + hook.insert_versions(None, Some(&HookVersion::new(Tag::from_str("1.0.0", None)?))) .unwrap(); assert_that!(hook.0.as_str()).is_equal_to("mvn versions:set -DnewVersion=1.0.0"); @@ -189,17 +315,71 @@ mod test { #[test] fn replace_maven_version_with_expression() -> Result<()> { let mut hook = Hook::from_str("mvn versions:set -DnewVersion={{version+1minor-SNAPSHOT}}")?; - hook.insert_versions(None, &HookVersion::new(Tag::from_str("1.0.0", None)?)) + hook.insert_versions(None, Some(&HookVersion::new(Tag::from_str("1.0.0", None)?))) .unwrap(); assert_that!(hook.0.as_str()).is_equal_to("mvn versions:set -DnewVersion=1.1.0-SNAPSHOT"); Ok(()) } + #[test] + fn replace_version_tag_with_expression() -> Result<()> { + let mut hook = + Hook::from_str("mvn versions:set -DnewVersion={{version_tag+1minor-SNAPSHOT}}")?; + let tag = Tag { + package: None, + prefix: Some("v".to_string()), + version: Version::new(1, 0, 0), + oid: None, + }; + + hook.insert_versions(None, Some(&HookVersion::new(tag))) + .unwrap(); + + assert_that!(hook.0.as_str()).is_equal_to("mvn versions:set -DnewVersion=v1.1.0-SNAPSHOT"); + Ok(()) + } + + #[sealed_test] + fn replace_package_version_tag_with_expression() -> Result<()> { + let mut packages = HashMap::new(); + packages.insert("cog".to_string(), MonoRepoPackage::default()); + let settings = Settings { + packages, + ..Default::default() + }; + + let settings = toml::to_string(&settings)?; + + run_cmd!( + git init; + echo $settings > cog.toml; + git add .; + git commit -m "first commit"; + )?; + + let mut hook = + Hook::from_str("mvn versions:set -DnewVersion={{version_tag+1minor-SNAPSHOT}}")?; + + let tag = Tag { + package: Some("cog".to_string()), + prefix: Some("v".to_string()), + version: Version::new(1, 0, 0), + oid: None, + }; + + hook.insert_versions(None, Some(&HookVersion::new(tag))) + .unwrap(); + + assert_that!(hook.0.as_str()) + .is_equal_to("mvn versions:set -DnewVersion=cog-v1.1.0-SNAPSHOT"); + Ok(()) + } + #[test] fn leave_hook_untouched_when_no_version() -> Result<()> { let mut hook = Hook::from_str("echo \"Hello World\"")?; - hook.insert_versions(None, &HookVersion::new(Tag::from_str("1.0.0", None)?)) + hook.insert_versions(None, Some(&HookVersion::new(Tag::from_str("1.0.0", None)?))) .unwrap(); assert_that!(hook.0.as_str()).is_equal_to("echo \"Hello World\""); @@ -209,7 +389,7 @@ mod test { #[test] fn replace_quoted_version() -> Result<()> { let mut hook = Hook::from_str("echo \"{{version}}\"")?; - hook.insert_versions(None, &HookVersion::new(Tag::from_str("1.0.0", None)?)) + hook.insert_versions(None, Some(&HookVersion::new(Tag::from_str("1.0.0", None)?))) .unwrap(); assert_that!(hook.0.as_str()).is_equal_to("echo \"1.0.0\""); @@ -220,7 +400,7 @@ mod test { fn replace_version_with_nested_simple_quoted_arg() -> Result<()> { let mut hook = Hook::from_str("cog commit chore 'bump snapshot to {{version+1minor-pre}}'")?; - hook.insert_versions(None, &HookVersion::new(Tag::from_str("1.0.0", None)?)) + hook.insert_versions(None, Some(&HookVersion::new(Tag::from_str("1.0.0", None)?))) .unwrap(); assert_that!(hook.0.as_str()).is_equal_to("cog commit chore 'bump snapshot to 1.1.0-pre'"); @@ -231,7 +411,7 @@ mod test { fn replace_version_with_nested_double_quoted_arg() -> Result<()> { let mut hook = Hook::from_str("cog commit chore \"bump snapshot to {{version+1minor-pre}}\"")?; - hook.insert_versions(None, &HookVersion::new(Tag::from_str("1.0.0", None)?)) + hook.insert_versions(None, Some(&HookVersion::new(Tag::from_str("1.0.0", None)?))) .unwrap(); assert_that!(hook.0.as_str()) @@ -244,7 +424,7 @@ mod test { let mut hook = Hook::from_str("echo \"the latest {{latest}}, the greatest {{version}}\"")?; hook.insert_versions( Some(&HookVersion::new(Tag::from_str("0.5.9", None)?)), - &HookVersion::new(Tag::from_str("1.0.0", None)?), + Some(&HookVersion::new(Tag::from_str("1.0.0", None)?)), ) .unwrap(); @@ -259,7 +439,7 @@ mod test { )?; hook.insert_versions( Some(&HookVersion::new(Tag::from_str("0.5.9", None)?)), - &HookVersion::new(Tag::from_str("1.0.0", None)?), + Some(&HookVersion::new(Tag::from_str("1.0.0", None)?)), ) .unwrap(); @@ -271,7 +451,7 @@ mod test { fn replace_version_with_pre_and_build_metadata() -> Result<()> { let mut hook = Hook::from_str("echo \"the latest {{version+1major-pre.alpha-bravo+build.42}}\"")?; - hook.insert_versions(None, &HookVersion::new(Tag::from_str("1.0.0", None)?)) + hook.insert_versions(None, Some(&HookVersion::new(Tag::from_str("1.0.0", None)?))) .unwrap(); assert_that!(hook.0.as_str()) @@ -279,13 +459,34 @@ mod test { Ok(()) } + #[test] + fn replace_version_tag_with_pre_and_build_metadata() -> Result<()> { + let mut hook = + Hook::from_str("echo \"the latest {{version_tag+1major-pre.alpha-bravo+build.42}}\"")?; + + let tag = Tag { + package: None, + prefix: Some("v".to_string()), + version: Version::new(1, 0, 0), + oid: None, + }; + + hook.insert_versions(None, Some(&HookVersion::new(tag))) + .unwrap(); + + assert_that!(hook.0.as_str()) + .is_equal_to("echo \"the latest v2.0.0-pre.alpha-bravo+build.42\""); + + Ok(()) + } + #[sealed_test] fn parenthesis_in_hook_works() -> Result<()> { Repository::init(".")?; let mut hook = Hook::from_str("git commit --allow-empty -m 'chore(snapshot): bump snapshot to {{version+1patch-SNAPSHOT}}'")?; - hook.insert_versions(None, &HookVersion::new(Tag::from_str("1.0.0", None)?)) + hook.insert_versions(None, Some(&HookVersion::new(Tag::from_str("1.0.0", None)?))) .unwrap(); let outcome = hook.run(None); @@ -294,4 +495,51 @@ mod test { Ok(()) } + + #[sealed_test] + fn replace_package_name_and_version_tag_with_expression() -> Result<()> { + let mut packages = HashMap::new(); + packages.insert("cog".to_string(), MonoRepoPackage::default()); + let settings = Settings { + packages, + ..Default::default() + }; + + let settings = toml::to_string(&settings)?; + + run_cmd!( + git init; + echo $settings > cog.toml; + git add .; + git commit -m "first commit"; + )?; + + let mut hook = Hook::from_str( + r#"echo "{{package}}, version: {{version}}, tag: {{version_tag}}, current: {{latest}}, current_tag: {{latest_tag}}""#, + )?; + + let current = Tag { + package: Some("cog".to_string()), + prefix: Some("v".to_string()), + version: Version::new(1, 0, 0), + oid: None, + }; + + let tag = Tag { + package: Some("cog".to_string()), + prefix: Some("v".to_string()), + version: Version::new(1, 1, 0), + oid: None, + }; + + hook.insert_versions( + Some(&HookVersion::new(current)), + Some(&HookVersion::new(tag)), + ) + .unwrap(); + + assert_that!(hook.0.as_str()) + .is_equal_to(r#"echo "cog, version: 1.1.0, tag: cog-v1.1.0, current: 1.0.0, current_tag: cog-v1.0.0""#); + Ok(()) + } } diff --git a/src/hook/parser.rs b/src/hook/parser.rs index b14295ec..0f0efcb0 100644 --- a/src/hook/parser.rs +++ b/src/hook/parser.rs @@ -16,14 +16,17 @@ struct HookDslParser; #[derive(Debug, Eq, PartialEq)] pub enum Token { Version, + VersionTag, LatestVersion, + LatestVersionTag, + Package, Amount(u64), Add, Major, Minor, Patch, - PreRelease(semver::Prerelease), - BuildMetadata(semver::BuildMetadata), + PreRelease(Prerelease), + BuildMetadata(BuildMetadata), } pub fn parse(hook: &str) -> Result { @@ -55,7 +58,10 @@ fn parse_version(pair: Pair) -> Result { for pair in pair.into_inner() { match pair.as_rule() { Rule::current_version => tokens.push_back(Token::Version), + Rule::current_tag => tokens.push_back(Token::VersionTag), Rule::latest_version => tokens.push_back(Token::LatestVersion), + Rule::latest_tag => tokens.push_back(Token::LatestVersionTag), + Rule::package => tokens.push_back(Token::Package), Rule::ops => parse_operator(&mut tokens, pair.into_inner())?, Rule::pre_release => { let identifiers = pair.into_inner().next().unwrap(); @@ -122,6 +128,29 @@ mod test { }); } + #[test] + fn parse_version_tag() -> anyhow::Result<()> { + let span = + parser::parse("the latest {{latest_tag+1minor}}, the greatest {{version_tag+patch}}")?; + + assert_that!(&span.version_spans).contains(&VersionSpan { + range: 11..32, + tokens: VecDeque::from(vec![ + Token::LatestVersionTag, + Token::Add, + Token::Amount(1), + Token::Minor, + ]), + }); + + assert_that!(&span.version_spans).contains(&VersionSpan { + range: 47..68, + tokens: VecDeque::from(vec![Token::VersionTag, Token::Add, Token::Patch]), + }); + + Ok(()) + } + #[test] fn parse_version_with_pre_release() { let result = parser::parse("the greatest {{version+patch-pre.alpha0}}"); @@ -139,6 +168,18 @@ mod test { }); } + #[test] + fn parse_package() { + let result = parser::parse("version package: {{package}}"); + assert_that!(result) + .is_ok() + .map(|span| &span.version_spans) + .contains(&VersionSpan { + range: 17..28, + tokens: VecDeque::from(vec![Token::Package]), + }); + } + #[test] fn invalid_dsl_is_err() { let result = parser::parse("the greatest {{+patch-pre.alpha0}}"); diff --git a/src/hook/version_dsl.pest b/src/hook/version_dsl.pest index 8b16127d..23183b19 100644 --- a/src/hook/version_dsl.pest +++ b/src/hook/version_dsl.pest @@ -8,6 +8,8 @@ delimiter_start = _{ "{{" } delimiter_end = _{ "}}" } current_version = { "version" } latest_version = { "latest" } +current_tag = { "version_tag" } +latest_tag = { "latest_tag" } add = { "+" } @@ -29,5 +31,9 @@ pre_release = { pre_release_separator ~ identifiers } build_metadata = { build_metadata_separator ~ identifiers } -version = { delimiter_start ~ (current_version | latest_version) ~ ops* ~ pre_release? ~ build_metadata? ~ delimiter_end} -version_dsl = { SOI ~ (version | (!delimiter_start ~ ANY) )* ~ EOI } +package = { "package" } +version = { delimiter_start ~ ( + ((current_tag | current_version | latest_tag | latest_version) ~ ops* ~ pre_release? ~ build_metadata?) + | package + ) ~ delimiter_end} +version_dsl = { SOI ~ ( version | (!delimiter_start ~ ANY) )* ~ EOI } diff --git a/src/lib.rs b/src/lib.rs index c60b2e3e..ccce97d8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,6 @@ use std::collections::HashMap; +use std::path::PathBuf; +use std::process::{Command, Stdio}; use anyhow::Result; @@ -11,7 +13,7 @@ use conventional::version::IncrementCommand; use error::BumpError; use git::repository::Repository; -use settings::{HookType, Settings}; +use settings::Settings; use crate::git::error::{Git2Error, TagError}; @@ -48,6 +50,13 @@ pub struct CocoGitto { repository: Repository, } +pub enum CommitHook { + PreCommit, + PrepareCommitMessage(String), + CommitMessage, + PostCommit, +} + impl CocoGitto { pub fn get() -> Result { let repository = Repository::open(&std::env::current_dir()?)?; @@ -95,4 +104,46 @@ impl CocoGitto { Ok(conventional_message) } + + pub fn run_commit_hook(&self, hook: CommitHook) -> Result<(), Git2Error> { + let repo_dir = self.repository.get_repo_dir().expect("git repository"); + let hooks_dir = repo_dir.join(".git/hooks"); + let edit_message = repo_dir.join(".git/COMMIT_EDITMSG"); + let edit_message = edit_message.to_string_lossy(); + + let (hook_path, args) = match hook { + CommitHook::PreCommit => (hooks_dir.join("pre-commit"), vec![]), + CommitHook::PrepareCommitMessage(template) => ( + hooks_dir.join("prepare-commit-msg"), + vec![edit_message.to_string(), template], + ), + CommitHook::CommitMessage => { + (hooks_dir.join("commit-msg"), vec![edit_message.to_string()]) + } + CommitHook::PostCommit => (hooks_dir.join("post-commit"), vec![]), + }; + + if hook_path.exists() { + let status = Command::new(hook_path) + .args(args) + .stdout(Stdio::inherit()) + .stdin(Stdio::inherit()) + .stderr(Stdio::inherit()) + .output()? + .status; + + if !status.success() { + return Err(Git2Error::GitHookNonZeroExit(status.code().unwrap_or(1))); + } + } + + Ok(()) + } + + pub fn prepare_edit_message_path(&self) -> PathBuf { + self.repository + .get_repo_dir() + .map(|path| path.join(".git/COMMIT_EDITMSG")) + .expect("git repository") + } } diff --git a/src/log/output.rs b/src/log/output.rs index 82aea7db..c3f1852d 100644 --- a/src/log/output.rs +++ b/src/log/output.rs @@ -140,7 +140,7 @@ impl OutputBuilder { let pager = if AsRef::::as_ref(&pager).exists() { PathBuf::from(pager) } else { - which::which(&pager).context(format!("Cannot find pager `{}`", pager))? + which::which(&pager).context(format!("Cannot find pager `{pager}`"))? }; let args = words.collect(); diff --git a/src/settings/mod.rs b/src/settings/mod.rs index 8ba0602d..21de2363 100644 --- a/src/settings/mod.rs +++ b/src/settings/mod.rs @@ -1,4 +1,6 @@ use std::collections::HashMap; +use std::fmt; +use std::fmt::Formatter; use std::path::PathBuf; use crate::conventional::commit::CommitConfig; @@ -7,7 +9,7 @@ use crate::{CommitsMetadata, CONFIG_PATH, SETTINGS}; use crate::conventional::changelog::error::ChangelogError; use crate::conventional::changelog::template::{RemoteContext, Template}; -use crate::git::hook::Hooks; +use crate::hook::Hooks; use crate::settings::error::SettingError; use config::{Config, File}; use conventional_commit_parser::commit::CommitType; @@ -25,36 +27,145 @@ pub enum HookType { PostBump, } -#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Default)] -#[serde(deny_unknown_fields)] +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq)] +#[serde(deny_unknown_fields, default)] pub struct Settings { - #[serde(default)] pub from_latest_tag: bool, - #[serde(default)] pub ignore_merge_commits: bool, - #[serde(default)] + pub generate_mono_repository_global_tag: bool, pub monorepo_version_separator: Option, - #[serde(default)] pub branch_whitelist: Vec, pub tag_prefix: Option, - #[serde(default)] pub pre_bump_hooks: Vec, - #[serde(default)] pub post_bump_hooks: Vec, - #[serde(default)] pub pre_package_bump_hooks: Vec, - #[serde(default)] pub post_package_bump_hooks: Vec, - #[serde(default)] + pub git_hooks: HashMap, pub commit_types: CommitsMetadataSettings, - #[serde(default)] pub changelog: Changelog, - #[serde(default)] pub bump_profiles: HashMap, - #[serde(default)] pub packages: HashMap, } +impl Default for Settings { + fn default() -> Self { + Self { + from_latest_tag: false, + ignore_merge_commits: false, + generate_mono_repository_global_tag: true, + monorepo_version_separator: None, + branch_whitelist: vec![], + tag_prefix: None, + pre_bump_hooks: vec![], + post_bump_hooks: vec![], + pre_package_bump_hooks: vec![], + post_package_bump_hooks: vec![], + git_hooks: HashMap::new(), + commit_types: Default::default(), + changelog: Default::default(), + bump_profiles: Default::default(), + packages: Default::default(), + } + } +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Hash, Copy, Clone)] +#[serde(deny_unknown_fields, rename_all = "kebab-case", into = "&str")] +pub enum GitHookType { + ApplypatchMsg, + PreApplypatch, + PostApplypatch, + PreCommit, + PreMergeCommit, + PrePrepareCommitMsg, + CommitMsg, + PostCommit, + PreRebase, + PostCheckout, + PostMerge, + PrePush, + PreAutoGc, + PostRewrite, + SendemailValidate, + FsmonitorWatchman, + P4Changelist, + P4PrepareChangelist, + P4Postchangelist, + P4PreSubmit, + PostIndexChange, +} + +impl From for GitHookType { + fn from(value: String) -> Self { + match value.as_str() { + "applypatch-msg" => Self::ApplypatchMsg, + "pre-applypatch" => Self::PreApplypatch, + "post-applypatch" => Self::PostApplypatch, + "pre-commit" => Self::PreCommit, + "pre-merge-commit" => Self::PreMergeCommit, + "pre-commit-msg" => Self::PrePrepareCommitMsg, + "commit-msg" => Self::CommitMsg, + "post-commit" => Self::PostCommit, + "pre-rebase" => Self::PreRebase, + "post-checkout" => Self::PostCheckout, + "post-merge" => Self::PostMerge, + "pre-push" => Self::PrePush, + "pre-auto-gc" => Self::PreAutoGc, + "post-rewrite" => Self::PostRewrite, + "sendemail-validate" => Self::SendemailValidate, + "fsmonitor-watchman" => Self::FsmonitorWatchman, + "p4-changelist" => Self::P4Changelist, + "p4-prepare-changelist" => Self::P4PrepareChangelist, + "p4-postchangelist" => Self::P4Postchangelist, + "p4-pre-submit" => Self::P4PreSubmit, + "post-index-change" => Self::PostIndexChange, + _ => unreachable!(), + } + } +} + +impl From for &str { + fn from(val: GitHookType) -> Self { + match val { + GitHookType::ApplypatchMsg => "applypatch-msg", + GitHookType::PreApplypatch => "pre-applypatch", + GitHookType::PostApplypatch => "post-applypatch", + GitHookType::PreCommit => "pre-commit", + GitHookType::PreMergeCommit => "pre-merge-commit", + GitHookType::PrePrepareCommitMsg => "pre-commit-msg", + GitHookType::CommitMsg => "commit-msg", + GitHookType::PostCommit => "post-commit", + GitHookType::PreRebase => "pre-rebase", + GitHookType::PostCheckout => "post-checkout", + GitHookType::PostMerge => "post-merge", + GitHookType::PrePush => "pre-push", + GitHookType::PreAutoGc => "pre-auto-gc", + GitHookType::PostRewrite => "post-rewrite", + GitHookType::SendemailValidate => "sendemail-validate", + GitHookType::FsmonitorWatchman => "fsmonitor-watchman", + GitHookType::P4Changelist => "p4-changelist", + GitHookType::P4PrepareChangelist => "p4-prepare-changelist", + GitHookType::P4Postchangelist => "p4-postchangelist", + GitHookType::P4PreSubmit => "p4-pre-submit", + GitHookType::PostIndexChange => "post-index-change", + } + } +} + +impl fmt::Display for GitHookType { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let value: &str = (*self).into(); + write!(f, "{}", value) + } +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq)] +#[serde(deny_unknown_fields, untagged)] +pub enum GitHook { + Script { script: String }, + File { path: PathBuf }, +} + #[derive(Debug, Deserialize, Serialize, Eq, PartialEq)] #[serde(deny_unknown_fields, default)] pub struct MonoRepoPackage { diff --git a/tests/assets/commit_message.txt b/tests/assets/commit_message.txt new file mode 100644 index 00000000..493355f2 --- /dev/null +++ b/tests/assets/commit_message.txt @@ -0,0 +1 @@ +chore: a commit message diff --git a/tests/cog_tests/changelog.rs b/tests/cog_tests/changelog.rs index 54c74f42..31184708 100644 --- a/tests/cog_tests/changelog.rs +++ b/tests/cog_tests/changelog.rs @@ -362,7 +362,7 @@ fn get_changelog_from_tag_to_tagged_head() -> Result<()> { } #[sealed_test] -fn get_changelog_whith_custom_template() -> Result<()> { +fn get_changelog_with_custom_template() -> Result<()> { // Arrange let crate_dir = env!("CARGO_MANIFEST_DIR"); let template = PathBuf::from(crate_dir).join("tests/cog_tests/template.md"); @@ -437,3 +437,66 @@ fn get_changelog_whith_custom_template() -> Result<()> { ); Ok(()) } + +#[sealed_test] +/// Test that the `omit_from_changelog` configuration +/// directive is honored if/when it is specified for +/// a given commit type. +fn ensure_omit_from_changelog_is_honored() -> Result<()> { + // Arrange + git_init()?; + + let cog_toml = indoc!( + "[changelog] + remote = \"github.com\" + repository = \"test\" + owner = \"test\" + + [commit_types] + wip = { changelog_title = \"Work In Progress\", omit_from_changelog = false }" + ); + + let _setup = ( + run_cmd!(echo $cog_toml > cog.toml;)?, + fs::read_to_string("cog.toml")?, + git_commit("chore: init")?, + git_commit("wip(some-scope): getting there")?, + git_tag("1.0.0")?, + ); + + let changelog = Command::cargo_bin("cog")? + .arg("changelog") + // Assert + .assert() + .success(); + + let changelog = changelog.get_output(); + let changelog = String::from_utf8_lossy(&changelog.stdout); + + assert!( + changelog.as_ref().contains("#### Work In Progress"), + "Expected changelog to contain a \"Work In Progress\" entry but got:\n\n{}", + changelog.as_ref() + ); + + let cog_toml = cog_toml.replace("omit_from_changelog = false", "omit_from_changelog = true"); + + run_cmd!(echo $cog_toml > cog.toml;)?; + + let changelog = Command::cargo_bin("cog")? + .arg("changelog") + // Assert + .assert() + .success(); + + let changelog = changelog.get_output(); + let changelog = String::from_utf8_lossy(&changelog.stdout); + + assert!( + !changelog.as_ref().contains("#### Work In Progress"), + "Expected \"Work In Progress\" entry to be omitted from changelog but got:\n\n{}", + changelog.as_ref() + ); + + Ok(()) +} diff --git a/tests/cog_tests/check.rs b/tests/cog_tests/check.rs index aad069c3..8f8f2e42 100644 --- a/tests/cog_tests/check.rs +++ b/tests/cog_tests/check.rs @@ -83,3 +83,63 @@ fn cog_check_from_latest_tag_failure() -> Result<()> { .stderr(predicate::str::contains("Found 1 non compliant commits")); Ok(()) } + +#[sealed_test] +fn cog_check_commit_range_ok() -> Result<()> { + // Arrange + git_init()?; + let range_start = git_commit("chore: init")?; + git_commit("feat: feature")?; + let range_end = git_commit("fix: bug fix")?; + let range = format!("{range_start}..{range_end}"); + + // Act + Command::cargo_bin("cog")? + .arg("check") + .arg(range) + // Assert + .assert() + .success() + .stderr(predicate::str::contains("No errored commits")); + Ok(()) +} + +#[sealed_test] +fn cog_check_commit_range_failure() -> Result<()> { + // Arrange + git_init()?; + let range_start = git_commit("chore: init")?; + git_commit("toto: errored commit")?; + git_commit("feat: feature")?; + git_commit("fix: bug fix")?; + let range_end = git_commit("toto: africa")?; + let range = format!("{range_start}..{range_end}"); + + // Act + Command::cargo_bin("cog")? + .arg("check") + .arg(range) + // Assert + .assert() + .failure() + .stderr(predicate::str::contains("Found 2 non compliant commits")); + Ok(()) +} + +#[sealed_test] +fn cog_check_from_latest_tag_and_commit_range_failure() -> Result<()> { + // Arrange + + // Act + Command::cargo_bin("cog")? + .arg("check") + .arg("--from-latest-tag") + .arg("abcdef..fedcba") + // Assert + .assert() + .failure() + .stderr(predicate::str::contains( + "the argument '--from-latest-tag' cannot be used with '[RANGE]'", + )); + Ok(()) +} diff --git a/tests/cog_tests/get_version.rs b/tests/cog_tests/get_version.rs new file mode 100644 index 00000000..961a5bc9 --- /dev/null +++ b/tests/cog_tests/get_version.rs @@ -0,0 +1,231 @@ +use std::process::Command; + +use anyhow::Result; +use assert_cmd::prelude::*; +use predicates::prelude::predicate; +use sealed_test::prelude::*; + +use cocogitto::settings::Settings; + +use crate::helpers::*; + +#[sealed_test] +fn get_initial_version_expected_error() -> Result<()> { + git_init()?; + git_commit("chore: init")?; + git_commit("feat(taef): feature")?; + + Command::cargo_bin("cog")? + .arg("get-version") + .assert() + .failure() + .stderr(predicate::str::starts_with("Error: No version yet\n")); + + Ok(()) +} + +#[sealed_test] +fn get_initial_version_fallback_ok() -> Result<()> { + git_init()?; + git_commit("chore: init")?; + git_commit("feat(taef): feature")?; + + Command::cargo_bin("cog")? + .arg("get-version") + .arg("-f") + .arg("2.1.0-Test+xx") + .assert() + .success() + .stdout(predicate::eq(b"2.1.0-Test+xx\n" as &[u8])); + + Ok(()) +} + +#[sealed_test] +fn get_initial_version_invalid_fallback_parse_error() -> Result<()> { + git_init()?; + git_commit("chore: init")?; + git_commit("feat(taef): feature")?; + + Command::cargo_bin("cog")? + .arg("get-version") + .arg("-f") + .arg("InvalidVersion") + .assert() + .failure() + .stderr(predicate::str::starts_with( + "Invalid fallback: InvalidVersion\n", + )); + + Ok(()) +} + +#[sealed_test] +fn get_version_invalid_fallback_parse_error_in_versioned_repo() -> Result<()> { + git_init()?; + git_commit("chore: init")?; + git_commit("feat(taef): feature")?; + + Command::cargo_bin("cog")? + .arg("bump") + .arg("--auto") + .assert() + .success(); + + Command::cargo_bin("cog")? + .arg("get-version") + .arg("-f") + .arg("InvalidVersion") + .assert() + .failure() + .stderr(predicate::str::starts_with( + "Invalid fallback: InvalidVersion\n", + )); + + Ok(()) +} + +#[sealed_test] +fn get_initial_version_fails_as_expected_error() -> Result<()> { + git_init()?; + git_commit("chore: init")?; + git_commit("feat(taef): feature")?; + + Command::cargo_bin("cog")? + .arg("get-version") + .assert() + .failure() + .stderr(predicate::str::starts_with("Error: No version yet\n")); + + Ok(()) +} + +#[sealed_test] +fn get_version_after_bump_ok() -> Result<()> { + git_init()?; + git_commit("chore: init")?; + git_commit("feat(taef): feature")?; + + Command::cargo_bin("cog")? + .arg("bump") + .arg("--auto") + .assert() + .success(); + + Command::cargo_bin("cog")? + .arg("get-version") + .assert() + .success() + .stdout(predicate::eq(b"0.1.0\n" as &[u8])) + .stderr(predicate::eq(b"Current version:\n" as &[u8])); + + Ok(()) +} + +#[sealed_test] +fn get_version_after_bump_fallback_not_used_ok() -> Result<()> { + git_init()?; + git_commit("chore: init")?; + git_commit("feat(taef): feature")?; + + Command::cargo_bin("cog")? + .arg("bump") + .arg("--auto") + .assert() + .success(); + + Command::cargo_bin("cog")? + .arg("get-version") + .arg("-f") + .arg("2.1.0-Test+xx") + .assert() + .success() + .stdout(predicate::eq(b"0.1.0\n" as &[u8])) + .stderr(predicate::eq(b"Current version:\n" as &[u8])); + + Ok(()) +} + +#[sealed_test] +fn get_initial_version_of_monorepo_expected_error() -> Result<()> { + init_monorepo(&mut Settings::default())?; + + Command::cargo_bin("cog")? + .arg("get-version") + .assert() + .failure() + .stderr(predicate::eq(b"Error: No version yet\n" as &[u8])); + + Ok(()) +} + +#[sealed_test] +fn get_initial_version_of_monorepo_package_expected_error() -> Result<()> { + init_monorepo(&mut Settings::default())?; + + Command::cargo_bin("cog")? + .arg("get-version") + .arg("--package=one") + .assert() + .failure() + .stderr(predicate::eq(b"Error: No version yet\n" as &[u8])); + + Ok(()) +} + +#[sealed_test] +fn get_version_of_monorepo_package_having_no_own_version_expected_error() -> Result<()> { + init_monorepo(&mut Settings::default())?; + + Command::cargo_bin("cog")? + .arg("bump") + .arg("--patch") + .assert() + .success(); + + Command::cargo_bin("cog")? + .arg("get-version") + .arg("--package=one") + .assert() + .failure() + .stderr(predicate::eq(b"Error: No version yet\n" as &[u8])); + + // Should differ + Command::cargo_bin("cog")? + .arg("get-version") + .assert() + .success() + .stdout(predicate::eq(b"0.0.1\n" as &[u8])) + .stderr(predicate::eq(b"Current version:\n" as &[u8])); + + Ok(()) +} + +#[sealed_test] +fn get_version_of_monorepo_package_having_own_version_ok() -> Result<()> { + init_monorepo(&mut Settings::default())?; + + Command::cargo_bin("cog")? + .arg("bump") + .arg("--patch") + .arg("--package=one") + .assert() + .success(); + + Command::cargo_bin("cog")? + .arg("get-version") + .arg("--package=one") + .assert() + .success() + .stdout(predicate::eq(b"0.0.1\n" as &[u8])) + .stderr(predicate::eq(b"Current version:\n" as &[u8])); + + // Should differ + Command::cargo_bin("cog")? + .arg("get-version") + .assert() + .failure() + .stderr(predicate::eq(b"Error: No version yet\n" as &[u8])); + + Ok(()) +} diff --git a/tests/cog_tests/mod.rs b/tests/cog_tests/mod.rs index 18cd7c3d..fa347e68 100644 --- a/tests/cog_tests/mod.rs +++ b/tests/cog_tests/mod.rs @@ -2,5 +2,6 @@ mod bump; mod changelog; mod check; mod commit; +mod get_version; mod init; mod verify; diff --git a/tests/cog_tests/verify.rs b/tests/cog_tests/verify.rs index 496d4a81..d7661937 100644 --- a/tests/cog_tests/verify.rs +++ b/tests/cog_tests/verify.rs @@ -1,3 +1,5 @@ +use std::fs; +use std::os::unix::fs::PermissionsExt; use std::process::Command; use crate::helpers::*; @@ -148,3 +150,65 @@ fn should_ignore_merge_commit_via_config() -> Result<()> { Ok(()) } + +#[sealed_test(files = ["tests/assets/commit_message.txt"])] +fn verify_file_ok() -> Result<()> { + // Arrange + git_init()?; + let expected = indoc!( + "a commit message (not committed) - now + \tAuthor: Tom + \tType: chore + \tScope: none + + ", + ); + + // Act + Command::cargo_bin("cog")? + .arg("verify") + .arg("--file") + .arg("commit_message.txt") + // Assert + .assert() + .success() + .stderr(expected); + + Ok(()) +} + +#[test] +fn verify_with_not_existing_file_fails() -> Result<()> { + // Act + Command::cargo_bin("cog")? + .arg("verify") + .arg("--file") + .arg("not_existing_file.txt") + // Assert + .assert() + .failure(); + + Ok(()) +} + +#[cfg(target_family = "unix")] +#[sealed_test(files = ["tests/assets/commit_message.txt"])] +fn verify_with_unreadable_file_fails() -> Result<()> { + let file_name = "commit_message.txt"; + + // Arrange + let mut perms = fs::metadata(file_name)?.permissions(); + perms.set_mode(0o333); // write-only + fs::set_permissions(file_name, perms)?; + + // Act + Command::cargo_bin("cog")? + .arg("verify") + .arg("--file") + .arg(file_name) + // Assert + .assert() + .failure(); + + Ok(()) +} diff --git a/tests/helpers.rs b/tests/helpers.rs index b76a46a9..52446f41 100644 --- a/tests/helpers.rs +++ b/tests/helpers.rs @@ -123,6 +123,13 @@ pub fn assert_latest_tag(tag: &str) -> Result<()> { Ok(()) } +pub fn assert_tag_is_annotated(tag: &str) -> Result<()> { + let objtype = run_fun!(git for-each-ref --format="%(objecttype)" refs/tags/$tag)?; + let objtype: Vec<&str> = objtype.split('\n').collect(); + assert_that!(objtype.first()).is_some().is_equal_to(&"tag"); + Ok(()) +} + /// Git log showing only the HEAD commit, this can be used to make assertion on the last commit pub fn git_log_head() -> Result { run_fun!(git log -1 --pretty=%B).map_err(|e| anyhow!(e)) diff --git a/tests/lib_tests/bump.rs b/tests/lib_tests/bump.rs index 8ea55d62..d3037f60 100644 --- a/tests/lib_tests/bump.rs +++ b/tests/lib_tests/bump.rs @@ -1,9 +1,11 @@ use anyhow::Result; use cmd_lib::run_cmd; -use cocogitto::settings::Settings; +use cocogitto::settings::{MonoRepoPackage, Settings}; use cocogitto::{conventional::version::IncrementCommand, CocoGitto}; use sealed_test::prelude::*; use speculoos::prelude::*; +use std::collections::HashMap; +use std::path::PathBuf; use crate::helpers::*; @@ -19,7 +21,7 @@ fn bump_ok() -> Result<()> { let mut cocogitto = CocoGitto::get()?; // Act - let result = cocogitto.create_version(IncrementCommand::Auto, None, None, false); + let result = cocogitto.create_version(IncrementCommand::Auto, None, None, None, false); // Assert assert_that!(result).is_ok(); @@ -27,6 +29,33 @@ fn bump_ok() -> Result<()> { Ok(()) } +#[sealed_test] +fn annotated_bump_ok() -> Result<()> { + // Arrange + git_init()?; + git_commit("chore: first commit")?; + git_commit("feat: add a feature commit")?; + git_tag("1.0.0")?; + git_commit("feat: add another feature commit")?; + + let mut cocogitto = CocoGitto::get()?; + + // Act + let result = cocogitto.create_version( + IncrementCommand::Auto, + None, + None, + Some(String::from("Release version {{version}}")), + false, + ); + + // Assert + assert_that!(result).is_ok(); + assert_latest_tag("1.1.0")?; + assert_tag_is_annotated("1.1.0")?; + Ok(()) +} + #[sealed_test] fn monorepo_bump_ok() -> Result<()> { // Arrange @@ -39,7 +68,7 @@ fn monorepo_bump_ok() -> Result<()> { let mut cocogitto = CocoGitto::get()?; // Act - let result = cocogitto.create_monorepo_version(IncrementCommand::Auto, None, None, false); + let result = cocogitto.create_monorepo_version(IncrementCommand::Auto, None, None, None, false); // Assert assert_that!(result).is_ok(); @@ -63,7 +92,8 @@ fn monorepo_bump_manual_ok() -> Result<()> { let mut cocogitto = CocoGitto::get()?; // Act - let result = cocogitto.create_monorepo_version(IncrementCommand::Major, None, None, false); + let result = + cocogitto.create_monorepo_version(IncrementCommand::Major, None, None, None, false); // Assert assert_that!(result).is_ok(); @@ -84,7 +114,7 @@ fn monorepo_with_tag_prefix_bump_ok() -> Result<()> { let mut cocogitto = CocoGitto::get()?; // Act - let result = cocogitto.create_monorepo_version(IncrementCommand::Auto, None, None, false); + let result = cocogitto.create_monorepo_version(IncrementCommand::Auto, None, None, None, false); // Assert assert_that!(result).is_ok(); @@ -110,6 +140,7 @@ fn package_bump_ok() -> Result<()> { IncrementCommand::AutoPackage("one".to_string()), None, None, + None, false, ); @@ -120,6 +151,99 @@ fn package_bump_ok() -> Result<()> { Ok(()) } +#[sealed_test] +fn consecutive_package_bump_ok() -> Result<()> { + // Arrange + let mut packages = HashMap::new(); + let jenkins = || MonoRepoPackage { + path: PathBuf::from("jenkins"), + public_api: false, + changelog_path: Some("jenkins/CHANGELOG.md".to_owned()), + ..Default::default() + }; + + packages.insert("jenkins".to_owned(), jenkins()); + + let thumbor = || MonoRepoPackage { + path: PathBuf::from("thumbor"), + public_api: false, + changelog_path: Some("thumbor/CHANGELOG.md".to_owned()), + ..Default::default() + }; + + packages.insert("thumbor".to_owned(), thumbor()); + + let settings = Settings { + packages, + ignore_merge_commits: true, + ..Default::default() + }; + + let settings = toml::to_string(&settings)?; + + git_init()?; + run_cmd!( + echo Hello > README.md; + git add .; + git commit -m "first commit"; + mkdir jenkins; + echo "some jenkins stuff" > jenkins/file; + git add .; + git commit -m "feat(jenkins): add jenkins stuffs"; + mkdir thumbor; + echo "some thumbor stuff" > thumbor/file; + git add .; + git commit -m "feat(thumbor): add thumbor stuffs"; + echo $settings > cog.toml; + git add .; + git commit -m "chore: add cog.toml"; + )?; + + let mut cocogitto = CocoGitto::get()?; + + // Act + cocogitto.create_package_version( + ("thumbor", &thumbor()), + IncrementCommand::AutoPackage("thumbor".to_owned()), + None, + None, + None, + false, + )?; + + cocogitto.create_package_version( + ("jenkins", &jenkins()), + IncrementCommand::AutoPackage("jenkins".to_owned()), + None, + None, + None, + false, + )?; + + run_cmd!( + echo "fix jenkins bug" > jenkins/fix; + git add .; + git commit -m "fix(jenkins): bug fix on jenkins package"; + )?; + + cocogitto.create_package_version( + ("jenkins", &jenkins()), + IncrementCommand::AutoPackage("jenkins".to_owned()), + None, + None, + None, + false, + )?; + + // Assert + assert_tag_exists("jenkins-0.1.0")?; + assert_tag_exists("thumbor-0.1.0")?; + assert_tag_exists("jenkins-0.1.1")?; + assert_tag_does_not_exist("jenkins-0.2.0")?; + assert_tag_does_not_exist("0.1.0")?; + Ok(()) +} + #[sealed_test] fn should_fallback_to_0_0_0_when_there_is_no_tag() -> Result<()> { // Arrange @@ -130,7 +254,7 @@ fn should_fallback_to_0_0_0_when_there_is_no_tag() -> Result<()> { let mut cocogitto = CocoGitto::get()?; // Act - let result = cocogitto.create_version(IncrementCommand::Auto, None, None, false); + let result = cocogitto.create_version(IncrementCommand::Auto, None, None, None, false); // Assert assert_that!(result).is_ok(); @@ -138,6 +262,76 @@ fn should_fallback_to_0_0_0_when_there_is_no_tag() -> Result<()> { Ok(()) } +#[sealed_test] +fn auto_bump_package_only_ok() -> Result<()> { + // Arrange + let mut packages = HashMap::new(); + let jenkins = || MonoRepoPackage { + path: PathBuf::from("jenkins"), + public_api: false, + changelog_path: Some("jenkins/CHANGELOG.md".to_owned()), + ..Default::default() + }; + + packages.insert("jenkins".to_owned(), jenkins()); + + let thumbor = || MonoRepoPackage { + path: PathBuf::from("thumbor"), + public_api: false, + changelog_path: Some("thumbor/CHANGELOG.md".to_owned()), + ..Default::default() + }; + + packages.insert("thumbor".to_owned(), thumbor()); + + let settings = Settings { + packages, + generate_mono_repository_global_tag: false, + ..Default::default() + }; + + let settings = toml::to_string(&settings)?; + + git_init()?; + run_cmd!( + echo Hello > README.md; + git add .; + git commit -m "first commit"; + mkdir jenkins; + echo "some jenkins stuff" > jenkins/file; + git add .; + git commit -m "feat(jenkins): add jenkins stuffs"; + mkdir thumbor; + echo "some thumbor stuff" > thumbor/file; + git add .; + git commit -m "feat(thumbor): add thumbor stuffs"; + echo $settings > cog.toml; + git add .; + git commit -m "chore: add cog.toml"; + )?; + + let mut cocogitto = CocoGitto::get()?; + + // Act + cocogitto.create_all_package_version_auto(None, None, false)?; + + assert_tag_exists("jenkins-0.1.0")?; + assert_tag_exists("thumbor-0.1.0")?; + assert_tag_does_not_exist("0.1.0")?; + + run_cmd!( + echo "fix jenkins bug" > jenkins/fix; + git add .; + git commit -m "fix(jenkins): bug fix on jenkins package"; + )?; + + cocogitto.create_all_package_version_auto(None, None, false)?; + + // Assert + assert_tag_exists("jenkins-0.1.1")?; + Ok(()) +} + // FIXME: Failing on non compliant tag should be configurable // until it's implemented we will ignore non compliant tags // #[sealed_test] @@ -183,7 +377,7 @@ fn bump_with_whitelisted_branch_ok() -> Result<()> { let mut cocogitto = CocoGitto::get()?; // Act - let result = cocogitto.create_version(IncrementCommand::Auto, None, None, false); + let result = cocogitto.create_version(IncrementCommand::Auto, None, None, None, false); // Assert assert_that!(result).is_ok(); @@ -208,7 +402,7 @@ fn bump_with_whitelisted_branch_fails() -> Result<()> { let mut cocogitto = CocoGitto::get()?; // Act - let result = cocogitto.create_version(IncrementCommand::Auto, None, None, false); + let result = cocogitto.create_version(IncrementCommand::Auto, None, None, None, false); // Assert assert_that!(result.unwrap_err().to_string()).is_equal_to( @@ -237,7 +431,7 @@ fn bump_with_whitelisted_branch_pattern_ok() -> Result<()> { let mut cocogitto = CocoGitto::get()?; // Act - let result = cocogitto.create_version(IncrementCommand::Auto, None, None, false); + let result = cocogitto.create_version(IncrementCommand::Auto, None, None, None, false); // Assert assert_that!(result).is_ok(); @@ -262,10 +456,279 @@ fn bump_with_whitelisted_branch_pattern_err() -> Result<()> { let mut cocogitto = CocoGitto::get()?; // Act - let result = cocogitto.create_version(IncrementCommand::Auto, None, None, false); + let result = cocogitto.create_version(IncrementCommand::Auto, None, None, None, false); // Assert assert_that!(result).is_err(); Ok(()) } + +#[sealed_test] +fn bump_no_error_should_be_thrown_on_only_chore_docs_commit() -> Result<()> { + // Arrange + let mut packages = HashMap::new(); + let jenkins = || MonoRepoPackage { + path: PathBuf::from("jenkins"), + changelog_path: Some("jenkins/CHANGELOG.md".to_owned()), + ..Default::default() + }; + + packages.insert("jenkins".to_owned(), jenkins()); + + let thumbor = || MonoRepoPackage { + path: PathBuf::from("thumbor"), + changelog_path: Some("thumbor/CHANGELOG.md".to_owned()), + ..Default::default() + }; + + packages.insert("thumbor".to_owned(), thumbor()); + + let settings = Settings { + packages, + ignore_merge_commits: true, + ..Default::default() + }; + + let settings = toml::to_string(&settings)?; + + git_init()?; + run_cmd!( + echo Hello > README.md; + git add .; + git commit -m "first commit"; + mkdir jenkins; + echo "some jenkins stuff" > jenkins/file; + git add .; + git commit -m "feat(jenkins): add jenkins stuffs"; + mkdir thumbor; + echo "some thumbor stuff" > thumbor/file; + git add .; + git commit -m "feat(thumbor): add thumbor stuffs"; + echo $settings > cog.toml; + git add .; + git commit -m "chore: add cog.toml"; + )?; + + let mut cocogitto = CocoGitto::get()?; + + // Act + cocogitto.create_monorepo_version(IncrementCommand::Auto, None, None, None, false)?; + + run_cmd!( + echo "chore on jenkins" > jenkins/fix; + git add .; + git commit -m "chore(jenkins): jenkins chore"; + echo "docs on jenkins" > jenkins/fix; + git add .; + git commit -m "docs(jenkins): jenkins docs"; + )?; + + cocogitto.create_monorepo_version(IncrementCommand::Auto, None, None, None, false)?; + + cocogitto.create_package_version( + ("jenkins", &jenkins()), + IncrementCommand::AutoPackage("jenkins".to_owned()), + None, + None, + None, + false, + )?; + + run_cmd!( + echo "more feat on thumbor" > thumbor/feat; + git add .; + git commit -m "feat(thumbor): more feat on thumbor"; + )?; + + cocogitto.create_monorepo_version(IncrementCommand::Auto, None, None, None, false)?; + + // Assert + assert_tag_exists("jenkins-0.1.0")?; + assert_tag_exists("thumbor-0.1.0")?; + assert_tag_exists("thumbor-0.2.0")?; + assert_tag_exists("0.1.0")?; + assert_tag_exists("0.2.0")?; + + assert_tag_does_not_exist("jenkins-0.1.1")?; + assert_tag_does_not_exist("jenkins-0.2.0")?; + assert_tag_does_not_exist("jenkins-1.0.0")?; + Ok(()) +} + +#[sealed_test] +fn error_on_no_conventionnal_commits_found_for_monorepo() -> Result<()> { + let settings = Settings { + ..Default::default() + }; + + let settings = toml::to_string(&settings)?; + + git_init()?; + + run_cmd!( + echo Hello > README.md; + git add .; + git commit -m "chore: first commit"; + + echo $settings > cog.toml; + git add .; + + echo "first feature" > file; + git add .; + git commit -m "feat: feature commit"; + )?; + + let mut cocogitto = CocoGitto::get()?; + + // Act + let first_result = cocogitto.create_version(IncrementCommand::Auto, None, None, None, false); + + // Assert + assert_that!(first_result).is_ok(); + + run_cmd!( + echo "second feature" >> file; + git add .; + )?; + + git_commit("second unconventional feature commit")?; + + // Act + let second_result = cocogitto.create_version(IncrementCommand::Auto, None, None, None, false); + + // Assert + assert_that!(second_result).is_err(); + + Ok(()) +} + +#[sealed_test] +fn error_on_no_conventionnal_commits_found_for_package() -> Result<()> { + // Arrange + let mut packages = HashMap::new(); + let jenkins = || MonoRepoPackage { + path: PathBuf::from("jenkins"), + changelog_path: Some("jenkins/CHANGELOG.md".to_owned()), + ..Default::default() + }; + + packages.insert("jenkins".to_owned(), jenkins()); + + let settings = Settings { + packages, + ignore_merge_commits: true, + ..Default::default() + }; + + let settings = toml::to_string(&settings)?; + + git_init()?; + run_cmd!( + echo Hello > README.md; + git add .; + git commit -m "first commit"; + + echo $settings > cog.toml; + git add .; + git commit -m "chore: cog config"; + + mkdir jenkins; + echo "some jenkins stuff" > jenkins/file; + git add .; + git commit -m "feat(jenkins): some jenkins stuff"; + )?; + + let mut cocogitto = CocoGitto::get()?; + + let first_result = cocogitto.create_package_version( + ("jenkins", &jenkins()), + IncrementCommand::AutoPackage("jenkins".to_owned()), + None, + None, + None, + false, + ); + + assert_that!(first_result).is_ok(); + + run_cmd!( + echo "some other jenkins stuff" >> jenkins/file; + git add .; + git commit -m "some other jenkins stuff"; + )?; + + let second_result = cocogitto.create_package_version( + ("jenkins", &jenkins()), + IncrementCommand::AutoPackage("jenkins".to_owned()), + None, + None, + None, + false, + ); + + assert_that!(second_result).is_err(); + + Ok(()) +} + +#[sealed_test] +fn bump_with_unconventionnal_and_conventional_commits_found_for_packages() -> Result<()> { + // Arrange + let mut packages = HashMap::new(); + let jenkins = || MonoRepoPackage { + path: PathBuf::from("jenkins"), + changelog_path: Some("jenkins/CHANGELOG.md".to_owned()), + ..Default::default() + }; + + packages.insert("jenkins".to_owned(), jenkins()); + + let thumbor = || MonoRepoPackage { + path: PathBuf::from("thumbor"), + changelog_path: Some("thumbor/CHANGELOG.md".to_owned()), + ..Default::default() + }; + + packages.insert("thumbor".to_owned(), thumbor()); + + let settings = Settings { + packages, + ignore_merge_commits: true, + ..Default::default() + }; + + let settings = toml::to_string(&settings)?; + + git_init()?; + run_cmd!( + echo Hello > README.md; + git add .; + git commit -m "first commit"; + mkdir jenkins; + echo "unconventional jenkins stuff" > jenkins/file; + git add .; + git commit -m "unconventional jenkins stuff"; + mkdir thumbor; + echo "conventional thumbor stuff" > thumbor/file; + git add .; + git commit -m "feat(thumbor): conventional thumbor stuff"; + echo $settings > cog.toml; + git add .; + git commit -m "chore: add cog.toml"; + )?; + + let mut cocogitto = CocoGitto::get()?; + + // Act + let result = cocogitto.create_monorepo_version(IncrementCommand::Auto, None, None, None, false); + + // Assert + assert_that!(result).is_ok(); + assert_tag_exists("thumbor-0.1.0")?; + assert_tag_exists("0.1.0")?; + + assert_tag_does_not_exist("jenkins-0.1.0")?; + + Ok(()) +} diff --git a/tests/lib_tests/cocogitto.rs b/tests/lib_tests/cocogitto.rs index 897db57d..23789258 100644 --- a/tests/lib_tests/cocogitto.rs +++ b/tests/lib_tests/cocogitto.rs @@ -42,7 +42,7 @@ fn check_commit_history_ok() -> Result<()> { let cocogitto = CocoGitto::get()?; // Act - let check = cocogitto.check(false, false); + let check = cocogitto.check(false, false, None); // Assert assert_that!(check).is_ok(); @@ -58,7 +58,7 @@ fn check_commit_history_err_with_merge_commit() -> Result<()> { let cocogitto = CocoGitto::get()?; // Act - let check = cocogitto.check(false, false); + let check = cocogitto.check(false, false, None); // Assert assert_that!(check).is_err(); @@ -81,7 +81,7 @@ fn check_commit_history_ok_with_merge_commit_ignored() -> Result<()> { let cocogitto = CocoGitto::get()?; // Act - let check = cocogitto.check(false, true); + let check = cocogitto.check(false, true, None); // Assert assert_that!(check).is_ok(); @@ -98,7 +98,7 @@ fn check_commit_history_err() -> Result<()> { let cocogitto = CocoGitto::get()?; // Act - let check = cocogitto.check(false, false); + let check = cocogitto.check(false, false, None); // Assert assert_that!(check).is_err(); @@ -117,7 +117,7 @@ fn check_commit_ok_from_latest_tag() -> Result<()> { let cocogitto = CocoGitto::get()?; // Act - let check = cocogitto.check(true, false); + let check = cocogitto.check(true, false, None); // Assert assert_that!(check).is_ok(); @@ -135,7 +135,7 @@ fn check_commit_err_from_latest_tag() -> Result<()> { let cocogitto = CocoGitto::get()?; // Act - let check = cocogitto.check(true, false); + let check = cocogitto.check(true, false, None); // Assert assert_that!(check).is_err(); @@ -152,8 +152,84 @@ fn long_commit_summary_does_not_panic() -> Result<()> { git_add("Hello", "file")?; cocogitto.conventional_commit("feat", None, message, None, None, false, false)?; - let check = cocogitto.check(false, false); + let check = cocogitto.check(false, false, None); assert_that!(check.is_ok()); Ok(()) } + +#[sealed_test] +fn check_commit_ok_commit_range() -> Result<()> { + // Arrange + git_init()?; + let range_start = git_commit("feat: a valid commit")?; + let range_end = git_commit("chore(test): another valid commit")?; + let range = format!("{range_start}..{range_end}"); + let cocogitto = CocoGitto::get()?; + + // Act + let check = cocogitto.check(true, false, Some(range)); + + // Assert + assert_that!(check).is_ok(); + Ok(()) +} + +#[sealed_test] +fn check_commit_err_commit_range() -> Result<()> { + // Arrange + git_init_and_set_current_path("commit_history_err")?; + create_empty_config()?; + let range_start = git_commit("feat: a valid commit")?; + let range_end = git_commit("errored commit")?; + let range = format!("{range_start}..{range_end}"); + let cocogitto = CocoGitto::get()?; + + // Act + let check = cocogitto.check(true, false, Some(range)); + + // Assert + assert_that!(check).is_err(); + Ok(()) +} + +#[sealed_test] +fn check_commit_range_err_with_merge_commit() -> Result<()> { + // Arrange + git_init()?; + let range_start = git_commit("feat: a valid commit")?; + let range_end = git_commit("Merge feature one into main")?; + let range = format!("{range_start}..{range_end}"); + let cocogitto = CocoGitto::get()?; + + // Act + let check = cocogitto.check(false, false, Some(range)); + + // Assert + assert_that!(check).is_err(); + Ok(()) +} + +#[sealed_test] +fn check_commit_range_ok_with_merge_commit_ignored() -> Result<()> { + // Arrange + git_init()?; + let range_start = git_commit("feat: a valid commit")?; + run_cmd!(git checkout -b branch;)?; + git_commit("feat: commit on another branch")?; + run_cmd!( + git checkout -; + git merge branch --no-ff; + git --no-pager log; + )?; + let range_end = git_commit("chore(test): another valid commit")?; + let range = format!("{range_start}..{range_end}"); + let cocogitto = CocoGitto::get()?; + + // Act + let check = cocogitto.check(false, true, Some(range)); + + // Assert + assert_that!(check).is_ok(); + Ok(()) +}