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) 2015, 2016 by Delphix. All rights reserved. 22 # Copyright 2016 Nexenta Systems, Inc. 23 # Copyright 2018 Joyent, 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 abspath in git_file_list(parent, paths): 202 path = relpath(abspath, '.') 203 try: 204 res = git("diff %s HEAD %s" % (parent, path)) 205 except GitError, e: 206 # This ignores all the errors that can be thrown. Usually, this 207 # means that git returned non-zero because the file doesn't 208 # exist, but it could also fail if git can't create a new file 209 # or it can't be executed. Such errors are 1) unlikely, and 2) 210 # will be caught by other invocations of git(). 211 continue 212 empty = not res.readline() 213 if (os.path.isfile(path) and not empty and 214 select(path) and not exclude(abspath)): 215 yield path 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 def wscheck(root, parent, flist, output): 315 ret = 0 316 output.write("white space nits:\n") 317 for f in flist(): 318 fh = open(f, 'r') 319 ret |= WsCheck.wscheck(fh, output=output) 320 fh.close() 321 return ret 322 323 def run_checks(root, parent, cmds, paths='', opts={}): 324 """Run the checks given in 'cmds', expected to have well-known signatures, 325 and report results for any which fail. 326 327 Return failure if any of them did. 328 329 NB: the function name of the commands passed in is used to name the NOT 330 file which excepts files from them.""" 331 332 ret = 0 333 334 for cmd in cmds: 335 s = StringIO() 336 337 exclude = not_check(root, cmd.func_name) 338 result = cmd(root, parent, gen_files(root, parent, paths, exclude), 339 output=s) 340 ret |= result 341 342 if result != 0: 343 print s.getvalue() 344 345 return ret 346 347 348 def nits(root, parent, paths): 349 cmds = [copyright, 350 cstyle, 351 hdrchk, 352 jstyle, 353 keywords, 354 manlint, 355 mapfilechk, 356 wscheck] 357 run_checks(root, parent, cmds, paths) 358 359 360 def pbchk(root, parent, paths): 361 cmds = [comchk, 362 copyright, 363 cstyle, 364 hdrchk, 365 jstyle, 366 keywords, 367 manlint, 368 mapfilechk, 369 wscheck] 370 run_checks(root, parent, cmds) 371 372 373 def main(cmd, args): 374 parent_branch = None 375 checkname = None 376 377 try: 378 opts, args = getopt.getopt(args, 'c:p:') 379 except getopt.GetoptError, e: 380 sys.stderr.write(str(e) + '\n') 381 sys.stderr.write("Usage: %s [-c check] [-p branch] [path...]\n" % cmd) 382 sys.exit(1) 383 384 for opt, arg in opts: 385 # backwards compatibility 386 if opt == '-b': 387 parent_branch = arg 388 elif opt == '-c': 389 checkname = arg 390 elif opt == '-p': 391 parent_branch = arg 392 393 if not parent_branch: 394 parent_branch = git_parent_branch(git_branch()) 395 396 if checkname is None: 397 if cmd == 'git-pbchk': 398 checkname= 'pbchk' 399 else: 400 checkname = 'nits' 401 402 if checkname == 'pbchk': 403 if args: 404 sys.stderr.write("only complete workspaces may be pbchk'd\n"); 405 sys.exit(1) 406 pbchk(git_root(), parent_branch, None) 407 elif checkname == 'nits': 408 nits(git_root(), parent_branch, args) 409 else: 410 run_checks(git_root(), parent_branch, [eval(checkname)], args) 411 412 if __name__ == '__main__': 413 try: 414 main(os.path.basename(sys.argv[0]), sys.argv[1:]) 415 except GitError, e: 416 sys.stderr.write("failed to run git:\n %s\n" % str(e)) 417 sys.exit(1)