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

Side by Side Diff: trychange.py

Issue 2269413002: Delete gcl, drover, and trychange (Closed) Base URL: https://chromium.googlesource.com/chromium/tools/depot_tools.git@master
Patch Set: Comments Created 4 years, 4 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
« no previous file with comments | « tests/trychange_unittest.py ('k') | 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/env python
2 # Copyright (c) 2012 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file.
5
6 """Client-side script to send a try job to the try server. It communicates to
7 the try server by either writting to a svn/git repository or by directly
8 connecting to the server by HTTP.
9 """
10
11 import contextlib
12 import datetime
13 import errno
14 import getpass
15 import itertools
16 import json
17 import logging
18 import optparse
19 import os
20 import posixpath
21 import re
22 import shutil
23 import sys
24 import tempfile
25 import urllib
26 import urllib2
27 import urlparse
28
29 import fix_encoding
30 import gcl
31 import gclient_utils
32 import gerrit_util
33 import scm
34 import subprocess2
35
36
37 __version__ = '1.2'
38
39
40 # Constants
41 HELP_STRING = "Sorry, Tryserver is not available."
42 USAGE = r"""%prog [options]
43
44 Client-side script to send a try job to the try server. It communicates to
45 the try server by either writting to a svn repository or by directly connecting
46 to the server by HTTP."""
47
48 EPILOG = """
49 Examples:
50 Send a patch directly from rietveld:
51 %(prog)s -R codereview.chromium.org/1337
52 --email recipient@example.com --root src
53
54 Try a change against a particular revision:
55 %(prog)s -r 123
56
57 Try a change including changes to a sub repository:
58 %(prog)s -s third_party/WebKit
59
60 A git patch off a web site (git inserts a/ and b/) and fix the base dir:
61 %(prog)s --url http://url/to/patch.diff --patchlevel 1 --root src
62
63 Use svn to store the try job, specify an alternate email address and use a
64 premade diff file on the local drive:
65 %(prog)s --email user@example.com
66 --svn_repo svn://svn.chromium.org/chrome-try/try --diff foo.diff
67
68 Running only on a 'mac' slave with revision 123 and clobber first; specify
69 manually the 3 source files to use for the try job:
70 %(prog)s --bot mac --revision 123 --clobber -f src/a.cc -f src/a.h
71 -f include/b.h
72 """
73
74 GIT_PATCH_DIR_BASENAME = os.path.join('git-try', 'patches-git')
75 GIT_BRANCH_FILE = 'ref'
76 _GIT_PUSH_ATTEMPTS = 3
77
78 def DieWithError(message):
79 print >> sys.stderr, message
80 sys.exit(1)
81
82
83 def RunCommand(args, error_ok=False, error_message=None, **kwargs):
84 try:
85 return subprocess2.check_output(args, shell=False, **kwargs)
86 except subprocess2.CalledProcessError, e:
87 if not error_ok:
88 DieWithError(
89 'Command "%s" failed.\n%s' % (
90 ' '.join(args), error_message or e.stdout or ''))
91 return e.stdout
92
93
94 def RunGit(args, **kwargs):
95 """Returns stdout."""
96 return RunCommand(['git'] + args, **kwargs)
97
98 class Error(Exception):
99 """An error during a try job submission.
100
101 For this error, trychange.py does not display stack trace, only message
102 """
103
104 class InvalidScript(Error):
105 def __str__(self):
106 return self.args[0] + '\n' + HELP_STRING
107
108
109 class NoTryServerAccess(Error):
110 def __str__(self):
111 return self.args[0] + '\n' + HELP_STRING
112
113 def Escape(name):
114 """Escapes characters that could interfere with the file system or try job
115 parsing.
116 """
117 return re.sub(r'[^\w#-]', '_', name)
118
119
120 class SCM(object):
121 """Simplistic base class to implement one function: ProcessOptions."""
122 def __init__(self, options, path, file_list):
123 items = path.split('@')
124 assert len(items) <= 2
125 self.checkout_root = os.path.abspath(items[0])
126 items.append(None)
127 self.diff_against = items[1]
128 self.options = options
129 # Lazy-load file list from the SCM unless files were specified in options.
130 self._files = None
131 self._file_tuples = None
132 if file_list:
133 self._files = file_list
134 self._file_tuples = [('M', f) for f in self.files]
135 self.options.files = None
136 self.codereview_settings = None
137 self.codereview_settings_file = 'codereview.settings'
138 self.toplevel_root = None
139
140 def GetFileNames(self):
141 """Return the list of files in the diff."""
142 return self.files
143
144 def GetCodeReviewSetting(self, key):
145 """Returns a value for the given key for this repository.
146
147 Uses gcl-style settings from the repository.
148 """
149 if gcl:
150 gcl_setting = gcl.GetCodeReviewSetting(key)
151 if gcl_setting != '':
152 return gcl_setting
153 if self.codereview_settings is None:
154 self.codereview_settings = {}
155 settings_file = self.ReadRootFile(self.codereview_settings_file)
156 if settings_file:
157 for line in settings_file.splitlines():
158 if not line or line.lstrip().startswith('#'):
159 continue
160 k, v = line.split(":", 1)
161 self.codereview_settings[k.strip()] = v.strip()
162 return self.codereview_settings.get(key, '')
163
164 def _GclStyleSettings(self):
165 """Set default settings based on the gcl-style settings from the repository.
166
167 The settings in the self.options object will only be set if no previous
168 value exists (i.e. command line flags to the try command will override the
169 settings in codereview.settings).
170 """
171 settings = {
172 'port': self.GetCodeReviewSetting('TRYSERVER_HTTP_PORT'),
173 'host': self.GetCodeReviewSetting('TRYSERVER_HTTP_HOST'),
174 'svn_repo': self.GetCodeReviewSetting('TRYSERVER_SVN_URL'),
175 'gerrit_url': self.GetCodeReviewSetting('TRYSERVER_GERRIT_URL'),
176 'git_repo': self.GetCodeReviewSetting('TRYSERVER_GIT_URL'),
177 'project': self.GetCodeReviewSetting('TRYSERVER_PROJECT'),
178 # Primarily for revision=auto
179 'revision': self.GetCodeReviewSetting('TRYSERVER_REVISION'),
180 'root': self.GetCodeReviewSetting('TRYSERVER_ROOT'),
181 'patchlevel': self.GetCodeReviewSetting('TRYSERVER_PATCHLEVEL'),
182 }
183 logging.info('\n'.join(['%s: %s' % (k, v)
184 for (k, v) in settings.iteritems() if v]))
185 for (k, v) in settings.iteritems():
186 # Avoid overwriting options already set using command line flags.
187 if v and getattr(self.options, k) is None:
188 setattr(self.options, k, v)
189
190 def AutomagicalSettings(self):
191 """Determines settings based on supported code review and checkout tools.
192 """
193 # Try to find gclient or repo root first.
194 if not self.options.no_search:
195 self.toplevel_root = gclient_utils.FindGclientRoot(self.checkout_root)
196 if self.toplevel_root:
197 logging.info('Found .gclient at %s' % self.toplevel_root)
198 else:
199 self.toplevel_root = gclient_utils.FindFileUpwards(
200 os.path.join('..', '.repo'), self.checkout_root)
201 if self.toplevel_root:
202 logging.info('Found .repo dir at %s'
203 % os.path.dirname(self.toplevel_root))
204
205 # Parse TRYSERVER_* settings from codereview.settings before falling back
206 # on setting self.options.root manually further down. Otherwise
207 # TRYSERVER_ROOT would never be used in codereview.settings.
208 self._GclStyleSettings()
209
210 if self.toplevel_root and not self.options.root:
211 assert os.path.abspath(self.toplevel_root) == self.toplevel_root
212 self.options.root = gclient_utils.PathDifference(self.toplevel_root,
213 self.checkout_root)
214 else:
215 self._GclStyleSettings()
216
217 def ReadRootFile(self, filename):
218 cur = self.checkout_root
219 root = self.toplevel_root or self.checkout_root
220
221 assert cur.startswith(root), (root, cur)
222 while cur.startswith(root):
223 filepath = os.path.join(cur, filename)
224 if os.path.isfile(filepath):
225 logging.info('Found %s at %s' % (filename, cur))
226 return gclient_utils.FileRead(filepath)
227 cur = os.path.dirname(cur)
228 logging.warning('Didn\'t find %s' % filename)
229 return None
230
231 def _SetFileTuples(self, file_tuples):
232 excluded = ['!', '?', 'X', ' ', '~']
233 def Excluded(f):
234 if f[0][0] in excluded:
235 return True
236 for r in self.options.exclude:
237 if re.search(r, f[1]):
238 logging.info('Ignoring "%s"' % f[1])
239 return True
240 return False
241
242 self._file_tuples = [f for f in file_tuples if not Excluded(f)]
243 self._files = [f[1] for f in self._file_tuples]
244
245 def CaptureStatus(self):
246 """Returns the 'svn status' emulated output as an array of (status, file)
247 tuples."""
248 raise NotImplementedError(
249 "abstract method -- subclass %s must override" % self.__class__)
250
251 @property
252 def files(self):
253 if self._files is None:
254 self._SetFileTuples(self.CaptureStatus())
255 return self._files
256
257 @property
258 def file_tuples(self):
259 if self._file_tuples is None:
260 self._SetFileTuples(self.CaptureStatus())
261 return self._file_tuples
262
263
264 class SVN(SCM):
265 """Gathers the options and diff for a subversion checkout."""
266 def __init__(self, *args, **kwargs):
267 SCM.__init__(self, *args, **kwargs)
268 self.checkout_root = scm.SVN.GetCheckoutRoot(self.checkout_root)
269 if not self.options.email:
270 # Assumes the svn credential is an email address.
271 self.options.email = scm.SVN.GetEmail(self.checkout_root)
272 logging.info("SVN(%s)" % self.checkout_root)
273
274 def ReadRootFile(self, filename):
275 data = SCM.ReadRootFile(self, filename)
276 if data:
277 return data
278
279 # Try to search on the subversion repository for the file.
280 if not gcl:
281 return None
282 data = gcl.GetCachedFile(filename)
283 logging.debug('%s:\n%s' % (filename, data))
284 return data
285
286 def CaptureStatus(self):
287 return scm.SVN.CaptureStatus(None, self.checkout_root)
288
289 def GenerateDiff(self):
290 """Returns a string containing the diff for the given file list.
291
292 The files in the list should either be absolute paths or relative to the
293 given root.
294 """
295 return scm.SVN.GenerateDiff(self.files, self.checkout_root, full_move=True,
296 revision=self.diff_against)
297
298
299 class GIT(SCM):
300 """Gathers the options and diff for a git checkout."""
301 def __init__(self, *args, **kwargs):
302 SCM.__init__(self, *args, **kwargs)
303 self.checkout_root = scm.GIT.GetCheckoutRoot(self.checkout_root)
304 if not self.options.name:
305 self.options.name = scm.GIT.GetPatchName(self.checkout_root)
306 if not self.options.email:
307 self.options.email = scm.GIT.GetEmail(self.checkout_root)
308 if not self.diff_against:
309 self.diff_against = scm.GIT.GetUpstreamBranch(self.checkout_root)
310 if not self.diff_against:
311 raise NoTryServerAccess(
312 "Unable to determine default branch to diff against. "
313 "Verify this branch is set up to track another"
314 "(via the --track argument to \"git checkout -b ...\"")
315 logging.info("GIT(%s)" % self.checkout_root)
316
317 def CaptureStatus(self):
318 return scm.GIT.CaptureStatus(
319 [],
320 self.checkout_root.replace(os.sep, '/'),
321 self.diff_against)
322
323 def GenerateDiff(self):
324 if RunGit(['diff-index', 'HEAD']):
325 print 'Cannot try with a dirty tree. You must commit locally first.'
326 return None
327 return scm.GIT.GenerateDiff(
328 self.checkout_root,
329 files=self.files,
330 full_move=True,
331 branch=self.diff_against)
332
333
334 def _ParseBotList(botlist, testfilter):
335 """Parses bot configurations from a list of strings."""
336 bots = []
337 if testfilter:
338 for bot in itertools.chain.from_iterable(botspec.split(',')
339 for botspec in botlist):
340 tests = set()
341 if ':' in bot:
342 if bot.endswith(':compile'):
343 tests |= set(['compile'])
344 else:
345 raise ValueError(
346 'Can\'t use both --testfilter and --bot builder:test formats '
347 'at the same time')
348
349 bots.append((bot, tests))
350 else:
351 for botspec in botlist:
352 botname = botspec.split(':')[0]
353 tests = set()
354 if ':' in botspec:
355 tests |= set(filter(None, botspec.split(':')[1].split(',')))
356 bots.append((botname, tests))
357 return bots
358
359
360 def _ApplyTestFilter(testfilter, bot_spec):
361 """Applies testfilter from CLI.
362
363 Specifying a testfilter strips off any builder-specified tests (except for
364 compile).
365 """
366 if testfilter:
367 return [(botname, set(testfilter) | (tests & set(['compile'])))
368 for botname, tests in bot_spec]
369 else:
370 return bot_spec
371
372
373 def _GenTSBotSpec(checkouts, change, changed_files, options):
374 bot_spec = []
375 # Get try slaves from PRESUBMIT.py files if not specified.
376 # Even if the diff comes from options.url, use the local checkout for bot
377 # selection.
378 try:
379 import presubmit_support
380 root_presubmit = checkouts[0].ReadRootFile('PRESUBMIT.py')
381 if not change:
382 if not changed_files:
383 changed_files = checkouts[0].file_tuples
384 change = presubmit_support.Change(options.name,
385 '',
386 checkouts[0].checkout_root,
387 changed_files,
388 options.issue,
389 options.patchset,
390 options.email)
391 masters = presubmit_support.DoGetTryMasters(
392 change,
393 checkouts[0].GetFileNames(),
394 checkouts[0].checkout_root,
395 root_presubmit,
396 options.project,
397 options.verbose,
398 sys.stdout)
399
400 # Compatibility for old checkouts and bots that were on tryserver.chromium.
401 trybots = masters.get('tryserver.chromium', [])
402
403 # Compatibility for checkouts that are not using tryserver.chromium
404 # but are stuck with git-try or gcl-try.
405 if not trybots and len(masters) == 1:
406 trybots = masters.values()[0]
407
408 if trybots:
409 old_style = filter(lambda x: isinstance(x, basestring), trybots)
410 new_style = filter(lambda x: isinstance(x, tuple), trybots)
411
412 # _ParseBotList's testfilter is set to None otherwise it will complain.
413 bot_spec = _ApplyTestFilter(options.testfilter,
414 _ParseBotList(old_style, None))
415
416 bot_spec.extend(_ApplyTestFilter(options.testfilter, new_style))
417
418 except ImportError:
419 pass
420
421 return bot_spec
422
423
424 def _ParseSendChangeOptions(bot_spec, options):
425 """Parse common options passed to _SendChangeHTTP, _SendChangeSVN and
426 _SendChangeGit.
427 """
428 values = [
429 ('user', options.user),
430 ('name', options.name),
431 ]
432 # A list of options to copy.
433 optional_values = (
434 'email',
435 'revision',
436 'root',
437 'patchlevel',
438 'issue',
439 'patchset',
440 'target',
441 'project',
442 )
443 for option_name in optional_values:
444 value = getattr(options, option_name)
445 if value:
446 values.append((option_name, value))
447
448 # Not putting clobber to optional_names
449 # because it used to have lower-case 'true'.
450 if options.clobber:
451 values.append(('clobber', 'true'))
452
453 for bot, tests in bot_spec:
454 values.append(('bot', ('%s:%s' % (bot, ','.join(tests)))))
455
456 return values
457
458
459 def _SendChangeHTTP(bot_spec, options):
460 """Send a change to the try server using the HTTP protocol."""
461 if not options.host:
462 raise NoTryServerAccess('Please use the --host option to specify the try '
463 'server host to connect to.')
464 if not options.port:
465 raise NoTryServerAccess('Please use the --port option to specify the try '
466 'server port to connect to.')
467
468 values = _ParseSendChangeOptions(bot_spec, options)
469 values.append(('patch', options.diff))
470
471 url = 'http://%s:%s/send_try_patch' % (options.host, options.port)
472
473 logging.info('Sending by HTTP')
474 logging.info(''.join("%s=%s\n" % (k, v) for k, v in values))
475 logging.info(url)
476 logging.info(options.diff)
477 if options.dry_run:
478 return
479
480 try:
481 logging.info('Opening connection...')
482 connection = urllib2.urlopen(url, urllib.urlencode(values))
483 logging.info('Done')
484 except IOError, e:
485 logging.info(str(e))
486 if bot_spec and len(e.args) > 2 and e.args[2] == 'got a bad status line':
487 raise NoTryServerAccess('%s is unaccessible. Bad --bot argument?' % url)
488 else:
489 raise NoTryServerAccess('%s is unaccessible. Reason: %s' % (url,
490 str(e.args)))
491 if not connection:
492 raise NoTryServerAccess('%s is unaccessible.' % url)
493 logging.info('Reading response...')
494 response = connection.read()
495 logging.info('Done')
496 if response != 'OK':
497 raise NoTryServerAccess('%s is unaccessible. Got:\n%s' % (url, response))
498
499 PrintSuccess(bot_spec, options)
500
501 @contextlib.contextmanager
502 def _TempFilename(name, contents=None):
503 """Create a temporary directory, append the specified name and yield.
504
505 In contrast to NamedTemporaryFile, does not keep the file open.
506 Deletes the file on __exit__.
507 """
508 temp_dir = tempfile.mkdtemp(prefix=name)
509 try:
510 path = os.path.join(temp_dir, name)
511 if contents is not None:
512 with open(path, 'wb') as f:
513 f.write(contents)
514 yield path
515 finally:
516 shutil.rmtree(temp_dir, True)
517
518
519 @contextlib.contextmanager
520 def _PrepareDescriptionAndPatchFiles(description, options):
521 """Creates temporary files with description and patch.
522
523 __enter__ called on the return value returns a tuple of patch_filename and
524 description_filename.
525
526 Args:
527 description: contents of description file.
528 options: patchset options object. Must have attributes: user,
529 name (of patch) and diff (contents of patch).
530 """
531 current_time = str(datetime.datetime.now()).replace(':', '.')
532 patch_basename = '%s.%s.%s.diff' % (Escape(options.user),
533 Escape(options.name), current_time)
534 with _TempFilename('description', description) as description_filename:
535 with _TempFilename(patch_basename, options.diff) as patch_filename:
536 yield patch_filename, description_filename
537
538
539 def _SendChangeSVN(bot_spec, options):
540 """Send a change to the try server by committing a diff file on a subversion
541 server."""
542 if not options.svn_repo:
543 raise NoTryServerAccess('Please use the --svn_repo option to specify the'
544 ' try server svn repository to connect to.')
545
546 values = _ParseSendChangeOptions(bot_spec, options)
547 description = ''.join("%s=%s\n" % (k, v) for k, v in values)
548 logging.info('Sending by SVN')
549 logging.info(description)
550 logging.info(options.svn_repo)
551 logging.info(options.diff)
552 if options.dry_run:
553 return
554
555 with _PrepareDescriptionAndPatchFiles(description, options) as (
556 patch_filename, description_filename):
557 if sys.platform == "cygwin":
558 # Small chromium-specific issue here:
559 # git-try uses /usr/bin/python on cygwin but svn.bat will be used
560 # instead of /usr/bin/svn by default. That causes bad things(tm) since
561 # Windows' svn.exe has no clue about cygwin paths. Hence force to use
562 # the cygwin version in this particular context.
563 exe = "/usr/bin/svn"
564 else:
565 exe = "svn"
566 patch_dir = os.path.dirname(patch_filename)
567 command = [exe, 'import', '-q', patch_dir, options.svn_repo, '--file',
568 description_filename]
569 if scm.SVN.AssertVersion("1.5")[0]:
570 command.append('--no-ignore')
571
572 try:
573 subprocess2.check_call(command)
574 except subprocess2.CalledProcessError, e:
575 raise NoTryServerAccess(str(e))
576
577 PrintSuccess(bot_spec, options)
578
579 def _GetPatchGitRepo(git_url):
580 """Gets a path to a Git repo with patches.
581
582 Stores patches in .git/git-try/patches-git directory, a git repo. If it
583 doesn't exist yet or its origin URL is different, cleans up and clones it.
584 If it existed before, then pulls changes.
585
586 Does not support SVN repo.
587
588 Returns a path to the directory with patches.
589 """
590 git_dir = scm.GIT.GetGitDir(os.getcwd())
591 patch_dir = os.path.join(git_dir, GIT_PATCH_DIR_BASENAME)
592
593 logging.info('Looking for git repo for patches')
594 # Is there already a repo with the expected url or should we clone?
595 clone = True
596 if os.path.exists(patch_dir) and scm.GIT.IsInsideWorkTree(patch_dir):
597 existing_url = scm.GIT.Capture(
598 ['config', '--local', 'remote.origin.url'],
599 cwd=patch_dir)
600 clone = existing_url != git_url
601
602 if clone:
603 if os.path.exists(patch_dir):
604 logging.info('Cleaning up')
605 shutil.rmtree(patch_dir, True)
606 logging.info('Cloning patch repo')
607 scm.GIT.Capture(['clone', git_url, GIT_PATCH_DIR_BASENAME], cwd=git_dir)
608 email = scm.GIT.GetEmail(cwd=os.getcwd())
609 scm.GIT.Capture(['config', '--local', 'user.email', email], cwd=patch_dir)
610 else:
611 if scm.GIT.IsWorkTreeDirty(patch_dir):
612 logging.info('Work dir is dirty: hard reset!')
613 scm.GIT.Capture(['reset', '--hard'], cwd=patch_dir)
614 logging.info('Updating patch repo')
615 scm.GIT.Capture(['pull', 'origin', 'master'], cwd=patch_dir)
616
617 return os.path.abspath(patch_dir)
618
619
620 def _SendChangeGit(bot_spec, options):
621 """Sends a change to the try server by committing a diff file to a GIT repo.
622
623 Creates a temp orphan branch, commits patch.diff, creates a ref pointing to
624 that commit, deletes the temp branch, checks master out, adds 'ref' file
625 containing the name of the new ref, pushes master and the ref to the origin.
626
627 TODO: instead of creating a temp branch, use git-commit-tree.
628 """
629
630 if not options.git_repo:
631 raise NoTryServerAccess('Please use the --git_repo option to specify the '
632 'try server git repository to connect to.')
633
634 values = _ParseSendChangeOptions(bot_spec, options)
635 comment_subject = '%s.%s' % (options.user, options.name)
636 comment_body = ''.join("%s=%s\n" % (k, v) for k, v in values)
637 description = '%s\n\n%s' % (comment_subject, comment_body)
638 logging.info('Sending by GIT')
639 logging.info(description)
640 logging.info(options.git_repo)
641 logging.info(options.diff)
642 if options.dry_run:
643 return
644
645 patch_dir = _GetPatchGitRepo(options.git_repo)
646 def patch_git(*args):
647 return scm.GIT.Capture(list(args), cwd=patch_dir)
648 def add_and_commit(filename, comment_filename):
649 patch_git('add', filename)
650 patch_git('commit', '-F', comment_filename)
651
652 assert scm.GIT.IsInsideWorkTree(patch_dir)
653 assert not scm.GIT.IsWorkTreeDirty(patch_dir)
654
655 with _PrepareDescriptionAndPatchFiles(description, options) as (
656 patch_filename, description_filename):
657 logging.info('Committing patch')
658
659 temp_branch = 'tmp_patch'
660 target_ref = 'refs/patches/%s/%s' % (
661 Escape(options.user),
662 os.path.basename(patch_filename).replace(' ','_'))
663 target_filename = os.path.join(patch_dir, 'patch.diff')
664 branch_file = os.path.join(patch_dir, GIT_BRANCH_FILE)
665
666 patch_git('checkout', 'master')
667 try:
668 # Try deleting an existing temp branch, if any.
669 try:
670 patch_git('branch', '-D', temp_branch)
671 logging.debug('Deleted an existing temp branch.')
672 except subprocess2.CalledProcessError:
673 pass
674 # Create a new branch and put the patch there.
675 patch_git('checkout', '--orphan', temp_branch)
676 patch_git('reset')
677 patch_git('clean', '-f')
678 shutil.copyfile(patch_filename, target_filename)
679 add_and_commit(target_filename, description_filename)
680 assert not scm.GIT.IsWorkTreeDirty(patch_dir)
681
682 # Create a ref and point it to the commit referenced by temp_branch.
683 patch_git('update-ref', target_ref, temp_branch)
684
685 # Delete the temp ref.
686 patch_git('checkout', 'master')
687 patch_git('branch', '-D', temp_branch)
688
689 # Update the branch file in the master.
690 def update_branch():
691 with open(branch_file, 'w') as f:
692 f.write(target_ref)
693 add_and_commit(branch_file, description_filename)
694
695 update_branch()
696
697 # Push master and target_ref to origin.
698 logging.info('Pushing patch')
699 for attempt in xrange(_GIT_PUSH_ATTEMPTS):
700 try:
701 patch_git('push', 'origin', 'master', target_ref)
702 except subprocess2.CalledProcessError as e:
703 is_last = attempt == _GIT_PUSH_ATTEMPTS - 1
704 if is_last:
705 raise NoTryServerAccess(str(e))
706 # Fetch, reset, update branch file again.
707 patch_git('fetch', 'origin')
708 patch_git('reset', '--hard', 'origin/master')
709 update_branch()
710 except subprocess2.CalledProcessError, e:
711 # Restore state.
712 patch_git('checkout', 'master')
713 patch_git('reset', '--hard', 'origin/master')
714 raise
715
716 PrintSuccess(bot_spec, options)
717
718 def _SendChangeGerrit(bot_spec, options):
719 """Posts a try job to a Gerrit change.
720
721 Reads Change-Id from the HEAD commit, resolves the current revision, checks
722 that local revision matches the uploaded one, posts a try job in form of a
723 message, sets Tryjob-Request label to 1.
724
725 Gerrit message format: starts with !tryjob, optionally followed by a tryjob
726 definition in JSON format:
727 buildNames: list of strings specifying build names.
728 build_properties: a dict of build properties.
729 """
730
731 logging.info('Sending by Gerrit')
732 if not options.gerrit_url:
733 raise NoTryServerAccess('Please use --gerrit_url option to specify the '
734 'Gerrit instance url to connect to')
735 gerrit_host = urlparse.urlparse(options.gerrit_url).hostname
736 logging.debug('Gerrit host: %s' % gerrit_host)
737
738 def GetChangeId(commmitish):
739 """Finds Change-ID of the HEAD commit."""
740 CHANGE_ID_RGX = '^Change-Id: (I[a-f0-9]{10,})'
741 comment = scm.GIT.Capture(['log', '-1', commmitish, '--format=%b'],
742 cwd=os.getcwd())
743 change_id_match = re.search(CHANGE_ID_RGX, comment, re.I | re.M)
744 if not change_id_match:
745 raise Error('Change-Id was not found in the HEAD commit. Make sure you '
746 'have a Git hook installed that generates and inserts a '
747 'Change-Id into a commit message automatically.')
748 change_id = change_id_match.group(1)
749 return change_id
750
751 def FormatMessage():
752 # Build job definition.
753 job_def = {}
754 build_properties = {}
755 if options.testfilter:
756 build_properties['testfilter'] = options.testfilter
757 builderNames = [builder for builder, _ in bot_spec]
758 if builderNames:
759 job_def['builderNames'] = builderNames
760 if build_properties:
761 job_def['build_properties'] = build_properties
762
763 # Format message.
764 msg = '!tryjob'
765 if job_def:
766 msg = '%s %s' % (msg, json.dumps(job_def, sort_keys=True))
767 return msg
768
769 def PostTryjob(message):
770 logging.info('Posting gerrit message: %s' % message)
771 if not options.dry_run:
772 # Post a message and set TryJob=1 label.
773 try:
774 gerrit_util.SetReview(gerrit_host, change_id, msg=message,
775 labels={'Tryjob-Request': 1})
776 except gerrit_util.GerritError, e:
777 if e.http_status == 400:
778 raise Error(e.message)
779 else:
780 raise
781
782 head_sha = scm.GIT.Capture(['log', '-1', '--format=%H'], cwd=os.getcwd())
783
784 change_id = GetChangeId(head_sha)
785
786 try:
787 # Check that the uploaded revision matches the local one.
788 changes = gerrit_util.GetChangeCurrentRevision(gerrit_host, change_id)
789 except gerrit_util.GerritAuthenticationError, e:
790 raise NoTryServerAccess(e.message)
791
792 assert len(changes) <= 1, 'Multiple changes with id %s' % change_id
793 if not changes:
794 raise Error('A change %s was not found on the server. Was it uploaded?' %
795 change_id)
796 logging.debug('Found Gerrit change: %s' % changes[0])
797 if changes[0]['current_revision'] != head_sha:
798 raise Error('Please upload your latest local changes to Gerrit.')
799
800 # Post a try job.
801 message = FormatMessage()
802 PostTryjob(message)
803 change_url = urlparse.urljoin(options.gerrit_url,
804 '/#/c/%s' % changes[0]['_number'])
805 print('A tryjob was posted on change %s' % change_url)
806
807 def PrintSuccess(bot_spec, options):
808 if not options.dry_run:
809 text = 'Patch \'%s\' sent to try server' % options.name
810 if bot_spec:
811 text += ': %s' % ', '.join(
812 '%s:%s' % (b[0], ','.join(b[1])) for b in bot_spec)
813 print(text)
814
815
816 def GuessVCS(options, path, file_list):
817 """Helper to guess the version control system.
818
819 NOTE: Very similar to upload.GuessVCS. Doesn't look for hg since we don't
820 support it yet.
821
822 This examines the path directory, guesses which SCM we're using, and
823 returns an instance of the appropriate class. Exit with an error if we can't
824 figure it out.
825
826 Returns:
827 A SCM instance. Exits if the SCM can't be guessed.
828 """
829 __pychecker__ = 'no-returnvalues'
830 real_path = path.split('@')[0]
831 logging.info("GuessVCS(%s)" % path)
832 # Subversion has a .svn in all working directories.
833 if os.path.isdir(os.path.join(real_path, '.svn')):
834 return SVN(options, path, file_list)
835
836 # Git has a command to test if you're in a git tree.
837 # Try running it, but don't die if we don't have git installed.
838 try:
839 subprocess2.check_output(
840 ['git', 'rev-parse', '--is-inside-work-tree'], cwd=real_path,
841 stderr=subprocess2.VOID)
842 return GIT(options, path, file_list)
843 except OSError, e:
844 if e.errno != errno.ENOENT:
845 raise
846 except subprocess2.CalledProcessError, e:
847 if e.returncode != errno.ENOENT and e.returncode != 128:
848 # ENOENT == 2 = they don't have git installed.
849 # 128 = git error code when not in a repo.
850 logging.warning('Unexpected error code: %s' % e.returncode)
851 raise
852 raise NoTryServerAccess(
853 ( 'Could not guess version control system for %s.\n'
854 'Are you in a working copy directory?') % path)
855
856
857 def GetMungedDiff(path_diff, diff):
858 # Munge paths to match svn.
859 changed_files = []
860 for i in range(len(diff)):
861 if diff[i].startswith('--- ') or diff[i].startswith('+++ '):
862 new_file = posixpath.join(path_diff, diff[i][4:]).replace('\\', '/')
863 if diff[i].startswith('--- '):
864 file_path = new_file.split('\t')[0].strip()
865 if file_path.startswith('a/'):
866 file_path = file_path[2:]
867 changed_files.append(('M', file_path))
868 diff[i] = diff[i][0:4] + new_file
869 return (diff, changed_files)
870
871
872 class OptionParser(optparse.OptionParser):
873 def format_epilog(self, _):
874 """Removes epilog formatting."""
875 return self.epilog or ''
876
877
878 def gen_parser(prog):
879 # Parse argv
880 parser = OptionParser(usage=USAGE, version=__version__, prog=prog)
881 parser.add_option("-v", "--verbose", action="count", default=0,
882 help="Prints debugging infos")
883 group = optparse.OptionGroup(parser, "Result and status")
884 group.add_option("-u", "--user", default=getpass.getuser(),
885 help="Owner user name [default: %default]")
886 group.add_option("-e", "--email",
887 default=os.environ.get('TRYBOT_RESULTS_EMAIL_ADDRESS',
888 os.environ.get('EMAIL_ADDRESS')),
889 help="Email address where to send the results. Use either "
890 "the TRYBOT_RESULTS_EMAIL_ADDRESS environment "
891 "variable or EMAIL_ADDRESS to set the email address "
892 "the try bots report results to [default: %default]")
893 group.add_option("-n", "--name",
894 help="Descriptive name of the try job")
895 group.add_option("--issue", type='int',
896 help="Update rietveld issue try job status")
897 group.add_option("--patchset", type='int',
898 help="Update rietveld issue try job status. This is "
899 "optional if --issue is used, In that case, the "
900 "latest patchset will be used.")
901 group.add_option("--dry_run", action='store_true',
902 help="Don't send the try job. This implies --verbose, so "
903 "it will print the diff.")
904 parser.add_option_group(group)
905
906 group = optparse.OptionGroup(parser, "Try job options")
907 group.add_option(
908 "-b", "--bot", action="append",
909 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
910 "times to specify multiple builders. ex: "
911 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
912 "the try server waterfall for the builders name and the tests "
913 "available. Can also be used to specify gtest_filter, e.g. "
914 "-bwin_rel:base_unittests:ValuesTest.*Value"))
915 group.add_option("-B", "--print_bots", action="store_true",
916 help="Print bots we would use (e.g. from PRESUBMIT.py)"
917 " and exit. Do not send patch. Like --dry_run"
918 " but less verbose.")
919 group.add_option("-r", "--revision",
920 help="Revision to use for the try job. If 'auto' is "
921 "specified, it is resolved to the revision a patch is "
922 "generated against (Git only). Default: the "
923 "revision will be determined by the try server; see "
924 "its waterfall for more info")
925 group.add_option("-c", "--clobber", action="store_true",
926 help="Force a clobber before building; e.g. don't do an "
927 "incremental build")
928 # TODO(maruel): help="Select a specific configuration, usually 'debug' or "
929 # "'release'"
930 group.add_option("--target", help=optparse.SUPPRESS_HELP)
931
932 group.add_option("--project",
933 help="Override which project to use. Projects are defined "
934 "server-side to define what default bot set to use")
935
936 group.add_option(
937 "-t", "--testfilter", action="append", default=[],
938 help=("Apply a testfilter to all the selected builders. Unless the "
939 "builders configurations are similar, use multiple "
940 "--bot <builder>:<test> arguments."))
941
942 parser.add_option_group(group)
943
944 group = optparse.OptionGroup(parser, "Patch to run")
945 group.add_option("-f", "--file", default=[], dest="files",
946 metavar="FILE", action="append",
947 help="Use many times to list the files to include in the "
948 "try, relative to the repository root")
949 group.add_option("--diff",
950 help="File containing the diff to try")
951 group.add_option("--url",
952 help="Url where to grab a patch, e.g. "
953 "http://example.com/x.diff")
954 group.add_option("-R", "--rietveld_url", default="codereview.chromium.org",
955 metavar="URL",
956 help="Has 2 usages, both refer to the rietveld instance: "
957 "Specify which code review patch to use as the try job "
958 "or rietveld instance to update the try job results "
959 "Default:%default")
960 group.add_option("--root",
961 help="Root to use for the patch; base subdirectory for "
962 "patch created in a subdirectory")
963 group.add_option("-p", "--patchlevel", type='int', metavar="LEVEL",
964 help="Used as -pN parameter to patch")
965 group.add_option("-s", "--sub_rep", action="append", default=[],
966 help="Subcheckout to use in addition. This is mainly "
967 "useful for gclient-style checkouts. In git, checkout "
968 "the branch with changes first. Use @rev or "
969 "@branch to specify the "
970 "revision/branch to diff against. If no @branch is "
971 "given the diff will be against the upstream branch. "
972 "If @branch then the diff is branch..HEAD. "
973 "All edits must be checked in.")
974 group.add_option("--no_search", action="store_true",
975 help=("Disable automatic search for gclient or repo "
976 "checkout root."))
977 group.add_option("-E", "--exclude", action="append",
978 default=['ChangeLog'], metavar='REGEXP',
979 help="Regexp patterns to exclude files. Default: %default")
980 group.add_option("--upstream_branch", action="store",
981 help="Specify the upstream branch to diff against in the "
982 "main checkout")
983 parser.add_option_group(group)
984
985 group = optparse.OptionGroup(parser, "Access the try server by HTTP")
986 group.add_option("--use_http",
987 action="store_const",
988 const=_SendChangeHTTP,
989 dest="send_patch",
990 help="Use HTTP to talk to the try server [default]")
991 group.add_option("-H", "--host",
992 help="Host address")
993 group.add_option("-P", "--port", type="int",
994 help="HTTP port")
995 parser.add_option_group(group)
996
997 group = optparse.OptionGroup(parser, "Access the try server with SVN")
998 group.add_option("--use_svn",
999 action="store_const",
1000 const=_SendChangeSVN,
1001 dest="send_patch",
1002 help="Use SVN to talk to the try server")
1003 group.add_option("-S", "--svn_repo",
1004 metavar="SVN_URL",
1005 help="SVN url to use to write the changes in; --use_svn is "
1006 "implied when using --svn_repo")
1007 parser.add_option_group(group)
1008
1009 group = optparse.OptionGroup(parser, "Access the try server with Git")
1010 group.add_option("--use_git",
1011 action="store_const",
1012 const=_SendChangeGit,
1013 dest="send_patch",
1014 help="Use GIT to talk to the try server")
1015 group.add_option("-G", "--git_repo",
1016 metavar="GIT_URL",
1017 help="GIT url to use to write the changes in; --use_git is "
1018 "implied when using --git_repo")
1019 parser.add_option_group(group)
1020
1021 group = optparse.OptionGroup(parser, "Access the try server with Gerrit")
1022 group.add_option("--use_gerrit",
1023 action="store_const",
1024 const=_SendChangeGerrit,
1025 dest="send_patch",
1026 help="Use Gerrit to talk to the try server")
1027 group.add_option("--gerrit_url",
1028 metavar="GERRIT_URL",
1029 help="Gerrit url to post a tryjob to; --use_gerrit is "
1030 "implied when using --gerrit_url")
1031 parser.add_option_group(group)
1032
1033 return parser
1034
1035
1036 def TryChange(argv,
1037 change,
1038 swallow_exception,
1039 prog=None,
1040 extra_epilog=None):
1041 """
1042 Args:
1043 argv: Arguments and options.
1044 change: Change instance corresponding to the CL.
1045 swallow_exception: Whether we raise or swallow exceptions.
1046 """
1047 parser = gen_parser(prog)
1048 epilog = EPILOG % { 'prog': prog }
1049 if extra_epilog:
1050 epilog += extra_epilog
1051 parser.epilog = epilog
1052
1053 options, args = parser.parse_args(argv)
1054
1055 # If they've asked for help, give it to them
1056 if len(args) == 1 and args[0] == 'help':
1057 parser.print_help()
1058 return 0
1059
1060 # If they've said something confusing, don't spawn a try job until you
1061 # understand what they want.
1062 if args:
1063 parser.error('Extra argument(s) "%s" not understood' % ' '.join(args))
1064
1065 if options.dry_run:
1066 options.verbose += 1
1067
1068 LOG_FORMAT = '%(levelname)s %(filename)s(%(lineno)d): %(message)s'
1069 if not swallow_exception:
1070 if options.verbose == 0:
1071 logging.basicConfig(level=logging.WARNING, format=LOG_FORMAT)
1072 elif options.verbose == 1:
1073 logging.basicConfig(level=logging.INFO, format=LOG_FORMAT)
1074 elif options.verbose > 1:
1075 logging.basicConfig(level=logging.DEBUG, format=LOG_FORMAT)
1076
1077 logging.debug(argv)
1078
1079 if (options.patchlevel is not None and
1080 (options.patchlevel < 0 or options.patchlevel > 10)):
1081 parser.error(
1082 'Have you tried --port instead? You probably confused -p and -P.')
1083
1084 # Strip off any @ in the user, otherwise svn gets confused.
1085 options.user = options.user.split('@', 1)[0]
1086
1087 if options.rietveld_url:
1088 # Try to extract the review number if possible and fix the protocol.
1089 if not '://' in options.rietveld_url:
1090 options.rietveld_url = 'http://' + options.rietveld_url
1091 match = re.match(r'^(.*)/(\d+)/?$', options.rietveld_url)
1092 if match:
1093 if options.issue or options.patchset:
1094 parser.error('Cannot use both --issue and use a review number url')
1095 options.issue = int(match.group(2))
1096 options.rietveld_url = match.group(1)
1097
1098 try:
1099 changed_files = None
1100 # Always include os.getcwd() in the checkout settings.
1101 path = os.getcwd()
1102
1103 file_list = []
1104 if options.files:
1105 file_list = options.files
1106 elif change:
1107 file_list = [f.LocalPath() for f in change.AffectedFiles()]
1108
1109 if options.upstream_branch:
1110 path += '@' + options.upstream_branch
1111 # Clear file list so that the correct list will be retrieved from the
1112 # upstream branch.
1113 file_list = []
1114
1115 current_vcs = GuessVCS(options, path, file_list)
1116 current_vcs.AutomagicalSettings()
1117 options = current_vcs.options
1118 vcs_is_git = type(current_vcs) is GIT
1119
1120 # So far, git_repo doesn't work with SVN
1121 if options.git_repo and not vcs_is_git:
1122 parser.error('--git_repo option is supported only for GIT repositories')
1123
1124 # If revision==auto, resolve it
1125 if options.revision and options.revision.lower() == 'auto':
1126 if not vcs_is_git:
1127 parser.error('--revision=auto is supported only for GIT repositories')
1128 options.revision = scm.GIT.Capture(
1129 ['rev-parse', current_vcs.diff_against],
1130 cwd=path)
1131
1132 checkouts = [current_vcs]
1133 for item in options.sub_rep:
1134 # Pass file_list=None because we don't know the sub repo's file list.
1135 checkout = GuessVCS(options,
1136 os.path.join(current_vcs.checkout_root, item),
1137 None)
1138 if checkout.checkout_root in [c.checkout_root for c in checkouts]:
1139 parser.error('Specified the root %s two times.' %
1140 checkout.checkout_root)
1141 checkouts.append(checkout)
1142
1143 can_http = options.port and options.host
1144 can_svn = options.svn_repo
1145 can_git = options.git_repo
1146 can_gerrit = options.gerrit_url
1147 can_something = can_http or can_svn or can_git or can_gerrit
1148 # If there was no transport selected yet, now we must have enough data to
1149 # select one.
1150 if not options.send_patch and not can_something:
1151 parser.error('Please specify an access method.')
1152
1153 # Convert options.diff into the content of the diff.
1154 if options.url:
1155 if options.files:
1156 parser.error('You cannot specify files and --url at the same time.')
1157 options.diff = urllib2.urlopen(options.url).read()
1158 elif options.diff:
1159 if options.files:
1160 parser.error('You cannot specify files and --diff at the same time.')
1161 options.diff = gclient_utils.FileRead(options.diff, 'rb')
1162 elif options.issue and options.patchset is None:
1163 # Retrieve the patch from rietveld when the diff is not specified.
1164 # When patchset is specified, it's because it's done by gcl/git-try.
1165 api_url = '%s/api/%d' % (options.rietveld_url, options.issue)
1166 logging.debug(api_url)
1167 contents = json.loads(urllib2.urlopen(api_url).read())
1168 options.patchset = contents['patchsets'][-1]
1169 diff_url = ('%s/download/issue%d_%d.diff' %
1170 (options.rietveld_url, options.issue, options.patchset))
1171 diff = GetMungedDiff('', urllib2.urlopen(diff_url).readlines())
1172 options.diff = ''.join(diff[0])
1173 changed_files = diff[1]
1174 else:
1175 # Use this as the base.
1176 root = checkouts[0].checkout_root
1177 diffs = []
1178 for checkout in checkouts:
1179 raw_diff = checkout.GenerateDiff()
1180 if not raw_diff:
1181 continue
1182 diff = raw_diff.splitlines(True)
1183 path_diff = gclient_utils.PathDifference(root, checkout.checkout_root)
1184 # Munge it.
1185 diffs.extend(GetMungedDiff(path_diff, diff)[0])
1186 if not diffs:
1187 logging.error('Empty or non-existant diff, exiting.')
1188 return 1
1189 options.diff = ''.join(diffs)
1190
1191 if not options.name:
1192 if options.issue:
1193 options.name = 'Issue %s' % options.issue
1194 else:
1195 options.name = 'Unnamed'
1196 print('Note: use --name NAME to change the try job name.')
1197
1198 if not options.email:
1199 parser.error('Using an anonymous checkout. Please use --email or set '
1200 'the TRYBOT_RESULTS_EMAIL_ADDRESS environment variable.')
1201 print('Results will be emailed to: ' + options.email)
1202
1203 if options.bot:
1204 bot_spec = _ApplyTestFilter(
1205 options.testfilter, _ParseBotList(options.bot, options.testfilter))
1206 else:
1207 bot_spec = _GenTSBotSpec(checkouts, change, changed_files, options)
1208
1209 if options.testfilter:
1210 bot_spec = _ApplyTestFilter(options.testfilter, bot_spec)
1211
1212 if any('triggered' in b[0] for b in bot_spec):
1213 print >> sys.stderr, (
1214 'ERROR You are trying to send a job to a triggered bot. This type of'
1215 ' bot requires an\ninitial job from a parent (usually a builder). '
1216 'Instead send your job to the parent.\nBot list: %s' % bot_spec)
1217 return 1
1218
1219 if options.print_bots:
1220 print 'Bots which would be used:'
1221 for bot in bot_spec:
1222 if bot[1]:
1223 print ' %s:%s' % (bot[0], ','.join(bot[1]))
1224 else:
1225 print ' %s' % (bot[0])
1226 return 0
1227
1228 # Determine sending protocol
1229 if options.send_patch:
1230 # If forced.
1231 senders = [options.send_patch]
1232 else:
1233 # Try sending patch using avaialble protocols
1234 all_senders = [
1235 (_SendChangeHTTP, can_http),
1236 (_SendChangeSVN, can_svn),
1237 (_SendChangeGerrit, can_gerrit),
1238 (_SendChangeGit, can_git),
1239 ]
1240 senders = [sender for sender, can in all_senders if can]
1241
1242 # Send the patch.
1243 for sender in senders:
1244 try:
1245 sender(bot_spec, options)
1246 return 0
1247 except NoTryServerAccess:
1248 is_last = sender == senders[-1]
1249 if is_last:
1250 raise
1251 assert False, "Unreachable code"
1252 except Error, e:
1253 if swallow_exception:
1254 return 1
1255 print >> sys.stderr, e
1256 return 1
1257 except (gclient_utils.Error, subprocess2.CalledProcessError), e:
1258 print >> sys.stderr, e
1259 return 1
1260 return 0
1261
1262
1263 if __name__ == "__main__":
1264 fix_encoding.fix_encoding()
1265 sys.exit(TryChange(None, None, False))
OLDNEW
« no previous file with comments | « tests/trychange_unittest.py ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698