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