blob: b34baac41837f0da8f61b3e656e80a773a2bd2f8 [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'
commit-bot@chromium.org34f47f92014-01-21 21:38:49 +000067 self.reviewers_list = ','.join([
68 # 'rmistry@google.com',
69 # 'reed@google.com',
70 # 'bsalomon@google.com',
71 # 'robertphillips@google.com',
72 ])
73 self.cc_list = ','.join([
74 # 'skia-team@google.com',
75 ])
halcanary@google.com31fdb922014-01-06 19:50:22 +000076
77 @staticmethod
78 def GetOptionParser():
79 # pylint: disable=I0011,C0103
80 """Returns an optparse.OptionParser object.
81
82 Returns:
83 An optparse.OptionParser object.
84
85 Called by the main() function.
86 """
87 default_bots_list = [
88 'android_clang_dbg',
89 'android_dbg',
90 'android_rel',
91 'cros_daisy',
92 'linux',
93 'linux_asan',
94 'linux_chromeos',
95 'linux_chromeos_asan',
96 'linux_gpu',
halcanary@google.com31fdb922014-01-06 19:50:22 +000097 'linux_layout',
98 'linux_layout_rel',
99 'mac',
100 'mac_asan',
101 'mac_gpu',
102 'mac_layout',
103 'mac_layout_rel',
104 'win',
105 'win_gpu',
106 'win_layout',
107 'win_layout_rel',
108 ]
109
110 option_parser = optparse.OptionParser(usage=__doc__)
111 # Anyone using this script on a regular basis should set the
112 # CHROMIUM_CHECKOUT_PATH environment variable.
113 option_parser.add_option(
114 '-c', '--chromium_path', help='Path to local Chromium Git'
115 ' repository checkout, defaults to CHROMIUM_CHECKOUT_PATH'
116 ' if that environment variable is set.',
117 default=os.environ.get('CHROMIUM_CHECKOUT_PATH'))
118 option_parser.add_option(
119 '-r', '--revision', type='int', default=None,
120 help='The Skia SVN revision number, defaults to top of tree.')
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000121 option_parser.add_option(
122 '-g', '--git_hash', default=None,
123 help='A partial Skia Git hash. Do not set this and revision.')
124
halcanary@google.com31fdb922014-01-06 19:50:22 +0000125 # Anyone using this script on a regular basis should set the
126 # SKIA_GIT_CHECKOUT_PATH environment variable.
127 option_parser.add_option(
128 '', '--skia_git_path',
129 help='Path of a pure-git Skia repository checkout. If empty,'
130 ' a temporary will be cloned. Defaults to SKIA_GIT_CHECKOUT'
131 '_PATH, if that environment variable is set.',
132 default=os.environ.get('SKIA_GIT_CHECKOUT_PATH'))
133 option_parser.add_option(
134 '', '--search_depth', type='int', default=100,
135 help='How far back to look for the revision.')
136 option_parser.add_option(
137 '', '--git_path', help='Git executable, defaults to "git".',
138 default='git')
139 option_parser.add_option(
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000140 '', '--delete_branches', help='Delete the temporary branches',
141 action='store_true', dest='delete_branches', default=False)
halcanary@google.com31fdb922014-01-06 19:50:22 +0000142 option_parser.add_option(
143 '', '--verbose', help='Do not suppress the output from `git cl`.',
144 action='store_true', dest='verbose', default=False)
145 option_parser.add_option(
146 '', '--skip_cl_upload', help='Skip the cl upload step; useful'
147 ' for testing or with --save_branches.',
148 action='store_true', default=False)
149
150 default_bots_help = (
151 'Comma-separated list of bots, defaults to a list of %d bots.'
152 ' To skip `git cl try`, set this to an empty string.'
153 % len(default_bots_list))
154 default_bots = ','.join(default_bots_list)
155 option_parser.add_option(
156 '', '--bots', help=default_bots_help, default=default_bots)
157
158 return option_parser
159
160
161def test_git_executable(git_executable):
162 """Test the git executable.
163
164 Args:
165 git_executable: git executable path.
166 Returns:
167 True if test is successful.
168 """
169 with open(os.devnull, 'w') as devnull:
170 try:
171 subprocess.call([git_executable, '--version'], stdout=devnull)
172 except (OSError,):
173 return False
174 return True
175
176
177class DepsRollError(Exception):
178 """Exceptions specific to this module."""
179 pass
180
181
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000182class VerboseSubprocess(object):
183 """Call subprocess methods, but print out command before executing.
halcanary@google.com31fdb922014-01-06 19:50:22 +0000184
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000185 Attributes:
186 verbose: (boolean) should we print out the command or not. If
187 not, this is the same as calling the subprocess method
188 quiet: (boolean) suppress stdout on check_call and call.
189 prefix: (string) When verbose, what to print before each command.
halcanary@google.com31fdb922014-01-06 19:50:22 +0000190 """
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000191
192 def __init__(self, verbose):
193 self.verbose = verbose
194 self.quiet = not verbose
195 self.prefix = '~~$ '
196
197 @staticmethod
198 def _fix(string):
199 """Quote and escape a string if necessary."""
200 if ' ' in string or '\n' in string:
201 string = '"%s"' % string.replace('\n', '\\n')
202 return string
203
204 @staticmethod
205 def print_subprocess_args(prefix, *args, **kwargs):
206 """Print out args in a human-readable manner."""
207 if 'cwd' in kwargs:
208 print '%scd %s' % (prefix, kwargs['cwd'])
209 print prefix + ' '.join(VerboseSubprocess._fix(arg) for arg in args[0])
210 if 'cwd' in kwargs:
211 print '%scd -' % prefix
212
213 def check_call(self, *args, **kwargs):
214 """Wrapper for subprocess.check_call().
215
216 Args:
217 *args: to be passed to subprocess.check_call()
218 **kwargs: to be passed to subprocess.check_call()
219 Returns:
220 Whatever subprocess.check_call() returns.
221 Raises:
222 OSError or subprocess.CalledProcessError: raised by check_call.
223 """
224 if self.verbose:
225 self.print_subprocess_args(self.prefix, *args, **kwargs)
226 if self.quiet:
227 with open(os.devnull, 'w') as devnull:
228 return subprocess.check_call(*args, stdout=devnull, **kwargs)
229 else:
230 return subprocess.check_call(*args, **kwargs)
231
232 def call(self, *args, **kwargs):
233 """Wrapper for subprocess.check().
234
235 Args:
236 *args: to be passed to subprocess.check_call()
237 **kwargs: to be passed to subprocess.check_call()
238 Returns:
239 Whatever subprocess.call() returns.
240 Raises:
241 OSError or subprocess.CalledProcessError: raised by call.
242 """
243 if self.verbose:
244 self.print_subprocess_args(self.prefix, *args, **kwargs)
245 if self.quiet:
246 with open(os.devnull, 'w') as devnull:
247 return subprocess.call(*args, stdout=devnull, **kwargs)
248 else:
249 return subprocess.call(*args, **kwargs)
250
251 def check_output(self, *args, **kwargs):
252 """Wrapper for subprocess.check_output().
253
254 Args:
255 *args: to be passed to subprocess.check_output()
256 **kwargs: to be passed to subprocess.check_output()
257 Returns:
258 Whatever subprocess.check_output() returns.
259 Raises:
260 OSError or subprocess.CalledProcessError: raised by check_output.
261 """
262 if self.verbose:
263 self.print_subprocess_args(self.prefix, *args, **kwargs)
264 return subprocess.check_output(*args, **kwargs)
265
266 def strip_output(self, *args, **kwargs):
267 """Wrap subprocess.check_output and str.strip().
268
269 Pass the given arguments into subprocess.check_output() and return
270 the results, after stripping any excess whitespace.
271
272 Args:
273 *args: to be passed to subprocess.check_output()
274 **kwargs: to be passed to subprocess.check_output()
275
276 Returns:
277 The output of the process as a string without leading or
278 trailing whitespace.
279 Raises:
280 OSError or subprocess.CalledProcessError: raised by check_output.
281 """
282 if self.verbose:
283 self.print_subprocess_args(self.prefix, *args, **kwargs)
284 return str(subprocess.check_output(*args, **kwargs)).strip()
285
286 def popen(self, *args, **kwargs):
287 """Wrapper for subprocess.Popen().
288
289 Args:
290 *args: to be passed to subprocess.Popen()
291 **kwargs: to be passed to subprocess.Popen()
292 Returns:
293 The output of subprocess.Popen()
294 Raises:
295 OSError or subprocess.CalledProcessError: raised by Popen.
296 """
297 if self.verbose:
298 self.print_subprocess_args(self.prefix, *args, **kwargs)
299 return subprocess.Popen(*args, **kwargs)
halcanary@google.com31fdb922014-01-06 19:50:22 +0000300
301
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000302class ChangeDir(object):
303 """Use with a with-statement to temporarily change directories."""
304 # pylint: disable=I0011,R0903
halcanary@google.com31fdb922014-01-06 19:50:22 +0000305
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000306 def __init__(self, directory, verbose=False):
307 self._directory = directory
308 self._verbose = verbose
309
310 def __enter__(self):
311 if self._verbose:
312 print '~~$ cd %s' % self._directory
313 cwd = os.getcwd()
314 os.chdir(self._directory)
315 self._directory = cwd
316
317 def __exit__(self, etype, value, traceback):
318 if self._verbose:
319 print '~~$ cd %s' % self._directory
320 os.chdir(self._directory)
321
322
323class ReSearch(object):
324 """A collection of static methods for regexing things."""
325
326 @staticmethod
327 def search_within_stream(input_stream, pattern, default=None):
328 """Search for regular expression in a file-like object.
329
330 Opens a file for reading and searches line by line for a match to
331 the regex and returns the parenthesized group named return for the
332 first match. Does not search across newlines.
333
334 For example:
335 pattern = '^root(:[^:]*){4}:(?P<return>[^:]*)'
336 with open('/etc/passwd', 'r') as stream:
337 return search_within_file(stream, pattern)
338 should return root's home directory (/root on my system).
339
340 Args:
341 input_stream: file-like object to be read
342 pattern: (string) to be passed to re.compile
343 default: what to return if no match
344
345 Returns:
346 A string or whatever default is
347 """
348 pattern_object = re.compile(pattern)
349 for line in input_stream:
350 match = pattern_object.search(line)
351 if match:
352 return match.group('return')
353 return default
354
355 @staticmethod
356 def search_within_string(input_string, pattern, default=None):
357 """Search for regular expression in a string.
358
359 Args:
360 input_string: (string) to be searched
361 pattern: (string) to be passed to re.compile
362 default: what to return if no match
363
364 Returns:
365 A string or whatever default is
366 """
367 match = re.search(pattern, input_string)
368 return match.group('return') if match else default
369
370 @staticmethod
371 def search_within_output(verbose, pattern, default, *args, **kwargs):
372 """Search for regular expression in a process output.
373
374 Does not search across newlines.
375
376 Args:
377 verbose: (boolean) shoule we call
378 VerboseSubprocess.print_subprocess_args?
379 pattern: (string) to be passed to re.compile
380 default: what to return if no match
381 *args: to be passed to subprocess.Popen()
382 **kwargs: to be passed to subprocess.Popen()
383
384 Returns:
385 A string or whatever default is
386 """
387 if verbose:
388 VerboseSubprocess.print_subprocess_args(
389 '~~$ ', *args, **kwargs)
390 proc = subprocess.Popen(*args, stdout=subprocess.PIPE, **kwargs)
391 return ReSearch.search_within_stream(proc.stdout, pattern, default)
392
393
394def get_svn_revision(config, commit):
395 """Works in both git and git-svn. returns a string."""
396 svn_format = (
397 '(git-svn-id: [^@ ]+@|SVN changes up to revision |'
398 'LKGR w/ DEPS up to revision )(?P<return>[0-9]+)')
399 svn_revision = ReSearch.search_within_output(
400 config.verbose, svn_format, None,
401 [config.git, 'log', '-n', '1', '--format=format:%B', commit])
402 if not svn_revision:
403 raise DepsRollError(
404 'Revision number missing from Chromium origin/master.')
405 return int(svn_revision)
406
407
408class SkiaGitCheckout(object):
409 """Class to create a temporary skia git checkout, if necessary.
halcanary@google.com31fdb922014-01-06 19:50:22 +0000410 """
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000411 # pylint: disable=I0011,R0903
412
413 def __init__(self, config, depth):
414 self._config = config
415 self._depth = depth
416 self._use_temp = None
417 self._original_cwd = None
418
419 def __enter__(self):
420 config = self._config
421 git = config.git
422 skia_dir = None
423 self._original_cwd = os.getcwd()
424 if config.skia_git_checkout_path:
425 skia_dir = config.skia_git_checkout_path
426 ## Update origin/master if needed.
427 if self._config.verbose:
428 print '~~$', 'cd', skia_dir
429 os.chdir(skia_dir)
430 config.vsp.check_call([git, 'fetch', '-q', 'origin'])
431 self._use_temp = None
432 else:
433 skia_dir = tempfile.mkdtemp(prefix='git_skia_tmp_')
434 self._use_temp = skia_dir
435 try:
436 os.chdir(skia_dir)
437 config.vsp.check_call(
438 [git, 'clone', '-q', '--depth=%d' % self._depth,
439 '--single-branch', config.skia_url, '.'])
440 except (OSError, subprocess.CalledProcessError) as error:
441 shutil.rmtree(skia_dir)
442 raise error
443
444 def __exit__(self, etype, value, traceback):
445 if self._config.verbose:
446 print '~~$', 'cd', self._original_cwd
447 os.chdir(self._original_cwd)
448 if self._use_temp:
449 shutil.rmtree(self._use_temp)
halcanary@google.com31fdb922014-01-06 19:50:22 +0000450
451
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000452def revision_and_hash(config):
halcanary@google.com31fdb922014-01-06 19:50:22 +0000453 """Finds revision number and git hash of origin/master in the Skia tree.
454
455 Args:
456 config: (roll_deps.DepsRollConfig) object containing options.
halcanary@google.com31fdb922014-01-06 19:50:22 +0000457
458 Returns:
459 A tuple (revision, hash)
460 revision: (int) SVN revision number.
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000461 git_hash: (string) full Git commit hash.
halcanary@google.com31fdb922014-01-06 19:50:22 +0000462
463 Raises:
464 roll_deps.DepsRollError: if the revision can't be found.
465 OSError: failed to execute git or git-cl.
466 subprocess.CalledProcessError: git returned unexpected status.
467 """
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000468 with SkiaGitCheckout(config, 1):
469 revision = get_svn_revision(config, 'origin/master')
470 git_hash = config.vsp.strip_output(
471 [config.git, 'show-ref', 'origin/master', '--hash'])
472 if not git_hash:
halcanary@google.com31fdb922014-01-06 19:50:22 +0000473 raise DepsRollError('Git hash can not be found.')
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000474 return revision, git_hash
475
476
477def revision_and_hash_from_revision(config, revision):
478 """Finds revision number and git hash of a commit in the Skia tree.
479
480 Args:
481 config: (roll_deps.DepsRollConfig) object containing options.
482 revision: (int) SVN revision number.
483
484 Returns:
485 A tuple (revision, hash)
486 revision: (int) SVN revision number.
487 git_hash: (string) full Git commit hash.
488
489 Raises:
490 roll_deps.DepsRollError: if the revision can't be found.
491 OSError: failed to execute git or git-cl.
492 subprocess.CalledProcessError: git returned unexpected status.
493 """
494 with SkiaGitCheckout(config, config.search_depth):
495 revision_regex = config.revision_format % revision
496 git_hash = config.vsp.strip_output(
497 [config.git, 'log', '--grep', revision_regex,
498 '--format=format:%H', 'origin/master'])
499 if not git_hash:
500 raise DepsRollError('Git hash can not be found.')
501 return revision, git_hash
502
503
504def revision_and_hash_from_partial(config, partial_hash):
505 """Returns the SVN revision number and full git hash.
506
507 Args:
508 config: (roll_deps.DepsRollConfig) object containing options.
509 partial_hash: (string) Partial git commit hash.
510
511 Returns:
512 A tuple (revision, hash)
513 revision: (int) SVN revision number.
514 git_hash: (string) full Git commit hash.
515
516 Raises:
517 roll_deps.DepsRollError: if the revision can't be found.
518 OSError: failed to execute git or git-cl.
519 subprocess.CalledProcessError: git returned unexpected status.
520 """
521 with SkiaGitCheckout(config, config.search_depth):
522 git_hash = config.vsp.strip_output(
523 ['git', 'log', '-n', '1', '--format=format:%H', partial_hash])
524 if not git_hash:
525 raise DepsRollError('Partial Git hash can not be found.')
526 revision = get_svn_revision(config, git_hash)
527 return revision, git_hash
halcanary@google.com31fdb922014-01-06 19:50:22 +0000528
529
530class GitBranchCLUpload(object):
531 """Class to manage git branches and git-cl-upload.
532
533 This class allows one to create a new branch in a repository based
534 off of origin/master, make changes to the tree inside the
535 with-block, upload that new branch to Rietveld, restore the original
536 tree state, and delete the local copy of the new branch.
537
538 See roll_deps() for an example of use.
539
540 Constructor Args:
541 config: (roll_deps.DepsRollConfig) object containing options.
542 message: (string) the commit message, can be multiline.
543 set_brach_name: (string or none) if not None, the name of the
544 branch to use. If None, then use a temporary branch that
545 will be deleted.
546
547 Attributes:
548 issue: a string describing the codereview issue, after __exit__
549 has been called, othrwise, None.
550
551 Raises:
552 OSError: failed to execute git or git-cl.
553 subprocess.CalledProcessError: git returned unexpected status.
554 """
555 # pylint: disable=I0011,R0903,R0902
556
557 def __init__(self, config, message, set_branch_name):
558 self._message = message
559 self._file_list = []
560 self._branch_name = set_branch_name
561 self._stash = None
562 self._original_branch = None
563 self._config = config
halcanary@google.com31fdb922014-01-06 19:50:22 +0000564 self.issue = None
565
566 def stage_for_commit(self, *paths):
567 """Calls `git add ...` on each argument.
568
569 Args:
570 *paths: (list of strings) list of filenames to pass to `git add`.
571 """
572 self._file_list.extend(paths)
573
574 def __enter__(self):
575 git = self._config.git
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000576 vsp = self._config.vsp
halcanary@google.com31fdb922014-01-06 19:50:22 +0000577 def branch_exists(branch):
578 """Return true iff branch exists."""
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000579 return 0 == vsp.call([git, 'show-ref', '--quiet', branch])
halcanary@google.com31fdb922014-01-06 19:50:22 +0000580 def has_diff():
581 """Return true iff repository has uncommited changes."""
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000582 return bool(vsp.call([git, 'diff', '--quiet', 'HEAD']))
583
halcanary@google.com31fdb922014-01-06 19:50:22 +0000584 self._stash = has_diff()
585 if self._stash:
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000586 vsp.check_call([git, 'stash', 'save'])
halcanary@google.com31fdb922014-01-06 19:50:22 +0000587 try:
borenet@google.com4d5e1362014-01-10 21:02:25 +0000588 full_branch = vsp.strip_output([git, 'symbolic-ref', 'HEAD'])
589 self._original_branch = full_branch.split('/')[-1]
halcanary@google.com31fdb922014-01-06 19:50:22 +0000590 except (subprocess.CalledProcessError,):
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000591 self._original_branch = vsp.strip_output(
halcanary@google.com31fdb922014-01-06 19:50:22 +0000592 [git, 'rev-parse', 'HEAD'])
593
594 if not self._branch_name:
595 self._branch_name = self._config.default_branch_name
596
597 if branch_exists(self._branch_name):
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000598 vsp.check_call([git, 'checkout', '-q', 'master'])
borenet@google.com4d5e1362014-01-10 21:02:25 +0000599 vsp.check_call([git, 'branch', '-D', self._branch_name])
halcanary@google.com31fdb922014-01-06 19:50:22 +0000600
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000601 vsp.check_call(
602 [git, 'checkout', '-q', '-b', self._branch_name, 'origin/master'])
halcanary@google.com31fdb922014-01-06 19:50:22 +0000603
604 def __exit__(self, etype, value, traceback):
605 # pylint: disable=I0011,R0912
606 git = self._config.git
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000607 vsp = self._config.vsp
608 svn_info = str(get_svn_revision(self._config, 'HEAD'))
halcanary@google.com31fdb922014-01-06 19:50:22 +0000609
610 for filename in self._file_list:
611 assert os.path.exists(filename)
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000612 vsp.check_call([git, 'add', filename])
613 vsp.check_call([git, 'commit', '-q', '-m', self._message])
halcanary@google.com31fdb922014-01-06 19:50:22 +0000614
commit-bot@chromium.org34f47f92014-01-21 21:38:49 +0000615 git_cl = [git, 'cl', 'upload', '-f',
halcanary@google.com31fdb922014-01-06 19:50:22 +0000616 '--bypass-hooks', '--bypass-watchlists']
commit-bot@chromium.org34f47f92014-01-21 21:38:49 +0000617 if self._config.cc_list:
618 git_cl.append('--cc=%s' % self._config.cc_list)
619 if self._config.reviewers_list:
620 git_cl.append('--reviewers=%s' % self._config.reviewers_list)
621
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000622 git_try = [git, 'cl', 'try', '--revision', svn_info]
halcanary@google.com31fdb922014-01-06 19:50:22 +0000623 git_try.extend([arg for bot in self._config.cl_bot_list
624 for arg in ('-b', bot)])
625
626 if self._config.skip_cl_upload:
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000627 print 'You should call:'
628 print ' cd %s' % os.getcwd()
629 VerboseSubprocess.print_subprocess_args(
630 ' ', [git, 'checkout', self._branch_name])
631 VerboseSubprocess.print_subprocess_args(' ', git_cl)
halcanary@google.com31fdb922014-01-06 19:50:22 +0000632 if self._config.cl_bot_list:
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000633 VerboseSubprocess.print_subprocess_args(' ', git_try)
634 print
halcanary@google.com31fdb922014-01-06 19:50:22 +0000635 self.issue = ''
636 else:
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000637 vsp.check_call(git_cl)
638 self.issue = vsp.strip_output([git, 'cl', 'issue'])
halcanary@google.com31fdb922014-01-06 19:50:22 +0000639 if self._config.cl_bot_list:
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000640 vsp.check_call(git_try)
halcanary@google.com31fdb922014-01-06 19:50:22 +0000641
642 # deal with the aftermath of failed executions of this script.
643 if self._config.default_branch_name == self._original_branch:
644 self._original_branch = 'master'
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000645 vsp.check_call([git, 'checkout', '-q', self._original_branch])
halcanary@google.com31fdb922014-01-06 19:50:22 +0000646
647 if self._config.default_branch_name == self._branch_name:
borenet@google.com4d5e1362014-01-10 21:02:25 +0000648 vsp.check_call([git, 'branch', '-D', self._branch_name])
halcanary@google.com31fdb922014-01-06 19:50:22 +0000649 if self._stash:
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000650 vsp.check_call([git, 'stash', 'pop'])
halcanary@google.com31fdb922014-01-06 19:50:22 +0000651
652
653def change_skia_deps(revision, git_hash, depspath):
654 """Update the DEPS file.
655
656 Modify the skia_revision and skia_hash entries in the given DEPS file.
657
658 Args:
659 revision: (int) Skia SVN revision.
660 git_hash: (string) Skia Git hash.
661 depspath: (string) path to DEPS file.
662 """
663 temp_file = tempfile.NamedTemporaryFile(delete=False,
664 prefix='skia_DEPS_ROLL_tmp_')
665 try:
666 deps_regex_rev = re.compile('"skia_revision": "[0-9]*",')
667 deps_regex_hash = re.compile('"skia_hash": "[0-9a-f]*",')
668
669 deps_regex_rev_repl = '"skia_revision": "%d",' % revision
670 deps_regex_hash_repl = '"skia_hash": "%s",' % git_hash
671
672 with open(depspath, 'r') as input_stream:
673 for line in input_stream:
674 line = deps_regex_rev.sub(deps_regex_rev_repl, line)
675 line = deps_regex_hash.sub(deps_regex_hash_repl, line)
676 temp_file.write(line)
677 finally:
678 temp_file.close()
679 shutil.move(temp_file.name, depspath)
680
681
halcanary@google.com31fdb922014-01-06 19:50:22 +0000682def roll_deps(config, revision, git_hash):
683 """Upload changed DEPS and a whitespace change.
684
685 Given the correct git_hash, create two Reitveld issues.
686
687 Args:
688 config: (roll_deps.DepsRollConfig) object containing options.
689 revision: (int) Skia SVN revision.
690 git_hash: (string) Skia Git hash.
691
692 Returns:
693 a tuple containing textual description of the two issues.
694
695 Raises:
696 OSError: failed to execute git or git-cl.
697 subprocess.CalledProcessError: git returned unexpected status.
698 """
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000699
halcanary@google.com31fdb922014-01-06 19:50:22 +0000700 git = config.git
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000701 with ChangeDir(config.chromium_path, config.verbose):
702 config.vsp.check_call([git, 'fetch', '-q', 'origin'])
703
704 old_revision = ReSearch.search_within_output(
705 config.verbose, '"skia_revision": "(?P<return>[0-9]+)",', None,
706 [git, 'show', 'origin/master:DEPS'])
707 assert old_revision
708 if revision == int(old_revision):
709 print 'DEPS is up to date!'
710 return None
711
712 master_hash = config.vsp.strip_output(
halcanary@google.com31fdb922014-01-06 19:50:22 +0000713 [git, 'show-ref', 'origin/master', '--hash'])
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000714 master_revision = get_svn_revision(config, 'origin/master')
715
716 branch = None
halcanary@google.com31fdb922014-01-06 19:50:22 +0000717
718 # master_hash[8] gives each whitespace CL a unique name.
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000719 message = ('whitespace change %s\n\n'
720 'Chromium base revision: %d / %s\n\n'
721 'This CL was created by Skia\'s roll_deps.py script.\n'
722 ) % (master_hash[:8], master_revision, master_hash[:8])
723 if config.save_branches:
724 branch = 'control_%s' % master_hash[:8]
halcanary@google.com31fdb922014-01-06 19:50:22 +0000725
726 codereview = GitBranchCLUpload(config, message, branch)
727 with codereview:
728 with open('build/whitespace_file.txt', 'a') as output_stream:
729 output_stream.write('\nCONTROL\n')
730 codereview.stage_for_commit('build/whitespace_file.txt')
731 whitespace_cl = codereview.issue
732 if branch:
733 whitespace_cl = '%s\n branch: %s' % (whitespace_cl, branch)
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000734
735 control_url = ReSearch.search_within_string(
736 codereview.issue, '(?P<return>https?://[^) ]+)', '?')
737
738 if config.save_branches:
739 branch = 'roll_%d_%s' % (revision, master_hash[:8])
740 message = (
741 'roll skia DEPS to %d\n\n'
742 'Chromium base revision: %d / %s\n'
743 'Old Skia revision: %s\n'
744 'New Skia revision: %d\n'
745 'Control CL: %s\n\n'
746 'This CL was created by Skia\'s roll_deps.py script.\n'
747 % (revision, master_revision, master_hash[:8],
748 old_revision, revision, control_url))
halcanary@google.com31fdb922014-01-06 19:50:22 +0000749 codereview = GitBranchCLUpload(config, message, branch)
750 with codereview:
751 change_skia_deps(revision, git_hash, 'DEPS')
752 codereview.stage_for_commit('DEPS')
753 deps_cl = codereview.issue
754 if branch:
755 deps_cl = '%s\n branch: %s' % (deps_cl, branch)
756
757 return deps_cl, whitespace_cl
halcanary@google.com31fdb922014-01-06 19:50:22 +0000758
759
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000760def find_hash_and_roll_deps(config, revision=None, partial_hash=None):
halcanary@google.com31fdb922014-01-06 19:50:22 +0000761 """Call find_hash_from_revision() and roll_deps().
762
763 The calls to git will be verbose on standard output. After a
764 successful upload of both issues, print links to the new
765 codereview issues.
766
767 Args:
768 config: (roll_deps.DepsRollConfig) object containing options.
769 revision: (int or None) the Skia SVN revision number or None
770 to use the tip of the tree.
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000771 partial_hash: (string or None) a partial pure-git Skia commit
772 hash. Don't pass both partial_hash and revision.
halcanary@google.com31fdb922014-01-06 19:50:22 +0000773
774 Raises:
775 roll_deps.DepsRollError: if the revision can't be found.
776 OSError: failed to execute git or git-cl.
777 subprocess.CalledProcessError: git returned unexpected status.
778 """
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000779
780 if revision and partial_hash:
781 raise DepsRollError('Pass revision or partial_hash, not both.')
782
783 if partial_hash:
784 revision, git_hash = revision_and_hash_from_partial(
785 config, partial_hash)
786 elif revision:
787 revision, git_hash = revision_and_hash_from_revision(config, revision)
788 else:
789 revision, git_hash = revision_and_hash(config)
halcanary@google.com31fdb922014-01-06 19:50:22 +0000790
791 print 'revision=%r\nhash=%r\n' % (revision, git_hash)
792
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000793 roll = roll_deps(config, revision, git_hash)
halcanary@google.com31fdb922014-01-06 19:50:22 +0000794
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000795 if roll:
796 deps_issue, whitespace_issue = roll
797 print 'DEPS roll:\n %s\n' % deps_issue
798 print 'Whitespace change:\n %s\n' % whitespace_issue
halcanary@google.com31fdb922014-01-06 19:50:22 +0000799
800
801def main(args):
802 """main function; see module-level docstring and GetOptionParser help.
803
804 Args:
805 args: sys.argv[1:]-type argument list.
806 """
807 option_parser = DepsRollConfig.GetOptionParser()
808 options = option_parser.parse_args(args)[0]
809
810 if not options.chromium_path:
811 option_parser.error('Must specify chromium_path.')
812 if not os.path.isdir(options.chromium_path):
813 option_parser.error('chromium_path must be a directory.')
814 if not test_git_executable(options.git_path):
815 option_parser.error('Invalid git executable.')
816
817 config = DepsRollConfig(options)
halcanary@google.com8c5d2c12014-01-08 21:29:34 +0000818 find_hash_and_roll_deps(config, options.revision, options.git_hash)
halcanary@google.com31fdb922014-01-06 19:50:22 +0000819
820
821if __name__ == '__main__':
822 main(sys.argv[1:])
823