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