Skip to content

Commit

Permalink
[WIP] Add support for linting a range of commits natively.
Browse files Browse the repository at this point in the history
Fixes #14.
  • Loading branch information
tommyip committed Mar 5, 2017
1 parent 6f29bf8 commit 53062ec
Show file tree
Hide file tree
Showing 2 changed files with 47 additions and 32 deletions.
19 changes: 14 additions & 5 deletions gitlint/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ def build_config(ctx, target, config_path, c, extra_path, ignore, verbose, silen
@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('--commits', default='HEAD^...', help="The range of commits to lint.")
@click.option('-e', '--extra-path', help="Path to a directory with extra user-defined rules",
type=click.Path(exists=True, resolve_path=True, file_okay=False, readable=True))
@click.option('--ignore', default="", help="Ignore rules (comma-separated by id or name).")
Expand All @@ -79,14 +80,14 @@ def build_config(ctx, target, config_path, c, extra_path, ignore, verbose, silen
@click.option('-d', '--debug', help="Enable debugging output.", is_flag=True)
@click.version_option(version=gitlint.__version__)
@click.pass_context
def cli(ctx, target, config, c, extra_path, ignore, verbose, silent, debug):
def cli(ctx, target, config, c, commits, extra_path, ignore, verbose, silent, debug):
""" Git lint tool, checks your git commit messages for styling issues """

# 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(ctx, target, config, c, extra_path, ignore, verbose, silent, debug)

ctx.obj = (config, config_builder)
ctx.obj = (config, config_builder, commits)

# If no subcommand is specified, then just lint
if ctx.invoked_subcommand is None:
Expand All @@ -101,7 +102,7 @@ def lint(ctx):
try:
if sys.stdin.isatty():
# If target has not been set explicitly before, fallback to the current directory
gitcontext = GitContext.from_local_repository(lint_config.target)
gitcontext = GitContext.from_local_repository(lint_config.target, ctx.obj[2])
else:
stdin_str = ustr(sys.stdin.read())
gitcontext = GitContext.from_commit_msg(stdin_str)
Expand All @@ -117,8 +118,16 @@ def lint(ctx):

# Let's get linting!
linter = GitLinter(lint_config)
violations = linter.lint(last_commit)
linter.print_violations(violations)
number_of_commits = len(gitcontext.commits)
for index, commit in enumerate(gitcontext.commits):
violations = linter.lint(commit)
if violations:
click.echo(u"{0}Commit {1}:".format(
"\n" if index not in [0, number_of_commits] else "",
commit.sha[:10]
))
linter.print_violations(violations)

exit_code = min(MAX_VIOLATION_ERROR_CODE, len(violations))
ctx.exit(exit_code)

Expand Down
60 changes: 33 additions & 27 deletions gitlint/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,11 @@ class GitCommit(object):
In the context of gitlint, only the git context and commit message are required.
"""

def __init__(self, context, message, date=None, author_name=None, author_email=None, parents=None,
def __init__(self, context, message, sha, date=None, author_name=None, author_email=None, parents=None,
is_merge_commit=False, changed_files=None):
self.context = context
self.message = message
self.sha = sha
self.author_name = author_name
self.author_email = author_email
self.date = date
Expand Down Expand Up @@ -117,10 +118,12 @@ def from_commit_msg(commit_msg_str):
return context

@staticmethod
def from_local_repository(repository_path):
def from_local_repository(repository_path, refspec="HEAD^..."):
""" Retrieves the git context from a local git repository.
:param repository_path: Path to the git repository to retrieve the context from
:param refspec: The commit(s) to retrieve
"""

context = GitContext()
try:
# Special arguments passed to sh: http://amoffat.github.io/sh/special_arguments.html
Expand All @@ -129,18 +132,36 @@ def from_local_repository(repository_path):
'_cwd': repository_path
}

sha_list = sh.git("rev-list", refspec, **sh_special_args).split()

# Get info from the local git repository
# https://git-scm.com/docs/pretty-formats
commit_msg = sh.git.log("-1", "--pretty=%B", **sh_special_args)
commit_author_name = sh.git.log("-1", "--pretty=%aN", **sh_special_args)
commit_author_email = sh.git.log("-1", "--pretty=%aE", **sh_special_args)
# %aI -> ISO 8601-like format, while %aI is strict ISO 8601, it seems to be less widely supporte
commit_date_str = sh.git.log("-1", "--pretty=%ai", **sh_special_args)
commit_parents = sh.git.log("-1", "--pretty=%P", **sh_special_args).split(" ")
commit_is_merge_commit = len(commit_parents) > 1

# changed files in last commit
changed_files_str = sh.git("diff-tree", "--no-commit-id", "--name-only", "-r", "HEAD", **sh_special_args)
for sha in sha_list:
raw_commit = sh.git.log(sha, "-n 1", "--pretty=%aN,%aE,%ai,%P%n%B", **sh_special_args).split("\n")
meta_data, commit_msg = raw_commit[0], "\n".join(raw_commit[1:])

name, email, date, parents = meta_data.split(",")

commit_parents = parents.split(" ")
commit_is_merge_commit = len(commit_parents) > 1

# changed files in last commit
changed_files = sh.git("diff-tree", "--no-commit-id", "--name-only",
"-r", sha, **sh_special_args).split("\n")

# "YYYY-MM-DD HH:mm:ss Z" -> ISO 8601-like format
# Use arrow for datetime parsing, because apparently python is quirky around ISO-8601 dates:
# http://stackoverflow.com/a/30696682/381010
commit_date = arrow.get(ustr(date), "YYYY-MM-DD HH:mm:ss Z").datetime

# Create Git commit object with the retrieved info
commit_msg_obj = GitCommitMessage.from_full_message(commit_msg)
commit = GitCommit(context, commit_msg_obj, sha, author_name=name,
author_email=email, date=commit_date, changed_files=changed_files,
parents=commit_parents, is_merge_commit=commit_is_merge_commit)

context.commits.append(commit)

except CommandNotFound:
error_msg = u"'git' command not found. You need to install git to use gitlint on a local repository. " + \
u"See https://git-scm.com/book/en/v2/Getting-Started-Installing-Git on how to install git."
Expand All @@ -153,21 +174,6 @@ def from_local_repository(repository_path):
error_msg = u"An error occurred while executing '{0}': {1}".format(e.full_cmd, error_msg)
raise GitContextError(error_msg)

# "YYYY-MM-DD HH:mm:ss Z" -> ISO 8601-like format
# Use arrow for datetime parsing, because apparently python is quirky around ISO-8601 dates:
# http://stackoverflow.com/a/30696682/381010
commit_date = arrow.get(ustr(commit_date_str), "YYYY-MM-DD HH:mm:ss Z").datetime

# Create Git commit object with the retrieved info
changed_files = [changed_file for changed_file in changed_files_str.strip().split("\n")]
commit_msg_obj = GitCommitMessage.from_full_message(commit_msg)
commit = GitCommit(context, commit_msg_obj, author_name=commit_author_name, author_email=commit_author_email,
date=commit_date, changed_files=changed_files, parents=commit_parents,
is_merge_commit=commit_is_merge_commit)

# Create GitContext info with the commit object and return

context.commits.append(commit)
return context

def __eq__(self, other):
Expand Down

0 comments on commit 53062ec

Please sign in to comment.