OLD | NEW |
---|---|
1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | 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 | 3 # Use of this source code is governed by a BSD-style license that can be |
4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
5 | 5 |
6 """Moves a C++ file to a new location, updating any include paths that | 6 """Moves a C++ file to a new location, updating any include paths that |
7 point to it. Updates include guards in moved header files. Assumes | 7 point to it. Updates include guards in moved header files. Assumes |
8 Chromium coding style. | 8 Chromium coding style. |
9 | 9 |
10 Does not reorder headers (you can use tools/sort-headers.py), and does | 10 Does not reorder headers; instead, use this after committing all of |
11 not update .gypi files. | 11 your moves: |
12 ./tools/git/for-all-touched-files.py -c "tools/sort-headers.py [[FILENAME]]" | |
12 | 13 |
13 Relies on git for a fast way to find files that include the moved file. | 14 Updates paths used in .gyp(i) files, but does not reorder or |
15 restructure .gyp(i) files in any way. | |
16 | |
17 Must run in a git checkout, as it relies on git for a fast way to find | |
18 files that reference the moved file. | |
14 """ | 19 """ |
15 | 20 |
16 | 21 |
17 import os | 22 import os |
23 import re | |
18 import subprocess | 24 import subprocess |
19 import sys | 25 import sys |
20 | 26 |
21 | |
22 HANDLED_EXTENSIONS = ['.cc', '.mm', '.h', '.hh'] | 27 HANDLED_EXTENSIONS = ['.cc', '.mm', '.h', '.hh'] |
23 | 28 |
24 | 29 |
25 def MoveFile(from_path, to_path): | 30 def MakeDestinationPath(from_path, to_path): |
26 """Moves a file from |from_path| to |to_path|, updating its include | 31 """Given the from and to paths, return a correct destination path. |
27 guard to match the new path and updating all #includes and #imports | 32 |
28 of the file in other files in the same git repository, with the | 33 The initial destination path may either a full path or a directory, |
29 assumption that they include it using the Chromium style | 34 in which case the path must end with /. Also does basic sanity |
30 guide-standard full path from root. | 35 checks. |
31 """ | 36 """ |
32 extension = os.path.splitext(from_path)[1] | 37 if os.path.splitext(from_path)[1] not in HANDLED_EXTENSIONS: |
33 if extension not in HANDLED_EXTENSIONS: | |
34 raise Exception('Only intended to move individual source files.') | 38 raise Exception('Only intended to move individual source files.') |
35 | |
36 dest_extension = os.path.splitext(to_path)[1] | 39 dest_extension = os.path.splitext(to_path)[1] |
37 if dest_extension not in HANDLED_EXTENSIONS: | 40 if dest_extension not in HANDLED_EXTENSIONS: |
38 if to_path.endswith('/') or to_path.endswith('\\'): | 41 if to_path.endswith('/') or to_path.endswith('\\'): |
39 to_path += os.path.basename(from_path) | 42 to_path += os.path.basename(from_path) |
40 else: | 43 else: |
41 raise Exception('Destination must be either full path or end with /.') | 44 raise Exception('Destination must be either full path or end with /.') |
45 return to_path | |
42 | 46 |
47 | |
48 def MoveFile(from_path, to_path): | |
49 """Performs a git mv command to move a file from |from_path| to |to_path|. | |
50 """ | |
43 if not os.system('git mv %s %s' % (from_path, to_path)) == 0: | 51 if not os.system('git mv %s %s' % (from_path, to_path)) == 0: |
44 raise Exception('Fatal: Failed to run git mv command.') | 52 raise Exception('Fatal: Failed to run git mv command.') |
45 | 53 |
46 if extension in ['.h', '.hh']: | 54 |
55 def MultiFileFindReplace(original, | |
56 replacement, | |
57 grep_pattern, | |
58 file_globs, | |
59 guard_formats, | |
60 matchers=None): | |
61 """Implements fast multi-file find and replace with optional guards. | |
62 | |
63 Given an |original| string and a |replacement| string, search for | |
64 them by formatting |grep_pattern| with |original| and running git | |
65 grep on the result, for files matching any of |file_globs|. | |
66 | |
67 Once files are found, two things are done in this sequence: | |
68 | |
69 a) Search for any of |guard_formats| formatted with |original| and | |
70 replace each match with the same guard format as matched, formatted | |
71 with |replacement|. | |
72 | |
73 b) Call re.sub(matcher, ...) for each of |matchers| in a way that | |
74 replaces the matched string with the matched string with |original| | |
75 replaced with |replacement|. | |
76 | |
77 Args: | |
78 original: 'chrome/browser/ui/browser.h' | |
79 replacement: 'chrome/browser/ui/browser/browser.h' | |
80 grep_pattern: r'#\(include\|import\)\s*["<]%s[>"]' | |
81 file_globs: ['*.cc', '*.h', '*.m', '*.mm'] | |
82 guard_formats: None or ('"%s"', '<%s>') | |
83 matchers: None or ('//.*%s' % original, ) | |
84 | |
85 Raises an exception on error. | |
86 """ | |
87 out, err = subprocess.Popen( | |
88 ['git', 'grep', '--name-only', | |
89 grep_pattern % original, '--'] + file_globs, | |
Nico
2012/11/15 18:19:02
If you pass -E, then the called doesn't have to es
Jói
2012/11/20 14:43:12
Done.
| |
90 stdout=subprocess.PIPE).communicate() | |
91 referees = out.splitlines() | |
92 | |
93 for referee in referees: | |
94 with open(referee) as f: | |
95 original_contents = f.read() | |
96 contents = original_contents | |
97 for guard_format in guard_formats or []: | |
98 contents = contents.replace(guard_format % original, | |
99 guard_format % replacement) | |
100 for matcher in matchers or []: | |
101 def ReplaceMatch(match_obj): | |
102 return match_obj.group(0).replace(original, replacement) | |
103 contents = re.sub(matcher, ReplaceMatch, contents) | |
104 if contents == original_contents: | |
105 raise Exception('No change in file %s although matched in grep' % | |
106 referee) | |
107 with open(referee, 'w') as f: | |
108 f.write(contents) | |
109 | |
110 | |
111 def UpdatePostMove(from_path, to_path): | |
112 """Given a file that has moved from |from_path| to |to_path|, | |
113 updates the moved file's include guard to match the new path and | |
114 updates all references to the file in other source files and .gyp(i) | |
115 files in the same git repository, with the assumption that they | |
116 include it using the Chromium style guide-standard full path from | |
117 root, or the .gyp(i) standard full path from root minus first path | |
118 component. | |
119 """ | |
120 # Include paths always use forward slashes. | |
121 from_path = from_path.replace('\\', '/') | |
122 to_path = to_path.replace('\\', '/') | |
123 | |
124 if os.path.splitext(from_path)[1] in ['.h', '.hh']: | |
47 UpdateIncludeGuard(from_path, to_path) | 125 UpdateIncludeGuard(from_path, to_path) |
48 UpdateIncludes(from_path, to_path) | 126 |
127 # Update include/import references. | |
128 MultiFileFindReplace( | |
129 from_path, | |
130 to_path, | |
131 r'#\(include\|import\)\s*["<]%s[>"]', | |
132 ['*.cc', '*.h', '*.m', '*.mm'], | |
133 ['"%s"', '<%s>']) | |
134 | |
135 # Update comments; only supports // comments, which are primarily | |
136 # used in our code. | |
137 MultiFileFindReplace( | |
138 from_path, | |
139 to_path, | |
140 r'//.*%s', | |
141 ['*.cc', '*.h', '*.m', '*.mm'], | |
142 guard_formats=None, | |
143 matchers=['//.*%s' % from_path]) | |
144 | |
145 # Update references in .gyp(i) files. | |
146 def PathMinusFirstComponent(path): | |
147 parts = re.split(r"[/\\]", path, 1) | |
148 if len(parts) == 2: | |
149 return parts[1] | |
150 else: | |
151 return parts[0] | |
152 MultiFileFindReplace(PathMinusFirstComponent(from_path), | |
153 PathMinusFirstComponent(to_path), | |
154 r'[\'"]%s[\'"]', | |
155 ['*.gyp*'], | |
156 ["'%s'", '"%s"']) | |
49 | 157 |
50 | 158 |
51 def MakeIncludeGuardName(path_from_root): | 159 def MakeIncludeGuardName(path_from_root): |
52 """Returns an include guard name given a path from root.""" | 160 """Returns an include guard name given a path from root.""" |
53 guard = path_from_root.replace('/', '_') | 161 guard = path_from_root.replace('/', '_') |
54 guard = guard.replace('\\', '_') | 162 guard = guard.replace('\\', '_') |
55 guard = guard.replace('.', '_') | 163 guard = guard.replace('.', '_') |
56 guard += '_' | 164 guard += '_' |
57 return guard.upper() | 165 return guard.upper() |
58 | 166 |
(...skipping 13 matching lines...) Expand all Loading... | |
72 | 180 |
73 new_contents = contents.replace(old_guard, new_guard) | 181 new_contents = contents.replace(old_guard, new_guard) |
74 if new_contents == contents: | 182 if new_contents == contents: |
75 raise Exception( | 183 raise Exception( |
76 'Error updating include guard; perhaps old guard is not per style guide?') | 184 'Error updating include guard; perhaps old guard is not per style guide?') |
77 | 185 |
78 with open(new_path, 'w') as f: | 186 with open(new_path, 'w') as f: |
79 f.write(new_contents) | 187 f.write(new_contents) |
80 | 188 |
81 | 189 |
82 def UpdateIncludes(old_path, new_path): | |
83 """Given the |old_path| and |new_path| of a file being moved, update | |
84 #include and #import statements in all files in the same git | |
85 repository referring to the moved file. | |
86 """ | |
87 # Include paths always use forward slashes. | |
88 old_path = old_path.replace('\\', '/') | |
89 new_path = new_path.replace('\\', '/') | |
90 | |
91 out, err = subprocess.Popen( | |
92 ['git', 'grep', '--name-only', | |
93 r'#\(include\|import\)\s*["<]%s[>"]' % old_path], | |
94 stdout=subprocess.PIPE).communicate() | |
95 includees = out.splitlines() | |
96 | |
97 for includee in includees: | |
98 with open(includee) as f: | |
99 contents = f.read() | |
100 new_contents = contents.replace('"%s"' % old_path, '"%s"' % new_path) | |
101 new_contents = new_contents.replace('<%s>' % old_path, '<%s>' % new_path) | |
102 if new_contents == contents: | |
103 raise Exception('Error updating include in file %s' % includee) | |
104 with open(includee, 'w') as f: | |
105 f.write(new_contents) | |
106 | |
107 | |
108 def main(): | 190 def main(): |
109 if not os.path.isdir('.git'): | 191 if not os.path.isdir('.git'): |
110 print 'Fatal: You must run from the root of a git checkout.' | 192 print 'Fatal: You must run from the root of a git checkout.' |
111 return 1 | 193 return 1 |
112 args = sys.argv[1:] | 194 args = sys.argv[1:] |
113 if len(args) != 2: | 195 if not len(args) in [2, 3]: |
114 print 'Usage: move_file.py FROM_PATH TO_PATH\n\n%s' % __doc__ | 196 print ('Usage: move_source_file.py [--already-moved] FROM_PATH TO_PATH' |
197 '\n\n%s' % __doc__) | |
115 return 1 | 198 return 1 |
116 MoveFile(args[0], args[1]) | 199 |
200 already_moved = False | |
201 if args[0] == '--already-moved': | |
202 args = args[1:] | |
203 already_moved = True | |
204 | |
205 from_path = args[0] | |
206 to_path = args[1] | |
207 | |
208 to_path = MakeDestinationPath(from_path, to_path) | |
209 if not already_moved: | |
210 MoveFile(from_path, to_path) | |
211 UpdatePostMove(from_path, to_path) | |
117 return 0 | 212 return 0 |
118 | 213 |
119 | 214 |
120 if __name__ == '__main__': | 215 if __name__ == '__main__': |
121 sys.exit(main()) | 216 sys.exit(main()) |
OLD | NEW |