diff --git a/commitizen/cli.py b/commitizen/cli.py index 839e6cf5d7..bf751b44a4 100644 --- a/commitizen/cli.py +++ b/commitizen/cli.py @@ -2,12 +2,18 @@ import logging import sys from functools import partial +from typing import List import argcomplete from decli import cli -from commitizen import commands, config -from commitizen.exceptions import CommitizenException, ExpectedExit, NoCommandFoundError +from commitizen import commands, config, out +from commitizen.exceptions import ( + CommitizenException, + ExitCode, + ExpectedExit, + NoCommandFoundError, +) logger = logging.getLogger(__name__) data = { @@ -24,6 +30,12 @@ "name": ["-n", "--name"], "help": "use the given commitizen (default: cz_conventional_commits)", }, + { + "name": ["-nr", "--no-raise"], + "type": str, + "required": False, + "help": "comma separated error codes that won't rise error, e.g: cz -nr 1,2,3 bump. See codes at https://commitizen-tools.github.io/commitizen/exit_codes/", + }, ], "subcommands": { "title": "commands", @@ -274,13 +286,20 @@ original_excepthook = sys.excepthook -def commitizen_excepthook(type, value, tracekback, debug=False): +def commitizen_excepthook( + type, value, tracekback, debug=False, no_raise: List[int] = None +): + if not no_raise: + no_raise = [] if isinstance(value, CommitizenException): if value.message: value.output_method(value.message) if debug: original_excepthook(type, value, tracekback) - sys.exit(value.exit_code) + exit_code = value.exit_code + if exit_code in no_raise: + exit_code = 0 + sys.exit(exit_code) else: original_excepthook(type, value, tracekback) @@ -290,6 +309,28 @@ def commitizen_excepthook(type, value, tracekback, debug=False): sys.excepthook = commitizen_excepthook +def parse_no_raise(comma_separated_no_raise: str) -> List[int]: + """Convert the given string to exit codes. + + Receives digits and strings and outputs the parsed integer which + represents the exit code found in exceptions. + """ + no_raise_items: List[str] = comma_separated_no_raise.split(",") + no_raise_codes = [] + for item in no_raise_items: + if item.isdecimal(): + no_raise_codes.append(int(item)) + continue + try: + exit_code = ExitCode[item.strip()] + except KeyError: + out.warn(f"WARN: no_raise key `{item}` does not exist. Skipping.") + continue + else: + no_raise_codes.append(exit_code.value) + return no_raise_codes + + def main(): conf = config.read_cfg() parser = cli(data) @@ -319,6 +360,12 @@ def main(): if args.debug: logging.getLogger("commitizen").setLevel(logging.DEBUG) sys.excepthook = commitizen_debug_excepthook + elif args.no_raise: + no_raise_exit_codes = parse_no_raise(args.no_raise) + no_raise_debug_excepthook = partial( + commitizen_excepthook, no_raise=no_raise_exit_codes + ) + sys.excepthook = no_raise_debug_excepthook args.func(conf, vars(args))() diff --git a/commitizen/exceptions.py b/commitizen/exceptions.py index 9ae97ee1b0..a95ab3b587 100644 --- a/commitizen/exceptions.py +++ b/commitizen/exceptions.py @@ -25,6 +25,7 @@ class ExitCode(enum.IntEnum): INVALID_COMMAND_ARGUMENT = 18 INVALID_CONFIGURATION = 19 NOT_ALLOWED = 20 + NO_INCREMENT = 21 class CommitizenException(Exception): @@ -56,7 +57,7 @@ class DryRunExit(ExpectedExit): class NoneIncrementExit(CommitizenException): - exit_code = ExitCode.NO_COMMITS_FOUND + exit_code = ExitCode.NO_INCREMENT class NoCommitizenFoundException(CommitizenException): diff --git a/commitizen/out.py b/commitizen/out.py index a25f0f2e32..811fe09c9f 100644 --- a/commitizen/out.py +++ b/commitizen/out.py @@ -30,3 +30,8 @@ def info(value: str) -> None: def diagnostic(value: str): line(value, file=sys.stderr) + + +def warn(value: str) -> None: + message = colored(value, "magenta") + line(message) diff --git a/docs/README.md b/docs/README.md index 3bb2db5588..f459ccfa28 100644 --- a/docs/README.md +++ b/docs/README.md @@ -124,11 +124,9 @@ Read more about the `check` command [here](check.md). ### Help -```bash +```sh $ cz --help -usage: cz [-h] [--debug] [-n NAME] [--version] - {init,commit,c,ls,example,info,schema,bump,changelog,ch,check,version} - ... +usage: cz [-h] [--debug] [-n NAME] [-nr NO_RAISE] {init,commit,c,ls,example,info,schema,bump,changelog,ch,check,version} ... Commitizen is a cli tool to generate conventional commits. For more information about the topic go to https://conventionalcommits.org/ @@ -136,9 +134,10 @@ For more information about the topic go to https://conventionalcommits.org/ optional arguments: -h, --help show this help message and exit --debug use debug mode - -n NAME, --name NAME use the given commitizen (default: - cz_conventional_commits) - --version get the version of the installed commitizen + -n NAME, --name NAME use the given commitizen (default: cz_conventional_commits) + -nr NO_RAISE, --no-raise NO_RAISE + comma separated error codes that won't rise error, e.g: cz -nr 1,2,3 bump. See codes at https://commitizen- + tools.github.io/commitizen/exit_codes/ commands: {init,commit,c,ls,example,info,schema,bump,changelog,ch,check,version} @@ -149,12 +148,9 @@ commands: info show information about the cz schema show commit schema bump bump semantic version based on the git log - changelog (ch) generate changelog (note that it will overwrite - existing file) - check validates that a commit message matches the commitizen - schema - version get the version of the installed commitizen or the - current project (default: installed commitizen) + changelog (ch) generate changelog (note that it will overwrite existing file) + check validates that a commit message matches the commitizen schema + version get the version of the installed commitizen or the current project (default: installed commitizen) ``` ## Setting up bash completion diff --git a/docs/bump.md b/docs/bump.md index 38e4f5272b..cbf7fef171 100644 --- a/docs/bump.md +++ b/docs/bump.md @@ -179,6 +179,69 @@ Example: cz bump --changelog --changelog-to-stdout > body.md ``` +## Avoid raising errors + +Some situations from commitizen rise an exit code different than 0. +If the error code is different than 0, any CI or script running commitizen might be interrupted. + +If you have special use case, where you don't want one of this error codes to be raised, you can +tell commitizen to not raise them. + +### Recommended use case + +At the moment, we've identified that the most common error code to skip is + +| Error name | Exit code | +| ----------------- | --------- | +| NoneIncrementExit | 21 | + +There are some situations where you don't want to get an error code when some +commits do not match your rules, you just want those commits to be skipped. + +```sh +cz -nr 21 bump +``` + +### Easy way + +Check which error code was raised by commitizen by running in the terminal + +```sh +echo $? +``` + +The output should be an integer like this + +```sh +3 +``` + +And then you can tell commitizen to ignore it: + +```sh +cz --no-raise 3 +``` + +You can tell commitizen to skip more than one if needed: + +```sh +cz --no-raise 3,4,5 +``` + +### Longer way + +Check the list of [exit_codes](./exit_codes.md) and understand which one you have +to skip and why. + +Remember to document somewhere this, because you'll forget. + +For example if the system raises a `NoneIncrementExit` error, you look it up +on the list and then you can use the exit code: + +```sh +cz -nr 21 bump +``` + ## Configuration ### `tag_format` diff --git a/docs/exit_codes.md b/docs/exit_codes.md index 28f8f64198..f4c2fa8236 100644 --- a/docs/exit_codes.md +++ b/docs/exit_codes.md @@ -4,26 +4,28 @@ Commitizen handles expected exceptions through `CommitizenException` and returns These exit codes can be found in `commitizen/exceptions.py::ExitCode`. -| Exception | Exit Code | Description | -| --------- | --------- | ----------- | -| ExpectedExit | 0 | Expected exit | -| DryRunExit | 0 | Exit due to passing `--dry-run` option | -| NoCommitizenFoundException | 1 | Using a cz (e.g., `cz_jira`) that cannot be found in your system | -| NotAGitProjectError | 2 | Not in a git project | -| NoCommitsFoundError | 3 | No commit found | -| NoVersionSpecifiedError | 4 | Version can not be found in configuration file | -| NoPatternMapError | 5 | bump / changelog pattern or map can not be found in configuration file | -| BumpCommitFailedError | 6 | Commit error when bumping version | -| BumpTagFailedError | 7 | Tag error when bumping version | -| NoAnswersError | 8 | No user response given | -| CommitError | 9 | git commit error | -| NoCommitBackupError | 10 | Commit back up file cannot be found | -| NothingToCommitError | 11 | Nothing in staging to be committed | -| CustomError | 12 | `CzException` raised | -| NoCommandFoundError | 13 | No command found when running commitizen cli (e.g., `cz --debug`) | -| InvalidCommitMessageError | 14 | The commit message does not pass `cz check` | -| MissingConfigError | 15 | Configuration missed for `cz_customize` | -| NoRevisionError | 16 | No revision found | -| CurrentVersionNotFoundError | 17 | current version cannot be found in *version_files* | -| InvalidCommandArgumentError | 18 | The argument provide to command is invalid (e.g. `cz check -commit-msg-file filename --rev-range master..`) | -| InvalidConfigurationError | 19 | An error was found in the Commitizen Configuration, such as duplicates in `change_type_order` | +| Exception | Exit Code | Description | +| --------------------------- | --------- | ----------------------------------------------------------------------------------------------------------- | +| ExpectedExit | 0 | Expected exit | +| DryRunExit | 0 | Exit due to passing `--dry-run` option | +| NoCommitizenFoundException | 1 | Using a cz (e.g., `cz_jira`) that cannot be found in your system | +| NotAGitProjectError | 2 | Not in a git project | +| NoCommitsFoundError | 3 | No commit found | +| NoVersionSpecifiedError | 4 | Version can not be found in configuration file | +| NoPatternMapError | 5 | bump / changelog pattern or map can not be found in configuration file | +| BumpCommitFailedError | 6 | Commit error when bumping version | +| BumpTagFailedError | 7 | Tag error when bumping version | +| NoAnswersError | 8 | No user response given | +| CommitError | 9 | git commit error | +| NoCommitBackupError | 10 | Commit back up file cannot be found | +| NothingToCommitError | 11 | Nothing in staging to be committed | +| CustomError | 12 | `CzException` raised | +| NoCommandFoundError | 13 | No command found when running commitizen cli (e.g., `cz --debug`) | +| InvalidCommitMessageError | 14 | The commit message does not pass `cz check` | +| MissingConfigError | 15 | Configuration missed for `cz_customize` | +| NoRevisionError | 16 | No revision found | +| CurrentVersionNotFoundError | 17 | current version cannot be found in _version_files_ | +| InvalidCommandArgumentError | 18 | The argument provide to command is invalid (e.g. `cz check -commit-msg-file filename --rev-range master..`) | +| InvalidConfigurationError | 19 | An error was found in the Commitizen Configuration, such as duplicates in `change_type_order` | +| NotAllowed | 20 | `--incremental` cannot be combined with a `rev_range` | +| NoneIncrementExit | 21 | The commits found are not elegible to be bumped | diff --git a/docs/tutorials/github_actions.md b/docs/tutorials/github_actions.md index 58726afcd2..b5d05bb935 100644 --- a/docs/tutorials/github_actions.md +++ b/docs/tutorials/github_actions.md @@ -12,7 +12,7 @@ the new tag, back to your master branch, we have to: with the following content. !!! warning -If you use `GITHUB_TOKEN` instead of `PERSONAL_ACCESS_TOKEN`, the job won't trigger another workflow. It's like using `[skip ci]` in other CI's. + If you use `GITHUB_TOKEN` instead of `PERSONAL_ACCESS_TOKEN`, the job won't trigger another workflow. It's like using `[skip ci]` in other CI's. ```yaml name: Bump version diff --git a/tests/commands/test_bump_command.py b/tests/commands/test_bump_command.py index b5c7b6b122..1d1df88979 100644 --- a/tests/commands/test_bump_command.py +++ b/tests/commands/test_bump_command.py @@ -358,7 +358,7 @@ def test_none_increment_should_not_call_git_tag_and_error_code_is_not_zero( cli.main() except NoneIncrementExit as e: git.tag.assert_not_called() - assert e.exit_code == ExitCode.NO_COMMITS_FOUND + assert e.exit_code == ExitCode.NO_INCREMENT raise e # restore pop stashed diff --git a/tests/test_cli.py b/tests/test_cli.py index 44f182b0d5..383d7a18cf 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -95,3 +95,46 @@ def test_argcomplete_activation(): output = subprocess.run(["register-python-argcomplete", "cz"]) assert output.returncode == 0 + + +def test_commitizen_excepthook_no_raises(capsys): + with pytest.raises(SystemExit) as excinfo: + cli.commitizen_excepthook( + NotAGitProjectError, + NotAGitProjectError(), + "", + no_raise=[NotAGitProjectError.exit_code], + ) + + assert excinfo.type == SystemExit + assert excinfo.value.code == 0 + + +def test_parse_no_raise_single_integer(): + input_str = "1" + result = cli.parse_no_raise(input_str) + assert result == [1] + + +def test_parse_no_raise_integers(): + input_str = "1,2,3" + result = cli.parse_no_raise(input_str) + assert result == [1, 2, 3] + + +def test_parse_no_raise_error_code(): + input_str = "NO_COMMITIZEN_FOUND,NO_COMMITS_FOUND,NO_PATTERN_MAP" + result = cli.parse_no_raise(input_str) + assert result == [1, 3, 5] + + +def test_parse_no_raise_mix_integer_error_code(): + input_str = "NO_COMMITIZEN_FOUND,2,NO_COMMITS_FOUND,4" + result = cli.parse_no_raise(input_str) + assert result == [1, 2, 3, 4] + + +def test_parse_no_raise_mix_invalid_arg_is_skipped(): + input_str = "NO_COMMITIZEN_FOUND,2,nothing,4" + result = cli.parse_no_raise(input_str) + assert result == [1, 2, 4]