Actually I believe this should be an EINVAL.
[tor.git] / scripts / maint / lintChanges.py
blobd5b8fcae5cf3c4ffca146d01ffcfeef57748c870
1 #!/usr/bin/python
3 from __future__ import print_function
4 from __future__ import with_statement
5 import sys
6 import re
7 import os
10 KNOWN_GROUPS = set([
11 "Minor bugfix",
12 "Minor bugfixes",
13 "Major bugfix",
14 "Major bugfixes",
15 "Minor feature",
16 "Minor features",
17 "Major feature",
18 "Major features",
19 "New system requirements",
20 "Testing",
21 "Documentation",
22 "Code simplification and refactoring",
23 "Removed features",
24 "Deprecated features",
25 "Directory authority changes"])
27 NEEDS_SUBCATEGORIES = set([
28 "Minor bugfix",
29 "Minor bugfixes",
30 "Major bugfix",
31 "Major bugfixes",
32 "Minor feature",
33 "Minor features",
34 "Major feature",
35 "Major features",
38 def lintfile(fname):
39 have_warned = []
41 def warn(s):
42 if not have_warned:
43 have_warned.append(1)
44 print("{}:".format(fname))
45 print("\t{}".format(s))
47 m = re.search(r'(\d{3,})', os.path.basename(fname))
48 if m:
49 bugnum = m.group(1)
50 else:
51 bugnum = None
53 with open(fname) as f:
54 contents = f.read()
56 if bugnum and bugnum not in contents:
57 warn("bug number {} does not appear".format(bugnum))
59 m = re.match(r'^[ ]{2}o ([^\(:]*)([^:]*):', contents)
60 if not m:
61 warn("Header not in format expected. (' o Foo:' or ' o Foo (Bar):')")
62 elif m.group(1).strip() not in KNOWN_GROUPS:
63 warn("Unrecognized header: %r" % m.group(1))
64 elif (m.group(1) in NEEDS_SUBCATEGORIES and '(' not in m.group(2)):
65 warn("Missing subcategory on %r" % m.group(1))
67 if m:
68 isBug = ("bug" in m.group(1).lower() or "fix" in m.group(1).lower())
69 else:
70 isBug = False
72 contents = " ".join(contents.split())
74 if re.search(r'\#\d{2,}', contents):
75 warn("Don't use a # before ticket numbers. ('bug 1234' not '#1234')")
77 if isBug and not re.search(r'(\d+)', contents):
78 warn("Ticket marked as bugfix, but does not mention a number.")
79 elif isBug and not re.search(r'Fixes ([a-z ]*)bugs? (\d+)', contents):
80 warn("Ticket marked as bugfix, but does not say 'Fixes bug XXX'")
82 if re.search(r'[bB]ug (\d+)', contents):
83 if not re.search(r'[Bb]ugfix on ', contents):
84 warn("Bugfix does not say 'bugfix on X.Y.Z'")
85 elif not re.search('[fF]ixes ([a-z ]*)bugs? (\d+)((, \d+)* and \d+)?; bugfix on ',
86 contents):
87 warn("Bugfix does not say 'Fixes bug X; bugfix on Y'")
88 elif re.search('tor-([0-9]+)', contents):
89 warn("Do not prefix versions with 'tor-'. ('0.1.2', not 'tor-0.1.2'.)")
91 return have_warned != []
93 def files(args):
94 """Walk through the arguments: for directories, yield their contents;
95 for files, just yield the files. Only search one level deep, because
96 that's how the changes directory is laid out."""
97 for f in args:
98 if os.path.isdir(f):
99 for item in os.listdir(f):
100 if item.startswith("."): #ignore dotfiles
101 continue
102 yield os.path.join(f, item)
103 else:
104 yield f
106 if __name__ == '__main__':
107 problems = 0
108 for fname in files(sys.argv[1:]):
109 if fname.endswith("~"):
110 continue
111 if lintfile(fname):
112 problems += 1
114 if problems:
115 sys.exit(1)
116 else:
117 sys.exit(0)