mirror of
https://github.com/PyCQA/flake8.git
synced 2026-04-14 08:24:46 +00:00
Make a first pass at a git pre-commit hook
This commit is contained in:
parent
9c47fe3c08
commit
8b9b9bbe89
4 changed files with 219 additions and 5 deletions
192
flake8/api/git.py
Normal file
192
flake8/api/git.py
Normal file
|
|
@ -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'),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
|
@ -4,5 +4,7 @@ EXCLUDE = '.svn,CVS,.bzr,.hg,.git,__pycache__,.tox'
|
||||||
IGNORE = 'E121,E123,E126,E226,E24,E704,W503,W504'
|
IGNORE = 'E121,E123,E126,E226,E24,E704,W503,W504'
|
||||||
MAX_LINE_LENGTH = 79
|
MAX_LINE_LENGTH = 79
|
||||||
|
|
||||||
|
TRUTHY_VALUES = set(['true', '1', 't'])
|
||||||
|
|
||||||
# Other consants
|
# Other consants
|
||||||
WHITESPACE = frozenset(' \t')
|
WHITESPACE = frozenset(' \t')
|
||||||
|
|
|
||||||
|
|
@ -41,3 +41,12 @@ class InvalidSyntax(Flake8Exception):
|
||||||
# strerror attribute instead of a message attribute
|
# strerror attribute instead of a message attribute
|
||||||
self.error_message = self.original_exception.strerror
|
self.error_message = self.original_exception.strerror
|
||||||
super(InvalidSyntax, self).__init__(*args, **kwargs)
|
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)
|
||||||
|
|
|
||||||
|
|
@ -271,8 +271,11 @@ class Application(object):
|
||||||
#: :attr:`option_manager`
|
#: :attr:`option_manager`
|
||||||
self.args = None
|
self.args = None
|
||||||
#: The number of errors, warnings, and other messages after running
|
#: The number of errors, warnings, and other messages after running
|
||||||
#: flake8
|
#: flake8 and taking into account ignored errors and lines.
|
||||||
self.result_count = 0
|
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
|
#: Whether the program is processing a diff or not
|
||||||
self.running_against_diff = False
|
self.running_against_diff = False
|
||||||
|
|
@ -406,11 +409,15 @@ class Application(object):
|
||||||
results = self.file_checker_manager.report()
|
results = self.file_checker_manager.report()
|
||||||
self.total_result_count, self.result_count = results
|
self.total_result_count, self.result_count = results
|
||||||
LOG.info('Found a total of %d results and reported %d',
|
LOG.info('Found a total of %d results and reported %d',
|
||||||
self.total_result_count,
|
self.total_result_count, self.result_count)
|
||||||
self.result_count)
|
|
||||||
|
|
||||||
def _run(self, argv):
|
def initialize(self, argv):
|
||||||
# type: (Union[NoneType, List[str]]) -> NoneType
|
# 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.find_plugins()
|
||||||
self.register_plugin_options()
|
self.register_plugin_options()
|
||||||
self.parse_configuration_and_cli(argv)
|
self.parse_configuration_and_cli(argv)
|
||||||
|
|
@ -418,6 +425,10 @@ class Application(object):
|
||||||
self.make_notifier()
|
self.make_notifier()
|
||||||
self.make_guide()
|
self.make_guide()
|
||||||
self.make_file_checker_manager()
|
self.make_file_checker_manager()
|
||||||
|
|
||||||
|
def _run(self, argv):
|
||||||
|
# type: (Union[NoneType, List[str]]) -> NoneType
|
||||||
|
self.initialize(argv)
|
||||||
self.run_checks()
|
self.run_checks()
|
||||||
self.report_errors()
|
self.report_errors()
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue