1 #!/usr/bin/python2.6
   2 #
   3 #  This program is free software; you can redistribute it and/or modify
   4 #  it under the terms of the GNU General Public License version 2
   5 #  as published by the Free Software Foundation.
   6 #
   7 #  This program is distributed in the hope that it will be useful,
   8 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
   9 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  10 #  GNU General Public License for more details.
  11 #
  12 #  You should have received a copy of the GNU General Public License
  13 #  along with this program; if not, write to the Free Software
  14 #  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
  15 #
  16 
  17 #
  18 # Copyright (c) 2008, 2010, Oracle and/or its affiliates. All rights reserved.
  19 # Copyright 2008, 2012 Richard Lowe
  20 #
  21 
  22 import getopt
  23 import os
  24 import re
  25 import subprocess
  26 import sys
  27 import tempfile
  28 
  29 from cStringIO import StringIO
  30 
  31 # This is necessary because, in a fit of pique, we used hg-format ignore lists
  32 # for NOT files.
  33 from mercurial import ignore
  34 
  35 #
  36 # Adjust the load path based on our location and the version of python into
  37 # which it is being loaded.  This assumes the normal onbld directory
  38 # structure, where we are in bin/ and the modules are in
  39 # lib/python(version)?/onbld/Scm/.  If that changes so too must this.
  40 #
  41 sys.path.insert(1, os.path.join(os.path.dirname(__file__), "..", "lib",
  42                                 "python%d.%d" % sys.version_info[:2]))
  43 
  44 #
  45 # Add the relative path to usr/src/tools to the load path, such that when run
  46 # from the source tree we use the modules also within the source tree.
  47 #
  48 sys.path.insert(2, os.path.join(os.path.dirname(__file__), ".."))
  49 
  50 from onbld.Checks import Comments, Copyright, CStyle, HdrChk
  51 from onbld.Checks import JStyle, Keywords, Mapfile
  52 
  53 
  54 class GitError(Exception):
  55     pass
  56 
  57 def git(command):
  58     """Run a command and return a stream containing its stdout (and write its
  59     stderr to its stdout)"""
  60 
  61     if type(command) != list:
  62         command = command.split()
  63 
  64     command = ["git"] + command
  65 
  66     try:
  67         tmpfile = tempfile.TemporaryFile(prefix="git-nits")
  68     except EnvironmentError, e:
  69         raise GitError("Could not create temporary file: %s\n" % e)
  70 
  71     try:
  72         p = subprocess.Popen(command,
  73                              stdout=tmpfile,
  74                              stderr=subprocess.STDOUT)
  75     except OSError, e:
  76         raise GitError("could not execute %s: %s\n" (command, e))
  77 
  78     err = p.wait()
  79     if err != 0:
  80         raise GitError(p.stdout.read())
  81 
  82     tmpfile.seek(0)
  83     return tmpfile
  84 
  85 
  86 def git_root():
  87     """Return the root of the current git workspace"""
  88 
  89     p = git('rev-parse --git-dir')
  90 
  91     if not p:
  92         sys.stderr.write("Failed finding git workspace\n")
  93         sys.exit(err)
  94 
  95     return os.path.abspath(os.path.join(p.readlines()[0],
  96                                         os.path.pardir))
  97 
  98 
  99 def git_branch():
 100     """Return the current git branch"""
 101 
 102     p = git('branch')
 103 
 104     if not p:
 105         sys.stderr.write("Failed finding git branch\n")
 106         sys.exit(err)
 107 
 108     for elt in p:
 109         if elt[0] == '*':
 110             if elt.endswith('(no branch)'):
 111                 return None
 112             return elt.split()[1]
 113 
 114 
 115 def git_parent_branch(branch):
 116     """Return the parent of the current git branch.
 117 
 118     If this branch tracks a remote branch, return the remote branch which is
 119     tracked.  If not, default to origin/master."""
 120 
 121     if not branch:
 122         return None
 123 
 124     p = git("for-each-ref --format=%(refname:short) %(upstream:short) " +
 125             "refs/heads/")
 126 
 127     if not p:
 128         sys.stderr.write("Failed finding git parent branch\n")
 129         sys.exit(err)
 130 
 131     for line in p:
 132         # Git 1.7 will leave a ' ' trailing any non-tracking branch
 133         if ' ' in line and not line.endswith(' \n'):
 134             local, remote = line.split()
 135             if local == branch:
 136                 return remote
 137     return 'origin/master'
 138 
 139 
 140 def git_comments(parent):
 141     """Return a list of any checkin comments on this git branch"""
 142 
 143     p = git('log --pretty=tformat:%%B:SEP: %s..' % parent)
 144 
 145     if not p:
 146         sys.stderr.write("Failed getting git comments\n")
 147         sys.exit(err)
 148 
 149     return [x.strip() for x in p.readlines() if x != ':SEP:\n']
 150 
 151 
 152 def git_file_list(parent, paths=None):
 153     """Return the set of files which have ever changed on this branch.
 154 
 155     NB: This includes files which no longer exist, or no longer actually
 156     differ."""
 157 
 158     p = git("log --name-only --pretty=format: %s.. %s" %
 159              (parent, ' '.join(paths)))
 160 
 161     if not p:
 162         sys.stderr.write("Failed building file-list from git\n")
 163         sys.exit(err)
 164 
 165     ret = set()
 166     for fname in p:
 167         if fname and not fname.isspace() and fname not in ret:
 168             ret.add(fname.strip())
 169 
 170     return ret
 171 
 172 
 173 def not_check(root, cmd):
 174     """Return a function which returns True if a file given as an argument
 175     should be excluded from the check named by 'cmd'"""
 176 
 177     ignorefiles = filter(os.path.exists,
 178                          [os.path.join(root, ".git", "%s.NOT" % cmd),
 179                           os.path.join(root, "exception_lists", cmd)])
 180     if len(ignorefiles) > 0:
 181         return ignore.ignore(root, ignorefiles, sys.stderr.write)
 182     else:
 183         return lambda x: False
 184 
 185 
 186 def gen_files(root, parent, paths, exclude):
 187     """Return a function producing file names, relative to the current
 188     directory, of any file changed on this branch (limited to 'paths' if
 189     requested), and excluding files for which exclude returns a true value """
 190 
 191     # Taken entirely from Python 2.6's os.path.relpath which we would use if we
 192     # could.
 193     def relpath(path, here):
 194         c = os.path.abspath(os.path.join(root, path)).split(os.path.sep)
 195         s = os.path.abspath(here).split(os.path.sep)
 196         l = len(os.path.commonprefix((s, c)))
 197         return os.path.join(*[os.path.pardir] * (len(s)-l) + c[l:])
 198 
 199     def ret(select=None):
 200         if not select:
 201             select = lambda x: True
 202 
 203         for f in git_file_list(parent, paths):
 204             f = relpath(f, '.')
 205             if (os.path.exists(f) and select(f) and not exclude(f)):
 206                 yield f
 207     return ret
 208 
 209 
 210 def comchk(root, parent, flist, output):
 211     output.write("Comments:\n")
 212 
 213     return Comments.comchk(git_comments(parent), check_db=True,
 214                            output=output)
 215 
 216 
 217 def mapfilechk(root, parent, flist, output):
 218     ret = 0
 219 
 220     # We are interested in examining any file that has the following
 221     # in its final path segment:
 222     #    - Contains the word 'mapfile'
 223     #    - Begins with 'map.'
 224     #    - Ends with '.map'
 225     # We don't want to match unless these things occur in final path segment
 226     # because directory names with these strings don't indicate a mapfile.
 227     # We also ignore files with suffixes that tell us that the files
 228     # are not mapfiles.
 229     MapfileRE = re.compile(r'.*((mapfile[^/]*)|(/map\.+[^/]*)|(\.map))$',
 230         re.IGNORECASE)
 231     NotMapSuffixRE = re.compile(r'.*\.[ch]$', re.IGNORECASE)
 232 
 233     output.write("Mapfile comments:\n")
 234 
 235     for f in flist(lambda x: MapfileRE.match(x) and not
 236                    NotMapSuffixRE.match(x)):
 237         fh = open(f, 'r')
 238         ret |= Mapfile.mapfilechk(fh, output=output)
 239         fh.close()
 240     return ret
 241 
 242 
 243 def copyright(root, parent, flist, output):
 244     ret = 0
 245     output.write("Copyrights:\n")
 246     for f in flist():
 247         fh = open(f, 'r')
 248         ret |= Copyright.copyright(fh, output=output)
 249         fh.close()
 250     return ret
 251 
 252 
 253 def hdrchk(root, parent, flist, output):
 254     ret = 0
 255     output.write("Header format:\n")
 256     for f in flist(lambda x: x.endswith('.h')):
 257         fh = open(f, 'r')
 258         ret |= HdrChk.hdrchk(fh, lenient=True, output=output)
 259         fh.close()
 260     return ret
 261 
 262 
 263 def cstyle(root, parent, flist, output):
 264     ret = 0
 265     output.write("C style:\n")
 266     for f in flist(lambda x: x.endswith('.c') or x.endswith('.h')):
 267         fh = open(f, 'r')
 268         ret |= CStyle.cstyle(fh, output=output, picky=True,
 269                              check_posix_types=True,
 270                              check_continuation=True)
 271         fh.close()
 272     return ret
 273 
 274 
 275 def jstyle(root, parent, flist, output):
 276     ret = 0
 277     output.write("Java style:\n")
 278     for f in flist(lambda x: x.endswith('.java')):
 279         fh = open(f, 'r')
 280         ret |= JStyle.jstyle(fh, output=output, picky=True)
 281         fh.close()
 282     return ret
 283 
 284 
 285 def keywords(root, parent, flist, output):
 286     ret = 0
 287     output.write("SCCS Keywords:\n")
 288     for f in flist():
 289         fh = open(f, 'r')
 290         ret |= Keywords.keywords(fh, output=output)
 291         fh.close()
 292     return ret
 293 
 294 
 295 def run_checks(root, parent, cmds, paths='', opts={}):
 296     """Run the checks given in 'cmds', expected to have well-known signatures,
 297     and report results for any which fail.
 298 
 299     Return failure if any of them did.
 300 
 301     NB: the function name of the commands passed in is used to name the NOT
 302     file which excepts files from them."""
 303 
 304     ret = 0
 305 
 306     for cmd in cmds:
 307         s = StringIO()
 308 
 309         exclude = not_check(root, cmd.func_name)
 310         result = cmd(root, parent, gen_files(root, parent, paths, exclude),
 311                      output=s)
 312         ret |= result
 313 
 314         if result != 0:
 315             print s.getvalue()
 316 
 317     return ret
 318 
 319 
 320 def nits(root, parent, paths):
 321     cmds = [copyright,
 322             cstyle,
 323             hdrchk,
 324             jstyle,
 325             keywords,
 326             mapfilechk]
 327     run_checks(root, parent, cmds, paths)
 328 
 329 
 330 def pbchk(root, parent, paths):
 331     cmds = [comchk,
 332             copyright,
 333             cstyle,
 334             hdrchk,
 335             jstyle,
 336             keywords,
 337             mapfilechk]
 338     run_checks(root, parent, cmds)
 339 
 340 
 341 def main(cmd, args):
 342     parent_branch = None
 343 
 344     try:
 345         opts, args = getopt.getopt(args, 'b:')
 346     except getopt.GetoptError, e:
 347         sys.stderr.write(str(e) + '\n')
 348         sys.stderr.write("Usage: %s [-b branch] [path...]\n" % cmd)
 349         sys.exit(1)
 350 
 351     for opt, arg in opts:
 352         if opt == '-b':
 353             parent_branch = arg
 354 
 355     if not parent_branch:
 356         parent_branch = git_parent_branch(git_branch())
 357 
 358     func = nits
 359     if cmd == 'git-pbchk':
 360         func = pbchk
 361         if args:
 362             sys.stderr.write("only complete workspaces may be pbchk'd\n");
 363             sys.exit(1)
 364 
 365     func(git_root(), parent_branch, args)
 366 
 367 if __name__ == '__main__':
 368     try:
 369         main(os.path.basename(sys.argv[0]), sys.argv[1:])
 370     except GitError, e:
 371         sys.stderr.write("failed to run git:\n %s\n" % str(e))
 372         sys.exit(1)