diff options
Diffstat (limited to 'gitlint-core/gitlint/cli.py')
-rw-r--r-- | gitlint-core/gitlint/cli.py | 135 |
1 files changed, 92 insertions, 43 deletions
diff --git a/gitlint-core/gitlint/cli.py b/gitlint-core/gitlint/cli.py index 19676b3..387072e 100644 --- a/gitlint-core/gitlint/cli.py +++ b/gitlint-core/gitlint/cli.py @@ -11,6 +11,7 @@ import click import gitlint from gitlint.lint import GitLinter from gitlint.config import LintConfigBuilder, LintConfigError, LintConfigGenerator +from gitlint.deprecation import LOG as DEPRECATED_LOG, DEPRECATED_LOG_FORMAT from gitlint.git import GitContext, GitContextError, git_version from gitlint import hooks from gitlint.shell import shell @@ -37,19 +38,29 @@ LOG = logging.getLogger("gitlint.cli") class GitLintUsageError(GitlintError): - """ Exception indicating there is an issue with how gitlint is used. """ + """Exception indicating there is an issue with how gitlint is used.""" + pass def setup_logging(): - """ Setup gitlint logging """ + """Setup gitlint logging""" + + # Root log, mostly used for debug root_log = logging.getLogger("gitlint") root_log.propagate = False # Don't propagate to child loggers, the gitlint root logger handles everything + root_log.setLevel(logging.ERROR) handler = logging.StreamHandler() formatter = logging.Formatter(LOG_FORMAT) handler.setFormatter(formatter) root_log.addHandler(handler) - root_log.setLevel(logging.ERROR) + + # Deprecated log, to log deprecation warnings + DEPRECATED_LOG.propagate = False # Don't propagate to child logger + DEPRECATED_LOG.setLevel(logging.WARNING) + deprecated_log_handler = logging.StreamHandler() + deprecated_log_handler.setFormatter(logging.Formatter(DEPRECATED_LOG_FORMAT)) + DEPRECATED_LOG.addHandler(deprecated_log_handler) def log_system_info(): @@ -62,10 +73,20 @@ def log_system_info(): 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 + 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. """ + """Creates a LintConfig object based on a set of commandline parameters.""" config_builder = LintConfigBuilder() # Config precedence: # First, load default config or config from configfile @@ -79,33 +100,33 @@ def build_config( # pylint: disable=too-many-arguments # Finally, overwrite with any convenience commandline flags if ignore: - config_builder.set_option('general', 'ignore', ignore) + config_builder.set_option("general", "ignore", ignore) if contrib: - config_builder.set_option('general', 'contrib', contrib) + config_builder.set_option("general", "contrib", contrib) if ignore_stdin: - config_builder.set_option('general', 'ignore-stdin', ignore_stdin) + config_builder.set_option("general", "ignore-stdin", ignore_stdin) if silent: - config_builder.set_option('general', 'verbosity', 0) + config_builder.set_option("general", "verbosity", 0) elif verbose > 0: - config_builder.set_option('general', 'verbosity', verbose) + config_builder.set_option("general", "verbosity", verbose) if extra_path: - config_builder.set_option('general', 'extra-path', extra_path) + config_builder.set_option("general", "extra-path", extra_path) if target: - config_builder.set_option('general', 'target', target) + config_builder.set_option("general", "target", target) if debug: - config_builder.set_option('general', 'debug', debug) + config_builder.set_option("general", "debug", debug) if staged: - config_builder.set_option('general', 'staged', staged) + config_builder.set_option("general", "staged", staged) if fail_without_commits: - config_builder.set_option('general', 'fail-without-commits', fail_without_commits) + config_builder.set_option("general", "fail-without-commits", fail_without_commits) config = config_builder.build() @@ -113,7 +134,7 @@ def build_config( # pylint: disable=too-many-arguments def get_stdin_data(): - """ Helper function that returns data send to stdin or False if nothing is send """ + """Helper function that returns data sent to stdin or False if nothing is sent""" # 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) @@ -145,13 +166,17 @@ def get_stdin_data(): def build_git_context(lint_config, msg_filename, commit_hash, refspec): - """ Builds a git context based on passed parameters and order of precedence """ + """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 + from_commit_msg = ( + lambda message: GitContext.from_staged_commit( # pylint: disable=unnecessary-lambda-assignment + message, lint_config.target + ) + ) # Order of precedence: # 1. Any data specified via --msg-filename @@ -168,8 +193,10 @@ def build_git_context(lint_config, msg_filename, commit_hash, refspec): 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.") + 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.") @@ -177,11 +204,25 @@ def build_git_context(lint_config, msg_filename, commit_hash, refspec): 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) + # 3.1 Linting a range of commits + if refspec: + # 3.1.1 Not real refspec, but comma-separated list of commit hashes + if "," in refspec: + commit_hashes = [hash.strip() for hash in refspec.split(",")] + return GitContext.from_local_repository(lint_config.target, commit_hashes=commit_hashes) + # 3.1.2 Real refspec + return GitContext.from_local_repository(lint_config.target, refspec=refspec) + + # 3.2 Linting a specific commit + if commit_hash: + return GitContext.from_local_repository(lint_config.target, commit_hashes=[commit_hash]) + + # 3.3 Fallback to linting the current HEAD + return GitContext.from_local_repository(lint_config.target) def handle_gitlint_error(ctx, exc): - """ Helper function to handle exceptions """ + """Helper function to handle exceptions""" if isinstance(exc, GitContextError): click.echo(exc) ctx.exit(GIT_CONTEXT_ERROR_CODE) @@ -194,7 +235,7 @@ def handle_gitlint_error(ctx, exc): class ContextObj: - """ Simple class to hold data that is passed between Click commands via the Click context. """ + """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 @@ -205,29 +246,34 @@ class ContextObj: self.gitcontext = gitcontext +# fmt: off @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), +@click.option('-C', '--config', envvar='GITLINT_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 <rule>.<option>=<value> (e.g.: -c T1.line-length=80). " + "Flag can be used multiple times to set multiple config values.") # pylint: disable=bad-continuation @click.option('--commit', envvar='GITLINT_COMMIT', default=None, help="Hash (SHA) of specific commit to lint.") -@click.option('--commits', envvar='GITLINT_COMMITS', default=None, help="The range of commits to lint. [default: HEAD]") +@click.option('--commits', envvar='GITLINT_COMMITS', default=None, + help="The range of commits (refspec or comma-separated hashes) to lint. [default: HEAD]") @click.option('-e', '--extra-path', envvar='GITLINT_EXTRA_PATH', help="Path to a directory or python module with extra user-defined rules", type=click.Path(exists=True, resolve_path=True, readable=True)) @click.option('--ignore', envvar='GITLINT_IGNORE', default="", help="Ignore rules (comma-separated by id or name).") @click.option('--contrib', envvar='GITLINT_CONTRIB', default="", help="Contrib rules to enable (comma-separated by id or name).") -@click.option('--msg-filename', type=click.File(), help="Path to a file containing a commit-msg.") +@click.option('--msg-filename', type=click.File(encoding=gitlint.utils.DEFAULT_ENCODING), + help="Path to a file containing a commit-msg.") @click.option('--ignore-stdin', envvar='GITLINT_IGNORE_STDIN', is_flag=True, help="Ignore any stdin data. Useful for running in CI server.") @click.option('--staged', envvar='GITLINT_STAGED', is_flag=True, - help="Read staged commit meta-info from the local repository.") + help="Attempt smart guesses about meta info (like author name, email, branch, changed files, etc) " + + "for staged commits.") @click.option('--fail-without-commits', envvar='GITLINT_FAIL_WITHOUT_COMMITS', is_flag=True, help="Hard fail when the target commit range is empty.") @click.option('-v', '--verbose', envvar='GITLINT_VERBOSITY', count=True, default=0, @@ -246,18 +292,18 @@ def cli( # pylint: disable=too-many-arguments Documentation: http://jorisroovers.github.io/gitlint """ - try: if debug: logging.getLogger("gitlint").setLevel(logging.DEBUG) + DEPRECATED_LOG.setLevel(logging.DEBUG) LOG.debug("To report issues, please visit https://github.com/jorisroovers/gitlint/issues") log_system_info() # Get the lint config from the commandline parameters and # store it in the context (click allows storing an arbitrary object in ctx.obj). - config, config_builder = build_config(target, config, c, extra_path, ignore, contrib, ignore_stdin, staged, - fail_without_commits, verbose, silent, debug) + config, config_builder = build_config(target, config, c, extra_path, ignore, contrib, ignore_stdin, + staged, fail_without_commits, verbose, silent, debug) LOG.debug("Configuration\n%s", config) ctx.obj = ContextObj(config, config_builder, commit, commits, msg_filename) @@ -268,12 +314,13 @@ def cli( # pylint: disable=too-many-arguments except GitlintError as e: handle_gitlint_error(ctx, e) +# fmt: on @cli.command("lint") @click.pass_context def lint(ctx): - """ Lints a git repository [default command] """ + """Lints a git repository [default command]""" lint_config = ctx.obj.config refspec = ctx.obj.refspec commit_hash = ctx.obj.commit_hash @@ -295,7 +342,7 @@ def lint(ctx): raise GitLintUsageError(f'No commits in range "{refspec}"') ctx.exit(GITLINT_SUCCESS) - LOG.debug('Linting %d commit(s)', number_of_commits) + LOG.debug("Linting %d commit(s)", number_of_commits) general_config_builder = ctx.obj.config_builder last_commit = gitcontext.commits[-1] @@ -334,7 +381,7 @@ def lint(ctx): @cli.command("install-hook") @click.pass_context def install_hook(ctx): - """ Install gitlint as a git commit-msg hook. """ + """Install gitlint as a git commit-msg hook.""" try: hooks.GitHookInstaller.install_commit_msg_hook(ctx.obj.config) hook_path = hooks.GitHookInstaller.commit_msg_hook_path(ctx.obj.config) @@ -348,7 +395,7 @@ def install_hook(ctx): @cli.command("uninstall-hook") @click.pass_context def uninstall_hook(ctx): - """ Uninstall gitlint commit-msg hook. """ + """Uninstall gitlint commit-msg hook.""" try: hooks.GitHookInstaller.uninstall_commit_msg_hook(ctx.obj.config) hook_path = hooks.GitHookInstaller.commit_msg_hook_path(ctx.obj.config) @@ -362,7 +409,7 @@ def uninstall_hook(ctx): @cli.command("run-hook") @click.pass_context def run_hook(ctx): - """ Runs the gitlint commit-msg hook. """ + """Runs the gitlint commit-msg hook.""" exit_code = 1 while exit_code > 0: @@ -378,16 +425,18 @@ def run_hook(ctx): exit_code = e.exit_code if exit_code == GITLINT_SUCCESS: - click.echo("gitlint: " + click.style("OK", fg='green') + " (no violations in commit message)") + click.echo("gitlint: " + click.style("OK", fg="green") + " (no violations in commit message)") continue click.echo("-----------------------------------------------") - click.echo("gitlint: " + click.style("Your commit message contains violations.", fg='red')) + click.echo("gitlint: " + click.style("Your commit message contains violations.", fg="red")) value = None while value not in ["y", "n", "e"]: - click.echo("Continue with commit anyways (this keeps the current commit message)? " - "[y(es)/n(no)/e(dit)] ", nl=False) + click.echo( + "Continue with commit anyways (this keeps the current commit message)? [y(es)/n(no)/e(dit)] ", + nl=False, + ) # Ideally, we'd want to use click.getchar() or click.prompt() to get user's input here instead of # input(). However, those functions currently don't support getting answers from stdin. @@ -431,15 +480,15 @@ def run_hook(ctx): @cli.command("generate-config") @click.pass_context def generate_config(ctx): - """ Generates a sample gitlint config file. """ - path = click.prompt('Please specify a location for the sample gitlint config file', default=DEFAULT_CONFIG_FILE) + """Generates a sample gitlint config file.""" + path = click.prompt("Please specify a location for the sample gitlint config file", default=DEFAULT_CONFIG_FILE) path = os.path.realpath(path) dir_name = os.path.dirname(path) if not os.path.exists(dir_name): click.echo(f"Error: Directory '{dir_name}' does not exist.", err=True) ctx.exit(USAGE_ERROR_CODE) elif os.path.exists(path): - click.echo(f"Error: File \"{path}\" already exists.", err=True) + click.echo(f'Error: File "{path}" already exists.', err=True) ctx.exit(USAGE_ERROR_CODE) LintConfigGenerator.generate_config(path) |