summaryrefslogtreecommitdiffstats
path: root/tools/refactor/move_file.py
blob: c379cdf38af2521e3cda9c8b0f40962a85d32b60 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
#!/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 a C++ file to a new location, updating any include paths that
point to it.  Updates include guards in moved header files.  Assumes
Chromium coding style.

Does not reorder headers (you can use tools/sort-headers.py), and does
not update .gypi files.

Relies on git for a fast way to find files that include the moved file.
"""


import os
import subprocess
import sys


HANDLED_EXTENSIONS = ['.cc', '.mm', '.h', '.hh']


def MoveFile(from_path, to_path):
  """Moves a file from |from_path| to |to_path|, updating its include
  guard to match the new path and updating all #includes and #imports
  of the file in other files in the same git repository, with the
  assumption that they include it using the Chromium style
  guide-standard full path from root.
  """
  extension = os.path.splitext(from_path)[1]
  if extension not in HANDLED_EXTENSIONS:
    raise Exception('Only intended to move individual source files.')

  dest_extension = os.path.splitext(to_path)[1]
  if dest_extension not in HANDLED_EXTENSIONS:
    if to_path.endswith('/') or to_path.endswith('\\'):
      to_path += os.path.basename(from_path)
    else:
      raise Exception('Destination must be either full path or end with /.')

  if not os.system('git mv %s %s' % (from_path, to_path)) == 0:
    raise Exception('Fatal: Failed to run git mv command.')

  if extension in ['.h', '.hh']:
    UpdateIncludeGuard(from_path, to_path)
    UpdateIncludes(from_path, to_path)


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.

  Errors out if an include guard per Chromium style guide cannot be
  found for the old path.
  """
  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)
  if new_contents == contents:
    raise Exception(
      'Error updating include guard; perhaps old guard is not per style guide?')

  with open(new_path, 'w') as f:
    f.write(new_contents)


def UpdateIncludes(old_path, new_path):
  """Given the |old_path| and |new_path| of a file being moved, update
  #include and #import statements in all files in the same git
  repository referring to the moved file.
  """
  # Include paths always use forward slashes.
  old_path = old_path.replace('\\', '/')
  new_path = new_path.replace('\\', '/')

  out, err = subprocess.Popen(
    ['git', 'grep', '--name-only',
     r'#\(include\|import\)\s*["<]%s[>"]' % old_path],
    stdout=subprocess.PIPE).communicate()
  includees = out.splitlines()

  for includee in includees:
    with open(includee) as f:
      contents = f.read()
    new_contents = contents.replace('"%s"' % old_path, '"%s"' % new_path)
    new_contents = new_contents.replace('<%s>' % old_path, '<%s>' % new_path)
    if new_contents == contents:
      raise Exception('Error updating include in file %s' % includee)
    with open(includee, '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
  args = sys.argv[1:]
  if len(args) != 2:
    print 'Usage: move_file.py FROM_PATH TO_PATH\n\n%s' % __doc__
    return 1
  MoveFile(args[0], args[1])
  return 0


if __name__ == '__main__':
  sys.exit(main())