Unleashed v1.4
[unleashed.git] / usr / src / tools / scripts / git-pbchk.py
blobe6541545acf1056359ed6d15b9549d791bc13fb4
1 #!@PYTHON@
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
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 # Copyright 2018 OmniOS Community Edition (OmniOSce) Association.
27 from __future__ import print_function
29 import getopt
30 import io
31 import os
32 import re
33 import subprocess
34 import sys
35 import tempfile
37 if sys.version_info[0] < 3:
38 from cStringIO import StringIO
39 else:
40 from io import StringIO
43 # Adjust the load path based on our location and the version of python into
44 # which it is being loaded. This assumes the normal onbld directory
45 # structure, where we are in bin/ and the modules are in
46 # lib/python(version)?/onbld/Scm/. If that changes so too must this.
48 sys.path.insert(1, os.path.join(os.path.dirname(__file__), "..", "lib",
49 "python%d.%d" % sys.version_info[:2]))
52 # Add the relative path to usr/src/tools to the load path, such that when run
53 # from the source tree we use the modules also within the source tree.
55 sys.path.insert(2, os.path.join(os.path.dirname(__file__), ".."))
57 from onbld.Scm import Ignore
58 from onbld.Checks import Comments, Copyright, CStyle, HdrChk, WsCheck
59 from onbld.Checks import Keywords, ManLint, Mapfile, SpellCheck
62 class GitError(Exception):
63 pass
65 def git(command):
66 """Run a command and return a stream containing its stdout (and write its
67 stderr to its stdout)"""
69 if type(command) != list:
70 command = command.split()
72 command = ["git"] + command
74 try:
75 tmpfile = tempfile.TemporaryFile(prefix="git-nits", mode="w+b")
76 except EnvironmentError as e:
77 raise GitError("Could not create temporary file: %s\n" % e)
79 try:
80 p = subprocess.Popen(command,
81 stdout=tmpfile,
82 stderr=subprocess.PIPE)
83 except OSError as e:
84 raise GitError("could not execute %s: %s\n" % (command, e))
86 err = p.wait()
87 if err != 0:
88 raise GitError(p.stderr.read())
90 tmpfile.seek(0)
91 lines = []
92 for l in tmpfile:
93 lines.append(l.decode('utf-8', 'replace'))
94 return lines
97 def git_root():
98 """Return the root of the current git workspace"""
100 p = git('rev-parse --git-dir')
102 return os.path.abspath(os.path.join(dir, os.path.pardir))
104 def git_branch():
105 """Return the current git branch"""
107 p = git('branch')
109 for elt in p:
110 if elt[0] == '*':
111 if elt.endswith('(no branch)'):
112 return None
113 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'
139 def git_comments(parent):
140 """Return a list of any checkin comments on this git branch"""
142 p = git('log --pretty=tformat:%%B:SEP: %s..' % parent)
144 if not p:
145 sys.stderr.write("Failed getting git comments\n")
146 sys.exit(err)
148 return [x.strip() for x in p if x != ':SEP:\n']
150 def git_file_list(parent, paths=None):
151 """Return the set of files which have ever changed on this branch.
153 NB: This includes files which no longer exist, or no longer actually
154 differ."""
156 p = git("log --name-only --pretty=format: %s.. %s" %
157 (parent, ' '.join(paths)))
159 if not p:
160 sys.stderr.write("Failed building file-list from git\n")
161 sys.exit(err)
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())
168 return ret
170 def not_check(root, cmd):
171 """Return a function which returns True if a file given as an argument
172 should be excluded from the check named by 'cmd'"""
174 ignorefiles = list(filter(os.path.exists,
175 [os.path.join(root, ".git", "%s.NOT" % cmd),
176 os.path.join(root, "exception_lists", cmd)]))
177 return Ignore.ignore(root, ignorefiles)
179 def gen_files(root, parent, paths, exclude):
180 """Return a function producing file names, relative to the current
181 directory, of any file changed on this branch (limited to 'paths' if
182 requested), and excluding files for which exclude returns a true value """
184 # Taken entirely from Python 2.6's os.path.relpath which we would use if we
185 # could.
186 def relpath(path, here):
187 c = os.path.abspath(os.path.join(root, path)).split(os.path.sep)
188 s = os.path.abspath(here).split(os.path.sep)
189 l = len(os.path.commonprefix((s, c)))
190 return os.path.join(*[os.path.pardir] * (len(s)-l) + c[l:])
192 def ret(select=None):
193 if not select:
194 select = lambda x: True
196 for abspath in git_file_list(parent, paths):
197 path = relpath(abspath, '.')
198 try:
199 res = git("diff %s HEAD %s" % (parent, path))
200 except GitError as e:
201 # This ignores all the errors that can be thrown. Usually, this
202 # means that git returned non-zero because the file doesn't
203 # exist, but it could also fail if git can't create a new file
204 # or it can't be executed. Such errors are 1) unlikely, and 2)
205 # will be caught by other invocations of git().
206 continue
207 empty = not res
208 if (os.path.isfile(path) and not empty and
209 select(path) and not exclude(abspath)):
210 yield path
211 return ret
213 def comchk(root, parent, flist, output):
214 output.write("Comments:\n")
216 return Comments.comchk(git_comments(parent), check_db=True,
217 output=output)
220 def mapfilechk(root, parent, flist, output):
221 ret = 0
223 # We are interested in examining any file that has the following
224 # in its final path segment:
225 # - Contains the word 'mapfile'
226 # - Begins with 'map.'
227 # - Ends with '.map'
228 # We don't want to match unless these things occur in final path segment
229 # because directory names with these strings don't indicate a mapfile.
230 # We also ignore files with suffixes that tell us that the files
231 # are not mapfiles.
232 MapfileRE = re.compile(r'.*((mapfile[^/]*)|(/map\.+[^/]*)|(\.map))$',
233 re.IGNORECASE)
234 NotMapSuffixRE = re.compile(r'.*\.[ch]$', re.IGNORECASE)
236 output.write("Mapfile comments:\n")
238 for f in flist(lambda x: MapfileRE.match(x) and not
239 NotMapSuffixRE.match(x)):
240 with io.open(f, encoding='utf-8', errors='replace') as fh:
241 ret |= Mapfile.mapfilechk(fh, output=output)
242 return ret
245 def copyright(root, parent, flist, output):
246 ret = 0
247 output.write("Copyrights:\n")
248 for f in flist():
249 with io.open(f, encoding='utf-8', errors='replace') as fh:
250 ret |= Copyright.copyright(fh, output=output)
251 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 with io.open(f, encoding='utf-8', errors='replace') as fh:
258 ret |= HdrChk.hdrchk(fh, lenient=True, output=output)
259 return ret
262 def cstyle(root, parent, flist, output):
263 ret = 0
264 output.write("C style:\n")
265 for f in flist(lambda x: x.endswith('.c') or x.endswith('.h')):
266 with io.open(f, encoding='utf-8', errors='replace') as fh:
267 ret |= CStyle.cstyle(fh, output=output, picky=True,
268 check_posix_types=True,
269 check_continuation=True)
270 return ret
273 def manlint(root, parent, flist, output):
274 ret = 0
275 output.write("Man page format/spelling:\n")
276 ManfileRE = re.compile(r'.*\.[0-9][a-z]*$', re.IGNORECASE)
277 for f in flist(lambda x: ManfileRE.match(x)):
278 with io.open(f, encoding='utf-8', errors='replace') as fh:
279 ret |= ManLint.manlint(fh, output=output, picky=True)
280 ret |= SpellCheck.spellcheck(fh, output=output)
281 return ret
283 def keywords(root, parent, flist, output):
284 ret = 0
285 output.write("SCCS Keywords:\n")
286 for f in flist():
287 with io.open(f, encoding='utf-8', errors='replace') as fh:
288 ret |= Keywords.keywords(fh, output=output)
289 return ret
291 def wscheck(root, parent, flist, output):
292 ret = 0
293 output.write("white space nits:\n")
294 for f in flist():
295 with io.open(f, encoding='utf-8', errors='replace') as fh:
296 ret |= WsCheck.wscheck(fh, output=output)
297 return ret
299 def run_checks(root, parent, cmds, paths='', opts={}):
300 """Run the checks given in 'cmds', expected to have well-known signatures,
301 and report results for any which fail.
303 Return failure if any of them did.
305 NB: the function name of the commands passed in is used to name the NOT
306 file which excepts files from them."""
308 ret = 0
310 for cmd in cmds:
311 s = StringIO()
313 exclude = not_check(root, cmd.__name__)
314 result = cmd(root, parent, gen_files(root, parent, paths, exclude),
315 output=s)
316 ret |= result
318 if result != 0:
319 print(s.getvalue())
321 return ret
323 def nits(root, parent, paths):
324 cmds = [copyright,
325 cstyle,
326 hdrchk,
327 keywords,
328 manlint,
329 mapfilechk,
330 wscheck]
331 run_checks(root, parent, cmds, paths)
333 def pbchk(root, parent, paths):
334 cmds = [comchk,
335 copyright,
336 cstyle,
337 hdrchk,
338 keywords,
339 manlint,
340 mapfilechk,
341 wscheck]
342 run_checks(root, parent, cmds)
344 def main(cmd, args):
345 parent_branch = None
346 checkname = None
348 try:
349 opts, args = getopt.getopt(args, 'b:p:')
350 except getopt.GetoptError, e:
351 sys.stderr.write(str(e) + '\n')
352 sys.stderr.write("Usage: %s [-p branch] [path...]\n" % cmd)
353 sys.exit(1)
355 for opt, arg in opts:
356 # We accept "-b" as an alias of "-p" for backwards compatibility.
357 if opt == '-p' or opt == '-b':
358 parent_branch = arg
360 if not parent_branch:
361 parent_branch = git_parent_branch(git_branch())
363 if checkname is None:
364 if cmd == 'git-pbchk':
365 checkname = 'pbchk'
367 if checkname == 'pbchk':
368 if args:
369 sys.stderr.write("only complete workspaces may be pbchk'd\n");
370 sys.exit(1)
371 pbchk(git_root(), parent_branch, None)
372 else:
373 run_checks(git_root(), parent_branch, [eval(checkname)], args)
375 if __name__ == '__main__':
376 try:
377 main(os.path.basename(sys.argv[0]), sys.argv[1:])
378 except GitError as e:
379 sys.stderr.write("failed to run git:\n %s\n" % str(e))
380 sys.exit(1)