tor

The Tor anonymity network
git clone https://git.dasho.dev/tor.git
Log | Files | Refs | README | LICENSE

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)