diff options
Diffstat (limited to 'qa')
36 files changed, 1352 insertions, 464 deletions
@@ -1,32 +1,21 @@ -# -*- coding: utf-8 -*- -# pylint: disable=bad-option-value,unidiomatic-typecheck,undefined-variable,no-else-return, -# pylint: disable=too-many-function-args,unexpected-keyword-arg - -import io import os import platform import shutil import sys import tempfile -from datetime import datetime +from datetime import datetime, timezone +from unittest import TestCase from uuid import uuid4 import arrow -try: - # python 2.x - from unittest2 import TestCase -except ImportError: - # python 3.x - from unittest import TestCase - -from qa.shell import git, gitlint, RunningCommand -from qa.utils import DEFAULT_ENCODING, ustr +from qa.shell import RunningCommand, git, gitlint +from qa.utils import FILE_ENCODING, PLATFORM_IS_WINDOWS, TERMINAL_ENCODING class BaseTestCase(TestCase): - """ Base class of which all gitlint integration test classes are derived. - Provides a number of convenience methods. """ + """Base class of which all gitlint integration test classes are derived. + Provides a number of convenience methods.""" # In case of assert failures, print the full error message maxDiff = None @@ -34,43 +23,39 @@ class BaseTestCase(TestCase): GITLINT_USE_SH_LIB = os.environ.get("GITLINT_USE_SH_LIB", "[NOT SET]") GIT_CONTEXT_ERROR_CODE = 254 - - @classmethod - def setUpClass(cls): - """ Sets up the integration tests by creating a new temporary git repository """ - cls.tmp_git_repos = [] - cls.tmp_git_repo = cls.create_tmp_git_repo() - - @classmethod - def tearDownClass(cls): - """ Cleans up the temporary git repositories """ - for repo in cls.tmp_git_repos: - shutil.rmtree(repo) + GITLINT_USAGE_ERROR = 253 def setUp(self): + """Sets up the integration tests by creating a new temporary git repository""" self.tmpfiles = [] + self.tmp_git_repos = [] + self.tmp_git_repo = self.create_tmp_git_repo() def tearDown(self): + # Clean up temporary files and repos for tmpfile in self.tmpfiles: os.remove(tmpfile) + for repo in self.tmp_git_repos: + # On windows we need to ignore errors because git might still be holding on to some files + shutil.rmtree(repo, ignore_errors=PLATFORM_IS_WINDOWS) - def assertEqualStdout(self, output, expected): # pylint: disable=invalid-name + def assertEqualStdout(self, output, expected): self.assertIsInstance(output, RunningCommand) - output = ustr(output.stdout) - output = output.replace('\r', '') + output = output.stdout.decode(TERMINAL_ENCODING) + output = output.replace("\r", "") self.assertMultiLineEqual(output, expected) - @classmethod - def generate_temp_path(cls): - return os.path.realpath("/tmp/gitlint-test-{0}".format(datetime.now().strftime("%Y%m%d-%H%M%S-%f"))) + @staticmethod + def generate_temp_path(): + timestamp = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S-%f") + return os.path.realpath(f"/tmp/gitlint-test-{timestamp}") # noqa - @classmethod - def create_tmp_git_repo(cls): - """ Creates a temporary git repository and returns its directory path """ - tmp_git_repo = cls.generate_temp_path() - cls.tmp_git_repos.append(tmp_git_repo) + def create_tmp_git_repo(self): + """Creates a temporary git repository and returns its directory path""" + tmp_git_repo = self.generate_temp_path() + self.tmp_git_repos.append(tmp_git_repo) - git("init", tmp_git_repo) + git("init", "--initial-branch", "main", tmp_git_repo) # configuring name and email is required in every git repot git("config", "user.name", "gitlint-test-user", _cwd=tmp_git_repo) git("config", "user.email", "gitlint@test.com", _cwd=tmp_git_repo) @@ -84,18 +69,48 @@ class BaseTestCase(TestCase): # http://stackoverflow.com/questions/5581857/git-and-the-umlaut-problem-on-mac-os-x git("config", "core.precomposeunicode", "true", _cwd=tmp_git_repo) + # Git now does commit message cleanup by default (e.g. removing trailing whitespace), disable that for testing + git("config", "commit.cleanup", "verbatim", _cwd=tmp_git_repo) + return tmp_git_repo @staticmethod - def create_file(parent_dir): - """ Creates a file inside a passed directory. Returns filename.""" - test_filename = u"test-fïle-" + str(uuid4()) - io.open(os.path.join(parent_dir, test_filename), 'a', encoding=DEFAULT_ENCODING).close() + def create_file(parent_dir, content=None): + """Creates a file inside a passed directory. Returns filename.""" + test_filename = "test-fïle-" + str(uuid4()) + full_path = os.path.join(parent_dir, test_filename) + + if content: + if isinstance(content, bytes): + open_kwargs = {"mode": "wb"} + else: + open_kwargs = {"mode": "w", "encoding": FILE_ENCODING} + + with open(full_path, **open_kwargs) as f: + f.write(content) + else: + open(full_path, "a", encoding=FILE_ENCODING).close() # noqa: SIM115 (Use context handler for opening files) + return test_filename - def create_simple_commit(self, message, out=None, ok_code=None, env=None, git_repo=None, tty_in=False): - """ Creates a simple commit with an empty test file. - :param message: Commit message for the commit. """ + @staticmethod + def create_environment(envvars=None): + """Creates a copy of the current os.environ and adds/overwrites a given set of variables to it""" + environment = os.environ.copy() + if envvars: + environment.update(envvars) + return environment + + def create_tmp_git_config(self, contents): + """Creates an environment with the GIT_CONFIG variable set to a file with the given contents.""" + tmp_config = self.create_tmpfile(contents) + return self.create_environment({"GIT_CONFIG": tmp_config}) + + def create_simple_commit( + self, message, *, file_contents=None, out=None, ok_code=None, env=None, git_repo=None, tty_in=False + ): + """Creates a simple commit with an empty test file. + :param message: Commit message for the commit.""" git_repo = self.tmp_git_repo if git_repo is None else git_repo @@ -103,28 +118,42 @@ class BaseTestCase(TestCase): # variables can influence how git runs. # This was needed to fix https://github.com/jorisroovers/gitlint/issues/15 as we need to make sure to use # the PATH variable that contains the virtualenv's python binary. - environment = os.environ - if env: - environment.update(env) + environment = self.create_environment(env) # Create file and add to git - test_filename = self.create_file(git_repo) + test_filename = self.create_file(git_repo, file_contents) git("add", test_filename, _cwd=git_repo) # https://amoffat.github.io/sh/#interactive-callbacks if not ok_code: ok_code = [0] - git("commit", "-m", message, _cwd=git_repo, _err_to_out=True, _out=out, _tty_in=tty_in, - _ok_code=ok_code, _env=environment) + git( + "commit", + "-m", + message, + _cwd=git_repo, + _err_to_out=True, + _out=out, + _tty_in=tty_in, + _ok_code=ok_code, + _env=environment, + ) return test_filename def create_tmpfile(self, content): - """ Utility method to create temp files. These are cleaned at the end of the test """ - # Not using a context manager to avoid unneccessary identation in test code + """Utility method to create temp files. These are cleaned at the end of the test""" + # Not using a context manager to avoid unnecessary indentation in test code tmpfile, tmpfilepath = tempfile.mkstemp() self.tmpfiles.append(tmpfilepath) - with io.open(tmpfile, "w", encoding=DEFAULT_ENCODING) as f: + + if isinstance(content, bytes): + open_kwargs = {"mode": "wb"} + else: + open_kwargs = {"mode": "w", "encoding": FILE_ENCODING} + + with open(tmpfile, **open_kwargs) as f: f.write(content) + return tmpfilepath @staticmethod @@ -147,32 +176,40 @@ class BaseTestCase(TestCase): @staticmethod def get_expected(filename="", variable_dict=None): - """ Utility method to read an 'expected' file and return it as a string. Optionally replace template variables - specified by variable_dict. """ + """Utility method to read an 'expected' file and return it as a string. Optionally replace template variables + specified by variable_dict.""" expected_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "expected") expected_path = os.path.join(expected_dir, filename) - expected = io.open(expected_path, encoding=DEFAULT_ENCODING).read() + # Expected files are UTF-8 encoded (not dependent on the system's default encoding) + with open(expected_path, encoding=FILE_ENCODING) as file: + expected = file.read() - if variable_dict: - expected = expected.format(**variable_dict) - return expected + if variable_dict: + expected = expected.format(**variable_dict) + return expected @staticmethod def get_system_info_dict(): - """ Returns a dict with items related to system values logged by `gitlint --debug` """ - expected_gitlint_version = gitlint("--version").replace("gitlint, version ", "").replace("\n", "") - expected_git_version = git("--version").replace("\n", "") - return {'platform': platform.platform(), 'python_version': sys.version, - 'git_version': expected_git_version, 'gitlint_version': expected_gitlint_version, - 'GITLINT_USE_SH_LIB': BaseTestCase.GITLINT_USE_SH_LIB} + """Returns a dict with items related to system values logged by `gitlint --debug`""" + expected_gitlint_version = gitlint("--version").replace("gitlint, version ", "").strip() + expected_git_version = git("--version").strip() + return { + "platform": platform.platform(), + "python_version": sys.version, + "git_version": expected_git_version, + "gitlint_version": expected_gitlint_version, + "GITLINT_USE_SH_LIB": BaseTestCase.GITLINT_USE_SH_LIB, + "TERMINAL_ENCODING": TERMINAL_ENCODING, + "FILE_ENCODING": FILE_ENCODING, + } def get_debug_vars_last_commit(self, git_repo=None): - """ Returns a dict with items related to `gitlint --debug` output for the last commit. """ + """Returns a dict with items related to `gitlint --debug` output for the last commit.""" target_repo = git_repo if git_repo else self.tmp_git_repo commit_sha = self.get_last_commit_hash(git_repo=target_repo) expected_date = git("log", "-1", "--pretty=%ai", _tty_out=False, _cwd=target_repo) expected_date = arrow.get(str(expected_date), "YYYY-MM-DD HH:mm:ss Z").format("YYYY-MM-DD HH:mm:ss Z") expected_kwargs = self.get_system_info_dict() - expected_kwargs.update({'target': target_repo, 'commit_sha': commit_sha, 'commit_date': expected_date}) + expected_kwargs.update({"target": target_repo, "commit_sha": commit_sha, "commit_date": expected_date}) return expected_kwargs diff --git a/qa/expected/test_commits/test_csv_hash_list_1 b/qa/expected/test_commits/test_csv_hash_list_1 new file mode 100644 index 0000000..bbd9f51 --- /dev/null +++ b/qa/expected/test_commits/test_csv_hash_list_1 @@ -0,0 +1,11 @@ +Commit {commit_sha2}: +1: T3 Title has trailing punctuation (.): "Sïmple title2." +3: B6 Body message is missing + +Commit {commit_sha1}: +1: T3 Title has trailing punctuation (.): "Sïmple title1." +3: B6 Body message is missing + +Commit {commit_sha4}: +1: T3 Title has trailing punctuation (.): "Sïmple title4." +3: B6 Body message is missing diff --git a/qa/expected/test_commits/test_ignore_commits_1 b/qa/expected/test_commits/test_ignore_commits_1 index f9062c1..01cf8bd 100644 --- a/qa/expected/test_commits/test_ignore_commits_1 +++ b/qa/expected/test_commits/test_ignore_commits_1 @@ -1,3 +1,5 @@ +WARNING: I1 - ignore-by-title: gitlint will be switching from using Python regex 'match' (match beginning) to 'search' (match anywhere) semantics. Please review your ignore-by-title.regex option accordingly. To remove this warning, set general.regex-style-search=True. More details: https://jorisroovers.github.io/gitlint/configuration/#regex-style-search +WARNING: I2 - ignore-by-body: gitlint will be switching from using Python regex 'match' (match beginning) to 'search' (match anywhere) semantics. Please review your ignore-by-body.regex option accordingly. To remove this warning, set general.regex-style-search=True. More details: https://jorisroovers.github.io/gitlint/configuration/#regex-style-search Commit {commit_sha0}: 1: T3 Title has trailing punctuation (.): "Sïmple title4." diff --git a/qa/expected/test_commits/test_lint_staged_msg_filename_1 b/qa/expected/test_commits/test_lint_staged_msg_filename_1 index 878bc4c..03a558c 100644 --- a/qa/expected/test_commits/test_lint_staged_msg_filename_1 +++ b/qa/expected/test_commits/test_lint_staged_msg_filename_1 @@ -1,9 +1,12 @@ DEBUG: gitlint.cli To report issues, please visit https://github.com/jorisroovers/gitlint/issues DEBUG: gitlint.cli Platform: {platform} DEBUG: gitlint.cli Python version: {python_version} +DEBUG: gitlint.git ('--version',) DEBUG: gitlint.cli Git version: {git_version} DEBUG: gitlint.cli Gitlint version: {gitlint_version} DEBUG: gitlint.cli GITLINT_USE_SH_LIB: {GITLINT_USE_SH_LIB} +DEBUG: gitlint.cli TERMINAL_ENCODING: {TERMINAL_ENCODING} +DEBUG: gitlint.cli FILE_ENCODING: {FILE_ENCODING} DEBUG: gitlint.cli Configuration config-path: None [GENERAL] @@ -12,10 +15,13 @@ contrib: [] ignore: ignore-merge-commits: True ignore-fixup-commits: True +ignore-fixup-amend-commits: True ignore-squash-commits: True ignore-revert-commits: True ignore-stdin: False staged: True +fail-without-commits: False +regex-style-search: False verbosity: 3 debug: True target: {target} @@ -26,6 +32,11 @@ target: {target} I2: ignore-by-body ignore=all regex=None + I3: ignore-body-lines + regex=None + I4: ignore-by-author-name + ignore=all + regex=None T1: title-max-length line-length=72 T2: title-trailing-whitespace @@ -35,7 +46,9 @@ target: {target} T5: title-must-not-contain-word words=WIP T7: title-match-regex - regex=.* + regex=None + T8: title-min-length + min-length=5 B1: body-max-line-length line-length=80 B5: body-min-length @@ -47,13 +60,20 @@ target: {target} B4: body-first-line-empty B7: body-changed-file-mention files= + B8: body-match-regex + regex=None M1: author-valid-email - regex=[^@ ]+@[^@ ]+\.[^@ ]+ + regex=^[^@ ]+@[^@ ]+\.[^@ ]+ DEBUG: gitlint.cli Fetching additional meta-data from staged commit DEBUG: gitlint.cli Using --msg-filename. +DEBUG: gitlint.git ('config', '--get', 'core.commentchar') DEBUG: gitlint.cli Linting 1 commit(s) DEBUG: gitlint.lint Linting commit [SHA UNKNOWN] +DEBUG: gitlint.git ('diff', '--staged', '--numstat', '-r') +DEBUG: gitlint.git ('config', '--get', 'user.name') +DEBUG: gitlint.git ('config', '--get', 'user.email') +DEBUG: gitlint.git ('rev-parse', '--abbrev-ref', 'HEAD') DEBUG: gitlint.lint Commit Object --- Commit Message ---- WIP: from fïle test. @@ -62,10 +82,14 @@ Author: gitlint-test-user <gitlint@test.com> Date: {staged_date} is-merge-commit: False is-fixup-commit: False +is-fixup-amend-commit: False is-squash-commit: False is-revert-commit: False -Branches: ['master'] +Parents: [] +Branches: ['main'] Changed Files: {changed_files} +Changed Files Stats: + {changed_files_stats} ----------------------- 1: T3 Title has trailing punctuation (.): "WIP: from fïle test." 1: T5 Title contains the word 'WIP' (case-insensitive): "WIP: from fïle test." diff --git a/qa/expected/test_commits/test_lint_staged_stdin_1 b/qa/expected/test_commits/test_lint_staged_stdin_1 index 3f178f8..7892865 100644 --- a/qa/expected/test_commits/test_lint_staged_stdin_1 +++ b/qa/expected/test_commits/test_lint_staged_stdin_1 @@ -1,9 +1,12 @@ DEBUG: gitlint.cli To report issues, please visit https://github.com/jorisroovers/gitlint/issues DEBUG: gitlint.cli Platform: {platform} DEBUG: gitlint.cli Python version: {python_version} +DEBUG: gitlint.git ('--version',) DEBUG: gitlint.cli Git version: {git_version} DEBUG: gitlint.cli Gitlint version: {gitlint_version} DEBUG: gitlint.cli GITLINT_USE_SH_LIB: {GITLINT_USE_SH_LIB} +DEBUG: gitlint.cli TERMINAL_ENCODING: {TERMINAL_ENCODING} +DEBUG: gitlint.cli FILE_ENCODING: {FILE_ENCODING} DEBUG: gitlint.cli Configuration config-path: None [GENERAL] @@ -12,10 +15,13 @@ contrib: [] ignore: ignore-merge-commits: True ignore-fixup-commits: True +ignore-fixup-amend-commits: True ignore-squash-commits: True ignore-revert-commits: True ignore-stdin: False staged: True +fail-without-commits: False +regex-style-search: False verbosity: 3 debug: True target: {target} @@ -26,6 +32,11 @@ target: {target} I2: ignore-by-body ignore=all regex=None + I3: ignore-body-lines + regex=None + I4: ignore-by-author-name + ignore=all + regex=None T1: title-max-length line-length=72 T2: title-trailing-whitespace @@ -35,7 +46,9 @@ target: {target} T5: title-must-not-contain-word words=WIP T7: title-match-regex - regex=.* + regex=None + T8: title-min-length + min-length=5 B1: body-max-line-length line-length=80 B5: body-min-length @@ -47,15 +60,22 @@ target: {target} B4: body-first-line-empty B7: body-changed-file-mention files= + B8: body-match-regex + regex=None M1: author-valid-email - regex=[^@ ]+@[^@ ]+\.[^@ ]+ + regex=^[^@ ]+@[^@ ]+\.[^@ ]+ DEBUG: gitlint.cli Fetching additional meta-data from staged commit DEBUG: gitlint.cli Stdin data: 'WIP: Pïpe test. ' DEBUG: gitlint.cli Stdin detected and not ignored. Using as input. +DEBUG: gitlint.git ('config', '--get', 'core.commentchar') DEBUG: gitlint.cli Linting 1 commit(s) DEBUG: gitlint.lint Linting commit [SHA UNKNOWN] +DEBUG: gitlint.git ('diff', '--staged', '--numstat', '-r') +DEBUG: gitlint.git ('config', '--get', 'user.name') +DEBUG: gitlint.git ('config', '--get', 'user.email') +DEBUG: gitlint.git ('rev-parse', '--abbrev-ref', 'HEAD') DEBUG: gitlint.lint Commit Object --- Commit Message ---- WIP: Pïpe test. @@ -64,10 +84,14 @@ Author: gitlint-test-user <gitlint@test.com> Date: {staged_date} is-merge-commit: False is-fixup-commit: False +is-fixup-amend-commit: False is-squash-commit: False is-revert-commit: False -Branches: ['master'] +Parents: [] +Branches: ['main'] Changed Files: {changed_files} +Changed Files Stats: + {changed_files_stats} ----------------------- 1: T3 Title has trailing punctuation (.): "WIP: Pïpe test." 1: T5 Title contains the word 'WIP' (case-insensitive): "WIP: Pïpe test." diff --git a/qa/expected/test_config/test_config_from_env_1 b/qa/expected/test_config/test_config_from_env_1 new file mode 100644 index 0000000..91eee40 --- /dev/null +++ b/qa/expected/test_config/test_config_from_env_1 @@ -0,0 +1,104 @@ +DEBUG: gitlint.cli To report issues, please visit https://github.com/jorisroovers/gitlint/issues +DEBUG: gitlint.cli Platform: {platform} +DEBUG: gitlint.cli Python version: {python_version} +DEBUG: gitlint.git ('--version',) +DEBUG: gitlint.cli Git version: {git_version} +DEBUG: gitlint.cli Gitlint version: {gitlint_version} +DEBUG: gitlint.cli GITLINT_USE_SH_LIB: {GITLINT_USE_SH_LIB} +DEBUG: gitlint.cli TERMINAL_ENCODING: {TERMINAL_ENCODING} +DEBUG: gitlint.cli FILE_ENCODING: {FILE_ENCODING} +DEBUG: gitlint.cli Configuration +config-path: None +[GENERAL] +extra-path: None +contrib: ['CC1', 'CT1'] +ignore: T1,T2 +ignore-merge-commits: True +ignore-fixup-commits: True +ignore-fixup-amend-commits: True +ignore-squash-commits: True +ignore-revert-commits: True +ignore-stdin: True +staged: False +fail-without-commits: True +regex-style-search: False +verbosity: 2 +debug: True +target: {target} +[RULES] + I1: ignore-by-title + ignore=all + regex=None + I2: ignore-by-body + ignore=all + regex=None + I3: ignore-body-lines + regex=None + I4: ignore-by-author-name + ignore=all + regex=None + T1: title-max-length + line-length=72 + T2: title-trailing-whitespace + T6: title-leading-whitespace + T3: title-trailing-punctuation + T4: title-hard-tab + T5: title-must-not-contain-word + words=WIP + T7: title-match-regex + regex=None + T8: title-min-length + min-length=5 + B1: body-max-line-length + line-length=80 + B5: body-min-length + min-length=20 + B6: body-is-missing + ignore-merge-commits=True + B2: body-trailing-whitespace + B3: body-hard-tab + B4: body-first-line-empty + B7: body-changed-file-mention + files= + B8: body-match-regex + regex=None + M1: author-valid-email + regex=^[^@ ]+@[^@ ]+\.[^@ ]+ + CC1: contrib-body-requires-signed-off-by + CT1: contrib-title-conventional-commits + types=fix,feat,chore,docs,style,refactor,perf,test,revert,ci,build + +DEBUG: gitlint.cli No --msg-filename flag, no or empty data passed to stdin. Using the local repo. +DEBUG: gitlint.git ('rev-list', '{commit_sha}') +DEBUG: gitlint.cli Linting 1 commit(s) +DEBUG: gitlint.git ('log', '{commit_sha}', '-1', '--pretty=%aN%x00%aE%x00%ai%x00%P%n%B') +DEBUG: gitlint.git ('config', '--get', 'core.commentchar') +DEBUG: gitlint.lint Linting commit {commit_sha} +DEBUG: gitlint.git ('diff-tree', '--no-commit-id', '--numstat', '-r', '--root', '{commit_sha}') +DEBUG: gitlint.git ('branch', '--contains', '{commit_sha}') +DEBUG: gitlint.lint Commit Object +--- Commit Message ---- +WIP: Thïs is a title thåt is a bit longer. +Content on the second line +This line of the body is here because we need it + +--- Meta info --------- +Author: gitlint-test-user <gitlint@test.com> +Date: {commit_date} +is-merge-commit: False +is-fixup-commit: False +is-fixup-amend-commit: False +is-squash-commit: False +is-revert-commit: False +Parents: [] +Branches: ['main'] +Changed Files: {changed_files} +Changed Files Stats: + {changed_files_stats} +----------------------- +1: CC1 Body does not contain a 'Signed-off-by' line +1: CT1 Title does not start with one of fix, feat, chore, docs, style, refactor, perf, test, revert, ci, build +1: T3 Title has trailing punctuation (.) +1: T5 Title contains the word 'WIP' (case-insensitive) +2: B4 Second line is not empty +DEBUG: gitlint.cli Exit Code = 5 diff --git a/qa/expected/test_config/test_config_from_env_2 b/qa/expected/test_config/test_config_from_env_2 new file mode 100644 index 0000000..06b0c1b --- /dev/null +++ b/qa/expected/test_config/test_config_from_env_2 @@ -0,0 +1,93 @@ +DEBUG: gitlint.cli To report issues, please visit https://github.com/jorisroovers/gitlint/issues +DEBUG: gitlint.cli Platform: {platform} +DEBUG: gitlint.cli Python version: {python_version} +DEBUG: gitlint.git ('--version',) +DEBUG: gitlint.cli Git version: {git_version} +DEBUG: gitlint.cli Gitlint version: {gitlint_version} +DEBUG: gitlint.cli GITLINT_USE_SH_LIB: {GITLINT_USE_SH_LIB} +DEBUG: gitlint.cli TERMINAL_ENCODING: {TERMINAL_ENCODING} +DEBUG: gitlint.cli FILE_ENCODING: {FILE_ENCODING} +DEBUG: gitlint.cli Configuration +config-path: None +[GENERAL] +extra-path: None +contrib: [] +ignore: +ignore-merge-commits: True +ignore-fixup-commits: True +ignore-fixup-amend-commits: True +ignore-squash-commits: True +ignore-revert-commits: True +ignore-stdin: False +staged: True +fail-without-commits: False +regex-style-search: False +verbosity: 0 +debug: True +target: {target} +[RULES] + I1: ignore-by-title + ignore=all + regex=None + I2: ignore-by-body + ignore=all + regex=None + I3: ignore-body-lines + regex=None + I4: ignore-by-author-name + ignore=all + regex=None + T1: title-max-length + line-length=72 + T2: title-trailing-whitespace + T6: title-leading-whitespace + T3: title-trailing-punctuation + T4: title-hard-tab + T5: title-must-not-contain-word + words=WIP + T7: title-match-regex + regex=None + T8: title-min-length + min-length=5 + B1: body-max-line-length + line-length=80 + B5: body-min-length + min-length=20 + B6: body-is-missing + ignore-merge-commits=True + B2: body-trailing-whitespace + B3: body-hard-tab + B4: body-first-line-empty + B7: body-changed-file-mention + files= + B8: body-match-regex + regex=None + M1: author-valid-email + regex=^[^@ ]+@[^@ ]+\.[^@ ]+ + +DEBUG: gitlint.cli Fetching additional meta-data from staged commit +DEBUG: gitlint.cli Using --msg-filename. +DEBUG: gitlint.git ('config', '--get', 'core.commentchar') +DEBUG: gitlint.cli Linting 1 commit(s) +DEBUG: gitlint.lint Linting commit [SHA UNKNOWN] +DEBUG: gitlint.git ('diff', '--staged', '--numstat', '-r') +DEBUG: gitlint.git ('config', '--get', 'user.name') +DEBUG: gitlint.git ('config', '--get', 'user.email') +DEBUG: gitlint.git ('rev-parse', '--abbrev-ref', 'HEAD') +DEBUG: gitlint.lint Commit Object +--- Commit Message ---- +WIP: msg-fïlename test. +--- Meta info --------- +Author: gitlint-test-user <gitlint@test.com> +Date: {date} +is-merge-commit: False +is-fixup-commit: False +is-fixup-amend-commit: False +is-squash-commit: False +is-revert-commit: False +Parents: [] +Branches: ['main'] +Changed Files: [] +Changed Files Stats: {{}} +----------------------- +DEBUG: gitlint.cli Exit Code = 3 diff --git a/qa/expected/test_config/test_config_from_file_debug_1 b/qa/expected/test_config/test_config_from_file_debug_1 index 443ee26..279fb32 100644 --- a/qa/expected/test_config/test_config_from_file_debug_1 +++ b/qa/expected/test_config/test_config_from_file_debug_1 @@ -1,9 +1,12 @@ DEBUG: gitlint.cli To report issues, please visit https://github.com/jorisroovers/gitlint/issues DEBUG: gitlint.cli Platform: {platform} DEBUG: gitlint.cli Python version: {python_version} +DEBUG: gitlint.git ('--version',) DEBUG: gitlint.cli Git version: {git_version} DEBUG: gitlint.cli Gitlint version: {gitlint_version} DEBUG: gitlint.cli GITLINT_USE_SH_LIB: {GITLINT_USE_SH_LIB} +DEBUG: gitlint.cli TERMINAL_ENCODING: {TERMINAL_ENCODING} +DEBUG: gitlint.cli FILE_ENCODING: {FILE_ENCODING} DEBUG: gitlint.cli Configuration config-path: {config_path} [GENERAL] @@ -12,10 +15,13 @@ contrib: [] ignore: title-trailing-punctuation,B2 ignore-merge-commits: True ignore-fixup-commits: True +ignore-fixup-amend-commits: True ignore-squash-commits: True ignore-revert-commits: True ignore-stdin: False staged: False +fail-without-commits: False +regex-style-search: False verbosity: 2 debug: True target: {target} @@ -26,6 +32,11 @@ target: {target} I2: ignore-by-body ignore=all regex=None + I3: ignore-body-lines + regex=None + I4: ignore-by-author-name + ignore=all + regex=None T1: title-max-length line-length=20 T2: title-trailing-whitespace @@ -35,7 +46,9 @@ target: {target} T5: title-must-not-contain-word words=WIP,thåt T7: title-match-regex - regex=.* + regex=None + T8: title-min-length + min-length=5 B1: body-max-line-length line-length=30 B5: body-min-length @@ -47,12 +60,19 @@ target: {target} B4: body-first-line-empty B7: body-changed-file-mention files= + B8: body-match-regex + regex=None M1: author-valid-email - regex=[^@ ]+@[^@ ]+\.[^@ ]+ + regex=^[^@ ]+@[^@ ]+\.[^@ ]+ DEBUG: gitlint.cli No --msg-filename flag, no or empty data passed to stdin. Using the local repo. +DEBUG: gitlint.git ('log', '-1', '--pretty=%H') DEBUG: gitlint.cli Linting 1 commit(s) +DEBUG: gitlint.git ('log', '{commit_sha}', '-1', '--pretty=%aN%x00%aE%x00%ai%x00%P%n%B') +DEBUG: gitlint.git ('config', '--get', 'core.commentchar') DEBUG: gitlint.lint Linting commit {commit_sha} +DEBUG: gitlint.git ('diff-tree', '--no-commit-id', '--numstat', '-r', '--root', '{commit_sha}') +DEBUG: gitlint.git ('branch', '--contains', '{commit_sha}') DEBUG: gitlint.lint Commit Object --- Commit Message ---- WIP: Thïs is a title thåt is a bit longer. @@ -64,10 +84,14 @@ Author: gitlint-test-user <gitlint@test.com> Date: {commit_date} is-merge-commit: False is-fixup-commit: False +is-fixup-amend-commit: False is-squash-commit: False is-revert-commit: False -Branches: ['master'] +Parents: [] +Branches: ['main'] Changed Files: {changed_files} +Changed Files Stats: + {changed_files_stats} ----------------------- 1: T1 Title exceeds max length (42>20) 1: T5 Title contains the word 'WIP' (case-insensitive) diff --git a/qa/expected/test_contrib/test_contrib_rules_1 b/qa/expected/test_contrib/test_contrib_rules_1 index 99b33b7..6ab7512 100644 --- a/qa/expected/test_contrib/test_contrib_rules_1 +++ b/qa/expected/test_contrib/test_contrib_rules_1 @@ -1,4 +1,3 @@ -1: CC1 Body does not contain a 'Signed-Off-By' line -1: CT1 Title does not start with one of fix, feat, chore, docs, style, refactor, perf, test, revert: "WIP Thi$ is å title" +1: CC1 Body does not contain a 'Signed-off-by' line 1: CT1 Title does not follow ConventionalCommits.org format 'type(optional-scope): description': "WIP Thi$ is å title" 1: T5 Title contains the word 'WIP' (case-insensitive): "WIP Thi$ is å title" diff --git a/qa/expected/test_contrib/test_contrib_rules_with_config_1 b/qa/expected/test_contrib/test_contrib_rules_with_config_1 index 21d467a..6ab7512 100644 --- a/qa/expected/test_contrib/test_contrib_rules_with_config_1 +++ b/qa/expected/test_contrib/test_contrib_rules_with_config_1 @@ -1,4 +1,3 @@ -1: CC1 Body does not contain a 'Signed-Off-By' line -1: CT1 Title does not start with one of föo, bår: "WIP Thi$ is å title" +1: CC1 Body does not contain a 'Signed-off-by' line 1: CT1 Title does not follow ConventionalCommits.org format 'type(optional-scope): description': "WIP Thi$ is å title" 1: T5 Title contains the word 'WIP' (case-insensitive): "WIP Thi$ is å title" diff --git a/qa/expected/test_gitlint/test_commit_binary_file_1 b/qa/expected/test_gitlint/test_commit_binary_file_1 new file mode 100644 index 0000000..83faf1b --- /dev/null +++ b/qa/expected/test_gitlint/test_commit_binary_file_1 @@ -0,0 +1,95 @@ +DEBUG: gitlint.cli To report issues, please visit https://github.com/jorisroovers/gitlint/issues +DEBUG: gitlint.cli Platform: {platform} +DEBUG: gitlint.cli Python version: {python_version} +DEBUG: gitlint.git ('--version',) +DEBUG: gitlint.cli Git version: {git_version} +DEBUG: gitlint.cli Gitlint version: {gitlint_version} +DEBUG: gitlint.cli GITLINT_USE_SH_LIB: {GITLINT_USE_SH_LIB} +DEBUG: gitlint.cli TERMINAL_ENCODING: {TERMINAL_ENCODING} +DEBUG: gitlint.cli FILE_ENCODING: {FILE_ENCODING} +DEBUG: gitlint.cli Configuration +config-path: None +[GENERAL] +extra-path: None +contrib: [] +ignore: +ignore-merge-commits: True +ignore-fixup-commits: True +ignore-fixup-amend-commits: True +ignore-squash-commits: True +ignore-revert-commits: True +ignore-stdin: False +staged: False +fail-without-commits: False +regex-style-search: False +verbosity: 3 +debug: True +target: {target} +[RULES] + I1: ignore-by-title + ignore=all + regex=None + I2: ignore-by-body + ignore=all + regex=None + I3: ignore-body-lines + regex=None + I4: ignore-by-author-name + ignore=all + regex=None + T1: title-max-length + line-length=72 + T2: title-trailing-whitespace + T6: title-leading-whitespace + T3: title-trailing-punctuation + T4: title-hard-tab + T5: title-must-not-contain-word + words=WIP + T7: title-match-regex + regex=None + T8: title-min-length + min-length=5 + B1: body-max-line-length + line-length=80 + B5: body-min-length + min-length=20 + B6: body-is-missing + ignore-merge-commits=True + B2: body-trailing-whitespace + B3: body-hard-tab + B4: body-first-line-empty + B7: body-changed-file-mention + files= + B8: body-match-regex + regex=None + M1: author-valid-email + regex=^[^@ ]+@[^@ ]+\.[^@ ]+ + +DEBUG: gitlint.cli No --msg-filename flag, no or empty data passed to stdin. Using the local repo. +DEBUG: gitlint.git ('log', '-1', '--pretty=%H') +DEBUG: gitlint.cli Linting 1 commit(s) +DEBUG: gitlint.git ('log', '{commit_sha}', '-1', '--pretty=%aN%x00%aE%x00%ai%x00%P%n%B') +DEBUG: gitlint.git ('config', '--get', 'core.commentchar') +DEBUG: gitlint.lint Linting commit {commit_sha} +DEBUG: gitlint.git ('diff-tree', '--no-commit-id', '--numstat', '-r', '--root', '{commit_sha}') +DEBUG: gitlint.git ('branch', '--contains', '{commit_sha}') +DEBUG: gitlint.lint Commit Object +--- Commit Message ---- +Sïmple commit + +--- Meta info --------- +Author: gitlint-test-user <gitlint@test.com> +Date: {commit_date} +is-merge-commit: False +is-fixup-commit: False +is-fixup-amend-commit: False +is-squash-commit: False +is-revert-commit: False +Parents: [] +Branches: ['main'] +Changed Files: {changed_files} +Changed Files Stats: + {changed_files_stats} +----------------------- +3: B6 Body message is missing +DEBUG: gitlint.cli Exit Code = 1 diff --git a/qa/expected/test_named_rules/test_named_rule_1 b/qa/expected/test_named_rules/test_named_rule_1 new file mode 100644 index 0000000..e5a380c --- /dev/null +++ b/qa/expected/test_named_rules/test_named_rule_1 @@ -0,0 +1,5 @@ +1: T5 Title contains the word 'WIP' (case-insensitive): "WIP: thåt dûr bår" +1: T5 Title contains the word 'thåt' (case-insensitive): "WIP: thåt dûr bår" +1: T5:even$more%wôrds Title contains the word 'bår' (case-insensitive): "WIP: thåt dûr bår" +1: T5:extra-wôrds Title contains the word 'dûr' (case-insensitive): "WIP: thåt dûr bår" +3: B5 Body message is too short (18<20): "Sïmple commit body" diff --git a/qa/expected/test_named_rules/test_named_user_rule_1 b/qa/expected/test_named_rules/test_named_user_rule_1 new file mode 100644 index 0000000..3cd18b4 --- /dev/null +++ b/qa/expected/test_named_rules/test_named_user_rule_1 @@ -0,0 +1,9 @@ +1: UC4 int-öption: 2 +1: UC4 str-öption: föo +1: UC4 list-öption: ['foo', 'bar'] +1: UC4:bår int-öption: 2 +1: UC4:bår str-öption: bår +1: UC4:bår list-öption: ['bar', 'list'] +1: UC4:föo int-öption: 3 +1: UC4:föo str-öption: föo +1: UC4:föo list-öption: ['foo', 'bar'] diff --git a/qa/expected/test_rules/test_ignore_rules_1 b/qa/expected/test_rules/test_ignore_rules_1 new file mode 100644 index 0000000..f87f303 --- /dev/null +++ b/qa/expected/test_rules/test_ignore_rules_1 @@ -0,0 +1,3 @@ +1: T5 Title contains the word 'WIP' (case-insensitive): "WIP: Commït Tïtle" +3: B3 Line contains hard tab characters (\t): "Sïmple commit body" +4: B2 Line has trailing whitespace: "Anōther Line " diff --git a/qa/expected/test_rules/test_ignore_rules_2 b/qa/expected/test_rules/test_ignore_rules_2 new file mode 100644 index 0000000..dc6428c --- /dev/null +++ b/qa/expected/test_rules/test_ignore_rules_2 @@ -0,0 +1,2 @@ +1: T5 Title contains the word 'WIP' (case-insensitive): "WIP: Commït Tïtle" +3: B3 Line contains hard tab characters (\t): "Sïmple commit body" diff --git a/qa/expected/test_rules/test_match_regex_rules_1 b/qa/expected/test_rules/test_match_regex_rules_1 new file mode 100644 index 0000000..3bfaa58 --- /dev/null +++ b/qa/expected/test_rules/test_match_regex_rules_1 @@ -0,0 +1,2 @@ +1: T7 Title does not match regex (foo): "Thåt dûr bår" +4: B8 Body does not match regex (bar) diff --git a/qa/expected/test_user_defined/test_user_defined_rules_examples_1 b/qa/expected/test_user_defined/test_user_defined_rules_examples_1 index 9d00445..e675d7b 100644 --- a/qa/expected/test_user_defined/test_user_defined_rules_examples_1 +++ b/qa/expected/test_user_defined/test_user_defined_rules_examples_1 @@ -1,5 +1,5 @@ 1: T5 Title contains the word 'WIP' (case-insensitive): "WIP: Thi$ is å title" -1: UC2 Body does not contain a 'Signed-Off-By' line -1: UC3 Branch name 'master' does not start with one of ['feature/', 'hotfix/', 'release/'] +1: UC2 Body does not contain a 'Signed-off-by' line +1: UC3 Branch name 'main' does not start with one of ['feature/', 'hotfix/', 'release/'] 1: UL1 Title contains the special character '$': "WIP: Thi$ is å title" 2: B4 Second line is not empty: "Content on the second line" diff --git a/qa/expected/test_user_defined/test_user_defined_rules_examples_2 b/qa/expected/test_user_defined/test_user_defined_rules_examples_2 new file mode 100644 index 0000000..d706b12 --- /dev/null +++ b/qa/expected/test_user_defined/test_user_defined_rules_examples_2 @@ -0,0 +1,5 @@ +1: UC2 Body does not contain a 'Signed-off-by' line +1: UC3 Branch name 'main' does not start with one of ['feature/', 'hotfix/', 'release/'] +1: UL1 Title contains the special character '$' +2: B4 Second line is not empty +3: B3 Line contains hard tab characters (\t) diff --git a/qa/expected/test_user_defined/test_user_defined_rules_examples_with_config_1 b/qa/expected/test_user_defined/test_user_defined_rules_examples_with_config_1 index a143715..6e0d4cd 100644 --- a/qa/expected/test_user_defined/test_user_defined_rules_examples_with_config_1 +++ b/qa/expected/test_user_defined/test_user_defined_rules_examples_with_config_1 @@ -1,6 +1,6 @@ 1: T5 Title contains the word 'WIP' (case-insensitive): "WIP: Thi$ is å title" 1: UC1 Body contains too many lines (2 > 1) -1: UC2 Body does not contain a 'Signed-Off-By' line -1: UC3 Branch name 'master' does not start with one of ['feature/', 'hotfix/', 'release/'] +1: UC2 Body does not contain a 'Signed-off-by' line +1: UC3 Branch name 'main' does not start with one of ['feature/', 'hotfix/', 'release/'] 1: UL1 Title contains the special character '$': "WIP: Thi$ is å title" 2: B4 Second line is not empty: "Content on the second line" diff --git a/qa/expected/test_user_defined/test_user_defined_rules_extra_1 b/qa/expected/test_user_defined/test_user_defined_rules_extra_1 index 65f3507..77642dc 100644 --- a/qa/expected/test_user_defined/test_user_defined_rules_extra_1 +++ b/qa/expected/test_user_defined/test_user_defined_rules_extra_1 @@ -1,5 +1,9 @@ 1: T5 Title contains the word 'WIP' (case-insensitive): "WIP: Thi$ is å title" -1: UC1 GitContext.current_branch: master +1: UC1 GitContext.current_branch: main 1: UC1 GitContext.commentchar: # -1: UC2 GitCommit.branches: ['master'] -2: B4 Second line is not empty: "Content on the second line" +1: UC2 GitCommit.branches: ['main'] +1: UC2 GitCommit.custom_prop: foöbar +1: UC4 int-öption: 2 +1: UC4 str-öption: föo +1: UC4 list-öption: ['foo', 'bar'] +4: B2 Line has trailing whitespace: "{repo-path} " diff --git a/qa/requirements.txt b/qa/requirements.txt deleted file mode 100644 index f042dad..0000000 --- a/qa/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -sh==1.12.14 -pytest==4.6.3; -arrow==0.15.5; -gitlint # no version as you want to test the currently installed version diff --git a/qa/samples/config/contrib-enabled b/qa/samples/config/contrib-enabled deleted file mode 100644 index e69de29..0000000 --- a/qa/samples/config/contrib-enabled +++ /dev/null diff --git a/qa/samples/config/named-rules b/qa/samples/config/named-rules new file mode 100644 index 0000000..f9bbdf5 --- /dev/null +++ b/qa/samples/config/named-rules @@ -0,0 +1,8 @@ +[title-must-not-contain-word] +words=WIP,thåt + +[title-must-not-contain-word:extra-wôrds] +words=hûr,dûr + +[title-must-not-contain-word: even$more%wôrds ] +words=fôo,bår
\ No newline at end of file diff --git a/qa/samples/config/named-user-rules b/qa/samples/config/named-user-rules new file mode 100644 index 0000000..ed811fb --- /dev/null +++ b/qa/samples/config/named-user-rules @@ -0,0 +1,15 @@ +# Ignore other user-defined rules +[general] +ignore=UC1,UC2,UC3,configürable:ignöred + +[UC4:föo] +int-öption=3 +str-öption=föo + +[configürable:bår] +str-öption=bår +list-öption=bar,list + +# The following rule will be ignored +[configürable:ignöred] +str-öption=foöbar
\ No newline at end of file diff --git a/qa/samples/user_rules/extra/extra_rules.py b/qa/samples/user_rules/extra/extra_rules.py index 8109299..7996590 100644 --- a/qa/samples/user_rules/extra/extra_rules.py +++ b/qa/samples/user_rules/extra/extra_rules.py @@ -1,29 +1,72 @@ -from gitlint.rules import CommitRule, RuleViolation -from gitlint.utils import sstr +from gitlint.options import IntOption, ListOption, StrOption +from gitlint.rules import CommitRule, ConfigurationRule, RuleViolation class GitContextRule(CommitRule): - """ Rule that tests whether we can correctly access certain gitcontext properties """ - name = "gitcontext" + """Rule that tests whether we can correctly access certain gitcontext properties""" + + name = "gïtcontext" id = "UC1" def validate(self, commit): violations = [ - RuleViolation(self.id, "GitContext.current_branch: {0}".format(commit.context.current_branch), line_nr=1), - RuleViolation(self.id, "GitContext.commentchar: {0}".format(commit.context.commentchar), line_nr=1) + RuleViolation(self.id, f"GitContext.current_branch: {commit.context.current_branch}", line_nr=1), + RuleViolation(self.id, f"GitContext.commentchar: {commit.context.commentchar}", line_nr=1), ] return violations class GitCommitRule(CommitRule): - """ Rule that tests whether we can correctly access certain commit properties """ - name = "gitcommit" + """Rule that tests whether we can correctly access certain commit properties""" + + name = "gïtcommit" id = "UC2" def validate(self, commit): violations = [ - RuleViolation(self.id, "GitCommit.branches: {0}".format(sstr(commit.branches)), line_nr=1), + RuleViolation(self.id, f"GitCommit.branches: {commit.branches}", line_nr=1), + RuleViolation(self.id, f"GitCommit.custom_prop: {commit.custom_prop}", line_nr=1), + ] + + return violations + + +class GitlintConfigurationRule(ConfigurationRule): + """Rule that tests whether we can correctly access the config as well as modify the commit message""" + + name = "cönfigrule" + id = "UC3" + + def apply(self, config, commit): + # We add a line to the commit message body that pulls a value from config, this proves we can modify the body + # and read the config contents + commit.message.body.append(f"{config.target} ") # trailing whitespace deliberate to trigger violation + + # We set a custom property that we access in CommitRule, to prove we can add extra properties to the commit + commit.custom_prop = "foöbar" + + # We also ignore some extra rules, proving that we can modify the config + config.ignore.append("B4") + + +class ConfigurableCommitRule(CommitRule): + """Rule that tests that we can add configuration to user-defined rules""" + + name = "configürable" + id = "UC4" + + options_spec = [ + IntOption("int-öption", 2, "int-öption description"), + StrOption("str-öption", "föo", "int-öption description"), + ListOption("list-öption", ["foo", "bar"], "list-öption description"), + ] + + def validate(self, _): + violations = [ + RuleViolation(self.id, f"int-öption: {self.options['int-öption'].value}", line_nr=1), + RuleViolation(self.id, f"str-öption: {self.options['str-öption'].value}", line_nr=1), + RuleViolation(self.id, f"list-öption: {self.options['list-öption'].value}", line_nr=1), ] return violations diff --git a/qa/shell.py b/qa/shell.py index 8ba6dc1..3ef874d 100644 --- a/qa/shell.py +++ b/qa/shell.py @@ -1,43 +1,69 @@ - -# This code is mostly duplicated from the `gitlint.shell` module. We conciously duplicate this code as to not depend +# This code is mostly duplicated from the `gitlint.shell` module. We consciously duplicate this code as to not depend # on gitlint internals for our integration testing framework. import subprocess -import sys -from qa.utils import ustr, USE_SH_LIB + +from qa.utils import TERMINAL_ENCODING, USE_SH_LIB if USE_SH_LIB: - from sh import git, echo, gitlint # pylint: disable=unused-import,no-name-in-module,import-error + from sh import ( + echo, + git, + gitlint, + ) + + gitlint = gitlint.bake(_unify_ttys=True, _tty_in=True) # import exceptions separately, this makes it a little easier to mock them out in the unit tests - from sh import CommandNotFound, ErrorReturnCode, RunningCommand # pylint: disable=import-error + from sh import ( + CommandNotFound, + ErrorReturnCode, + RunningCommand, + ) else: class CommandNotFound(Exception): - """ Exception indicating a command was not found during execution """ - pass + """Exception indicating a command was not found during execution""" - class RunningCommand(object): - pass + class RunningCommand: + ... class ShResult(RunningCommand): - """ Result wrapper class. We use this to more easily migrate from using https://amoffat.github.io/sh/ to using - the builtin subprocess module. """ + """Result wrapper class. We use this to more easily migrate from using https://amoffat.github.io/sh/ to using + the builtin subprocess module.""" - def __init__(self, full_cmd, stdout, stderr='', exitcode=0): + def __init__(self, full_cmd, stdout, stderr="", exitcode=0): self.full_cmd = full_cmd # TODO(jorisroovers): The 'sh' library by default will merge stdout and stderr. We mimic this behavior # for now until we fully remove the 'sh' library. - self.stdout = stdout + ustr(stderr) - self.stderr = stderr + self._stdout = stdout + stderr + self._stderr = stderr self.exit_code = exitcode def __str__(self): + return self.stdout.decode(TERMINAL_ENCODING) + + def __unicode__(self): return self.stdout + @property + def stdout(self): + return self._stdout + + @property + def stderr(self): + return self._stderr + + def __getattr__(self, p): + # https://github.com/amoffat/sh/blob/e0ed8e244e9d973ef4e0749b2b3c2695e7b5255b/sh.py#L952= + _unicode_methods = set(dir(str())) # noqa + if p in _unicode_methods: + return getattr(str(self), p) + + raise AttributeError + class ErrorReturnCode(ShResult, Exception): - """ ShResult subclass for unexpected results (acts as an exception). """ - pass + """ShResult subclass for unexpected results (acts as an exception).""" def git(*command_parts, **kwargs): return run_command("git", *command_parts, **kwargs) @@ -49,39 +75,41 @@ else: return run_command("gitlint", *command_parts, **kwargs) def run_command(command, *args, **kwargs): - args = [command] + list(args) - result = _exec(*args, **kwargs) - # If we reach this point and the result has an exit_code that is larger than 0, this means that we didn't - # get an exception (which is the default sh behavior for non-zero exit codes) and so the user is expecting - # a non-zero exit code -> just return the entire result - if hasattr(result, 'exit_code') and result.exit_code > 0: - return result - return ustr(result) + args = [command, *list(args)] + return _exec(*args, **kwargs) def _exec(*args, **kwargs): - if sys.version_info[0] == 2: - no_command_error = OSError # noqa pylint: disable=undefined-variable,invalid-name - else: - no_command_error = FileNotFoundError # noqa pylint: disable=undefined-variable - - pipe = subprocess.PIPE - popen_kwargs = {'stdout': pipe, 'stderr': pipe, 'shell': kwargs.get('_tty_out', False)} - if '_cwd' in kwargs: - popen_kwargs['cwd'] = kwargs['_cwd'] + popen_kwargs = { + "stdout": subprocess.PIPE, + "stderr": subprocess.PIPE, + "stdin": subprocess.PIPE, + "shell": kwargs.get("_tty_out", False), + "cwd": kwargs.get("_cwd", None), + "env": kwargs.get("_env", None), + } + + stdin_input = None + if len(args) > 1 and isinstance(args[1], ShResult): + stdin_input = args[1].stdout + # pop args[1] from the array and use it as stdin + args = list(args) + args.pop(1) + popen_kwargs["stdin"] = subprocess.PIPE try: - p = subprocess.Popen(args, **popen_kwargs) - result = p.communicate() - except no_command_error: - raise CommandNotFound + with subprocess.Popen(args, **popen_kwargs) as p: + result = p.communicate(stdin_input) + + except FileNotFoundError as exc: + raise CommandNotFound from exc exit_code = p.returncode - stdout = ustr(result[0]) + stdout = result[0] stderr = result[1] # 'sh' does not decode the stderr bytes to unicode - full_cmd = '' if args is None else ' '.join(args) + full_cmd = "" if args is None else " ".join(args) # If not _ok_code is specified, then only a 0 exit code is allowed - ok_exit_codes = kwargs.get('_ok_code', [0]) + ok_exit_codes = kwargs.get("_ok_code", [0]) if exit_code in ok_exit_codes: return ShResult(full_cmd, stdout, stderr, exit_code) diff --git a/qa/test_commits.py b/qa/test_commits.py index f485856..11d1851 100644 --- a/qa/test_commits.py +++ b/qa/test_commits.py @@ -1,67 +1,145 @@ -# -*- coding: utf-8 -*- -# pylint: disable=too-many-function-args,unexpected-keyword-arg import re import arrow -from qa.shell import echo, git, gitlint from qa.base import BaseTestCase -from qa.utils import sstr +from qa.shell import echo, git, gitlint class CommitsTests(BaseTestCase): - """ Integration tests for the --commits argument, i.e. linting multiple commits at once or linting specific commits - """ + """Integration tests for the --commits argument, i.e. linting multiple commits or linting specific commits""" def test_successful(self): - """ Test linting multiple commits without violations """ + """Test linting multiple commits without violations""" git("checkout", "-b", "test-branch-commits-base", _cwd=self.tmp_git_repo) - self.create_simple_commit(u"Sïmple title\n\nSimple bödy describing the commit") + self.create_simple_commit("Sïmple title\n\nSimple bödy describing the commit") git("checkout", "-b", "test-branch-commits", _cwd=self.tmp_git_repo) - self.create_simple_commit(u"Sïmple title2\n\nSimple bödy describing the commit2") - self.create_simple_commit(u"Sïmple title3\n\nSimple bödy describing the commit3") - output = gitlint("--commits", "test-branch-commits-base...test-branch-commits", - _cwd=self.tmp_git_repo, _tty_in=True) + self.create_simple_commit("Sïmple title2\n\nSimple bödy describing the commit2") + self.create_simple_commit("Sïmple title3\n\nSimple bödy describing the commit3") + output = gitlint( + "--commits", "test-branch-commits-base...test-branch-commits", _cwd=self.tmp_git_repo, _tty_in=True + ) self.assertEqualStdout(output, "") def test_violations(self): - """ Test linting multiple commits with violations """ + """Test linting multiple commits with violations""" git("checkout", "-b", "test-branch-commits-violations-base", _cwd=self.tmp_git_repo) - self.create_simple_commit(u"Sïmple title.\n") + self.create_simple_commit("Sïmple title.\n") git("checkout", "-b", "test-branch-commits-violations", _cwd=self.tmp_git_repo) - self.create_simple_commit(u"Sïmple title2.\n") + self.create_simple_commit("Sïmple title2.\n") commit_sha1 = self.get_last_commit_hash()[:10] - self.create_simple_commit(u"Sïmple title3.\n") + self.create_simple_commit("Sïmple title3.\n") commit_sha2 = self.get_last_commit_hash()[:10] - output = gitlint("--commits", "test-branch-commits-violations-base...test-branch-commits-violations", - _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[4]) + output = gitlint( + "--commits", + "test-branch-commits-violations-base...test-branch-commits-violations", + _cwd=self.tmp_git_repo, + _tty_in=True, + _ok_code=[4], + ) self.assertEqual(output.exit_code, 4) - expected_kwargs = {'commit_sha1': commit_sha1, 'commit_sha2': commit_sha2} + expected_kwargs = {"commit_sha1": commit_sha1, "commit_sha2": commit_sha2} self.assertEqualStdout(output, self.get_expected("test_commits/test_violations_1", expected_kwargs)) + def test_csv_hash_list(self): + """Test linting multiple commits (comma-separated) with violations""" + git("checkout", "-b", "test-branch-commits-violations-base", _cwd=self.tmp_git_repo) + self.create_simple_commit("Sïmple title1.\n") + commit_sha1 = self.get_last_commit_hash()[:10] + git("checkout", "-b", "test-branch-commits-violations", _cwd=self.tmp_git_repo) + + self.create_simple_commit("Sïmple title2.\n") + commit_sha2 = self.get_last_commit_hash()[:10] + self.create_simple_commit("Sïmple title3.\n") + self.create_simple_commit("Sïmple title4.\n") + commit_sha4 = self.get_last_commit_hash()[:10] + + # Lint subset of the commits in a specific order, passed in via csv list + output = gitlint( + "--commits", + f"{commit_sha2},{commit_sha1},{commit_sha4}", + _cwd=self.tmp_git_repo, + _tty_in=True, + _ok_code=[6], + ) + + self.assertEqual(output.exit_code, 6) + expected_kwargs = {"commit_sha1": commit_sha1, "commit_sha2": commit_sha2, "commit_sha4": commit_sha4} + self.assertEqualStdout(output, self.get_expected("test_commits/test_csv_hash_list_1", expected_kwargs)) + + def test_lint_empty_commit_range(self): + """Tests `gitlint --commits <sha>^...<sha>` --fail-without-commits where the provided range is empty.""" + self.create_simple_commit("Sïmple title.\n") + self.create_simple_commit("Sïmple title2.\n") + commit_sha = self.get_last_commit_hash() + # git revspec -> 2 dots: <exclusive sha>..<inclusive sha> -> empty range when using same start and end sha + refspec = f"{commit_sha}..{commit_sha}" + + # Regular gitlint invocation should run without issues + output = gitlint("--commits", refspec, _cwd=self.tmp_git_repo, _tty_in=True) + self.assertEqual(output.exit_code, 0) + self.assertEqualStdout(output, "") + + # Gitlint should fail when --fail-without-commits is used + output = gitlint( + "--commits", + refspec, + "--fail-without-commits", + _cwd=self.tmp_git_repo, + _tty_in=True, + _ok_code=[self.GITLINT_USAGE_ERROR], + ) + self.assertEqual(output.exit_code, self.GITLINT_USAGE_ERROR) + self.assertEqualStdout(output, f'Error: No commits in range "{refspec}"\n') + def test_lint_single_commit(self): - """ Tests `gitlint --commits <sha>` """ - self.create_simple_commit(u"Sïmple title.\n") - self.create_simple_commit(u"Sïmple title2.\n") + """Tests `gitlint --commits <sha>^...<same sha>`""" + self.create_simple_commit("Sïmple title.\n") + first_commit_sha = self.get_last_commit_hash() + self.create_simple_commit("Sïmple title2.\n") commit_sha = self.get_last_commit_hash() - refspec = "{0}^...{0}".format(commit_sha) - self.create_simple_commit(u"Sïmple title3.\n") + refspec = f"{commit_sha}^...{commit_sha}" + self.create_simple_commit("Sïmple title3.\n") + + expected = '1: T3 Title has trailing punctuation (.): "Sïmple title2."\n' + "3: B6 Body message is missing\n" + + # Lint using --commit <commit sha> + output = gitlint("--commit", commit_sha, _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[2]) + self.assertEqual(output.exit_code, 2) + self.assertEqualStdout(output, expected) + + # Lint using --commits <commit sha>, + output = gitlint("--commits", f"{commit_sha},", _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[2]) + self.assertEqual(output.exit_code, 2) + self.assertEqualStdout(output, expected) + + # Lint a single commit using --commits <refspec> pointing to the single commit output = gitlint("--commits", refspec, _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[2]) - expected = (u"1: T3 Title has trailing punctuation (.): \"Sïmple title2.\"\n" + - u"3: B6 Body message is missing\n") self.assertEqual(output.exit_code, 2) self.assertEqualStdout(output, expected) + # Lint the first commit in the repository. This is a use-case that is not supported by --commits + # As <sha>^...<sha> is not correct refspec in case <sha> points to the initial commit (which has no parents) + expected = '1: T3 Title has trailing punctuation (.): "Sïmple title."\n' + "3: B6 Body message is missing\n" + output = gitlint("--commit", first_commit_sha, _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[2]) + self.assertEqual(output.exit_code, 2) + self.assertEqualStdout(output, expected) + + # Assert that indeed --commits <refspec> is not supported when <refspec> points the the first commit + refspec = f"{first_commit_sha}^...{first_commit_sha}" + output = gitlint("--commits", refspec, _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[254]) + self.assertEqual(output.exit_code, 254) + def test_lint_staged_stdin(self): - """ Tests linting a staged commit. Gitint should lint the passed commit message andfetch additional meta-data - from the underlying repository. The easiest way to test this is by inspecting `--debug` output. - This is the equivalent of doing: - echo "WIP: Pïpe test." | gitlint --staged --debug + """Tests linting a staged commit. Gitint should lint the passed commit message and fetch additional meta-data + from the underlying repository. The easiest way to test this is by inspecting `--debug` output. + This is the equivalent of doing: + echo "WIP: Pïpe test." | gitlint --staged --debug """ # Create a commit first, before we stage changes. This ensures the repo is properly initialized. - self.create_simple_commit(u"Sïmple title.\n") + self.create_simple_commit("Sïmple title.\n") # Add some files, stage them: they should show up in the debug output as changed file filename1 = self.create_file(self.tmp_git_repo) @@ -69,33 +147,48 @@ class CommitsTests(BaseTestCase): filename2 = self.create_file(self.tmp_git_repo) git("add", filename2, _cwd=self.tmp_git_repo) - output = gitlint(echo(u"WIP: Pïpe test."), "--staged", "--debug", - _cwd=self.tmp_git_repo, _tty_in=False, _err_to_out=True, _ok_code=[3]) + output = gitlint( + echo("WIP: Pïpe test."), + "--staged", + "--debug", + _cwd=self.tmp_git_repo, + _tty_in=False, + _err_to_out=True, + _ok_code=[3], + ) # Determine variable parts of expected output expected_kwargs = self.get_debug_vars_last_commit() - expected_kwargs.update({'changed_files': sstr(sorted([filename1, filename2]))}) + filenames = sorted([filename1, filename2]) + expected_kwargs.update( + { + "changed_files": filenames, + "changed_files_stats": ( + f"{filenames[0]}: 0 additions, 0 deletions\n {filenames[1]}: 0 additions, 0 deletions" + ), + } + ) # It's not really possible to determine the "Date: ..." line that is part of the debug output as this date # is not taken from git but instead generated by gitlint itself. As a workaround, we extract the date from the # gitlint output using a regex, parse the date to ensure the format is correct, and then pass that as an # expected variable. - matches = re.search(r'^Date:\s+(.*)', str(output), re.MULTILINE) + matches = re.search(r"^Date:\s+(.*)", str(output), re.MULTILINE) if matches: expected_date = arrow.get(str(matches.group(1)), "YYYY-MM-DD HH:mm:ss Z").format("YYYY-MM-DD HH:mm:ss Z") - expected_kwargs['staged_date'] = expected_date + expected_kwargs["staged_date"] = expected_date self.assertEqualStdout(output, self.get_expected("test_commits/test_lint_staged_stdin_1", expected_kwargs)) self.assertEqual(output.exit_code, 3) def test_lint_staged_msg_filename(self): - """ Tests linting a staged commit. Gitint should lint the passed commit message andfetch additional meta-data - from the underlying repository. The easiest way to test this is by inspecting `--debug` output. - This is the equivalent of doing: - gitlint --msg-filename /tmp/my-commit-msg --staged --debug + """Tests linting a staged commit. Gitint should lint the passed commit message andfetch additional meta-data + from the underlying repository. The easiest way to test this is by inspecting `--debug` output. + This is the equivalent of doing: + gitlint --msg-filename /tmp/my-commit-msg --staged --debug """ # Create a commit first, before we stage changes. This ensures the repo is properly initialized. - self.create_simple_commit(u"Sïmple title.\n") + self.create_simple_commit("Sïmple title.\n") # Add some files, stage them: they should show up in the debug output as changed file filename1 = self.create_file(self.tmp_git_repo) @@ -103,59 +196,81 @@ class CommitsTests(BaseTestCase): filename2 = self.create_file(self.tmp_git_repo) git("add", filename2, _cwd=self.tmp_git_repo) - tmp_commit_msg_file = self.create_tmpfile(u"WIP: from fïle test.") + tmp_commit_msg_file = self.create_tmpfile("WIP: from fïle test.") - output = gitlint("--msg-filename", tmp_commit_msg_file, "--staged", "--debug", - _cwd=self.tmp_git_repo, _tty_in=False, _err_to_out=True, _ok_code=[3]) + output = gitlint( + "--msg-filename", + tmp_commit_msg_file, + "--staged", + "--debug", + _cwd=self.tmp_git_repo, + _tty_in=False, + _err_to_out=True, + _ok_code=[3], + ) # Determine variable parts of expected output expected_kwargs = self.get_debug_vars_last_commit() - expected_kwargs.update({'changed_files': sstr(sorted([filename1, filename2]))}) + filenames = sorted([filename1, filename2]) + expected_kwargs.update( + { + "changed_files": filenames, + "changed_files_stats": ( + f"{filenames[0]}: 0 additions, 0 deletions\n {filenames[1]}: 0 additions, 0 deletions" + ), + } + ) # It's not really possible to determine the "Date: ..." line that is part of the debug output as this date # is not taken from git but instead generated by gitlint itself. As a workaround, we extract the date from the # gitlint output using a regex, parse the date to ensure the format is correct, and then pass that as an # expected variable. - matches = re.search(r'^Date:\s+(.*)', str(output), re.MULTILINE) + matches = re.search(r"^Date:\s+(.*)", str(output), re.MULTILINE) if matches: expected_date = arrow.get(str(matches.group(1)), "YYYY-MM-DD HH:mm:ss Z").format("YYYY-MM-DD HH:mm:ss Z") - expected_kwargs['staged_date'] = expected_date + expected_kwargs["staged_date"] = expected_date expected = self.get_expected("test_commits/test_lint_staged_msg_filename_1", expected_kwargs) self.assertEqualStdout(output, expected) self.assertEqual(output.exit_code, 3) def test_lint_head(self): - """ Testing whether we can also recognize special refs like 'HEAD' """ + """Testing whether we can also recognize special refs like 'HEAD'""" tmp_git_repo = self.create_tmp_git_repo() - self.create_simple_commit(u"Sïmple title.\n\nSimple bödy describing the commit", git_repo=tmp_git_repo) - self.create_simple_commit(u"Sïmple title", git_repo=tmp_git_repo) - self.create_simple_commit(u"WIP: Sïmple title\n\nSimple bödy describing the commit", git_repo=tmp_git_repo) + self.create_simple_commit("Sïmple title.\n\nSimple bödy describing the commit", git_repo=tmp_git_repo) + self.create_simple_commit("Sïmple title", git_repo=tmp_git_repo) + self.create_simple_commit("WIP: Sïmple title\n\nSimple bödy describing the commit", git_repo=tmp_git_repo) output = gitlint("--commits", "HEAD", _cwd=tmp_git_repo, _tty_in=True, _ok_code=[3]) revlist = git("rev-list", "HEAD", _tty_in=True, _cwd=tmp_git_repo).split() - expected_kwargs = {"commit_sha0": revlist[0][:10], "commit_sha1": revlist[1][:10], - "commit_sha2": revlist[2][:10]} + expected_kwargs = { + "commit_sha0": revlist[0][:10], + "commit_sha1": revlist[1][:10], + "commit_sha2": revlist[2][:10], + } self.assertEqualStdout(output, self.get_expected("test_commits/test_lint_head_1", expected_kwargs)) def test_ignore_commits(self): - """ Tests multiple commits of which some rules get igonored because of ignore-* rules """ + """Tests multiple commits of which some rules get ignored because of ignore-* rules""" # Create repo and some commits tmp_git_repo = self.create_tmp_git_repo() - self.create_simple_commit(u"Sïmple title.\n\nSimple bödy describing the commit", git_repo=tmp_git_repo) + self.create_simple_commit("Sïmple title.\n\nSimple bödy describing the commit", git_repo=tmp_git_repo) # Normally, this commit will give T3 (trailing-punctuation), T5 (WIP) and B5 (bod-too-short) violations # But in this case only B5 because T3 and T5 are being ignored because of config - self.create_simple_commit(u"Release: WIP tïtle.\n\nShort", git_repo=tmp_git_repo) + self.create_simple_commit("Release: WIP tïtle.\n\nShort", git_repo=tmp_git_repo) # In the following 2 commits, the T3 violations are as normal - self.create_simple_commit( - u"Sïmple WIP title3.\n\nThis is \ta relëase commit\nMore info", git_repo=tmp_git_repo) - self.create_simple_commit(u"Sïmple title4.\n\nSimple bödy describing the commit4", git_repo=tmp_git_repo) + self.create_simple_commit("Sïmple WIP title3.\n\nThis is \ta relëase commit\nMore info", git_repo=tmp_git_repo) + self.create_simple_commit("Sïmple title4.\n\nSimple bödy describing the commit4", git_repo=tmp_git_repo) revlist = git("rev-list", "HEAD", _tty_in=True, _cwd=tmp_git_repo).split() config_path = self.get_sample_path("config/ignore-release-commits") output = gitlint("--commits", "HEAD", "--config", config_path, _cwd=tmp_git_repo, _tty_in=True, _ok_code=[4]) - expected_kwargs = {"commit_sha0": revlist[0][:10], "commit_sha1": revlist[1][:10], - "commit_sha2": revlist[2][:10], "commit_sha3": revlist[3][:10]} + expected_kwargs = { + "commit_sha0": revlist[0][:10], + "commit_sha1": revlist[1][:10], + "commit_sha2": revlist[2][:10], + "commit_sha3": revlist[3][:10], + } self.assertEqualStdout(output, self.get_expected("test_commits/test_ignore_commits_1", expected_kwargs)) diff --git a/qa/test_config.py b/qa/test_config.py index b893b1d..d051686 100644 --- a/qa/test_config.py +++ b/qa/test_config.py @@ -1,31 +1,36 @@ -# -*- coding: utf-8 -*- -# pylint: disable=too-many-function-args,unexpected-keyword-arg -from qa.shell import gitlint +import os +import re + from qa.base import BaseTestCase -from qa.utils import sstr +from qa.shell import gitlint class ConfigTests(BaseTestCase): - """ Integration tests for gitlint configuration and configuration precedence. """ + """Integration tests for gitlint configuration and configuration precedence.""" def test_ignore_by_id(self): - self.create_simple_commit(u"WIP: Thïs is a title.\nContënt on the second line") + self.create_simple_commit("WIP: Thïs is a title.\nContënt on the second line") output = gitlint("--ignore", "T5,B4", _tty_in=True, _cwd=self.tmp_git_repo, _ok_code=[1]) - expected = u"1: T3 Title has trailing punctuation (.): \"WIP: Thïs is a title.\"\n" + expected = '1: T3 Title has trailing punctuation (.): "WIP: Thïs is a title."\n' self.assertEqualStdout(output, expected) def test_ignore_by_name(self): - self.create_simple_commit(u"WIP: Thïs is a title.\nContënt on the second line") - output = gitlint("--ignore", "title-must-not-contain-word,body-first-line-empty", - _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[1]) - expected = u"1: T3 Title has trailing punctuation (.): \"WIP: Thïs is a title.\"\n" + self.create_simple_commit("WIP: Thïs is a title.\nContënt on the second line") + output = gitlint( + "--ignore", + "title-must-not-contain-word,body-first-line-empty", + _cwd=self.tmp_git_repo, + _tty_in=True, + _ok_code=[1], + ) + expected = '1: T3 Title has trailing punctuation (.): "WIP: Thïs is a title."\n' self.assertEqualStdout(output, expected) def test_verbosity(self): - self.create_simple_commit(u"WIP: Thïs is a title.\nContënt on the second line") + self.create_simple_commit("WIP: Thïs is a title.\nContënt on the second line") output = gitlint("-v", _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[3]) - expected = u"1: T3\n1: T5\n2: B4\n" + expected = "1: T3\n1: T5\n2: B4\n" self.assertEqualStdout(output, expected) output = gitlint("-vv", _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[3]) @@ -39,29 +44,90 @@ class ConfigTests(BaseTestCase): self.assertEqualStdout(output, "") def test_set_rule_option(self): - self.create_simple_commit(u"This ïs a title.") + self.create_simple_commit("This ïs a title.") output = gitlint("-c", "title-max-length.line-length=5", _tty_in=True, _cwd=self.tmp_git_repo, _ok_code=[3]) self.assertEqualStdout(output, self.get_expected("test_config/test_set_rule_option_1")) def test_config_from_file(self): - commit_msg = u"WIP: Thïs is a title thåt is a bit longer.\nContent on the second line\n" + \ - "This line of the body is here because we need it" + commit_msg = ( + "WIP: Thïs is a title thåt is a bit longer.\nContent on the second line\n" + "This line of the body is here because we need it" + ) self.create_simple_commit(commit_msg) config_path = self.get_sample_path("config/gitlintconfig") output = gitlint("--config", config_path, _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[5]) self.assertEqualStdout(output, self.get_expected("test_config/test_config_from_file_1")) def test_config_from_file_debug(self): - # Test bot on existing and new repo (we've had a bug in the past that was unique to empty repos) + # Test both on existing and new repo (we've had a bug in the past that was unique to empty repos) repos = [self.tmp_git_repo, self.create_tmp_git_repo()] for target_repo in repos: - commit_msg = u"WIP: Thïs is a title thåt is a bit longer.\nContent on the second line\n" + \ - "This line of the body is here because we need it" + commit_msg = ( + "WIP: Thïs is a title thåt is a bit longer.\nContent on the second line\n" + "This line of the body is here because we need it" + ) filename = self.create_simple_commit(commit_msg, git_repo=target_repo) - config_path = self.get_sample_path("config/gitlintconfig") + config_path = self.get_sample_path(os.path.join("config", "gitlintconfig")) output = gitlint("--config", config_path, "--debug", _cwd=target_repo, _tty_in=True, _ok_code=[5]) expected_kwargs = self.get_debug_vars_last_commit(git_repo=target_repo) - expected_kwargs.update({'config_path': config_path, 'changed_files': sstr([filename])}) - self.assertEqualStdout(output, self.get_expected("test_config/test_config_from_file_debug_1", - expected_kwargs)) + expected_kwargs.update( + { + "config_path": config_path, + "changed_files": [filename], + "changed_files_stats": f"{filename}: 0 additions, 0 deletions", + } + ) + self.assertEqualStdout( + output, self.get_expected("test_config/test_config_from_file_debug_1", expected_kwargs) + ) + + def test_config_from_env(self): + """Test for configuring gitlint from environment variables""" + + # We invoke gitlint, configuring it via env variables, we can check whether gitlint picks these up correctly + # by comparing the debug output with what we'd expect + target_repo = self.create_tmp_git_repo() + commit_msg = ( + "WIP: Thïs is a title thåt is a bit longer.\nContent on the second line\n" + "This line of the body is here because we need it" + ) + filename = self.create_simple_commit(commit_msg, git_repo=target_repo) + env = self.create_environment( + { + "GITLINT_DEBUG": "1", + "GITLINT_VERBOSITY": "2", + "GITLINT_IGNORE": "T1,T2", + "GITLINT_CONTRIB": "CC1,CT1", + "GITLINT_FAIL_WITHOUT_COMMITS": "1", + "GITLINT_IGNORE_STDIN": "1", + "GITLINT_TARGET": target_repo, + "GITLINT_COMMITS": self.get_last_commit_hash(git_repo=target_repo), + } + ) + output = gitlint(_env=env, _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[5]) + expected_kwargs = self.get_debug_vars_last_commit(git_repo=target_repo) + expected_kwargs.update( + {"changed_files": [filename], "changed_files_stats": f"{filename}: 0 additions, 0 deletions"} + ) + + self.assertEqualStdout(output, self.get_expected("test_config/test_config_from_env_1", expected_kwargs)) + + # For some env variables, we need a separate test ast they are mutually exclusive with the ones tested above + tmp_commit_msg_file = self.create_tmpfile("WIP: msg-fïlename test.") + env = self.create_environment( + {"GITLINT_DEBUG": "1", "GITLINT_TARGET": target_repo, "GITLINT_SILENT": "1", "GITLINT_STAGED": "1"} + ) + + output = gitlint( + "--msg-filename", tmp_commit_msg_file, _env=env, _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[3] + ) + + # Extract date from actual output to insert it into the expected output + # We have to do this since there's no way for us to deterministically know that date otherwise + p = re.compile("Date: (.*)\n", re.UNICODE | re.MULTILINE) + result = p.search(str(output)) + date = result.group(1).strip() + expected_kwargs.update({"date": date}) + + self.assertEqualStdout(output, self.get_expected("test_config/test_config_from_env_2", expected_kwargs)) diff --git a/qa/test_contrib.py b/qa/test_contrib.py index e2b4bc5..d3a45ba 100644 --- a/qa/test_contrib.py +++ b/qa/test_contrib.py @@ -1,26 +1,31 @@ -# -*- coding: utf-8 -*- -# pylint: disable= -from qa.shell import gitlint from qa.base import BaseTestCase +from qa.shell import gitlint class ContribRuleTests(BaseTestCase): - """ Integration tests for contrib rules.""" + """Integration tests for contrib rules.""" def test_contrib_rules(self): - self.create_simple_commit(u"WIP Thi$ is å title\n\nMy bödy that is a bit longer than 20 chars") - output = gitlint("--contrib", "contrib-title-conventional-commits,CC1", - _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[4]) + self.create_simple_commit("WIP Thi$ is å title\n\nMy bödy that is a bit longer than 20 chars") + output = gitlint( + "--contrib", "contrib-title-conventional-commits,CC1", _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[3] + ) self.assertEqualStdout(output, self.get_expected("test_contrib/test_contrib_rules_1")) def test_contrib_rules_with_config(self): - self.create_simple_commit(u"WIP Thi$ is å title\n\nMy bödy that is a bit longer than 20 chars") - output = gitlint("--contrib", "contrib-title-conventional-commits,CC1", - "-c", u"contrib-title-conventional-commits.types=föo,bår", - _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[4]) + self.create_simple_commit("WIP Thi$ is å title\n\nMy bödy that is a bit longer than 20 chars") + output = gitlint( + "--contrib", + "contrib-title-conventional-commits,CC1", + "-c", + "contrib-title-conventional-commits.types=föo,bår", + _cwd=self.tmp_git_repo, + _tty_in=True, + _ok_code=[3], + ) self.assertEqualStdout(output, self.get_expected("test_contrib/test_contrib_rules_with_config_1")) def test_invalid_contrib_rules(self): self.create_simple_commit("WIP: test") - output = gitlint("--contrib", u"föobar,CC1", _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[255]) - self.assertEqualStdout(output, u"Config Error: No contrib rule with id or name 'föobar' found.\n") + output = gitlint("--contrib", "föobar,CC1", _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[255]) + self.assertEqualStdout(output, "Config Error: No contrib rule with id or name 'föobar' found.\n") diff --git a/qa/test_gitlint.py b/qa/test_gitlint.py index 4762721..7a04a39 100644 --- a/qa/test_gitlint.py +++ b/qa/test_gitlint.py @@ -1,47 +1,45 @@ -# -*- coding: utf-8 -*- -# pylint: disable=too-many-function-args,unexpected-keyword-arg -import io import os -from qa.shell import echo, git, gitlint + from qa.base import BaseTestCase -from qa.utils import DEFAULT_ENCODING +from qa.shell import echo, git, gitlint +from qa.utils import FILE_ENCODING class IntegrationTests(BaseTestCase): - """ Simple set of integration tests for gitlint """ + """Simple set of integration tests for gitlint""" def test_successful(self): # Test for STDIN with and without a TTY attached - self.create_simple_commit(u"Sïmple title\n\nSimple bödy describing the commit") + self.create_simple_commit("Sïmple title\n\nSimple bödy describing the commit") output = gitlint(_cwd=self.tmp_git_repo, _tty_in=True, _err_to_out=True) self.assertEqualStdout(output, "") def test_successful_gitconfig(self): - """ Test gitlint when the underlying repo has specific git config set. - In the past, we've had issues with gitlint failing on some of these, so this acts as a regression test. """ + """Test gitlint when the underlying repo has specific git config set. + In the past, we've had issues with gitlint failing on some of these, so this acts as a regression test.""" # Different commentchar (Note: tried setting this to a special unicode char, but git doesn't like that) git("config", "--add", "core.commentchar", "$", _cwd=self.tmp_git_repo) - self.create_simple_commit(u"Sïmple title\n\nSimple bödy describing the commit\n$after commentchar\t ignored") + self.create_simple_commit("Sïmple title\n\nSimple bödy describing the commit\n$after commentchar\t ignored") output = gitlint(_cwd=self.tmp_git_repo, _tty_in=True, _err_to_out=True) self.assertEqualStdout(output, "") def test_successful_merge_commit(self): - # Create branch on master - self.create_simple_commit(u"Cömmit on master\n\nSimple bödy") + # Create branch on main + self.create_simple_commit("Cömmit on main\n\nSimple bödy") # Create test branch, add a commit and determine the commit hash git("checkout", "-b", "test-branch", _cwd=self.tmp_git_repo) git("checkout", "test-branch", _cwd=self.tmp_git_repo) - commit_title = u"Commit on test-brånch with a pretty long title that will cause issues when merging" - self.create_simple_commit(u"{0}\n\nSïmple body".format(commit_title)) + commit_title = "Commit on test-brånch with a pretty long title that will cause issues when merging" + self.create_simple_commit(f"{commit_title}\n\nSïmple body") hash = self.get_last_commit_hash() - # Checkout master and merge the commit + # Checkout main and merge the commit # We explicitly set the title of the merge commit to the title of the previous commit as this or similar # behavior is what many tools do that handle merges (like github, gerrit, etc). - git("checkout", "master", _cwd=self.tmp_git_repo) - git("merge", "--no-ff", "-m", u"Merge '{0}'".format(commit_title), hash, _cwd=self.tmp_git_repo) + git("checkout", "main", _cwd=self.tmp_git_repo) + git("merge", "--no-ff", "-m", f"Merge '{commit_title}'", hash, _cwd=self.tmp_git_repo) # Run gitlint and assert output is empty output = gitlint(_cwd=self.tmp_git_repo, _tty_in=True) @@ -50,23 +48,18 @@ class IntegrationTests(BaseTestCase): # Assert that we do see the error if we disable the ignore-merge-commits option output = gitlint("-c", "general.ignore-merge-commits=false", _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[1]) self.assertEqual(output.exit_code, 1) - self.assertEqualStdout(output, - u"1: T1 Title exceeds max length (90>72): \"Merge '{0}'\"\n".format(commit_title)) + self.assertEqualStdout(output, f"1: T1 Title exceeds max length (90>72): \"Merge '{commit_title}'\"\n") def test_fixup_commit(self): # Create a normal commit and assert that it has a violation - test_filename = self.create_simple_commit(u"Cömmit on WIP master\n\nSimple bödy that is long enough") + test_filename = self.create_simple_commit("Cömmit on WIP main\n\nSimple bödy that is long enough") output = gitlint(_cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[1]) - expected = u"1: T5 Title contains the word 'WIP' (case-insensitive): \"Cömmit on WIP master\"\n" + expected = "1: T5 Title contains the word 'WIP' (case-insensitive): \"Cömmit on WIP main\"\n" self.assertEqualStdout(output, expected) # Make a small modification to the commit and commit it using fixup commit - with io.open(os.path.join(self.tmp_git_repo, test_filename), "a", encoding=DEFAULT_ENCODING) as fh: - # Wanted to write a unicode string, but that's obnoxious if you want to do it across Python 2 and 3. - # https://stackoverflow.com/questions/22392377/ - # error-writing-a-file-with-file-write-in-python-unicodeencodeerror - # So just keeping it simple - ASCII will here - fh.write(u"Appending some stuff\n") + with open(os.path.join(self.tmp_git_repo, test_filename), "a", encoding=FILE_ENCODING) as fh: + fh.write("Appending söme stuff\n") git("add", test_filename, _cwd=self.tmp_git_repo) @@ -79,13 +72,44 @@ class IntegrationTests(BaseTestCase): # Make sure that if we set the ignore-fixup-commits option to false that we do still see the violations output = gitlint("-c", "general.ignore-fixup-commits=false", _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[2]) - expected = u"1: T5 Title contains the word 'WIP' (case-insensitive): \"fixup! Cömmit on WIP master\"\n" + \ - u"3: B6 Body message is missing\n" + expected = ( + "1: T5 Title contains the word 'WIP' (case-insensitive): \"fixup! Cömmit on WIP main\"\n" + "3: B6 Body message is missing\n" + ) + + self.assertEqualStdout(output, expected) + + def test_fixup_amend_commit(self): + # Create a normal commit and assert that it has a violation + test_filename = self.create_simple_commit("Cömmit on WIP main\n\nSimple bödy that is long enough") + output = gitlint(_cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[1]) + expected = "1: T5 Title contains the word 'WIP' (case-insensitive): \"Cömmit on WIP main\"\n" + self.assertEqualStdout(output, expected) + + # Make a small modification to the commit and commit it using fixup=amend commit + with open(os.path.join(self.tmp_git_repo, test_filename), "a", encoding=FILE_ENCODING) as fh: + fh.write("Appending söme stuff\n") + + git("add", test_filename, _cwd=self.tmp_git_repo) + + # We have to use --no-edit to avoid git starting $EDITOR to modify the commit message that is being amended + git("commit", "--no-edit", f"--fixup=amend:{self.get_last_commit_hash()}", _cwd=self.tmp_git_repo) + + # Assert that gitlint does not show an error for the fixup commit + output = gitlint(_cwd=self.tmp_git_repo, _tty_in=True) + # No need to check exit code, the command above throws an exception on > 0 exit codes + self.assertEqualStdout(output, "") + + # Make sure that if we set the ignore-fixup-commits option to false that we do still see the violations + output = gitlint( + "-c", "general.ignore-fixup-amend-commits=false", _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[1] + ) + expected = "1: T5 Title contains the word 'WIP' (case-insensitive): \"amend! Cömmit on WIP main\"\n" self.assertEqualStdout(output, expected) def test_revert_commit(self): - self.create_simple_commit(u"WIP: Cömmit on master.\n\nSimple bödy") + self.create_simple_commit("WIP: Cömmit on main.\n\nSimple bödy") hash = self.get_last_commit_hash() git("revert", hash, _cwd=self.tmp_git_repo) @@ -94,30 +118,31 @@ class IntegrationTests(BaseTestCase): self.assertEqualStdout(output, "") # Assert that we do see the error if we disable the ignore-revert-commits option - output = gitlint("-c", "general.ignore-revert-commits=false", - _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[1]) + output = gitlint( + "-c", "general.ignore-revert-commits=false", _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[1] + ) self.assertEqual(output.exit_code, 1) - expected = u"1: T5 Title contains the word 'WIP' (case-insensitive): \"Revert \"WIP: Cömmit on master.\"\"\n" + expected = '1: T5 Title contains the word \'WIP\' (case-insensitive): "Revert "WIP: Cömmit on main.""\n' self.assertEqualStdout(output, expected) def test_squash_commit(self): # Create a normal commit and assert that it has a violation - test_filename = self.create_simple_commit(u"Cömmit on WIP master\n\nSimple bödy that is long enough") + test_filename = self.create_simple_commit("Cömmit on WIP main\n\nSimple bödy that is long enough") output = gitlint(_cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[1]) - expected = u"1: T5 Title contains the word 'WIP' (case-insensitive): \"Cömmit on WIP master\"\n" + expected = "1: T5 Title contains the word 'WIP' (case-insensitive): \"Cömmit on WIP main\"\n" self.assertEqualStdout(output, expected) # Make a small modification to the commit and commit it using squash commit - with io.open(os.path.join(self.tmp_git_repo, test_filename), "a", encoding=DEFAULT_ENCODING) as fh: + with open(os.path.join(self.tmp_git_repo, test_filename), "a", encoding=FILE_ENCODING) as fh: # Wanted to write a unicode string, but that's obnoxious if you want to do it across Python 2 and 3. # https://stackoverflow.com/questions/22392377/ # error-writing-a-file-with-file-write-in-python-unicodeencodeerror # So just keeping it simple - ASCII will here - fh.write(u"Appending some stuff\n") + fh.write("Appending some stuff\n") git("add", test_filename, _cwd=self.tmp_git_repo) - git("commit", "--squash", self.get_last_commit_hash(), "-m", u"Töo short body", _cwd=self.tmp_git_repo) + git("commit", "--squash", self.get_last_commit_hash(), "-m", "Töo short body", _cwd=self.tmp_git_repo) # Assert that gitlint does not show an error for the fixup commit output = gitlint(_cwd=self.tmp_git_repo, _tty_in=True) @@ -125,47 +150,118 @@ class IntegrationTests(BaseTestCase): self.assertEqualStdout(output, "") # Make sure that if we set the ignore-squash-commits option to false that we do still see the violations - output = gitlint("-c", "general.ignore-squash-commits=false", - _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[2]) - expected = u"1: T5 Title contains the word 'WIP' (case-insensitive): \"squash! Cömmit on WIP master\"\n" + \ - u"3: B5 Body message is too short (14<20): \"Töo short body\"\n" + output = gitlint( + "-c", "general.ignore-squash-commits=false", _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[2] + ) + expected = ( + "1: T5 Title contains the word 'WIP' (case-insensitive): \"squash! Cömmit on WIP main\"\n" + '3: B5 Body message is too short (14<20): "Töo short body"\n' + ) self.assertEqualStdout(output, expected) def test_violations(self): - commit_msg = u"WIP: This ïs a title.\nContent on the sëcond line" + commit_msg = "WIP: This ïs a title.\nContent on the sëcond line" self.create_simple_commit(commit_msg) output = gitlint(_cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[3]) self.assertEqualStdout(output, self.get_expected("test_gitlint/test_violations_1")) def test_msg_filename(self): - tmp_commit_msg_file = self.create_tmpfile(u"WIP: msg-fïlename test.") - output = gitlint("--msg-filename", tmp_commit_msg_file, _tty_in=True, _ok_code=[3]) + tmp_commit_msg_file = self.create_tmpfile("WIP: msg-fïlename test.") + output = gitlint("--msg-filename", tmp_commit_msg_file, _tty_in=True, _cwd=self.tmp_git_repo, _ok_code=[3]) self.assertEqualStdout(output, self.get_expected("test_gitlint/test_msg_filename_1")) def test_msg_filename_no_tty(self): - """ Make sure --msg-filename option also works with no TTY attached """ - tmp_commit_msg_file = self.create_tmpfile(u"WIP: msg-fïlename NO TTY test.") + """Make sure --msg-filename option also works with no TTY attached""" + tmp_commit_msg_file = self.create_tmpfile("WIP: msg-fïlename NO TTY test.") # We need to set _err_to_out explicitly for sh to merge stdout and stderr output in case there's # no TTY attached to STDIN # http://amoffat.github.io/sh/sections/special_arguments.html?highlight=_tty_in#err-to-out # We need to pass some whitespace to _in as sh will otherwise hang, see # https://github.com/amoffat/sh/issues/427 - output = gitlint("--msg-filename", tmp_commit_msg_file, _in=" ", - _tty_in=False, _err_to_out=True, _ok_code=[3]) + output = gitlint( + "--msg-filename", + tmp_commit_msg_file, + _cwd=self.tmp_git_repo, + _in=" ", + _tty_in=False, + _err_to_out=True, + _ok_code=[3], + ) self.assertEqualStdout(output, self.get_expected("test_gitlint/test_msg_filename_no_tty_1")) - def test_git_errors(self): + def test_no_git_name_set(self): + """Ensure we print out a helpful message if user.name is not set""" + tmp_commit_msg_file = self.create_tmpfile("WIP: msg-fïlename NO name test.") + # Name is checked before email so this isn't strictly + # necessary but seems good for consistency. + env = self.create_tmp_git_config("[user]\n email = test-emåil@foo.com\n") + output = gitlint( + "--staged", + "--msg-filename", + tmp_commit_msg_file, + _ok_code=[self.GIT_CONTEXT_ERROR_CODE], + _env=env, + _cwd=self.tmp_git_repo, + ) + expected = "Missing git configuration: please set user.name\n" + self.assertEqualStdout(output, expected) + + def test_no_git_email_set(self): + """Ensure we print out a helpful message if user.email is not set""" + tmp_commit_msg_file = self.create_tmpfile("WIP: msg-fïlename NO email test.") + env = self.create_tmp_git_config("[user]\n name = test åuthor\n") + output = gitlint( + "--staged", + "--msg-filename", + tmp_commit_msg_file, + _ok_code=[self.GIT_CONTEXT_ERROR_CODE], + _env=env, + _cwd=self.tmp_git_repo, + ) + expected = "Missing git configuration: please set user.email\n" + self.assertEqualStdout(output, expected) + + def test_git_empty_repo(self): # Repo has no commits: caused by `git log` empty_git_repo = self.create_tmp_git_repo() output = gitlint(_cwd=empty_git_repo, _tty_in=True, _ok_code=[self.GIT_CONTEXT_ERROR_CODE]) - expected = u"Current branch has no commits. Gitlint requires at least one commit to function.\n" + expected = "Current branch has no commits. Gitlint requires at least one commit to function.\n" + self.assertEqualStdout(output, expected) + + def test_git_empty_repo_staged(self): + """When repo is empty, we can still use gitlint when using --staged flag and piping a message into it""" + empty_git_repo = self.create_tmp_git_repo() + expected = ( + '1: T3 Title has trailing punctuation (.): "WIP: Pïpe test."\n' + "1: T5 Title contains the word 'WIP' (case-insensitive): \"WIP: Pïpe test.\"\n" + "3: B6 Body message is missing\n" + ) + + output = gitlint( + echo("WIP: Pïpe test."), "--staged", _cwd=empty_git_repo, _tty_in=False, _err_to_out=True, _ok_code=[3] + ) self.assertEqualStdout(output, expected) - # Repo has no commits: caused by `git rev-parse` - output = gitlint(echo(u"WIP: Pïpe test."), "--staged", _cwd=empty_git_repo, _tty_in=False, - _err_to_out=True, _ok_code=[self.GIT_CONTEXT_ERROR_CODE]) + def test_commit_binary_file(self): + """When committing a binary file, git shows somewhat different output in diff commands, + this test ensures gitlint deals with that correctly""" + binary_filename = self.create_simple_commit("Sïmple commit", file_contents=bytes([0x48, 0x00, 0x49, 0x00])) + output = gitlint( + "--debug", + _ok_code=[1], + _cwd=self.tmp_git_repo, + ) + + expected_kwargs = self.get_debug_vars_last_commit() + expected_kwargs.update( + { + "changed_files": [binary_filename], + "changed_files_stats": (f"{binary_filename}: None additions, None deletions"), + } + ) + expected = self.get_expected("test_gitlint/test_commit_binary_file_1", expected_kwargs) self.assertEqualStdout(output, expected) diff --git a/qa/test_hooks.py b/qa/test_hooks.py index a41580b..99e76dd 100644 --- a/qa/test_hooks.py +++ b/qa/test_hooks.py @@ -1,22 +1,24 @@ -# -*- coding: utf-8 -*- -# pylint: disable=too-many-function-args,unexpected-keyword-arg import os -from qa.shell import git, gitlint + from qa.base import BaseTestCase +from qa.shell import git, gitlint class HookTests(BaseTestCase): - """ Integration tests for gitlint commitmsg hooks""" - - VIOLATIONS = ['gitlint: checking commit message...\n', - u'1: T3 Title has trailing punctuation (.): "WIP: This ïs a title."\n', - u'1: T5 Title contains the word \'WIP\' (case-insensitive): "WIP: This ïs a title."\n', - u'2: B4 Second line is not empty: "Contënt on the second line"\n', - '3: B6 Body message is missing\n', - '-----------------------------------------------\n', - 'gitlint: \x1b[31mYour commit message contains the above violations.\x1b[0m\n'] + """Integration tests for gitlint commitmsg hooks""" + + VIOLATIONS = [ + "gitlint: checking commit message...\n", + '1: T3 Title has trailing punctuation (.): "WIP: This ïs a title."\n', + "1: T5 Title contains the word 'WIP' (case-insensitive): \"WIP: This ïs a title.\"\n", + '2: B4 Second line is not empty: "Contënt on the second line"\n', + "3: B6 Body message is missing\n", + "-----------------------------------------------\n", + "gitlint: \x1b[31mYour commit message contains violations.\x1b[0m\n", + ] def setUp(self): + super().setUp() self.responses = [] self.response_index = 0 self.githook_output = [] @@ -24,20 +26,23 @@ class HookTests(BaseTestCase): # The '--staged' flag used in the commit-msg hook fetches additional information from the underlying # git repo which means there already needs to be a commit in the repo # (as gitlint --staged doesn't work against empty repos) - self.create_simple_commit(u"Commït Title\n\nCommit Body explaining commit.") + self.create_simple_commit("Commït Title\n\nCommit Body explaining commit.") # install git commit-msg hook and assert output output_installed = gitlint("install-hook", _cwd=self.tmp_git_repo) - expected_installed = u"Successfully installed gitlint commit-msg hook in %s/.git/hooks/commit-msg\n" % \ - self.tmp_git_repo + commit_msg_hook_path = os.path.join(self.tmp_git_repo, ".git", "hooks", "commit-msg") + expected_installed = f"Successfully installed gitlint commit-msg hook in {commit_msg_hook_path}\n" + self.assertEqualStdout(output_installed, expected_installed) def tearDown(self): # uninstall git commit-msg hook and assert output output_uninstalled = gitlint("uninstall-hook", _cwd=self.tmp_git_repo) - expected_uninstalled = u"Successfully uninstalled gitlint commit-msg hook from %s/.git/hooks/commit-msg\n" % \ - self.tmp_git_repo + commit_msg_hook_path = os.path.join(self.tmp_git_repo, ".git", "hooks", "commit-msg") + expected_uninstalled = f"Successfully uninstalled gitlint commit-msg hook from {commit_msg_hook_path}\n" + self.assertEqualStdout(output_uninstalled, expected_uninstalled) + super().tearDown() def _violations(self): # Make a copy of the violations array so that we don't inadvertently edit it in the test (like I did :D) @@ -48,58 +53,78 @@ class HookTests(BaseTestCase): def _interact(self, line, stdin): self.githook_output.append(line) # Answer 'yes' to question to keep violating commit-msg - if "Your commit message contains the above violations" in line: + if "Your commit message contains violations" in line: response = self.responses[self.response_index] - stdin.put("{0}\n".format(response)) + stdin.put(f"{response}\n") self.response_index = (self.response_index + 1) % len(self.responses) + def test_commit_hook_no_violations(self): + test_filename = self.create_simple_commit( + "This ïs a title\n\nBody contënt that should work", out=self._interact, tty_in=True + ) + + short_hash = self.get_last_commit_short_hash() + expected_output = [ + "gitlint: checking commit message...\n", + "gitlint: \x1b[32mOK\x1b[0m (no violations in commit message)\n", + f"[main {short_hash}] This ïs a title\n", + " 1 file changed, 0 insertions(+), 0 deletions(-)\n", + f" create mode 100644 {test_filename}\n", + ] + for output, expected in zip(self.githook_output, expected_output): + self.assertMultiLineEqual(output.replace("\r", ""), expected.replace("\r", "")) + def test_commit_hook_continue(self): self.responses = ["y"] - test_filename = self.create_simple_commit(u"WIP: This ïs a title.\nContënt on the second line", - out=self._interact, tty_in=True) + test_filename = self.create_simple_commit( + "WIP: This ïs a title.\nContënt on the second line", out=self._interact, tty_in=True + ) # Determine short commit-msg hash, needed to determine expected output short_hash = self.get_last_commit_short_hash() expected_output = self._violations() - expected_output += ["Continue with commit anyways (this keeps the current commit message)? " + - "[y(es)/n(no)/e(dit)] " + - u"[master %s] WIP: This ïs a title. Contënt on the second line\n" - % short_hash, - " 1 file changed, 0 insertions(+), 0 deletions(-)\n", - u" create mode 100644 %s\n" % test_filename] - - assert len(self.githook_output) == len(expected_output) + expected_output += [ + "Continue with commit anyways (this keeps the current commit message)? " + "[y(es)/n(no)/e(dit)] " + f"[main {short_hash}] WIP: This ïs a title. Contënt on the second line\n", + " 1 file changed, 0 insertions(+), 0 deletions(-)\n", + f" create mode 100644 {test_filename}\n", + ] + for output, expected in zip(self.githook_output, expected_output): - self.assertMultiLineEqual( - output.replace('\r', ''), - expected.replace('\r', '')) + self.assertMultiLineEqual(output.replace("\r", ""), expected.replace("\r", "")) def test_commit_hook_abort(self): self.responses = ["n"] - test_filename = self.create_simple_commit(u"WIP: This ïs a title.\nContënt on the second line", - out=self._interact, ok_code=1, tty_in=True) + test_filename = self.create_simple_commit( + "WIP: This ïs a title.\nContënt on the second line", out=self._interact, ok_code=1, tty_in=True + ) git("rm", "-f", test_filename, _cwd=self.tmp_git_repo) # Determine short commit-msg hash, needed to determine expected output expected_output = self._violations() - expected_output += ["Continue with commit anyways (this keeps the current commit message)? " + - "[y(es)/n(no)/e(dit)] " + - "Commit aborted.\n", - "Your commit message: \n", - "-----------------------------------------------\n", - u"WIP: This ïs a title.\n", - u"Contënt on the second line\n", - "-----------------------------------------------\n"] + expected_output += [ + "Continue with commit anyways (this keeps the current commit message)? " + "[y(es)/n(no)/e(dit)] " + "Commit aborted.\n", + "Your commit message: \n", + "-----------------------------------------------\n", + "WIP: This ïs a title.\n", + "Contënt on the second line\n", + "-----------------------------------------------\n", + ] - self.assertListEqual(expected_output, self.githook_output) + for output, expected in zip(self.githook_output, expected_output): + self.assertMultiLineEqual(output.replace("\r", ""), expected.replace("\r", "")) def test_commit_hook_edit(self): self.responses = ["e", "y"] env = {"EDITOR": ":"} - test_filename = self.create_simple_commit(u"WIP: This ïs a title.\nContënt on the second line", - out=self._interact, env=env, tty_in=True) + test_filename = self.create_simple_commit( + "WIP: This ïs a title.\nContënt on the second line", out=self._interact, env=env, tty_in=True + ) git("rm", "-f", test_filename, _cwd=self.tmp_git_repo) short_hash = git("rev-parse", "--short", "HEAD", _cwd=self.tmp_git_repo, _tty_in=True).replace("\n", "") @@ -107,23 +132,23 @@ class HookTests(BaseTestCase): # Determine short commit-msg hash, needed to determine expected output expected_output = self._violations() - expected_output += ['Continue with commit anyways (this keeps the current commit message)? ' + - '[y(es)/n(no)/e(dit)] ' + self._violations()[0]] + expected_output += [ + "Continue with commit anyways (this keeps the current commit message)? [y(es)/n(no)/e(dit)] " + + self._violations()[0] + ] expected_output += self._violations()[1:] - expected_output += ['Continue with commit anyways (this keeps the current commit message)? ' + - "[y(es)/n(no)/e(dit)] " + - u"[master %s] WIP: This ïs a title. Contënt on the second line\n" % short_hash, - " 1 file changed, 0 insertions(+), 0 deletions(-)\n", - u" create mode 100644 %s\n" % test_filename] + expected_output += [ + "Continue with commit anyways (this keeps the current commit message)? [y(es)/n(no)/e(dit)] " + f"[main {short_hash}] WIP: This ïs a title. Contënt on the second line\n", + " 1 file changed, 0 insertions(+), 0 deletions(-)\n", + f" create mode 100644 {test_filename}\n", + ] - assert len(self.githook_output) == len(expected_output) for output, expected in zip(self.githook_output, expected_output): - self.assertMultiLineEqual( - output.replace('\r', ''), - expected.replace('\r', '')) + self.assertMultiLineEqual(output.replace("\r", ""), expected.replace("\r", "")) def test_commit_hook_worktree(self): - """ Tests that hook installation and un-installation also work in git worktrees. + """Tests that hook installation and un-installation also work in git worktrees. Test steps: ```sh git init <tmpdir> @@ -135,7 +160,7 @@ class HookTests(BaseTestCase): ``` """ tmp_git_repo = self.create_tmp_git_repo() - self.create_simple_commit(u"Simple title\n\nContënt in the body", git_repo=tmp_git_repo) + self.create_simple_commit("Simple title\n\nContënt in the body", git_repo=tmp_git_repo) worktree_dir = self.generate_temp_path() self.tmp_git_repos.append(worktree_dir) # make sure we clean up the worktree afterwards @@ -144,10 +169,10 @@ class HookTests(BaseTestCase): output_installed = gitlint("install-hook", _cwd=worktree_dir) expected_hook_path = os.path.join(tmp_git_repo, ".git", "hooks", "commit-msg") - expected_msg = "Successfully installed gitlint commit-msg hook in {0}\n".format(expected_hook_path) - self.assertEqual(output_installed, expected_msg) + expected_msg = f"Successfully installed gitlint commit-msg hook in {expected_hook_path}\n" + self.assertEqualStdout(output_installed, expected_msg) output_uninstalled = gitlint("uninstall-hook", _cwd=worktree_dir) expected_hook_path = os.path.join(tmp_git_repo, ".git", "hooks", "commit-msg") - expected_msg = "Successfully uninstalled gitlint commit-msg hook from {0}\n".format(expected_hook_path) - self.assertEqual(output_uninstalled, expected_msg) + expected_msg = f"Successfully uninstalled gitlint commit-msg hook from {expected_hook_path}\n" + self.assertEqualStdout(output_uninstalled, expected_msg) diff --git a/qa/test_named_rules.py b/qa/test_named_rules.py new file mode 100644 index 0000000..e3c6908 --- /dev/null +++ b/qa/test_named_rules.py @@ -0,0 +1,23 @@ +from qa.base import BaseTestCase +from qa.shell import gitlint + + +class NamedRuleTests(BaseTestCase): + """Integration tests for named rules.""" + + def test_named_rule(self): + commit_msg = "WIP: thåt dûr bår\n\nSïmple commit body" + self.create_simple_commit(commit_msg) + config_path = self.get_sample_path("config/named-rules") + output = gitlint("--config", config_path, _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[5]) + self.assertEqualStdout(output, self.get_expected("test_named_rules/test_named_rule_1")) + + def test_named_user_rule(self): + commit_msg = "Normal cömmit title\n\nSïmple commit message body" + self.create_simple_commit(commit_msg) + config_path = self.get_sample_path("config/named-user-rules") + extra_path = self.get_sample_path("user_rules/extra") + output = gitlint( + "--extra-path", extra_path, "--config", config_path, _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[9] + ) + self.assertEqualStdout(output, self.get_expected("test_named_rules/test_named_user_rule_1")) diff --git a/qa/test_rules.py b/qa/test_rules.py new file mode 100644 index 0000000..218a13a --- /dev/null +++ b/qa/test_rules.py @@ -0,0 +1,61 @@ +from qa.base import BaseTestCase +from qa.shell import gitlint + + +class RuleTests(BaseTestCase): + """ + Tests for specific rules that are worth testing as integration tests. + It's not a goal to test every edge case of each rule, that's what the unit tests do. + """ + + def test_match_regex_rules(self): + """ + Test that T7 (title-match-regex) and B8 (body-match-regex) work as expected. + By default, these rules don't do anything, only when setting a custom regex will they run. + """ + + commit_msg = "Thåt dûr bår\n\nSïmple commit message body" + self.create_simple_commit(commit_msg) + + # Assert violations when T7 and B8 regexes don't match + output = gitlint("-c", "T7.regex=foo", "-c", "B8.regex=bar", _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[2]) + self.assertEqualStdout(output, self.get_expected("test_rules/test_match_regex_rules_1")) + + # Assert no violations when T7 and B8 regexes do match + output = gitlint("-c", "T7.regex=^Thåt", "-c", "B8.regex=commit message", _cwd=self.tmp_git_repo, _tty_in=True) + self.assertEqualStdout(output, "") + + def test_ignore_rules(self): + """ + Test that ignore rules work as expected: + ignore-by-title, ignore-by-body, ignore-by-author-name, ignore-body-lines + By default, these rules don't do anything, only when setting a custom regex will they run. + """ + commit_msg = "WIP: Commït Tïtle\n\nSïmple commit\tbody\nAnōther Line \nLåst Line" + self.create_simple_commit(commit_msg) + + # Assert violations when not ignoring anything + output = gitlint(_cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[3]) + self.assertEqualStdout(output, self.get_expected("test_rules/test_ignore_rules_1")) + + # Simple convenience function that passes in common arguments for this test + def invoke_gitlint(*args, **kwargs): + return gitlint( + *args, "-c", "general.regex-style-search=True", **kwargs, _cwd=self.tmp_git_repo, _tty_in=True + ) + + # ignore-by-title + output = invoke_gitlint("-c", "ignore-by-title.regex=Commït") + self.assertEqualStdout(output, "") + + # ignore-by-body + output = invoke_gitlint("-c", "ignore-by-body.regex=Anōther Line") + self.assertEqualStdout(output, "") + + # ignore-by-author-name + output = invoke_gitlint("-c", "ignore-by-author-name.regex=gitlint-test-user") + self.assertEqualStdout(output, "") + + # ignore-body-lines + output = invoke_gitlint("-c", "ignore-body-lines.regex=^Anōther", _ok_code=[2]) + self.assertEqualStdout(output, self.get_expected("test_rules/test_ignore_rules_2")) diff --git a/qa/test_stdin.py b/qa/test_stdin.py index fff636f..04a3de9 100644 --- a/qa/test_stdin.py +++ b/qa/test_stdin.py @@ -1,34 +1,31 @@ -# -*- coding: utf-8 -*- -# pylint: disable=too-many-function-args,unexpected-keyword-arg -import io import subprocess -from qa.shell import echo, gitlint + from qa.base import BaseTestCase -from qa.utils import ustr, DEFAULT_ENCODING +from qa.shell import echo, gitlint +from qa.utils import FILE_ENCODING, TERMINAL_ENCODING class StdInTests(BaseTestCase): - """ Integration tests for various STDIN scenarios for gitlint """ + """Integration tests for various STDIN scenarios for gitlint""" def test_stdin_pipe(self): - """ Test piping input into gitlint. - This is the equivalent of doing: - $ echo "foo" | gitlint + """Test piping input into gitlint. + This is the equivalent of doing: + $ echo "foo" | gitlint """ # NOTE: There is no use in testing this with _tty_in=True, because if you pipe something into a command # there never is a TTY connected to stdin (per definition). We're setting _tty_in=False here to be explicit # but note that this is always true when piping something into a command. - output = gitlint(echo(u"WIP: Pïpe test."), - _cwd=self.tmp_git_repo, _tty_in=False, _err_to_out=True, _ok_code=[3]) + output = gitlint(echo("WIP: Pïpe test."), _cwd=self.tmp_git_repo, _tty_in=False, _err_to_out=True, _ok_code=[3]) self.assertEqualStdout(output, self.get_expected("test_stdin/test_stdin_pipe_1")) def test_stdin_pipe_empty(self): - """ Test the scenario where no TTY is attached an nothing is piped into gitlint. This occurs in - CI runners like Jenkins and Gitlab, see https://github.com/jorisroovers/gitlint/issues/42 for details. - This is the equivalent of doing: - $ echo -n "" | gitlint + """Test the scenario where no TTY is attached and nothing is piped into gitlint. This occurs in + CI runners like Jenkins and Gitlab, see https://github.com/jorisroovers/gitlint/issues/42 for details. + This is the equivalent of doing: + $ echo -n "" | gitlint """ - commit_msg = u"WIP: This ïs a title.\nContent on the sëcond line" + commit_msg = "WIP: This ïs a title.\nContent on the sëcond line" self.create_simple_commit(commit_msg) # We need to set _err_to_out explicitly for sh to merge stdout and stderr output in case there's @@ -36,21 +33,21 @@ class StdInTests(BaseTestCase): # http://amoffat.github.io/sh/sections/special_arguments.html?highlight=_tty_in#err-to-out output = gitlint(echo("-n", ""), _cwd=self.tmp_git_repo, _tty_in=False, _err_to_out=True, _ok_code=[3]) - self.assertEqual(ustr(output), self.get_expected("test_stdin/test_stdin_pipe_empty_1")) + self.assertEqualStdout(output, self.get_expected("test_stdin/test_stdin_pipe_empty_1")) def test_stdin_file(self): - """ Test the scenario where STDIN is a regular file (stat.S_ISREG = True) - This is the equivalent of doing: - $ gitlint < myfile + """Test the scenario where STDIN is a regular file (stat.S_ISREG = True) + This is the equivalent of doing: + $ gitlint < myfile """ - tmp_commit_msg_file = self.create_tmpfile(u"WIP: STDIN ïs a file test.") - - with io.open(tmp_commit_msg_file, encoding=DEFAULT_ENCODING) as file_handle: + tmp_commit_msg_file = self.create_tmpfile("WIP: STDIN ïs a file test.") + with open(tmp_commit_msg_file, encoding=FILE_ENCODING) as file_handle: # noqa: SIM117 # We need to use subprocess.Popen() here instead of sh because when passing a file_handle to sh, it will # deal with reading the file itself instead of passing it on to gitlint as a STDIN. Since we're trying to # test for the condition where stat.S_ISREG == True that won't work for us here. - p = subprocess.Popen(u"gitlint", stdin=file_handle, cwd=self.tmp_git_repo, - stdout=subprocess.PIPE, stderr=subprocess.STDOUT) - output, _ = p.communicate() - self.assertEqual(ustr(output), self.get_expected("test_stdin/test_stdin_file_1")) + with subprocess.Popen( + "gitlint", stdin=file_handle, cwd=self.tmp_git_repo, stdout=subprocess.PIPE, stderr=subprocess.STDOUT + ) as p: + output, _ = p.communicate() + self.assertEqual(output.decode(TERMINAL_ENCODING), self.get_expected("test_stdin/test_stdin_file_1")) diff --git a/qa/test_user_defined.py b/qa/test_user_defined.py index cf7effd..718766c 100644 --- a/qa/test_user_defined.py +++ b/qa/test_user_defined.py @@ -1,38 +1,57 @@ -# -*- coding: utf-8 -*- -# pylint: disable=too-many-function-args,unexpected-keyword-arg -from qa.shell import gitlint from qa.base import BaseTestCase +from qa.shell import gitlint class UserDefinedRuleTests(BaseTestCase): - """ Integration tests for user-defined rules.""" + """Integration tests for user-defined rules.""" - def test_user_defined_rules_examples(self): + def test_user_defined_rules_examples1(self): + """Test the user defined rules in the top-level `examples/` directory""" extra_path = self.get_example_path() - commit_msg = u"WIP: Thi$ is å title\nContent on the second line" + commit_msg = "WIP: Thi$ is å title\nContent on the second line" self.create_simple_commit(commit_msg) output = gitlint("--extra-path", extra_path, _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[5]) self.assertEqualStdout(output, self.get_expected("test_user_defined/test_user_defined_rules_examples_1")) + def test_user_defined_rules_examples2(self): + """Test the user defined rules in the top-level `examples/` directory""" + extra_path = self.get_example_path() + commit_msg = "Release: Thi$ is å title\nContent on the second line\n$This line is ignored \nThis isn't\t\n" + self.create_simple_commit(commit_msg) + output = gitlint("--extra-path", extra_path, _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[5]) + self.assertEqualStdout(output, self.get_expected("test_user_defined/test_user_defined_rules_examples_2")) + def test_user_defined_rules_examples_with_config(self): + """Test the user defined rules in the top-level `examples/` directory""" extra_path = self.get_example_path() - commit_msg = u"WIP: Thi$ is å title\nContent on the second line" + commit_msg = "WIP: Thi$ is å title\nContent on the second line" self.create_simple_commit(commit_msg) - output = gitlint("--extra-path", extra_path, "-c", "body-max-line-count.max-line-count=1", - _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[6]) + output = gitlint( + "--extra-path", + extra_path, + "-c", + "body-max-line-count.max-line-count=1", + _cwd=self.tmp_git_repo, + _tty_in=True, + _ok_code=[6], + ) expected_path = "test_user_defined/test_user_defined_rules_examples_with_config_1" self.assertEqualStdout(output, self.get_expected(expected_path)) def test_user_defined_rules_extra(self): extra_path = self.get_sample_path("user_rules/extra") - commit_msg = u"WIP: Thi$ is å title\nContent on the second line" + commit_msg = "WIP: Thi$ is å title\nContent on the second line" self.create_simple_commit(commit_msg) - output = gitlint("--extra-path", extra_path, _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[5]) - self.assertEqualStdout(output, self.get_expected("test_user_defined/test_user_defined_rules_extra_1")) + output = gitlint("--extra-path", extra_path, _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[9]) + self.assertEqualStdout( + output, + self.get_expected("test_user_defined/test_user_defined_rules_extra_1", {"repo-path": self.tmp_git_repo}), + ) def test_invalid_user_defined_rules(self): extra_path = self.get_sample_path("user_rules/incorrect_linerule") self.create_simple_commit("WIP: test") output = gitlint("--extra-path", extra_path, _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[255]) - self.assertEqualStdout(output, - "Config Error: User-defined rule class 'MyUserLineRule' must have a 'validate' method\n") + self.assertEqualStdout( + output, "Config Error: User-defined rule class 'MyUserLineRule' must have a 'validate' method\n" + ) diff --git a/qa/utils.py b/qa/utils.py index eb9869a..d560d86 100644 --- a/qa/utils.py +++ b/qa/utils.py @@ -1,9 +1,6 @@ -# pylint: disable=bad-option-value,unidiomatic-typecheck,undefined-variable,no-else-return -import platform -import sys -import os - import locale +import os +import platform ######################################################################################################################## # PLATFORM_IS_WINDOWS @@ -23,7 +20,7 @@ PLATFORM_IS_WINDOWS = platform_is_windows() def use_sh_library(): - gitlint_use_sh_lib_env = os.environ.get('GITLINT_QA_USE_SH_LIB', None) + gitlint_use_sh_lib_env = os.environ.get("GITLINT_QA_USE_SH_LIB", None) if gitlint_use_sh_lib_env: return gitlint_use_sh_lib_env == "1" return not PLATFORM_IS_WINDOWS @@ -32,68 +29,20 @@ def use_sh_library(): USE_SH_LIB = use_sh_library() ######################################################################################################################## -# DEFAULT_ENCODING +# TERMINAL_ENCODING +# Encoding for reading gitlint command output def getpreferredencoding(): - """ Modified version of local.getpreferredencoding() that takes into account LC_ALL, LC_CTYPE, LANG env vars - on windows and falls back to UTF-8. """ - default_encoding = locale.getpreferredencoding() or "UTF-8" - - # On Windows, we mimic git/linux by trying to read the LC_ALL, LC_CTYPE, LANG env vars manually - # (on Linux/MacOS the `getpreferredencoding()` call will take care of this). - # We fallback to UTF-8 - if PLATFORM_IS_WINDOWS: - default_encoding = "UTF-8" - for env_var in ["LC_ALL", "LC_CTYPE", "LANG"]: - encoding = os.environ.get(env_var, False) - if encoding: - # Support dotted (C.UTF-8) and non-dotted (C or UTF-8) charsets: - # If encoding contains a dot: split and use second part, otherwise use everything - dot_index = encoding.find(".") - if dot_index != -1: - default_encoding = encoding[dot_index + 1:] - else: - default_encoding = encoding - break - - return default_encoding - - -DEFAULT_ENCODING = getpreferredencoding() + """Use local.getpreferredencoding() or fallback to UTF-8.""" + return locale.getpreferredencoding() or "UTF-8" + + +TERMINAL_ENCODING = getpreferredencoding() -######################################################################################################################## -# Unicode utility functions - - -def ustr(obj): - """ Python 2 and 3 utility method that converts an obj to unicode in python 2 and to a str object in python 3""" - if sys.version_info[0] == 2: - # If we are getting a string, then do an explicit decode - # else, just call the unicode method of the object - if type(obj) in [str, basestring]: # pragma: no cover # noqa - return unicode(obj, DEFAULT_ENCODING) # pragma: no cover # noqa - else: - return unicode(obj) # pragma: no cover # noqa - else: - if type(obj) in [bytes]: - return obj.decode(DEFAULT_ENCODING) - else: - return str(obj) - - -def sstr(obj): - """ Python 2 and 3 utility method that converts an obj to a DEFAULT_ENCODING encoded string in python 2 - and to unicode in python 3. - Especially useful for implementing __str__ methods in python 2: http://stackoverflow.com/a/1307210/381010""" - if sys.version_info[0] == 2: - # For lists in python2, remove unicode string representation characters. - # i.e. ensure lists are printed as ['a', 'b'] and not [u'a', u'b'] - if type(obj) in [list]: - return [sstr(item) for item in obj] # pragma: no cover # noqa - - return unicode(obj).encode(DEFAULT_ENCODING) # pragma: no cover # noqa - else: - return obj # pragma: no cover ######################################################################################################################## +# FILE_ENCODING + +# Encoding for reading/writing files within the tests, this is always UTF-8 +FILE_ENCODING = "UTF-8" |