summaryrefslogtreecommitdiffstats
path: root/tools/git/mffr.py
blob: 7f791d0f9d3a69ccf799ff9bb2cb9b51c68d29d3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
#!/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)
  if sys.platform == 'win32':
    posix_ere_original = posix_ere_original.replace('"', '""')
  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())