blob: 2f98fb548cb7a3254d90b223f8ec42d9b44bc207 [file] [log] [blame]
halcanary@google.com31fdb922014-01-06 19:50:22 +00001#!/usr/bin/python2
2
3# Copyright 2014 Google Inc.
4#
5# Use of this source code is governed by a BSD-style license that can be
6# found in the LICENSE file.
7
8"""Skia's Chromium DEPS roll script.
9
10This script:
11- searches through the last N Skia git commits to find out the hash that is
12 associated with the SVN revision number.
13- creates a new branch in the Chromium tree, modifies the DEPS file to
14 point at the given Skia commit, commits, uploads to Rietveld, and
15 deletes the local copy of the branch.
16- creates a whitespace-only commit and uploads that to to Rietveld.
17- returns the Chromium tree to its previous state.
18
19Usage:
20 %prog -c CHROMIUM_PATH -r REVISION [OPTIONAL_OPTIONS]
21"""
22
23
24import optparse
25import os
26import re
27import shutil
28import subprocess
halcanary@google.com31fdb922014-01-06 19:50:22 +000029import sys
30import tempfile
31
32
33class DepsRollConfig(object):
34 """Contains configuration options for this module.
35
36 Attributes:
37 git: (string) The git executable.
38 chromium_path: (string) path to a local chromium git repository.
39 save_branches: (boolean) iff false, delete temporary branches.
40 verbose: (boolean) iff false, suppress the output from git-cl.
41 search_depth: (int) how far back to look for the revision.
42 skia_url: (string) Skia's git repository.
43 self.skip_cl_upload: (boolean)
44 self.cl_bot_list: (list of strings)
45 """
46
47 # pylint: disable=I0011,R0903,R0902
48 def __init__(self, options=None):
49 self.skia_url = 'https://skia.googlesource.com/skia.git'
50 self.revision_format = (
51 'git-svn-id: http://skia.googlecode.com/svn/trunk@%d ')
52
53 if not options:
54 options = DepsRollConfig.GetOptionParser()
55 # pylint: disable=I0011,E1103
56 self.verbose = options.verbose
halcanary@google.com8c5d2c12014-01-08 21:29:34 +000057 self.vsp = VerboseSubprocess(self.verbose)
58 self.save_branches = not options.delete_branches
halcanary@google.com31fdb922014-01-06 19:50:22 +000059 self.search_depth = options.search_depth
60 self.chromium_path = options.chromium_path
61 self.git = options.git_path
62 self.skip_cl_upload = options.skip_cl_upload
63 # Split and remove empty strigns from the bot list.
64 self.cl_bot_list = [bot for bot in options.bots.split(',') if bot]
65 self.skia_git_checkout_path = options.skia_git_path
66 self.default_branch_name = 'autogenerated_deps_roll_branch'
67
68 @staticmethod
69 def GetOptionParser():
70 # pylint: disable=I0011,C0103
71 """Returns an optparse.OptionParser object.
72
73 Returns:
74 An optparse.OptionParser object.
75
76 Called by the main() function.
77 """
78 default_bots_list = [
79 'android_clang_dbg',
80 'android_dbg',
81 'android_rel',
82 'cros_daisy',
83 'linux',
84 'linux_asan',
85 'linux_chromeos',
86 'linux_chromeos_asan',
87 'linux_gpu',
88 'linux_heapcheck',
89 'linux_layout',
90 'linux_layout_rel',
91 'mac',
92 'mac_asan',
93 'mac_gpu',
94 'mac_layout',
95 'mac_layout_rel',
96 'win',
97 'win_gpu',
98 'win_layout',
99 'win_layout_rel',
100 ]
101
102 option_parser = optparse.OptionParser(usage=__doc__)
103 # Anyone using this script on a regular basis should set the
104 # CHROMIUM_CHECKOUT_PATH environment variable.
105 option_parser.add_option(
106 '-c', '--chromium_path', help='Path to local Chromium Git'
107 ' repository checkout, defaults to CHROMIUM_CHECKOUT_PATH'
108 ' if that environment variable is set.',
109 default=os.environ.get('CHROMIUM_CHECKOUT_PATH'))
110 option_parser.add_option(
111 '-r', '--revision', type='int', default=None,
112 help='The Skia SVN revision number, defaults to top of tree.')
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000113 option_parser.add_option(
114 '-g', '--git_hash', default=None,
115 help='A partial Skia Git hash. Do not set this and revision.')
116
halcanary@google.com31fdb922014-01-06 19:50:22 +0000117 # Anyone using this script on a regular basis should set the
118 # SKIA_GIT_CHECKOUT_PATH environment variable.
119 option_parser.add_option(
120 '', '--skia_git_path',
121 help='Path of a pure-git Skia repository checkout. If empty,'
122 ' a temporary will be cloned. Defaults to SKIA_GIT_CHECKOUT'
123 '_PATH, if that environment variable is set.',
124 default=os.environ.get('SKIA_GIT_CHECKOUT_PATH'))
125 option_parser.add_option(
126 '', '--search_depth', type='int', default=100,
127 help='How far back to look for the revision.')
128 option_parser.add_option(
129 '', '--git_path', help='Git executable, defaults to "git".',
130 default='git')
131 option_parser.add_option(
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000132 '', '--delete_branches', help='Delete the temporary branches',
133 action='store_true', dest='delete_branches', default=False)
halcanary@google.com31fdb922014-01-06 19:50:22 +0000134 option_parser.add_option(
135 '', '--verbose', help='Do not suppress the output from `git cl`.',
136 action='store_true', dest='verbose', default=False)
137 option_parser.add_option(
138 '', '--skip_cl_upload', help='Skip the cl upload step; useful'
139 ' for testing or with --save_branches.',
140 action='store_true', default=False)
141
142 default_bots_help = (
143 'Comma-separated list of bots, defaults to a list of %d bots.'
144 ' To skip `git cl try`, set this to an empty string.'
145 % len(default_bots_list))
146 default_bots = ','.join(default_bots_list)
147 option_parser.add_option(
148 '', '--bots', help=default_bots_help, default=default_bots)
149
150 return option_parser
151
152
153def test_git_executable(git_executable):
154 """Test the git executable.
155
156 Args:
157 git_executable: git executable path.
158 Returns:
159 True if test is successful.
160 """
161 with open(os.devnull, 'w') as devnull:
162 try:
163 subprocess.call([git_executable, '--version'], stdout=devnull)
164 except (OSError,):
165 return False
166 return True
167
168
169class DepsRollError(Exception):
170 """Exceptions specific to this module."""
171 pass
172
173
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000174class VerboseSubprocess(object):
175 """Call subprocess methods, but print out command before executing.
halcanary@google.com31fdb922014-01-06 19:50:22 +0000176
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000177 Attributes:
178 verbose: (boolean) should we print out the command or not. If
179 not, this is the same as calling the subprocess method
180 quiet: (boolean) suppress stdout on check_call and call.
181 prefix: (string) When verbose, what to print before each command.
halcanary@google.com31fdb922014-01-06 19:50:22 +0000182 """
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000183
184 def __init__(self, verbose):
185 self.verbose = verbose
186 self.quiet = not verbose
187 self.prefix = '~~$ '
188
189 @staticmethod
190 def _fix(string):
191 """Quote and escape a string if necessary."""
192 if ' ' in string or '\n' in string:
193 string = '"%s"' % string.replace('\n', '\\n')
194 return string
195
196 @staticmethod
197 def print_subprocess_args(prefix, *args, **kwargs):
198 """Print out args in a human-readable manner."""
199 if 'cwd' in kwargs:
200 print '%scd %s' % (prefix, kwargs['cwd'])
201 print prefix + ' '.join(VerboseSubprocess._fix(arg) for arg in args[0])
202 if 'cwd' in kwargs:
203 print '%scd -' % prefix
204
205 def check_call(self, *args, **kwargs):
206 """Wrapper for subprocess.check_call().
207
208 Args:
209 *args: to be passed to subprocess.check_call()
210 **kwargs: to be passed to subprocess.check_call()
211 Returns:
212 Whatever subprocess.check_call() returns.
213 Raises:
214 OSError or subprocess.CalledProcessError: raised by check_call.
215 """
216 if self.verbose:
217 self.print_subprocess_args(self.prefix, *args, **kwargs)
218 if self.quiet:
219 with open(os.devnull, 'w') as devnull:
220 return subprocess.check_call(*args, stdout=devnull, **kwargs)
221 else:
222 return subprocess.check_call(*args, **kwargs)
223
224 def call(self, *args, **kwargs):
225 """Wrapper for subprocess.check().
226
227 Args:
228 *args: to be passed to subprocess.check_call()
229 **kwargs: to be passed to subprocess.check_call()
230 Returns:
231 Whatever subprocess.call() returns.
232 Raises:
233 OSError or subprocess.CalledProcessError: raised by call.
234 """
235 if self.verbose:
236 self.print_subprocess_args(self.prefix, *args, **kwargs)
237 if self.quiet:
238 with open(os.devnull, 'w') as devnull:
239 return subprocess.call(*args, stdout=devnull, **kwargs)
240 else:
241 return subprocess.call(*args, **kwargs)
242
243 def check_output(self, *args, **kwargs):
244 """Wrapper for subprocess.check_output().
245
246 Args:
247 *args: to be passed to subprocess.check_output()
248 **kwargs: to be passed to subprocess.check_output()
249 Returns:
250 Whatever subprocess.check_output() returns.
251 Raises:
252 OSError or subprocess.CalledProcessError: raised by check_output.
253 """
254 if self.verbose:
255 self.print_subprocess_args(self.prefix, *args, **kwargs)
256 return subprocess.check_output(*args, **kwargs)
257
258 def strip_output(self, *args, **kwargs):
259 """Wrap subprocess.check_output and str.strip().
260
261 Pass the given arguments into subprocess.check_output() and return
262 the results, after stripping any excess whitespace.
263
264 Args:
265 *args: to be passed to subprocess.check_output()
266 **kwargs: to be passed to subprocess.check_output()
267
268 Returns:
269 The output of the process as a string without leading or
270 trailing whitespace.
271 Raises:
272 OSError or subprocess.CalledProcessError: raised by check_output.
273 """
274 if self.verbose:
275 self.print_subprocess_args(self.prefix, *args, **kwargs)
276 return str(subprocess.check_output(*args, **kwargs)).strip()
277
278 def popen(self, *args, **kwargs):
279 """Wrapper for subprocess.Popen().
280
281 Args:
282 *args: to be passed to subprocess.Popen()
283 **kwargs: to be passed to subprocess.Popen()
284 Returns:
285 The output of subprocess.Popen()
286 Raises:
287 OSError or subprocess.CalledProcessError: raised by Popen.
288 """
289 if self.verbose:
290 self.print_subprocess_args(self.prefix, *args, **kwargs)
291 return subprocess.Popen(*args, **kwargs)
halcanary@google.com31fdb922014-01-06 19:50:22 +0000292
293
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000294class ChangeDir(object):
295 """Use with a with-statement to temporarily change directories."""
296 # pylint: disable=I0011,R0903
halcanary@google.com31fdb922014-01-06 19:50:22 +0000297
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000298 def __init__(self, directory, verbose=False):
299 self._directory = directory
300 self._verbose = verbose
301
302 def __enter__(self):
303 if self._verbose:
304 print '~~$ cd %s' % self._directory
305 cwd = os.getcwd()
306 os.chdir(self._directory)
307 self._directory = cwd
308
309 def __exit__(self, etype, value, traceback):
310 if self._verbose:
311 print '~~$ cd %s' % self._directory
312 os.chdir(self._directory)
313
314
315class ReSearch(object):
316 """A collection of static methods for regexing things."""
317
318 @staticmethod
319 def search_within_stream(input_stream, pattern, default=None):
320 """Search for regular expression in a file-like object.
321
322 Opens a file for reading and searches line by line for a match to
323 the regex and returns the parenthesized group named return for the
324 first match. Does not search across newlines.
325
326 For example:
327 pattern = '^root(:[^:]*){4}:(?P<return>[^:]*)'
328 with open('/etc/passwd', 'r') as stream:
329 return search_within_file(stream, pattern)
330 should return root's home directory (/root on my system).
331
332 Args:
333 input_stream: file-like object to be read
334 pattern: (string) to be passed to re.compile
335 default: what to return if no match
336
337 Returns:
338 A string or whatever default is
339 """
340 pattern_object = re.compile(pattern)
341 for line in input_stream:
342 match = pattern_object.search(line)
343 if match:
344 return match.group('return')
345 return default
346
347 @staticmethod
348 def search_within_string(input_string, pattern, default=None):
349 """Search for regular expression in a string.
350
351 Args:
352 input_string: (string) to be searched
353 pattern: (string) to be passed to re.compile
354 default: what to return if no match
355
356 Returns:
357 A string or whatever default is
358 """
359 match = re.search(pattern, input_string)
360 return match.group('return') if match else default
361
362 @staticmethod
363 def search_within_output(verbose, pattern, default, *args, **kwargs):
364 """Search for regular expression in a process output.
365
366 Does not search across newlines.
367
368 Args:
369 verbose: (boolean) shoule we call
370 VerboseSubprocess.print_subprocess_args?
371 pattern: (string) to be passed to re.compile
372 default: what to return if no match
373 *args: to be passed to subprocess.Popen()
374 **kwargs: to be passed to subprocess.Popen()
375
376 Returns:
377 A string or whatever default is
378 """
379 if verbose:
380 VerboseSubprocess.print_subprocess_args(
381 '~~$ ', *args, **kwargs)
382 proc = subprocess.Popen(*args, stdout=subprocess.PIPE, **kwargs)
383 return ReSearch.search_within_stream(proc.stdout, pattern, default)
384
385
386def get_svn_revision(config, commit):
387 """Works in both git and git-svn. returns a string."""
388 svn_format = (
389 '(git-svn-id: [^@ ]+@|SVN changes up to revision |'
390 'LKGR w/ DEPS up to revision )(?P<return>[0-9]+)')
391 svn_revision = ReSearch.search_within_output(
392 config.verbose, svn_format, None,
393 [config.git, 'log', '-n', '1', '--format=format:%B', commit])
394 if not svn_revision:
395 raise DepsRollError(
396 'Revision number missing from Chromium origin/master.')
397 return int(svn_revision)
398
399
400class SkiaGitCheckout(object):
401 """Class to create a temporary skia git checkout, if necessary.
halcanary@google.com31fdb922014-01-06 19:50:22 +0000402 """
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000403 # pylint: disable=I0011,R0903
404
405 def __init__(self, config, depth):
406 self._config = config
407 self._depth = depth
408 self._use_temp = None
409 self._original_cwd = None
410
411 def __enter__(self):
412 config = self._config
413 git = config.git
414 skia_dir = None
415 self._original_cwd = os.getcwd()
416 if config.skia_git_checkout_path:
417 skia_dir = config.skia_git_checkout_path
418 ## Update origin/master if needed.
419 if self._config.verbose:
420 print '~~$', 'cd', skia_dir
421 os.chdir(skia_dir)
422 config.vsp.check_call([git, 'fetch', '-q', 'origin'])
423 self._use_temp = None
424 else:
425 skia_dir = tempfile.mkdtemp(prefix='git_skia_tmp_')
426 self._use_temp = skia_dir
427 try:
428 os.chdir(skia_dir)
429 config.vsp.check_call(
430 [git, 'clone', '-q', '--depth=%d' % self._depth,
431 '--single-branch', config.skia_url, '.'])
432 except (OSError, subprocess.CalledProcessError) as error:
433 shutil.rmtree(skia_dir)
434 raise error
435
436 def __exit__(self, etype, value, traceback):
437 if self._config.verbose:
438 print '~~$', 'cd', self._original_cwd
439 os.chdir(self._original_cwd)
440 if self._use_temp:
441 shutil.rmtree(self._use_temp)
halcanary@google.com31fdb922014-01-06 19:50:22 +0000442
443
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000444def revision_and_hash(config):
halcanary@google.com31fdb922014-01-06 19:50:22 +0000445 """Finds revision number and git hash of origin/master in the Skia tree.
446
447 Args:
448 config: (roll_deps.DepsRollConfig) object containing options.
halcanary@google.com31fdb922014-01-06 19:50:22 +0000449
450 Returns:
451 A tuple (revision, hash)
452 revision: (int) SVN revision number.
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000453 git_hash: (string) full Git commit hash.
halcanary@google.com31fdb922014-01-06 19:50:22 +0000454
455 Raises:
456 roll_deps.DepsRollError: if the revision can't be found.
457 OSError: failed to execute git or git-cl.
458 subprocess.CalledProcessError: git returned unexpected status.
459 """
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000460 with SkiaGitCheckout(config, 1):
461 revision = get_svn_revision(config, 'origin/master')
462 git_hash = config.vsp.strip_output(
463 [config.git, 'show-ref', 'origin/master', '--hash'])
464 if not git_hash:
halcanary@google.com31fdb922014-01-06 19:50:22 +0000465 raise DepsRollError('Git hash can not be found.')
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000466 return revision, git_hash
467
468
469def revision_and_hash_from_revision(config, revision):
470 """Finds revision number and git hash of a commit in the Skia tree.
471
472 Args:
473 config: (roll_deps.DepsRollConfig) object containing options.
474 revision: (int) SVN revision number.
475
476 Returns:
477 A tuple (revision, hash)
478 revision: (int) SVN revision number.
479 git_hash: (string) full Git commit hash.
480
481 Raises:
482 roll_deps.DepsRollError: if the revision can't be found.
483 OSError: failed to execute git or git-cl.
484 subprocess.CalledProcessError: git returned unexpected status.
485 """
486 with SkiaGitCheckout(config, config.search_depth):
487 revision_regex = config.revision_format % revision
488 git_hash = config.vsp.strip_output(
489 [config.git, 'log', '--grep', revision_regex,
490 '--format=format:%H', 'origin/master'])
491 if not git_hash:
492 raise DepsRollError('Git hash can not be found.')
493 return revision, git_hash
494
495
496def revision_and_hash_from_partial(config, partial_hash):
497 """Returns the SVN revision number and full git hash.
498
499 Args:
500 config: (roll_deps.DepsRollConfig) object containing options.
501 partial_hash: (string) Partial git commit hash.
502
503 Returns:
504 A tuple (revision, hash)
505 revision: (int) SVN revision number.
506 git_hash: (string) full Git commit hash.
507
508 Raises:
509 roll_deps.DepsRollError: if the revision can't be found.
510 OSError: failed to execute git or git-cl.
511 subprocess.CalledProcessError: git returned unexpected status.
512 """
513 with SkiaGitCheckout(config, config.search_depth):
514 git_hash = config.vsp.strip_output(
515 ['git', 'log', '-n', '1', '--format=format:%H', partial_hash])
516 if not git_hash:
517 raise DepsRollError('Partial Git hash can not be found.')
518 revision = get_svn_revision(config, git_hash)
519 return revision, git_hash
halcanary@google.com31fdb922014-01-06 19:50:22 +0000520
521
522class GitBranchCLUpload(object):
523 """Class to manage git branches and git-cl-upload.
524
525 This class allows one to create a new branch in a repository based
526 off of origin/master, make changes to the tree inside the
527 with-block, upload that new branch to Rietveld, restore the original
528 tree state, and delete the local copy of the new branch.
529
530 See roll_deps() for an example of use.
531
532 Constructor Args:
533 config: (roll_deps.DepsRollConfig) object containing options.
534 message: (string) the commit message, can be multiline.
535 set_brach_name: (string or none) if not None, the name of the
536 branch to use. If None, then use a temporary branch that
537 will be deleted.
538
539 Attributes:
540 issue: a string describing the codereview issue, after __exit__
541 has been called, othrwise, None.
542
543 Raises:
544 OSError: failed to execute git or git-cl.
545 subprocess.CalledProcessError: git returned unexpected status.
546 """
547 # pylint: disable=I0011,R0903,R0902
548
549 def __init__(self, config, message, set_branch_name):
550 self._message = message
551 self._file_list = []
552 self._branch_name = set_branch_name
553 self._stash = None
554 self._original_branch = None
555 self._config = config
halcanary@google.com31fdb922014-01-06 19:50:22 +0000556 self.issue = None
557
558 def stage_for_commit(self, *paths):
559 """Calls `git add ...` on each argument.
560
561 Args:
562 *paths: (list of strings) list of filenames to pass to `git add`.
563 """
564 self._file_list.extend(paths)
565
566 def __enter__(self):
567 git = self._config.git
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000568 vsp = self._config.vsp
halcanary@google.com31fdb922014-01-06 19:50:22 +0000569 def branch_exists(branch):
570 """Return true iff branch exists."""
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000571 return 0 == vsp.call([git, 'show-ref', '--quiet', branch])
halcanary@google.com31fdb922014-01-06 19:50:22 +0000572 def has_diff():
573 """Return true iff repository has uncommited changes."""
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000574 return bool(vsp.call([git, 'diff', '--quiet', 'HEAD']))
575
halcanary@google.com31fdb922014-01-06 19:50:22 +0000576 self._stash = has_diff()
577 if self._stash:
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000578 vsp.check_call([git, 'stash', 'save'])
halcanary@google.com31fdb922014-01-06 19:50:22 +0000579 try:
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000580 self._original_branch = vsp.strip_output(
halcanary@google.com31fdb922014-01-06 19:50:22 +0000581 [git, 'symbolic-ref', '--short', 'HEAD'])
582 except (subprocess.CalledProcessError,):
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000583 self._original_branch = vsp.strip_output(
halcanary@google.com31fdb922014-01-06 19:50:22 +0000584 [git, 'rev-parse', 'HEAD'])
585
586 if not self._branch_name:
587 self._branch_name = self._config.default_branch_name
588
589 if branch_exists(self._branch_name):
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000590 vsp.check_call([git, 'checkout', '-q', 'master'])
591 vsp.check_call([git, 'branch', '-q', '-D', self._branch_name])
halcanary@google.com31fdb922014-01-06 19:50:22 +0000592
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000593 vsp.check_call(
594 [git, 'checkout', '-q', '-b', self._branch_name, 'origin/master'])
halcanary@google.com31fdb922014-01-06 19:50:22 +0000595
596 def __exit__(self, etype, value, traceback):
597 # pylint: disable=I0011,R0912
598 git = self._config.git
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000599 vsp = self._config.vsp
600 svn_info = str(get_svn_revision(self._config, 'HEAD'))
halcanary@google.com31fdb922014-01-06 19:50:22 +0000601
602 for filename in self._file_list:
603 assert os.path.exists(filename)
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000604 vsp.check_call([git, 'add', filename])
605 vsp.check_call([git, 'commit', '-q', '-m', self._message])
halcanary@google.com31fdb922014-01-06 19:50:22 +0000606
607 git_cl = [git, 'cl', 'upload', '-f', '--cc=skia-team@google.com',
608 '--bypass-hooks', '--bypass-watchlists']
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000609 git_try = [git, 'cl', 'try', '--revision', svn_info]
halcanary@google.com31fdb922014-01-06 19:50:22 +0000610 git_try.extend([arg for bot in self._config.cl_bot_list
611 for arg in ('-b', bot)])
612
613 if self._config.skip_cl_upload:
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000614 print 'You should call:'
615 print ' cd %s' % os.getcwd()
616 VerboseSubprocess.print_subprocess_args(
617 ' ', [git, 'checkout', self._branch_name])
618 VerboseSubprocess.print_subprocess_args(' ', git_cl)
halcanary@google.com31fdb922014-01-06 19:50:22 +0000619 if self._config.cl_bot_list:
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000620 VerboseSubprocess.print_subprocess_args(' ', git_try)
621 print
halcanary@google.com31fdb922014-01-06 19:50:22 +0000622 self.issue = ''
623 else:
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000624 vsp.check_call(git_cl)
625 self.issue = vsp.strip_output([git, 'cl', 'issue'])
halcanary@google.com31fdb922014-01-06 19:50:22 +0000626 if self._config.cl_bot_list:
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000627 vsp.check_call(git_try)
halcanary@google.com31fdb922014-01-06 19:50:22 +0000628
629 # deal with the aftermath of failed executions of this script.
630 if self._config.default_branch_name == self._original_branch:
631 self._original_branch = 'master'
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000632 vsp.check_call([git, 'checkout', '-q', self._original_branch])
halcanary@google.com31fdb922014-01-06 19:50:22 +0000633
634 if self._config.default_branch_name == self._branch_name:
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000635 vsp.check_call([git, 'branch', '-q', '-D', self._branch_name])
halcanary@google.com31fdb922014-01-06 19:50:22 +0000636 if self._stash:
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000637 vsp.check_call([git, 'stash', 'pop'])
halcanary@google.com31fdb922014-01-06 19:50:22 +0000638
639
640def change_skia_deps(revision, git_hash, depspath):
641 """Update the DEPS file.
642
643 Modify the skia_revision and skia_hash entries in the given DEPS file.
644
645 Args:
646 revision: (int) Skia SVN revision.
647 git_hash: (string) Skia Git hash.
648 depspath: (string) path to DEPS file.
649 """
650 temp_file = tempfile.NamedTemporaryFile(delete=False,
651 prefix='skia_DEPS_ROLL_tmp_')
652 try:
653 deps_regex_rev = re.compile('"skia_revision": "[0-9]*",')
654 deps_regex_hash = re.compile('"skia_hash": "[0-9a-f]*",')
655
656 deps_regex_rev_repl = '"skia_revision": "%d",' % revision
657 deps_regex_hash_repl = '"skia_hash": "%s",' % git_hash
658
659 with open(depspath, 'r') as input_stream:
660 for line in input_stream:
661 line = deps_regex_rev.sub(deps_regex_rev_repl, line)
662 line = deps_regex_hash.sub(deps_regex_hash_repl, line)
663 temp_file.write(line)
664 finally:
665 temp_file.close()
666 shutil.move(temp_file.name, depspath)
667
668
halcanary@google.com31fdb922014-01-06 19:50:22 +0000669def roll_deps(config, revision, git_hash):
670 """Upload changed DEPS and a whitespace change.
671
672 Given the correct git_hash, create two Reitveld issues.
673
674 Args:
675 config: (roll_deps.DepsRollConfig) object containing options.
676 revision: (int) Skia SVN revision.
677 git_hash: (string) Skia Git hash.
678
679 Returns:
680 a tuple containing textual description of the two issues.
681
682 Raises:
683 OSError: failed to execute git or git-cl.
684 subprocess.CalledProcessError: git returned unexpected status.
685 """
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000686
halcanary@google.com31fdb922014-01-06 19:50:22 +0000687 git = config.git
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000688 with ChangeDir(config.chromium_path, config.verbose):
689 config.vsp.check_call([git, 'fetch', '-q', 'origin'])
690
691 old_revision = ReSearch.search_within_output(
692 config.verbose, '"skia_revision": "(?P<return>[0-9]+)",', None,
693 [git, 'show', 'origin/master:DEPS'])
694 assert old_revision
695 if revision == int(old_revision):
696 print 'DEPS is up to date!'
697 return None
698
699 master_hash = config.vsp.strip_output(
halcanary@google.com31fdb922014-01-06 19:50:22 +0000700 [git, 'show-ref', 'origin/master', '--hash'])
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000701 master_revision = get_svn_revision(config, 'origin/master')
702
703 branch = None
halcanary@google.com31fdb922014-01-06 19:50:22 +0000704
705 # master_hash[8] gives each whitespace CL a unique name.
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000706 message = ('whitespace change %s\n\n'
707 'Chromium base revision: %d / %s\n\n'
708 'This CL was created by Skia\'s roll_deps.py script.\n'
709 ) % (master_hash[:8], master_revision, master_hash[:8])
710 if config.save_branches:
711 branch = 'control_%s' % master_hash[:8]
halcanary@google.com31fdb922014-01-06 19:50:22 +0000712
713 codereview = GitBranchCLUpload(config, message, branch)
714 with codereview:
715 with open('build/whitespace_file.txt', 'a') as output_stream:
716 output_stream.write('\nCONTROL\n')
717 codereview.stage_for_commit('build/whitespace_file.txt')
718 whitespace_cl = codereview.issue
719 if branch:
720 whitespace_cl = '%s\n branch: %s' % (whitespace_cl, branch)
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000721
722 control_url = ReSearch.search_within_string(
723 codereview.issue, '(?P<return>https?://[^) ]+)', '?')
724
725 if config.save_branches:
726 branch = 'roll_%d_%s' % (revision, master_hash[:8])
727 message = (
728 'roll skia DEPS to %d\n\n'
729 'Chromium base revision: %d / %s\n'
730 'Old Skia revision: %s\n'
731 'New Skia revision: %d\n'
732 'Control CL: %s\n\n'
733 'This CL was created by Skia\'s roll_deps.py script.\n'
734 % (revision, master_revision, master_hash[:8],
735 old_revision, revision, control_url))
halcanary@google.com31fdb922014-01-06 19:50:22 +0000736 codereview = GitBranchCLUpload(config, message, branch)
737 with codereview:
738 change_skia_deps(revision, git_hash, 'DEPS')
739 codereview.stage_for_commit('DEPS')
740 deps_cl = codereview.issue
741 if branch:
742 deps_cl = '%s\n branch: %s' % (deps_cl, branch)
743
744 return deps_cl, whitespace_cl
halcanary@google.com31fdb922014-01-06 19:50:22 +0000745
746
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000747def find_hash_and_roll_deps(config, revision=None, partial_hash=None):
halcanary@google.com31fdb922014-01-06 19:50:22 +0000748 """Call find_hash_from_revision() and roll_deps().
749
750 The calls to git will be verbose on standard output. After a
751 successful upload of both issues, print links to the new
752 codereview issues.
753
754 Args:
755 config: (roll_deps.DepsRollConfig) object containing options.
756 revision: (int or None) the Skia SVN revision number or None
757 to use the tip of the tree.
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000758 partial_hash: (string or None) a partial pure-git Skia commit
759 hash. Don't pass both partial_hash and revision.
halcanary@google.com31fdb922014-01-06 19:50:22 +0000760
761 Raises:
762 roll_deps.DepsRollError: if the revision can't be found.
763 OSError: failed to execute git or git-cl.
764 subprocess.CalledProcessError: git returned unexpected status.
765 """
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000766
767 if revision and partial_hash:
768 raise DepsRollError('Pass revision or partial_hash, not both.')
769
770 if partial_hash:
771 revision, git_hash = revision_and_hash_from_partial(
772 config, partial_hash)
773 elif revision:
774 revision, git_hash = revision_and_hash_from_revision(config, revision)
775 else:
776 revision, git_hash = revision_and_hash(config)
halcanary@google.com31fdb922014-01-06 19:50:22 +0000777
778 print 'revision=%r\nhash=%r\n' % (revision, git_hash)
779
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000780 roll = roll_deps(config, revision, git_hash)
halcanary@google.com31fdb922014-01-06 19:50:22 +0000781
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000782 if roll:
783 deps_issue, whitespace_issue = roll
784 print 'DEPS roll:\n %s\n' % deps_issue
785 print 'Whitespace change:\n %s\n' % whitespace_issue
halcanary@google.com31fdb922014-01-06 19:50:22 +0000786
787
788def main(args):
789 """main function; see module-level docstring and GetOptionParser help.
790
791 Args:
792 args: sys.argv[1:]-type argument list.
793 """
794 option_parser = DepsRollConfig.GetOptionParser()
795 options = option_parser.parse_args(args)[0]
796
797 if not options.chromium_path:
798 option_parser.error('Must specify chromium_path.')
799 if not os.path.isdir(options.chromium_path):
800 option_parser.error('chromium_path must be a directory.')
801 if not test_git_executable(options.git_path):
802 option_parser.error('Invalid git executable.')
803
804 config = DepsRollConfig(options)
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000805 find_hash_and_roll_deps(config, options.revision, options.git_hash)
halcanary@google.com31fdb922014-01-06 19:50:22 +0000806
807
808if __name__ == '__main__':
809 main(sys.argv[1:])
810