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