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 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 (c) 2019, Joyent, Inc. 24# Copyright 2019 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): 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 # Taken entirely from Python 2.6's os.path.relpath which we would use if we 184 # could. 185 def relpath(path, here): 186 c = os.path.abspath(os.path.join(root, path)).split(os.path.sep) 187 s = os.path.abspath(here).split(os.path.sep) 188 l = len(os.path.commonprefix((s, c))) 189 return os.path.join(*[os.path.pardir] * (len(s)-l) + c[l:]) 190 191 def ret(select=None): 192 if not select: 193 select = lambda x: True 194 195 for abspath in git_file_list(parent, paths): 196 path = relpath(abspath, '.') 197 try: 198 res = git("diff %s HEAD %s" % (parent, path)) 199 except GitError as e: 200 # This ignores all the errors that can be thrown. Usually, this 201 # means that git returned non-zero because the file doesn't 202 # exist, but it could also fail if git can't create a new file 203 # or it can't be executed. Such errors are 1) unlikely, and 2) 204 # will be caught by other invocations of git(). 205 continue 206 empty = not res 207 if (os.path.isfile(path) and not empty and 208 select(path) and not exclude(abspath)): 209 yield path 210 return ret 211 212def comchk(root, parent, flist, output): 213 output.write("Comments:\n") 214 215 return Comments.comchk(git_comments(parent), check_db=True, 216 output=output) 217 218 219def mapfilechk(root, parent, flist, output): 220 ret = 0 221 222 # We are interested in examining any file that has the following 223 # in its final path segment: 224 # - Contains the word 'mapfile' 225 # - Begins with 'map.' 226 # - Ends with '.map' 227 # We don't want to match unless these things occur in final path segment 228 # because directory names with these strings don't indicate a mapfile. 229 # We also ignore files with suffixes that tell us that the files 230 # are not mapfiles. 231 MapfileRE = re.compile(r'.*((mapfile[^/]*)|(/map\.+[^/]*)|(\.map))$', 232 re.IGNORECASE) 233 NotMapSuffixRE = re.compile(r'.*\.[ch]$', re.IGNORECASE) 234 235 output.write("Mapfile comments:\n") 236 237 for f in flist(lambda x: MapfileRE.match(x) and not 238 NotMapSuffixRE.match(x)): 239 with io.open(f, encoding='utf-8', errors='replace') as fh: 240 ret |= Mapfile.mapfilechk(fh, output=output) 241 return ret 242 243def copyright(root, parent, flist, output): 244 ret = 0 245 output.write("Copyrights:\n") 246 for f in flist(): 247 with io.open(f, encoding='utf-8', errors='replace') as fh: 248 ret |= Copyright.copyright(fh, output=output) 249 return ret 250 251def hdrchk(root, parent, flist, output): 252 ret = 0 253 output.write("Header format:\n") 254 for f in flist(lambda x: x.endswith('.h')): 255 with io.open(f, encoding='utf-8', errors='replace') as fh: 256 ret |= HdrChk.hdrchk(fh, lenient=True, output=output) 257 return ret 258 259def cstyle(root, parent, flist, output): 260 ret = 0 261 output.write("C style:\n") 262 for f in flist(lambda x: x.endswith('.c') or x.endswith('.h')): 263 with io.open(f, mode='rb') as fh: 264 ret |= CStyle.cstyle(fh, output=output, picky=True, 265 check_posix_types=True, 266 check_continuation=True) 267 return ret 268 269def jstyle(root, parent, flist, output): 270 ret = 0 271 output.write("Java style:\n") 272 for f in flist(lambda x: x.endswith('.java')): 273 with io.open(f, encoding='utf-8', errors='replace') as fh: 274 ret |= JStyle.jstyle(fh, output=output, picky=True) 275 return ret 276 277def manlint(root, parent, flist, output): 278 ret = 0 279 output.write("Man page format/spelling:\n") 280 ManfileRE = re.compile(r'.*\.[0-9][a-z]*$', re.IGNORECASE) 281 for f in flist(lambda x: ManfileRE.match(x)): 282 with io.open(f, mode='rb') as fh: 283 ret |= ManLint.manlint(fh, output=output, picky=True) 284 ret |= SpellCheck.spellcheck(fh, output=output) 285 return ret 286 287def keywords(root, parent, flist, output): 288 ret = 0 289 output.write("SCCS Keywords:\n") 290 for f in flist(): 291 with io.open(f, encoding='utf-8', errors='replace') as fh: 292 ret |= Keywords.keywords(fh, output=output) 293 return ret 294 295def wscheck(root, parent, flist, output): 296 ret = 0 297 output.write("white space nits:\n") 298 for f in flist(): 299 with io.open(f, encoding='utf-8', errors='replace') as fh: 300 ret |= WsCheck.wscheck(fh, output=output) 301 return ret 302 303def run_checks(root, parent, cmds, paths='', opts={}): 304 """Run the checks given in 'cmds', expected to have well-known signatures, 305 and report results for any which fail. 306 307 Return failure if any of them did. 308 309 NB: the function name of the commands passed in is used to name the NOT 310 file which excepts files from them.""" 311 312 ret = 0 313 314 for cmd in cmds: 315 s = StringIO() 316 317 exclude = not_check(root, cmd.__name__) 318 result = cmd(root, parent, gen_files(root, parent, paths, exclude), 319 output=s) 320 ret |= result 321 322 if result != 0: 323 print(s.getvalue()) 324 325 return ret 326 327def nits(root, parent, paths): 328 cmds = [copyright, 329 cstyle, 330 hdrchk, 331 jstyle, 332 keywords, 333 manlint, 334 mapfilechk, 335 wscheck] 336 run_checks(root, parent, cmds, paths) 337 338def pbchk(root, parent, paths): 339 cmds = [comchk, 340 copyright, 341 cstyle, 342 hdrchk, 343 jstyle, 344 keywords, 345 manlint, 346 mapfilechk, 347 wscheck] 348 run_checks(root, parent, cmds) 349 350def main(cmd, args): 351 parent_branch = None 352 checkname = None 353 354 try: 355 opts, args = getopt.getopt(args, 'b:c:p:') 356 except getopt.GetoptError as e: 357 sys.stderr.write(str(e) + '\n') 358 sys.stderr.write("Usage: %s [-c check] [-p branch] [path...]\n" % cmd) 359 sys.exit(1) 360 361 for opt, arg in opts: 362 # We accept "-b" as an alias of "-p" for backwards compatibility. 363 if opt == '-p' or opt == '-b': 364 parent_branch = arg 365 elif opt == '-c': 366 checkname = arg 367 368 if not parent_branch: 369 parent_branch = git_parent_branch(git_branch()) 370 371 if checkname is None: 372 if cmd == 'git-pbchk': 373 checkname = 'pbchk' 374 else: 375 checkname = 'nits' 376 377 if checkname == 'pbchk': 378 if args: 379 sys.stderr.write("only complete workspaces may be pbchk'd\n"); 380 sys.exit(1) 381 pbchk(git_root(), parent_branch, None) 382 elif checkname == 'nits': 383 nits(git_root(), parent_branch, args) 384 else: 385 run_checks(git_root(), parent_branch, [eval(checkname)], args) 386 387if __name__ == '__main__': 388 try: 389 main(os.path.basename(sys.argv[0]), sys.argv[1:]) 390 except GitError as e: 391 sys.stderr.write("failed to run git:\n %s\n" % str(e)) 392 sys.exit(1) 393