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