Commit d1b77726 authored by David H's avatar David H

Got mercurial hook working, with improvements

This commit fixes a problem that caused "flake8 --install-hook mercurial" to
die with this message:

    Could not find the mercurial directory

Also fixes two usages of configparser (can't supply a non-text value when
calling .set(), and need to use .getboolean() to convert the value to a

This commit converts the hook itself from a function hook to a script hook.
This enables the hook to run even when flake8 is installed in a virtual
environment and Mercurial is installed globally (a common case.) This also
makes it possible for flake8 to be installed for Python 3; Mercurial is
Python 2.x only.

Also convert the hook event from "commit" to "pretxncommit" so that strict
mode would actually work. A regular commit hook runs only after the commit
is completed; a pretxncommit hook will cause the transaction to be rolled
back if it exits with a non-zero code.

Finally, put extensions on the hook names: pretxncommit.flake8 and
qrefresh.flake8. This allows the flake8 hook to coexist with other hooks the
user might have put in place.

I tested flake8 mercurial hook installation and execution on both Python 2.7
and Python 3.5. I did *not* test the qrefresh.flake8 hook. I could not find
documentation on qrefresh hook event; I presume it has something to do with
the Mercurial MQ extension, which I have never used. Probably this commit
does not make it any worse than it was before.

For more info see:

Closes #283
parent 348722d7
Pipeline #5446279 passed with stages
in 3 minutes and 24 seconds
"""Module containing the main mecurial hook interface and helpers.
.. autofunction:: hook
.. autofunction:: install
import configparser
import os
import subprocess
import sys
from flake8 import exceptions as exc
__all__ = ('hook', 'install')
__all__ = ('install',)
def hook(ui, repo, **kwargs):
"""Execute Flake8 on the repository provided by Mercurial.
def main():
"""Execute Flake8 on commit to Mercurial repository.
To understand the parameters read more of the Mercurial documentation
around Hooks:
Because Mercurial is Python 2.x only, and Flake8 can run under
Python 3, this hook is installed as a "script hook", not as a Python
function. Mercurial will run this module using "python -m", with the
current directory set to the repository root.
We avoid using the ``ui`` attribute because it can cause issues with
the GPL license tha Mercurial is under. We don't import it, but we
avoid using it all the same.
For more information, see the Mercurial documentation on Hooks:
from flake8.main import application
hgrc = find_hgrc(create_if_missing=False)
hgrc = find_hgrc(create_if_missing=False, hg_directory=os.getcwd())
if hgrc is None:
print('Cannot locate your root mercurial repository.')
print('Cannot locate your mercurial repository .hg/hgrc file.')
raise SystemExit(True)
hgconfig = configparser_for(hgrc)
strict = hgconfig.get('flake8', 'strict', fallback=True)
strict = hgconfig.getboolean('flake8', 'strict', fallback=True)
filenames = list(get_filenames_from(repo, kwargs))
filenames = list(get_changed_or_added_py_file_names())
if not filenames:
return 0
app = application.Application()
......@@ -40,8 +43,8 @@ def hook(ui, repo, **kwargs):
if strict:
return app.result_count
if strict and app.result_count:
return 1
return 0
......@@ -56,26 +59,27 @@ def install():
if not hgconfig.has_section('hooks'):
if hgconfig.has_option('hooks', 'commit'):
if hgconfig.has_option('hooks', 'pretxncommit.flake8'):
raise exc.MercurialCommitHookAlreadyExists(
value=hgconfig.get('hooks', 'commit'),
if hgconfig.has_option('hooks', 'qrefresh'):
if hgconfig.has_option('hooks', 'qrefresh.flake8'):
raise exc.MercurialQRefreshHookAlreadyExists(
value=hgconfig.get('hooks', 'qrefresh'),
hgconfig.set('hooks', 'commit', 'python:flake8.main.mercurial.hook')
hgconfig.set('hooks', 'qrefresh', 'python:flake8.main.mercurial.hook')
hook_script = subprocess.list2cmdline([sys.executable, '-m', __name__])
hgconfig.set('hooks', 'pretxncommit.flake8', hook_script)
hgconfig.set('hooks', 'qrefresh.flake8', hook_script)
if not hgconfig.has_section('flake8'):
if not hgconfig.has_option('flake8', 'strict'):
hgconfig.set('flake8', 'strict', False)
hgconfig.set('flake8', 'strict', 'false')
with open(hgrc, 'w') as fd:
......@@ -83,32 +87,38 @@ def install():
return True
def get_filenames_from(repository, kwargs):
seen_filenames = set()
node = kwargs['node']
for revision in range(repository[node], len(repository)):
for filename in repository[revision].files():
full_filename = os.path.join(repository.root, filename)
have_seen_filename = full_filename in seen_filenames
filename_does_not_exist = not os.path.exists(full_filename)
if have_seen_filename or filename_does_not_exist:
if full_filename.endswith('.py'):
yield full_filename
def find_hgrc(create_if_missing=False):
root = subprocess.Popen(
['hg', 'root'],
def get_changed_or_added_py_file_names():
proc = subprocess.Popen(
['hg', 'st'],
(hg_st, _) = proc.communicate()
if callable(getattr(hg_st, 'decode', None)):
hg_st = hg_st.decode('utf-8')
for line in hg_st.split('\n'):
line = line.strip()
if len(line) < 2:
status, filename = line[0], line[2:]
if status not in ('M', 'A'):
if filename.endswith('.py'):
yield filename
def find_hgrc(create_if_missing=False, hg_directory=None):
if hg_directory is None:
root = subprocess.Popen(
['hg', 'root'],
(hg_directory, _) = root.communicate()
if callable(getattr(hg_directory, 'decode', None)):
hg_directory = hg_directory.decode('utf-8')
(hg_directory, _) = root.communicate()
if callable(getattr(hg_directory, 'decode', None)):
hg_directory = hg_directory.decode('utf-8')
hg_directory = hg_directory.strip()
if not os.path.isdir(hg_directory):
return None
......@@ -129,3 +139,7 @@ def configparser_for(path):
parser = configparser.ConfigParser(interpolation=None)
return parser
if __name__ == '__main__':
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment