rename_c_identifier.py (8099B)
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 """ 10 Helpful script to replace one or more C identifiers, and optionally 11 generate a commit message explaining what happened. 12 """ 13 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 18 19 import argparse 20 import fileinput 21 import os 22 import re 23 import shlex 24 import subprocess 25 import sys 26 import tempfile 27 28 TOPDIR = "src" 29 30 31 def is_c_file(fn): 32 """ 33 Return true iff fn is the name of a C file. 34 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"} 52 53 54 def list_c_files(topdir=TOPDIR): 55 """ 56 Use git to list all the C files under version control. 57 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 74 75 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. 81 82 ("A word is defined as a sequence of alphanumeric or underscore 83 characters", according to the documentation.) 84 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() 91 3 92 93 """ 94 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)) 104 105 self._count = 0 106 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 113 114 def get_count(self): 115 """Return the number of identifiers that this rewriter has 116 rewritten.""" 117 return self._count 118 119 120 def rewrite_files(files, rewriter): 121 """ 122 Apply `rewriter` to every file in `files`, replacing those files 123 with their rewritten contents. 124 """ 125 for line in fileinput.input(files, inplace=True): 126 sys.stdout.write(rewriter.apply(line)) 127 128 129 def make_commit_msg(pairs, no_verify): 130 """Return a commit message to explain what was replaced by the provided 131 arguments. 132 """ 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) 139 140 if len(pairs) == 1: 141 line1 = "Rename {} to {}".format(*pairs[0]) 142 else: 143 line1 = "Replace several C identifiers." 144 145 msg = """\ 146 {} 147 148 This is an automated commit, generated by this command: 149 150 {} 151 """.format(line1, script) 152 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. 157 """ 158 159 return msg 160 161 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. 166 """ 167 args = [] 168 if no_verify: 169 args.append("--no-verify") 170 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) 188 189 return True 190 191 192 def any_uncommitted_changes(): 193 """Return True if git says there are any uncommitted changes in the current 194 working tree; false otherwise. 195 """ 196 s = subprocess.run(["git", "diff-index", "--quiet", "HEAD"]) 197 return s.returncode != 0 198 199 200 DESC = "Replace one identifier with another throughout our source." 201 EXAMPLES = """\ 202 Examples: 203 204 rename_c_identifier.py set_ctrl_id set_controller_id 205 (Replaces every occurrence of "set_ctrl_id" with "set_controller_id".) 206 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.) 209 210 rename_c_identifier.py a b c d 211 (Replace "a" with "b", and "c" with "d".)""" 212 213 214 def revert_changes(): 215 """Tell git to revert all the changes in the current working tree. 216 """ 217 print('Reverting changes.', file=sys.stderr) 218 subprocess.run(["git", "checkout", "--quiet", TOPDIR]) 219 220 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) 226 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") 235 236 args = parser.parse_args(argv[1:]) 237 238 if len(args.more) % 2 != 0: 239 print("I require an even number of identifiers.", file=sys.stderr) 240 return 1 241 242 if args.commit and any_uncommitted_changes(): 243 print("Uncommitted changes found. Not running.", file=sys.stderr) 244 return 1 245 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)) 254 255 rewriter = Rewriter(pairs) 256 257 rewrite_files(list_c_files(), rewriter) 258 259 print("Replaced {} identifiers".format(rewriter.get_count()), 260 file=sys.stderr) 261 262 if args.commit: 263 commit(pairs, args.no_verify) 264 265 266 if __name__ == '__main__': 267 main(sys.argv)