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
10 Helpful script to replace one or more C identifiers, and optionally
11 generate a commit message explaining what happened.
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
33 Return true iff fn is the name of a C file.
35 >>> is_c_file("a/b/module.c")
37 >>> is_c_file("a/b/module.h")
39 >>> is_c_file("a/b/module.c~")
41 >>> is_c_file("a/b/.module.c")
43 >>> is_c_file("a/b/module.cpp")
46 fn
= os
.path
.split(fn
)[1]
47 # Avoid editor temporary files
48 if fn
.startswith(".") or fn
.startswith("#"):
50 ext
= os
.path
.splitext(fn
)[1]
51 return ext
in {".c", ".h", ".i", ".inc"}
54 def list_c_files(topdir
=TOPDIR
):
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
61 >>> "src/core/mainloop/twiddledeedoo.c" in lst
63 >>> "micro-revision.i" in lst
66 proc
= subprocess
.Popen(
67 ["git", "ls-tree", "--name-only", "-r", "HEAD", topdir
],
68 stdout
=subprocess
.PIPE
,
70 for line
in proc
.stdout
.readlines():
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'
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.
101 for id1
, id2
in replacements
:
102 pat
= re
.compile(r
"\b{}\b".format(re
.escape(id1
)))
103 self
._patterns
.append((pat
, id2
))
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
)
115 """Return the number of identifiers that this rewriter has
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
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
)
141 line1
= "Rename {} to {}".format(*pairs
[0])
143 line1
= "Replace several C identifiers."
148 This is an automated commit, generated by this command:
151 """.format(line1
, script
)
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.
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
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.
177 with tempfile
.NamedTemporaryFile(mode
="w", delete
=False) as f
:
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?',
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."
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
])
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
)
242 if args
.commit
and any_uncommitted_changes():
243 print("Uncommitted changes found. Not running.", file=sys
.stderr
)
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):
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()),
263 commit(pairs
, args
.no_verify
)
266 if __name__
== '__main__':