I wanted to share my progress so far with using cdm bits on the gate where they can be used for policy enforcement. I've concentrated on the few that are necessary to prevent people from making a mess of the gate. In my mind this includes tagchk, branchchk, and comchk (with caveat). My team requires the ability to "lock" the gate to prevent pushes at certain times during the week or the release cycle, though we don't have a need to bar pulls. Let's consider this my first draft.
I want a single account to own multiple gates, as I intend to handle branches via clones. This means I need finer granularity than .ssh/authorized_keys on the gate. I've added an "allowpush" file to each gate for this, and I've extended hg-ssh slightly to pass a username and repo name in a place where my hooks can find them. I'll post my new version of hg-ssh in a separate email. For tagchk, we need to make it possible for our "administrator" to push a new tag. I'd even like to make it somewhat easy, given that we intend that this should happen at least once per week per gate. So I've added an "allowtagpush" file to each gate for this. This means the tagchk is no longer black-and-white; now it depends on who you are. The cstyle check is perhaps specific to my own team's project. We know which files are cstyle-clean, and it seems reasonable, and easy, for the gate to prevent regressions in this area. In the past, I've grumbled on this list about enforcement of the summary line of a cset, and I've shown at least one example from onnv where someone didn't use wx or chose to ignore it. It ranks high on my peeve list. It's the reason I began working on these hooks. I've added a hook for comchk but I've limited it to the summary line and made it skip the boilerplate message applied by the hg tag command. In my gate repo I currently have the following in the <repo>/.hg/hgrc file: ----cut--- [hooks] prechangegroup.A = python:hgext.cdmhooks.cdmhook_allowpush #prechangegroup.D = env | egrep '^HG_'; exit 1 #prechangegroup.M = env | egrep '^HG_|CALL'; exit 1 pretxnchangegroup.A_branchchk = python:hgext.cdmhooks.cdmhook_branchchk pretxnchangegroup.B_tagchk = python:hgext.cdmhooks.cdmhook_tagchk pretxnchangegroup.C_comchk = python:hgext.cdmhooks.cdmhook_comchk #pretxnchangegroup.D_cstyle = python:hgext.cdmhooks.cdmhook_cstyle # It can take a long time to get through all the hooks, so before we # let this go in, check allowpush again. pretxnchangegroup.X = python:hgext.cdmhooks.cdmhook_allowpush pretxnchangegroup.Y_bad = exit 1 ---cut--- I'd appreciate any time you can apply to a review of this code. My hope is that I can produce a solution that is usable by onnv as well. Dean Index: onbld/hgext/cdmhooks.py =================================================================== --- /dev/null 1970-01-01 00:00:00.000000000 +0000 +++ onbld/hgext/cdmhooks.py 2008-04-16 12:10:59.708954000 -0500 @@ -0,0 +1,199 @@ +from mercurial import version, util + +import sys, os, pwd, ConfigParser, re +from fnmatch import fnmatch +from mercurial import cmdutil, changegroup, patch, node, commands +from cStringIO import StringIO + +from onbld.Scm.WorkSpace import WorkSpace, ActiveEntry +from onbld.Checks import Cddl, Comments, Copyright, CStyle, HdrChk +from onbld.Checks import JStyle, Keywords, Rti, onSWAN + +from cdm import cdm_branchchk, cdm_tagchk + +def cdmhook_branchchk(ui, repo, **opts): + '''check if multiple heads (or branches) are present''' + + return cdm_branchchk(ui, repo, None) + + +def cdmhook_comchk(ui, repo, **opts): + '''check checkin comments for active files + + Check that checkin comments conform to O/N rules, however at this + point we just verify the summary line. If users really wanted to + include more text in the description, then let them do that.''' + + print "node %s" % opts['node'] + a = repo.changelog.count() - 1 + print "cnt %s" % a + + revs = cmdutil.revrange(repo, (opts['node'], 'tip')) + print "R1 %s" % min(revs) + print "R2 %s" % max(revs) + + tagre = re.compile(r'^Added tag .* for changeset ') + + ret = 0 + for rev in revs: + cs = repo.changelog.node(rev) + print "node %d:%s" % (rev, node.short(cs)) + changes = repo.changelog.read(cs) + print "files: %s" % " ".join(changes[3]) + description = changes[4].strip() + summary = [] + summary.append(description.splitlines()[0]) + print "%s" % summary[0] + if not tagre.match(summary[0]): + ret |= Comments.comchk(summary, check_db=True, output=ui) + + return ret + + +def cdmhook_tagchk(ui, repo, **opts): + '''check if .hgtags is active and issue warning + + Tag sharing among repositories is restricted to administrators''' + + def allow_tag_push(): + '''Is this user allowed to push tags?''' + + allowtagfile = "".join([os.environ['CALLED_REPO'],"-allowtagpush"]) + print "allowtagfile(%s)" % allowtagfile + + try: + fh = open(allowtagfile, 'r') + except: + return 0 + + at_user = {} + for line in fh.readlines(): + at_user[line.strip()] = 1 + fh.close() + if os.environ['CALLING_USER'] in at_user: + return 1 + return 0 + + + ui.write('Checking for new tags:\n') + + print "node %s" % opts['node'] + a = repo.changelog.count() - 1 + print "cnt %s" % a + + revs = cmdutil.revrange(repo, (opts['node'], 'tip')) + print "R1 %s" % min(revs) + print "R2 %s" % max(revs) + + allowtags = allow_tag_push() + + ret = 0 + for rev in revs: + cs = repo.changelog.node(rev) + changes = repo.changelog.read(cs) + if (".hgtags" in changes[3]) and not allowtags: + print "Tag push by user %s is denied." % os.environ['CALLING_USER'] + ret = 1 + break + return ret + + +def cdmhook_cstyle(ui, repo, **opts): + '''check active C source files conform to the C Style Guide + + See http://opensolaris.org/os/community/documentation/getting_started_docs/cstyle.ms.pdf''' + + ui.write('C style check:\n') + + print "node %s" % opts['node'] + a = repo.changelog.count() - 1 + print "cnt %s" % a + + revs = cmdutil.revrange(repo, (opts['node'], 'tip')) + print "R1 %s" % min(revs) + print "R2 %s" % max(revs) + + node1 = repo.changelog.node(min(revs)-1) + node2 = repo.changelog.node(max(revs)) + files_changed = repo.status(node1, node2)[:2] + + files = {} + for f in files_changed[0]: + files[f] = 1 + for f in files_changed[1]: + files[f] = 1 + + # sam-qfs has a list of files that are not cstyle-clean. + nocstyle = {} + # Need to know the proper way to open the parent. + #fh = open("misc/cstyle/cstyle.cf", 'r') + #for line in fh.readlines(): + # nocstyle[line.strip()] = 1 + #fh.close() + + ret = 0 + ui.pushbuffer() + for f in files: + if f in nocstyle: + continue + if fnmatch(f, '*.c') or fnmatch(f, '*.h'): + # Need to know the proper way to open the parent. + fh = open(f, 'r') + # ON cstyle exit status is zero, even if it printed warnings. + ret |= CStyle.cstyle(fh, output=ui, + picky=True, check_posix_types=True, + check_continuation=True) + fh.close() + output = ui.popbuffer() + ui.write(output, '\n') + + # Find out if it printed warnings. + lines = output.splitlines() + if len(lines) != 0: + ret = 1 + + return ret + + +def cdmhook_allowpush(ui, repo, **opts): + '''Does the user have permission to push? They must be listed + in the repo's allowpush file. If they aren't listed, then + print the motd in the hope that it will explain the situation + to the user.''' + + def get_motd(ui): + '''Print a message-of-the-day for the given repository''' + + motd = "".join([os.environ['CALLED_REPO'],"-motd"]) + print "motd(%s)" % motd + + try: + fh = open(motd, 'r') + except: + return + + for line in fh.readlines(): + print line.strip() + fh.close() + + + allowfile = "".join([os.environ['CALLED_REPO'],"-allowpush"]) + print "allowfile(%s)" % allowfile + + try: + fh = open(allowfile, 'r') + except: + print "This repository is currently not allowing pushes." + get_motd(ui) + return 1 + + af_user = {} + for line in fh.readlines(): + af_user[line.strip()] = 1 + fh.close() + if os.environ['CALLING_USER'] in af_user: + return 0 + print "This repository is currently not allowing pushes from you (%s)." % os.environ['CALLING_USER'] + get_motd(ui) + return 1 +