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
37 if sys
.version_info
[0] < 3:
38 from cStringIO
import StringIO
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):
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
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
)
80 p
= subprocess
.Popen(command
,
82 stderr
=subprocess
.PIPE
)
84 raise GitError("could not execute %s: %s\n" % (command
, e
))
88 raise GitError(p
.stderr
.read())
93 lines
.append(l
.decode('utf-8', 'replace'))
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
))
105 """Return the current git branch"""
111 if elt
.endswith('(no branch)'):
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."""
124 p
= git(["for-each-ref", "--format=%(refname:short) %(upstream:short)",
128 sys
.stderr
.write("Failed finding git parent branch\n")
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()
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
)
145 sys
.stderr
.write("Failed getting git comments\n")
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
156 p
= git("log --name-only --pretty=format: %s.. %s" %
157 (parent
, ' '.join(paths
)))
160 sys
.stderr
.write("Failed building file-list from git\n")
165 if fname
and not fname
.isspace() and fname
not in ret
:
166 ret
.add(fname
.strip())
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
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):
194 select
= lambda x
: True
196 for abspath
in git_file_list(parent
, paths
):
197 path
= relpath(abspath
, '.')
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().
208 if (os
.path
.isfile(path
) and not empty
and
209 select(path
) and not exclude(abspath
)):
213 def comchk(root
, parent
, flist
, output
):
214 output
.write("Comments:\n")
216 return Comments
.comchk(git_comments(parent
), check_db
=True,
220 def mapfilechk(root
, parent
, flist
, output
):
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.'
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
232 MapfileRE
= re
.compile(r
'.*((mapfile[^/]*)|(/map\.+[^/]*)|(\.map))$',
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
)
245 def copyright(root
, parent
, flist
, output
):
247 output
.write("Copyrights:\n")
249 with io
.open(f
, encoding
='utf-8', errors
='replace') as fh
:
250 ret |
= Copyright
.copyright(fh
, output
=output
)
253 def hdrchk(root
, parent
, flist
, output
):
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
)
262 def cstyle(root
, parent
, flist
, output
):
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)
273 def manlint(root
, parent
, flist
, output
):
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
)
283 def keywords(root
, parent
, flist
, output
):
285 output
.write("SCCS Keywords:\n")
287 with io
.open(f
, encoding
='utf-8', errors
='replace') as fh
:
288 ret |
= Keywords
.keywords(fh
, output
=output
)
291 def wscheck(root
, parent
, flist
, output
):
293 output
.write("white space nits:\n")
295 with io
.open(f
, encoding
='utf-8', errors
='replace') as fh
:
296 ret |
= WsCheck
.wscheck(fh
, output
=output
)
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."""
313 exclude
= not_check(root
, cmd
.__name
__)
314 result
= cmd(root
, parent
, gen_files(root
, parent
, paths
, exclude
),
323 def nits(root
, parent
, paths
):
331 run_checks(root
, parent
, cmds
, paths
)
333 def pbchk(root
, parent
, paths
):
342 run_checks(root
, parent
, cmds
)
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
)
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':
360 if not parent_branch
:
361 parent_branch
= git_parent_branch(git_branch())
363 if checkname
is None:
364 if cmd
== 'git-pbchk':
367 if checkname
== 'pbchk':
369 sys
.stderr
.write("only complete workspaces may be pbchk'd\n");
371 pbchk(git_root(), parent_branch
, None)
373 run_checks(git_root(), parent_branch
, [eval(checkname
)], args
)
375 if __name__
== '__main__':
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
))