check-commit
98 lines
| 3.1 KiB
| text/plain
|
TextLexer
/ contrib / check-commit
Matt Mackall
|
r22043 | #!/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 | ||||
# | ||||
Matt Mackall
|
r26421 | # See also: https://mercurial-scm.org/wiki/ContributingChanges | ||
Matt Mackall
|
r22043 | |||
import re, sys, os | ||||
timeless
|
r27782 | commitheader = r"^(?:# [^\n]*\n)*" | ||
afterheader = commitheader + r"(?!#)" | ||||
beforepatch = afterheader + r"(?!\n(?!@@))" | ||||
Matt Mackall
|
r22043 | errors = [ | ||
timeless
|
r27782 | (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+[^:] ", | ||||
Matt Mackall
|
r22043 | "summary line doesn't start with 'topic: '"), | ||
timeless
|
r27782 | (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*: ", | ||||
Matt Mackall
|
r27692 | "summary keyword should be most user-relevant one-word command or topic"), | ||
timeless
|
r27782 | (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"), | ||||
Matt Mackall
|
r22043 | ] | ||
timeless
|
r27782 | word = re.compile('\S') | ||
def nonempty(first, second): | ||||
if word.search(first): | ||||
return first | ||||
return second | ||||
timeless
|
r27781 | def checkcommit(commit, node = None): | ||
timeless
|
r27780 | exitcode = 0 | ||
timeless
|
r27781 | printed = node is None | ||
timeless
|
r27783 | hits = [] | ||
timeless
|
r27780 | for exp, msg in errors: | ||
timeless
|
r27782 | m = re.search(exp, commit) | ||
timeless
|
r27780 | if m: | ||
timeless
|
r27782 | end = m.end() | ||
trailing = re.search(r'(\\n)+$', exp) | ||||
if trailing: | ||||
end -= len(trailing.group()) / 2 | ||||
timeless
|
r27783 | 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] | ||||
timeless
|
r27782 | if pos < end: | ||
timeless
|
r27780 | break | ||
timeless
|
r27783 | 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) | ||||
timeless
|
r27780 | return exitcode | ||
Matt Mackall
|
r22043 | |||
timeless
|
r27780 | def readcommit(node): | ||
return os.popen("hg export %s" % node).read() | ||||
if __name__ == "__main__": | ||||
timeless
|
r27781 | exitcode = 0 | ||
timeless
|
r27780 | node = os.environ.get("HG_NODE") | ||
Matt Mackall
|
r22043 | |||
timeless
|
r27780 | if node: | ||
commit = readcommit(node) | ||||
timeless
|
r27781 | exitcode = checkcommit(commit) | ||
elif sys.argv[1:]: | ||||
for node in sys.argv[1:]: | ||||
exitcode |= checkcommit(readcommit(node), node) | ||||
timeless
|
r27780 | else: | ||
commit = sys.stdin.read() | ||||
timeless
|
r27781 | exitcode = checkcommit(commit) | ||
timeless
|
r27780 | sys.exit(exitcode) | ||