summaryrefslogtreecommitdiffstats
path: root/tools/git/move_source_file.py
blob: f1dde3b3e11ff39023dba95e6b9b6ec50b9fd889 (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
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
#!/usr/bin/env python
# Copyright (c) 2012 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.

"""Moves C++ files to a new location, updating any include paths that point
to them, and re-ordering headers as needed.  If multiple source files are
specified, the destination must be a directory.  Updates include guards in
moved header files.  Assumes Chromium coding style.

Attempts to update paths used in .gyp(i) files, but does not reorder
or restructure .gyp(i) files in any way.

Updates full-path references to files in // comments in source files.

Must run in a git checkout, as it relies on git grep for a fast way to
find files that reference the moved file.
"""


import optparse
import os
import re
import subprocess
import sys

import mffr

if __name__ == '__main__':
  # Need to add the directory containing sort-headers.py to the Python
  # classpath.
  sys.path.append(os.path.abspath(os.path.join(sys.path[0], '..')))
sort_headers = __import__('sort-headers')
import sort_sources


HANDLED_EXTENSIONS = ['.cc', '.mm', '.h', '.hh', '.cpp']


def IsHandledFile(path):
  return os.path.splitext(path)[1] in HANDLED_EXTENSIONS


def MakeDestinationPath(from_path, to_path):
  """Given the from and to paths, return a correct destination path.

  The initial destination path may either a full path or a directory.
  Also does basic sanity checks.
  """
  if not IsHandledFile(from_path):
    raise Exception('Only intended to move individual source files '
                    '(%s does not have a recognized extension).' %
                    from_path)

  # Remove '.', '..', etc.
  to_path = os.path.normpath(to_path)

  if os.path.isdir(to_path):
    to_path = os.path.join(to_path, os.path.basename(from_path))
  else:
    dest_extension = os.path.splitext(to_path)[1]
    if dest_extension not in HANDLED_EXTENSIONS:
      raise Exception('Destination must be either a full path with '
                      'a recognized extension or a directory.')
  return to_path


def UpdateIncludePathForBlink(path):
  """Updates |path| as it would be when used in an include statement in Blink.

  As Blink has its 'public' and 'Source' folders in the include search path,
  these prefixes of file paths are not included in include statements. For
  example, if |path| is 'public/foo/bar.h', the matching include statement
  is '#include "foo/bar.h"'.
  """
  for prefix in ('public/', 'Source/'):
    if path.startswith(prefix):
      return path[len(prefix):]

  return path


def MoveFile(from_path, to_path):
  """Performs a git mv command to move a file from |from_path| to |to_path|.
  """
  if not os.system('git mv %s %s' % (from_path, to_path)) == 0:
    raise Exception('Fatal: Failed to run git mv command.')


def UpdatePostMove(from_path, to_path, in_blink):
  """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
  updates all references to the file in other source files. Also tries
  to update references in .gyp(i) files using a heuristic.
  """
  # Include paths always use forward slashes.
  from_path = from_path.replace('\\', '/')
  to_path = to_path.replace('\\', '/')

  if os.path.splitext(from_path)[1] in ['.h', '.hh']:
    UpdateIncludeGuard(from_path, to_path)

    from_include_path = from_path
    to_include_path = to_path
    if in_blink:
      from_include_path = UpdateIncludePathForBlink(from_include_path)
      to_include_path = UpdateIncludePathForBlink(to_include_path)

    # Update include/import references.
    files_with_changed_includes = mffr.MultiFileFindReplace(
        r'(#(include|import)\s*["<])%s([>"])' % re.escape(from_include_path),
        r'\1%s\3' % to_include_path,
        ['*.cc', '*.h', '*.m', '*.mm', '*.cpp'])

    # Reorder headers in files that changed.
    for changed_file in files_with_changed_includes:
      def AlwaysConfirm(a, b): return True
      sort_headers.FixFileWithConfirmFunction(changed_file, AlwaysConfirm, True,
                                              in_blink)

  # Update comments; only supports // comments, which are primarily
  # used in our code.
  #
  # 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.
  mffr.MultiFileFindReplace(
      r'(//.*)%s' % re.escape(from_path),
      r'\1%s' % to_path,
      ['*.cc', '*.h', '*.m', '*.mm', '*.cpp'])

  # Update references in GYP and BUILD.gn files.
  #
  # GYP files are mostly located under the first level directory (ex.
  # chrome/chrome_browser.gypi), but sometimes they are located in
  # directories at a deeper level (ex. extensions/shell/app_shell.gypi). On
  # the other hand, BUILD.gn files can be placed in any directories.
  #
  # Paths in a GYP or BUILD.gn file are relative to the directory where the
  # file is placed.
  #
  # For instance, "chrome/browser/chromeos/device_uma.h" is listed as
  # "browser/chromeos/device_uma.h" in "chrome/chrome_browser_chromeos.gypi",
  # but it's listed as "device_uma.h" in "chrome/browser/chromeos/BUILD.gn".
  #
  # To handle this, the code here will visit directories from the top level
  # src directory to the directory of |from_path| and try to update GYP and
  # BUILD.gn files in each directory.
  #
  # The code only handles files moved/renamed within the same build file. If
  # files are moved beyond the same build file, the affected build files
  # should be fixed manually.
  def SplitByFirstComponent(path):
    """'foo/bar/baz' -> ('foo', 'bar/baz')
       'bar' -> ('bar', '')
       '' -> ('', '')
    """
    parts = re.split(r"[/\\]", path, 1)
    if len(parts) == 2:
      return (parts[0], parts[1])
    else:
      return (parts[0], '')

  visiting_directory = ''
  from_rest = from_path
  to_rest = to_path
  while True:
    files_with_changed_sources = mffr.MultiFileFindReplace(
        r'([\'"])%s([\'"])' % from_rest,
        r'\1%s\2' % to_rest,
        [os.path.join(visiting_directory, 'BUILD.gn'),
         os.path.join(visiting_directory, '*.gyp*')])
    for changed_file in files_with_changed_sources:
      sort_sources.ProcessFile(changed_file, should_confirm=False)
    from_first, from_rest = SplitByFirstComponent(from_rest)
    to_first, to_rest = SplitByFirstComponent(to_rest)
    visiting_directory = os.path.join(visiting_directory, from_first)
    if not from_rest or not to_rest or from_rest == to_rest:
        break


def MakeIncludeGuardName(path_from_root):
  """Returns an include guard name given a path from root."""
  guard = path_from_root.replace('/', '_')
  guard = guard.replace('\\', '_')
  guard = guard.replace('.', '_')
  guard += '_'
  return guard.upper()


def UpdateIncludeGuard(old_path, new_path):
  """Updates the include guard in a file now residing at |new_path|,
  previously residing at |old_path|, with an up-to-date include guard.

  Prints a warning if the update could not be completed successfully (e.g.,
  because the old include guard was not formatted correctly per Chromium style).
  """
  old_guard = MakeIncludeGuardName(old_path)
  new_guard = MakeIncludeGuardName(new_path)

  with open(new_path) as f:
    contents = f.read()

  new_contents = contents.replace(old_guard, new_guard)
  # The file should now have three instances of the new guard: two at the top
  # of the file plus one at the bottom for the comment on the #endif.
  if new_contents.count(new_guard) != 3:
    print ('WARNING: Could not successfully update include guard; perhaps '
           'old guard is not per style guide? You will have to update the '
           'include guard manually. (%s)' % new_path)

  with open(new_path, 'w') as f:
    f.write(new_contents)

def main():
  if not os.path.isdir('.git'):
    print 'Fatal: You must run from the root of a git checkout.'
    return 1

  in_blink = os.getcwd().endswith("third_party/WebKit")

  parser = optparse.OptionParser(usage='%prog FROM_PATH... TO_PATH')
  parser.add_option('--already_moved', action='store_true',
                    dest='already_moved',
                    help='Causes the script to skip moving the file.')
  parser.add_option('--no_error_for_non_source_file', action='store_false',
                    default='True',
                    dest='error_for_non_source_file',
                    help='Causes the script to simply print a warning on '
                    'encountering a non-source file rather than raising an '
                    'error.')
  opts, args = parser.parse_args()

  if len(args) < 2:
    parser.print_help()
    return 1

  from_paths = args[:len(args)-1]
  orig_to_path = args[-1]

  if len(from_paths) > 1 and not os.path.isdir(orig_to_path):
    print 'Target %s is not a directory.' % orig_to_path
    print
    parser.print_help()
    return 1

  for from_path in from_paths:
    if not opts.error_for_non_source_file and not IsHandledFile(from_path):
      print '%s does not appear to be a source file, skipping' % (from_path)
      continue
    to_path = MakeDestinationPath(from_path, orig_to_path)
    if not opts.already_moved:
      MoveFile(from_path, to_path)
    UpdatePostMove(from_path, to_path, in_blink)
  return 0


if __name__ == '__main__':
  sys.exit(main())