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