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