1#!@TOOLS_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 2019 Garrett D'Amore <garrett@damore.org> 21# Copyright (c) 2015, 2016 by Delphix. All rights reserved. 22# Copyright 2016 Nexenta Systems, Inc. 23# Copyright (c) 2019, Joyent, Inc. 24# Copyright 2020 OmniOS Community Edition (OmniOSce) Association. 25# 26 27from __future__ import print_function 28 29import getopt 30import io 31import os 32import re 33import subprocess 34import sys 35import tempfile 36 37if sys.version_info[0] < 3: 38 from cStringIO import StringIO 39else: 40 from io import StringIO 41 42# 43# Adjust the load path based on our location and the version of python into 44# which it is being loaded. This assumes the normal onbld directory 45# structure, where we are in bin/ and the modules are in 46# lib/python(version)?/onbld/Scm/. If that changes so too must this. 47# 48sys.path.insert(1, os.path.join(os.path.dirname(__file__), "..", "lib", 49 "python%d.%d" % sys.version_info[:2])) 50 51# 52# Add the relative path to usr/src/tools to the load path, such that when run 53# from the source tree we use the modules also within the source tree. 54# 55sys.path.insert(2, os.path.join(os.path.dirname(__file__), "..")) 56 57from onbld.Scm import Ignore 58from onbld.Checks import Comments, Copyright, CStyle, HdrChk, WsCheck 59from onbld.Checks import JStyle, Keywords, ManLint, Mapfile, SpellCheck 60 61class GitError(Exception): 62 pass 63 64def git(command): 65 """Run a command and return a stream containing its stdout (and write its 66 stderr to its stdout)""" 67 68 if type(command) != list: 69 command = command.split() 70 71 command = ["git"] + command 72 73 try: 74 tmpfile = tempfile.TemporaryFile(prefix="git-nits", mode="w+b") 75 except EnvironmentError as e: 76 raise GitError("Could not create temporary file: %s\n" % e) 77 78 try: 79 p = subprocess.Popen(command, 80 stdout=tmpfile, 81 stderr=subprocess.PIPE) 82 except OSError as e: 83 raise GitError("could not execute %s: %s\n" % (command, e)) 84 85 err = p.wait() 86 if err != 0: 87 raise GitError(p.stderr.read()) 88 89 tmpfile.seek(0) 90 lines = [] 91 for l in tmpfile: 92 lines.append(l.decode('utf-8', 'replace')) 93 return lines 94 95def git_root(): 96 """Return the root of the current git workspace""" 97 98 p = git('rev-parse --git-dir') 99 dir = p[0] 100 101 return os.path.abspath(os.path.join(dir, os.path.pardir)) 102 103def git_branch(): 104 """Return the current git branch""" 105 106 p = git('branch') 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 114def git_parent_branch(branch): 115 """Return the parent of the current git branch. 116 117 If this branch tracks a remote branch, return the remote branch which is 118 tracked. If not, default to origin/master.""" 119 120 if not branch: 121 return None 122 123 p = git(["for-each-ref", "--format=%(refname:short) %(upstream:short)", 124 "refs/heads/"]) 125 126 if not p: 127 sys.stderr.write("Failed finding git parent branch\n") 128 sys.exit(1) 129 130 for line in p: 131 # Git 1.7 will leave a ' ' trailing any non-tracking branch 132 if ' ' in line and not line.endswith(' \n'): 133 local, remote = line.split() 134 if local == branch: 135 return remote 136 return 'origin/master' 137 138def 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("No outgoing changesets found - missing -p option?\n"); 145 sys.exit(1) 146 147 return [x.strip() for x in p if x != ':SEP:\n'] 148 149def git_file_list(parent, paths=None): 150 """Return the set of files which have ever changed on this branch. 151 152 NB: This includes files which no longer exist, or no longer actually 153 differ.""" 154 155 p = git("log --name-only --pretty=format: %s.. %s" % 156 (parent, ' '.join(paths))) 157 158 if not p: 159 sys.stderr.write("Failed building file-list from git\n") 160 sys.exit(1) 161 162 ret = set() 163 for fname in p: 164 if fname and not fname.isspace() and fname not in ret: 165 ret.add(fname.strip()) 166 167 return ret 168 169def not_check(root, cmd): 170 """Return a function which returns True if a file given as an argument 171 should be excluded from the check named by 'cmd'""" 172 173 ignorefiles = list(filter(os.path.exists, 174 [os.path.join(root, ".git", "%s.NOT" % cmd), 175 os.path.join(root, "exception_lists", cmd)])) 176 return Ignore.ignore(root, ignorefiles) 177 178def gen_files(root, parent, paths, exclude, filter=None): 179 """Return a function producing file names, relative to the current 180 directory, of any file changed on this branch (limited to 'paths' if 181 requested), and excluding files for which exclude returns a true value """ 182 183 if filter is None: 184 filter = lambda x: os.path.isfile(x) 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 abspath in git_file_list(parent, paths): 199 path = relpath(abspath, '.') 200 try: 201 res = git("diff %s HEAD %s" % (parent, path)) 202 except GitError as e: 203 # This ignores all the errors that can be thrown. Usually, this 204 # means that git returned non-zero because the file doesn't 205 # exist, but it could also fail if git can't create a new file 206 # or it can't be executed. Such errors are 1) unlikely, and 2) 207 # will be caught by other invocations of git(). 208 continue 209 empty = not res 210 if (filter(path) and not empty and 211 select(path) and not exclude(abspath)): 212 yield path 213 return ret 214 215def gen_links(root, parent, paths, exclude): 216 """Return a function producing symbolic link names, relative to the current 217 directory, of any file changed on this branch (limited to 'paths' if 218 requested), and excluding files for which exclude returns a true value """ 219 220 return gen_files(root, parent, paths, exclude, lambda x: os.path.islink(x)) 221 222def comchk(root, parent, flist, output): 223 output.write("Comments:\n") 224 225 return Comments.comchk(git_comments(parent), check_db=True, 226 output=output) 227 228 229def mapfilechk(root, parent, flist, output): 230 ret = 0 231 232 # We are interested in examining any file that has the following 233 # in its final path segment: 234 # - Contains the word 'mapfile' 235 # - Begins with 'map.' 236 # - Ends with '.map' 237 # We don't want to match unless these things occur in final path segment 238 # because directory names with these strings don't indicate a mapfile. 239 # We also ignore files with suffixes that tell us that the files 240 # are not mapfiles. 241 MapfileRE = re.compile(r'.*((mapfile[^/]*)|(/map\.+[^/]*)|(\.map))$', 242 re.IGNORECASE) 243 NotMapSuffixRE = re.compile(r'.*\.[ch]$', re.IGNORECASE) 244 245 output.write("Mapfile comments:\n") 246 247 for f in flist(lambda x: MapfileRE.match(x) and not 248 NotMapSuffixRE.match(x)): 249 with io.open(f, encoding='utf-8', errors='replace') as fh: 250 ret |= Mapfile.mapfilechk(fh, output=output) 251 return ret 252 253def copyright(root, parent, flist, output): 254 ret = 0 255 output.write("Copyrights:\n") 256 for f in flist(): 257 with io.open(f, encoding='utf-8', errors='replace') as fh: 258 ret |= Copyright.copyright(fh, output=output) 259 return ret 260 261def 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 with io.open(f, encoding='utf-8', errors='replace') as fh: 266 ret |= HdrChk.hdrchk(fh, lenient=True, output=output) 267 return ret 268 269def cstyle(root, parent, flist, output): 270 ret = 0 271 output.write("C style:\n") 272 for f in flist(lambda x: x.endswith('.c') or x.endswith('.h')): 273 with io.open(f, mode='rb') as fh: 274 ret |= CStyle.cstyle(fh, output=output, picky=True, 275 check_posix_types=True, 276 check_continuation=True) 277 return ret 278 279def jstyle(root, parent, flist, output): 280 ret = 0 281 output.write("Java style:\n") 282 for f in flist(lambda x: x.endswith('.java')): 283 with io.open(f, mode='rb') as fh: 284 ret |= JStyle.jstyle(fh, output=output, picky=True) 285 return ret 286 287def manlint(root, parent, flist, output): 288 ret = 0 289 output.write("Man page format/spelling:\n") 290 ManfileRE = re.compile(r'.*\.[0-9][a-z]*$', re.IGNORECASE) 291 for f in flist(lambda x: ManfileRE.match(x)): 292 with io.open(f, mode='rb') as fh: 293 ret |= ManLint.manlint(fh, output=output, picky=True) 294 ret |= SpellCheck.spellcheck(fh, output=output) 295 return ret 296 297def keywords(root, parent, flist, output): 298 ret = 0 299 output.write("SCCS Keywords:\n") 300 for f in flist(): 301 with io.open(f, encoding='utf-8', errors='replace') as fh: 302 ret |= Keywords.keywords(fh, output=output) 303 return ret 304 305def wscheck(root, parent, flist, output): 306 ret = 0 307 output.write("white space nits:\n") 308 for f in flist(): 309 with io.open(f, encoding='utf-8', errors='replace') as fh: 310 ret |= WsCheck.wscheck(fh, output=output) 311 return ret 312 313def symlinks(root, parent, flist, output): 314 ret = 0 315 output.write("Symbolic links:\n") 316 for f in flist(): 317 output.write(" "+f+"\n") 318 ret |= 1 319 return ret 320 321def iswinreserved(name): 322 reserved = [ 323 'con', 'prn', 'aux', 'nul', 324 'com1', 'com2', 'com3', 'com4', 'com5', 325 'com6', 'com7', 'com8', 'com9', 'com0', 326 'lpt1', 'lpt2', 'lpt3', 'lpt4', 'lpt5', 327 'lpt6', 'lpt7', 'lpt8', 'lpt9', 'lpt0' ] 328 l = name.lower() 329 for r in reserved: 330 if l == r or l.startswith(r+"."): 331 return True 332 return False 333 334def haswinspecial(name): 335 specials = '<>:"\\|?*' 336 for c in name: 337 if c in specials: 338 return True 339 return False 340 341def winnames(root, parent, flist, output): 342 ret = 0 343 output.write("Illegal filenames (Windows):\n") 344 for f in flist(): 345 if haswinspecial(f): 346 output.write(" "+f+": invalid character in name\n") 347 ret |= 1 348 continue 349 350 parts = f.split('/') 351 for p in parts: 352 if iswinreserved(p): 353 output.write(" "+f+": reserved file name\n") 354 ret |= 1 355 break 356 357 return ret 358 359def run_checks(root, parent, cmds, scmds, paths='', opts={}): 360 """Run the checks given in 'cmds', expected to have well-known signatures, 361 and report results for any which fail. 362 363 Return failure if any of them did. 364 365 NB: the function name of the commands passed in is used to name the NOT 366 file which excepts files from them.""" 367 368 ret = 0 369 370 for cmd in cmds: 371 s = StringIO() 372 373 exclude = not_check(root, cmd.__name__) 374 result = cmd(root, parent, gen_files(root, parent, paths, exclude), 375 output=s) 376 ret |= result 377 378 if result != 0: 379 print(s.getvalue()) 380 381 for cmd in scmds: 382 s = StringIO() 383 384 exclude = not_check(root, cmd.__name__) 385 result = cmd(root, parent, gen_links(root, parent, paths, exclude), 386 output=s) 387 ret |= result 388 389 if result != 0: 390 print(s.getvalue()) 391 392 return ret 393 394def nits(root, parent, paths): 395 cmds = [copyright, 396 cstyle, 397 hdrchk, 398 jstyle, 399 keywords, 400 manlint, 401 mapfilechk, 402 winnames, 403 wscheck] 404 scmds = [symlinks] 405 run_checks(root, parent, cmds, scmds, paths) 406 407def pbchk(root, parent, paths): 408 cmds = [comchk, 409 copyright, 410 cstyle, 411 hdrchk, 412 jstyle, 413 keywords, 414 manlint, 415 mapfilechk, 416 winnames, 417 wscheck] 418 scmds = [symlinks] 419 run_checks(root, parent, cmds, scmds) 420 421def main(cmd, args): 422 parent_branch = None 423 checkname = None 424 425 try: 426 opts, args = getopt.getopt(args, 'b:c:p:') 427 except getopt.GetoptError as e: 428 sys.stderr.write(str(e) + '\n') 429 sys.stderr.write("Usage: %s [-c check] [-p branch] [path...]\n" % cmd) 430 sys.exit(1) 431 432 for opt, arg in opts: 433 # We accept "-b" as an alias of "-p" for backwards compatibility. 434 if opt == '-p' or opt == '-b': 435 parent_branch = arg 436 elif opt == '-c': 437 checkname = arg 438 439 if not parent_branch: 440 parent_branch = git_parent_branch(git_branch()) 441 442 if checkname is None: 443 if cmd == 'git-pbchk': 444 checkname = 'pbchk' 445 else: 446 checkname = 'nits' 447 448 if checkname == 'pbchk': 449 if args: 450 sys.stderr.write("only complete workspaces may be pbchk'd\n"); 451 sys.exit(1) 452 pbchk(git_root(), parent_branch, None) 453 elif checkname == 'nits': 454 nits(git_root(), parent_branch, args) 455 else: 456 run_checks(git_root(), parent_branch, [eval(checkname)], args) 457 458if __name__ == '__main__': 459 try: 460 main(os.path.basename(sys.argv[0]), sys.argv[1:]) 461 except GitError as e: 462 sys.stderr.write("failed to run git:\n %s\n" % str(e)) 463 sys.exit(1) 464