Add support for escaped target names in isolate driver (4th try)
[chromium-blink-merge.git] / tools / git / move_source_file.py
blob18ef1ce82721ce348948d57dccd85f63d5d9ff6c
1 #!/usr/bin/env python
2 # Copyright (c) 2012 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 """Moves C++ files to a new location, updating any include paths that point
7 to them, and re-ordering headers as needed. If multiple source files are
8 specified, the destination must be a directory. Updates include guards in
9 moved header files. Assumes Chromium coding style.
11 Attempts to update paths used in .gyp(i) files, but does not reorder
12 or restructure .gyp(i) files in any way.
14 Updates full-path references to files in // comments in source files.
16 Must run in a git checkout, as it relies on git grep for a fast way to
17 find files that reference the moved file.
18 """
21 import optparse
22 import os
23 import re
24 import subprocess
25 import sys
27 import mffr
29 if __name__ == '__main__':
30 # Need to add the directory containing sort-headers.py to the Python
31 # classpath.
32 sys.path.append(os.path.abspath(os.path.join(sys.path[0], '..')))
33 sort_headers = __import__('sort-headers')
34 import sort_sources
37 HANDLED_EXTENSIONS = ['.cc', '.mm', '.h', '.hh', '.cpp']
40 def IsHandledFile(path):
41 return os.path.splitext(path)[1] in HANDLED_EXTENSIONS
44 def MakeDestinationPath(from_path, to_path):
45 """Given the from and to paths, return a correct destination path.
47 The initial destination path may either a full path or a directory.
48 Also does basic sanity checks.
49 """
50 if not IsHandledFile(from_path):
51 raise Exception('Only intended to move individual source files '
52 '(%s does not have a recognized extension).' %
53 from_path)
55 # Remove '.', '..', etc.
56 to_path = os.path.normpath(to_path)
58 if os.path.isdir(to_path):
59 to_path = os.path.join(to_path, os.path.basename(from_path))
60 else:
61 dest_extension = os.path.splitext(to_path)[1]
62 if dest_extension not in HANDLED_EXTENSIONS:
63 raise Exception('Destination must be either a full path with '
64 'a recognized extension or a directory.')
65 return to_path
68 def MoveFile(from_path, to_path):
69 """Performs a git mv command to move a file from |from_path| to |to_path|.
70 """
71 if not os.system('git mv %s %s' % (from_path, to_path)) == 0:
72 raise Exception('Fatal: Failed to run git mv command.')
75 def UpdatePostMove(from_path, to_path):
76 """Given a file that has moved from |from_path| to |to_path|,
77 updates the moved file's include guard to match the new path and
78 updates all references to the file in other source files. Also tries
79 to update references in .gyp(i) files using a heuristic.
80 """
81 # Include paths always use forward slashes.
82 from_path = from_path.replace('\\', '/')
83 to_path = to_path.replace('\\', '/')
85 if os.path.splitext(from_path)[1] in ['.h', '.hh']:
86 UpdateIncludeGuard(from_path, to_path)
88 # Update include/import references.
89 files_with_changed_includes = mffr.MultiFileFindReplace(
90 r'(#(include|import)\s*["<])%s([>"])' % re.escape(from_path),
91 r'\1%s\3' % to_path,
92 ['*.cc', '*.h', '*.m', '*.mm', '*.cpp'])
94 # Reorder headers in files that changed.
95 for changed_file in files_with_changed_includes:
96 def AlwaysConfirm(a, b): return True
97 sort_headers.FixFileWithConfirmFunction(changed_file, AlwaysConfirm, True)
99 # Update comments; only supports // comments, which are primarily
100 # used in our code.
102 # This work takes a bit of time. If this script starts feeling too
103 # slow, one good way to speed it up is to make the comment handling
104 # optional under a flag.
105 mffr.MultiFileFindReplace(
106 r'(//.*)%s' % re.escape(from_path),
107 r'\1%s' % to_path,
108 ['*.cc', '*.h', '*.m', '*.mm', '*.cpp'])
110 # Update references in GYP and BUILD.gn files.
112 # GYP files are mostly located under the first level directory (ex.
113 # chrome/chrome_browser.gypi), but sometimes they are located in
114 # directories at a deeper level (ex. extensions/shell/app_shell.gypi). On
115 # the other hand, BUILD.gn files can be placed in any directories.
117 # Paths in a GYP or BUILD.gn file are relative to the directory where the
118 # file is placed.
120 # For instance, "chrome/browser/chromeos/device_uma.h" is listed as
121 # "browser/chromeos/device_uma.h" in "chrome/chrome_browser_chromeos.gypi",
122 # but it's listed as "device_uma.h" in "chrome/browser/chromeos/BUILD.gn".
124 # To handle this, the code here will visit directories from the top level
125 # src directory to the directory of |from_path| and try to update GYP and
126 # BUILD.gn files in each directory.
128 # The code only handles files moved/renamed within the same build file. If
129 # files are moved beyond the same build file, the affected build files
130 # should be fixed manually.
131 def SplitByFirstComponent(path):
132 """'foo/bar/baz' -> ('foo', 'bar/baz')
133 'bar' -> ('bar', '')
134 '' -> ('', '')
136 parts = re.split(r"[/\\]", path, 1)
137 if len(parts) == 2:
138 return (parts[0], parts[1])
139 else:
140 return (parts[0], '')
142 visiting_directory = ''
143 from_rest = from_path
144 to_rest = to_path
145 while True:
146 files_with_changed_sources = mffr.MultiFileFindReplace(
147 r'([\'"])%s([\'"])' % from_rest,
148 r'\1%s\2' % to_rest,
149 [os.path.join(visiting_directory, 'BUILD.gn'),
150 os.path.join(visiting_directory, '*.gyp*')])
151 for changed_file in files_with_changed_sources:
152 sort_sources.ProcessFile(changed_file, should_confirm=False)
153 from_first, from_rest = SplitByFirstComponent(from_rest)
154 to_first, to_rest = SplitByFirstComponent(to_rest)
155 visiting_directory = os.path.join(visiting_directory, from_first)
156 if not from_rest or not to_rest:
157 break
160 def MakeIncludeGuardName(path_from_root):
161 """Returns an include guard name given a path from root."""
162 guard = path_from_root.replace('/', '_')
163 guard = guard.replace('\\', '_')
164 guard = guard.replace('.', '_')
165 guard += '_'
166 return guard.upper()
169 def UpdateIncludeGuard(old_path, new_path):
170 """Updates the include guard in a file now residing at |new_path|,
171 previously residing at |old_path|, with an up-to-date include guard.
173 Prints a warning if the update could not be completed successfully (e.g.,
174 because the old include guard was not formatted correctly per Chromium style).
176 old_guard = MakeIncludeGuardName(old_path)
177 new_guard = MakeIncludeGuardName(new_path)
179 with open(new_path) as f:
180 contents = f.read()
182 new_contents = contents.replace(old_guard, new_guard)
183 # The file should now have three instances of the new guard: two at the top
184 # of the file plus one at the bottom for the comment on the #endif.
185 if new_contents.count(new_guard) != 3:
186 print ('WARNING: Could not successfully update include guard; perhaps '
187 'old guard is not per style guide? You will have to update the '
188 'include guard manually. (%s)' % new_path)
190 with open(new_path, 'w') as f:
191 f.write(new_contents)
193 def main():
194 if not os.path.isdir('.git'):
195 print 'Fatal: You must run from the root of a git checkout.'
196 return 1
198 parser = optparse.OptionParser(usage='%prog FROM_PATH... TO_PATH')
199 parser.add_option('--already_moved', action='store_true',
200 dest='already_moved',
201 help='Causes the script to skip moving the file.')
202 parser.add_option('--no_error_for_non_source_file', action='store_false',
203 default='True',
204 dest='error_for_non_source_file',
205 help='Causes the script to simply print a warning on '
206 'encountering a non-source file rather than raising an '
207 'error.')
208 opts, args = parser.parse_args()
210 if len(args) < 2:
211 parser.print_help()
212 return 1
214 from_paths = args[:len(args)-1]
215 orig_to_path = args[-1]
217 if len(from_paths) > 1 and not os.path.isdir(orig_to_path):
218 print 'Target %s is not a directory.' % orig_to_path
219 print
220 parser.print_help()
221 return 1
223 for from_path in from_paths:
224 if not opts.error_for_non_source_file and not IsHandledFile(from_path):
225 print '%s does not appear to be a source file, skipping' % (from_path)
226 continue
227 to_path = MakeDestinationPath(from_path, orig_to_path)
228 if not opts.already_moved:
229 MoveFile(from_path, to_path)
230 UpdatePostMove(from_path, to_path)
231 return 0
234 if __name__ == '__main__':
235 sys.exit(main())