summaryrefslogtreecommitdiffstats
path: root/gitlint-core/gitlint/cli.py
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2022-11-19 14:52:50 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2022-11-19 14:53:01 +0000
commitf3b6c222fb11c96e2f8bbaa0622f46c8ec486874 (patch)
tree0f38497775e27d3e16b20573b36dd22aa5b24f3e /gitlint-core/gitlint/cli.py
parentReleasing debian version 0.17.0-1. (diff)
downloadgitlint-f3b6c222fb11c96e2f8bbaa0622f46c8ec486874.tar.xz
gitlint-f3b6c222fb11c96e2f8bbaa0622f46c8ec486874.zip
Merging upstream version 0.18.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'gitlint-core/gitlint/cli.py')
-rw-r--r--gitlint-core/gitlint/cli.py135
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)