Add code to turn the style stuff into a mercurial hook.
authorNathan Binkert <nate@binkert.org>
Sun, 22 Jul 2007 05:33:08 +0000 (22:33 -0700)
committerNathan Binkert <nate@binkert.org>
Sun, 22 Jul 2007 05:33:08 +0000 (22:33 -0700)
Nag the user during compile if they have an hg cloned copy of M5, have
mercurial installed, but don't have the style hook enabled.

--HG--
extra : convert_revision : 6bcbb67f1a3fcd36db7d3ef16a9ff19680f126f2

SConstruct
util/style.py [new file with mode: 0644]

index fa2366963f227a0d310679b7d99c105c81fed472..1c2dcab1c47ef46aa14b62f61a605fb0d2acdcde 100644 (file)
@@ -67,7 +67,7 @@ import sys
 import os
 import subprocess
 
-from os.path import join as joinpath
+from os.path import isdir, join as joinpath
 
 # Check for recent-enough Python and SCons versions.  If your system's
 # default installation of Python is not recent enough, you can use a
@@ -97,6 +97,34 @@ SRCDIR = joinpath(ROOT, 'src')
 # tell python where to find m5 python code
 sys.path.append(joinpath(ROOT, 'src/python'))
 
+def check_style_hook(ui):
+    ui.readconfig(joinpath(ROOT, '.hg', 'hgrc'))
+    style_hook = ui.config('hooks', 'pretxncommit.style', None)
+
+    if not style_hook:
+        print """\
+You're missing the M5 style hook.
+Please install the hook so we can ensure that all code fits a common style.
+
+All you'd need to do is add the following lines to your repository .hg/hgrc
+or your personal .hgrc
+----------------
+
+[extensions]
+style = %s/util/style.py
+
+[hooks]
+pretxncommit.style = python:style.check_whitespace
+""" % (ROOT)
+        sys.exit(1)
+
+if isdir(joinpath(ROOT, '.hg')):
+    try:
+        from mercurial import ui
+        check_style_hook(ui.ui())
+    except ImportError:
+        pass
+
 ###################################################
 #
 # Figure out which configurations to set up based on the path(s) of
diff --git a/util/style.py b/util/style.py
new file mode 100644 (file)
index 0000000..3824cd7
--- /dev/null
@@ -0,0 +1,374 @@
+#! /usr/bin/env python
+# Copyright (c) 2007 The Regents of The University of Michigan
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met: redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer;
+# redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution;
+# neither the name of the copyright holders nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+# Authors: Nathan Binkert
+
+import re
+import os
+import sys
+
+lead = re.compile(r'^([ \t]+)')
+trail = re.compile(r'([ \t]+)$')
+any_control = re.compile(r'\b(if|while|for)[ \t]*[(]')
+good_control = re.compile(r'\b(if|while|for) [(]')
+
+lang_types = { 'c'   : "C",
+               'h'   : "C",
+               'cc'  : "C++",
+               'hh'  : "C++",
+               'cxx' : "C++",
+               'hxx' : "C++",
+               'cpp' : "C++",
+               'hpp' : "C++",
+               'C'   : "C++",
+               'H'   : "C++",
+               'i'   : "swig",
+               'py'  : "python",
+               's'   : "asm",
+               'S'   : "asm",
+               'isa' : "isa" }
+whitespace_types = ('C', 'C++', 'swig', 'python', 'asm', 'isa')
+format_types = ( 'C', 'C++' )
+
+def file_type(filename):
+    extension = filename.split('.')
+    extension = len(extension) > 1 and extension[-1]
+    return lang_types.get(extension, None)
+
+def checkwhite_line(line):
+    match = lead.search(line)
+    if match and match.group(1).find('\t') != -1:
+        return False
+
+    match = trail.search(line)
+    if match:
+        return False
+
+    return True
+
+def checkwhite(filename):
+    if file_type(filename) not in whitespace_types:
+        return
+
+    try:
+        f = file(filename, 'r+')
+    except OSError, msg:
+        print 'could not open file %s: %s' % (filename, msg)
+        return
+
+    for num,line in enumerate(f):
+        if not checkwhite_line(line):
+            yield line,num + 1
+
+def fixwhite_line(line):
+    if lead.search(line):
+        newline = ''
+        for i,c in enumerate(line):
+            if c == ' ':
+                newline += ' '
+            elif c == '\t':
+                newline += ' ' * (tabsize - len(newline) % tabsize)
+            else:
+                newline += line[i:]
+                break
+
+        line = newline
+
+    return line.rstrip() + '\n'
+
+def fixwhite(filename, tabsize, fixonly=None):
+    if file_type(filename) not in whitespace_types:
+        return
+
+    try:
+        f = file(filename, 'r+')
+    except OSError, msg:
+        print 'could not open file %s: %s' % (filename, msg)
+        return
+
+    lines = list(f)
+
+    f.seek(0)
+    f.truncate()
+
+    for i,line in enumerate(lines):
+        if fixonly is None or i in fixonly:
+            line = fixwhite_line(line)
+
+        print >>f, line,
+
+def linelen(line):
+    tabs = line.count('\t')
+    if not tabs:
+        return len(line)
+
+    count = 0
+    for c in line:
+        if c == '\t':
+            count += tabsize - count % tabsize
+        else:
+            count += 1
+
+    return count
+
+class ValidationStats(object):
+    def __init__(self):
+        self.toolong = 0
+        self.toolong80 = 0
+        self.leadtabs = 0
+        self.trailwhite = 0
+        self.badcontrol = 0
+        self.cret = 0
+
+    def dump(self):
+        print '''\
+%d violations of lines over 79 chars. %d of which are 80 chars exactly.
+%d cases of whitespace at the end of a line.
+%d cases of tabs to indent.
+%d bad parens after if/while/for.
+%d carriage returns found.
+''' % (self.toolong, self.toolong80, self.trailwhite, self.leadtabs,
+       self.badcontrol, self.cret)
+
+    def __nonzero__(self):
+        return self.toolong or self.toolong80 or self.leadtabs or \
+               self.trailwhite or self.badcontrol or self.cret
+
+def validate(filename, stats, verbose, exit_code):
+    if file_type(filename) not in format_types:
+        return
+
+    def msg(lineno, line, message):
+        print '%s:%d>' % (filename, lineno + 1), message
+        if verbose > 2:
+            print line
+
+    def bad():
+        if exit_code is not None:
+            sys.exit(exit_code)
+
+    cpp = filename.endswith('.cc') or filename.endswith('.hh')
+    py = filename.endswith('.py')
+
+    if py + cpp != 1:
+        raise AttributeError, \
+              "I don't know how to deal with the file %s" % filename
+
+    try:
+        f = file(filename, 'r')
+    except OSError:
+        if verbose > 0:
+            print 'could not open file %s' % filename
+        bad()
+        return
+
+    for i,line in enumerate(f):
+        line = line.rstrip('\n')
+
+        # no carriage returns
+        if line.find('\r') != -1:
+            self.cret += 1
+            if verbose > 1:
+                msg(i, line, 'carriage return found')
+            bad()
+
+        # lines max out at 79 chars
+        llen = linelen(line)
+        if llen > 79:
+            stats.toolong += 1
+            if llen == 80:
+                stats.toolong80 += 1
+            if verbose > 1:
+                msg(i, line, 'line too long (%d chars)' % llen)
+            bad()
+
+        # no tabs used to indent
+        match = lead.search(line)
+        if match and match.group(1).find('\t') != -1:
+            stats.leadtabs += 1
+            if verbose > 1:
+                msg(i, line, 'using tabs to indent')
+            bad()
+
+        # no trailing whitespace
+        if trail.search(line):
+            stats.trailwhite +=1
+            if verbose > 1:
+                msg(i, line, 'trailing whitespace')
+            bad()
+
+        # for c++, exactly one space betwen if/while/for and (
+        if cpp:
+            match = any_control.search(line)
+            if match and not good_control.search(line):
+                stats.badcontrol += 1
+                if verbose > 1:
+                    msg(i, line, 'improper spacing after %s' % match.group(1))
+                bad()
+
+def check_whitespace(ui, repo, hooktype, node, parent1, parent2):
+    from mercurial import bdiff, mdiff, util
+    if hooktype != 'pretxncommit':
+        raise AttributeError, \
+              "This hook is only meant for pretxncommit, not %s" % hooktype
+
+    tabsize = 8
+    verbose = ui.configbool('style', 'verbose', False)
+    def prompt(name, fixonly=None):
+        result = ui.prompt("(a)bort, (i)gnore, or (f)ix?", "^[aif]$", "a")
+        if result == 'a':
+            return True
+        elif result == 'i':
+            pass
+        elif result == 'f':
+            fixwhite(name, tabsize, fixonly)
+        else:
+            raise RepoError, "Invalid response: '%s'" % result
+
+        return False
+
+    modified, added, removed, deleted, unknown, ignore, clean = repo.status()
+
+    for fname in added:
+        ok = True
+        for line,num in checkwhite(fname):
+            ui.write("invalid whitespace in %s:%d\n" % (fname, num))
+            if verbose:
+                ui.write(">>%s<<\n" % line[-1])
+            ok = False
+
+        if not ok:
+            if prompt(fname):
+                return True
+
+    wctx = repo.workingctx()
+    for fname in modified:
+        fctx = wctx.filectx(fname)
+        pctx = fctx.parents()
+        assert len(pctx) == 1
+
+        pdata = pctx[0].data()
+        fdata = fctx.data()
+
+        fixonly = set()
+        lines = enumerate(mdiff.splitnewlines(fdata))
+        for pbeg, pend, fbeg, fend in bdiff.blocks(pdata, fdata):
+            for i, line in lines:
+                if i < fbeg:
+                    if checkwhite_line(line):
+                        continue
+
+                    ui.write("invalid whitespace: %s:%d\n" % (fname, i+1))
+                    if verbose:
+                        ui.write(">>%s<<\n" % line[:-1])
+                    fixonly.add(i)
+                elif i + 1 >= fend:
+                    break
+
+        if fixonly:
+            if prompt(fname, fixonly):
+                return True
+
+def check_format(ui, repo, hooktype, node, parent1, parent2):
+    if hooktype != 'pretxncommit':
+        raise AttributeError, \
+              "This hook is only meant for pretxncommit, not %s" % hooktype
+
+    modified, added, removed, deleted, unknown, ignore, clean = repo.status()
+
+    verbose = 0
+    stats = ValidationStats()
+    for f in modified + added:
+        validate(f, stats, verbose, None)
+
+    if stats:
+        stats.dump()
+        result = ui.prompt("invalid formatting\n(i)gnore or (a)bort?",
+                           "^[ia]$", "a")
+        if result.startswith('i'):
+            pass
+        elif result.startswith('a'):
+            return True
+        else:
+            raise RepoError, "Invalid response: '%s'" % result
+
+    return False
+
+if __name__ == '__main__':
+    import getopt
+
+    progname = sys.argv[0]
+    if len(sys.argv) < 2:
+        sys.exit('usage: %s <command> [<command args>]' % progname)
+
+    fixwhite_usage = '%s fixwhite [-t <tabsize> ] <path> [...] \n' % progname
+    chkformat_usage = '%s chkformat <path> [...] \n' % progname
+    chkwhite_usage = '%s chkwhite <path> [...] \n' % progname
+
+    command = sys.argv[1]
+    if command == 'fixwhite':
+        flags = 't:'
+        usage = fixwhite_usage
+    elif command == 'chkwhite':
+        flags = 'nv'
+        usage = chkwhite_usage
+    elif command == 'chkformat':
+        flags = 'nv'
+        usage = chkformat_usage
+    else:
+        sys.exit(fixwhite_usage + chkwhite_usage + chkformat_usage)
+
+    opts, args = getopt.getopt(sys.argv[2:], flags)
+
+    code = 1
+    verbose = 1
+    tabsize = 8
+    for opt,arg in opts:
+        if opt == '-n':
+            code = None
+        if opt == '-t':
+            tabsize = int(arg)
+        if opt == '-v':
+            verbose += 1
+
+    if command == 'fixwhite':
+        for filename in args:
+            fixwhite(filename, tabsize)
+    elif command == 'chkwhite':
+        for filename in args:
+            line = checkwhite(filename)
+            if line:
+                print 'invalid whitespace at %s:%d' % (filename, line)
+    elif command == 'chkformat':
+        stats = ValidationStats()
+        for filename in files:
+            validate(filename, stats=stats, verbose=verbose, exit_code=code)
+
+        if verbose > 0:
+            stats.dump()
+    else:
+        sys.exit("command '%s' not found" % command)