Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(84)

Side by Side Diff: roll_dep.py

Issue 318153003: Convenience tool for rolling git-style deps with an svn revision. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/depot_tools
Patch Set: Created 6 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « roll-dep.bat ('k') | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 #!/usr/bin/env python
2 # Copyright (c) 2014 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 """This scripts takes the path to a dep and an svn revision, and updates the
7 parent repo's DEPS file with the corresponding git revision. Sample invocation:
8
9 [chromium/src]$ roll-dep third_party/WebKit 12345
10
11 After the script completes, the DEPS file will be dirty with the new revision.
12 The user can then:
13
14 $ git add DEPS
15 $ git commit
16 """
17
18 import ast
19 import os
20 import re
21 import sys
22
23 from itertools import izip
24 from subprocess import Popen, PIPE
25 from textwrap import dedent
26
27
28 def posix_path(path):
29 """Convert a possibly-Windows path to a posix-style path."""
30 return re.sub('^[A-Z]:', '', path.replace(os.sep, '/'))
31
32
33 def platform_path(path):
34 """Convert a path to the native path format of the host OS."""
35 return path.replace('/', os.sep)
36
37
38 def find_gclient_root():
39 """Find the directory containing the .gclient file."""
40 cwd = posix_path(os.getcwd())
41 result = ''
42 for _ in xrange(len(cwd.split('/'))):
43 if os.path.exists(os.path.join(result, '.gclient')):
44 return result
45 result = os.path.join(result, os.pardir)
46 assert False, 'Could not find root of your gclient checkout.'
47
48
49 def get_solution(gclient_root, dep_path):
50 """Find the solution in .gclient containing the dep being rolled."""
51 dep_path = os.path.relpath(dep_path, gclient_root)
52 cwd = os.getcwd().rstrip(os.sep) + os.sep
53 gclient_root = os.path.realpath(gclient_root)
54 gclient_path = os.path.join(gclient_root, '.gclient')
55 gclient_locals = {}
56 execfile(gclient_path, {}, gclient_locals)
57 for soln in gclient_locals['solutions']:
58 soln_relpath = platform_path(soln['name'].rstrip('/')) + os.sep
59 if (dep_path.startswith(soln_relpath) or
60 cwd.startswith(os.path.join(gclient_root, soln_relpath))):
61 return soln
62 assert False, 'Could not determine the parent project for %s' % dep_path
63
64
65 def verify_git_revision(dep_path, revision):
66 """Verify that a git revision exists in a repository."""
67 p = Popen(['git', 'rev-list', '-n', '1', revision],
68 cwd=dep_path, stdout=PIPE, stderr=PIPE)
69 result = p.communicate()[0].strip()
70 if p.returncode != 0 or not re.match('^[a-fA-F0-9]{40}$', result):
71 result = None
72 return result
73
74
75 def convert_svn_revision(dep_path, revision):
76 """Find the git revision corresponding to an svn revision."""
77 err_msg = 'Unknown error'
78 revision = int(revision)
79 with open(os.devnull, 'w') as devnull:
80 for ref in ('HEAD', 'origin/master'):
81 try:
82 log_p = Popen(['git', 'log', ref],
83 cwd=dep_path, stdout=PIPE, stderr=devnull)
84 grep_p = Popen(['grep', '-e', '^commit ', '-e', '^ *git-svn-id: '],
85 stdin=log_p.stdout, stdout=PIPE, stderr=devnull)
86 git_rev = None
87 prev_svn_rev = None
88 for line in grep_p.stdout:
89 if line.startswith('commit '):
90 git_rev = line.split()[1]
91 continue
92 try:
93 svn_rev = int(line.split()[1].partition('@')[2])
94 except (IndexError, ValueError):
95 print >> sys.stderr, (
96 'WARNING: Could not parse svn revision out of "%s"' % line)
97 continue
98 if svn_rev == revision:
99 return git_rev
100 if svn_rev > revision:
101 prev_svn_rev = svn_rev
102 continue
103 if prev_svn_rev:
104 err_msg = 'git history skips from revision %d to revision %d.' % (
105 svn_rev, prev_svn_rev)
106 else:
107 err_msg = (
108 'latest available revision is %d; you may need to '
109 '"git fetch origin" to get the latest commits.' % svn_rev)
110 finally:
111 log_p.terminate()
112 grep_p.terminate()
113 raise RuntimeError('No match for revision %d; %s' % (revision, err_msg))
114
115
116 def get_git_revision(dep_path, revision):
117 """Convert the revision argument passed to the script to a git revision."""
118 if revision.startswith('r'):
119 result = convert_svn_revision(dep_path, revision[1:])
120 elif re.search('[a-fA-F]', revision):
121 result = verify_git_revision(dep_path, revision)
122 elif len(revision) > 6:
123 result = verify_git_revision(dep_path, revision)
124 if not result:
125 result = convert_svn_revision(dep_path, revision)
126 else:
127 try:
128 result = convert_svn_revision(dep_path, revision)
129 except RuntimeError:
130 result = verify_git_revision(dep_path, revision)
131 if not result:
132 raise
133 return result
134
135
136 def ast_err_msg(node):
137 return 'ERROR: Undexpected DEPS file AST structure at line %d column %d' % (
138 node.lineno, node.col_offset)
139
140
141 def find_deps_section(deps_ast, section):
142 """Find a top-level section of the DEPS file in the AST."""
143 try:
144 result = [n.value for n in deps_ast.body if
145 n.__class__ is ast.Assign and
146 n.targets[0].__class__ is ast.Name and
147 n.targets[0].id == section][0]
148 return result
149 except IndexError:
150 return None
151
152
153 def find_dict_index(dict_node, key):
154 """Given a key, find the index of the corresponding dict entry."""
155 assert dict_node.__class__ is ast.Dict, ast_err_msg(dict_node)
156 indices = [i for i, n in enumerate(dict_node.keys) if
157 n.__class__ is ast.Str and n.s == key]
158 assert len(indices) < 2, (
159 'Found redundant dict entries for key "%s"' % key)
160 return indices[0] if indices else None
161
162
163 def update_node(deps_lines, deps_ast, node, git_revision):
164 """Update an AST node with the new git revision."""
165 if node.__class__ is ast.Str:
166 return update_string(deps_lines, node, git_revision)
167 elif node.__class__ is ast.BinOp:
168 return update_binop(deps_lines, deps_ast, node, git_revision)
169 elif node.__class__ is ast.Call:
170 return update_call(deps_lines, deps_ast, node, git_revision)
171 else:
172 assert False, ast_err_msg(node)
173
174
175 def update_string(deps_lines, string_node, git_revision):
176 """Update a string node in the AST with the new git revision."""
177 line_idx = string_node.lineno - 1
178 start_idx = string_node.col_offset - 1
179 line = deps_lines[line_idx]
180 (prefix, sep, old_rev) = string_node.s.partition('@')
181 if sep:
182 start_idx = line.find(prefix + sep, start_idx) + len(prefix + sep)
183 tail_idx = start_idx + len(old_rev)
184 else:
185 start_idx = line.find(prefix, start_idx)
186 tail_idx = start_idx + len(prefix)
187 old_rev = prefix
188 deps_lines[line_idx] = line[:start_idx] + git_revision + line[tail_idx:]
189 return old_rev
190
191
192 def update_binop(deps_lines, deps_ast, binop_node, git_revision):
193 """Update a binary operation node in the AST with the new git revision."""
194 # Since the revision part is always last, assume that it's the right-hand
195 # operand that needs to be updated.
196 return update_node(deps_lines, deps_ast, binop_node.right, git_revision)
197
198
199 def update_call(deps_lines, deps_ast, call_node, git_revision):
200 """Update a function call node in the AST with the new git revision."""
201 # The only call we know how to handle is Var()
202 assert call_node.func.id == 'Var', ast_err_msg(call_node)
203 assert call_node.args and call_node.args[0].__class__ is ast.Str, (
204 ast_err_msg(call_node))
205 return update_var(deps_lines, deps_ast, call_node.args[0].s, git_revision)
206
207
208 def update_var(deps_lines, deps_ast, var_name, git_revision):
209 """Update an entry in the vars section of the DEPS file with the new
210 git revision."""
211 vars_node = find_deps_section(deps_ast, 'vars')
212 assert vars_node, 'Could not find "vars" section of DEPS file.'
213 var_idx = find_dict_index(vars_node, var_name)
214 assert var_idx is not None, (
215 'Could not find definition of "%s" var in DEPS file.' % var_name)
216 val_node = vars_node.values[var_idx]
217 return update_node(deps_lines, deps_ast, val_node, git_revision)
218
219
220 def generate_commit_message(deps_section, dep_name, new_rev):
221 (url, _, old_rev) = deps_section[dep_name].partition('@')
222 if url.endswith('.git'):
223 url = url[:-4]
224 url += '/+log/%s..%s' % (old_rev[:12], new_rev[:12])
225 return dedent('''\
226 Rolled %s
227 from revision %s
228 to revision %s
229 Summary of changes available at:
230 %s\n''' % (dep_name, old_rev, new_rev, url))
231
232 def update_deps(soln_path, dep_name, new_rev):
233 """Update the DEPS file with the new git revision."""
234 commit_msg = ''
235 deps_file = os.path.join(soln_path, 'DEPS')
236 with open(deps_file) as fh:
237 deps_content = fh.read()
238 deps_locals = {}
239 def _Var(key):
240 return deps_locals['vars'][key]
241 deps_locals['Var'] = _Var
242 exec deps_content in {}, deps_locals
243 deps_lines = deps_content.splitlines()
244 deps_ast = ast.parse(deps_content, deps_file)
245 deps_node = find_deps_section(deps_ast, 'deps')
246 assert deps_node, 'Could not find "deps" section of DEPS file'
247 dep_idx = find_dict_index(deps_node, dep_name)
248 if dep_idx is not None:
249 value_node = deps_node.values[dep_idx]
250 update_node(deps_lines, deps_ast, value_node, new_rev)
251 commit_msg = generate_commit_message(deps_locals['deps'], dep_name, new_rev)
252 deps_os_node = find_deps_section(deps_ast, 'deps_os')
253 if deps_os_node:
254 for (os_name, os_node) in izip(deps_os_node.keys, deps_os_node.values):
255 dep_idx = find_dict_index(os_node, dep_name)
256 if dep_idx is not None:
257 value_node = os_node.values[dep_idx]
258 if value_node.__class__ is ast.Name and value_node.id == 'None':
259 pass
260 else:
261 update_node(deps_lines, deps_ast, value_node, new_rev)
262 commit_msg = generate_commit_message(
263 deps_locals['deps_os'][os_name], dep_name, new_rev)
264 if commit_msg:
265 print 'Pinning %s' % dep_name
266 print 'to revision %s' % new_rev
267 print 'in %s' % deps_file
268 with open(deps_file, 'w') as fh:
269 for line in deps_lines:
270 print >> fh, line
271 with open(os.path.join(soln_path, '.git', 'MERGE_MSG'), 'a') as fh:
272 fh.write(commit_msg)
273 else:
274 print 'Could not find an entry in %s to update.' % deps_file
275 return 0 if commit_msg else 1
276
277
278 def main(argv):
279 if len(argv) != 2 :
280 print >> sys.stderr, 'Usage: roll_dep.py <dep path> <svn revision>'
281 return 1
282 (dep_path, revision) = argv[0:2]
283 dep_path = platform_path(dep_path)
284 assert os.path.isdir(dep_path), 'No such directory: %s' % dep_path
285 gclient_root = find_gclient_root()
286 soln = get_solution(gclient_root, dep_path)
287 soln_path = os.path.relpath(os.path.join(gclient_root, soln['name']))
288 dep_name = posix_path(os.path.relpath(dep_path, gclient_root))
289 new_rev = get_git_revision(dep_path, revision)
290 assert new_rev, 'Could not find git revision matching %s' % revision
291 return update_deps(soln_path, dep_name, new_rev)
292
293 if __name__ == '__main__':
294 sys.exit(main(sys.argv[1:]))
OLDNEW
« no previous file with comments | « roll-dep.bat ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698