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

Side by Side Diff: roll_dep.py

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

Powered by Google App Engine
This is Rietveld 408576698