summaryrefslogtreecommitdiff
path: root/flake8
diff options
context:
space:
mode:
authorIan Cordasco <graffatcolmingov@gmail.com>2016-06-13 07:15:14 -0500
committerIan Cordasco <graffatcolmingov@gmail.com>2016-06-13 07:15:14 -0500
commit8b9b9bbe89af7064096312c7fcd9296e41409b6a (patch)
tree049a794272d6734d0f1130ee225e677e93ce7331 /flake8
parent9c47fe3c088c9deff081862ed6514cc8d175b53e (diff)
downloadflake8-8b9b9bbe89af7064096312c7fcd9296e41409b6a.tar.gz
Make a first pass at a git pre-commit hook
Diffstat (limited to 'flake8')
-rw-r--r--flake8/api/git.py192
-rw-r--r--flake8/defaults.py2
-rw-r--r--flake8/exceptions.py9
-rw-r--r--flake8/main/cli.py21
4 files changed, 219 insertions, 5 deletions
diff --git a/flake8/api/git.py b/flake8/api/git.py
new file mode 100644
index 0000000..087e31b
--- /dev/null
+++ b/flake8/api/git.py
@@ -0,0 +1,192 @@
+"""Module containing the main git hook interface and helpers.
+
+.. autofunction:: hook
+
+"""
+import contextlib
+import os
+import shutil
+import stat
+import subprocess
+import tempdir
+
+from flake8 import defaults
+from flake8 import exceptions
+from flake8.main import cli
+
+__all__ = ('hook', 'install')
+
+
+def hook(lazy=False, strict=False):
+ """Execute Flake8 on the files in git's index.
+
+ Determine which files are about to be committed and run Flake8 over them
+ to check for violations.
+
+ :param bool lazy:
+ Find files not added to the index prior to committing. This is useful
+ if you frequently use ``git commit -a`` for example. This defaults to
+ False since it will otherwise include files not in the index.
+ :param bool strict:
+ If True, return the total number of errors/violations found by Flake8.
+ This will cause the hook to fail.
+ :returns:
+ Total number of errors found during the run.
+ :rtype:
+ int
+ """
+ app = cli.Application()
+ with make_temporary_directory() as tempdir:
+ filepaths = list(copy_indexed_files_to(tempdir, lazy))
+ app.initialize(filepaths)
+ app.run_checks()
+
+ app.report_errors()
+ if strict:
+ return app.result_count
+ return 0
+
+
+def install():
+ """Install the git hook script.
+
+ This searches for the ``.git`` directory and will install an executable
+ pre-commit python script in the hooks sub-directory if one does not
+ already exist.
+
+ :returns:
+ True if successful, False if the git directory doesn't exist.
+ :rtype:
+ bool
+ :raises:
+ flake8.exceptions.GitHookAlreadyExists
+ """
+ git_directory = find_git_directory()
+ if git_directory is None or not os.path.exists(git_directory):
+ return False
+
+ hooks_directory = os.path.join(git_directory, 'hooks')
+ if not os.path.exists(hooks_directory):
+ os.mkdir(hooks_directory)
+
+ pre_commit_file = os.path.join(hooks_directory, 'hooks', 'pre-commit')
+ if os.path.exists(pre_commit_file):
+ raise exceptions.GitHookAlreadyExists(
+ 'File already exists',
+ path=pre_commit_file,
+ )
+
+ with open(pre_commit_file, 'w') as fd:
+ fd.write(_HOOK_TEMPLATE)
+
+ # NOTE(sigmavirus24): The following sets:
+ # - read, write, and execute permissions for the owner
+ # - read permissions for people in the group
+ # - read permissions for other people
+ # The owner needs the file to be readable, writable, and executable
+ # so that git can actually execute it as a hook.
+ pre_commit_permissions = stat.S_IRWXU | stat.S_IRGRP | stat.IROTH
+ os.chmod(pre_commit_file, pre_commit_permissions)
+ return True
+
+
+def find_git_directory():
+ rev_parse = piped_process(['git', 'rev-parse', '--git-dir'])
+
+ (stdout, _) = rev_parse.communicate()
+ stdout = to_text(stdout)
+
+ if rev_parse.returncode == 0:
+ return stdout.strip()
+ return None
+
+
+def copy_indexed_files_to(temporary_directory, lazy):
+ modified_files = find_modified_files(lazy)
+ for filename in modified_files:
+ contents = get_staged_contents_from(filename)
+ yield copy_file_to(temporary_directory, filename, contents)
+
+
+def copy_file_to(destination_directory, filepath, contents):
+ directory, filename = os.path.split(os.path.abspath(filepath))
+ temporary_directory = make_temporary_directory_from(destination_directory,
+ directory)
+ temporary_filepath = os.path.join(temporary_directory, filename)
+ with open(temporary_filepath, 'wb') as fd:
+ fd.write(contents)
+ return temporary_filepath
+
+
+def make_temporary_directory_from(destination, directory):
+ prefix = os.path.commonprefix([directory, destination])
+ common_directory_path = os.path.relpath(directory, start=prefix)
+ return os.path.join(destination, common_directory_path)
+
+
+def find_modified_files(lazy):
+ diff_index = piped_process(
+ ['git', 'diff-index', '--cached', '--name-only',
+ '--diff-filter=ACMRTUXB', 'HEAD'],
+ )
+
+ (stdout, _) = diff_index.communicate()
+ stdout = to_text(stdout)
+ return stdout.splitlines()
+
+
+def get_staged_contents_from(filename):
+ git_show = piped_process(['git', 'show', ':{0}'.format(filename)])
+ (stdout, _) = git_show.communicate()
+ return stdout
+
+
+@contextlib.contextmanager
+def make_temporary_directory():
+ temporary_directory = tempdir.mkdtemp()
+ yield temporary_directory
+ shutil.rmtree(temporary_directory, ignore_errors=True)
+
+
+def to_text(string):
+ """Ensure that the string is text."""
+ if callable(getattr(string, 'decode', None)):
+ return string.decode('utf-8')
+ return string
+
+
+def piped_process(command):
+ return subprocess.Popen(
+ command,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ )
+
+
+def git_config_for(parameter):
+ config = piped_process(['git', 'config', '--get', '--bool', parameter])
+ (stdout, ) = config.communicate()
+ return to_text(stdout)
+
+
+def config_for(parameter):
+ environment_variable = 'flake8_{0}'.format(parameter).upper()
+ git_variable = 'flake8.{0}'.format(parameter)
+ value = os.environ.get(environment_variable, git_config_for(git_variable))
+ return value.lower() in defaults.TRUTHY_VALUES
+
+
+_HOOK_TEMPLATE = """#!/usr/bin/env python
+import os
+import sys
+
+from flake8.api import git
+
+if __name__ == '__main__':
+ sys.exit(
+ git.hook(
+ strict=git.config_for('strict'),
+ lazy=git.config_for('lazy'),
+ )
+ )
+"""
diff --git a/flake8/defaults.py b/flake8/defaults.py
index d484f4c..2007eed 100644
--- a/flake8/defaults.py
+++ b/flake8/defaults.py
@@ -4,5 +4,7 @@ EXCLUDE = '.svn,CVS,.bzr,.hg,.git,__pycache__,.tox'
IGNORE = 'E121,E123,E126,E226,E24,E704,W503,W504'
MAX_LINE_LENGTH = 79
+TRUTHY_VALUES = set(['true', '1', 't'])
+
# Other consants
WHITESPACE = frozenset(' \t')
diff --git a/flake8/exceptions.py b/flake8/exceptions.py
index 18ee90c..c5f3c5a 100644
--- a/flake8/exceptions.py
+++ b/flake8/exceptions.py
@@ -41,3 +41,12 @@ class InvalidSyntax(Flake8Exception):
# strerror attribute instead of a message attribute
self.error_message = self.original_exception.strerror
super(InvalidSyntax, self).__init__(*args, **kwargs)
+
+
+class GitHookAlreadyExists(Flake8Exception):
+ """Exception raised when the git pre-commit hook file already exists."""
+
+ def __init__(self, *args, **kwargs):
+ """Initialize the path attribute."""
+ self.path = kwargs.pop('path')
+ super(GitHookAlreadyExists, self).__init__(*args, **kwargs)
diff --git a/flake8/main/cli.py b/flake8/main/cli.py
index 93c4a14..23135c4 100644
--- a/flake8/main/cli.py
+++ b/flake8/main/cli.py
@@ -271,8 +271,11 @@ class Application(object):
#: :attr:`option_manager`
self.args = None
#: The number of errors, warnings, and other messages after running
- #: flake8
+ #: flake8 and taking into account ignored errors and lines.
self.result_count = 0
+ #: The total number of errors before accounting for ignored errors and
+ #: lines.
+ self.total_result_count = 0
#: Whether the program is processing a diff or not
self.running_against_diff = False
@@ -406,11 +409,15 @@ class Application(object):
results = self.file_checker_manager.report()
self.total_result_count, self.result_count = results
LOG.info('Found a total of %d results and reported %d',
- self.total_result_count,
- self.result_count)
+ self.total_result_count, self.result_count)
- def _run(self, argv):
- # type: (Union[NoneType, List[str]]) -> NoneType
+ def initialize(self, argv):
+ # type: () -> NoneType
+ """Initialize the application to be run.
+
+ This finds the plugins, registers their options, and parses the
+ command-line arguments.
+ """
self.find_plugins()
self.register_plugin_options()
self.parse_configuration_and_cli(argv)
@@ -418,6 +425,10 @@ class Application(object):
self.make_notifier()
self.make_guide()
self.make_file_checker_manager()
+
+ def _run(self, argv):
+ # type: (Union[NoneType, List[str]]) -> NoneType
+ self.initialize(argv)
self.run_checks()
self.report_errors()