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