Home | History | Annotate | Download | only in git
      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.
      5 
      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.
     10 
     11 Attempts to update paths used in .gyp(i) files, but does not reorder
     12 or restructure .gyp(i) files in any way.
     13 
     14 Updates full-path references to files in // comments in source files.
     15 
     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 """
     19 
     20 
     21 import optparse
     22 import os
     23 import re
     24 import subprocess
     25 import sys
     26 
     27 import mffr
     28 
     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 
     35 
     36 HANDLED_EXTENSIONS = ['.cc', '.mm', '.h', '.hh']
     37 
     38 
     39 def IsHandledFile(path):
     40   return os.path.splitext(path)[1] in HANDLED_EXTENSIONS
     41 
     42 
     43 def MakeDestinationPath(from_path, to_path):
     44   """Given the from and to paths, return a correct destination path.
     45 
     46   The initial destination path may either a full path or a directory.
     47   Also does basic sanity checks.
     48   """
     49   if not IsHandledFile(from_path):
     50     raise Exception('Only intended to move individual source files '
     51                     '(%s does not have a recognized extension).' %
     52                     from_path)
     53   if os.path.isdir(to_path):
     54     to_path = os.path.join(to_path, os.path.basename(from_path))
     55   else:
     56     dest_extension = os.path.splitext(to_path)[1]
     57     if dest_extension not in HANDLED_EXTENSIONS:
     58       raise Exception('Destination must be either a full path with '
     59                       'a recognized extension or a directory.')
     60   return to_path
     61 
     62 
     63 def MoveFile(from_path, to_path):
     64   """Performs a git mv command to move a file from |from_path| to |to_path|.
     65   """
     66   if not os.system('git mv %s %s' % (from_path, to_path)) == 0:
     67     raise Exception('Fatal: Failed to run git mv command.')
     68 
     69 
     70 def UpdatePostMove(from_path, to_path):
     71   """Given a file that has moved from |from_path| to |to_path|,
     72   updates the moved file's include guard to match the new path and
     73   updates all references to the file in other source files. Also tries
     74   to update references in .gyp(i) files using a heuristic.
     75   """
     76   # Include paths always use forward slashes.
     77   from_path = from_path.replace('\\', '/')
     78   to_path = to_path.replace('\\', '/')
     79 
     80   if os.path.splitext(from_path)[1] in ['.h', '.hh']:
     81     UpdateIncludeGuard(from_path, to_path)
     82 
     83     # Update include/import references.
     84     files_with_changed_includes = mffr.MultiFileFindReplace(
     85         r'(#(include|import)\s*["<])%s([>"])' % re.escape(from_path),
     86         r'\1%s\3' % to_path,
     87         ['*.cc', '*.h', '*.m', '*.mm'])
     88 
     89     # Reorder headers in files that changed.
     90     for changed_file in files_with_changed_includes:
     91       def AlwaysConfirm(a, b): return True
     92       sort_headers.FixFileWithConfirmFunction(changed_file, AlwaysConfirm, True)
     93 
     94   # Update comments; only supports // comments, which are primarily
     95   # used in our code.
     96   #
     97   # This work takes a bit of time. If this script starts feeling too
     98   # slow, one good way to speed it up is to make the comment handling
     99   # optional under a flag.
    100   mffr.MultiFileFindReplace(
    101       r'(//.*)%s' % re.escape(from_path),
    102       r'\1%s' % to_path,
    103       ['*.cc', '*.h', '*.m', '*.mm'])
    104 
    105   # Update references in .gyp(i) files.
    106   def PathMinusFirstComponent(path):
    107     """foo/bar/baz -> bar/baz"""
    108     parts = re.split(r"[/\\]", path, 1)
    109     if len(parts) == 2:
    110       return parts[1]
    111     else:
    112       return parts[0]
    113   mffr.MultiFileFindReplace(
    114       r'([\'"])%s([\'"])' % re.escape(PathMinusFirstComponent(from_path)),
    115       r'\1%s\2' % PathMinusFirstComponent(to_path),
    116       ['*.gyp*'])
    117 
    118 
    119 def MakeIncludeGuardName(path_from_root):
    120   """Returns an include guard name given a path from root."""
    121   guard = path_from_root.replace('/', '_')
    122   guard = guard.replace('\\', '_')
    123   guard = guard.replace('.', '_')
    124   guard += '_'
    125   return guard.upper()
    126 
    127 
    128 def UpdateIncludeGuard(old_path, new_path):
    129   """Updates the include guard in a file now residing at |new_path|,
    130   previously residing at |old_path|, with an up-to-date include guard.
    131 
    132   Prints a warning if the update could not be completed successfully (e.g.,
    133   because the old include guard was not formatted correctly per Chromium style).
    134   """
    135   old_guard = MakeIncludeGuardName(old_path)
    136   new_guard = MakeIncludeGuardName(new_path)
    137 
    138   with open(new_path) as f:
    139     contents = f.read()
    140 
    141   new_contents = contents.replace(old_guard, new_guard)
    142   # The file should now have three instances of the new guard: two at the top
    143   # of the file plus one at the bottom for the comment on the #endif.
    144   if new_contents.count(new_guard) != 3:
    145     print ('WARNING: Could not successfully update include guard; perhaps '
    146            'old guard is not per style guide? You will have to update the '
    147            'include guard manually. (%s)' % new_path)
    148 
    149   with open(new_path, 'w') as f:
    150     f.write(new_contents)
    151 
    152 def main():
    153   if not os.path.isdir('.git'):
    154     print 'Fatal: You must run from the root of a git checkout.'
    155     return 1
    156 
    157   parser = optparse.OptionParser(usage='%prog FROM_PATH... TO_PATH')
    158   parser.add_option('--already_moved', action='store_true',
    159                     dest='already_moved',
    160                     help='Causes the script to skip moving the file.')
    161   parser.add_option('--no_error_for_non_source_file', action='store_false',
    162                     default='True',
    163                     dest='error_for_non_source_file',
    164                     help='Causes the script to simply print a warning on '
    165                     'encountering a non-source file rather than raising an '
    166                     'error.')
    167   opts, args = parser.parse_args()
    168 
    169   if len(args) < 2:
    170     parser.print_help()
    171     return 1
    172 
    173   from_paths = args[:len(args)-1]
    174   orig_to_path = args[-1]
    175 
    176   if len(from_paths) > 1 and not os.path.isdir(orig_to_path):
    177     print 'Target %s is not a directory.' % orig_to_path
    178     print
    179     parser.print_help()
    180     return 1
    181 
    182   for from_path in from_paths:
    183     if not opts.error_for_non_source_file and not IsHandledFile(from_path):
    184       print '%s does not appear to be a source file, skipping' % (from_path)
    185       continue
    186     to_path = MakeDestinationPath(from_path, orig_to_path)
    187     if not opts.already_moved:
    188       MoveFile(from_path, to_path)
    189     UpdatePostMove(from_path, to_path)
    190   return 0
    191 
    192 
    193 if __name__ == '__main__':
    194   sys.exit(main())
    195