tor

The Tor anonymity network
git clone https://git.dasho.dev/tor.git
Log | Files | Refs | README | LICENSE

lintChanges.py (6556B)


      1 #!/usr/bin/env python
      2 
      3 # Future imports for Python 2.7, mandatory in 3.0
      4 from __future__ import division
      5 from __future__ import print_function
      6 from __future__ import unicode_literals
      7 
      8 import sys
      9 import re
     10 import os
     11 
     12 
     13 KNOWN_GROUPS = set([
     14    "Minor bugfix",
     15    "Minor bugfixes",
     16    "Major bugfix",
     17    "Major bugfixes",
     18    "Minor feature",
     19    "Minor features",
     20    "Major feature",
     21    "Major features",
     22    "New system requirements",
     23    "Testing",
     24    "Documentation",
     25    "Code simplification and refactoring",
     26    "Removed features",
     27    "Deprecated features",
     28    "Directory authority changes",
     29 
     30    # These aren't preferred, but sortChanges knows how to clean them up.
     31    "Code simplifications and refactoring",
     32    "Code simplification and refactorings",
     33    "Code simplifications and refactorings"])
     34 
     35 NEEDS_SUBCATEGORIES = set([
     36    "Minor bugfix",
     37    "Minor bugfixes",
     38    "Major bugfix",
     39    "Major bugfixes",
     40    "Minor feature",
     41    "Minor features",
     42    "Major feature",
     43    "Major features",
     44    ])
     45 
     46 def split_tor_version(version):
     47    '''
     48    Return the initial numeric components of the Tor version as a list of ints.
     49    For versions earlier than 0.1.0, returns MAJOR, MINOR, and MICRO.
     50    For versions 0.1.0 and later, returns MAJOR, MINOR, MICRO, and PATCHLEVEL if present.
     51 
     52    If the version is malformed, returns None.
     53    '''
     54    version_match = re.match(r'([0-9]+)\.([0-9]+)\.([0-9]+)(\.([0-9]+))?', version)
     55    if version_match is None:
     56        return None
     57 
     58    version_groups = version_match.groups()
     59    if version_groups is None:
     60        return None
     61    if len(version_groups) < 3:
     62        return None
     63 
     64    if len(version_groups) != 5:
     65        return None
     66    version_components = version_groups[0:3]
     67    version_components += version_groups[4:5]
     68 
     69    try:
     70        version_list = [int(v) for v in version_components if v is not None]
     71    except ValueError:
     72        return None
     73 
     74    return version_list
     75 
     76 def lintfile(fname):
     77    have_warned = []
     78 
     79    def warn(s):
     80        if not have_warned:
     81            have_warned.append(1)
     82            print("{}:".format(fname))
     83        print("\t{}".format(s))
     84 
     85    m = re.search(r'(\d{3,})', os.path.basename(fname))
     86    if m:
     87        bugnum = m.group(1)
     88    else:
     89        bugnum = None
     90 
     91    with open(fname) as f:
     92        contents = f.read()
     93 
     94    if bugnum and bugnum not in contents:
     95        warn("bug number {} does not appear".format(bugnum))
     96 
     97    m = re.match(r'^[ ]{2}o ([^\(:]*)([^:]*):', contents)
     98    if not m:
     99        warn("Header not in format expected. ('  o Foo:' or '  o Foo (Bar):')")
    100    elif m.group(1).strip() not in KNOWN_GROUPS:
    101        warn("Unrecognized header: %r" % m.group(1))
    102    elif (m.group(1) in NEEDS_SUBCATEGORIES and '(' not in m.group(2)):
    103        warn("Missing subcategory on %r" % m.group(1))
    104 
    105    if m:
    106        isBug = ("bug" in m.group(1).lower() or "fix" in m.group(1).lower())
    107    else:
    108        isBug = False
    109 
    110    contents = " ".join(contents.split())
    111 
    112    if re.search(r'\#\d{2,}', contents):
    113        warn("Don't use a # before ticket numbers. ('bug 1234' not '#1234')")
    114 
    115    if isBug and not re.search(r'(\d+)', contents):
    116        warn("Ticket marked as bugfix, but does not mention a number.")
    117    elif isBug and not re.search(r'Fixes ([a-z ]*)bugs? (\d+)', contents):
    118        warn("Ticket marked as bugfix, but does not say 'Fixes bug XXX'")
    119 
    120    if re.search(r'[bB]ug (\d+)', contents):
    121        if not re.search(r'[Bb]ugfix on ', contents):
    122            warn("Bugfix does not say 'bugfix on X.Y.Z'")
    123        elif not re.search(r'[fF]ixes ([a-z ]*)bugs? (\d+)((, \d+)* and \d+)?; bugfix on ',
    124                           contents):
    125            warn("Bugfix does not say 'Fixes bug X; bugfix on Y'")
    126        elif re.search('tor-([0-9]+)', contents):
    127            warn("Do not prefix versions with 'tor-'. ('0.1.2', not 'tor-0.1.2'.)")
    128        else:
    129            bugfix_match = re.search(r'bugfix on ([0-9]+\.[0-9]+\.[0-9]+)', contents)
    130            if bugfix_match is None:
    131                warn("Versions must have at least 3 digits. ('0.1.2', '0.3.4.8', or '0.3.5.1-alpha'.)")
    132            elif bugfix_match.group(0) is None:
    133                warn("Versions must have at least 3 digits. ('0.1.2', '0.3.4.8', or '0.3.5.1-alpha'.)")
    134            else:
    135                bugfix_match = re.search('bugfix on ([0-9a-z][-.0-9a-z]+[0-9a-z])', contents)
    136                bugfix_group = bugfix_match.groups() if bugfix_match is not None else None
    137                bugfix_version = bugfix_group[0] if bugfix_group is not None else None
    138                package_version = os.environ.get('PACKAGE_VERSION', None)
    139                if bugfix_version is None:
    140                    # This should be unreachable, unless the patterns are out of sync
    141                    warn("Malformed bugfix version.")
    142                elif package_version is not None:
    143                    # If $PACKAGE_VERSION isn't set, skip this check
    144                    bugfix_split = split_tor_version(bugfix_version)
    145                    package_split = split_tor_version(package_version)
    146                    if bugfix_split is None:
    147                        # This should be unreachable, unless the patterns are out of sync
    148                        warn("Malformed bugfix version: '{}'.".format(bugfix_version))
    149                    elif package_split is None:
    150                        # This should be unreachable, unless the patterns are out of sync, or the package versioning scheme has changed
    151                        warn("Malformed $PACKAGE_VERSION: '{}'.".format(package_version))
    152                    elif bugfix_split > package_split:
    153                        warn("Bugfixes must be made on earlier versions (or this version). (Bugfix on version: '{}', current tor package version: '{}'.)".format(bugfix_version, package_version))
    154 
    155    return have_warned != []
    156 
    157 def files(args):
    158    """Walk through the arguments: for directories, yield their contents;
    159       for files, just yield the files. Only search one level deep, because
    160       that's how the changes directory is laid out."""
    161    for f in args:
    162        if os.path.isdir(f):
    163            for item in os.listdir(f):
    164                if item.startswith("."): #ignore dotfiles
    165                    continue
    166                yield os.path.join(f, item)
    167        else:
    168            yield f
    169 
    170 if __name__ == '__main__':
    171    problems = 0
    172    for fname in files(sys.argv[1:]):
    173        if fname.endswith("~"):
    174            continue
    175        if lintfile(fname):
    176            problems += 1
    177 
    178    if problems:
    179        sys.exit(1)
    180    else:
    181        sys.exit(0)