3288 git-pbchk invents blank lines, then complains about them
[illumos-gate.git] / usr / src / tools / scripts / git-pbchk.py
blob656e22f6b4596d641acfaaf869e9dcefcf6842e4
1 #!/usr/bin/python2.6
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.
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.
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.
18 # Copyright (c) 2008, 2010, Oracle and/or its affiliates. All rights reserved.
19 # Copyright 2008, 2012 Richard Lowe
22 import getopt
23 import os
24 import re
25 import subprocess
26 import sys
27 import tempfile
29 from cStringIO import StringIO
31 # This is necessary because, in a fit of pique, we used hg-format ignore lists
32 # for NOT files.
33 from mercurial import ignore
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.
41 sys.path.insert(1, os.path.join(os.path.dirname(__file__), "..", "lib",
42 "python%d.%d" % sys.version_info[:2]))
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.
48 sys.path.insert(2, os.path.join(os.path.dirname(__file__), ".."))
50 from onbld.Checks import Comments, Copyright, CStyle, HdrChk
51 from onbld.Checks import JStyle, Keywords, Mapfile
54 class GitError(Exception):
55 pass
57 def git(command):
58 """Run a command and return a stream containing its stdout (and write its
59 stderr to its stdout)"""
61 if type(command) != list:
62 command = command.split()
64 command = ["git"] + command
66 try:
67 tmpfile = tempfile.TemporaryFile(prefix="git-nits")
68 except EnvironmentError, e:
69 raise GitError("Could not create temporary file: %s\n" % e)
71 try:
72 p = subprocess.Popen(command,
73 stdout=tmpfile,
74 stderr=subprocess.STDOUT)
75 except OSError, e:
76 raise GitError("could not execute %s: %s\n" (command, e))
78 err = p.wait()
79 if err != 0:
80 raise GitError(p.stdout.read())
82 tmpfile.seek(0)
83 return tmpfile
86 def git_root():
87 """Return the root of the current git workspace"""
89 p = git('rev-parse --git-dir')
91 if not p:
92 sys.stderr.write("Failed finding git workspace\n")
93 sys.exit(err)
95 return os.path.abspath(os.path.join(p.readlines()[0],
96 os.path.pardir))
99 def git_branch():
100 """Return the current git branch"""
102 p = git('branch')
104 if not p:
105 sys.stderr.write("Failed finding git branch\n")
106 sys.exit(err)
108 for elt in p:
109 if elt[0] == '*':
110 if elt.endswith('(no branch)'):
111 return None
112 return elt.split()[1]
115 def git_parent_branch(branch):
116 """Return the parent of the current git branch.
118 If this branch tracks a remote branch, return the remote branch which is
119 tracked. If not, default to origin/master."""
121 if not branch:
122 return None
124 p = git("for-each-ref --format=%(refname:short) %(upstream:short) " +
125 "refs/heads/")
127 if not p:
128 sys.stderr.write("Failed finding git parent branch\n")
129 sys.exit(err)
131 for line in p:
132 # Git 1.7 will leave a ' ' trailing any non-tracking branch
133 if ' ' in line and not line.endswith(' \n'):
134 local, remote = line.split()
135 if local == branch:
136 return remote
137 return 'origin/master'
140 def git_comments(parent):
141 """Return a list of any checkin comments on this git branch"""
143 p = git('log --pretty=tformat:%%B:SEP: %s..' % parent)
145 if not p:
146 sys.stderr.write("Failed getting git comments\n")
147 sys.exit(err)
149 return [x.strip() for x in p.readlines() if x != ':SEP:\n']
152 def git_file_list(parent, paths=None):
153 """Return the set of files which have ever changed on this branch.
155 NB: This includes files which no longer exist, or no longer actually
156 differ."""
158 p = git("log --name-only --pretty=format: %s.. %s" %
159 (parent, ' '.join(paths)))
161 if not p:
162 sys.stderr.write("Failed building file-list from git\n")
163 sys.exit(err)
165 ret = set()
166 for fname in p:
167 if fname and not fname.isspace() and fname not in ret:
168 ret.add(fname.strip())
170 return ret
173 def not_check(root, cmd):
174 """Return a function which returns True if a file given as an argument
175 should be excluded from the check named by 'cmd'"""
177 ignorefiles = filter(os.path.exists,
178 [os.path.join(root, ".git", "%s.NOT" % cmd),
179 os.path.join(root, "exception_lists", cmd)])
180 if len(ignorefiles) > 0:
181 return ignore.ignore(root, ignorefiles, sys.stderr.write)
182 else:
183 return lambda x: False
186 def gen_files(root, parent, paths, exclude):
187 """Return a function producing file names, relative to the current
188 directory, of any file changed on this branch (limited to 'paths' if
189 requested), and excluding files for which exclude returns a true value """
191 # Taken entirely from Python 2.6's os.path.relpath which we would use if we
192 # could.
193 def relpath(path, here):
194 c = os.path.abspath(os.path.join(root, path)).split(os.path.sep)
195 s = os.path.abspath(here).split(os.path.sep)
196 l = len(os.path.commonprefix((s, c)))
197 return os.path.join(*[os.path.pardir] * (len(s)-l) + c[l:])
199 def ret(select=None):
200 if not select:
201 select = lambda x: True
203 for f in git_file_list(parent, paths):
204 f = relpath(f, '.')
205 if (os.path.exists(f) and select(f) and not exclude(f)):
206 yield f
207 return ret
210 def comchk(root, parent, flist, output):
211 output.write("Comments:\n")
213 return Comments.comchk(git_comments(parent), check_db=True,
214 output=output)
217 def mapfilechk(root, parent, flist, output):
218 ret = 0
220 # We are interested in examining any file that has the following
221 # in its final path segment:
222 # - Contains the word 'mapfile'
223 # - Begins with 'map.'
224 # - Ends with '.map'
225 # We don't want to match unless these things occur in final path segment
226 # because directory names with these strings don't indicate a mapfile.
227 # We also ignore files with suffixes that tell us that the files
228 # are not mapfiles.
229 MapfileRE = re.compile(r'.*((mapfile[^/]*)|(/map\.+[^/]*)|(\.map))$',
230 re.IGNORECASE)
231 NotMapSuffixRE = re.compile(r'.*\.[ch]$', re.IGNORECASE)
233 output.write("Mapfile comments:\n")
235 for f in flist(lambda x: MapfileRE.match(x) and not
236 NotMapSuffixRE.match(x)):
237 fh = open(f, 'r')
238 ret |= Mapfile.mapfilechk(fh, output=output)
239 fh.close()
240 return ret
243 def copyright(root, parent, flist, output):
244 ret = 0
245 output.write("Copyrights:\n")
246 for f in flist():
247 fh = open(f, 'r')
248 ret |= Copyright.copyright(fh, output=output)
249 fh.close()
250 return ret
253 def hdrchk(root, parent, flist, output):
254 ret = 0
255 output.write("Header format:\n")
256 for f in flist(lambda x: x.endswith('.h')):
257 fh = open(f, 'r')
258 ret |= HdrChk.hdrchk(fh, lenient=True, output=output)
259 fh.close()
260 return ret
263 def cstyle(root, parent, flist, output):
264 ret = 0
265 output.write("C style:\n")
266 for f in flist(lambda x: x.endswith('.c') or x.endswith('.h')):
267 fh = open(f, 'r')
268 ret |= CStyle.cstyle(fh, output=output, picky=True,
269 check_posix_types=True,
270 check_continuation=True)
271 fh.close()
272 return ret
275 def jstyle(root, parent, flist, output):
276 ret = 0
277 output.write("Java style:\n")
278 for f in flist(lambda x: x.endswith('.java')):
279 fh = open(f, 'r')
280 ret |= JStyle.jstyle(fh, output=output, picky=True)
281 fh.close()
282 return ret
285 def keywords(root, parent, flist, output):
286 ret = 0
287 output.write("SCCS Keywords:\n")
288 for f in flist():
289 fh = open(f, 'r')
290 ret |= Keywords.keywords(fh, output=output)
291 fh.close()
292 return ret
295 def run_checks(root, parent, cmds, paths='', opts={}):
296 """Run the checks given in 'cmds', expected to have well-known signatures,
297 and report results for any which fail.
299 Return failure if any of them did.
301 NB: the function name of the commands passed in is used to name the NOT
302 file which excepts files from them."""
304 ret = 0
306 for cmd in cmds:
307 s = StringIO()
309 exclude = not_check(root, cmd.func_name)
310 result = cmd(root, parent, gen_files(root, parent, paths, exclude),
311 output=s)
312 ret |= result
314 if result != 0:
315 print s.getvalue()
317 return ret
320 def nits(root, parent, paths):
321 cmds = [copyright,
322 cstyle,
323 hdrchk,
324 jstyle,
325 keywords,
326 mapfilechk]
327 run_checks(root, parent, cmds, paths)
330 def pbchk(root, parent, paths):
331 cmds = [comchk,
332 copyright,
333 cstyle,
334 hdrchk,
335 jstyle,
336 keywords,
337 mapfilechk]
338 run_checks(root, parent, cmds)
341 def main(cmd, args):
342 parent_branch = None
344 try:
345 opts, args = getopt.getopt(args, 'b:')
346 except getopt.GetoptError, e:
347 sys.stderr.write(str(e) + '\n')
348 sys.stderr.write("Usage: %s [-b branch] [path...]\n" % cmd)
349 sys.exit(1)
351 for opt, arg in opts:
352 if opt == '-b':
353 parent_branch = arg
355 if not parent_branch:
356 parent_branch = git_parent_branch(git_branch())
358 func = nits
359 if cmd == 'git-pbchk':
360 func = pbchk
361 if args:
362 sys.stderr.write("only complete workspaces may be pbchk'd\n");
363 sys.exit(1)
365 func(git_root(), parent_branch, args)
367 if __name__ == '__main__':
368 try:
369 main(os.path.basename(sys.argv[0]), sys.argv[1:])
370 except GitError, e:
371 sys.stderr.write("failed to run git:\n %s\n" % str(e))
372 sys.exit(1)