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
7to them, and re-ordering headers as needed.  If multiple source files are
8specified, the destination must be a directory.  Updates include guards in
9moved header files.  Assumes Chromium coding style.
10
11Attempts to update paths used in .gyp(i) files, but does not reorder
12or restructure .gyp(i) files in any way.
13
14Updates full-path references to files in // comments in source files.
15
16Must run in a git checkout, as it relies on git grep for a fast way to
17find files that reference the moved file.
18"""
19
20
21import optparse
22import os
23import re
24import subprocess
25import sys
26
27import mffr
28
29if __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], '..')))
33sort_headers = __import__('sort-headers')
34
35
36HANDLED_EXTENSIONS = ['.cc', '.mm', '.h', '.hh']
37
38
39def IsHandledFile(path):
40  return os.path.splitext(path)[1] in HANDLED_EXTENSIONS
41
42
43def 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
63def 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
70def 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
119def 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
128def 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
152def 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
193if __name__ == '__main__':
194  sys.exit(main())
195