dotfiles / hg / hooks / whitespace.py

The default branch has multiple heads:

#!/usr/bin/env python
# From:
# http://hgbook.red-bean.com/read/handling-repository-events-with-hooks.html#id403945
#
# Add this file to .hgrc as pretxncommit hook:
# 
#     [hooks]
#     pretxncommit.whitespace = dotfiles/hg/check_whitespace.py

from __future__ import print_function

import os, sys, pwd
import re

from subprocess import check_output

def trailing_whitespace_filter(difflines):
    linenum, header = 0, False

    for line in difflines:
        # Remove the final newline character from each line, or it will
        # be detected as 'trailing whitespace' further on.
        while 1 <= len(line) and line[-1] in ['\n', '\r']:
            line = line[:-1]

        if header:
            # remember the name of the file that this diff affects
            m = re.match(r'(?:---|\+\+\+) ([^\t]+)', line)
            if m and m.group(1) != '/dev/null':
                filename = m.group(1).split('/', 1)[-1].strip()
            if line.startswith('+++ '):
                header = False
            continue

        if line.startswith('diff '):
            header = True
            continue

        # hunk header - save the line number
        m = re.match(r'@@ -\d+,\d+ \+(\d+),', line)
        if m:
            linenum = int(m.group(1))
            continue

        # hunk body - check for an added line with trailing whitespace
        m = re.match(r'^\+.*\s$', line)
        if m:
            yield filename, linenum

        if line and line[0] in ' +':
            linenum += 1

def is_trailing_whitespace_added():
    added = 0
    filenames = []
    for filename, linenum in trailing_whitespace_filter(os.popen('hg export tip')):
        print(
            ('whitespace: {0}:{1}: trailing whitespace added'.format(filename, linenum)),
            file=sys.stderr
        )
        added += 1
        filenames.append(filename)
    return added, filenames


def is_merge():
    parents = check_output(['hg', 'parents', '--template', '{rev}\n']).rstrip().split('\n')
    return 1 < len(parents)


def is_run_as_root():
    return os.getuid() == 0



# pretxncommit
def hook(ui, repo, hooktype, node, **kwargs):
    from pprint import pprint

    trailing_whitespace, filenames = is_trailing_whitespace_added()

    if is_merge():
        # We always allow merges to proceed.
        if trailing_whitespace:
            print("whitespace: Am I my brother's keeper?", file=sys.stderr)
            print("whitespace: Ignoring trailing whitespace introduced during merge", file=sys.stderr)
        else:
            print('whitespace: Merge commit, not enforcing whitespace', file=sys.stderr)
        return 0

    if is_run_as_root():
        print("whitespace: Ignoring whitespace when root, for etckeeper's sake", file=sys.stderr)
        return 0

    if trailing_whitespace:
        # save the commit message so we don't need to retype it, and exit non-zero.
        os.system('hg tip --template "{desc}" > .hg/last-message.txt')
        print('whitespace: Trailing whitespace detected', file=sys.stderr)
        print('whitespace: Commit message saved to .hg/last-message.txt', file=sys.stderr)
        print(
            "whitespace: # sed -i 's, *$,,' {filenames}".format(
                filenames=' '.join(filenames)
            ),
            file=sys.stderr
        )
        return 1