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
+

Reply via email to