OLD | NEW |
1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | 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 | 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 | 5 |
6 """Client-side script to send a try job to the try server. It communicates to | 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 | 7 the try server by either writting to a svn/git repository or by directly |
8 connecting to the server by HTTP. | 8 connecting to the server by HTTP. |
9 """ | 9 """ |
10 | 10 |
11 import contextlib | 11 import contextlib |
12 import datetime | 12 import datetime |
13 import errno | 13 import errno |
14 import getpass | 14 import getpass |
15 import itertools | 15 import itertools |
16 import json | 16 import json |
17 import logging | 17 import logging |
18 import optparse | 18 import optparse |
19 import os | 19 import os |
20 import posixpath | 20 import posixpath |
21 import re | 21 import re |
22 import shutil | 22 import shutil |
23 import sys | 23 import sys |
24 import tempfile | 24 import tempfile |
25 import urllib | 25 import urllib |
26 import urllib2 | 26 import urllib2 |
| 27 import urlparse |
27 | 28 |
28 import breakpad # pylint: disable=W0611 | 29 import breakpad # pylint: disable=W0611 |
29 | 30 |
| 31 import fix_encoding |
30 import gcl | 32 import gcl |
31 import fix_encoding | |
32 import gclient_utils | 33 import gclient_utils |
| 34 import gerrit_util |
33 import scm | 35 import scm |
34 import subprocess2 | 36 import subprocess2 |
35 | 37 |
36 | 38 |
37 __version__ = '1.2' | 39 __version__ = '1.2' |
38 | 40 |
39 | 41 |
40 # Constants | 42 # Constants |
41 HELP_STRING = "Sorry, Tryserver is not available." | 43 HELP_STRING = "Sorry, Tryserver is not available." |
42 USAGE = r"""%prog [options] | 44 USAGE = r"""%prog [options] |
(...skipping 45 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
88 DieWithError( | 90 DieWithError( |
89 'Command "%s" failed.\n%s' % ( | 91 'Command "%s" failed.\n%s' % ( |
90 ' '.join(args), error_message or e.stdout or '')) | 92 ' '.join(args), error_message or e.stdout or '')) |
91 return e.stdout | 93 return e.stdout |
92 | 94 |
93 | 95 |
94 def RunGit(args, **kwargs): | 96 def RunGit(args, **kwargs): |
95 """Returns stdout.""" | 97 """Returns stdout.""" |
96 return RunCommand(['git'] + args, **kwargs) | 98 return RunCommand(['git'] + args, **kwargs) |
97 | 99 |
| 100 class Error(Exception): |
| 101 """An error during a try job submission. |
98 | 102 |
99 class InvalidScript(Exception): | 103 For this error, trychange.py does not display stack trace, only message |
| 104 """ |
| 105 |
| 106 class InvalidScript(Error): |
100 def __str__(self): | 107 def __str__(self): |
101 return self.args[0] + '\n' + HELP_STRING | 108 return self.args[0] + '\n' + HELP_STRING |
102 | 109 |
103 | 110 |
104 class NoTryServerAccess(Exception): | 111 class NoTryServerAccess(Error): |
105 def __str__(self): | 112 def __str__(self): |
106 return self.args[0] + '\n' + HELP_STRING | 113 return self.args[0] + '\n' + HELP_STRING |
107 | 114 |
108 | |
109 def Escape(name): | 115 def Escape(name): |
110 """Escapes characters that could interfere with the file system or try job | 116 """Escapes characters that could interfere with the file system or try job |
111 parsing. | 117 parsing. |
112 """ | 118 """ |
113 return re.sub(r'[^\w#-]', '_', name) | 119 return re.sub(r'[^\w#-]', '_', name) |
114 | 120 |
115 | 121 |
116 class SCM(object): | 122 class SCM(object): |
117 """Simplistic base class to implement one function: ProcessOptions.""" | 123 """Simplistic base class to implement one function: ProcessOptions.""" |
118 def __init__(self, options, path, file_list): | 124 def __init__(self, options, path, file_list): |
(...skipping 42 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
161 """Set default settings based on the gcl-style settings from the repository. | 167 """Set default settings based on the gcl-style settings from the repository. |
162 | 168 |
163 The settings in the self.options object will only be set if no previous | 169 The settings in the self.options object will only be set if no previous |
164 value exists (i.e. command line flags to the try command will override the | 170 value exists (i.e. command line flags to the try command will override the |
165 settings in codereview.settings). | 171 settings in codereview.settings). |
166 """ | 172 """ |
167 settings = { | 173 settings = { |
168 'port': self.GetCodeReviewSetting('TRYSERVER_HTTP_PORT'), | 174 'port': self.GetCodeReviewSetting('TRYSERVER_HTTP_PORT'), |
169 'host': self.GetCodeReviewSetting('TRYSERVER_HTTP_HOST'), | 175 'host': self.GetCodeReviewSetting('TRYSERVER_HTTP_HOST'), |
170 'svn_repo': self.GetCodeReviewSetting('TRYSERVER_SVN_URL'), | 176 'svn_repo': self.GetCodeReviewSetting('TRYSERVER_SVN_URL'), |
| 177 'gerrit_url': self.GetCodeReviewSetting('TRYSERVER_GERRIT_URL'), |
171 'git_repo': self.GetCodeReviewSetting('TRYSERVER_GIT_URL'), | 178 'git_repo': self.GetCodeReviewSetting('TRYSERVER_GIT_URL'), |
172 'project': self.GetCodeReviewSetting('TRYSERVER_PROJECT'), | 179 'project': self.GetCodeReviewSetting('TRYSERVER_PROJECT'), |
173 # Primarily for revision=auto | 180 # Primarily for revision=auto |
174 'revision': self.GetCodeReviewSetting('TRYSERVER_REVISION'), | 181 'revision': self.GetCodeReviewSetting('TRYSERVER_REVISION'), |
175 'root': self.GetCodeReviewSetting('TRYSERVER_ROOT'), | 182 'root': self.GetCodeReviewSetting('TRYSERVER_ROOT'), |
176 'patchlevel': self.GetCodeReviewSetting('TRYSERVER_PATCHLEVEL'), | 183 'patchlevel': self.GetCodeReviewSetting('TRYSERVER_PATCHLEVEL'), |
177 } | 184 } |
178 logging.info('\n'.join(['%s: %s' % (k, v) | 185 logging.info('\n'.join(['%s: %s' % (k, v) |
179 for (k, v) in settings.iteritems() if v])) | 186 for (k, v) in settings.iteritems() if v])) |
180 for (k, v) in settings.iteritems(): | 187 for (k, v) in settings.iteritems(): |
(...skipping 303 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
484 raise NoTryServerAccess('%s is unaccessible. Reason: %s' % (url, | 491 raise NoTryServerAccess('%s is unaccessible. Reason: %s' % (url, |
485 str(e.args))) | 492 str(e.args))) |
486 if not connection: | 493 if not connection: |
487 raise NoTryServerAccess('%s is unaccessible.' % url) | 494 raise NoTryServerAccess('%s is unaccessible.' % url) |
488 logging.info('Reading response...') | 495 logging.info('Reading response...') |
489 response = connection.read() | 496 response = connection.read() |
490 logging.info('Done') | 497 logging.info('Done') |
491 if response != 'OK': | 498 if response != 'OK': |
492 raise NoTryServerAccess('%s is unaccessible. Got:\n%s' % (url, response)) | 499 raise NoTryServerAccess('%s is unaccessible. Got:\n%s' % (url, response)) |
493 | 500 |
| 501 PrintSuccess(bot_spec, options) |
494 | 502 |
495 @contextlib.contextmanager | 503 @contextlib.contextmanager |
496 def _TempFilename(name, contents=None): | 504 def _TempFilename(name, contents=None): |
497 """Create a temporary directory, append the specified name and yield. | 505 """Create a temporary directory, append the specified name and yield. |
498 | 506 |
499 In contrast to NamedTemporaryFile, does not keep the file open. | 507 In contrast to NamedTemporaryFile, does not keep the file open. |
500 Deletes the file on __exit__. | 508 Deletes the file on __exit__. |
501 """ | 509 """ |
502 temp_dir = tempfile.mkdtemp(prefix=name) | 510 temp_dir = tempfile.mkdtemp(prefix=name) |
503 try: | 511 try: |
(...skipping 57 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
561 command = [exe, 'import', '-q', patch_dir, options.svn_repo, '--file', | 569 command = [exe, 'import', '-q', patch_dir, options.svn_repo, '--file', |
562 description_filename] | 570 description_filename] |
563 if scm.SVN.AssertVersion("1.5")[0]: | 571 if scm.SVN.AssertVersion("1.5")[0]: |
564 command.append('--no-ignore') | 572 command.append('--no-ignore') |
565 | 573 |
566 try: | 574 try: |
567 subprocess2.check_call(command) | 575 subprocess2.check_call(command) |
568 except subprocess2.CalledProcessError, e: | 576 except subprocess2.CalledProcessError, e: |
569 raise NoTryServerAccess(str(e)) | 577 raise NoTryServerAccess(str(e)) |
570 | 578 |
| 579 PrintSuccess(bot_spec, options) |
571 | 580 |
572 def _GetPatchGitRepo(git_url): | 581 def _GetPatchGitRepo(git_url): |
573 """Gets a path to a Git repo with patches. | 582 """Gets a path to a Git repo with patches. |
574 | 583 |
575 Stores patches in .git/git-try/patches-git directory, a git repo. If it | 584 Stores patches in .git/git-try/patches-git directory, a git repo. If it |
576 doesn't exist yet or its origin URL is different, cleans up and clones it. | 585 doesn't exist yet or its origin URL is different, cleans up and clones it. |
577 If it existed before, then pulls changes. | 586 If it existed before, then pulls changes. |
578 | 587 |
579 Does not support SVN repo. | 588 Does not support SVN repo. |
580 | 589 |
(...skipping 118 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
699 # Fetch, reset, update branch file again. | 708 # Fetch, reset, update branch file again. |
700 patch_git('fetch', 'origin') | 709 patch_git('fetch', 'origin') |
701 patch_git('reset', '--hard', 'origin/master') | 710 patch_git('reset', '--hard', 'origin/master') |
702 update_branch() | 711 update_branch() |
703 except subprocess2.CalledProcessError, e: | 712 except subprocess2.CalledProcessError, e: |
704 # Restore state. | 713 # Restore state. |
705 patch_git('checkout', 'master') | 714 patch_git('checkout', 'master') |
706 patch_git('reset', '--hard', 'origin/master') | 715 patch_git('reset', '--hard', 'origin/master') |
707 raise | 716 raise |
708 | 717 |
| 718 PrintSuccess(bot_spec, options) |
| 719 |
| 720 def _SendChangeGerrit(bot_spec, options): |
| 721 """Posts a try job to a Gerrit change. |
| 722 |
| 723 Reads Change-Id from the HEAD commit, resolves the current revision, checks |
| 724 that local revision matches the uploaded one, posts a try job in form of a |
| 725 message, sets Tryjob label to 1. |
| 726 |
| 727 Gerrit message format: starts with !tryjob, optionally followed by a tryjob |
| 728 definition in JSON format: |
| 729 buildNames: list of strings specifying build names. |
| 730 """ |
| 731 |
| 732 logging.info('Sending by Gerrit') |
| 733 if not options.gerrit_url: |
| 734 raise NoTryServerAccess('Please use --gerrit_url option to specify the ' |
| 735 'Gerrit instance url to connect to') |
| 736 gerrit_host = urlparse.urlparse(options.gerrit_url).hostname |
| 737 logging.debug('Gerrit host: %s' % gerrit_host) |
| 738 |
| 739 def GetChangeId(commmitish): |
| 740 """Finds Change-ID of the HEAD commit.""" |
| 741 CHANGE_ID_RGX = '^Change-Id: (I[a-f0-9]{10,})' |
| 742 comment = scm.GIT.Capture(['log', '-1', commmitish, '--format=%b'], |
| 743 cwd=os.getcwd()) |
| 744 change_id_match = re.search(CHANGE_ID_RGX, comment, re.I | re.M) |
| 745 if not change_id_match: |
| 746 raise Error('Change-Id was not found in the HEAD commit. Make sure you ' |
| 747 'have a Git hook installed that generates and inserts a ' |
| 748 'Change-Id into a commit message automatically.') |
| 749 change_id = change_id_match.group(1) |
| 750 return change_id |
| 751 |
| 752 def FormatMessage(): |
| 753 # Build job definition. |
| 754 job_def = {} |
| 755 builderNames = [builder for builder, _ in bot_spec] |
| 756 if builderNames: |
| 757 job_def['builderNames'] = builderNames |
| 758 |
| 759 # Format message. |
| 760 msg = '!tryjob' |
| 761 if job_def: |
| 762 msg = '%s %s' % (msg, json.dumps(job_def, sort_keys=True)) |
| 763 return msg |
| 764 |
| 765 def PostTryjob(message): |
| 766 logging.info('Posting gerrit message: %s' % message) |
| 767 if not options.dry_run: |
| 768 # Post a message and set TryJob=1 label. |
| 769 try: |
| 770 gerrit_util.SetReview(gerrit_host, change_id, msg=message, |
| 771 labels={'Tryjob': 1}) |
| 772 except gerrit_util.GerritError, e: |
| 773 if e.http_status == 400: |
| 774 raise Error(e.reason) |
| 775 else: |
| 776 raise |
| 777 |
| 778 head_sha = scm.GIT.Capture(['log', '-1', '--format=%H'], cwd=os.getcwd()) |
| 779 |
| 780 change_id = GetChangeId(head_sha) |
| 781 |
| 782 # Check that the uploaded revision matches the local one. |
| 783 changes = gerrit_util.GetChangeCurrentRevision(gerrit_host, change_id) |
| 784 assert len(changes) <= 1, 'Multiple changes with id %s' % change_id |
| 785 if not changes: |
| 786 raise Error('A change %s was not found on the server. Was it uploaded?' % |
| 787 change_id) |
| 788 logging.debug('Found Gerrit change: %s' % changes[0]) |
| 789 if changes[0]['current_revision'] != head_sha: |
| 790 raise Error('Please upload your latest local changes to Gerrit.') |
| 791 |
| 792 # Post a try job. |
| 793 message = FormatMessage() |
| 794 PostTryjob(message) |
| 795 change_url = urlparse.urljoin(options.gerrit_url, |
| 796 '/#/c/%s' % changes[0]['_number']) |
| 797 print('A tryjob was posted on change %s' % change_url) |
709 | 798 |
710 def PrintSuccess(bot_spec, options): | 799 def PrintSuccess(bot_spec, options): |
711 if not options.dry_run: | 800 if not options.dry_run: |
712 text = 'Patch \'%s\' sent to try server' % options.name | 801 text = 'Patch \'%s\' sent to try server' % options.name |
713 if bot_spec: | 802 if bot_spec: |
714 text += ': %s' % ', '.join( | 803 text += ': %s' % ', '.join( |
715 '%s:%s' % (b[0], ','.join(b[1])) for b in bot_spec) | 804 '%s:%s' % (b[0], ','.join(b[1])) for b in bot_spec) |
716 print(text) | 805 print(text) |
717 | 806 |
718 | 807 |
(...skipping 194 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
913 group.add_option("--use_git", | 1002 group.add_option("--use_git", |
914 action="store_const", | 1003 action="store_const", |
915 const=_SendChangeGit, | 1004 const=_SendChangeGit, |
916 dest="send_patch", | 1005 dest="send_patch", |
917 help="Use GIT to talk to the try server") | 1006 help="Use GIT to talk to the try server") |
918 group.add_option("-G", "--git_repo", | 1007 group.add_option("-G", "--git_repo", |
919 metavar="GIT_URL", | 1008 metavar="GIT_URL", |
920 help="GIT url to use to write the changes in; --use_git is " | 1009 help="GIT url to use to write the changes in; --use_git is " |
921 "implied when using --git_repo") | 1010 "implied when using --git_repo") |
922 parser.add_option_group(group) | 1011 parser.add_option_group(group) |
| 1012 |
| 1013 group = optparse.OptionGroup(parser, "Access the try server with Gerrit") |
| 1014 group.add_option("--use_gerrit", |
| 1015 action="store_const", |
| 1016 const=_SendChangeGerrit, |
| 1017 dest="send_patch", |
| 1018 help="Use Gerrit to talk to the try server") |
| 1019 group.add_option("--gerrit_url", |
| 1020 metavar="GERRIT_URL", |
| 1021 help="Gerrit url to post a tryjob to; --use_gerrit is " |
| 1022 "implied when using --gerrit_url") |
| 1023 parser.add_option_group(group) |
| 1024 |
923 return parser | 1025 return parser |
924 | 1026 |
925 | 1027 |
926 def TryChange(argv, | 1028 def TryChange(argv, |
927 change, | 1029 change, |
928 swallow_exception, | 1030 swallow_exception, |
929 prog=None, | 1031 prog=None, |
930 extra_epilog=None): | 1032 extra_epilog=None): |
931 """ | 1033 """ |
932 Args: | 1034 Args: |
(...skipping 93 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1026 os.path.join(current_vcs.checkout_root, item), | 1128 os.path.join(current_vcs.checkout_root, item), |
1027 None) | 1129 None) |
1028 if checkout.checkout_root in [c.checkout_root for c in checkouts]: | 1130 if checkout.checkout_root in [c.checkout_root for c in checkouts]: |
1029 parser.error('Specified the root %s two times.' % | 1131 parser.error('Specified the root %s two times.' % |
1030 checkout.checkout_root) | 1132 checkout.checkout_root) |
1031 checkouts.append(checkout) | 1133 checkouts.append(checkout) |
1032 | 1134 |
1033 can_http = options.port and options.host | 1135 can_http = options.port and options.host |
1034 can_svn = options.svn_repo | 1136 can_svn = options.svn_repo |
1035 can_git = options.git_repo | 1137 can_git = options.git_repo |
| 1138 can_gerrit = options.gerrit_url |
| 1139 can_something = can_http or can_svn or can_git or can_gerrit |
1036 # If there was no transport selected yet, now we must have enough data to | 1140 # If there was no transport selected yet, now we must have enough data to |
1037 # select one. | 1141 # select one. |
1038 if not options.send_patch and not (can_http or can_svn or can_git): | 1142 if not options.send_patch and not can_something: |
1039 parser.error('Please specify an access method.') | 1143 parser.error('Please specify an access method.') |
1040 | 1144 |
1041 # Convert options.diff into the content of the diff. | 1145 # Convert options.diff into the content of the diff. |
1042 if options.url: | 1146 if options.url: |
1043 if options.files: | 1147 if options.files: |
1044 parser.error('You cannot specify files and --url at the same time.') | 1148 parser.error('You cannot specify files and --url at the same time.') |
1045 options.diff = urllib2.urlopen(options.url).read() | 1149 options.diff = urllib2.urlopen(options.url).read() |
1046 elif options.diff: | 1150 elif options.diff: |
1047 if options.files: | 1151 if options.files: |
1048 parser.error('You cannot specify files and --diff at the same time.') | 1152 parser.error('You cannot specify files and --diff at the same time.') |
(...skipping 66 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1115 | 1219 |
1116 # Determine sending protocol | 1220 # Determine sending protocol |
1117 if options.send_patch: | 1221 if options.send_patch: |
1118 # If forced. | 1222 # If forced. |
1119 senders = [options.send_patch] | 1223 senders = [options.send_patch] |
1120 else: | 1224 else: |
1121 # Try sending patch using avaialble protocols | 1225 # Try sending patch using avaialble protocols |
1122 all_senders = [ | 1226 all_senders = [ |
1123 (_SendChangeHTTP, can_http), | 1227 (_SendChangeHTTP, can_http), |
1124 (_SendChangeSVN, can_svn), | 1228 (_SendChangeSVN, can_svn), |
1125 (_SendChangeGit, can_git) | 1229 (_SendChangeGerrit, can_gerrit), |
| 1230 (_SendChangeGit, can_git), |
1126 ] | 1231 ] |
1127 senders = [sender for sender, can in all_senders if can] | 1232 senders = [sender for sender, can in all_senders if can] |
1128 | 1233 |
1129 # Send the patch. | 1234 # Send the patch. |
1130 for sender in senders: | 1235 for sender in senders: |
1131 try: | 1236 try: |
1132 sender(bot_spec, options) | 1237 sender(bot_spec, options) |
1133 PrintSuccess(bot_spec, options) | |
1134 return 0 | 1238 return 0 |
1135 except NoTryServerAccess: | 1239 except NoTryServerAccess: |
1136 is_last = sender == senders[-1] | 1240 is_last = sender == senders[-1] |
1137 if is_last: | 1241 if is_last: |
1138 raise | 1242 raise |
1139 assert False, "Unreachable code" | 1243 assert False, "Unreachable code" |
1140 except (InvalidScript, NoTryServerAccess), e: | 1244 except Error, e: |
1141 if swallow_exception: | 1245 if swallow_exception: |
1142 return 1 | 1246 return 1 |
1143 print >> sys.stderr, e | 1247 print >> sys.stderr, e |
1144 return 1 | 1248 return 1 |
1145 except (gclient_utils.Error, subprocess2.CalledProcessError), e: | 1249 except (gclient_utils.Error, subprocess2.CalledProcessError), e: |
1146 print >> sys.stderr, e | 1250 print >> sys.stderr, e |
1147 return 1 | 1251 return 1 |
1148 return 0 | 1252 return 0 |
1149 | 1253 |
1150 | 1254 |
1151 if __name__ == "__main__": | 1255 if __name__ == "__main__": |
1152 fix_encoding.fix_encoding() | 1256 fix_encoding.fix_encoding() |
1153 sys.exit(TryChange(None, None, False)) | 1257 sys.exit(TryChange(None, None, False)) |
OLD | NEW |