OLD | NEW |
1 #!/usr/bin/python | 1 #!/usr/bin/python |
2 # Copyright (c) 2009 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2009 The Chromium Authors. All rights reserved. |
3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
5 """Client-side script to send a try job to the try server. It communicates to | 5 """Client-side script to send a try job to the try server. It communicates to |
6 the try server by either writting to a svn repository or by directly connecting | 6 the try server by either writting to a svn repository or by directly connecting |
7 to the server by HTTP. | 7 to the server by HTTP. |
8 """ | 8 """ |
9 | 9 |
10 import datetime | 10 import datetime |
11 import getpass | 11 import getpass |
12 import logging | 12 import logging |
13 import optparse | 13 import optparse |
14 import os | 14 import os |
15 import shutil | 15 import shutil |
16 import socket | 16 import socket |
17 import subprocess | 17 import subprocess |
18 import sys | 18 import sys |
19 import tempfile | 19 import tempfile |
20 import urllib | 20 import urllib |
21 | 21 |
22 import breakpad | 22 try: |
| 23 import breakpad |
| 24 except ImportError: |
| 25 pass |
23 | 26 |
24 import gcl | |
25 import gclient_utils | 27 import gclient_utils |
26 import scm | 28 import scm |
27 import presubmit_support | |
28 | 29 |
29 __version__ = '1.2' | 30 __version__ = '1.2' |
30 | 31 |
31 | 32 |
32 # Constants | 33 # Constants |
33 HELP_STRING = "Sorry, Tryserver is not available." | 34 HELP_STRING = "Sorry, Tryserver is not available." |
34 USAGE = r"""%prog [change_name] [options] | 35 USAGE = r"""%prog [change_name] [options] |
35 | 36 |
36 Client-side script to send a try job to the try server. It communicates to | 37 Client-side script to send a try job to the try server. It communicates to |
37 the try server by either writting to a svn repository or by directly connecting | 38 the try server by either writting to a svn repository or by directly connecting |
(...skipping 20 matching lines...) Expand all Loading... |
58 class InvalidScript(Exception): | 59 class InvalidScript(Exception): |
59 def __str__(self): | 60 def __str__(self): |
60 return self.args[0] + '\n' + HELP_STRING | 61 return self.args[0] + '\n' + HELP_STRING |
61 | 62 |
62 | 63 |
63 class NoTryServerAccess(Exception): | 64 class NoTryServerAccess(Exception): |
64 def __str__(self): | 65 def __str__(self): |
65 return self.args[0] + '\n' + HELP_STRING | 66 return self.args[0] + '\n' + HELP_STRING |
66 | 67 |
67 | 68 |
68 def GetTryServerSettings(): | |
69 """Grab try server settings local to the repository.""" | |
70 def _SafeResolve(host): | |
71 try: | |
72 return socket.getaddrinfo(host, None) | |
73 except socket.gaierror: | |
74 return None | |
75 | |
76 settings = {} | |
77 settings['http_port'] = gcl.GetCodeReviewSetting('TRYSERVER_HTTP_PORT') | |
78 settings['http_host'] = gcl.GetCodeReviewSetting('TRYSERVER_HTTP_HOST') | |
79 settings['svn_repo'] = gcl.GetCodeReviewSetting('TRYSERVER_SVN_URL') | |
80 settings['default_project'] = gcl.GetCodeReviewSetting('TRYSERVER_PROJECT') | |
81 settings['default_root'] = gcl.GetCodeReviewSetting('TRYSERVER_ROOT') | |
82 | |
83 # Pick a patchlevel, default to 0. | |
84 default_patchlevel = gcl.GetCodeReviewSetting('TRYSERVER_PATCHLEVEL') | |
85 if default_patchlevel: | |
86 default_patchlevel = int(default_patchlevel) | |
87 else: | |
88 default_patchlevel = 0 | |
89 settings['default_patchlevel'] = default_patchlevel | |
90 | |
91 # Use http is the http_host name resolve, fallback to svn otherwise. | |
92 if (settings['http_port'] and settings['http_host'] and | |
93 _SafeResolve(settings['http_host'])): | |
94 settings['default_transport'] = 'http' | |
95 elif settings.get('svn_repo'): | |
96 settings['default_transport'] = 'svn' | |
97 return settings | |
98 | |
99 | |
100 def EscapeDot(name): | 69 def EscapeDot(name): |
101 return name.replace('.', '-') | 70 return name.replace('.', '-') |
102 | 71 |
103 | 72 |
104 class SCM(object): | 73 class SCM(object): |
105 """Simplistic base class to implement one function: ProcessOptions.""" | 74 """Simplistic base class to implement one function: ProcessOptions.""" |
106 def __init__(self, options): | 75 def __init__(self, options): |
107 self.options = options | 76 self.options = options |
108 | 77 |
109 def GetFileNames(self): | 78 def GetFileNames(self): |
(...skipping 26 matching lines...) Expand all Loading... |
136 self.options.files = [f[1] for f in scm.SVN.CaptureStatus(None)] | 105 self.options.files = [f[1] for f in scm.SVN.CaptureStatus(None)] |
137 # Directories will return None so filter them out. | 106 # Directories will return None so filter them out. |
138 diff = filter(None, [scm.SVN.DiffItem(f) for f in self.options.files]) | 107 diff = filter(None, [scm.SVN.DiffItem(f) for f in self.options.files]) |
139 os.chdir(previous_cwd) | 108 os.chdir(previous_cwd) |
140 return "".join(diff) | 109 return "".join(diff) |
141 | 110 |
142 def GetLocalRoot(self): | 111 def GetLocalRoot(self): |
143 """Return the path of the repository root.""" | 112 """Return the path of the repository root.""" |
144 return self.checkout_root | 113 return self.checkout_root |
145 | 114 |
| 115 def GetBots(self): |
| 116 try: |
| 117 import gcl |
| 118 return gcl.GetCachedFile('PRESUBMIT.py', use_root=True) |
| 119 except ImportError: |
| 120 try: |
| 121 return gclient_utils.FileRead(os.path.join(self.checkout_root, |
| 122 'PRESUBMIT.py')) |
| 123 except OSError: |
| 124 return None |
| 125 |
146 | 126 |
147 class GIT(SCM): | 127 class GIT(SCM): |
148 """Gathers the options and diff for a git checkout.""" | 128 """Gathers the options and diff for a git checkout.""" |
149 def __init__(self, *args, **kwargs): | 129 def __init__(self, *args, **kwargs): |
150 SCM.__init__(self, *args, **kwargs) | 130 SCM.__init__(self, *args, **kwargs) |
151 self.checkout_root = os.path.abspath( | 131 self.checkout_root = os.path.abspath( |
152 gclient_utils.CheckCall(['git', 'rev-parse', '--show-cdup']).strip()) | 132 gclient_utils.CheckCall(['git', 'rev-parse', '--show-cdup']).strip()) |
153 if not self.options.diff: | 133 if not self.options.diff: |
154 self.options.diff = self._GenerateDiff() | 134 self.options.diff = self._GenerateDiff() |
155 if not self.options.name: | 135 if not self.options.name: |
(...skipping 21 matching lines...) Expand all Loading... |
177 if not branch.startswith('refs/heads/'): | 157 if not branch.startswith('refs/heads/'): |
178 # TODO(maruel): Find a better type. | 158 # TODO(maruel): Find a better type. |
179 raise NoTryServerAccess("Couldn't figure out branch name") | 159 raise NoTryServerAccess("Couldn't figure out branch name") |
180 branch = branch[len('refs/heads/'):] | 160 branch = branch[len('refs/heads/'):] |
181 return branch | 161 return branch |
182 | 162 |
183 def GetLocalRoot(self): | 163 def GetLocalRoot(self): |
184 """Return the path of the repository root.""" | 164 """Return the path of the repository root.""" |
185 return self.checkout_root | 165 return self.checkout_root |
186 | 166 |
| 167 def GetBots(self): |
| 168 try: |
| 169 return gclient_utils.FileRead(os.path.join(self.checkout_root, |
| 170 'PRESUBMIT.py')) |
| 171 except OSError: |
| 172 return None |
| 173 |
187 | 174 |
188 def _ParseSendChangeOptions(options): | 175 def _ParseSendChangeOptions(options): |
189 """Parse common options passed to _SendChangeHTTP and _SendChangeSVN.""" | 176 """Parse common options passed to _SendChangeHTTP and _SendChangeSVN.""" |
190 values = {} | 177 values = {} |
191 if options.email: | 178 if options.email: |
192 values['email'] = options.email | 179 values['email'] = options.email |
193 values['user'] = options.user | 180 values['user'] = options.user |
194 values['name'] = options.name | 181 values['name'] = options.name |
195 if options.bot: | 182 if options.bot: |
196 values['bot'] = ','.join(options.bot) | 183 values['bot'] = ','.join(options.bot) |
(...skipping 66 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
263 description += "%s=%s\n" % (k,v) | 250 description += "%s=%s\n" % (k,v) |
264 | 251 |
265 # Do an empty checkout. | 252 # Do an empty checkout. |
266 temp_dir = tempfile.mkdtemp() | 253 temp_dir = tempfile.mkdtemp() |
267 temp_file = tempfile.NamedTemporaryFile() | 254 temp_file = tempfile.NamedTemporaryFile() |
268 try: | 255 try: |
269 try: | 256 try: |
270 command = ['svn', 'checkout', '--depth', 'empty', '-q', | 257 command = ['svn', 'checkout', '--depth', 'empty', '-q', |
271 options.svn_repo, temp_dir] | 258 options.svn_repo, temp_dir] |
272 if options.email: | 259 if options.email: |
273 command += ['--username', options.email] | 260 command.extend(['--username', options.email]) |
274 gclient_utils.CheckCall(command) | 261 gclient_utils.CheckCall(command) |
275 | 262 |
276 # TODO(maruel): Use a subdirectory per user? | 263 # TODO(maruel): Use a subdirectory per user? |
277 current_time = str(datetime.datetime.now()).replace(':', '.') | 264 current_time = str(datetime.datetime.now()).replace(':', '.') |
278 file_name = (EscapeDot(options.user) + '.' + EscapeDot(options.name) + | 265 file_name = (EscapeDot(options.user) + '.' + EscapeDot(options.name) + |
279 '.%s.diff' % current_time) | 266 '.%s.diff' % current_time) |
280 full_path = os.path.join(temp_dir, file_name) | 267 full_path = os.path.join(temp_dir, file_name) |
281 full_url = options.svn_repo + '/' + file_name | 268 full_url = options.svn_repo + '/' + file_name |
282 file_found = False | 269 file_found = False |
283 try: | 270 try: |
284 gclient_utils.CheckCall(['svn', 'ls', full_url]) | 271 gclient_utils.CheckCall(['svn', 'ls', full_url], print_error=False) |
285 file_found = True | 272 file_found = True |
286 except gclient_utils.CheckCallError: | 273 except gclient_utils.CheckCallError: |
287 pass | 274 pass |
288 if file_found: | 275 if file_found: |
289 # The file already exists in the repo. Note that commiting a file is a | 276 # The file already exists in the repo. Note that commiting a file is a |
290 # no-op if the file's content (the diff) is not modified. This is why | 277 # no-op if the file's content (the diff) is not modified. This is why |
291 # the file name contains the date and time. | 278 # the file name contains the date and time. |
292 gclient_utils.CheckCall(['svn', 'update', full_path]) | 279 gclient_utils.CheckCall(['svn', 'update', full_path], |
293 f = open(full_path, 'wb') | 280 print_error=False) |
294 f.write(options.diff) | 281 gclient_utils.FileWrite(full_path, options.diff, 'wb') |
295 f.close() | |
296 else: | 282 else: |
297 # Add the file to the repo | 283 # Add the file to the repo. |
298 f = open(full_path, 'wb') | 284 gclient_utils.FileWrite(full_path, options.diff, 'wb') |
299 f.write(options.diff) | 285 gclient_utils.CheckCall(["svn", "add", full_path], print_error=False) |
300 f.close() | |
301 gclient_utils.CheckCall(["svn", "add", full_path]) | |
302 temp_file.write(description) | 286 temp_file.write(description) |
303 temp_file.flush() | 287 temp_file.flush() |
304 gclient_utils.CheckCall(["svn", "commit", full_path, '--file', | 288 gclient_utils.CheckCall(["svn", "commit", full_path, '--file', |
305 temp_file.name]) | 289 temp_file.name], print_error=False) |
306 except gclient_utils.CheckCallError, e: | 290 except gclient_utils.CheckCallError, e: |
307 raise NoTryServerAccess(' '.join(e.command) + '\nOuput:\n' + | 291 raise NoTryServerAccess(' '.join(e.command) + '\nOuput:\n' + |
308 e.stdout) | 292 e.stdout) |
309 finally: | 293 finally: |
310 temp_file.close() | 294 temp_file.close() |
311 shutil.rmtree(temp_dir, True) | 295 shutil.rmtree(temp_dir, True) |
312 | 296 |
313 | 297 |
314 def GuessVCS(options): | 298 def GuessVCS(options): |
315 """Helper to guess the version control system. | 299 """Helper to guess the version control system. |
(...skipping 30 matching lines...) Expand all Loading... |
346 def TryChange(argv, | 330 def TryChange(argv, |
347 file_list, | 331 file_list, |
348 swallow_exception, | 332 swallow_exception, |
349 prog=None): | 333 prog=None): |
350 """ | 334 """ |
351 Args: | 335 Args: |
352 argv: Arguments and options. | 336 argv: Arguments and options. |
353 file_list: Default value to pass to --file. | 337 file_list: Default value to pass to --file. |
354 swallow_exception: Whether we raise or swallow exceptions. | 338 swallow_exception: Whether we raise or swallow exceptions. |
355 """ | 339 """ |
356 default_settings = GetTryServerSettings() | |
357 transport_functions = { 'http': _SendChangeHTTP, 'svn': _SendChangeSVN } | |
358 default_transport = transport_functions.get( | |
359 default_settings.get('default_transport')) | |
360 | |
361 # Parse argv | 340 # Parse argv |
362 parser = optparse.OptionParser(usage=USAGE, | 341 parser = optparse.OptionParser(usage=USAGE, |
363 version=__version__, | 342 version=__version__, |
364 prog=prog) | 343 prog=prog) |
365 | 344 |
366 group = optparse.OptionGroup(parser, "Result and status") | 345 group = optparse.OptionGroup(parser, "Result and status") |
367 group.add_option("-u", "--user", default=getpass.getuser(), | 346 group.add_option("-u", "--user", default=getpass.getuser(), |
368 help="Owner user name [default: %default]") | 347 help="Owner user name [default: %default]") |
369 group.add_option("-e", "--email", | 348 group.add_option("-e", "--email", |
370 default=os.environ.get('TRYBOT_RESULTS_EMAIL_ADDRESS', | 349 default=os.environ.get('TRYBOT_RESULTS_EMAIL_ADDRESS', |
(...skipping 19 matching lines...) Expand all Loading... |
390 help="Revision to use for the try job; default: the " | 369 help="Revision to use for the try job; default: the " |
391 "revision will be determined by the try server; see " | 370 "revision will be determined by the try server; see " |
392 "its waterfall for more info") | 371 "its waterfall for more info") |
393 group.add_option("-c", "--clobber", action="store_true", | 372 group.add_option("-c", "--clobber", action="store_true", |
394 help="Force a clobber before building; e.g. don't do an " | 373 help="Force a clobber before building; e.g. don't do an " |
395 "incremental build") | 374 "incremental build") |
396 # TODO(maruel): help="Select a specific configuration, usually 'debug' or " | 375 # TODO(maruel): help="Select a specific configuration, usually 'debug' or " |
397 # "'release'" | 376 # "'release'" |
398 group.add_option("--target", help=optparse.SUPPRESS_HELP) | 377 group.add_option("--target", help=optparse.SUPPRESS_HELP) |
399 | 378 |
400 # TODO(bradnelson): help="Override which project to use" | 379 group.add_option("--project", |
401 group.add_option("--project", help=optparse.SUPPRESS_HELP, | 380 help="Override which project to use") |
402 default=default_settings['default_project']) | |
403 | 381 |
404 # Override the list of tests to run, use multiple times to list many tests | 382 # Override the list of tests to run, use multiple times to list many tests |
405 # (or comma separated) | 383 # (or comma separated) |
406 group.add_option("-t", "--tests", action="append", | 384 group.add_option("-t", "--tests", action="append", |
407 help=optparse.SUPPRESS_HELP) | 385 help=optparse.SUPPRESS_HELP) |
408 parser.add_option_group(group) | 386 parser.add_option_group(group) |
409 | 387 |
410 group = optparse.OptionGroup(parser, "Patch to run") | 388 group = optparse.OptionGroup(parser, "Patch to run") |
411 group.add_option("-f", "--file", default=file_list, dest="files", | 389 group.add_option("-f", "--file", default=file_list, dest="files", |
412 metavar="FILE", action="append", | 390 metavar="FILE", action="append", |
413 help="Use many times to list the files to include in the " | 391 help="Use many times to list the files to include in the " |
414 "try, relative to the repository root") | 392 "try, relative to the repository root") |
415 group.add_option("--diff", | 393 group.add_option("--diff", |
416 help="File containing the diff to try") | 394 help="File containing the diff to try") |
417 group.add_option("--url", | 395 group.add_option("--url", |
418 help="Url where to grab a patch") | 396 help="Url where to grab a patch") |
419 group.add_option("--root", | 397 group.add_option("--root", |
420 help="Root to use for the patch; base subdirectory for " | 398 help="Root to use for the patch; base subdirectory for " |
421 "patch created in a subdirectory", | 399 "patch created in a subdirectory") |
422 default=default_settings["default_root"]) | |
423 group.add_option("--patchlevel", type='int', metavar="LEVEL", | 400 group.add_option("--patchlevel", type='int', metavar="LEVEL", |
424 help="Used as -pN parameter to patch", | 401 help="Used as -pN parameter to patch") |
425 default=default_settings["default_patchlevel"]) | |
426 parser.add_option_group(group) | 402 parser.add_option_group(group) |
427 | 403 |
428 group = optparse.OptionGroup(parser, "Access the try server by HTTP") | 404 group = optparse.OptionGroup(parser, "Access the try server by HTTP") |
429 group.add_option("--use_http", | 405 group.add_option("--use_http", |
430 action="store_const", | 406 action="store_const", |
431 const=_SendChangeHTTP, | 407 const=_SendChangeHTTP, |
432 dest="send_patch", | 408 dest="send_patch", |
433 default=default_transport, | |
434 help="Use HTTP to talk to the try server [default]") | 409 help="Use HTTP to talk to the try server [default]") |
435 group.add_option("--host", | 410 group.add_option("--host", |
436 default=default_settings['http_host'], | |
437 help="Host address") | 411 help="Host address") |
438 group.add_option("--port", | 412 group.add_option("--port", |
439 default=default_settings['http_port'], | |
440 help="HTTP port") | 413 help="HTTP port") |
441 group.add_option("--proxy", | 414 group.add_option("--proxy", |
442 help="HTTP proxy") | 415 help="HTTP proxy") |
443 parser.add_option_group(group) | 416 parser.add_option_group(group) |
444 | 417 |
445 group = optparse.OptionGroup(parser, "Access the try server with SVN") | 418 group = optparse.OptionGroup(parser, "Access the try server with SVN") |
446 group.add_option("--use_svn", | 419 group.add_option("--use_svn", |
447 action="store_const", | 420 action="store_const", |
448 const=_SendChangeSVN, | 421 const=_SendChangeSVN, |
449 dest="send_patch", | 422 dest="send_patch", |
450 help="Use SVN to talk to the try server") | 423 help="Use SVN to talk to the try server") |
451 group.add_option("--svn_repo", | 424 group.add_option("--svn_repo", |
452 metavar="SVN_URL", | 425 metavar="SVN_URL", |
453 default=default_settings['svn_repo'], | |
454 help="SVN url to use to write the changes in; --use_svn is " | 426 help="SVN url to use to write the changes in; --use_svn is " |
455 "implied when using --svn_repo") | 427 "implied when using --svn_repo") |
456 parser.add_option_group(group) | 428 parser.add_option_group(group) |
457 | 429 |
458 options, args = parser.parse_args(argv) | 430 options, args = parser.parse_args(argv) |
459 | 431 |
460 # Switch the default accordingly if there was no default send_patch. | 432 # Switch the default accordingly if there was no default send_patch. |
461 if not options.send_patch: | 433 if not options.send_patch: |
462 if options.port and options.host: | 434 if options.port and options.host: |
463 options.send_patch = _SendChangeHTTP | 435 options.send_patch = _SendChangeHTTP |
(...skipping 20 matching lines...) Expand all Loading... |
484 try: | 456 try: |
485 options.scm = GuessVCS(options) | 457 options.scm = GuessVCS(options) |
486 except NoTryServerAccess, e: | 458 except NoTryServerAccess, e: |
487 # If we got the diff, we don't care. | 459 # If we got the diff, we don't care. |
488 if not options.diff: | 460 if not options.diff: |
489 # TODO(maruel): Raise what? | 461 # TODO(maruel): Raise what? |
490 raise | 462 raise |
491 | 463 |
492 # Get try slaves from PRESUBMIT.py files if not specified. | 464 # Get try slaves from PRESUBMIT.py files if not specified. |
493 if not options.bot: | 465 if not options.bot: |
494 if options.url: | 466 # Even if the diff comes from options.url, use the local checkout for bot |
495 parser.error('You need to specify which bots to use.') | 467 # selection. |
496 root_presubmit = gcl.GetCachedFile('PRESUBMIT.py', use_root=True) | 468 try: |
497 options.bot = presubmit_support.DoGetTrySlaves(options.scm.GetFileNames(), | 469 # Get try slaves from PRESUBMIT.py files if not specified. |
498 options.scm.GetLocalRoot(), | 470 import presubmit_support |
499 root_presubmit, | 471 root_presubmit = options.scm.GetBots() |
500 False, | 472 options.bot = presubmit_support.DoGetTrySlaves( |
501 sys.stdout) | 473 options.scm.GetFileNames(), |
| 474 options.scm.GetLocalRoot(), |
| 475 root_presubmit, |
| 476 False, |
| 477 sys.stdout) |
| 478 except ImportError: |
| 479 pass |
| 480 # If no bot is specified, either the default pool will be selected or the |
| 481 # try server will refuse the job. Either case we don't need to interfere. |
502 | 482 |
503 if options.name is None: | 483 if options.name is None: |
504 if options.issue: | 484 if options.issue: |
505 options.name = 'Issue %s' % options.issue | 485 options.name = 'Issue %s' % options.issue |
506 else: | 486 else: |
507 options.name = 'Unnamed' | 487 options.name = 'Unnamed' |
508 print('Note: use --name NAME to change the try job name.') | 488 print('Note: use --name NAME to change the try job name.') |
509 if not options.email: | 489 if not options.email: |
510 print('Warning: TRYBOT_RESULTS_EMAIL_ADDRESS is not set. Try server ' | 490 print('Warning: TRYBOT_RESULTS_EMAIL_ADDRESS is not set. Try server ' |
511 'results might\ngo to: %s@google.com.\n' % options.user) | 491 'results might\ngo to: %s@google.com.\n' % options.user) |
512 else: | 492 else: |
513 print('Results will be emailed to: ' + options.email) | 493 print('Results will be emailed to: ' + options.email) |
514 | 494 |
515 # Send the patch. | 495 # Send the patch. |
516 options.send_patch(options) | 496 options.send_patch(options) |
517 print 'Patch \'%s\' sent to try server: %s' % (options.name, | 497 print 'Patch \'%s\' sent to try server: %s' % (options.name, |
518 ', '.join(options.bot)) | 498 ', '.join(options.bot)) |
519 except (InvalidScript, NoTryServerAccess), e: | 499 except (InvalidScript, NoTryServerAccess), e: |
520 if swallow_exception: | 500 if swallow_exception: |
521 return 1 | 501 return 1 |
522 print e | 502 print e |
523 return 1 | 503 return 1 |
524 return 0 | 504 return 0 |
525 | 505 |
526 | 506 |
527 if __name__ == "__main__": | 507 if __name__ == "__main__": |
528 sys.exit(TryChange(None, [], False)) | 508 sys.exit(TryChange(None, [], False)) |
OLD | NEW |