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

Side by Side Diff: tools/roll_deps.py

Issue 123523003: DEPS roll script (Closed) Base URL: https://skia.googlecode.com/svn/trunk
Patch Set: changes 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
(Empty)
1 #!/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
10 This 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
19 Usage:
20 %prog -c CHROMIUM_PATH -r REVISION [OPTIONAL_OPTIONS]
21 """
22
23
24 import optparse
25 import os
26 import re
27 import shutil
28 import subprocess
29 from subprocess import check_call
30 import sys
31 import tempfile
32
33
34 class Config(object):
borenet 2014/01/06 19:04:28 This could be DepsRollConfig or something more des
hal.canary 2014/01/06 19:29:29 Done.
35 """Contains configuration options for this module.
36
37 Attributes:
38 git: (string) The git executable.
39 chromium_path: (string) path to a local chromium git repository.
40 save_branches: (boolean) iff false, delete temporary branches.
41 verbose: (boolean) iff false, suppress the output from git-cl.
42 search_depth: (int) how far back to look for the revision.
43 skia_url: (string) Skia's git repository.
44 self.skip_cl_upload: (boolean)
45 self.cl_bot_list: (list of strings)
46 """
47
48 # pylint: disable=I0011,R0903,R0902
49 def __init__(self, options=None):
50 self.skia_url = 'https://skia.googlesource.com/skia.git'
51 self.revision_format = (
52 'git-svn-id: http://skia.googlecode.com/svn/trunk@%d ')
53
54 if not options:
55 options = Config.GetOptionParser()
56 # pylint: disable=I0011,E1103
57 self.verbose = options.verbose
58 self.save_branches = options.save_branches
59 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',
borenet 2014/01/06 19:04:28 I dislike hard-coded lists like this, but I don't
100 ]
101
102 option_parser = optparse.OptionParser(usage=__doc__)
103 # Anyone using this script on a regular basis should set the
104 # CHROMIUM_REPO_PATH environment variable.
105 option_parser.add_option(
106 '-c', '--chromium_path', help='Path to Chromium Git repository,'
107 ' defaults to CHROMIUM_REPO_PATH if that environment variable'
108 ' is set.',
109 default=os.environ.get('CHROMIUM_REPO_PATH'))
110 option_parser.add_option(
111 '-r', '--revision', type='int', default=-1,
112 help='The Skia SVN revision number, defaults to top of tree.')
113 # Anyone using this script on a regular basis should set the
114 # SKIA_GIT_REPO_PATH environment variable.
borenet 2014/01/06 19:04:28 For clarity, I think this should be "CHECKOUT" ins
hal.canary 2014/01/06 19:29:29 Done.
115 option_parser.add_option(
116 '', '--skia_git_path',
117 help='Path of a pure-git Skia repo. If empty, a temporary will'
118 ' be cloned. Defaults to SKIA_GIT_REPO_PATH, if that '
119 'environment variable is set.',
120 default=os.environ.get('SKIA_GIT_REPO_PATH'))
121 option_parser.add_option(
122 '', '--search_depth', help='How far back to look for the revision',
123 type='int', default=100)
124 option_parser.add_option(
125 '', '--git_path', help='Git executable, defaults to "git".',
126 default='git')
127 option_parser.add_option(
128 '', '--save_branches', help='Save the temporary branches',
129 action='store_true', dest='save_branches', default=False)
130 option_parser.add_option(
131 '', '--verbose', help='Do not suppress the output from `git cl`.',
132 action='store_true', dest='verbose', default=False)
133 option_parser.add_option(
134 '', '--skip_cl_upload', help='Skip the cl upload step; useful'
135 ' for testing or with --save_branches.',
136 action='store_true', default=False)
137
138 default_bots_help = (
139 'Comma-separated list of bots, defaults to a list of %d bots.'
140 ' To skip `git cl try`, set this to an empty string.'
141 % len(default_bots_list))
142 default_bots = ','.join(default_bots_list)
143 option_parser.add_option(
144 '', '--bots', help=default_bots_help, default=default_bots)
145
146 return option_parser
147
148
149 def test_git_executable(git_executable):
150 """Test the git executable.
151
152 Args:
153 git_executable: git executable path.
154 Returns:
155 True if test is successful.
156 """
157 with open(os.devnull, 'w') as devnull:
158 try:
159 subprocess.call([git_executable, '--version'], stdout=devnull)
160 except (OSError,):
161 return False
162 return True
163
164
165 class Error(Exception):
borenet 2014/01/06 19:04:28 Please either name this something more specific or
hal.canary 2014/01/06 19:29:29 Done.
166 """Exceptions specific to this module."""
167 pass
168
169
170 def strip_output(*args, **kwargs):
171 """Wrap subprocess.check_output and str.strip().
172
173 Pass the given arguments into subprocess.check_output() and return
174 the results, after stripping any excess whitespace.
175
176 Args:
177 *args: to be passed to subprocess.check_output()
178 **kwargs: to be passed to subprocess.check_output()
179
180 Returns:
181 The output of the process as a string without leading or
182 trailing whitespace.
183 Raises:
184 OSError or subprocess.CalledProcessError: raised by check_output.
185 """
186 return str(subprocess.check_output(*args, **kwargs)).strip()
187
188
189 def create_temp_skia_clone(config, depth):
190 """Clones Skia in a temp dir.
191
192 Args:
193 config: (roll_deps.Config) 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.Config) object containing options.
217 revision: (int) SVN revision number. If -1, use tip-of-tree
borenet 2014/01/06 19:04:28 Why not allow the default to be None?
hal.canary 2014/01/06 19:29:29 Done.
218
219 Returns:
220 A tuple (revision, hash)
221 revision: (int) SVN revision number.
222 hash: (string) full Git commit hash.
223
224 Raises:
225 roll_deps.Error: if the revision can't be found.
226 OSError: failed to execute git or git-cl.
227 subprocess.CalledProcessError: git returned unexpected status.
228 """
229 git = config.git
230 use_temp = False
231 skia_dir = None
232 depth = 1 if (-1 == revision) else config.search_depth
233 try:
234 if config.skia_git_checkout_path:
235 skia_dir = config.skia_git_checkout_path
236 ## Update origin/master if needed.
237 check_call([git, 'fetch', '-q', 'origin'], cwd=skia_dir)
238 else:
239 skia_dir = create_temp_skia_clone(config, depth)
240 assert skia_dir
241 use_temp = True
242
243 if -1 == revision:
borenet 2014/01/06 19:04:28 Again, I'd really rather use None as a magic value
hal.canary 2014/01/06 19:29:29 Done.
244 message = subprocess.check_output(
245 [git, 'log', '-n', '1', '--format=format:%B', 'origin/master'],
246 cwd=skia_dir)
247 svn_format = (
248 'git-svn-id: http://skia.googlecode.com/svn/trunk@([0-9]+) ')
249 search = re.search(svn_format, message)
250 if not search:
251 raise Error('Revision number missing from origin/master.')
252 revision = int(search.group(1))
253 git_hash = strip_output(
254 [git, 'show-ref', 'origin/master', '--hash'], cwd=skia_dir)
255 else:
256 revision_regex = config.revision_format % revision
257 git_hash = strip_output(
258 [git, 'log', '--grep', revision_regex, '--format=format:%H',
259 'origin/master'], cwd=skia_dir)
260
261 if revision < 0 or not git_hash:
262 raise Error('Git hash can not be found.')
263 return revision, git_hash
264 finally:
265 if use_temp:
266 shutil.rmtree(skia_dir)
267
268
269 class GitBranchCLUpload(object):
270 """Class to manage git branches and git-cl-upload.
271
272 This class allows one to create a new branch in a repository based
273 off of origin/master, make changes to the tree inside the
274 with-block, upload that new branch to Rietveld, restore the original
275 tree state, and delete the local copy of the new branch.
276
277 See roll_deps() for an example of use.
278
279 Constructor Args:
280 config: (roll_deps.Config) object containing options.
281 message: (string) the commit message, can be multiline.
282 set_brach_name: (string or none) if not None, the name of the
283 branch to use. If None, then use a temporary branch that
284 will be deleted.
285
286 Attributes:
287 issue: a string describing the codereview issue, after __exit__
288 has been called, othrwise, None.
289
290 Raises:
291 OSError: failed to execute git or git-cl.
292 subprocess.CalledProcessError: git returned unexpected status.
293 """
294 # pylint: disable=I0011,R0903,R0902
295
296 def __init__(self, config, message, set_branch_name):
297 self._message = message
298 self._file_list = []
299 self._branch_name = set_branch_name
300 self._stash = None
301 self._original_branch = None
302 self._config = config
303 self._svn_info = None
304 self.issue = None
305
306 def stage_for_commit(self, *paths):
307 """Calls `git add ...` on each argument.
308
309 Args:
310 *paths: (list of strings) list of filenames to pass to `git add`.
311 """
312 self._file_list.extend(paths)
313
314 def __enter__(self):
315 git = self._config.git
316 def branch_exists(branch):
317 """Return true iff branch exists."""
318 return 0 == subprocess.call(
319 [git, 'show-ref', '--quiet', branch])
320 def has_diff():
321 """Return true iff repository has uncommited changes."""
322 return bool(subprocess.call([git, 'diff', '--quiet', 'HEAD']))
323 self._stash = has_diff()
324 if self._stash:
325 check_call([git, 'stash', 'save'])
326 try:
327 self._original_branch = strip_output(
328 [git, 'symbolic-ref', '--short', 'HEAD'])
329 except (subprocess.CalledProcessError,):
330 self._original_branch = strip_output(
331 [git, 'rev-parse', 'HEAD'])
332
333 if not self._branch_name:
334 self._branch_name = self._config.default_branch_name
335
336 if branch_exists(self._branch_name):
337 check_call([git, 'checkout', '-q', 'master'])
338 check_call([git, 'branch', '-q', '-D', self._branch_name])
339
340 check_call(
341 [git, 'checkout', '-q', '-b',
342 self._branch_name, 'origin/master'])
343
344 svn_info = subprocess.check_output(['git', 'svn', 'info'])
345 svn_info_search = re.search(r'Last Changed Rev: ([0-9]+)\W', svn_info)
346 assert svn_info_search
347 self._svn_info = svn_info_search.group(1)
348
349 def __exit__(self, etype, value, traceback):
350 # pylint: disable=I0011,R0912
351 git = self._config.git
352 def quiet_check_call(*args, **kwargs):
353 """Call check_call, but pipe output to devnull."""
354 with open(os.devnull, 'w') as devnull:
355 check_call(*args, stdout=devnull, **kwargs)
356
357 for filename in self._file_list:
358 assert os.path.exists(filename)
359 check_call([git, 'add', filename])
360 check_call([git, 'commit', '-q', '-m', self._message])
361
362 git_cl = [git, 'cl', 'upload', '-f', '--cc=skia-team@google.com',
363 '--bypass-hooks', '--bypass-watchlists']
364 git_try = [git, 'cl', 'try', '--revision', self._svn_info]
365 git_try.extend([arg for bot in self._config.cl_bot_list
366 for arg in ('-b', bot)])
367
368 if self._config.skip_cl_upload:
369 print ' '.join(git_cl)
370 print
371 if self._config.cl_bot_list:
372 print ' '.join(git_try)
373 print
374 self.issue = ''
375 else:
376 if self._config.verbose:
377 check_call(git_cl)
378 print
379 else:
380 quiet_check_call(git_cl)
381 self.issue = strip_output([git, 'cl', 'issue'])
382 if self._config.cl_bot_list:
383 if self._config.verbose:
384 check_call(git_try)
385 print
386 else:
387 quiet_check_call(git_try)
388
389 # deal with the aftermath of failed executions of this script.
390 if self._config.default_branch_name == self._original_branch:
391 self._original_branch = 'master'
392 check_call([git, 'checkout', '-q', self._original_branch])
393
394 if self._config.default_branch_name == self._branch_name:
395 check_call([git, 'branch', '-q', '-D', self._branch_name])
396 if self._stash:
397 check_call([git, 'stash', 'pop'])
398
399
400 def change_skia_deps(revision, git_hash, depspath):
401 """Update the DEPS file.
402
403 Modify the skia_revision and skia_hash entries in the given DEPS file.
404
405 Args:
406 revision: (int) Skia SVN revision.
407 git_hash: (string) Skia Git hash.
408 depspath: (string) path to DEPS file.
409 """
410 temp_file = tempfile.NamedTemporaryFile(delete=False,
411 prefix='skia_DEPS_ROLL_tmp_')
412 try:
413 deps_regex_rev = re.compile('"skia_revision": "[0-9]*",')
414 deps_regex_hash = re.compile('"skia_hash": "[0-9a-f]*",')
415
416 deps_regex_rev_repl = '"skia_revision": "%d",' % revision
417 deps_regex_hash_repl = '"skia_hash": "%s",' % git_hash
418
419 with open(depspath, 'r') as input_stream:
420 for line in input_stream:
421 line = deps_regex_rev.sub(deps_regex_rev_repl, line)
422 line = deps_regex_hash.sub(deps_regex_hash_repl, line)
423 temp_file.write(line)
424 finally:
425 temp_file.close()
426 shutil.move(temp_file.name, depspath)
427
428
429 def branch_name(message):
430 """Return the first line of a commit message to be used as a branch name.
431
432 Args:
433 message: (string)
434
435 Returns:
436 A string derived from message suitable for a branch name.
437 """
438 return message.lstrip().split('\n')[0].rstrip().replace(' ', '_')
439
440
441 def roll_deps(config, revision, git_hash):
442 """Upload changed DEPS and a whitespace change.
443
444 Given the correct git_hash, create two Reitveld issues.
445
446 Args:
447 config: (roll_deps.Config) object containing options.
448 revision: (int) Skia SVN revision.
449 git_hash: (string) Skia Git hash.
450
451 Returns:
452 a tuple containing textual description of the two issues.
453
454 Raises:
455 OSError: failed to execute git or git-cl.
456 subprocess.CalledProcessError: git returned unexpected status.
457 """
458 git = config.git
459 cwd = os.getcwd()
460 os.chdir(config.chromium_path)
461 try:
462 check_call([git, 'fetch', '-q', 'origin'])
463 master_hash = strip_output(
464 [git, 'show-ref', 'origin/master', '--hash'])
465
466 # master_hash[8] gives each whitespace CL a unique name.
467 message = ('whitespace change %s\n\nThis CL was created by'
468 ' Skia\'s roll_deps.py script.\n') % master_hash[:8]
469 branch = branch_name(message) if config.save_branches else None
470
471 codereview = GitBranchCLUpload(config, message, branch)
472 with codereview:
473 with open('build/whitespace_file.txt', 'a') as output_stream:
474 output_stream.write('\nCONTROL\n')
475 codereview.stage_for_commit('build/whitespace_file.txt')
476 whitespace_cl = codereview.issue
477 if branch:
478 whitespace_cl = '%s\n branch: %s' % (whitespace_cl, branch)
479 control_url_match = re.search('https?://[^) ]+', codereview.issue)
480 if control_url_match:
481 message = ('roll skia DEPS to %d\n\nThis CL was created by'
482 ' Skia\'s roll_deps.py script.\n\n'
483 'control: %s') % (revision, control_url_match.group(0))
484 else:
485 message = ('roll skia DEPS to %d\n\nThis CL was created by'
486 ' Skia\'s roll_deps.py script.') % revision
487 branch = branch_name(message) if config.save_branches else None
488 codereview = GitBranchCLUpload(config, message, branch)
489 with codereview:
490 change_skia_deps(revision, git_hash, 'DEPS')
491 codereview.stage_for_commit('DEPS')
492 deps_cl = codereview.issue
493 if branch:
494 deps_cl = '%s\n branch: %s' % (deps_cl, branch)
495
496 return deps_cl, whitespace_cl
497 finally:
498 os.chdir(cwd)
499
500
501 def find_hash_and_roll_deps(config, revision):
502 """Call find_hash_from_revision() and roll_deps().
503
504 The calls to git will be verbose on standard output. After a
505 successful upload of both issues, print links to the new
506 codereview issues.
507
508 Args:
509 config: (roll_deps.Config) object containing options.
510 revision: (int) the Skia SVN revision number or -1.
511
512 Raises:
513 roll_deps.Error: if the revision can't be found.
514 OSError: failed to execute git or git-cl.
515 subprocess.CalledProcessError: git returned unexpected status.
516 """
517 revision, git_hash = find_revision_and_hash(config, revision)
518
519 print 'revision=%r\nhash=%r\n' % (revision, git_hash)
520
521 deps_issue, whitespace_issue = roll_deps(config, revision, git_hash)
522
523 print 'DEPS roll:\n %s\n' % deps_issue
524 print 'Whitespace change:\n %s\n' % whitespace_issue
525
526
527 def main(args):
528 """main function; see module-level docstring and GetOptionParser help.
529
530 Args:
531 args: sys.argv[1:]-type argument list.
532 """
533 option_parser = Config.GetOptionParser()
534 options = option_parser.parse_args(args)[0]
535
536 if not options.chromium_path:
537 option_parser.error('Must specify chromium_path.')
538 if not os.path.isdir(options.chromium_path):
539 option_parser.error('chromium_path must be a directory.')
540 if not test_git_executable(options.git_path):
541 option_parser.error('Invalid git executable.')
542
543 config = Config(options)
544 find_hash_and_roll_deps(config, options.revision)
545
546
547 if __name__ == '__main__':
548 main(sys.argv[1:])
549
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