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

Side by Side Diff: roll_dep_svn.py

Issue 2297513002: Revert of Delete roll-dep-svn (Closed)
Patch Set: Created 4 years, 3 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
« no previous file with comments | « roll-dep-svn.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 """Rolls a git-svn dependency.
7
8 It takes the path to a dep and a git commit hash or svn revision, and updates
9 the parent repo's DEPS file with the corresponding git commit hash.
10
11 Sample invocation:
12
13 [chromium/src]$ roll-dep-svn third_party/WebKit 12345
14
15 After the script completes, the DEPS file will be dirty with the new revision.
16 The user can then:
17
18 $ git add DEPS
19 $ git commit
20 """
21
22 import ast
23 import optparse
24 import os
25 import re
26 import sys
27
28 from itertools import izip
29 from subprocess import check_output, Popen, PIPE
30 from textwrap import dedent
31
32
33 SHA1_RE = re.compile('^[a-fA-F0-9]{40}$')
34 GIT_SVN_ID_RE = re.compile('^git-svn-id: .*@([0-9]+) .*$')
35 ROLL_DESCRIPTION_STR = (
36 '''Roll %(dep_path)s %(before_rev)s:%(after_rev)s%(svn_range)s
37
38 Summary of changes available at:
39 %(revlog_url)s
40 ''')
41
42
43 def shorten_dep_path(dep):
44 """Shorten the given dep path if necessary."""
45 while len(dep) > 31:
46 dep = '.../' + dep.lstrip('./').partition('/')[2]
47 return dep
48
49
50 def posix_path(path):
51 """Convert a possibly-Windows path to a posix-style path."""
52 (_, path) = os.path.splitdrive(path)
53 return path.replace(os.sep, '/')
54
55
56 def platform_path(path):
57 """Convert a path to the native path format of the host OS."""
58 return path.replace('/', os.sep)
59
60
61 def find_gclient_root():
62 """Find the directory containing the .gclient file."""
63 cwd = posix_path(os.getcwd())
64 result = ''
65 for _ in xrange(len(cwd.split('/'))):
66 if os.path.exists(os.path.join(result, '.gclient')):
67 return result
68 result = os.path.join(result, os.pardir)
69 assert False, 'Could not find root of your gclient checkout.'
70
71
72 def get_solution(gclient_root, dep_path):
73 """Find the solution in .gclient containing the dep being rolled."""
74 dep_path = os.path.relpath(dep_path, gclient_root)
75 cwd = os.getcwd().rstrip(os.sep) + os.sep
76 gclient_root = os.path.realpath(gclient_root)
77 gclient_path = os.path.join(gclient_root, '.gclient')
78 gclient_locals = {}
79 execfile(gclient_path, {}, gclient_locals)
80 for soln in gclient_locals['solutions']:
81 soln_relpath = platform_path(soln['name'].rstrip('/')) + os.sep
82 if (dep_path.startswith(soln_relpath) or
83 cwd.startswith(os.path.join(gclient_root, soln_relpath))):
84 return soln
85 assert False, 'Could not determine the parent project for %s' % dep_path
86
87
88 def is_git_hash(revision):
89 """Determines if a given revision is a git hash."""
90 return SHA1_RE.match(revision)
91
92
93 def verify_git_revision(dep_path, revision):
94 """Verify that a git revision exists in a repository."""
95 p = Popen(['git', 'rev-list', '-n', '1', revision],
96 cwd=dep_path, stdout=PIPE, stderr=PIPE)
97 result = p.communicate()[0].strip()
98 if p.returncode != 0 or not is_git_hash(result):
99 result = None
100 return result
101
102
103 def get_svn_revision(dep_path, git_revision):
104 """Given a git revision, return the corresponding svn revision."""
105 p = Popen(['git', 'log', '-n', '1', '--pretty=format:%B', git_revision],
106 stdout=PIPE, cwd=dep_path)
107 (log, _) = p.communicate()
108 assert p.returncode == 0, 'git log %s failed.' % git_revision
109 for line in reversed(log.splitlines()):
110 m = GIT_SVN_ID_RE.match(line.strip())
111 if m:
112 return m.group(1)
113 return None
114
115
116 def convert_svn_revision(dep_path, revision):
117 """Find the git revision corresponding to an svn revision."""
118 err_msg = 'Unknown error'
119 revision = int(revision)
120 latest_svn_rev = None
121 with open(os.devnull, 'w') as devnull:
122 for ref in ('HEAD', 'origin/master'):
123 try:
124 log_p = Popen(['git', 'log', ref],
125 cwd=dep_path, stdout=PIPE, stderr=devnull)
126 grep_p = Popen(['grep', '-e', '^commit ', '-e', '^ *git-svn-id: '],
127 stdin=log_p.stdout, stdout=PIPE, stderr=devnull)
128 git_rev = None
129 prev_svn_rev = None
130 for line in grep_p.stdout:
131 if line.startswith('commit '):
132 git_rev = line.split()[1]
133 continue
134 try:
135 svn_rev = int(line.split()[1].partition('@')[2])
136 except (IndexError, ValueError):
137 print >> sys.stderr, (
138 'WARNING: Could not parse svn revision out of "%s"' % line)
139 continue
140 if not latest_svn_rev or int(svn_rev) > int(latest_svn_rev):
141 latest_svn_rev = svn_rev
142 if svn_rev == revision:
143 return git_rev
144 if svn_rev > revision:
145 prev_svn_rev = svn_rev
146 continue
147 if prev_svn_rev:
148 err_msg = 'git history skips from revision %d to revision %d.' % (
149 svn_rev, prev_svn_rev)
150 else:
151 err_msg = (
152 'latest available revision is %d; you may need to '
153 '"git fetch origin" to get the latest commits.' %
154 latest_svn_rev)
155 finally:
156 log_p.terminate()
157 grep_p.terminate()
158 raise RuntimeError('No match for revision %d; %s' % (revision, err_msg))
159
160
161 def get_git_revision(dep_path, revision):
162 """Convert the revision argument passed to the script to a git revision."""
163 svn_revision = None
164 if revision.startswith('r'):
165 git_revision = convert_svn_revision(dep_path, revision[1:])
166 svn_revision = revision[1:]
167 elif re.search('[a-fA-F]', revision):
168 git_revision = verify_git_revision(dep_path, revision)
169 if not git_revision:
170 raise RuntimeError('Please \'git fetch origin\' in %s' % dep_path)
171 svn_revision = get_svn_revision(dep_path, git_revision)
172 elif len(revision) > 6:
173 git_revision = verify_git_revision(dep_path, revision)
174 if git_revision:
175 svn_revision = get_svn_revision(dep_path, git_revision)
176 else:
177 git_revision = convert_svn_revision(dep_path, revision)
178 svn_revision = revision
179 else:
180 try:
181 git_revision = convert_svn_revision(dep_path, revision)
182 svn_revision = revision
183 except RuntimeError:
184 git_revision = verify_git_revision(dep_path, revision)
185 if not git_revision:
186 raise
187 svn_revision = get_svn_revision(dep_path, git_revision)
188 return git_revision, svn_revision
189
190
191 def ast_err_msg(node):
192 return 'ERROR: Undexpected DEPS file AST structure at line %d column %d' % (
193 node.lineno, node.col_offset)
194
195
196 def find_deps_section(deps_ast, section):
197 """Find a top-level section of the DEPS file in the AST."""
198 try:
199 result = [n.value for n in deps_ast.body if
200 n.__class__ is ast.Assign and
201 n.targets[0].__class__ is ast.Name and
202 n.targets[0].id == section][0]
203 return result
204 except IndexError:
205 return None
206
207
208 def find_dict_index(dict_node, key):
209 """Given a key, find the index of the corresponding dict entry."""
210 assert dict_node.__class__ is ast.Dict, ast_err_msg(dict_node)
211 indices = [i for i, n in enumerate(dict_node.keys) if
212 n.__class__ is ast.Str and n.s == key]
213 assert len(indices) < 2, (
214 'Found redundant dict entries for key "%s"' % key)
215 return indices[0] if indices else None
216
217
218 def update_node(deps_lines, deps_ast, node, git_revision):
219 """Update an AST node with the new git revision."""
220 if node.__class__ is ast.Str:
221 return update_string(deps_lines, node, git_revision)
222 elif node.__class__ is ast.BinOp:
223 return update_binop(deps_lines, deps_ast, node, git_revision)
224 elif node.__class__ is ast.Call:
225 return update_call(deps_lines, deps_ast, node, git_revision)
226 else:
227 assert False, ast_err_msg(node)
228
229
230 def update_string(deps_lines, string_node, git_revision):
231 """Update a string node in the AST with the new git revision."""
232 line_idx = string_node.lineno - 1
233 start_idx = string_node.col_offset - 1
234 line = deps_lines[line_idx]
235 (prefix, sep, old_rev) = string_node.s.partition('@')
236 if sep:
237 start_idx = line.find(prefix + sep, start_idx) + len(prefix + sep)
238 tail_idx = start_idx + len(old_rev)
239 else:
240 start_idx = line.find(prefix, start_idx)
241 tail_idx = start_idx + len(prefix)
242 old_rev = prefix
243 deps_lines[line_idx] = line[:start_idx] + git_revision + line[tail_idx:]
244 return line_idx
245
246
247 def update_binop(deps_lines, deps_ast, binop_node, git_revision):
248 """Update a binary operation node in the AST with the new git revision."""
249 # Since the revision part is always last, assume that it's the right-hand
250 # operand that needs to be updated.
251 return update_node(deps_lines, deps_ast, binop_node.right, git_revision)
252
253
254 def update_call(deps_lines, deps_ast, call_node, git_revision):
255 """Update a function call node in the AST with the new git revision."""
256 # The only call we know how to handle is Var()
257 assert call_node.func.id == 'Var', ast_err_msg(call_node)
258 assert call_node.args and call_node.args[0].__class__ is ast.Str, (
259 ast_err_msg(call_node))
260 return update_var(deps_lines, deps_ast, call_node.args[0].s, git_revision)
261
262
263 def update_var(deps_lines, deps_ast, var_name, git_revision):
264 """Update an entry in the vars section of the DEPS file with the new
265 git revision."""
266 vars_node = find_deps_section(deps_ast, 'vars')
267 assert vars_node, 'Could not find "vars" section of DEPS file.'
268 var_idx = find_dict_index(vars_node, var_name)
269 assert var_idx is not None, (
270 'Could not find definition of "%s" var in DEPS file.' % var_name)
271 val_node = vars_node.values[var_idx]
272 return update_node(deps_lines, deps_ast, val_node, git_revision)
273
274
275 def short_rev(rev, dep_path):
276 return check_output(['git', 'rev-parse', '--short', rev],
277 cwd=dep_path).rstrip()
278
279
280 def generate_commit_message(deps_section, dep_path, dep_name, new_rev):
281 (url, _, old_rev) = deps_section[dep_name].partition('@')
282 if url.endswith('.git'):
283 url = url[:-4]
284 old_rev_short = short_rev(old_rev, dep_path)
285 new_rev_short = short_rev(new_rev, dep_path)
286 url += '/+log/%s..%s' % (old_rev_short, new_rev_short)
287 try:
288 old_svn_rev = get_svn_revision(dep_path, old_rev)
289 new_svn_rev = get_svn_revision(dep_path, new_rev)
290 except Exception:
291 # Ignore failures that might arise from the repo not being checked out.
292 old_svn_rev = new_svn_rev = None
293 svn_range_str = ''
294 if old_svn_rev and new_svn_rev:
295 svn_range_str = ' (svn %s:%s)' % (old_svn_rev, new_svn_rev)
296 return dedent(ROLL_DESCRIPTION_STR % {
297 'dep_path': shorten_dep_path(dep_name),
298 'before_rev': old_rev_short,
299 'after_rev': new_rev_short,
300 'svn_range': svn_range_str,
301 'revlog_url': url,
302 })
303
304
305 def update_deps_entry(deps_lines, deps_ast, value_node, new_rev, comment):
306 line_idx = update_node(deps_lines, deps_ast, value_node, new_rev)
307 (content, _, _) = deps_lines[line_idx].partition('#')
308 if comment:
309 deps_lines[line_idx] = '%s # %s' % (content.rstrip(), comment)
310 else:
311 deps_lines[line_idx] = content.rstrip()
312
313
314 def update_deps(deps_file, dep_path, dep_name, new_rev, comment):
315 """Update the DEPS file with the new git revision."""
316 commit_msg = ''
317 with open(deps_file) as fh:
318 deps_content = fh.read()
319 deps_locals = {}
320 def _Var(key):
321 return deps_locals['vars'][key]
322 deps_locals['Var'] = _Var
323 exec deps_content in {}, deps_locals
324 deps_lines = deps_content.splitlines()
325 deps_ast = ast.parse(deps_content, deps_file)
326 deps_node = find_deps_section(deps_ast, 'deps')
327 assert deps_node, 'Could not find "deps" section of DEPS file'
328 dep_idx = find_dict_index(deps_node, dep_name)
329 if dep_idx is not None:
330 value_node = deps_node.values[dep_idx]
331 update_deps_entry(deps_lines, deps_ast, value_node, new_rev, comment)
332 commit_msg = generate_commit_message(deps_locals['deps'], dep_path,
333 dep_name, new_rev)
334 deps_os_node = find_deps_section(deps_ast, 'deps_os')
335 if deps_os_node:
336 for (os_name, os_node) in izip(deps_os_node.keys, deps_os_node.values):
337 dep_idx = find_dict_index(os_node, dep_name)
338 if dep_idx is not None:
339 value_node = os_node.values[dep_idx]
340 if value_node.__class__ is ast.Name and value_node.id == 'None':
341 pass
342 else:
343 update_deps_entry(deps_lines, deps_ast, value_node, new_rev, comment)
344 commit_msg = generate_commit_message(
345 deps_locals['deps_os'][os_name.s], dep_path, dep_name, new_rev)
346 if not commit_msg:
347 print 'Could not find an entry in %s to update.' % deps_file
348 return 1
349
350 print 'Pinning %s' % dep_name
351 print 'to revision %s' % new_rev
352 print 'in %s' % deps_file
353 with open(deps_file, 'w') as fh:
354 for line in deps_lines:
355 print >> fh, line
356 deps_file_dir = os.path.normpath(os.path.dirname(deps_file))
357 deps_file_root = Popen(
358 ['git', 'rev-parse', '--show-toplevel'],
359 cwd=deps_file_dir, stdout=PIPE).communicate()[0].strip()
360 with open(os.path.join(deps_file_root, '.git', 'MERGE_MSG'), 'w') as fh:
361 fh.write(commit_msg)
362 return 0
363
364
365 def main(argv):
366 usage = 'Usage: roll-dep-svn [options] <dep path> <rev> [ <DEPS file> ]'
367 parser = optparse.OptionParser(usage=usage, description=__doc__)
368 parser.add_option('--no-verify-revision',
369 help='Don\'t verify the revision passed in. This '
370 'also skips adding an svn revision comment '
371 'for git dependencies and requires the passed '
372 'revision to be a git hash.',
373 default=False, action='store_true')
374 options, args = parser.parse_args(argv)
375 if len(args) not in (2, 3):
376 parser.error('Expected either 2 or 3 positional parameters.')
377 arg_dep_path, revision = args[:2]
378 gclient_root = find_gclient_root()
379 dep_path = platform_path(arg_dep_path)
380 if not os.path.exists(dep_path):
381 dep_path = os.path.join(gclient_root, dep_path)
382 if not options.no_verify_revision:
383 # Only require the path to exist if the revision should be verified. A path
384 # to e.g. os deps might not be checked out.
385 if not os.path.isdir(dep_path):
386 print >> sys.stderr, 'No such directory: %s' % arg_dep_path
387 return 1
388 if len(args) > 2:
389 deps_file = args[2]
390 else:
391 soln = get_solution(gclient_root, dep_path)
392 soln_path = os.path.relpath(os.path.join(gclient_root, soln['name']))
393 deps_file = os.path.join(soln_path, 'DEPS')
394 dep_name = posix_path(os.path.relpath(dep_path, gclient_root))
395 if options.no_verify_revision:
396 if not is_git_hash(revision):
397 print >> sys.stderr, (
398 'The passed revision %s must be a git hash when skipping revision '
399 'verification.' % revision)
400 return 1
401 git_rev = revision
402 comment = None
403 else:
404 git_rev, svn_rev = get_git_revision(dep_path, revision)
405 comment = ('from svn revision %s' % svn_rev) if svn_rev else None
406 if not git_rev:
407 print >> sys.stderr, 'Could not find git revision matching %s.' % revision
408 return 1
409 return update_deps(deps_file, dep_path, dep_name, git_rev, comment)
410
411
412 if __name__ == '__main__':
413 try:
414 sys.exit(main(sys.argv[1:]))
415 except KeyboardInterrupt:
416 sys.stderr.write('interrupted\n')
417 sys.exit(1)
OLDNEW
« no previous file with comments | « roll-dep-svn.bat ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698