lintChanges.py 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  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 lintfile(fname):
  34. have_warned = []
  35. def warn(s):
  36. if not have_warned:
  37. have_warned.append(1)
  38. print("{}:".format(fname))
  39. print("\t{}".format(s))
  40. m = re.search(r'(\d{3,})', os.path.basename(fname))
  41. if m:
  42. bugnum = m.group(1)
  43. else:
  44. bugnum = None
  45. with open(fname) as f:
  46. contents = f.read()
  47. if bugnum and bugnum not in contents:
  48. warn("bug number {} does not appear".format(bugnum))
  49. m = re.match(r'^[ ]{2}o ([^\(:]*)([^:]*):', contents)
  50. if not m:
  51. warn("Header not in format expected. (' o Foo:' or ' o Foo (Bar):')")
  52. elif m.group(1).strip() not in KNOWN_GROUPS:
  53. warn("Unrecognized header: %r" % m.group(1))
  54. elif (m.group(1) in NEEDS_SUBCATEGORIES and '(' not in m.group(2)):
  55. warn("Missing subcategory on %r" % m.group(1))
  56. if m:
  57. isBug = ("bug" in m.group(1).lower() or "fix" in m.group(1).lower())
  58. else:
  59. isBug = False
  60. contents = " ".join(contents.split())
  61. if re.search(r'\#\d{2,}', contents):
  62. warn("Don't use a # before ticket numbers. ('bug 1234' not '#1234')")
  63. if isBug and not re.search(r'(\d+)', contents):
  64. warn("Ticket marked as bugfix, but does not mention a number.")
  65. elif isBug and not re.search(r'Fixes ([a-z ]*)bugs? (\d+)', contents):
  66. warn("Ticket marked as bugfix, but does not say 'Fixes bug XXX'")
  67. if re.search(r'[bB]ug (\d+)', contents):
  68. if not re.search(r'[Bb]ugfix on ', contents):
  69. warn("Bugfix does not say 'bugfix on X.Y.Z'")
  70. elif not re.search('[fF]ixes ([a-z ]*)bugs? (\d+)((, \d+)* and \d+)?; bugfix on ',
  71. contents):
  72. warn("Bugfix does not say 'Fixes bug X; bugfix on Y'")
  73. elif re.search('tor-([0-9]+)', contents):
  74. warn("Do not prefix versions with 'tor-'. ('0.1.2', not 'tor-0.1.2'.)")
  75. return have_warned != []
  76. def files(args):
  77. """Walk through the arguments: for directories, yield their contents;
  78. for files, just yield the files. Only search one level deep, because
  79. that's how the changes directory is laid out."""
  80. for f in args:
  81. if os.path.isdir(f):
  82. for item in os.listdir(f):
  83. if item.startswith("."): #ignore dotfiles
  84. continue
  85. yield os.path.join(f, item)
  86. else:
  87. yield f
  88. if __name__ == '__main__':
  89. problems = 0
  90. for fname in files(sys.argv[1:]):
  91. if fname.endswith("~"):
  92. continue
  93. if lintfile(fname):
  94. problems += 1
  95. if problems:
  96. sys.exit(1)
  97. else:
  98. sys.exit(0)