Skip to content

Commit

Permalink
Extract multi-file find and replace into a separate tool.
Browse files Browse the repository at this point in the history
BUG=none

Review URL: https://codereview.chromium.org/12149005

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@181693 0039d316-1c4b-4281-b951-d872f2087c98
  • Loading branch information
joi@chromium.org committed Feb 11, 2013
1 parent f1a6fd9 commit 9646105
Show file tree
Hide file tree
Showing 2 changed files with 119 additions and 46 deletions.
114 changes: 114 additions & 0 deletions tools/git/mffr.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
#!/usr/bin/env python
# Copyright (c) 2013 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""Usage: mffr.py [-d] [-g *.h] [-g *.cc] REGEXP REPLACEMENT
This tool performs a fast find-and-replace operation on files in
the current git repository.
The -d flag selects a default set of globs (C++ and Objective-C/C++
source files). The -g flag adds a single glob to the list and may
be used multiple times. If neither -d nor -g is specified, the tool
searches all files (*.*).
REGEXP uses full Python regexp syntax. REPLACEMENT can use
back-references.
"""

import re
import subprocess
import sys


# We need to use shell=True with subprocess on Windows so that it
# finds 'git' from the path, but can lead to undesired behavior on
# Linux.
_USE_SHELL = (sys.platform == 'win32')


def MultiFileFindReplace(original, replacement, file_globs):
"""Implements fast multi-file find and replace.
Given an |original| string and a |replacement| string, find matching
files by running git grep on |original| in files matching any
pattern in |file_globs|.
Once files are found, |re.sub| is run to replace |original| with
|replacement|. |replacement| may use capture group back-references.
Args:
original: '(#(include|import)\s*["<])chrome/browser/ui/browser.h([>"])'
replacement: '\1chrome/browser/ui/browser/browser.h\3'
file_globs: ['*.cc', '*.h', '*.m', '*.mm']
Returns the list of files modified.
Raises an exception on error.
"""
# Posix extended regular expressions do not reliably support the "\s"
# shorthand.
posix_ere_original = re.sub(r"\\s", "[[:space:]]", original)
out, err = subprocess.Popen(
['git', 'grep', '-E', '--name-only', posix_ere_original,
'--'] + file_globs,
stdout=subprocess.PIPE,
shell=_USE_SHELL).communicate()
referees = out.splitlines()

for referee in referees:
with open(referee) as f:
original_contents = f.read()
contents = re.sub(original, replacement, original_contents)
if contents == original_contents:
raise Exception('No change in file %s although matched in grep' %
referee)
with open(referee, 'wb') as f:
f.write(contents)

return referees


def main():
file_globs = []
force_unsafe_run = False
args = sys.argv[1:]
while args and args[0].startswith('-'):
if args[0] == '-d':
file_globs = ['*.cc', '*.h', '*.m', '*.mm']
args = args[1:]
elif args[0] == '-g':
file_globs.append(args[1])
args = args[2:]
elif args[0] == '-f':
force_unsafe_run = True
args = args[1:]
if not file_globs:
file_globs = ['*.*']
if not args:
print globals()['__doc__']
return 1
if not force_unsafe_run:
out, err = subprocess.Popen(['git', 'status', '--porcelain'],
stdout=subprocess.PIPE,
shell=_USE_SHELL).communicate()
if out:
print 'ERROR: This tool does not print any confirmation prompts,'
print 'so you should only run it with a clean staging area and cache'
print 'so that reverting a bad find/replace is as easy as running'
print ' git checkout -- .'
print ''
print 'To override this safeguard, pass the -f flag.'
return 1
original = args[0]
replacement = args[1]
print 'File globs: %s' % file_globs
print 'Original: %s' % original
print 'Replacement: %s' % replacement
MultiFileFindReplace(original, replacement, file_globs)
return 0


if __name__ == '__main__':
sys.exit(main())
51 changes: 5 additions & 46 deletions tools/git/move_source_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
import subprocess
import sys

import mffr

if __name__ == '__main__':
# Need to add the directory containing sort-headers.py to the Python
# classpath.
Expand Down Expand Up @@ -57,49 +59,6 @@ def MoveFile(from_path, to_path):
raise Exception('Fatal: Failed to run git mv command.')


def MultiFileFindReplace(original,
replacement,
file_globs):
"""Implements fast multi-file find and replace.
Given an |original| string and a |replacement| string, find matching
files by running git grep on |original| in files matching any
pattern in |file_globs|.
Once files are found, |re.sub| is run to replace |original| with
|replacement|. |replacement| may use capture group back-references.
Args:
original: '(#(include|import)\s*["<])chrome/browser/ui/browser.h([>"])'
replacement: '\1chrome/browser/ui/browser/browser.h\3'
file_globs: ['*.cc', '*.h', '*.m', '*.mm']
Returns the list of files modified.
Raises an exception on error.
"""
# Posix extended regular expressions do not reliably support the "\s"
# shorthand.
posix_ere_original = re.sub(r"\\s", "[[:space:]]", original)
out, err = subprocess.Popen(
['git', 'grep', '-E', '--name-only', posix_ere_original, '--'] +
file_globs,
stdout=subprocess.PIPE).communicate()
referees = out.splitlines()

for referee in referees:
with open(referee) as f:
original_contents = f.read()
contents = re.sub(original, replacement, original_contents)
if contents == original_contents:
raise Exception('No change in file %s although matched in grep' %
referee)
with open(referee, 'w') as f:
f.write(contents)

return referees


def UpdatePostMove(from_path, to_path):
"""Given a file that has moved from |from_path| to |to_path|,
updates the moved file's include guard to match the new path and
Expand All @@ -114,7 +73,7 @@ def UpdatePostMove(from_path, to_path):
UpdateIncludeGuard(from_path, to_path)

# Update include/import references.
files_with_changed_includes = MultiFileFindReplace(
files_with_changed_includes = mffr.MultiFileFindReplace(
r'(#(include|import)\s*["<])%s([>"])' % re.escape(from_path),
r'\1%s\3' % to_path,
['*.cc', '*.h', '*.m', '*.mm'])
Expand All @@ -130,7 +89,7 @@ def AlwaysConfirm(a, b): return True
# This work takes a bit of time. If this script starts feeling too
# slow, one good way to speed it up is to make the comment handling
# optional under a flag.
MultiFileFindReplace(
mffr.MultiFileFindReplace(
r'(//.*)%s' % re.escape(from_path),
r'\1%s' % to_path,
['*.cc', '*.h', '*.m', '*.mm'])
Expand All @@ -143,7 +102,7 @@ def PathMinusFirstComponent(path):
return parts[1]
else:
return parts[0]
MultiFileFindReplace(
mffr.MultiFileFindReplace(
r'([\'"])%s([\'"])' % re.escape(PathMinusFirstComponent(from_path)),
r'\1%s\2' % PathMinusFirstComponent(to_path),
['*.gyp*'])
Expand Down

0 comments on commit 9646105

Please sign in to comment.