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