diff options
-rw-r--r-- | docs/configuration.md | 6 | ||||
-rw-r--r-- | docs/index.md | 5 | ||||
-rw-r--r-- | gitlint/cli.py | 3 | ||||
-rw-r--r-- | gitlint/git.py | 86 | ||||
-rw-r--r-- | gitlint/tests/base.py | 3 | ||||
-rw-r--r-- | gitlint/tests/test_config.py | 14 | ||||
-rw-r--r-- | gitlint/tests/test_git.py | 31 | ||||
-rw-r--r-- | gitlint/tests/test_lint.py | 16 |
8 files changed, 112 insertions, 52 deletions
diff --git a/docs/configuration.md b/docs/configuration.md index 951b624..6486d6d 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -54,7 +54,7 @@ Details about rule config options can be found in the Rules page. # Commandline config # -Alternatively, you can use one or more ```-c``` flags like so: +You can also use one or more ```-c``` flags like so: ``` $ gitlint -c general.verbosity=2 -c title-max-length.line-length=80 -c B1.line-length=100 @@ -62,7 +62,9 @@ $ gitlint -c general.verbosity=2 -c title-max-length.line-length=80 -c B1.line-l The generic config flag format is ```-c <rule>.<option>=<value>``` and supports all the same rules and options which you can also use in a ```.gitlint``` config file. -Finally, you can also disable gitlint for specific commit messages by adding ```gitlint-ignore: all``` to the commit +# Commit specific config # + +You can also disable gitlint for specific commit messages by adding ```gitlint-ignore: all``` to the commit message like so: ``` diff --git a/docs/index.md b/docs/index.md index e630b78..3b1107b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -46,7 +46,10 @@ NOTE: The returned exit code equals the number of errors found. [Some exit codes For a list of available rules and their configuration options, have a look at the [Rules](rules.md) page. -You can modify verbosity using the ```-v``` flag, like so: +The [Configuration](configuration.md) page explains how you can modify gitlint's runtime behavior via command-line +flags, a ```.gitlint``` configuration file or on a per commit basis. + +As a simple example, you can modify gitlint's verbosity using the ```-v``` flag, like so: ```bash $ cat examples/commit-message-2 | gitlint -v 1: T1 diff --git a/gitlint/cli.py b/gitlint/cli.py index e794536..00ef3f9 100644 --- a/gitlint/cli.py +++ b/gitlint/cli.py @@ -101,8 +101,7 @@ def lint(ctx): if sys.stdin.isatty(): gitcontext = GitContext.from_local_repository(lint_config.target) else: - gitcontext = GitContext() - gitcontext.set_commit_msg(sys.stdin.read()) + gitcontext = GitContext.from_commit_msg(sys.stdin.read()) except GitContextError as e: click.echo(str(e)) ctx.exit(GIT_CONTEXT_ERROR_CODE) diff --git a/gitlint/git.py b/gitlint/git.py index 7aae7f5..f2c24af 100644 --- a/gitlint/git.py +++ b/gitlint/git.py @@ -22,6 +22,15 @@ class GitCommitMessage(object): self.title = title self.body = body + @staticmethod + def from_full_message(commit_msg_str): + """ Parses a full git commit message by parsing a given string into the different parts of a commit message """ + lines = [line for line in commit_msg_str.split("\n") if not line.startswith("#")] + full = "\n".join(lines) + title = lines[0] if len(lines) > 0 else "" + body = lines[1:] if len(lines) > 1 else [] + return GitCommitMessage(original=commit_msg_str, full=full, title=title, body=body) + def __str__(self): return self.full # pragma: no cover @@ -29,34 +38,76 @@ class GitCommitMessage(object): return self.__str__() # pragma: no cover +class GitCommit(object): + """ Class representing a git commit. + A commit consists of: message, author name, author email, date, list of changed files + In the context of gitlint, only the commit message is required. + """ + + def __init__(self, message, date=None, author_name=None, author_email=None, changed_files=None): + self.message = message + self.author_name = author_name + self.author_email = author_email + self.date = date + + if not changed_files: + self.changed_files = [] + else: + self.changed_files = changed_files + + def __str__(self): + format_str = "Author: %s <%s>\nDate: %s\n%s" # pragma: no cover + return format_str % (self.author_name, self.author_email, self.date, str(self.message)) # pragma: no cover + + def __repr__(self): + return self.__str__() # pragma: no cover + + class GitContext(object): """ Class representing the git context in which gitlint is operating: a data object storing information about the git repository that gitlint is linting. """ def __init__(self): - self.commit_msg = None - self.changed_files = [] + self.commits = [] - def set_commit_msg(self, commit_msg_str): - """ Sets the commit message by parsing a given string into the different parts of a commit message """ - lines = [line for line in commit_msg_str.split("\n") if not line.startswith("#")] - full = "\n".join(lines) - title = lines[0] if len(lines) > 0 else "" - body = lines[1:] if len(lines) > 1 else [] - self.commit_msg = GitCommitMessage(original=commit_msg_str, full=full, title=title, body=body) + @property + def commit_msg(self): + if len(self.commits) > 0: + return self.commits[-1].message + return None + + @staticmethod + def from_commit_msg(commit_msg_str): + """ Determines git context based on a commit message. + :param commit_msg_str: Full git commit message. + """ + commit_msg_obj = GitCommitMessage.from_full_message(commit_msg_str) + commit = GitCommit(commit_msg_obj) + + context = GitContext() + context.commits.append(commit) + return context @staticmethod def from_local_repository(repository_path): + """ Retrieves the git context from a local git repository. + :param repository_path: Path to the git repository to retrieve the context from + """ try: # Special arguments passed to sh: http://amoffat.github.io/sh/special_arguments.html sh_special_args = { '_tty_out': False, '_cwd': repository_path } + # Get info from the local git repository - # last commit message + # 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) + commit_date = sh.git.log("-1", "--pretty=%aD", **sh_special_args) + # changed files in last commit changed_files_str = sh.git("diff-tree", "--no-commit-id", "--name-only", "-r", "HEAD", **sh_special_args) except CommandNotFound: @@ -71,8 +122,13 @@ class GitContext(object): error_msg = "An error occurred while executing '{}': {}".format(e.full_cmd, error_msg) raise GitContextError(error_msg) - # Create GitContext object with the retrieved info and return - commit_info = GitContext() - commit_info.set_commit_msg(commit_msg) - commit_info.changed_files = [changed_file for changed_file in changed_files_str.strip().split("\n")] - return commit_info + # 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(commit_msg_obj, author_name=commit_author_name, author_email=commit_author_email, + date=commit_date, changed_files=changed_files) + + # Create GitContext info with the commit object and return + context = GitContext() + context.commits.append(commit) + return context diff --git a/gitlint/tests/base.py b/gitlint/tests/base.py index ed2f774..a8c9943 100644 --- a/gitlint/tests/base.py +++ b/gitlint/tests/base.py @@ -23,8 +23,7 @@ class BaseTestCase(TestCase): def gitcontext(commit_msg_str, changed_files=None): """ Utility method to easily create gitcontext objects based on a given commit msg string and set of changed files""" - gitcontext = GitContext() - gitcontext.set_commit_msg(commit_msg_str) + gitcontext = GitContext.from_commit_msg(commit_msg_str) if changed_files: gitcontext.changed_files = changed_files else: diff --git a/gitlint/tests/test_config.py b/gitlint/tests/test_config.py index 55787d7..82a7ff8 100644 --- a/gitlint/tests/test_config.py +++ b/gitlint/tests/test_config.py @@ -1,6 +1,5 @@ from gitlint.tests.base import BaseTestCase from gitlint.config import LintConfig, LintConfigError, LintConfigGenerator, GITLINT_CONFIG_TEMPLATE_SRC_PATH -from gitlint.git import GitContext from gitlint import rules from mock import patch @@ -145,34 +144,31 @@ class LintConfigTests(BaseTestCase): original_rules = config.rules # nothing gitlint - context = GitContext() - context.set_commit_msg("test\ngitlint\nfoo") + context = self.gitcontext("test\ngitlint\nfoo") config.apply_config_from_gitcontext(context) self.assertListEqual(config.rules, original_rules) # ignore all rules - context = GitContext() - context.set_commit_msg("test\ngitlint-ignore: all\nfoo") + context = self.gitcontext("test\ngitlint-ignore: all\nfoo") config.apply_config_from_gitcontext(context) self.assertEqual(config.rules, []) # ignore all rules, no space config = LintConfig() - context.set_commit_msg("test\ngitlint-ignore:all\nfoo") + context = self.gitcontext("test\ngitlint-ignore:all\nfoo") config.apply_config_from_gitcontext(context) self.assertEqual(config.rules, []) # ignore all rules, more spacing config = LintConfig() - context.set_commit_msg("test\ngitlint-ignore: \t all\nfoo") + context = self.gitcontext("test\ngitlint-ignore: \t all\nfoo") config.apply_config_from_gitcontext(context) self.assertEqual(config.rules, []) def test_gitcontext_ignore_specific(self): # ignore specific rules config = LintConfig() - context = GitContext() - context.set_commit_msg("test\ngitlint-ignore: T1, body-hard-tab") + context = self.gitcontext("test\ngitlint-ignore: T1, body-hard-tab") config.apply_config_from_gitcontext(context) expected_rules = [rule for rule in config.rules if rule.id not in ["T1", "body-hard-tab"]] self.assertEqual(config.rules, expected_rules) diff --git a/gitlint/tests/test_git.py b/gitlint/tests/test_git.py index 1681f9c..db3d700 100644 --- a/gitlint/tests/test_git.py +++ b/gitlint/tests/test_git.py @@ -1,13 +1,18 @@ from gitlint.tests.base import BaseTestCase from gitlint.git import GitContext, GitContextError from sh import ErrorReturnCode, CommandNotFound -from mock import patch +from mock import patch, call class GitTests(BaseTestCase): @patch('gitlint.git.sh') def test_get_latest_commit(self, sh): - sh.git.log.return_value = "commit-title\n\ncommit-body" + def git_log_side_effect(*args, **kwargs): + return_values = {'--pretty=%B': "commit-title\n\ncommit-body", '--pretty=%aN': "test author", + '--pretty=%aE': "test-email@foo.com", '--pretty=%aD': "Mon Feb 29 22:19:39 2016 +0100"} + return return_values[args[1]] + + sh.git.log.side_effect = git_log_side_effect sh.git.return_value = "file1.txt\npath/to/file2.txt\n" context = GitContext.from_local_repository("fake/path") @@ -15,15 +20,23 @@ class GitTests(BaseTestCase): '_tty_out': False, '_cwd': "fake/path" } - # assert that commit message was read using git command - sh.git.log.assert_called_once_with('-1', '--pretty=%B', **expected_sh_special_args) + # assert that commit info was read using git command + expected_calls = [call('-1', '--pretty=%B', _cwd='fake/path', _tty_out=False), + call('-1', '--pretty=%aN', _cwd='fake/path', _tty_out=False), + call('-1', '--pretty=%aE', _cwd='fake/path', _tty_out=False), + call('-1', '--pretty=%aD', _cwd='fake/path', _tty_out=False)] + + self.assertListEqual(sh.git.log.mock_calls, expected_calls) + self.assertEqual(context.commit_msg.title, "commit-title") self.assertEqual(context.commit_msg.body, ["", "commit-body"]) + self.assertEqual(context.commits[-1].author_name, "test author") + self.assertEqual(context.commits[-1].author_email, "test-email@foo.com") # assert that changed files are read using git command sh.git.assert_called_once_with('diff-tree', '--no-commit-id', '--name-only', '-r', 'HEAD', **expected_sh_special_args) - self.assertListEqual(context.changed_files, ["file1.txt", "path/to/file2.txt"]) + self.assertListEqual(context.commits[-1].changed_files, ["file1.txt", "path/to/file2.txt"]) @patch('gitlint.git.sh') def test_get_latest_commit_command_not_found(self, sh): @@ -47,9 +60,8 @@ class GitTests(BaseTestCase): # assert that commit message was read using git command sh.git.log.assert_called_once_with('-1', '--pretty=%B', _tty_out=False, _cwd="fake/path") - def test_set_commit_msg_full(self): - gitcontext = GitContext() - gitcontext.set_commit_msg(self.get_sample("commit_message/sample1")) + def test_from_commit_msg_full(self): + gitcontext = GitContext.from_commit_msg(self.get_sample("commit_message/sample1")) expected_title = "Commit title containing 'WIP', as well as trailing punctuation." expected_body = ["This line should be empty", @@ -66,8 +78,7 @@ class GitTests(BaseTestCase): self.assertEqual(gitcontext.commit_msg.original, expected_original) def test_set_commit_msg_just_title(self): - gitcontext = GitContext() - gitcontext.set_commit_msg(self.get_sample("commit_message/sample2")) + gitcontext = self.gitcontext(self.get_sample("commit_message/sample2")) self.assertEqual(gitcontext.commit_msg.title, "Just a title containing WIP") self.assertEqual(gitcontext.commit_msg.body, []) diff --git a/gitlint/tests/test_lint.py b/gitlint/tests/test_lint.py index 300b242..8af8281 100644 --- a/gitlint/tests/test_lint.py +++ b/gitlint/tests/test_lint.py @@ -2,7 +2,6 @@ from gitlint.tests.base import BaseTestCase from gitlint.lint import GitLinter from gitlint.rules import RuleViolation from gitlint.config import LintConfig -from gitlint.git import GitContext from mock import patch try: @@ -16,8 +15,7 @@ except ImportError: class RuleOptionTests(BaseTestCase): def test_lint_sample1(self): linter = GitLinter(LintConfig()) - gitcontext = GitContext() - gitcontext.set_commit_msg(self.get_sample("commit_message/sample1")) + gitcontext = self.gitcontext(self.get_sample("commit_message/sample1")) violations = linter.lint(gitcontext) expected_errors = [RuleViolation("T3", "Title has trailing punctuation (.)", "Commit title containing 'WIP', as well as trailing punctuation.", 1), @@ -37,8 +35,7 @@ class RuleOptionTests(BaseTestCase): def test_lint_sample2(self): linter = GitLinter(LintConfig()) - gitcontext = GitContext() - gitcontext.set_commit_msg(self.get_sample("commit_message/sample2")) + gitcontext = self.gitcontext(self.get_sample("commit_message/sample2")) violations = linter.lint(gitcontext) expected = [RuleViolation("T5", "Title contains the word 'WIP' (case-insensitive)", "Just a title containing WIP", 1), @@ -48,8 +45,7 @@ class RuleOptionTests(BaseTestCase): def test_lint_sample3(self): linter = GitLinter(LintConfig()) - gitcontext = GitContext() - gitcontext.set_commit_msg(self.get_sample("commit_message/sample3")) + gitcontext = self.gitcontext(self.get_sample("commit_message/sample3")) violations = linter.lint(gitcontext) title = " Commit title containing 'WIP', \tleading and trailing whitespace and longer than 72 characters." @@ -73,8 +69,7 @@ class RuleOptionTests(BaseTestCase): self.assertListEqual(violations, expected) def test_lint_sample4(self): - gitcontext = GitContext() - gitcontext.set_commit_msg(self.get_sample("commit_message/sample4")) + gitcontext = self.gitcontext(self.get_sample("commit_message/sample4")) lintconfig = LintConfig() lintconfig.apply_config_from_gitcontext(gitcontext) linter = GitLinter(lintconfig) @@ -84,8 +79,7 @@ class RuleOptionTests(BaseTestCase): self.assertListEqual(violations, expected) def test_lint_sample5(self): - gitcontext = GitContext() - gitcontext.set_commit_msg(self.get_sample("commit_message/sample5")) + gitcontext = self.gitcontext(self.get_sample("commit_message/sample5")) lintconfig = LintConfig() lintconfig.apply_config_from_gitcontext(gitcontext) linter = GitLinter(lintconfig) |