xref: /illumos-gate/usr/src/tools/scripts/git-pbchk.py (revision 04e56356520b98d5a93c496b10f02530bb6647e0)
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) 2015, 2016 by Delphix. All rights reserved.
22# Copyright 2016 Nexenta Systems, Inc.
23# Copyright 2018 Joyent, Inc.
24#
25
26import getopt
27import os
28import re
29import subprocess
30import sys
31import tempfile
32
33from cStringIO import StringIO
34
35#
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.
40#
41sys.path.insert(1, os.path.join(os.path.dirname(__file__), "..", "lib",
42                                "python%d.%d" % sys.version_info[:2]))
43
44#
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.
47#
48sys.path.insert(2, os.path.join(os.path.dirname(__file__), ".."))
49
50from onbld.Scm import Ignore
51from onbld.Checks import Comments, Copyright, CStyle, HdrChk, WsCheck
52from onbld.Checks import JStyle, Keywords, ManLint, Mapfile, SpellCheck
53
54
55class GitError(Exception):
56    pass
57
58def git(command):
59    """Run a command and return a stream containing its stdout (and write its
60    stderr to its stdout)"""
61
62    if type(command) != list:
63        command = command.split()
64
65    command = ["git"] + command
66
67    try:
68        tmpfile = tempfile.TemporaryFile(prefix="git-nits")
69    except EnvironmentError, e:
70        raise GitError("Could not create temporary file: %s\n" % e)
71
72    try:
73        p = subprocess.Popen(command,
74                             stdout=tmpfile,
75                             stderr=subprocess.PIPE)
76    except OSError, e:
77        raise GitError("could not execute %s: %s\n" % (command, e))
78
79    err = p.wait()
80    if err != 0:
81        raise GitError(p.stderr.read())
82
83    tmpfile.seek(0)
84    return tmpfile
85
86
87def git_root():
88    """Return the root of the current git workspace"""
89
90    p = git('rev-parse --git-dir')
91
92    if not p:
93        sys.stderr.write("Failed finding git workspace\n")
94        sys.exit(err)
95
96    return os.path.abspath(os.path.join(p.readlines()[0],
97                                        os.path.pardir))
98
99
100def git_branch():
101    """Return the current git branch"""
102
103    p = git('branch')
104
105    if not p:
106        sys.stderr.write("Failed finding git branch\n")
107        sys.exit(err)
108
109    for elt in p:
110        if elt[0] == '*':
111            if elt.endswith('(no branch)'):
112                return None
113            return elt.split()[1]
114
115
116def git_parent_branch(branch):
117    """Return the parent of the current git branch.
118
119    If this branch tracks a remote branch, return the remote branch which is
120    tracked.  If not, default to origin/master."""
121
122    if not branch:
123        return None
124
125    p = git(["for-each-ref", "--format=%(refname:short) %(upstream:short)",
126            "refs/heads/"])
127
128    if not p:
129        sys.stderr.write("Failed finding git parent branch\n")
130        sys.exit(err)
131
132    for line in p:
133        # Git 1.7 will leave a ' ' trailing any non-tracking branch
134        if ' ' in line and not line.endswith(' \n'):
135            local, remote = line.split()
136            if local == branch:
137                return remote
138    return 'origin/master'
139
140
141def git_comments(parent):
142    """Return a list of any checkin comments on this git branch"""
143
144    p = git('log --pretty=tformat:%%B:SEP: %s..' % parent)
145
146    if not p:
147        sys.stderr.write("Failed getting git comments\n")
148        sys.exit(err)
149
150    return [x.strip() for x in p.readlines() if x != ':SEP:\n']
151
152
153def git_file_list(parent, paths=None):
154    """Return the set of files which have ever changed on this branch.
155
156    NB: This includes files which no longer exist, or no longer actually
157    differ."""
158
159    p = git("log --name-only --pretty=format: %s.. %s" %
160             (parent, ' '.join(paths)))
161
162    if not p:
163        sys.stderr.write("Failed building file-list from git\n")
164        sys.exit(err)
165
166    ret = set()
167    for fname in p:
168        if fname and not fname.isspace() and fname not in ret:
169            ret.add(fname.strip())
170
171    return ret
172
173
174def not_check(root, cmd):
175    """Return a function which returns True if a file given as an argument
176    should be excluded from the check named by 'cmd'"""
177
178    ignorefiles = filter(os.path.exists,
179                         [os.path.join(root, ".git", "%s.NOT" % cmd),
180                          os.path.join(root, "exception_lists", cmd)])
181    return Ignore.ignore(root, ignorefiles)
182
183
184def gen_files(root, parent, paths, exclude):
185    """Return a function producing file names, relative to the current
186    directory, of any file changed on this branch (limited to 'paths' if
187    requested), and excluding files for which exclude returns a true value """
188
189    # Taken entirely from Python 2.6's os.path.relpath which we would use if we
190    # could.
191    def relpath(path, here):
192        c = os.path.abspath(os.path.join(root, path)).split(os.path.sep)
193        s = os.path.abspath(here).split(os.path.sep)
194        l = len(os.path.commonprefix((s, c)))
195        return os.path.join(*[os.path.pardir] * (len(s)-l) + c[l:])
196
197    def ret(select=None):
198        if not select:
199            select = lambda x: True
200
201        for abspath in git_file_list(parent, paths):
202            path = relpath(abspath, '.')
203            try:
204                res = git("diff %s HEAD %s" % (parent, path))
205            except GitError, e:
206                # This ignores all the errors that can be thrown. Usually, this
207                # means that git returned non-zero because the file doesn't
208                # exist, but it could also fail if git can't create a new file
209                # or it can't be executed.  Such errors are 1) unlikely, and 2)
210                # will be caught by other invocations of git().
211                continue
212            empty = not res.readline()
213            if (os.path.isfile(path) and not empty and
214                select(path) and not exclude(abspath)):
215                yield path
216    return ret
217
218
219def 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
226def 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
252def 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
262def 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
272def 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
284def 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
294def 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
305def 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
314def wscheck(root, parent, flist, output):
315    ret = 0
316    output.write("white space nits:\n")
317    for f in flist():
318        fh = open(f, 'r')
319        ret |= WsCheck.wscheck(fh, output=output)
320        fh.close()
321    return ret
322
323def run_checks(root, parent, cmds, paths='', opts={}):
324    """Run the checks given in 'cmds', expected to have well-known signatures,
325    and report results for any which fail.
326
327    Return failure if any of them did.
328
329    NB: the function name of the commands passed in is used to name the NOT
330    file which excepts files from them."""
331
332    ret = 0
333
334    for cmd in cmds:
335        s = StringIO()
336
337        exclude = not_check(root, cmd.func_name)
338        result = cmd(root, parent, gen_files(root, parent, paths, exclude),
339                     output=s)
340        ret |= result
341
342        if result != 0:
343            print s.getvalue()
344
345    return ret
346
347
348def nits(root, parent, paths):
349    cmds = [copyright,
350            cstyle,
351            hdrchk,
352            jstyle,
353            keywords,
354            manlint,
355            mapfilechk,
356	    wscheck]
357    run_checks(root, parent, cmds, paths)
358
359
360def pbchk(root, parent, paths):
361    cmds = [comchk,
362            copyright,
363            cstyle,
364            hdrchk,
365            jstyle,
366            keywords,
367            manlint,
368            mapfilechk,
369	    wscheck]
370    run_checks(root, parent, cmds)
371
372
373def main(cmd, args):
374    parent_branch = None
375    checkname = None
376
377    try:
378        opts, args = getopt.getopt(args, 'b:c:p:')
379    except getopt.GetoptError, e:
380        sys.stderr.write(str(e) + '\n')
381        sys.stderr.write("Usage: %s [-c check] [-p branch] [path...]\n" % cmd)
382        sys.exit(1)
383
384    for opt, arg in opts:
385        # We accept "-b" as an alias of "-p" for backwards compatibility.
386        if opt == '-p' or opt == '-b':
387            parent_branch = arg
388        elif opt == '-c':
389            checkname = arg
390
391    if not parent_branch:
392        parent_branch = git_parent_branch(git_branch())
393
394    if checkname is None:
395        if cmd == 'git-pbchk':
396            checkname = 'pbchk'
397        else:
398            checkname = 'nits'
399
400    if checkname == 'pbchk':
401        if args:
402            sys.stderr.write("only complete workspaces may be pbchk'd\n");
403            sys.exit(1)
404        pbchk(git_root(), parent_branch, None)
405    elif checkname == 'nits':
406        nits(git_root(), parent_branch, args)
407    else:
408        run_checks(git_root(), parent_branch, [eval(checkname)], args)
409
410if __name__ == '__main__':
411    try:
412        main(os.path.basename(sys.argv[0]), sys.argv[1:])
413    except GitError, e:
414        sys.stderr.write("failed to run git:\n %s\n" % str(e))
415        sys.exit(1)
416