#!/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') HANDLED_EXTENSIONS = ['.cc', '.mm', '.h', '.hh'] 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) 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 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): """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) # Update include/import references. 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']) # 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) # 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']) # Update references in .gyp(i) files. def PathMinusFirstComponent(path): """foo/bar/baz -> bar/baz""" parts = re.split(r"[/\\]", path, 1) if len(parts) == 2: return parts[1] else: return parts[0] mffr.MultiFileFindReplace( r'([\'"])%s([\'"])' % re.escape(PathMinusFirstComponent(from_path)), r'\1%s\2' % PathMinusFirstComponent(to_path), ['*.gyp*']) 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 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) return 0 if __name__ == '__main__': sys.exit(main())