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