Use EWMA instead of bare rtt for min rtt.
[tor.git] / scripts / maint / lintChanges.py
blobcf7b09fcc310f523fe47a7a53488e7ce6bb0f017
1 #!/usr/bin/env python
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
8 import sys
9 import re
10 import os
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",
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"])
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",
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.
52 If the version is malformed, returns None.
53 '''
54 version_match = re.match('([0-9]+)\.([0-9]+)\.([0-9]+)(\.([0-9]+))?', version)
55 if version_match is None:
56 return None
58 version_groups = version_match.groups()
59 if version_groups is None:
60 return None
61 if len(version_groups) < 3:
62 return None
64 if len(version_groups) != 5:
65 return None
66 version_components = version_groups[0:3]
67 version_components += version_groups[4:5]
69 try:
70 version_list = [int(v) for v in version_components if v is not None]
71 except ValueError:
72 return None
74 return version_list
76 def lintfile(fname):
77 have_warned = []
79 def warn(s):
80 if not have_warned:
81 have_warned.append(1)
82 print("{}:".format(fname))
83 print("\t{}".format(s))
85 m = re.search(r'(\d{3,})', os.path.basename(fname))
86 if m:
87 bugnum = m.group(1)
88 else:
89 bugnum = None
91 with open(fname) as f:
92 contents = f.read()
94 if bugnum and bugnum not in contents:
95 warn("bug number {} does not appear".format(bugnum))
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))
105 if m:
106 isBug = ("bug" in m.group(1).lower() or "fix" in m.group(1).lower())
107 else:
108 isBug = False
110 contents = " ".join(contents.split())
112 if re.search(r'\#\d{2,}', contents):
113 warn("Don't use a # before ticket numbers. ('bug 1234' not '#1234')")
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'")
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('[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('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))
155 return have_warned != []
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
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
178 if problems:
179 sys.exit(1)
180 else:
181 sys.exit(0)