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

Side by Side Diff: tools/roll_deps.py

Issue 126523002: Changes to roll_deps.py (Closed) Base URL: https://skia.googlecode.com/svn/trunk
Patch Set: skia:1995 Created 6 years, 11 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 | « no previous file | 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
1 #!/usr/bin/python2 1 #!/usr/bin/python2
2 2
3 # Copyright 2014 Google Inc. 3 # Copyright 2014 Google Inc.
4 # 4 #
5 # Use of this source code is governed by a BSD-style license that can be 5 # Use of this source code is governed by a BSD-style license that can be
6 # found in the LICENSE file. 6 # found in the LICENSE file.
7 7
8 """Skia's Chromium DEPS roll script. 8 """Skia's Chromium DEPS roll script.
9 9
10 This script: 10 This script:
11 - searches through the last N Skia git commits to find out the hash that is 11 - searches through the last N Skia git commits to find out the hash that is
12 associated with the SVN revision number. 12 associated with the SVN revision number.
13 - creates a new branch in the Chromium tree, modifies the DEPS file to 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 14 point at the given Skia commit, commits, uploads to Rietveld, and
15 deletes the local copy of the branch. 15 deletes the local copy of the branch.
16 - creates a whitespace-only commit and uploads that to to Rietveld. 16 - creates a whitespace-only commit and uploads that to to Rietveld.
17 - returns the Chromium tree to its previous state. 17 - returns the Chromium tree to its previous state.
18 18
19 Usage: 19 Usage:
20 %prog -c CHROMIUM_PATH -r REVISION [OPTIONAL_OPTIONS] 20 %prog -c CHROMIUM_PATH -r REVISION [OPTIONAL_OPTIONS]
21 """ 21 """
22 22
23 23
24 import optparse 24 import optparse
25 import os 25 import os
26 import re 26 import re
27 import shutil 27 import shutil
28 import subprocess 28 import subprocess
29 from subprocess import check_call
30 import sys 29 import sys
31 import tempfile 30 import tempfile
32 31
33 32
34 class DepsRollConfig(object): 33 class DepsRollConfig(object):
35 """Contains configuration options for this module. 34 """Contains configuration options for this module.
36 35
37 Attributes: 36 Attributes:
38 git: (string) The git executable. 37 git: (string) The git executable.
39 chromium_path: (string) path to a local chromium git repository. 38 chromium_path: (string) path to a local chromium git repository.
40 save_branches: (boolean) iff false, delete temporary branches. 39 save_branches: (boolean) iff false, delete temporary branches.
41 verbose: (boolean) iff false, suppress the output from git-cl. 40 verbose: (boolean) iff false, suppress the output from git-cl.
42 search_depth: (int) how far back to look for the revision. 41 search_depth: (int) how far back to look for the revision.
43 skia_url: (string) Skia's git repository. 42 skia_url: (string) Skia's git repository.
44 self.skip_cl_upload: (boolean) 43 self.skip_cl_upload: (boolean)
45 self.cl_bot_list: (list of strings) 44 self.cl_bot_list: (list of strings)
46 """ 45 """
47 46
48 # pylint: disable=I0011,R0903,R0902 47 # pylint: disable=I0011,R0903,R0902
49 def __init__(self, options=None): 48 def __init__(self, options=None):
50 self.skia_url = 'https://skia.googlesource.com/skia.git' 49 self.skia_url = 'https://skia.googlesource.com/skia.git'
51 self.revision_format = ( 50 self.revision_format = (
52 'git-svn-id: http://skia.googlecode.com/svn/trunk@%d ') 51 'git-svn-id: http://skia.googlecode.com/svn/trunk@%d ')
53 52
54 if not options: 53 if not options:
55 options = DepsRollConfig.GetOptionParser() 54 options = DepsRollConfig.GetOptionParser()
56 # pylint: disable=I0011,E1103 55 # pylint: disable=I0011,E1103
57 self.verbose = options.verbose 56 self.verbose = options.verbose
58 self.save_branches = options.save_branches 57 self.vsp = VerboseSubprocess(self.verbose)
58 self.save_branches = not options.delete_branches
59 self.search_depth = options.search_depth 59 self.search_depth = options.search_depth
60 self.chromium_path = options.chromium_path 60 self.chromium_path = options.chromium_path
61 self.git = options.git_path 61 self.git = options.git_path
62 self.skip_cl_upload = options.skip_cl_upload 62 self.skip_cl_upload = options.skip_cl_upload
63 # Split and remove empty strigns from the bot list. 63 # Split and remove empty strigns from the bot list.
64 self.cl_bot_list = [bot for bot in options.bots.split(',') if bot] 64 self.cl_bot_list = [bot for bot in options.bots.split(',') if bot]
65 self.skia_git_checkout_path = options.skia_git_path 65 self.skia_git_checkout_path = options.skia_git_path
66 self.default_branch_name = 'autogenerated_deps_roll_branch' 66 self.default_branch_name = 'autogenerated_deps_roll_branch'
67 67
68 @staticmethod 68 @staticmethod
(...skipping 34 matching lines...) Expand 10 before | Expand all | Expand 10 after
103 # Anyone using this script on a regular basis should set the 103 # Anyone using this script on a regular basis should set the
104 # CHROMIUM_CHECKOUT_PATH environment variable. 104 # CHROMIUM_CHECKOUT_PATH environment variable.
105 option_parser.add_option( 105 option_parser.add_option(
106 '-c', '--chromium_path', help='Path to local Chromium Git' 106 '-c', '--chromium_path', help='Path to local Chromium Git'
107 ' repository checkout, defaults to CHROMIUM_CHECKOUT_PATH' 107 ' repository checkout, defaults to CHROMIUM_CHECKOUT_PATH'
108 ' if that environment variable is set.', 108 ' if that environment variable is set.',
109 default=os.environ.get('CHROMIUM_CHECKOUT_PATH')) 109 default=os.environ.get('CHROMIUM_CHECKOUT_PATH'))
110 option_parser.add_option( 110 option_parser.add_option(
111 '-r', '--revision', type='int', default=None, 111 '-r', '--revision', type='int', default=None,
112 help='The Skia SVN revision number, defaults to top of tree.') 112 help='The Skia SVN revision number, defaults to top of tree.')
113 option_parser.add_option(
114 '-g', '--git_hash', default=None,
115 help='A partial Skia Git hash. Do not set this and revision.')
116
113 # Anyone using this script on a regular basis should set the 117 # Anyone using this script on a regular basis should set the
114 # SKIA_GIT_CHECKOUT_PATH environment variable. 118 # SKIA_GIT_CHECKOUT_PATH environment variable.
115 option_parser.add_option( 119 option_parser.add_option(
116 '', '--skia_git_path', 120 '', '--skia_git_path',
117 help='Path of a pure-git Skia repository checkout. If empty,' 121 help='Path of a pure-git Skia repository checkout. If empty,'
118 ' a temporary will be cloned. Defaults to SKIA_GIT_CHECKOUT' 122 ' a temporary will be cloned. Defaults to SKIA_GIT_CHECKOUT'
119 '_PATH, if that environment variable is set.', 123 '_PATH, if that environment variable is set.',
120 default=os.environ.get('SKIA_GIT_CHECKOUT_PATH')) 124 default=os.environ.get('SKIA_GIT_CHECKOUT_PATH'))
121 option_parser.add_option( 125 option_parser.add_option(
122 '', '--search_depth', type='int', default=100, 126 '', '--search_depth', type='int', default=100,
123 help='How far back to look for the revision.') 127 help='How far back to look for the revision.')
124 option_parser.add_option( 128 option_parser.add_option(
125 '', '--git_path', help='Git executable, defaults to "git".', 129 '', '--git_path', help='Git executable, defaults to "git".',
126 default='git') 130 default='git')
127 option_parser.add_option( 131 option_parser.add_option(
128 '', '--save_branches', help='Save the temporary branches', 132 '', '--delete_branches', help='Delete the temporary branches',
129 action='store_true', dest='save_branches', default=False) 133 action='store_true', dest='delete_branches', default=False)
130 option_parser.add_option( 134 option_parser.add_option(
131 '', '--verbose', help='Do not suppress the output from `git cl`.', 135 '', '--verbose', help='Do not suppress the output from `git cl`.',
132 action='store_true', dest='verbose', default=False) 136 action='store_true', dest='verbose', default=False)
133 option_parser.add_option( 137 option_parser.add_option(
134 '', '--skip_cl_upload', help='Skip the cl upload step; useful' 138 '', '--skip_cl_upload', help='Skip the cl upload step; useful'
135 ' for testing or with --save_branches.', 139 ' for testing or with --save_branches.',
136 action='store_true', default=False) 140 action='store_true', default=False)
137 141
138 default_bots_help = ( 142 default_bots_help = (
139 'Comma-separated list of bots, defaults to a list of %d bots.' 143 'Comma-separated list of bots, defaults to a list of %d bots.'
(...skipping 20 matching lines...) Expand all
160 except (OSError,): 164 except (OSError,):
161 return False 165 return False
162 return True 166 return True
163 167
164 168
165 class DepsRollError(Exception): 169 class DepsRollError(Exception):
166 """Exceptions specific to this module.""" 170 """Exceptions specific to this module."""
167 pass 171 pass
168 172
169 173
170 def strip_output(*args, **kwargs): 174 class VerboseSubprocess(object):
171 """Wrap subprocess.check_output and str.strip(). 175 """Call subprocess methods, but print out command before executing.
172 176
173 Pass the given arguments into subprocess.check_output() and return 177 Attributes:
174 the results, after stripping any excess whitespace. 178 verbose: (boolean) should we print out the command or not. If
175 179 not, this is the same as calling the subprocess method
176 Args: 180 quiet: (boolean) suppress stdout on check_call and call.
177 *args: to be passed to subprocess.check_output() 181 prefix: (string) When verbose, what to print before each command.
178 **kwargs: to be passed to subprocess.check_output() 182 """
179 183
180 Returns: 184 def __init__(self, verbose):
181 The output of the process as a string without leading or 185 self.verbose = verbose
182 trailing whitespace. 186 self.quiet = not verbose
183 Raises: 187 self.prefix = '~~$ '
184 OSError or subprocess.CalledProcessError: raised by check_output. 188
185 """ 189 @staticmethod
186 return str(subprocess.check_output(*args, **kwargs)).strip() 190 def _fix(string):
187 191 """Quote and escape a string if necessary."""
188 192 if ' ' in string or '\n' in string:
189 def create_temp_skia_clone(config, depth): 193 string = '"%s"' % string.replace('\n', '\\n')
190 """Clones Skia in a temp dir. 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)
292
293
294 class ChangeDir(object):
295 """Use with a with-statement to temporarily change directories."""
296 # pylint: disable=I0011,R0903
297
298 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
315 class 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
386 def 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
400 class SkiaGitCheckout(object):
401 """Class to create a temporary skia git checkout, if necessary.
402 """
403 # 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)
442
443
444 def revision_and_hash(config):
445 """Finds revision number and git hash of origin/master in the Skia tree.
191 446
192 Args: 447 Args:
193 config: (roll_deps.DepsRollConfig) object containing options. 448 config: (roll_deps.DepsRollConfig) object containing options.
194 depth: (int) how far back to clone the tree.
195 Returns:
196 temporary directory path if succcessful.
197 Raises:
198 OSError, subprocess.CalledProcessError on failure.
199 """
200 git = config.git
201 skia_dir = tempfile.mkdtemp(prefix='git_skia_tmp_')
202 try:
203 check_call(
204 [git, 'clone', '-q', '--depth=%d' % depth,
205 '--single-branch', config.skia_url, skia_dir])
206 return skia_dir
207 except (OSError, subprocess.CalledProcessError) as error:
208 shutil.rmtree(skia_dir)
209 raise error
210
211
212 def find_revision_and_hash(config, revision):
213 """Finds revision number and git hash of origin/master in the Skia tree.
214
215 Args:
216 config: (roll_deps.DepsRollConfig) object containing options.
217 revision: (int or None) SVN revision number. If None, use
218 tip-of-tree.
219 449
220 Returns: 450 Returns:
221 A tuple (revision, hash) 451 A tuple (revision, hash)
222 revision: (int) SVN revision number. 452 revision: (int) SVN revision number.
223 hash: (string) full Git commit hash. 453 git_hash: (string) full Git commit hash.
224 454
225 Raises: 455 Raises:
226 roll_deps.DepsRollError: if the revision can't be found. 456 roll_deps.DepsRollError: if the revision can't be found.
227 OSError: failed to execute git or git-cl. 457 OSError: failed to execute git or git-cl.
228 subprocess.CalledProcessError: git returned unexpected status. 458 subprocess.CalledProcessError: git returned unexpected status.
229 """ 459 """
230 git = config.git 460 with SkiaGitCheckout(config, 1):
231 use_temp = False 461 revision = get_svn_revision(config, 'origin/master')
232 skia_dir = None 462 git_hash = config.vsp.strip_output(
233 depth = 1 if (revision is None) else config.search_depth 463 [config.git, 'show-ref', 'origin/master', '--hash'])
234 try: 464 if not git_hash:
235 if config.skia_git_checkout_path:
236 skia_dir = config.skia_git_checkout_path
237 ## Update origin/master if needed.
238 check_call([git, 'fetch', '-q', 'origin'], cwd=skia_dir)
239 else:
240 skia_dir = create_temp_skia_clone(config, depth)
241 assert skia_dir
242 use_temp = True
243
244 if revision is None:
245 message = subprocess.check_output(
246 [git, 'log', '-n', '1', '--format=format:%B',
247 'origin/master'], cwd=skia_dir)
248 svn_format = (
249 'git-svn-id: http://skia.googlecode.com/svn/trunk@([0-9]+) ')
250 search = re.search(svn_format, message)
251 if not search:
252 raise DepsRollError(
253 'Revision number missing from origin/master.')
254 revision = int(search.group(1))
255 git_hash = strip_output(
256 [git, 'show-ref', 'origin/master', '--hash'], cwd=skia_dir)
257 else:
258 revision_regex = config.revision_format % revision
259 git_hash = strip_output(
260 [git, 'log', '--grep', revision_regex, '--format=format:%H',
261 'origin/master'], cwd=skia_dir)
262
263 if revision < 0 or not git_hash:
264 raise DepsRollError('Git hash can not be found.') 465 raise DepsRollError('Git hash can not be found.')
265 return revision, git_hash 466 return revision, git_hash
266 finally: 467
267 if use_temp: 468
268 shutil.rmtree(skia_dir) 469 def 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
496 def 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
269 520
270 521
271 class GitBranchCLUpload(object): 522 class GitBranchCLUpload(object):
272 """Class to manage git branches and git-cl-upload. 523 """Class to manage git branches and git-cl-upload.
273 524
274 This class allows one to create a new branch in a repository based 525 This class allows one to create a new branch in a repository based
275 off of origin/master, make changes to the tree inside the 526 off of origin/master, make changes to the tree inside the
276 with-block, upload that new branch to Rietveld, restore the original 527 with-block, upload that new branch to Rietveld, restore the original
277 tree state, and delete the local copy of the new branch. 528 tree state, and delete the local copy of the new branch.
278 529
(...skipping 16 matching lines...) Expand all
295 """ 546 """
296 # pylint: disable=I0011,R0903,R0902 547 # pylint: disable=I0011,R0903,R0902
297 548
298 def __init__(self, config, message, set_branch_name): 549 def __init__(self, config, message, set_branch_name):
299 self._message = message 550 self._message = message
300 self._file_list = [] 551 self._file_list = []
301 self._branch_name = set_branch_name 552 self._branch_name = set_branch_name
302 self._stash = None 553 self._stash = None
303 self._original_branch = None 554 self._original_branch = None
304 self._config = config 555 self._config = config
305 self._svn_info = None
306 self.issue = None 556 self.issue = None
307 557
308 def stage_for_commit(self, *paths): 558 def stage_for_commit(self, *paths):
309 """Calls `git add ...` on each argument. 559 """Calls `git add ...` on each argument.
310 560
311 Args: 561 Args:
312 *paths: (list of strings) list of filenames to pass to `git add`. 562 *paths: (list of strings) list of filenames to pass to `git add`.
313 """ 563 """
314 self._file_list.extend(paths) 564 self._file_list.extend(paths)
315 565
316 def __enter__(self): 566 def __enter__(self):
317 git = self._config.git 567 git = self._config.git
568 vsp = self._config.vsp
318 def branch_exists(branch): 569 def branch_exists(branch):
319 """Return true iff branch exists.""" 570 """Return true iff branch exists."""
320 return 0 == subprocess.call( 571 return 0 == vsp.call([git, 'show-ref', '--quiet', branch])
321 [git, 'show-ref', '--quiet', branch])
322 def has_diff(): 572 def has_diff():
323 """Return true iff repository has uncommited changes.""" 573 """Return true iff repository has uncommited changes."""
324 return bool(subprocess.call([git, 'diff', '--quiet', 'HEAD'])) 574 return bool(vsp.call([git, 'diff', '--quiet', 'HEAD']))
575
325 self._stash = has_diff() 576 self._stash = has_diff()
326 if self._stash: 577 if self._stash:
327 check_call([git, 'stash', 'save']) 578 vsp.check_call([git, 'stash', 'save'])
328 try: 579 try:
329 self._original_branch = strip_output( 580 self._original_branch = vsp.strip_output(
330 [git, 'symbolic-ref', '--short', 'HEAD']) 581 [git, 'symbolic-ref', '--short', 'HEAD'])
331 except (subprocess.CalledProcessError,): 582 except (subprocess.CalledProcessError,):
332 self._original_branch = strip_output( 583 self._original_branch = vsp.strip_output(
333 [git, 'rev-parse', 'HEAD']) 584 [git, 'rev-parse', 'HEAD'])
334 585
335 if not self._branch_name: 586 if not self._branch_name:
336 self._branch_name = self._config.default_branch_name 587 self._branch_name = self._config.default_branch_name
337 588
338 if branch_exists(self._branch_name): 589 if branch_exists(self._branch_name):
339 check_call([git, 'checkout', '-q', 'master']) 590 vsp.check_call([git, 'checkout', '-q', 'master'])
340 check_call([git, 'branch', '-q', '-D', self._branch_name]) 591 vsp.check_call([git, 'branch', '-q', '-D', self._branch_name])
341 592
342 check_call( 593 vsp.check_call(
343 [git, 'checkout', '-q', '-b', 594 [git, 'checkout', '-q', '-b', self._branch_name, 'origin/master'])
344 self._branch_name, 'origin/master'])
345
346 svn_info = subprocess.check_output(['git', 'svn', 'info'])
347 svn_info_search = re.search(r'Last Changed Rev: ([0-9]+)\W', svn_info)
348 assert svn_info_search
349 self._svn_info = svn_info_search.group(1)
350 595
351 def __exit__(self, etype, value, traceback): 596 def __exit__(self, etype, value, traceback):
352 # pylint: disable=I0011,R0912 597 # pylint: disable=I0011,R0912
353 git = self._config.git 598 git = self._config.git
354 def quiet_check_call(*args, **kwargs): 599 vsp = self._config.vsp
355 """Call check_call, but pipe output to devnull.""" 600 svn_info = str(get_svn_revision(self._config, 'HEAD'))
356 with open(os.devnull, 'w') as devnull:
357 check_call(*args, stdout=devnull, **kwargs)
358 601
359 for filename in self._file_list: 602 for filename in self._file_list:
360 assert os.path.exists(filename) 603 assert os.path.exists(filename)
361 check_call([git, 'add', filename]) 604 vsp.check_call([git, 'add', filename])
362 check_call([git, 'commit', '-q', '-m', self._message]) 605 vsp.check_call([git, 'commit', '-q', '-m', self._message])
363 606
364 git_cl = [git, 'cl', 'upload', '-f', '--cc=skia-team@google.com', 607 git_cl = [git, 'cl', 'upload', '-f', '--cc=skia-team@google.com',
365 '--bypass-hooks', '--bypass-watchlists'] 608 '--bypass-hooks', '--bypass-watchlists']
366 git_try = [git, 'cl', 'try', '--revision', self._svn_info] 609 git_try = [git, 'cl', 'try', '--revision', svn_info]
367 git_try.extend([arg for bot in self._config.cl_bot_list 610 git_try.extend([arg for bot in self._config.cl_bot_list
368 for arg in ('-b', bot)]) 611 for arg in ('-b', bot)])
369 612
370 if self._config.skip_cl_upload: 613 if self._config.skip_cl_upload:
371 print ' '.join(git_cl) 614 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)
619 if self._config.cl_bot_list:
620 VerboseSubprocess.print_subprocess_args(' ', git_try)
372 print 621 print
373 if self._config.cl_bot_list:
374 print ' '.join(git_try)
375 print
376 self.issue = '' 622 self.issue = ''
377 else: 623 else:
378 if self._config.verbose: 624 vsp.check_call(git_cl)
379 check_call(git_cl) 625 self.issue = vsp.strip_output([git, 'cl', 'issue'])
380 print
381 else:
382 quiet_check_call(git_cl)
383 self.issue = strip_output([git, 'cl', 'issue'])
384 if self._config.cl_bot_list: 626 if self._config.cl_bot_list:
385 if self._config.verbose: 627 vsp.check_call(git_try)
386 check_call(git_try)
387 print
388 else:
389 quiet_check_call(git_try)
390 628
391 # deal with the aftermath of failed executions of this script. 629 # deal with the aftermath of failed executions of this script.
392 if self._config.default_branch_name == self._original_branch: 630 if self._config.default_branch_name == self._original_branch:
393 self._original_branch = 'master' 631 self._original_branch = 'master'
394 check_call([git, 'checkout', '-q', self._original_branch]) 632 vsp.check_call([git, 'checkout', '-q', self._original_branch])
395 633
396 if self._config.default_branch_name == self._branch_name: 634 if self._config.default_branch_name == self._branch_name:
397 check_call([git, 'branch', '-q', '-D', self._branch_name]) 635 vsp.check_call([git, 'branch', '-q', '-D', self._branch_name])
398 if self._stash: 636 if self._stash:
399 check_call([git, 'stash', 'pop']) 637 vsp.check_call([git, 'stash', 'pop'])
400 638
401 639
402 def change_skia_deps(revision, git_hash, depspath): 640 def change_skia_deps(revision, git_hash, depspath):
403 """Update the DEPS file. 641 """Update the DEPS file.
404 642
405 Modify the skia_revision and skia_hash entries in the given DEPS file. 643 Modify the skia_revision and skia_hash entries in the given DEPS file.
406 644
407 Args: 645 Args:
408 revision: (int) Skia SVN revision. 646 revision: (int) Skia SVN revision.
409 git_hash: (string) Skia Git hash. 647 git_hash: (string) Skia Git hash.
(...skipping 11 matching lines...) Expand all
421 with open(depspath, 'r') as input_stream: 659 with open(depspath, 'r') as input_stream:
422 for line in input_stream: 660 for line in input_stream:
423 line = deps_regex_rev.sub(deps_regex_rev_repl, line) 661 line = deps_regex_rev.sub(deps_regex_rev_repl, line)
424 line = deps_regex_hash.sub(deps_regex_hash_repl, line) 662 line = deps_regex_hash.sub(deps_regex_hash_repl, line)
425 temp_file.write(line) 663 temp_file.write(line)
426 finally: 664 finally:
427 temp_file.close() 665 temp_file.close()
428 shutil.move(temp_file.name, depspath) 666 shutil.move(temp_file.name, depspath)
429 667
430 668
431 def branch_name(message):
432 """Return the first line of a commit message to be used as a branch name.
433
434 Args:
435 message: (string)
436
437 Returns:
438 A string derived from message suitable for a branch name.
439 """
440 return message.lstrip().split('\n')[0].rstrip().replace(' ', '_')
441
442
443 def roll_deps(config, revision, git_hash): 669 def roll_deps(config, revision, git_hash):
444 """Upload changed DEPS and a whitespace change. 670 """Upload changed DEPS and a whitespace change.
445 671
446 Given the correct git_hash, create two Reitveld issues. 672 Given the correct git_hash, create two Reitveld issues.
447 673
448 Args: 674 Args:
449 config: (roll_deps.DepsRollConfig) object containing options. 675 config: (roll_deps.DepsRollConfig) object containing options.
450 revision: (int) Skia SVN revision. 676 revision: (int) Skia SVN revision.
451 git_hash: (string) Skia Git hash. 677 git_hash: (string) Skia Git hash.
452 678
453 Returns: 679 Returns:
454 a tuple containing textual description of the two issues. 680 a tuple containing textual description of the two issues.
455 681
456 Raises: 682 Raises:
457 OSError: failed to execute git or git-cl. 683 OSError: failed to execute git or git-cl.
458 subprocess.CalledProcessError: git returned unexpected status. 684 subprocess.CalledProcessError: git returned unexpected status.
459 """ 685 """
686
460 git = config.git 687 git = config.git
461 cwd = os.getcwd() 688 with ChangeDir(config.chromium_path, config.verbose):
462 os.chdir(config.chromium_path) 689 config.vsp.check_call([git, 'fetch', '-q', 'origin'])
463 try: 690
464 check_call([git, 'fetch', '-q', 'origin']) 691 old_revision = ReSearch.search_within_output(
465 master_hash = strip_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(
466 [git, 'show-ref', 'origin/master', '--hash']) 700 [git, 'show-ref', 'origin/master', '--hash'])
701 master_revision = get_svn_revision(config, 'origin/master')
702
703 branch = None
467 704
468 # master_hash[8] gives each whitespace CL a unique name. 705 # master_hash[8] gives each whitespace CL a unique name.
469 message = ('whitespace change %s\n\nThis CL was created by' 706 message = ('whitespace change %s\n\n'
470 ' Skia\'s roll_deps.py script.\n') % master_hash[:8] 707 'Chromium base revision: %d / %s\n\n'
471 branch = branch_name(message) if config.save_branches else None 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]
472 712
473 codereview = GitBranchCLUpload(config, message, branch) 713 codereview = GitBranchCLUpload(config, message, branch)
474 with codereview: 714 with codereview:
475 with open('build/whitespace_file.txt', 'a') as output_stream: 715 with open('build/whitespace_file.txt', 'a') as output_stream:
476 output_stream.write('\nCONTROL\n') 716 output_stream.write('\nCONTROL\n')
477 codereview.stage_for_commit('build/whitespace_file.txt') 717 codereview.stage_for_commit('build/whitespace_file.txt')
478 whitespace_cl = codereview.issue 718 whitespace_cl = codereview.issue
479 if branch: 719 if branch:
480 whitespace_cl = '%s\n branch: %s' % (whitespace_cl, branch) 720 whitespace_cl = '%s\n branch: %s' % (whitespace_cl, branch)
481 control_url_match = re.search('https?://[^) ]+', codereview.issue) 721
482 if control_url_match: 722 control_url = ReSearch.search_within_string(
483 message = ('roll skia DEPS to %d\n\nThis CL was created by' 723 codereview.issue, '(?P<return>https?://[^) ]+)', '?')
484 ' Skia\'s roll_deps.py script.\n\ncontrol: %s' 724
485 % (revision, control_url_match.group(0))) 725 if config.save_branches:
486 else: 726 branch = 'roll_%d_%s' % (revision, master_hash[:8])
487 message = ('roll skia DEPS to %d\n\nThis CL was created by' 727 message = (
488 ' Skia\'s roll_deps.py script.') % revision 728 'roll skia DEPS to %d\n\n'
489 branch = branch_name(message) if config.save_branches else None 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))
490 codereview = GitBranchCLUpload(config, message, branch) 736 codereview = GitBranchCLUpload(config, message, branch)
491 with codereview: 737 with codereview:
492 change_skia_deps(revision, git_hash, 'DEPS') 738 change_skia_deps(revision, git_hash, 'DEPS')
493 codereview.stage_for_commit('DEPS') 739 codereview.stage_for_commit('DEPS')
494 deps_cl = codereview.issue 740 deps_cl = codereview.issue
495 if branch: 741 if branch:
496 deps_cl = '%s\n branch: %s' % (deps_cl, branch) 742 deps_cl = '%s\n branch: %s' % (deps_cl, branch)
497 743
498 return deps_cl, whitespace_cl 744 return deps_cl, whitespace_cl
499 finally:
500 os.chdir(cwd)
501 745
502 746
503 def find_hash_and_roll_deps(config, revision): 747 def find_hash_and_roll_deps(config, revision=None, partial_hash=None):
504 """Call find_hash_from_revision() and roll_deps(). 748 """Call find_hash_from_revision() and roll_deps().
505 749
506 The calls to git will be verbose on standard output. After a 750 The calls to git will be verbose on standard output. After a
507 successful upload of both issues, print links to the new 751 successful upload of both issues, print links to the new
508 codereview issues. 752 codereview issues.
509 753
510 Args: 754 Args:
511 config: (roll_deps.DepsRollConfig) object containing options. 755 config: (roll_deps.DepsRollConfig) object containing options.
512 revision: (int or None) the Skia SVN revision number or None 756 revision: (int or None) the Skia SVN revision number or None
513 to use the tip of the tree. 757 to use the tip of the tree.
758 partial_hash: (string or None) a partial pure-git Skia commit
759 hash. Don't pass both partial_hash and revision.
514 760
515 Raises: 761 Raises:
516 roll_deps.DepsRollError: if the revision can't be found. 762 roll_deps.DepsRollError: if the revision can't be found.
517 OSError: failed to execute git or git-cl. 763 OSError: failed to execute git or git-cl.
518 subprocess.CalledProcessError: git returned unexpected status. 764 subprocess.CalledProcessError: git returned unexpected status.
519 """ 765 """
520 revision, git_hash = find_revision_and_hash(config, revision) 766
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)
521 777
522 print 'revision=%r\nhash=%r\n' % (revision, git_hash) 778 print 'revision=%r\nhash=%r\n' % (revision, git_hash)
523 779
524 deps_issue, whitespace_issue = roll_deps(config, revision, git_hash) 780 roll = roll_deps(config, revision, git_hash)
525 781
526 print 'DEPS roll:\n %s\n' % deps_issue 782 if roll:
527 print 'Whitespace change:\n %s\n' % whitespace_issue 783 deps_issue, whitespace_issue = roll
784 print 'DEPS roll:\n %s\n' % deps_issue
785 print 'Whitespace change:\n %s\n' % whitespace_issue
528 786
529 787
530 def main(args): 788 def main(args):
531 """main function; see module-level docstring and GetOptionParser help. 789 """main function; see module-level docstring and GetOptionParser help.
532 790
533 Args: 791 Args:
534 args: sys.argv[1:]-type argument list. 792 args: sys.argv[1:]-type argument list.
535 """ 793 """
536 option_parser = DepsRollConfig.GetOptionParser() 794 option_parser = DepsRollConfig.GetOptionParser()
537 options = option_parser.parse_args(args)[0] 795 options = option_parser.parse_args(args)[0]
538 796
539 if not options.chromium_path: 797 if not options.chromium_path:
540 option_parser.error('Must specify chromium_path.') 798 option_parser.error('Must specify chromium_path.')
541 if not os.path.isdir(options.chromium_path): 799 if not os.path.isdir(options.chromium_path):
542 option_parser.error('chromium_path must be a directory.') 800 option_parser.error('chromium_path must be a directory.')
543 if not test_git_executable(options.git_path): 801 if not test_git_executable(options.git_path):
544 option_parser.error('Invalid git executable.') 802 option_parser.error('Invalid git executable.')
545 803
546 config = DepsRollConfig(options) 804 config = DepsRollConfig(options)
547 find_hash_and_roll_deps(config, options.revision) 805 find_hash_and_roll_deps(config, options.revision, options.git_hash)
548 806
549 807
550 if __name__ == '__main__': 808 if __name__ == '__main__':
551 main(sys.argv[1:]) 809 main(sys.argv[1:])
552 810
OLDNEW
« no previous file with comments | « no previous file | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698