From 72b8c35be4293bd21de123854491c658c53af100 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sat, 4 Dec 2021 04:31:41 +0100 Subject: Adding upstream version 0.17.0. Signed-off-by: Daniel Baumann --- gitlint-core/gitlint/cli.py | 454 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 454 insertions(+) create mode 100644 gitlint-core/gitlint/cli.py (limited to 'gitlint-core/gitlint/cli.py') diff --git a/gitlint-core/gitlint/cli.py b/gitlint-core/gitlint/cli.py new file mode 100644 index 0000000..19676b3 --- /dev/null +++ b/gitlint-core/gitlint/cli.py @@ -0,0 +1,454 @@ +# pylint: disable=bad-option-value,wrong-import-position +# We need to disable the import position checks because of the windows check that we need to do below +import copy +import logging +import os +import platform +import stat +import sys +import click + +import gitlint +from gitlint.lint import GitLinter +from gitlint.config import LintConfigBuilder, LintConfigError, LintConfigGenerator +from gitlint.git import GitContext, GitContextError, git_version +from gitlint import hooks +from gitlint.shell import shell +from gitlint.utils import LOG_FORMAT +from gitlint.exception import GitlintError + +# Error codes +GITLINT_SUCCESS = 0 +MAX_VIOLATION_ERROR_CODE = 252 +USAGE_ERROR_CODE = 253 +GIT_CONTEXT_ERROR_CODE = 254 +CONFIG_ERROR_CODE = 255 + +DEFAULT_CONFIG_FILE = ".gitlint" +# -n: disable swap files. This fixes a vim error on windows (E303: Unable to open swap file for ) +DEFAULT_COMMIT_MSG_EDITOR = "vim -n" + +# Since we use the return code to denote the amount of errors, we need to change the default click usage error code +click.UsageError.exit_code = USAGE_ERROR_CODE + +# We don't use logging.getLogger(__main__) here because that will cause DEBUG output to be lost +# when invoking gitlint as a python module (python -m gitlint.cli) +LOG = logging.getLogger("gitlint.cli") + + +class GitLintUsageError(GitlintError): + """ Exception indicating there is an issue with how gitlint is used. """ + pass + + +def setup_logging(): + """ Setup gitlint logging """ + root_log = logging.getLogger("gitlint") + root_log.propagate = False # Don't propagate to child loggers, the gitlint root logger handles everything + handler = logging.StreamHandler() + formatter = logging.Formatter(LOG_FORMAT) + handler.setFormatter(formatter) + root_log.addHandler(handler) + root_log.setLevel(logging.ERROR) + + +def log_system_info(): + LOG.debug("Platform: %s", platform.platform()) + LOG.debug("Python version: %s", sys.version) + LOG.debug("Git version: %s", git_version()) + LOG.debug("Gitlint version: %s", gitlint.__version__) + LOG.debug("GITLINT_USE_SH_LIB: %s", os.environ.get("GITLINT_USE_SH_LIB", "[NOT SET]")) + LOG.debug("DEFAULT_ENCODING: %s", gitlint.utils.DEFAULT_ENCODING) + + +def build_config( # pylint: disable=too-many-arguments + target, config_path, c, extra_path, ignore, contrib, ignore_stdin, staged, fail_without_commits, verbose, + silent, debug +): + """ Creates a LintConfig object based on a set of commandline parameters. """ + config_builder = LintConfigBuilder() + # Config precedence: + # First, load default config or config from configfile + if config_path: + config_builder.set_from_config_file(config_path) + elif os.path.exists(DEFAULT_CONFIG_FILE): + config_builder.set_from_config_file(DEFAULT_CONFIG_FILE) + + # Then process any commandline configuration flags + config_builder.set_config_from_string_list(c) + + # Finally, overwrite with any convenience commandline flags + if ignore: + config_builder.set_option('general', 'ignore', ignore) + + if contrib: + config_builder.set_option('general', 'contrib', contrib) + + if ignore_stdin: + config_builder.set_option('general', 'ignore-stdin', ignore_stdin) + + if silent: + config_builder.set_option('general', 'verbosity', 0) + elif verbose > 0: + config_builder.set_option('general', 'verbosity', verbose) + + if extra_path: + config_builder.set_option('general', 'extra-path', extra_path) + + if target: + config_builder.set_option('general', 'target', target) + + if debug: + config_builder.set_option('general', 'debug', debug) + + if staged: + config_builder.set_option('general', 'staged', staged) + + if fail_without_commits: + config_builder.set_option('general', 'fail-without-commits', fail_without_commits) + + config = config_builder.build() + + return config, config_builder + + +def get_stdin_data(): + """ Helper function that returns data send to stdin or False if nothing is send """ + # STDIN can only be 3 different types of things ("modes") + # 1. An interactive terminal device (i.e. a TTY -> sys.stdin.isatty() or stat.S_ISCHR) + # 2. A (named) pipe (stat.S_ISFIFO) + # 3. A regular file (stat.S_ISREG) + # Technically, STDIN can also be other device type like a named unix socket (stat.S_ISSOCK), but we don't + # support that in gitlint (at least not today). + # + # Now, the behavior that we want is the following: + # If someone sends something directly to gitlint via a pipe or a regular file, read it. If not, read from the + # local repository. + # Note that we don't care about whether STDIN is a TTY or not, we only care whether data is via a pipe or regular + # file. + # However, in case STDIN is not a TTY, it HAS to be one of the 2 other things (pipe or regular file), even if + # no-one is actually sending anything to gitlint over them. In this case, we still want to read from the local + # repository. + # To support this use-case (which is common in CI runners such as Jenkins and Gitlab), we need to actually attempt + # to read from STDIN in case it's a pipe or regular file. In case that fails, then we'll fall back to reading + # from the local repo. + + mode = os.fstat(sys.stdin.fileno()).st_mode + stdin_is_pipe_or_file = stat.S_ISFIFO(mode) or stat.S_ISREG(mode) + if stdin_is_pipe_or_file: + input_data = sys.stdin.read() + # Only return the input data if there's actually something passed + # i.e. don't consider empty piped data + if input_data: + return str(input_data) + return False + + +def build_git_context(lint_config, msg_filename, commit_hash, refspec): + """ Builds a git context based on passed parameters and order of precedence """ + + # Determine which GitContext method to use if a custom message is passed + from_commit_msg = GitContext.from_commit_msg + if lint_config.staged: + LOG.debug("Fetching additional meta-data from staged commit") + from_commit_msg = lambda message: GitContext.from_staged_commit(message, lint_config.target) # noqa + + # Order of precedence: + # 1. Any data specified via --msg-filename + if msg_filename: + LOG.debug("Using --msg-filename.") + return from_commit_msg(str(msg_filename.read())) + + # 2. Any data sent to stdin (unless stdin is being ignored) + if not lint_config.ignore_stdin: + stdin_input = get_stdin_data() + if stdin_input: + LOG.debug("Stdin data: '%s'", stdin_input) + LOG.debug("Stdin detected and not ignored. Using as input.") + return from_commit_msg(stdin_input) + + if lint_config.staged: + raise GitLintUsageError("The 'staged' option (--staged) can only be used when using '--msg-filename' or " + "when piping data to gitlint via stdin.") + + # 3. Fallback to reading from local repository + LOG.debug("No --msg-filename flag, no or empty data passed to stdin. Using the local repo.") + + if commit_hash and refspec: + raise GitLintUsageError("--commit and --commits are mutually exclusive, use one or the other.") + + return GitContext.from_local_repository(lint_config.target, refspec=refspec, commit_hash=commit_hash) + + +def handle_gitlint_error(ctx, exc): + """ Helper function to handle exceptions """ + if isinstance(exc, GitContextError): + click.echo(exc) + ctx.exit(GIT_CONTEXT_ERROR_CODE) + elif isinstance(exc, GitLintUsageError): + click.echo(f"Error: {exc}") + ctx.exit(USAGE_ERROR_CODE) + elif isinstance(exc, LintConfigError): + click.echo(f"Config Error: {exc}") + ctx.exit(CONFIG_ERROR_CODE) + + +class ContextObj: + """ Simple class to hold data that is passed between Click commands via the Click context. """ + + def __init__(self, config, config_builder, commit_hash, refspec, msg_filename, gitcontext=None): + self.config = config + self.config_builder = config_builder + self.commit_hash = commit_hash + self.refspec = refspec + self.msg_filename = msg_filename + self.gitcontext = gitcontext + + +@click.group(invoke_without_command=True, context_settings={'max_content_width': 120}, + epilog="When no COMMAND is specified, gitlint defaults to 'gitlint lint'.") +@click.option('--target', envvar='GITLINT_TARGET', + type=click.Path(exists=True, resolve_path=True, file_okay=False, readable=True), + help="Path of the target git repository. [default: current working directory]") +@click.option('-C', '--config', type=click.Path(exists=True, dir_okay=False, readable=True, resolve_path=True), + help=f"Config file location [default: {DEFAULT_CONFIG_FILE}]") +@click.option('-c', multiple=True, + help="Config flags in format .