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