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