Upstreaming browser/ui/uikit_ui_util from iOS.
[chromium-blink-merge.git] / tools / git / move_source_file.py
blobf1dde3b3e11ff39023dba95e6b9b6ec50b9fd889
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 UpdateIncludePathForBlink(path):
69 """Updates |path| as it would be when used in an include statement in Blink.
71 As Blink has its 'public' and 'Source' folders in the include search path,
72 these prefixes of file paths are not included in include statements. For
73 example, if |path| is 'public/foo/bar.h', the matching include statement
74 is '#include "foo/bar.h"'.
75 """
76 for prefix in ('public/', 'Source/'):
77 if path.startswith(prefix):
78 return path[len(prefix):]
80 return path
83 def MoveFile(from_path, to_path):
84 """Performs a git mv command to move a file from |from_path| to |to_path|.
85 """
86 if not os.system('git mv %s %s' % (from_path, to_path)) == 0:
87 raise Exception('Fatal: Failed to run git mv command.')
90 def UpdatePostMove(from_path, to_path, in_blink):
91 """Given a file that has moved from |from_path| to |to_path|,
92 updates the moved file's include guard to match the new path and
93 updates all references to the file in other source files. Also tries
94 to update references in .gyp(i) files using a heuristic.
95 """
96 # Include paths always use forward slashes.
97 from_path = from_path.replace('\\', '/')
98 to_path = to_path.replace('\\', '/')
100 if os.path.splitext(from_path)[1] in ['.h', '.hh']:
101 UpdateIncludeGuard(from_path, to_path)
103 from_include_path = from_path
104 to_include_path = to_path
105 if in_blink:
106 from_include_path = UpdateIncludePathForBlink(from_include_path)
107 to_include_path = UpdateIncludePathForBlink(to_include_path)
109 # Update include/import references.
110 files_with_changed_includes = mffr.MultiFileFindReplace(
111 r'(#(include|import)\s*["<])%s([>"])' % re.escape(from_include_path),
112 r'\1%s\3' % to_include_path,
113 ['*.cc', '*.h', '*.m', '*.mm', '*.cpp'])
115 # Reorder headers in files that changed.
116 for changed_file in files_with_changed_includes:
117 def AlwaysConfirm(a, b): return True
118 sort_headers.FixFileWithConfirmFunction(changed_file, AlwaysConfirm, True,
119 in_blink)
121 # Update comments; only supports // comments, which are primarily
122 # used in our code.
124 # This work takes a bit of time. If this script starts feeling too
125 # slow, one good way to speed it up is to make the comment handling
126 # optional under a flag.
127 mffr.MultiFileFindReplace(
128 r'(//.*)%s' % re.escape(from_path),
129 r'\1%s' % to_path,
130 ['*.cc', '*.h', '*.m', '*.mm', '*.cpp'])
132 # Update references in GYP and BUILD.gn files.
134 # GYP files are mostly located under the first level directory (ex.
135 # chrome/chrome_browser.gypi), but sometimes they are located in
136 # directories at a deeper level (ex. extensions/shell/app_shell.gypi). On
137 # the other hand, BUILD.gn files can be placed in any directories.
139 # Paths in a GYP or BUILD.gn file are relative to the directory where the
140 # file is placed.
142 # For instance, "chrome/browser/chromeos/device_uma.h" is listed as
143 # "browser/chromeos/device_uma.h" in "chrome/chrome_browser_chromeos.gypi",
144 # but it's listed as "device_uma.h" in "chrome/browser/chromeos/BUILD.gn".
146 # To handle this, the code here will visit directories from the top level
147 # src directory to the directory of |from_path| and try to update GYP and
148 # BUILD.gn files in each directory.
150 # The code only handles files moved/renamed within the same build file. If
151 # files are moved beyond the same build file, the affected build files
152 # should be fixed manually.
153 def SplitByFirstComponent(path):
154 """'foo/bar/baz' -> ('foo', 'bar/baz')
155 'bar' -> ('bar', '')
156 '' -> ('', '')
158 parts = re.split(r"[/\\]", path, 1)
159 if len(parts) == 2:
160 return (parts[0], parts[1])
161 else:
162 return (parts[0], '')
164 visiting_directory = ''
165 from_rest = from_path
166 to_rest = to_path
167 while True:
168 files_with_changed_sources = mffr.MultiFileFindReplace(
169 r'([\'"])%s([\'"])' % from_rest,
170 r'\1%s\2' % to_rest,
171 [os.path.join(visiting_directory, 'BUILD.gn'),
172 os.path.join(visiting_directory, '*.gyp*')])
173 for changed_file in files_with_changed_sources:
174 sort_sources.ProcessFile(changed_file, should_confirm=False)
175 from_first, from_rest = SplitByFirstComponent(from_rest)
176 to_first, to_rest = SplitByFirstComponent(to_rest)
177 visiting_directory = os.path.join(visiting_directory, from_first)
178 if not from_rest or not to_rest or from_rest == to_rest:
179 break
182 def MakeIncludeGuardName(path_from_root):
183 """Returns an include guard name given a path from root."""
184 guard = path_from_root.replace('/', '_')
185 guard = guard.replace('\\', '_')
186 guard = guard.replace('.', '_')
187 guard += '_'
188 return guard.upper()
191 def UpdateIncludeGuard(old_path, new_path):
192 """Updates the include guard in a file now residing at |new_path|,
193 previously residing at |old_path|, with an up-to-date include guard.
195 Prints a warning if the update could not be completed successfully (e.g.,
196 because the old include guard was not formatted correctly per Chromium style).
198 old_guard = MakeIncludeGuardName(old_path)
199 new_guard = MakeIncludeGuardName(new_path)
201 with open(new_path) as f:
202 contents = f.read()
204 new_contents = contents.replace(old_guard, new_guard)
205 # The file should now have three instances of the new guard: two at the top
206 # of the file plus one at the bottom for the comment on the #endif.
207 if new_contents.count(new_guard) != 3:
208 print ('WARNING: Could not successfully update include guard; perhaps '
209 'old guard is not per style guide? You will have to update the '
210 'include guard manually. (%s)' % new_path)
212 with open(new_path, 'w') as f:
213 f.write(new_contents)
215 def main():
216 if not os.path.isdir('.git'):
217 print 'Fatal: You must run from the root of a git checkout.'
218 return 1
220 in_blink = os.getcwd().endswith("third_party/WebKit")
222 parser = optparse.OptionParser(usage='%prog FROM_PATH... TO_PATH')
223 parser.add_option('--already_moved', action='store_true',
224 dest='already_moved',
225 help='Causes the script to skip moving the file.')
226 parser.add_option('--no_error_for_non_source_file', action='store_false',
227 default='True',
228 dest='error_for_non_source_file',
229 help='Causes the script to simply print a warning on '
230 'encountering a non-source file rather than raising an '
231 'error.')
232 opts, args = parser.parse_args()
234 if len(args) < 2:
235 parser.print_help()
236 return 1
238 from_paths = args[:len(args)-1]
239 orig_to_path = args[-1]
241 if len(from_paths) > 1 and not os.path.isdir(orig_to_path):
242 print 'Target %s is not a directory.' % orig_to_path
243 print
244 parser.print_help()
245 return 1
247 for from_path in from_paths:
248 if not opts.error_for_non_source_file and not IsHandledFile(from_path):
249 print '%s does not appear to be a source file, skipping' % (from_path)
250 continue
251 to_path = MakeDestinationPath(from_path, orig_to_path)
252 if not opts.already_moved:
253 MoveFile(from_path, to_path)
254 UpdatePostMove(from_path, to_path, in_blink)
255 return 0
258 if __name__ == '__main__':
259 sys.exit(main())