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