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# 21 22import getopt 23import os 24import re 25import subprocess 26import sys 27import tempfile 28 29from cStringIO import StringIO 30 31# This is necessary because, in a fit of pique, we used hg-format ignore lists 32# for NOT files. 33from mercurial import ignore 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# 41sys.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# 48sys.path.insert(2, os.path.join(os.path.dirname(__file__), "..")) 49 50from onbld.Checks import Comments, Copyright, CStyle, HdrChk 51from onbld.Checks import JStyle, Keywords, 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 if fname and not fname.isspace() and fname not in ret: 168 ret.add(fname.strip()) 169 170 return ret 171 172 173def not_check(root, cmd): 174 """Return a function which returns True if a file given as an argument 175 should be excluded from the check named by 'cmd'""" 176 177 ignorefiles = filter(os.path.exists, 178 [os.path.join(root, ".git", "%s.NOT" % cmd), 179 os.path.join(root, "exception_lists", cmd)]) 180 if len(ignorefiles) > 0: 181 return ignore.ignore(root, ignorefiles, sys.stderr.write) 182 else: 183 return lambda x: False 184 185 186def gen_files(root, parent, paths, exclude): 187 """Return a function producing file names, relative to the current 188 directory, of any file changed on this branch (limited to 'paths' if 189 requested), and excluding files for which exclude returns a true value """ 190 191 # Taken entirely from Python 2.6's os.path.relpath which we would use if we 192 # could. 193 def relpath(path, here): 194 c = os.path.abspath(os.path.join(root, path)).split(os.path.sep) 195 s = os.path.abspath(here).split(os.path.sep) 196 l = len(os.path.commonprefix((s, c))) 197 return os.path.join(*[os.path.pardir] * (len(s)-l) + c[l:]) 198 199 def ret(select=None): 200 if not select: 201 select = lambda x: True 202 203 for f in git_file_list(parent, paths): 204 f = relpath(f, '.') 205 if (os.path.exists(f) and select(f) and not exclude(f)): 206 yield f 207 return ret 208 209 210def comchk(root, parent, flist, output): 211 output.write("Comments:\n") 212 213 return Comments.comchk(git_comments(parent), check_db=True, 214 output=output) 215 216 217def mapfilechk(root, parent, flist, output): 218 ret = 0 219 220 # We are interested in examining any file that has the following 221 # in its final path segment: 222 # - Contains the word 'mapfile' 223 # - Begins with 'map.' 224 # - Ends with '.map' 225 # We don't want to match unless these things occur in final path segment 226 # because directory names with these strings don't indicate a mapfile. 227 # We also ignore files with suffixes that tell us that the files 228 # are not mapfiles. 229 MapfileRE = re.compile(r'.*((mapfile[^/]*)|(/map\.+[^/]*)|(\.map))$', 230 re.IGNORECASE) 231 NotMapSuffixRE = re.compile(r'.*\.[ch]$', re.IGNORECASE) 232 233 output.write("Mapfile comments:\n") 234 235 for f in flist(lambda x: MapfileRE.match(x) and not 236 NotMapSuffixRE.match(x)): 237 fh = open(f, 'r') 238 ret |= Mapfile.mapfilechk(fh, output=output) 239 fh.close() 240 return ret 241 242 243def copyright(root, parent, flist, output): 244 ret = 0 245 output.write("Copyrights:\n") 246 for f in flist(): 247 fh = open(f, 'r') 248 ret |= Copyright.copyright(fh, output=output) 249 fh.close() 250 return ret 251 252 253def hdrchk(root, parent, flist, output): 254 ret = 0 255 output.write("Header format:\n") 256 for f in flist(lambda x: x.endswith('.h')): 257 fh = open(f, 'r') 258 ret |= HdrChk.hdrchk(fh, lenient=True, output=output) 259 fh.close() 260 return ret 261 262 263def cstyle(root, parent, flist, output): 264 ret = 0 265 output.write("C style:\n") 266 for f in flist(lambda x: x.endswith('.c') or x.endswith('.h')): 267 fh = open(f, 'r') 268 ret |= CStyle.cstyle(fh, output=output, picky=True, 269 check_posix_types=True, 270 check_continuation=True) 271 fh.close() 272 return ret 273 274 275def jstyle(root, parent, flist, output): 276 ret = 0 277 output.write("Java style:\n") 278 for f in flist(lambda x: x.endswith('.java')): 279 fh = open(f, 'r') 280 ret |= JStyle.jstyle(fh, output=output, picky=True) 281 fh.close() 282 return ret 283 284 285def keywords(root, parent, flist, output): 286 ret = 0 287 output.write("SCCS Keywords:\n") 288 for f in flist(): 289 fh = open(f, 'r') 290 ret |= Keywords.keywords(fh, output=output) 291 fh.close() 292 return ret 293 294 295def run_checks(root, parent, cmds, paths='', opts={}): 296 """Run the checks given in 'cmds', expected to have well-known signatures, 297 and report results for any which fail. 298 299 Return failure if any of them did. 300 301 NB: the function name of the commands passed in is used to name the NOT 302 file which excepts files from them.""" 303 304 ret = 0 305 306 for cmd in cmds: 307 s = StringIO() 308 309 exclude = not_check(root, cmd.func_name) 310 result = cmd(root, parent, gen_files(root, parent, paths, exclude), 311 output=s) 312 ret |= result 313 314 if result != 0: 315 print s.getvalue() 316 317 return ret 318 319 320def nits(root, parent, paths): 321 cmds = [copyright, 322 cstyle, 323 hdrchk, 324 jstyle, 325 keywords, 326 mapfilechk] 327 run_checks(root, parent, cmds, paths) 328 329 330def pbchk(root, parent, paths): 331 cmds = [comchk, 332 copyright, 333 cstyle, 334 hdrchk, 335 jstyle, 336 keywords, 337 mapfilechk] 338 run_checks(root, parent, cmds) 339 340 341def main(cmd, args): 342 parent_branch = None 343 344 try: 345 opts, args = getopt.getopt(args, 'b:') 346 except getopt.GetoptError, e: 347 sys.stderr.write(str(e) + '\n') 348 sys.stderr.write("Usage: %s [-b branch] [path...]\n" % cmd) 349 sys.exit(1) 350 351 for opt, arg in opts: 352 if opt == '-b': 353 parent_branch = arg 354 355 if not parent_branch: 356 parent_branch = git_parent_branch(git_branch()) 357 358 func = nits 359 if cmd == 'git-pbchk': 360 func = pbchk 361 if args: 362 sys.stderr.write("only complete workspaces may be pbchk'd\n"); 363 sys.exit(1) 364 365 func(git_root(), parent_branch, args) 366 367if __name__ == '__main__': 368 try: 369 main(os.path.basename(sys.argv[0]), sys.argv[1:]) 370 except GitError, e: 371 sys.stderr.write("failed to run git:\n %s\n" % str(e)) 372 sys.exit(1) 373