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