Re: ChangeLogs in commit messages
Gary Benson <[email protected]>
| Newsgroups | gmane.comp.gdb.devel |
|---|---|
| Message-ID | <[email protected]> |
Joel Brobecker wrote:
> > Does anybody have any experience writing such checks? Or, does
> > anybody know of any project that already uses such checks? I can
> > look into doing it myself (it's a pre-receive hook, right?)
>
> I have loads of experience writing git hooks, but none with
> this repository's implementation.
>
> I would typically adjust the "update" hook for that, but it looks
> like the "pre-receive" hook would also work.
I've put together a quick pre-receive hook (inlined below). Each
received commit on the "master" branch that touches the "gdb"
subdirectory gets its message checked. The check itself is fairly
cursory: it splits the message using the "YYYY-MM-DD NAME <EMAIL>"
headers, checks each is preceeded by a path starting with "gdb/" and
ending with "/", and checks each is followed by more "NAME <EMAIL>"
lines, blank lines, or lines starting with tab. I don't know how
comprehensive we want to be here as the message should already have
been checked over by the reviewer.
I've never done anything server-side with git before, so there may
well be things I'm missing here. I was mainly experimenting to see
how difficult this all was :)
Cheers,
Gary
--
#!/usr/bin/env python
import re
import subprocess
import sys
GIT = "/usr/bin/git"
SHA1HASH = re.compile(r"^[0-9a-f]{40}$", re.IGNORECASE)
name_mail = r".* <[^@]+@[^>]+>$"
CL_HDR_A = re.compile(r"^\d{4}-\d{2}-\d{2} " + name_mail)
CL_HDR_B = re.compile(r"^\s+" + name_mail)
class MessageChecker:
def __init__(self, rev):
self.rev = rev
def check(self, condition, msg):
if not condition:
print "error: %s: bad commit message" % self.rev
print "error: %s" % msg
sys.exit(1)
def check_message(self, lines):
self.check(len(lines) > 2, "message is too short")
self.check(lines[1] == "", "second line should be blank")
splits = [index
for index in xrange(2, len(lines))
if CL_HDR_A.match(lines[index]) is not None]
self.check(splits, "no ChangeLog entries found")
for start, limit in zip(splits, splits[1:] + [0]):
start -= 1
limit -= 1
self.check_changelog(lines[start:limit])
def check_changelog(self, lines):
self.check(len(lines) > 2, "ChangeLog entry is too short")
path = lines.pop(0)
self.check(path.startswith("gdb/") and path.endswith("/"),
"each ChangeLog entry should be preceeded by its path")
lines.pop(0) # lines[1] has already been checked
while lines and CL_HDR_B.match(lines[0]) is not None:
lines.pop(0)
self.check(lines, "only ChangeLog headers found (no data)")
for line in lines:
self.check(not line or line.startswith("\t"),
"bad ChangeLog line %s" % repr(line))
def check_hash(hash):
assert SHA1HASH.match(hash) is not None
def git(*command):
fp = subprocess.Popen((GIT,) + command, stdout=subprocess.PIPE)
output = fp.communicate()[0]
if fp.returncode:
sys.exit(1)
return output
def git_rev_list(oldrev, newrev):
check_hash(oldrev)
check_hash(newrev)
return git("rev-list", oldrev + ".." + newrev)
def git_diff_tree(rev):
check_hash(rev)
return git("diff-tree", rev)
def git_cat_file_commit(rev):
check_hash(rev)
return git("cat-file", "commit", rev)
def commit_touches_gdb(rev):
lines = git_diff_tree(rev).rstrip().split("\n")
check = lines.pop(0)
assert check == rev
for line in lines:
if line.split()[-1] == "gdb":
return True
return False
def check_commit(rev):
lines = git_cat_file_commit(rev).rstrip().split("\n")
while lines and lines[0]:
lines.pop(0)
assert lines
lines.pop(0)
MessageChecker(rev).check_message(lines)
def process_pack(oldrev, newrev, refname):
if refname == "refs/heads/master":
revs = git_rev_list(oldrev, newrev).rstrip().split("\n")
revs.reverse()
for rev in revs:
if commit_touches_gdb(rev):
check_commit(rev)
print "%s: ok" % rev
def main():
for line in sys.stdin.xreadlines():
bits = line.rstrip().split()
assert len(bits) == 3
process_pack(*bits)
if __name__ == "__main__":
main()