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