xref: /illumos-gate/usr/src/tools/scripts/git-pbchk.py (revision cbbc171ddfa9cf0cd99cb88bc80ca0e36f07fc27)
13f770aabSAndy Fiddaman#!@TOOLS_PYTHON@ -Es
28bcea973SRichard Lowe#
38bcea973SRichard Lowe#  This program is free software; you can redistribute it and/or modify
48bcea973SRichard Lowe#  it under the terms of the GNU General Public License version 2
58bcea973SRichard Lowe#  as published by the Free Software Foundation.
68bcea973SRichard Lowe#
78bcea973SRichard Lowe#  This program is distributed in the hope that it will be useful,
88bcea973SRichard Lowe#  but WITHOUT ANY WARRANTY; without even the implied warranty of
98bcea973SRichard Lowe#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
108bcea973SRichard Lowe#  GNU General Public License for more details.
118bcea973SRichard Lowe#
128bcea973SRichard Lowe#  You should have received a copy of the GNU General Public License
138bcea973SRichard Lowe#  along with this program; if not, write to the Free Software
148bcea973SRichard Lowe#  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
158bcea973SRichard Lowe#
168bcea973SRichard Lowe
178bcea973SRichard Lowe#
188bcea973SRichard Lowe# Copyright (c) 2008, 2010, Oracle and/or its affiliates. All rights reserved.
198bcea973SRichard Lowe# Copyright 2008, 2012 Richard Lowe
20955eb5e1SGarrett D'Amore# Copyright 2019 Garrett D'Amore <garrett@damore.org>
2193d2a904SPaul Dagnelie# Copyright (c) 2015, 2016 by Delphix. All rights reserved.
2228e2b3adSHans Rosenfeld# Copyright 2016 Nexenta Systems, Inc.
23972282a0SJohn Levon# Copyright (c) 2019, Joyent, Inc.
2413904da8SAndy Fiddaman# Copyright 2021 OmniOS Community Edition (OmniOSce) Association.
259af2fe54SBill Sommerfeld# Copyright 2024 Bill Sommerfeld
26*cbbc171dSiximeow# Copyright 2024 Oxide Computer Company
278bcea973SRichard Lowe#
288bcea973SRichard Lowe
29ca13eaa5SAndy Fiddamanfrom __future__ import print_function
30ca13eaa5SAndy Fiddaman
318bcea973SRichard Loweimport getopt
32ca13eaa5SAndy Fiddamanimport io
338bcea973SRichard Loweimport os
348bcea973SRichard Loweimport re
358bcea973SRichard Loweimport subprocess
368bcea973SRichard Loweimport sys
37ff50e8e5SRichard Loweimport tempfile
3828e88f55SBill Sommerfeldimport textwrap
398bcea973SRichard Lowe
40ca13eaa5SAndy Fiddamanif sys.version_info[0] < 3:
418bcea973SRichard Lowe    from cStringIO import StringIO
42ca13eaa5SAndy Fiddamanelse:
43ca13eaa5SAndy Fiddaman    from io import StringIO
448bcea973SRichard Lowe
458bcea973SRichard Lowe#
468bcea973SRichard Lowe# Adjust the load path based on our location and the version of python into
478bcea973SRichard Lowe# which it is being loaded.  This assumes the normal onbld directory
488bcea973SRichard Lowe# structure, where we are in bin/ and the modules are in
498bcea973SRichard Lowe# lib/python(version)?/onbld/Scm/.  If that changes so too must this.
508bcea973SRichard Lowe#
518bcea973SRichard Lowesys.path.insert(1, os.path.join(os.path.dirname(__file__), "..", "lib",
528bcea973SRichard Lowe                                "python%d.%d" % sys.version_info[:2]))
538bcea973SRichard Lowe
548bcea973SRichard Lowe#
558bcea973SRichard Lowe# Add the relative path to usr/src/tools to the load path, such that when run
568bcea973SRichard Lowe# from the source tree we use the modules also within the source tree.
578bcea973SRichard Lowe#
588bcea973SRichard Lowesys.path.insert(2, os.path.join(os.path.dirname(__file__), ".."))
598bcea973SRichard Lowe
60e5587435SJoshua M. Clulowfrom onbld.Scm import Ignore
614ff15898SGordon Rossfrom onbld.Checks import Comments, Copyright, CStyle, HdrChk, WsCheck
6271af3be3SCody Peter Mellofrom onbld.Checks import JStyle, Keywords, ManLint, Mapfile, SpellCheck
6386d41711SAndy Fiddamanfrom onbld.Checks import ShellLint, PkgFmt
648bcea973SRichard Lowe
658bcea973SRichard Loweclass GitError(Exception):
668bcea973SRichard Lowe    pass
678bcea973SRichard Lowe
688bcea973SRichard Lowedef git(command):
698bcea973SRichard Lowe    """Run a command and return a stream containing its stdout (and write its
708bcea973SRichard Lowe    stderr to its stdout)"""
718bcea973SRichard Lowe
728bcea973SRichard Lowe    if type(command) != list:
738bcea973SRichard Lowe        command = command.split()
748bcea973SRichard Lowe
758bcea973SRichard Lowe    command = ["git"] + command
768bcea973SRichard Lowe
77ff50e8e5SRichard Lowe    try:
78ca13eaa5SAndy Fiddaman        tmpfile = tempfile.TemporaryFile(prefix="git-nits", mode="w+b")
79ca13eaa5SAndy Fiddaman    except EnvironmentError as e:
80ff50e8e5SRichard Lowe        raise GitError("Could not create temporary file: %s\n" % e)
81ff50e8e5SRichard Lowe
82ff50e8e5SRichard Lowe    try:
838bcea973SRichard Lowe        p = subprocess.Popen(command,
84ff50e8e5SRichard Lowe                             stdout=tmpfile,
85380fd671SMatthew Ahrens                             stderr=subprocess.PIPE)
86ca13eaa5SAndy Fiddaman    except OSError as e:
87709afb1dSDillon Amburgey        raise GitError("could not execute %s: %s\n" % (command, e))
888bcea973SRichard Lowe
898bcea973SRichard Lowe    err = p.wait()
908bcea973SRichard Lowe    if err != 0:
91380fd671SMatthew Ahrens        raise GitError(p.stderr.read())
928bcea973SRichard Lowe
93ff50e8e5SRichard Lowe    tmpfile.seek(0)
94ca13eaa5SAndy Fiddaman    lines = []
95ca13eaa5SAndy Fiddaman    for l in tmpfile:
96ca13eaa5SAndy Fiddaman        lines.append(l.decode('utf-8', 'replace'))
97ca13eaa5SAndy Fiddaman    return lines
988bcea973SRichard Lowe
998bcea973SRichard Lowedef git_root():
1008bcea973SRichard Lowe    """Return the root of the current git workspace"""
1018bcea973SRichard Lowe
1029af2fe54SBill Sommerfeld    p = git('rev-parse --show-toplevel')
1039af2fe54SBill Sommerfeld    dir = p[0].strip()
1048bcea973SRichard Lowe
1059af2fe54SBill Sommerfeld    return os.path.abspath(dir)
1068bcea973SRichard Lowe
1078bcea973SRichard Lowedef git_branch():
1088bcea973SRichard Lowe    """Return the current git branch"""
1098bcea973SRichard Lowe
1108bcea973SRichard Lowe    p = git('branch')
1118bcea973SRichard Lowe
1128bcea973SRichard Lowe    for elt in p:
1138bcea973SRichard Lowe        if elt[0] == '*':
1148bcea973SRichard Lowe            if elt.endswith('(no branch)'):
1158bcea973SRichard Lowe                return None
1168bcea973SRichard Lowe            return elt.split()[1]
1178bcea973SRichard Lowe
1188bcea973SRichard Lowedef git_parent_branch(branch):
1198bcea973SRichard Lowe    """Return the parent of the current git branch.
1208bcea973SRichard Lowe
1218bcea973SRichard Lowe    If this branch tracks a remote branch, return the remote branch which is
1228bcea973SRichard Lowe    tracked.  If not, default to origin/master."""
1238bcea973SRichard Lowe
1248bcea973SRichard Lowe    if not branch:
1258bcea973SRichard Lowe        return None
1268bcea973SRichard Lowe
12728e2b3adSHans Rosenfeld    p = git(["for-each-ref", "--format=%(refname:short) %(upstream:short)",
12828e2b3adSHans Rosenfeld            "refs/heads/"])
1298bcea973SRichard Lowe
1308bcea973SRichard Lowe    if not p:
1318bcea973SRichard Lowe        sys.stderr.write("Failed finding git parent branch\n")
132972282a0SJohn Levon        sys.exit(1)
1338bcea973SRichard Lowe
1348bcea973SRichard Lowe    for line in p:
1358bcea973SRichard Lowe        # Git 1.7 will leave a ' ' trailing any non-tracking branch
1368bcea973SRichard Lowe        if ' ' in line and not line.endswith(' \n'):
1378bcea973SRichard Lowe            local, remote = line.split()
1388bcea973SRichard Lowe            if local == branch:
1398bcea973SRichard Lowe                return remote
1408bcea973SRichard Lowe    return 'origin/master'
1418bcea973SRichard Lowe
1423f8945a7SBill Sommerfelddef slices(strlist, sep):
1433f8945a7SBill Sommerfeld    """Yield start & end of each commit within the list of comments"""
1443f8945a7SBill Sommerfeld    low = 0
1453f8945a7SBill Sommerfeld    for i, v in enumerate(strlist):
1463f8945a7SBill Sommerfeld        if v == sep:
1473f8945a7SBill Sommerfeld            yield(low, i)
1483f8945a7SBill Sommerfeld            low = i+1
1493f8945a7SBill Sommerfeld
1503f8945a7SBill Sommerfeld    if low != len(strlist):
1513f8945a7SBill Sommerfeld        yield(low, len(strlist))
1523f8945a7SBill Sommerfeld
1538bcea973SRichard Lowedef git_comments(parent):
1543f8945a7SBill Sommerfeld    """Return the checkin comments for each commit on this git branch,
1553f8945a7SBill Sommerfeld    structured as a list of lists of lines."""
1568bcea973SRichard Lowe
15727495383SRichard Lowe    p = git('log --pretty=tformat:%%B:SEP: %s..' % parent)
1588bcea973SRichard Lowe
1598bcea973SRichard Lowe    if not p:
160972282a0SJohn Levon        sys.stderr.write("No outgoing changesets found - missing -p option?\n");
161972282a0SJohn Levon        sys.exit(1)
1628bcea973SRichard Lowe
1633f8945a7SBill Sommerfeld    return [ [line.strip() for line in p[a:b]]
1643f8945a7SBill Sommerfeld             for (a, b) in slices(p, ':SEP:\n')]
1658bcea973SRichard Lowe
1668bcea973SRichard Lowedef git_file_list(parent, paths=None):
1678bcea973SRichard Lowe    """Return the set of files which have ever changed on this branch.
1688bcea973SRichard Lowe
1698bcea973SRichard Lowe    NB: This includes files which no longer exist, or no longer actually
1708bcea973SRichard Lowe    differ."""
1718bcea973SRichard Lowe
1728bcea973SRichard Lowe    p = git("log --name-only --pretty=format: %s.. %s" %
1738bcea973SRichard Lowe             (parent, ' '.join(paths)))
1748bcea973SRichard Lowe
1758bcea973SRichard Lowe    if not p:
1768bcea973SRichard Lowe        sys.stderr.write("Failed building file-list from git\n")
177972282a0SJohn Levon        sys.exit(1)
1788bcea973SRichard Lowe
1798bcea973SRichard Lowe    ret = set()
1808bcea973SRichard Lowe    for fname in p:
181da88d39fSBill Sommerfeld        fname = fname.strip()
182da88d39fSBill Sommerfeld        if fname and not fname.isspace():
183da88d39fSBill Sommerfeld            ret.add(fname)
1848bcea973SRichard Lowe
185da88d39fSBill Sommerfeld    return sorted(ret)
1868bcea973SRichard Lowe
1878bcea973SRichard Lowedef not_check(root, cmd):
1888bcea973SRichard Lowe    """Return a function which returns True if a file given as an argument
1898bcea973SRichard Lowe    should be excluded from the check named by 'cmd'"""
1908bcea973SRichard Lowe
191ca13eaa5SAndy Fiddaman    ignorefiles = list(filter(os.path.exists,
192c82c4676SGordon Ross                         [os.path.join(root, ".git/info", "%s.NOT" % cmd),
193ca13eaa5SAndy Fiddaman                          os.path.join(root, "exception_lists", cmd)]))
194e5587435SJoshua M. Clulow    return Ignore.ignore(root, ignorefiles)
1958bcea973SRichard Lowe
196955eb5e1SGarrett D'Amoredef gen_files(root, parent, paths, exclude, filter=None):
1978bcea973SRichard Lowe    """Return a function producing file names, relative to the current
1988bcea973SRichard Lowe    directory, of any file changed on this branch (limited to 'paths' if
1998bcea973SRichard Lowe    requested), and excluding files for which exclude returns a true value """
2008bcea973SRichard Lowe
201955eb5e1SGarrett D'Amore    if filter is None:
202955eb5e1SGarrett D'Amore        filter = lambda x: os.path.isfile(x)
203955eb5e1SGarrett D'Amore
2048bcea973SRichard Lowe    def ret(select=None):
2058bcea973SRichard Lowe        if not select:
2068bcea973SRichard Lowe            select = lambda x: True
2078bcea973SRichard Lowe
20838e36c53SJohn Levon        for abspath in git_file_list(parent, paths):
2099af2fe54SBill Sommerfeld            path = os.path.relpath(os.path.join(root, abspath), '.')
21093d2a904SPaul Dagnelie            try:
21138e36c53SJohn Levon                res = git("diff %s HEAD %s" % (parent, path))
212ca13eaa5SAndy Fiddaman            except GitError as e:
21338e36c53SJohn Levon                # This ignores all the errors that can be thrown. Usually, this
21438e36c53SJohn Levon                # means that git returned non-zero because the file doesn't
21538e36c53SJohn Levon                # exist, but it could also fail if git can't create a new file
21638e36c53SJohn Levon                # or it can't be executed.  Such errors are 1) unlikely, and 2)
21738e36c53SJohn Levon                # will be caught by other invocations of git().
21893d2a904SPaul Dagnelie                continue
219ca13eaa5SAndy Fiddaman            empty = not res
220955eb5e1SGarrett D'Amore            if (filter(path) and not empty and
22138e36c53SJohn Levon                select(path) and not exclude(abspath)):
22238e36c53SJohn Levon                yield path
2238bcea973SRichard Lowe    return ret
2248bcea973SRichard Lowe
225955eb5e1SGarrett D'Amoredef gen_links(root, parent, paths, exclude):
226955eb5e1SGarrett D'Amore    """Return a function producing symbolic link names, relative to the current
227955eb5e1SGarrett D'Amore    directory, of any file changed on this branch (limited to 'paths' if
228955eb5e1SGarrett D'Amore    requested), and excluding files for which exclude returns a true value """
229955eb5e1SGarrett D'Amore
230955eb5e1SGarrett D'Amore    return gen_files(root, parent, paths, exclude, lambda x: os.path.islink(x))
231955eb5e1SGarrett D'Amore
23228e88f55SBill Sommerfelddef gen_none(root, parent, paths, exclude):
23328e88f55SBill Sommerfeld    """ Return a function returning the empty list """
23428e88f55SBill Sommerfeld    return lambda x: []
23528e88f55SBill Sommerfeld
23628e88f55SBill Sommerfeld# The list of possible checks.   Each is recorded as two-function pair; the
23728e88f55SBill Sommerfeld# first is the actual checker, and the second is the generator which creates
23828e88f55SBill Sommerfeld# the list of things that the checker works on.
23928e88f55SBill Sommerfeld
24028e88f55SBill Sommerfeldchecks = {}
24128e88f55SBill Sommerfeldnits_checks = []
24228e88f55SBill Sommerfeldall_checks = []
24328e88f55SBill Sommerfeld
24428e88f55SBill Sommerfelddef add_check(fn, gen):
24528e88f55SBill Sommerfeld    """ Define a checker and add it to the appropriate lists """
24628e88f55SBill Sommerfeld    name = fn.__name__
24728e88f55SBill Sommerfeld    if fn.__doc__ is None:
24828e88f55SBill Sommerfeld        raise ValueError('Check function lacks a documentation string',
24928e88f55SBill Sommerfeld                         name)
25028e88f55SBill Sommerfeld    checks[name] = (fn, gen)
25128e88f55SBill Sommerfeld    all_checks.append(name)
25228e88f55SBill Sommerfeld    if gen != gen_none:
25328e88f55SBill Sommerfeld        nits_checks.append(name)
25428e88f55SBill Sommerfeld    return fn
25528e88f55SBill Sommerfeld
25628e88f55SBill Sommerfelddef filechecker(fn):
25728e88f55SBill Sommerfeld    """ Decorator which identifies a function as being a file-checker """
25828e88f55SBill Sommerfeld    return add_check(fn, gen_files)
25928e88f55SBill Sommerfeld
26028e88f55SBill Sommerfelddef linkchecker(fn):
26128e88f55SBill Sommerfeld    """ Decorator which identifies a function as being a symlink-checker """
26228e88f55SBill Sommerfeld    return add_check(fn, gen_links)
26328e88f55SBill Sommerfeld
26428e88f55SBill Sommerfelddef wschecker(fn):
26528e88f55SBill Sommerfeld    """ Decorator which identifies a function as being a workspace checker """
26628e88f55SBill Sommerfeld    return add_check(fn, gen_none)
26728e88f55SBill Sommerfeld
26828e88f55SBill Sommerfeld@wschecker
2698bcea973SRichard Lowedef comchk(root, parent, flist, output):
27028e88f55SBill Sommerfeld    "Check that putback comments follow the prescribed format"
2718bcea973SRichard Lowe    output.write("Comments:\n")
2728bcea973SRichard Lowe
2738d226a82SBill Sommerfeld    comments = git_comments(parent)
2743f8945a7SBill Sommerfeld    multi = len(comments) > 1
2753f8945a7SBill Sommerfeld    state = {}
2768d226a82SBill Sommerfeld
2773f8945a7SBill Sommerfeld    ret = 0
2783f8945a7SBill Sommerfeld    for commit in comments:
2798bcea973SRichard Lowe
2803f8945a7SBill Sommerfeld        s = StringIO()
2813f8945a7SBill Sommerfeld
2823f8945a7SBill Sommerfeld        result = Comments.comchk(commit, check_db=True,
2833f8945a7SBill Sommerfeld                                 output=s, bugs=state)
2843f8945a7SBill Sommerfeld        ret |= result
2853f8945a7SBill Sommerfeld
2863f8945a7SBill Sommerfeld        if result != 0:
2873f8945a7SBill Sommerfeld            if multi:
2883f8945a7SBill Sommerfeld                output.write('\n%s\n' % commit[0])
2893f8945a7SBill Sommerfeld            output.write(s.getvalue())
2903f8945a7SBill Sommerfeld
2913f8945a7SBill Sommerfeld    return ret
2928bcea973SRichard Lowe
29328e88f55SBill Sommerfeld@filechecker
29428e88f55SBill Sommerfelddef copyright(root, parent, flist, output):
29528e88f55SBill Sommerfeld    """Check that each source file contains a copyright notice for the current
29628e88f55SBill Sommerfeldyear. You don't need to fix this if you, the potential new copyright holder,
29728e88f55SBill Sommerfeldchooses not to."""
2988bcea973SRichard Lowe    ret = 0
29928e88f55SBill Sommerfeld    output.write("Copyrights:\n")
30028e88f55SBill Sommerfeld    for f in flist():
30128e88f55SBill Sommerfeld        with io.open(f, encoding='utf-8', errors='replace') as fh:
30228e88f55SBill Sommerfeld            ret |= Copyright.copyright(fh, output=output)
30328e88f55SBill Sommerfeld    return ret
3048bcea973SRichard Lowe
30528e88f55SBill Sommerfeld@filechecker
30628e88f55SBill Sommerfelddef cstyle(root, parent, flist, output):
30728e88f55SBill Sommerfeld    "Check that C source files conform to the illumos C style rules"
30828e88f55SBill Sommerfeld    ret = 0
30928e88f55SBill Sommerfeld    output.write("C style:\n")
31028e88f55SBill Sommerfeld    for f in flist(lambda x: x.endswith('.c') or x.endswith('.h')):
31128e88f55SBill Sommerfeld        with io.open(f, mode='rb') as fh:
31228e88f55SBill Sommerfeld            ret |= CStyle.cstyle(fh, output=output, picky=True,
31328e88f55SBill Sommerfeld                             check_posix_types=True,
31428e88f55SBill Sommerfeld                             check_continuation=True)
31528e88f55SBill Sommerfeld    return ret
31628e88f55SBill Sommerfeld
31728e88f55SBill Sommerfeld@filechecker
31828e88f55SBill Sommerfelddef hdrchk(root, parent, flist, output):
31928e88f55SBill Sommerfeld    "Check that C header files conform to the illumos header style rules"
32028e88f55SBill Sommerfeld    ret = 0
32128e88f55SBill Sommerfeld    output.write("Header format:\n")
32228e88f55SBill Sommerfeld    for f in flist(lambda x: x.endswith('.h')):
32328e88f55SBill Sommerfeld        with io.open(f, encoding='utf-8', errors='replace') as fh:
32428e88f55SBill Sommerfeld            ret |= HdrChk.hdrchk(fh, lenient=True, output=output)
32528e88f55SBill Sommerfeld    return ret
32628e88f55SBill Sommerfeld
32728e88f55SBill Sommerfeld@filechecker
32828e88f55SBill Sommerfelddef jstyle(root, parent, flist, output):
32928e88f55SBill Sommerfeld    """Check that Java source files conform to the illumos Java style rules
33028e88f55SBill Sommerfeld(which differ from the traditionally recommended Java style)"""
33128e88f55SBill Sommerfeld
33228e88f55SBill Sommerfeld    ret = 0
33328e88f55SBill Sommerfeld    output.write("Java style:\n")
33428e88f55SBill Sommerfeld    for f in flist(lambda x: x.endswith('.java')):
33528e88f55SBill Sommerfeld        with io.open(f, mode='rb') as fh:
33628e88f55SBill Sommerfeld            ret |= JStyle.jstyle(fh, output=output, picky=True)
33728e88f55SBill Sommerfeld    return ret
33828e88f55SBill Sommerfeld
33928e88f55SBill Sommerfeld@filechecker
34028e88f55SBill Sommerfelddef keywords(root, parent, flist, output):
34128e88f55SBill Sommerfeld    """Check that no source files contain unexpanded SCCS keywords.
34228e88f55SBill SommerfeldIt is possible that this check may false positive on certain inputs.
34328e88f55SBill SommerfeldIt is generally obvious when this is the case.
34428e88f55SBill Sommerfeld
34528e88f55SBill SommerfeldThis check does not check for expanded SCCS keywords, though the common
34628e88f55SBill Sommerfeld'ident'-style lines should be removed regardless of whether they are
34728e88f55SBill Sommerfeldexpanded."""
34828e88f55SBill Sommerfeld
34928e88f55SBill Sommerfeld    ret = 0
35028e88f55SBill Sommerfeld    output.write("SCCS Keywords:\n")
35128e88f55SBill Sommerfeld    for f in flist():
35228e88f55SBill Sommerfeld        with io.open(f, encoding='utf-8', errors='replace') as fh:
35328e88f55SBill Sommerfeld            ret |= Keywords.keywords(fh, output=output)
35428e88f55SBill Sommerfeld    return ret
35528e88f55SBill Sommerfeld
35628e88f55SBill Sommerfeld@filechecker
35728e88f55SBill Sommerfelddef manlint(root, parent, flist, output):
35828e88f55SBill Sommerfeld    "Check for problems with man pages."
35928e88f55SBill Sommerfeld
36028e88f55SBill Sommerfeld    ret = 0
36128e88f55SBill Sommerfeld    output.write("Man page format/spelling:\n")
36228e88f55SBill Sommerfeld    ManfileRE = re.compile(r'.*\.[0-9][a-z]*$', re.IGNORECASE)
36328e88f55SBill Sommerfeld    for f in flist(lambda x: ManfileRE.match(x)):
36428e88f55SBill Sommerfeld        with io.open(f, mode='rb') as fh:
36528e88f55SBill Sommerfeld            ret |= ManLint.manlint(fh, output=output, picky=True)
36628e88f55SBill Sommerfeld            ret |= SpellCheck.spellcheck(fh, output=output)
36728e88f55SBill Sommerfeld    return ret
36828e88f55SBill Sommerfeld
36928e88f55SBill Sommerfeld@filechecker
37028e88f55SBill Sommerfelddef mapfilechk(root, parent, flist, output):
37128e88f55SBill Sommerfeld    """Check that linker mapfiles contain a comment directing anyone
37228e88f55SBill Sommerfeldediting to read the directions in usr/lib/README.mapfiles."""
37328e88f55SBill Sommerfeld
37428e88f55SBill Sommerfeld    ret = 0
3758bcea973SRichard Lowe    # We are interested in examining any file that has the following
3768bcea973SRichard Lowe    # in its final path segment:
3778bcea973SRichard Lowe    #    - Contains the word 'mapfile'
3788bcea973SRichard Lowe    #    - Begins with 'map.'
3798bcea973SRichard Lowe    #    - Ends with '.map'
3808bcea973SRichard Lowe    # We don't want to match unless these things occur in final path segment
3818bcea973SRichard Lowe    # because directory names with these strings don't indicate a mapfile.
3828bcea973SRichard Lowe    # We also ignore files with suffixes that tell us that the files
3838bcea973SRichard Lowe    # are not mapfiles.
3848bcea973SRichard Lowe    MapfileRE = re.compile(r'.*((mapfile[^/]*)|(/map\.+[^/]*)|(\.map))$',
3858bcea973SRichard Lowe        re.IGNORECASE)
3868bcea973SRichard Lowe    NotMapSuffixRE = re.compile(r'.*\.[ch]$', re.IGNORECASE)
3878bcea973SRichard Lowe
3888bcea973SRichard Lowe    output.write("Mapfile comments:\n")
3898bcea973SRichard Lowe
3908bcea973SRichard Lowe    for f in flist(lambda x: MapfileRE.match(x) and not
3918bcea973SRichard Lowe                   NotMapSuffixRE.match(x)):
392ca13eaa5SAndy Fiddaman        with io.open(f, encoding='utf-8', errors='replace') as fh:
3938bcea973SRichard Lowe            ret |= Mapfile.mapfilechk(fh, output=output)
3948bcea973SRichard Lowe    return ret
3958bcea973SRichard Lowe
39628e88f55SBill Sommerfeld@filechecker
39713904da8SAndy Fiddamandef shelllint(root, parent, flist, output):
39828e88f55SBill Sommerfeld    """Check shell scripts for common errors."""
39913904da8SAndy Fiddaman    ret = 0
40013904da8SAndy Fiddaman    output.write("Shell lint:\n")
40113904da8SAndy Fiddaman
40213904da8SAndy Fiddaman    def isshell(x):
40313904da8SAndy Fiddaman        (_, ext) = os.path.splitext(x)
40413904da8SAndy Fiddaman        if ext in ['.sh', '.ksh']:
40513904da8SAndy Fiddaman            return True
40613904da8SAndy Fiddaman        if ext == '':
40713904da8SAndy Fiddaman            with io.open(x, mode='r', errors='ignore') as fh:
40813904da8SAndy Fiddaman                if re.match(r'^#.*\bk?sh\b', fh.readline()):
40913904da8SAndy Fiddaman                    return True
41013904da8SAndy Fiddaman        return False
41113904da8SAndy Fiddaman
41213904da8SAndy Fiddaman    for f in flist(isshell):
41313904da8SAndy Fiddaman        with io.open(f, mode='rb') as fh:
41413904da8SAndy Fiddaman            ret |= ShellLint.lint(fh, output=output)
41513904da8SAndy Fiddaman
41613904da8SAndy Fiddaman    return ret
41713904da8SAndy Fiddaman
41828e88f55SBill Sommerfeld@filechecker
41986d41711SAndy Fiddamandef pkgfmt(root, parent, flist, output):
42028e88f55SBill Sommerfeld    """Check package manifests for common errors."""
42186d41711SAndy Fiddaman    ret = 0
42286d41711SAndy Fiddaman    output.write("Package manifests:\n")
42386d41711SAndy Fiddaman
42425b05a3eSAndy Fiddaman    for f in flist(lambda x: x.endswith('.p5m')):
42586d41711SAndy Fiddaman        with io.open(f, mode='rb') as fh:
42686d41711SAndy Fiddaman            ret |= PkgFmt.check(fh, output=output)
42786d41711SAndy Fiddaman
42886d41711SAndy Fiddaman    return ret
42986d41711SAndy Fiddaman
430955eb5e1SGarrett D'Amoredef iswinreserved(name):
431955eb5e1SGarrett D'Amore    reserved = [
432955eb5e1SGarrett D'Amore        'con', 'prn', 'aux', 'nul',
433955eb5e1SGarrett D'Amore        'com1', 'com2', 'com3', 'com4', 'com5',
434955eb5e1SGarrett D'Amore        'com6', 'com7', 'com8', 'com9', 'com0',
435955eb5e1SGarrett D'Amore        'lpt1', 'lpt2', 'lpt3', 'lpt4', 'lpt5',
436955eb5e1SGarrett D'Amore        'lpt6', 'lpt7', 'lpt8', 'lpt9', 'lpt0' ]
437955eb5e1SGarrett D'Amore    l = name.lower()
438955eb5e1SGarrett D'Amore    for r in reserved:
439955eb5e1SGarrett D'Amore        if l == r or l.startswith(r+"."):
440955eb5e1SGarrett D'Amore            return True
441955eb5e1SGarrett D'Amore    return False
442955eb5e1SGarrett D'Amore
443955eb5e1SGarrett D'Amoredef haswinspecial(name):
444955eb5e1SGarrett D'Amore    specials = '<>:"\\|?*'
445955eb5e1SGarrett D'Amore    for c in name:
446955eb5e1SGarrett D'Amore        if c in specials:
447955eb5e1SGarrett D'Amore            return True
448955eb5e1SGarrett D'Amore    return False
449955eb5e1SGarrett D'Amore
45028e88f55SBill Sommerfeld@filechecker
451955eb5e1SGarrett D'Amoredef winnames(root, parent, flist, output):
45228e88f55SBill Sommerfeld    "Check for filenames which can't be used in a Windows filesystem."
453955eb5e1SGarrett D'Amore    ret = 0
454955eb5e1SGarrett D'Amore    output.write("Illegal filenames (Windows):\n")
455955eb5e1SGarrett D'Amore    for f in flist():
456955eb5e1SGarrett D'Amore        if haswinspecial(f):
457955eb5e1SGarrett D'Amore            output.write("  "+f+": invalid character in name\n")
458955eb5e1SGarrett D'Amore            ret |= 1
459955eb5e1SGarrett D'Amore            continue
460955eb5e1SGarrett D'Amore
461955eb5e1SGarrett D'Amore        parts = f.split('/')
462955eb5e1SGarrett D'Amore        for p in parts:
463955eb5e1SGarrett D'Amore            if iswinreserved(p):
464955eb5e1SGarrett D'Amore                output.write("  "+f+": reserved file name\n")
465955eb5e1SGarrett D'Amore                ret |= 1
466955eb5e1SGarrett D'Amore                break
467955eb5e1SGarrett D'Amore
468955eb5e1SGarrett D'Amore    return ret
469955eb5e1SGarrett D'Amore
47028e88f55SBill Sommerfeld@filechecker
47128e88f55SBill Sommerfelddef wscheck(root, parent, flist, output):
47228e88f55SBill Sommerfeld    "Check for whitespace issues such as mixed tabs/spaces in source files."
47328e88f55SBill Sommerfeld    ret = 0
47428e88f55SBill Sommerfeld    output.write("white space nits:\n")
47528e88f55SBill Sommerfeld    for f in flist():
47628e88f55SBill Sommerfeld        with io.open(f, encoding='utf-8', errors='replace') as fh:
47728e88f55SBill Sommerfeld            ret |= WsCheck.wscheck(fh, output=output)
47828e88f55SBill Sommerfeld    return ret
47928e88f55SBill Sommerfeld
48028e88f55SBill Sommerfeld@linkchecker
48128e88f55SBill Sommerfelddef symlinks(root, parent, flist, output):
48228e88f55SBill Sommerfeld    "Check for committed symlinks (there shouldn't be any)."
48328e88f55SBill Sommerfeld    ret = 0
48428e88f55SBill Sommerfeld    output.write("Symbolic links:\n")
48528e88f55SBill Sommerfeld    for f in flist():
48628e88f55SBill Sommerfeld        output.write("  "+f+"\n")
48728e88f55SBill Sommerfeld        ret |= 1
48828e88f55SBill Sommerfeld    return ret
48928e88f55SBill Sommerfeld
49028e88f55SBill Sommerfelddef run_checks(root, parent, checklist, paths=''):
49128e88f55SBill Sommerfeld    """Run the checks named in 'checklist',
4928bcea973SRichard Lowe    and report results for any which fail.
4938bcea973SRichard Lowe
4948bcea973SRichard Lowe    Return failure if any of them did.
4958bcea973SRichard Lowe
49628e88f55SBill Sommerfeld    NB: the check names also name the NOT
4978bcea973SRichard Lowe    file which excepts files from them."""
4988bcea973SRichard Lowe
4998bcea973SRichard Lowe    ret = 0
5008bcea973SRichard Lowe
50128e88f55SBill Sommerfeld    for check in checklist:
50228e88f55SBill Sommerfeld        (cmd, gen) = checks[check]
50328e88f55SBill Sommerfeld
5048bcea973SRichard Lowe        s = StringIO()
5058bcea973SRichard Lowe
50628e88f55SBill Sommerfeld        exclude = not_check(root, check)
50728e88f55SBill Sommerfeld        result = cmd(root, parent, gen(root, parent, paths, exclude),
508955eb5e1SGarrett D'Amore                     output=s)
509955eb5e1SGarrett D'Amore        ret |= result
510955eb5e1SGarrett D'Amore
511955eb5e1SGarrett D'Amore        if result != 0:
512955eb5e1SGarrett D'Amore            print(s.getvalue())
513955eb5e1SGarrett D'Amore
5148bcea973SRichard Lowe    return ret
5158bcea973SRichard Lowe
51628e88f55SBill Sommerfelddef print_checks():
5178bcea973SRichard Lowe
51828e88f55SBill Sommerfeld    for c in all_checks:
51928e88f55SBill Sommerfeld        print(textwrap.fill(
52028e88f55SBill Sommerfeld            "%-11s %s" % (c, checks[c][0].__doc__),
52128e88f55SBill Sommerfeld            width=78,
52228e88f55SBill Sommerfeld            subsequent_indent=' '*12), '\n')
5238bcea973SRichard Lowe
5248bcea973SRichard Lowedef main(cmd, args):
5258bcea973SRichard Lowe    parent_branch = None
526*cbbc171dSiximeow    allow_questionable_requests = False
52728e88f55SBill Sommerfeld
52828e88f55SBill Sommerfeld    checklist = []
5298bcea973SRichard Lowe
5308bcea973SRichard Lowe    try:
531*cbbc171dSiximeow        opts, args = getopt.getopt(args, 'lfb:c:p:')
532ca13eaa5SAndy Fiddaman    except getopt.GetoptError as e:
5338bcea973SRichard Lowe        sys.stderr.write(str(e) + '\n')
534*cbbc171dSiximeow        sys.stderr.write("Usage: %s [-l] [-f] [-c check] [-p branch] "
535*cbbc171dSiximeow                         "[path...]\n" % cmd)
5368bcea973SRichard Lowe        sys.exit(1)
5378bcea973SRichard Lowe
5388bcea973SRichard Lowe    for opt, arg in opts:
53928e88f55SBill Sommerfeld        if opt == '-l':
54028e88f55SBill Sommerfeld            print_checks()
54128e88f55SBill Sommerfeld            sys.exit(0)
542*cbbc171dSiximeow        elif opt == '-f':
543*cbbc171dSiximeow            allow_questionable_requests = True
54442a3762dSJoshua M. Clulow        # We accept "-b" as an alias of "-p" for backwards compatibility.
54528e88f55SBill Sommerfeld        elif opt == '-p' or opt == '-b':
5468bcea973SRichard Lowe            parent_branch = arg
547eabe844aSJohn Levon        elif opt == '-c':
54828e88f55SBill Sommerfeld            if arg not in checks:
54928e88f55SBill Sommerfeld                sys.stderr.write("Unknown check '%s'\n" % arg)
55028e88f55SBill Sommerfeld                sys.exit(1)
55128e88f55SBill Sommerfeld            checklist.append(arg)
5528bcea973SRichard Lowe
5538bcea973SRichard Lowe    if not parent_branch:
5548bcea973SRichard Lowe        parent_branch = git_parent_branch(git_branch())
5558bcea973SRichard Lowe
556*cbbc171dSiximeow    comments = git_comments(parent_branch)
557*cbbc171dSiximeow    if len(comments) > 5 and not allow_questionable_requests:
558*cbbc171dSiximeow        sys.stderr.write("Declining to check history since %s, would be %d "
559*cbbc171dSiximeow                         "commits. Rerun with -f if you really mean to.\n" %
560*cbbc171dSiximeow                         (parent_branch, len(comments)))
561*cbbc171dSiximeow        sys.exit(1)
562*cbbc171dSiximeow
56328e88f55SBill Sommerfeld    if len(checklist) == 0:
5648bcea973SRichard Lowe        if cmd == 'git-pbchk':
5658bcea973SRichard Lowe            if args:
5668bcea973SRichard Lowe                sys.stderr.write("only complete workspaces may be pbchk'd\n");
5678bcea973SRichard Lowe                sys.exit(1)
56828e88f55SBill Sommerfeld            checklist = all_checks
569eabe844aSJohn Levon        else:
57028e88f55SBill Sommerfeld            checklist = nits_checks
57128e88f55SBill Sommerfeld
57228e88f55SBill Sommerfeld    run_checks(git_root(), parent_branch, checklist, args)
5738bcea973SRichard Lowe
5748bcea973SRichard Loweif __name__ == '__main__':
5758bcea973SRichard Lowe    try:
5768bcea973SRichard Lowe        main(os.path.basename(sys.argv[0]), sys.argv[1:])
577ca13eaa5SAndy Fiddaman    except GitError as e:
5788bcea973SRichard Lowe        sys.stderr.write("failed to run git:\n %s\n" % str(e))
5798bcea973SRichard Lowe        sys.exit(1)
580