|
|
#!/usr/bin/env python
|
|
|
#
|
|
|
# Copyright 2014 Matt Mackall <mpm@selenic.com>
|
|
|
#
|
|
|
# A tool/hook to run basic sanity checks on commits/patches for
|
|
|
# submission to Mercurial. Install by adding the following to your
|
|
|
# .hg/hgrc:
|
|
|
#
|
|
|
# [hooks]
|
|
|
# pretxncommit = contrib/check-commit
|
|
|
#
|
|
|
# The hook can be temporarily bypassed with:
|
|
|
#
|
|
|
# $ BYPASS= hg commit
|
|
|
#
|
|
|
# See also: https://mercurial-scm.org/wiki/ContributingChanges
|
|
|
|
|
|
import re, sys, os
|
|
|
|
|
|
commitheader = r"^(?:# [^\n]*\n)*"
|
|
|
afterheader = commitheader + r"(?!#)"
|
|
|
beforepatch = afterheader + r"(?!\n(?!@@))"
|
|
|
|
|
|
errors = [
|
|
|
(beforepatch + r".*[(]bc[)]", "(BC) needs to be uppercase"),
|
|
|
(beforepatch + r".*[(]issue \d\d\d",
|
|
|
"no space allowed between issue and number"),
|
|
|
(beforepatch + r".*[(]bug(\d|\s)", "use (issueDDDD) instead of bug"),
|
|
|
(commitheader + r"# User [^@\n]+\n", "username is not an email address"),
|
|
|
(commitheader + r"(?!merge with )[^#]\S+[^:] ",
|
|
|
"summary line doesn't start with 'topic: '"),
|
|
|
(afterheader + r"[A-Z][a-z]\S+", "don't capitalize summary lines"),
|
|
|
(afterheader + r"[^\n]*: *[A-Z][a-z]\S+", "don't capitalize summary lines"),
|
|
|
(afterheader + r"\S*[^A-Za-z0-9-]\S*: ",
|
|
|
"summary keyword should be most user-relevant one-word command or topic"),
|
|
|
(afterheader + r".*\.\s*\n", "don't add trailing period on summary line"),
|
|
|
(afterheader + r".{79,}", "summary line too long (limit is 78)"),
|
|
|
(r"\n\+\n( |\+)\n", "adds double empty line"),
|
|
|
(r"\n \n\+\n", "adds double empty line"),
|
|
|
(r"\n\+[ \t]+def [a-z]+_[a-z]", "adds a function with foo_bar naming"),
|
|
|
]
|
|
|
|
|
|
word = re.compile('\S')
|
|
|
def nonempty(first, second):
|
|
|
if word.search(first):
|
|
|
return first
|
|
|
return second
|
|
|
|
|
|
def checkcommit(commit, node=None):
|
|
|
exitcode = 0
|
|
|
printed = node is None
|
|
|
hits = []
|
|
|
for exp, msg in errors:
|
|
|
for m in re.finditer(exp, commit):
|
|
|
end = m.end()
|
|
|
trailing = re.search(r'(\\n)+$', exp)
|
|
|
if trailing:
|
|
|
end -= len(trailing.group()) / 2
|
|
|
hits.append((end, exp, msg))
|
|
|
if hits:
|
|
|
hits.sort()
|
|
|
pos = 0
|
|
|
last = ''
|
|
|
for n, l in enumerate(commit.splitlines(True)):
|
|
|
pos += len(l)
|
|
|
while len(hits):
|
|
|
end, exp, msg = hits[0]
|
|
|
if pos < end:
|
|
|
break
|
|
|
if not printed:
|
|
|
printed = True
|
|
|
print "node: %s" % node
|
|
|
print "%d: %s" % (n, msg)
|
|
|
print " %s" % nonempty(l, last)[:-1]
|
|
|
if "BYPASS" not in os.environ:
|
|
|
exitcode = 1
|
|
|
del hits[0]
|
|
|
last = nonempty(l, last)
|
|
|
|
|
|
return exitcode
|
|
|
|
|
|
def readcommit(node):
|
|
|
return os.popen("hg export %s" % node).read()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
exitcode = 0
|
|
|
node = os.environ.get("HG_NODE")
|
|
|
|
|
|
if node:
|
|
|
commit = readcommit(node)
|
|
|
exitcode = checkcommit(commit)
|
|
|
elif sys.argv[1:]:
|
|
|
for node in sys.argv[1:]:
|
|
|
exitcode |= checkcommit(readcommit(node), node)
|
|
|
else:
|
|
|
commit = sys.stdin.read()
|
|
|
exitcode = checkcommit(commit)
|
|
|
sys.exit(exitcode)
|
|
|
|