# 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 # Error codes MAX_VIOLATION_ERROR_CODE = 252 # noqa USAGE_ERROR_CODE = 253 # noqa GIT_CONTEXT_ERROR_CODE = 254 # noqa CONFIG_ERROR_CODE = 255 # noqa 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.utils import ustr, LOG_FORMAT DEFAULT_CONFIG_FILE = ".gitlint" # 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 LOG = logging.getLogger(__name__) class GitLintUsageError(Exception): """ 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]")) def build_config( # pylint: disable=too-many-arguments target, config_path, c, extra_path, ignore, contrib, ignore_stdin, staged, 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) 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 ustr(input_data) return False def build_git_context(lint_config, msg_filename, 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(ustr(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(u"The 'staged' option (--staged) can only be used when using '--msg-filename' or " u"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.") return GitContext.from_local_repository(lint_config.target, refspec) @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', 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="Config file location [default: {0}]".format(DEFAULT_CONFIG_FILE)) @click.option('-c', multiple=True, help="Config flags in format .