123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267 |
- #!/usr/bin/env python3
- #
- # Copyright (c) 2001 Matej Pfajfar.
- # Copyright (c) 2001-2004, Roger Dingledine.
- # Copyright (c) 2004-2006, Roger Dingledine, Nick Mathewson.
- # Copyright (c) 2007-2019, The Tor Project, Inc.
- # See LICENSE for licensing information
- """
- Helpful script to replace one or more C identifiers, and optionally
- generate a commit message explaining what happened.
- """
- # Future imports for Python 2.7, mandatory in 3.0
- from __future__ import division
- from __future__ import print_function
- from __future__ import unicode_literals
- import argparse
- import fileinput
- import os
- import re
- import shlex
- import subprocess
- import sys
- import tempfile
- TOPDIR = "src"
- def is_c_file(fn):
- """
- Return true iff fn is the name of a C file.
- >>> is_c_file("a/b/module.c")
- True
- >>> is_c_file("a/b/module.h")
- True
- >>> is_c_file("a/b/module.c~")
- False
- >>> is_c_file("a/b/.module.c")
- False
- >>> is_c_file("a/b/module.cpp")
- False
- """
- fn = os.path.split(fn)[1]
- # Avoid editor temporary files
- if fn.startswith(".") or fn.startswith("#"):
- return False
- ext = os.path.splitext(fn)[1]
- return ext in {".c", ".h", ".i", ".inc"}
- def list_c_files(topdir=TOPDIR):
- """
- Use git to list all the C files under version control.
- >>> lst = list(list_c_files())
- >>> "src/core/mainloop/mainloop.c" in lst
- True
- >>> "src/core/mainloop/twiddledeedoo.c" in lst
- False
- >>> "micro-revision.i" in lst
- False
- """
- proc = subprocess.Popen(
- ["git", "ls-tree", "--name-only", "-r", "HEAD", topdir],
- stdout=subprocess.PIPE,
- encoding="utf-8")
- for line in proc.stdout.readlines():
- line = line.strip()
- if is_c_file(line):
- yield line
- class Rewriter:
- """
- A rewriter applies a series of word-by-word replacements, in
- sequence. Replacements only happen at "word boundaries",
- as determined by the \\b regular expression marker.
- ("A word is defined as a sequence of alphanumeric or underscore
- characters", according to the documentation.)
- >>> R = Rewriter([("magic", "secret"), ("words", "codes")])
- >>> R.apply("The magic words are rambunctious bluejay")
- 'The secret codes are rambunctious bluejay'
- >>> R.apply("The magical words are rambunctious bluejay")
- 'The magical codes are rambunctious bluejay'
- >>> R.get_count()
- 3
- """
- def __init__(self, replacements):
- """Make a new Rewriter. Takes a sequence of pairs of
- (from_id, to_id), where from_id is an identifier to replace,
- and to_id is its replacement.
- """
- self._patterns = []
- for id1, id2 in replacements:
- pat = re.compile(r"\b{}\b".format(re.escape(id1)))
- self._patterns.append((pat, id2))
- self._count = 0
- def apply(self, line):
- """Return `line` as transformed by this rewriter."""
- for pat, ident in self._patterns:
- line, count = pat.subn(ident, line)
- self._count += count
- return line
- def get_count(self):
- """Return the number of identifiers that this rewriter has
- rewritten."""
- return self._count
- def rewrite_files(files, rewriter):
- """
- Apply `rewriter` to every file in `files`, replacing those files
- with their rewritten contents.
- """
- for line in fileinput.input(files, inplace=True):
- sys.stdout.write(rewriter.apply(line))
- def make_commit_msg(pairs, no_verify):
- """Return a commit message to explain what was replaced by the provided
- arguments.
- """
- script = ["./scripts/maint/rename_c_identifier.py"]
- for id1, id2 in pairs:
- qid1 = shlex.quote(id1)
- qid2 = shlex.quote(id2)
- script.append(" {} {}".format(qid1, qid2))
- script = " \\\n".join(script)
- if len(pairs) == 1:
- line1 = "Rename {} to {}".format(*pairs[0])
- else:
- line1 = "Replace several C identifiers."
- msg = """\
- {}
- This is an automated commit, generated by this command:
- {}
- """.format(line1, script)
- if no_verify:
- msg += """
- It was generated with --no-verify, so it probably breaks some commit hooks.
- The committer should be sure to fix them up in a subsequent commit.
- """
- return msg
- def commit(pairs, no_verify=False):
- """Try to commit the current git state, generating the commit message as
- appropriate. If `no_verify` is True, pass the --no-verify argument to
- git commit.
- """
- args = []
- if no_verify:
- args.append("--no-verify")
- # We have to use a try block to delete the temporary file here, since we
- # are using tempfile with delete=False. We have to use delete=False,
- # since otherwise we are not guaranteed to be able to give the file to
- # git for it to open.
- fname = None
- try:
- with tempfile.NamedTemporaryFile(mode="w", delete=False) as f:
- fname = f.name
- f.write(make_commit_msg(pairs, no_verify))
- s = subprocess.run(["git", "commit", "-a", "-F", fname, "--edit"]+args)
- if s.returncode != 0 and not no_verify:
- print('"git commit" failed. Maybe retry with --no-verify?',
- file=sys.stderr)
- revert_changes()
- return False
- finally:
- os.unlink(fname)
- return True
- def any_uncommitted_changes():
- """Return True if git says there are any uncommitted changes in the current
- working tree; false otherwise.
- """
- s = subprocess.run(["git", "diff-index", "--quiet", "HEAD"])
- return s.returncode != 0
- DESC = "Replace one identifier with another throughout our source."
- EXAMPLES = """\
- Examples:
- rename_c_identifier.py set_ctrl_id set_controller_id
- (Replaces every occurrence of "set_ctrl_id" with "set_controller_id".)
- rename_c_identifier.py --commit set_ctrl_id set_controller_id
- (As above, but also generate a git commit with an appropriate message.)
- rename_c_identifier.py a b c d
- (Replace "a" with "b", and "c" with "d".)"""
- def revert_changes():
- """Tell git to revert all the changes in the current working tree.
- """
- print('Reverting changes.', file=sys.stderr)
- subprocess.run(["git", "checkout", "--quiet", TOPDIR])
- def main(argv):
- import argparse
- parser = argparse.ArgumentParser(description=DESC, epilog=EXAMPLES,
- # prevent re-wrapping the examples
- formatter_class=argparse.RawDescriptionHelpFormatter)
- parser.add_argument("--commit", action='store_true',
- help="Generate a Git commit.")
- parser.add_argument("--no-verify", action='store_true',
- help="Tell Git not to run its pre-commit hooks.")
- parser.add_argument("from_id", type=str, help="Original identifier")
- parser.add_argument("to_id", type=str, help="New identifier")
- parser.add_argument("more", type=str, nargs=argparse.REMAINDER,
- help="Additional identifier pairs")
- args = parser.parse_args(argv[1:])
- if len(args.more) % 2 != 0:
- print("I require an even number of identifiers.", file=sys.stderr)
- return 1
- if args.commit and any_uncommitted_changes():
- print("Uncommitted changes found. Not running.", file=sys.stderr)
- return 1
- pairs = []
- print("renaming {} to {}".format(args.from_id, args.to_id), file=sys.stderr)
- pairs.append((args.from_id, args.to_id))
- for idx in range(0, len(args.more), 2):
- id1 = args.more[idx]
- id2 = args.more[idx+1]
- print("renaming {} to {}".format(id1, id2))
- pairs.append((id1, id2))
- rewriter = Rewriter(pairs)
- rewrite_files(list_c_files(), rewriter)
- print("Replaced {} identifiers".format(rewriter.get_count()),
- file=sys.stderr)
- if args.commit:
- commit(pairs, args.no_verify)
- if __name__ == '__main__':
- main(sys.argv)
|