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