lintChanges.py 6.1 KB

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