rename_c_identifier.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. #!/usr/bin/env python3
  2. #
  3. # Copyright (c) 2001 Matej Pfajfar.
  4. # Copyright (c) 2001-2004, Roger Dingledine.
  5. # Copyright (c) 2004-2006, Roger Dingledine, Nick Mathewson.
  6. # Copyright (c) 2007-2019, The Tor Project, Inc.
  7. # See LICENSE for licensing information
  8. """
  9. Helpful script to replace one or more C identifiers, and optionally
  10. generate a commit message explaining what happened.
  11. """
  12. # Future imports for Python 2.7, mandatory in 3.0
  13. from __future__ import division
  14. from __future__ import print_function
  15. from __future__ import unicode_literals
  16. import argparse
  17. import fileinput
  18. import os
  19. import re
  20. import shlex
  21. import subprocess
  22. import sys
  23. import tempfile
  24. TOPDIR = "src"
  25. def is_c_file(fn):
  26. """
  27. Return true iff fn is the name of a C file.
  28. >>> is_c_file("a/b/module.c")
  29. True
  30. >>> is_c_file("a/b/module.h")
  31. True
  32. >>> is_c_file("a/b/module.c~")
  33. False
  34. >>> is_c_file("a/b/.module.c")
  35. False
  36. >>> is_c_file("a/b/module.cpp")
  37. False
  38. """
  39. fn = os.path.split(fn)[1]
  40. # Avoid editor temporary files
  41. if fn.startswith(".") or fn.startswith("#"):
  42. return False
  43. ext = os.path.splitext(fn)[1]
  44. return ext in {".c", ".h", ".i", ".inc"}
  45. def list_c_files(topdir=TOPDIR):
  46. """
  47. Use git to list all the C files under version control.
  48. >>> lst = list(list_c_files())
  49. >>> "src/core/mainloop/mainloop.c" in lst
  50. True
  51. >>> "src/core/mainloop/twiddledeedoo.c" in lst
  52. False
  53. >>> "micro-revision.i" in lst
  54. False
  55. """
  56. proc = subprocess.Popen(
  57. ["git", "ls-tree", "--name-only", "-r", "HEAD", topdir],
  58. stdout=subprocess.PIPE,
  59. encoding="utf-8")
  60. for line in proc.stdout.readlines():
  61. line = line.strip()
  62. if is_c_file(line):
  63. yield line
  64. class Rewriter:
  65. """
  66. A rewriter applies a series of word-by-word replacements, in
  67. sequence. Replacements only happen at "word boundaries",
  68. as determined by the \\b regular expression marker.
  69. ("A word is defined as a sequence of alphanumeric or underscore
  70. characters", according to the documentation.)
  71. >>> R = Rewriter([("magic", "secret"), ("words", "codes")])
  72. >>> R.apply("The magic words are rambunctious bluejay")
  73. 'The secret codes are rambunctious bluejay'
  74. >>> R.apply("The magical words are rambunctious bluejay")
  75. 'The magical codes are rambunctious bluejay'
  76. >>> R.get_count()
  77. 3
  78. """
  79. def __init__(self, replacements):
  80. """Make a new Rewriter. Takes a sequence of pairs of
  81. (from_id, to_id), where from_id is an identifier to replace,
  82. and to_id is its replacement.
  83. """
  84. self._patterns = []
  85. for id1, id2 in replacements:
  86. pat = re.compile(r"\b{}\b".format(re.escape(id1)))
  87. self._patterns.append((pat, id2))
  88. self._count = 0
  89. def apply(self, line):
  90. """Return `line` as transformed by this rewriter."""
  91. for pat, ident in self._patterns:
  92. line, count = pat.subn(ident, line)
  93. self._count += count
  94. return line
  95. def get_count(self):
  96. """Return the number of identifiers that this rewriter has
  97. rewritten."""
  98. return self._count
  99. def rewrite_files(files, rewriter):
  100. """
  101. Apply `rewriter` to every file in `files`, replacing those files
  102. with their rewritten contents.
  103. """
  104. for line in fileinput.input(files, inplace=True):
  105. sys.stdout.write(rewriter.apply(line))
  106. def make_commit_msg(pairs, no_verify):
  107. """Return a commit message to explain what was replaced by the provided
  108. arguments.
  109. """
  110. script = ["./scripts/maint/rename_c_identifier.py"]
  111. for id1, id2 in pairs:
  112. qid1 = shlex.quote(id1)
  113. qid2 = shlex.quote(id2)
  114. script.append(" {} {}".format(qid1, qid2))
  115. script = " \\\n".join(script)
  116. if len(pairs) == 1:
  117. line1 = "Rename {} to {}".format(*pairs[0])
  118. else:
  119. line1 = "Replace several C identifiers."
  120. msg = """\
  121. {}
  122. This is an automated commit, generated by this command:
  123. {}
  124. """.format(line1, script)
  125. if no_verify:
  126. msg += """
  127. It was generated with --no-verify, so it probably breaks some commit hooks.
  128. The committer should be sure to fix them up in a subsequent commit.
  129. """
  130. return msg
  131. def commit(pairs, no_verify=False):
  132. """Try to commit the current git state, generating the commit message as
  133. appropriate. If `no_verify` is True, pass the --no-verify argument to
  134. git commit.
  135. """
  136. args = []
  137. if no_verify:
  138. args.append("--no-verify")
  139. # We have to use a try block to delete the temporary file here, since we
  140. # are using tempfile with delete=False. We have to use delete=False,
  141. # since otherwise we are not guaranteed to be able to give the file to
  142. # git for it to open.
  143. fname = None
  144. try:
  145. with tempfile.NamedTemporaryFile(mode="w", delete=False) as f:
  146. fname = f.name
  147. f.write(make_commit_msg(pairs, no_verify))
  148. s = subprocess.run(["git", "commit", "-a", "-F", fname, "--edit"]+args)
  149. if s.returncode != 0 and not no_verify:
  150. print('"git commit" failed. Maybe retry with --no-verify?',
  151. file=sys.stderr)
  152. revert_changes()
  153. return False
  154. finally:
  155. os.unlink(fname)
  156. return True
  157. def any_uncommitted_changes():
  158. """Return True if git says there are any uncommitted changes in the current
  159. working tree; false otherwise.
  160. """
  161. s = subprocess.run(["git", "diff-index", "--quiet", "HEAD"])
  162. return s.returncode != 0
  163. DESC = "Replace one identifier with another throughout our source."
  164. EXAMPLES = """\
  165. Examples:
  166. rename_c_identifier.py set_ctrl_id set_controller_id
  167. (Replaces every occurrence of "set_ctrl_id" with "set_controller_id".)
  168. rename_c_identifier.py --commit set_ctrl_id set_controller_id
  169. (As above, but also generate a git commit with an appropriate message.)
  170. rename_c_identifier.py a b c d
  171. (Replace "a" with "b", and "c" with "d".)"""
  172. def revert_changes():
  173. """Tell git to revert all the changes in the current working tree.
  174. """
  175. print('Reverting changes.', file=sys.stderr)
  176. subprocess.run(["git", "checkout", "--quiet", TOPDIR])
  177. def main(argv):
  178. import argparse
  179. parser = argparse.ArgumentParser(description=DESC, epilog=EXAMPLES,
  180. # prevent re-wrapping the examples
  181. formatter_class=argparse.RawDescriptionHelpFormatter)
  182. parser.add_argument("--commit", action='store_true',
  183. help="Generate a Git commit.")
  184. parser.add_argument("--no-verify", action='store_true',
  185. help="Tell Git not to run its pre-commit hooks.")
  186. parser.add_argument("from_id", type=str, help="Original identifier")
  187. parser.add_argument("to_id", type=str, help="New identifier")
  188. parser.add_argument("more", type=str, nargs=argparse.REMAINDER,
  189. help="Additional identifier pairs")
  190. args = parser.parse_args(argv[1:])
  191. if len(args.more) % 2 != 0:
  192. print("I require an even number of identifiers.", file=sys.stderr)
  193. return 1
  194. if args.commit and any_uncommitted_changes():
  195. print("Uncommitted changes found. Not running.", file=sys.stderr)
  196. return 1
  197. pairs = []
  198. print("renaming {} to {}".format(args.from_id, args.to_id), file=sys.stderr)
  199. pairs.append((args.from_id, args.to_id))
  200. for idx in range(0, len(args.more), 2):
  201. id1 = args.more[idx]
  202. id2 = args.more[idx+1]
  203. print("renaming {} to {}".format(id1, id2))
  204. pairs.append((id1, id2))
  205. rewriter = Rewriter(pairs)
  206. rewrite_files(list_c_files(), rewriter)
  207. print("Replaced {} identifiers".format(rewriter.get_count()),
  208. file=sys.stderr)
  209. if args.commit:
  210. commit(pairs, args.no_verify)
  211. if __name__ == '__main__':
  212. main(sys.argv)