Bookmark Associator Refactoring
[chromium-blink-merge.git] / tools / git / mffr.py
blobd5b67c8c3f1d039be98132b6e8eacc170066f394
1 #!/usr/bin/env python
2 # Copyright (c) 2013 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file.
6 """Usage: mffr.py [-d] [-g *.h] [-g *.cc] REGEXP REPLACEMENT
8 This tool performs a fast find-and-replace operation on files in
9 the current git repository.
11 The -d flag selects a default set of globs (C++ and Objective-C/C++
12 source files). The -g flag adds a single glob to the list and may
13 be used multiple times. If neither -d nor -g is specified, the tool
14 searches all files (*.*).
16 REGEXP uses full Python regexp syntax. REPLACEMENT can use
17 back-references.
18 """
20 import optparse
21 import re
22 import subprocess
23 import sys
26 # We need to use shell=True with subprocess on Windows so that it
27 # finds 'git' from the path, but can lead to undesired behavior on
28 # Linux.
29 _USE_SHELL = (sys.platform == 'win32')
32 def MultiFileFindReplace(original, replacement, file_globs):
33 """Implements fast multi-file find and replace.
35 Given an |original| string and a |replacement| string, find matching
36 files by running git grep on |original| in files matching any
37 pattern in |file_globs|.
39 Once files are found, |re.sub| is run to replace |original| with
40 |replacement|. |replacement| may use capture group back-references.
42 Args:
43 original: '(#(include|import)\s*["<])chrome/browser/ui/browser.h([>"])'
44 replacement: '\1chrome/browser/ui/browser/browser.h\3'
45 file_globs: ['*.cc', '*.h', '*.m', '*.mm']
47 Returns the list of files modified.
49 Raises an exception on error.
50 """
51 # Posix extended regular expressions do not reliably support the "\s"
52 # shorthand.
53 posix_ere_original = re.sub(r"\\s", "[[:space:]]", original)
54 if sys.platform == 'win32':
55 posix_ere_original = posix_ere_original.replace('"', '""')
56 out, err = subprocess.Popen(
57 ['git', 'grep', '-E', '--name-only', posix_ere_original,
58 '--'] + file_globs,
59 stdout=subprocess.PIPE,
60 shell=_USE_SHELL).communicate()
61 referees = out.splitlines()
63 for referee in referees:
64 with open(referee) as f:
65 original_contents = f.read()
66 contents = re.sub(original, replacement, original_contents)
67 if contents == original_contents:
68 raise Exception('No change in file %s although matched in grep' %
69 referee)
70 with open(referee, 'wb') as f:
71 f.write(contents)
73 return referees
76 def main():
77 parser = optparse.OptionParser(usage='''
78 (1) %prog <options> REGEXP REPLACEMENT
79 REGEXP uses full Python regexp syntax. REPLACEMENT can use back-references.
81 (2) %prog <options> -i <file>
82 <file> should contain a list (in Python syntax) of
83 [REGEXP, REPLACEMENT, [GLOBS]] lists, e.g.:
85 [r"(foo|bar)", r"\1baz", ["*.cc", "*.h"]],
86 ["54", "42"],
88 As shown above, [GLOBS] can be omitted for a given search-replace list, in which
89 case the corresponding search-replace will use the globs specified on the
90 command line.''')
91 parser.add_option('-d', action='store_true',
92 dest='use_default_glob',
93 help='Perform the change on C++ and Objective-C(++) source '
94 'and header files.')
95 parser.add_option('-f', action='store_true',
96 dest='force_unsafe_run',
97 help='Perform the run even if there are uncommitted local '
98 'changes.')
99 parser.add_option('-g', action='append',
100 type='string',
101 default=[],
102 metavar="<glob>",
103 dest='user_supplied_globs',
104 help='Perform the change on the specified glob. Can be '
105 'specified multiple times, in which case the globs are '
106 'unioned.')
107 parser.add_option('-i', "--input_file",
108 type='string',
109 action='store',
110 default='',
111 metavar="<file>",
112 dest='input_filename',
113 help='Read arguments from <file> rather than the command '
114 'line. NOTE: To be sure of regular expressions being '
115 'interpreted correctly, use raw strings.')
116 opts, args = parser.parse_args()
117 if opts.use_default_glob and opts.user_supplied_globs:
118 print '"-d" and "-g" cannot be used together'
119 parser.print_help()
120 return 1
122 from_file = opts.input_filename != ""
123 if (from_file and len(args) != 0) or (not from_file and len(args) != 2):
124 parser.print_help()
125 return 1
127 if not opts.force_unsafe_run:
128 out, err = subprocess.Popen(['git', 'status', '--porcelain'],
129 stdout=subprocess.PIPE,
130 shell=_USE_SHELL).communicate()
131 if out:
132 print 'ERROR: This tool does not print any confirmation prompts,'
133 print 'so you should only run it with a clean staging area and cache'
134 print 'so that reverting a bad find/replace is as easy as running'
135 print ' git checkout -- .'
136 print ''
137 print 'To override this safeguard, pass the -f flag.'
138 return 1
140 global_file_globs = ['*.*']
141 if opts.use_default_glob:
142 global_file_globs = ['*.cc', '*.h', '*.m', '*.mm']
143 elif opts.user_supplied_globs:
144 global_file_globs = opts.user_supplied_globs
146 # Construct list of search-replace tasks.
147 search_replace_tasks = []
148 if opts.input_filename == '':
149 original = args[0]
150 replacement = args[1]
151 search_replace_tasks.append([original, replacement, global_file_globs])
152 else:
153 f = open(opts.input_filename)
154 search_replace_tasks = eval("".join(f.readlines()))
155 for task in search_replace_tasks:
156 if len(task) == 2:
157 task.append(global_file_globs)
158 f.close()
160 for (original, replacement, file_globs) in search_replace_tasks:
161 print 'File globs: %s' % file_globs
162 print 'Original: %s' % original
163 print 'Replacement: %s' % replacement
164 MultiFileFindReplace(original, replacement, file_globs)
165 return 0
168 if __name__ == '__main__':
169 sys.exit(main())