X Tutup
from __future__ import annotations import argparse import logging import sys from copy import deepcopy from functools import partial from pathlib import Path from types import TracebackType from typing import TYPE_CHECKING, cast import argcomplete from decli import cli from commitizen import commands, config, out, version_schemes from commitizen.defaults import DEFAULT_SETTINGS from commitizen.exceptions import ( CommitizenException, ExitCode, ExpectedExit, InvalidCommandArgumentError, NoCommandFoundError, ) logger = logging.getLogger(__name__) class ParseKwargs(argparse.Action): """ Parse arguments in the for `key=value`. Quoted strings are automatically unquoted. Can be submitted multiple times: ex: -k key=value -k double-quotes="value" -k single-quotes='value' will result in namespace["opt"] == { "key": "value", "double-quotes": "value", "single-quotes": "value", } """ def __call__( self, parser: argparse.ArgumentParser, namespace: argparse.Namespace, values: object, option_string: str | None = None, ) -> None: if not isinstance(values, str): return key, sep, value = values.partition("=") if not key or not sep: raise InvalidCommandArgumentError( f"Option {option_string} expect a key=value format" ) kwargs = getattr(namespace, self.dest, None) or {} kwargs[key] = value.strip("'\"") setattr(namespace, self.dest, kwargs) tpl_arguments = ( { "name": ["--template", "-t"], "help": ( "Changelog template file name (relative to the current working directory)." ), }, { "name": ["--extra", "-e"], "action": ParseKwargs, "dest": "extras", "metavar": "EXTRA", "help": "Changelog extra variables (in the form 'key=value').", }, ) data = { "prog": "cz", "description": ( "Commitizen is a powerful release management tool that helps teams maintain consistent and meaningful commit messages while automating version management.\n" "For more information, please visit https://commitizen-tools.github.io/commitizen" ), "formatter_class": argparse.RawDescriptionHelpFormatter, "arguments": [ { "name": "--config", "help": "The path to the configuration file.", }, {"name": "--debug", "action": "store_true", "help": "Use debug mode."}, { "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 raise error, e.g., cz -nr 1,2,3 bump. See codes at https://commitizen-tools.github.io/commitizen/exit_codes/", }, ], "subcommands": { "title": "commands", "required": True, "commands": [ { "name": ["init"], "description": "Initialize commitizen configuration", "help": "Initialize commitizen configuration.", "func": commands.Init, }, { "name": ["commit", "c"], "description": "Create new commit", "help": "Create new commit.", "func": commands.Commit, "arguments": [ { "name": ["--retry"], "action": "store_true", "help": "Retry the last commit.", }, { "name": ["--no-retry"], "action": "store_true", "default": False, "help": "Skip retry if --retry or `retry_after_failure` is set to true.", }, { "name": "--dry-run", "action": "store_true", "help": "Perform a dry run, without committing or modifying files.", }, { "name": "--write-message-to-file", "type": Path, "metavar": "FILE_PATH", "help": "Write message to FILE_PATH before committing (can be used with --dry-run).", }, { "name": ["-s", "--signoff"], "action": "store_true", "help": "Deprecated, use `cz commit -- -s` instead.", }, { "name": ["-a", "--all"], "action": "store_true", # The help text aligns with the description of git commit --all "help": "Automatically stage files that have been modified and deleted, but new files you have not told Git about are not affected.", }, { "name": ["-e", "--edit"], "action": "store_true", "default": False, "help": "Edit the commit message before committing.", }, { "name": ["-l", "--message-length-limit"], "type": int, "help": "Set the length limit of the commit message; 0 for no limit.", }, { "name": ["--"], "action": "store_true", "dest": "double_dash", "help": "Positional arguments separator (recommended).", }, ], }, { "name": "ls", "description": "Show available Commitizens", "help": "Show available Commitizens.", "func": commands.ListCz, }, { "name": "example", "description": "Show commit example", "help": "Show commit example.", "func": commands.Example, }, { "name": "info", "description": "Show information about the cz", "help": "Show information about the cz.", "func": commands.Info, }, { "name": "schema", "description": "Show commit schema", "help": "Show commit schema.", "func": commands.Schema, }, { "name": "bump", "description": "Bump semantic version based on the git log", "help": "Bump semantic version based on the git log.", "func": commands.Bump, "arguments": [ { "name": "--dry-run", "action": "store_true", "help": "Perform a dry run, without committing or modifying files.", }, { "name": "--files-only", # TODO: rename to --version-files-only "action": "store_true", "help": "Bump version in the `version_files` specified in the configuration file only(deprecated; use --version-files-only instead).", }, { "name": "--version-files-only", "action": "store_true", "help": "Bump version in the files from the config", }, { "name": "--local-version", "action": "store_true", "help": "Bump version only the local version portion (ignoring the public version).", }, { "name": ["--changelog", "-ch"], "action": "store_true", "default": False, "help": "Generate the changelog for the latest version.", }, { "name": ["--no-verify"], "action": "store_true", "default": False, # The help text aligns with the description of git commit --no-verify "help": "Bypass the pre-commit and commit-msg hooks.", }, { "name": "--yes", "action": "store_true", "help": "Accept automatically answered questions.", }, { "name": "--tag-format", "help": ( "The format used to tag the commit and read it. " "Use it in existing projects, and wrap around simple quotes." ), }, { "name": "--bump-message", "help": ( "Template used to create the release commit, useful when working with CI." ), }, { "name": ["--prerelease", "-pr"], "help": "Type of prerelease.", "choices": ["alpha", "beta", "rc"], }, { "name": ["--devrelease", "-d"], "help": "Specify non-negative integer for dev release.", "type": int, }, { "name": ["--increment"], "help": "Specify the desired increment.", "choices": ["MAJOR", "MINOR", "PATCH"], "type": str.upper, }, { "name": ["--increment-mode"], "choices": ["linear", "exact"], "default": "linear", "help": ( "Set the method by which the new version is chosen. " "'linear' (default) resolves the next version based on typical linear version progression, " "where bumping of a pre-release with lower precedence than the current pre-release " "phase maintains the current phase of higher precedence. " "'exact' applies the changes that have been specified (or determined from the commit log) " "without interpretation, ensuring the increment and pre-release are always honored." ), }, { "name": ["--check-consistency", "-cc"], "help": ( "Check consistency among versions defined in Commitizen configuration file and `version_files`." ), "action": "store_true", }, { "name": ["--annotated-tag", "-at"], "help": "Create annotated tag instead of lightweight one.", "action": "store_true", }, { "name": ["--annotated-tag-message", "-atm"], "help": "Create annotated tag message.", "type": str, }, { "name": ["--gpg-sign", "-s"], "help": "Sign tag instead of lightweight one.", "default": False, "action": "store_true", }, { "name": ["--changelog-to-stdout"], "action": "store_true", "default": False, "help": "Output changelog to stdout.", }, { "name": ["--git-output-to-stderr"], "action": "store_true", "default": False, "help": "Redirect git output to stderr.", }, { "name": ["--retry"], "action": "store_true", "default": False, "help": "Retry commit if it fails for the first time.", }, { "name": ["--major-version-zero"], "action": "store_true", "default": None, "help": "Keep major version at zero, even for breaking changes.", }, *deepcopy(tpl_arguments), { "name": "--file-name", "help": "File name of changelog (default: 'CHANGELOG.md').", }, { "name": ["--prerelease-offset"], "type": int, "default": None, "help": "Start pre-releases with this offset.", }, { "name": ["--version-scheme"], "help": "Choose version scheme.", "default": None, "choices": version_schemes.KNOWN_SCHEMES, }, { "name": ["--version-type"], "help": "Deprecated, use `--version-scheme` instead.", "default": None, "choices": version_schemes.KNOWN_SCHEMES, }, { "name": "manual_version", "type": str, "nargs": "?", "help": "Bump to the given version (e.g., 1.5.3).", "metavar": "MANUAL_VERSION", }, { "name": ["--build-metadata"], "help": "Add additional build-metadata to the version-number.", "default": None, }, { "name": ["--get-next"], "action": "store_true", "help": "Determine the next version and write to stdout.", "default": False, }, { "name": ["--allow-no-commit"], "default": False, "help": "Bump version without eligible commits.", "action": "store_true", }, ], }, { "name": ["changelog", "ch"], "description": ( "Generate changelog (note that it will overwrite existing files)" ), "help": ( "Generate changelog (note that it will overwrite existing files)." ), "func": commands.Changelog, "arguments": [ { "name": "--dry-run", "action": "store_true", "default": False, "help": "Show changelog to stdout.", }, { "name": "--file-name", "help": "File name of changelog (default: 'CHANGELOG.md').", }, { "name": "--unreleased-version", "help": ( "Set the value for the new version (use the tag value), " "instead of using unreleased versions." ), }, { "name": "--incremental", "action": "store_true", "default": False, "help": ( "Generate changelog from the last created version, " "useful if the changelog has been manually modified." ), }, { "name": "rev_range", "type": str, "nargs": "?", "help": "Generate changelog for the given version (e.g., 1.5.3) or version range (e.g., 1.5.3..1.7.9).", }, { "name": "--start-rev", "default": None, "help": ( "Start rev of the changelog. " "If not set, it will generate changelog from the beginning." ), }, { "name": "--merge-prerelease", "action": "store_true", "default": False, "help": ( "Collect all changes from prereleases into the next non-prerelease. " "If not set, it will include prereleases in the changelog." ), }, { "name": ["--version-scheme"], "help": "Choose version scheme.", "default": None, "choices": version_schemes.KNOWN_SCHEMES, }, { "name": "--export-template", "default": None, "help": "Export the changelog template into this file instead of rendering it.", }, *deepcopy(tpl_arguments), { "name": "--tag-format", "help": "The format of the tag, wrap around simple quotes.", }, ], }, { "name": ["check"], "description": "Validate that a commit message matches the commitizen schema", "help": "Validate that a commit message matches the commitizen schema.", "func": commands.Check, "arguments": [ { "name": "--commit-msg-file", "help": ( "Ask for the name of the temporary file that contains the commit message. " "Use it in a git hook script: MSG_FILE=$1." ), "exclusive_group": "group1", }, { "name": "--rev-range", "help": "Validate the commits in the given range of git rev, e.g., master..HEAD.", "exclusive_group": "group1", }, { "name": ["-d", "--use-default-range"], "action": "store_true", "default": False, "help": "Validate the commits from the default branch to HEAD, e.g., refs/remotes/origin/master..HEAD.", "exclusive_group": "group1", }, { "name": ["-m", "--message"], "help": "Validate the given commit message.", "exclusive_group": "group1", }, { "name": ["--allow-abort"], "action": "store_true", "default": False, "help": "Allow empty commit messages, which typically abort a commit.", }, { "name": ["--allowed-prefixes"], "nargs": "*", "help": "Skip validation for commit messages that start with the specified prefixes.", }, { "name": ["-l", "--message-length-limit"], "type": int, "help": "Restrict the length of the **first line** of the commit message; 0 for no limit.", }, ], }, { "name": ["version"], "description": ( "Get the version of the installed commitizen or the current project (default: installed commitizen)" ), "help": ( "Get the version of the installed commitizen or the current project (default: installed commitizen)." ), "func": commands.Version, "arguments": [ { "name": ["-r", "--report"], "help": "Output the system information for reporting bugs.", "action": "store_true", "exclusive_group": "group1", }, { "name": ["-p", "--project"], "help": "Output the version of the current project.", "action": "store_true", "exclusive_group": "group1", }, { "name": ["-c", "--commitizen"], "help": "Output the version of the installed commitizen.", "action": "store_true", "exclusive_group": "group1", }, { "name": ["-v", "--verbose"], "help": ( "Output the version of both the installed commitizen and the current project." ), "action": "store_true", "exclusive_group": "group1", }, { "name": ["--major"], "help": "Output just the major version. Must be used with --project or --verbose.", "action": "store_true", "exclusive_group": "group2", }, { "name": ["--minor"], "help": "Output just the minor version. Must be used with --project or --verbose.", "action": "store_true", "exclusive_group": "group2", }, { "name": ["--tag"], "help": "get the version with tag prefix. Need to be used with --project or --verbose.", "action": "store_true", "exclusive_group": "group2", }, ], }, ], }, } def commitizen_excepthook( exctype: type[BaseException], value: BaseException, traceback: TracebackType | None, debug: bool = False, no_raise: list[int] | None = None, ) -> None: traceback = traceback if isinstance(traceback, TracebackType) else None if not isinstance(value, CommitizenException): sys.__excepthook__(exctype, value, traceback) return if value.message: value.output_method(value.message) if debug: sys.__excepthook__(exctype, value, traceback) exit_code = value.exit_code if no_raise is not None and exit_code in no_raise: sys.exit(ExitCode.EXPECTED_EXIT) sys.exit(exit_code) 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. """ def exit_code_from_str_or_skip(s: str) -> ExitCode | None: try: return ExitCode.from_str(s) except (KeyError, ValueError): out.warn(f"WARN: no_raise value `{s}` is not a valid exit code. Skipping.") return None return [ code.value for s in comma_separated_no_raise.split(",") if (code := exit_code_from_str_or_skip(s)) is not None ] if TYPE_CHECKING: class Args(argparse.Namespace): config: str | None = None debug: bool = False name: str | None = None no_raise: str | None = None # comma-separated string, later parsed as list[int] report: bool = False project: bool = False commitizen: bool = False verbose: bool = False func: type[ commands.Init # init | commands.Commit # commit (c) | commands.ListCz # ls | commands.Example # example | commands.Info # info | commands.Schema # schema | commands.Bump # bump | commands.Changelog # changelog (ch) | commands.Check # check | commands.Version # version ] def main() -> None: sys.excepthook = commitizen_excepthook parser: argparse.ArgumentParser = cli(data) argcomplete.autocomplete(parser) # Show help if no arg provided if len(sys.argv) == 1: parser.print_help(sys.stderr) raise ExpectedExit() # This is for the command required constraint in 2.0 try: args, unknown_args = parser.parse_known_args() except SystemExit as e: if e.code == 2: raise NoCommandFoundError() raise e arguments = vars(args) if unknown_args: # Raise error for extra-args without -- separation if "--" not in unknown_args: raise InvalidCommandArgumentError( f"Invalid commitizen arguments were found: `{' '.join(unknown_args)}`. " "Please use -- separator for extra git args" ) # Raise error for extra-args before -- elif unknown_args[0] != "--": pos = unknown_args.index("--") raise InvalidCommandArgumentError( f"Invalid commitizen arguments were found before -- separator: `{' '.join(unknown_args[:pos])}`. " ) # Log warning for -- without any extra args elif len(unknown_args) == 1: logger.warning( "\nWARN: Incomplete commit command: received -- separator without any following git arguments\n" ) extra_args = " ".join(unknown_args[1:]) arguments["extra_cli_args"] = extra_args conf = config.read_cfg(args.config) args = cast("Args", args) if args.name: conf.update({"name": args.name}) elif not conf.path: conf.update({"name": DEFAULT_SETTINGS["name"]}) if args.debug: logging.getLogger("commitizen").setLevel(logging.DEBUG) sys.excepthook = partial(sys.excepthook, debug=True) if args.no_raise: sys.excepthook = partial(sys.excepthook, no_raise=parse_no_raise(args.no_raise)) args.func(conf, arguments)() # type: ignore[arg-type] if __name__ == "__main__": main()
X Tutup