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 import breakpad |
23 | 23 |
24 import gcl | 24 import gcl |
25 import gclient_utils | 25 import gclient_utils |
26 import scm | 26 import scm |
27 import presubmit_support | 27 import presubmit_support |
28 import upload | |
29 | 28 |
30 __version__ = '1.1.2' | 29 __version__ = '1.2' |
31 | 30 |
32 | 31 |
33 # Constants | 32 # Constants |
34 HELP_STRING = "Sorry, Tryserver is not available." | 33 HELP_STRING = "Sorry, Tryserver is not available." |
35 USAGE = r"""%prog [change_name] [options] | 34 USAGE = r"""%prog [change_name] [options] |
36 | 35 |
37 Client-side script to send a try job to the try server. It communicates to | 36 Client-side script to send a try job to the try server. It communicates to |
38 the try server by either writting to a svn repository or by directly connecting | 37 the try server by either writting to a svn repository or by directly connecting |
39 to the server by HTTP. | 38 to the server by HTTP. |
40 | 39 |
(...skipping 18 matching lines...) Expand all Loading... |
59 class InvalidScript(Exception): | 58 class InvalidScript(Exception): |
60 def __str__(self): | 59 def __str__(self): |
61 return self.args[0] + '\n' + HELP_STRING | 60 return self.args[0] + '\n' + HELP_STRING |
62 | 61 |
63 | 62 |
64 class NoTryServerAccess(Exception): | 63 class NoTryServerAccess(Exception): |
65 def __str__(self): | 64 def __str__(self): |
66 return self.args[0] + '\n' + HELP_STRING | 65 return self.args[0] + '\n' + HELP_STRING |
67 | 66 |
68 | 67 |
69 def PathDifference(root, subpath): | |
70 """Returns the difference subpath minus root.""" | |
71 if subpath.find(root) != 0: | |
72 return None | |
73 # If the root does not have a trailing \ or /, we add it so the returned path | |
74 # starts immediately after the seperator regardless of whether it is provided. | |
75 if not root.endswith(os.sep): | |
76 root += os.sep | |
77 return subpath[len(root):] | |
78 | |
79 | |
80 def GetSourceRoot(): | |
81 """Returns the absolute directory one level up from the repository root.""" | |
82 # TODO(maruel): This is odd to assume that '..' is the source root. | |
83 return os.path.abspath(os.path.join(gcl.GetRepositoryRoot(), '..')) | |
84 | |
85 | |
86 def GetTryServerSettings(): | 68 def GetTryServerSettings(): |
87 """Grab try server settings local to the repository.""" | 69 """Grab try server settings local to the repository.""" |
88 def _SafeResolve(host): | 70 def _SafeResolve(host): |
89 try: | 71 try: |
90 return socket.getaddrinfo(host, None) | 72 return socket.getaddrinfo(host, None) |
91 except socket.gaierror: | 73 except socket.gaierror: |
92 return None | 74 return None |
93 | 75 |
94 settings = {} | 76 settings = {} |
95 settings['http_port'] = gcl.GetCodeReviewSetting('TRYSERVER_HTTP_PORT') | 77 settings['http_port'] = gcl.GetCodeReviewSetting('TRYSERVER_HTTP_PORT') |
(...skipping 21 matching lines...) Expand all Loading... |
117 | 99 |
118 def EscapeDot(name): | 100 def EscapeDot(name): |
119 return name.replace('.', '-') | 101 return name.replace('.', '-') |
120 | 102 |
121 | 103 |
122 class SCM(object): | 104 class SCM(object): |
123 """Simplistic base class to implement one function: ProcessOptions.""" | 105 """Simplistic base class to implement one function: ProcessOptions.""" |
124 def __init__(self, options): | 106 def __init__(self, options): |
125 self.options = options | 107 self.options = options |
126 | 108 |
127 def ProcessOptions(self): | 109 def GetFileNames(self): |
128 raise NotImplementedError | 110 """Return the list of files in the diff.""" |
| 111 return self.options.files |
129 | 112 |
130 | 113 |
131 class SVN(SCM): | 114 class SVN(SCM): |
132 """Gathers the options and diff for a subversion checkout.""" | 115 """Gathers the options and diff for a subversion checkout.""" |
133 def GenerateDiff(self, files, root): | 116 def __init__(self, *args, **kwargs): |
| 117 SCM.__init__(self, *args, **kwargs) |
| 118 self.checkout_root = scm.SVN.GetCheckoutRoot(os.getcwd()) |
| 119 self.options.files |
| 120 if not self.options.diff: |
| 121 # Generate the diff from the scm. |
| 122 self.options.diff = self._GenerateDiff() |
| 123 if not self.options.email: |
| 124 # Assumes the svn credential is an email address. |
| 125 self.options.email = scm.SVN.GetEmail(self.checkout_root) |
| 126 |
| 127 def _GenerateDiff(self): |
134 """Returns a string containing the diff for the given file list. | 128 """Returns a string containing the diff for the given file list. |
135 | 129 |
136 The files in the list should either be absolute paths or relative to the | 130 The files in the list should either be absolute paths or relative to the |
137 given root. If no root directory is provided, the repository root will be | 131 given root. |
138 used. | |
139 """ | 132 """ |
140 previous_cwd = os.getcwd() | 133 previous_cwd = os.getcwd() |
141 os.chdir(root or scm.SVN.GetCheckoutRoot(previous_cwd)) | 134 os.chdir(self.checkout_root) |
142 | 135 if not self.options.files: |
| 136 self.options.files = [f[1] for f in scm.SVN.CaptureStatus(None)] |
143 # Directories will return None so filter them out. | 137 # Directories will return None so filter them out. |
144 diff = filter(None, [scm.SVN.DiffItem(f) for f in files]) | 138 diff = filter(None, [scm.SVN.DiffItem(f) for f in self.options.files]) |
145 os.chdir(previous_cwd) | 139 os.chdir(previous_cwd) |
146 return "".join(diff) | 140 return "".join(diff) |
147 | 141 |
148 def GetFileNames(self): | |
149 """Return the list of files in the diff.""" | |
150 return self.change_info.GetFileNames() | |
151 | |
152 def GetLocalRoot(self): | 142 def GetLocalRoot(self): |
153 """Return the path of the repository root.""" | 143 """Return the path of the repository root.""" |
154 return self.change_info.GetLocalRoot() | 144 return self.checkout_root |
155 | |
156 def ProcessOptions(self): | |
157 checkout_root = None | |
158 if not self.options.diff: | |
159 # Generate the diff with svn and write it to the submit queue path. The | |
160 # files are relative to the repository root, but we need patches relative | |
161 # to one level up from there (i.e., 'src'), so adjust both the file | |
162 # paths and the root of the diff. | |
163 # TODO(maruel): Remove this hack. | |
164 source_root = GetSourceRoot() | |
165 checkout_root = scm.SVN.GetCheckoutRoot(os.getcwd()) | |
166 prefix = PathDifference(source_root, checkout_root) | |
167 adjusted_paths = [os.path.join(prefix, x) for x in self.options.files] | |
168 self.options.diff = self.GenerateDiff(adjusted_paths, root=source_root) | |
169 self.change_info = gcl.LoadChangelistInfoForMultiple(self.options.name, | |
170 gcl.GetRepositoryRoot(), True, True) | |
171 if not self.options.email: | |
172 checkout_root = checkout_root or scm.SVN.GetCheckoutRoot(os.getcwd()) | |
173 self.options.email = scm.SVN.GetEmail(checkout_root) | |
174 | 145 |
175 | 146 |
176 class GIT(SCM): | 147 class GIT(SCM): |
177 """Gathers the options and diff for a git checkout.""" | 148 """Gathers the options and diff for a git checkout.""" |
178 def GenerateDiff(self): | 149 def __init__(self, *args, **kwargs): |
| 150 SCM.__init__(self, *args, **kwargs) |
| 151 self.checkout_root = os.path.abspath( |
| 152 gclient_utils.CheckCall(['git', 'rev-parse', '--show-cdup']).strip()) |
| 153 if not self.options.diff: |
| 154 self.options.diff = self._GenerateDiff() |
| 155 if not self.options.name: |
| 156 self.options.name = self._GetPatchName() |
| 157 if not self.options.email: |
| 158 self.options.email = scm.GIT.GetEmail('.') |
| 159 |
| 160 def _GenerateDiff(self): |
179 """Get the diff we'll send to the try server. We ignore the files list.""" | 161 """Get the diff we'll send to the try server. We ignore the files list.""" |
180 branch = upload.RunShell(['git', 'cl', 'upstream']).strip() | 162 branch = gclient_utils.CheckCall(['git', 'cl', 'upstream']).strip() |
181 diff = upload.RunShell(['git', 'diff-tree', '-p', '--no-prefix', | 163 diff = gclient_utils.CheckCall(['git', 'diff-tree', '-p', '--no-prefix', |
182 branch, 'HEAD']).splitlines(True) | 164 branch, 'HEAD']).splitlines(True) |
183 for i in range(len(diff)): | 165 for i in range(len(diff)): |
184 # In the case of added files, replace /dev/null with the path to the | 166 # In the case of added files, replace /dev/null with the path to the |
185 # file being added. | 167 # file being added. |
186 if diff[i].startswith('--- /dev/null'): | 168 if diff[i].startswith('--- /dev/null'): |
187 diff[i] = '--- %s' % diff[i+1][4:] | 169 diff[i] = '--- %s' % diff[i+1][4:] |
188 return ''.join(diff) | 170 return ''.join(diff) |
189 | 171 |
190 def GetFileNames(self): | 172 def _GetPatchName(self): |
191 """Return the list of files in the diff.""" | |
192 return self.options.files | |
193 | |
194 def GetLocalRoot(self): | |
195 """Return the path of the repository root.""" | |
196 # TODO: check for errors here? | |
197 root = upload.RunShell(['git', 'rev-parse', '--show-cdup']).strip() | |
198 return os.path.abspath(root) | |
199 | |
200 def GetPatchName(self): | |
201 """Construct a name for this patch.""" | 173 """Construct a name for this patch.""" |
202 # TODO: perhaps include the hash of the current commit, to distinguish | 174 # TODO: perhaps include the hash of the current commit, to distinguish |
203 # patches? | 175 # patches? |
204 branch = upload.RunShell(['git', 'symbolic-ref', 'HEAD']).strip() | 176 branch = gclient_utils.CheckCall(['git', 'symbolic-ref', 'HEAD']).strip() |
205 if not branch.startswith('refs/heads/'): | 177 if not branch.startswith('refs/heads/'): |
206 # TODO(maruel): Find a better type. | 178 # TODO(maruel): Find a better type. |
207 raise NoTryServerAccess("Couldn't figure out branch name") | 179 raise NoTryServerAccess("Couldn't figure out branch name") |
208 branch = branch[len('refs/heads/'):] | 180 branch = branch[len('refs/heads/'):] |
209 return branch | 181 return branch |
210 | 182 |
211 def ProcessOptions(self): | 183 def GetLocalRoot(self): |
212 if not self.options.diff: | 184 """Return the path of the repository root.""" |
213 self.options.diff = self.GenerateDiff() | 185 return self.checkout_root |
214 if not self.options.name: | |
215 self.options.name = self.GetPatchName() | |
216 if not self.options.email: | |
217 self.options.email = scm.GIT.GetEmail('.') | |
218 | 186 |
219 | 187 |
220 def _ParseSendChangeOptions(options): | 188 def _ParseSendChangeOptions(options): |
221 """Parse common options passed to _SendChangeHTTP and _SendChangeSVN.""" | 189 """Parse common options passed to _SendChangeHTTP and _SendChangeSVN.""" |
222 values = {} | 190 values = {} |
223 if options.email: | 191 if options.email: |
224 values['email'] = options.email | 192 values['email'] = options.email |
225 values['user'] = options.user | 193 values['user'] = options.user |
226 values['name'] = options.name | 194 values['name'] = options.name |
227 if options.bot: | 195 if options.bot: |
(...skipping 130 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
358 """ | 326 """ |
359 __pychecker__ = 'no-returnvalues' | 327 __pychecker__ = 'no-returnvalues' |
360 # Subversion has a .svn in all working directories. | 328 # Subversion has a .svn in all working directories. |
361 if os.path.isdir('.svn'): | 329 if os.path.isdir('.svn'): |
362 logging.info("Guessed VCS = Subversion") | 330 logging.info("Guessed VCS = Subversion") |
363 return SVN(options) | 331 return SVN(options) |
364 | 332 |
365 # Git has a command to test if you're in a git tree. | 333 # Git has a command to test if you're in a git tree. |
366 # Try running it, but don't die if we don't have git installed. | 334 # Try running it, but don't die if we don't have git installed. |
367 try: | 335 try: |
368 out, returncode = subprocess.Popen( | 336 gclient_utils.CheckCall(["git", "rev-parse", "--is-inside-work-tree"]) |
369 ["git", "rev-parse", "--is-inside-work-tree"], | 337 logging.info("Guessed VCS = Git") |
370 shell=sys.platform.startswith('win'), | 338 return GIT(options) |
371 stdout=subprocess.PIPE).communicate()[0] | 339 except gclient_utils.CheckCallError, e: |
372 if returncode == 0: | 340 if e.retcode != 2: # ENOENT -- they don't have git installed. |
373 logging.info("Guessed VCS = Git") | |
374 return GIT(options) | |
375 except OSError, (errno, message): | |
376 if errno != 2: # ENOENT -- they don't have git installed. | |
377 raise | 341 raise |
378 | |
379 raise NoTryServerAccess("Could not guess version control system. " | 342 raise NoTryServerAccess("Could not guess version control system. " |
380 "Are you in a working copy directory?") | 343 "Are you in a working copy directory?") |
381 | 344 |
382 | 345 |
383 def TryChange(argv, | 346 def TryChange(argv, |
384 file_list, | 347 file_list, |
385 swallow_exception, | 348 swallow_exception, |
386 prog=None): | 349 prog=None): |
387 """ | 350 """ |
388 Args: | 351 Args: |
(...skipping 124 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
513 | 476 |
514 try: | 477 try: |
515 # Convert options.diff into the content of the diff. | 478 # Convert options.diff into the content of the diff. |
516 if options.url: | 479 if options.url: |
517 options.diff = urllib.urlopen(options.url).read() | 480 options.diff = urllib.urlopen(options.url).read() |
518 elif options.diff: | 481 elif options.diff: |
519 options.diff = gclient_utils.FileRead(options.diff, 'rb') | 482 options.diff = gclient_utils.FileRead(options.diff, 'rb') |
520 # Process the VCS in any case at least to retrieve the email address. | 483 # Process the VCS in any case at least to retrieve the email address. |
521 try: | 484 try: |
522 options.scm = GuessVCS(options) | 485 options.scm = GuessVCS(options) |
523 options.scm.ProcessOptions() | |
524 except NoTryServerAccess, e: | 486 except NoTryServerAccess, e: |
525 # If we got the diff, we don't care. | 487 # If we got the diff, we don't care. |
526 if not options.diff: | 488 if not options.diff: |
527 # TODO(maruel): Raise what? | 489 # TODO(maruel): Raise what? |
528 raise | 490 raise |
529 | 491 |
530 # Get try slaves from PRESUBMIT.py files if not specified. | 492 # Get try slaves from PRESUBMIT.py files if not specified. |
531 if not options.bot: | 493 if not options.bot: |
532 if options.url: | 494 if options.url: |
533 parser.error('You need to specify which bots to use.') | 495 parser.error('You need to specify which bots to use.') |
(...skipping 23 matching lines...) Expand all Loading... |
557 except (InvalidScript, NoTryServerAccess), e: | 519 except (InvalidScript, NoTryServerAccess), e: |
558 if swallow_exception: | 520 if swallow_exception: |
559 return 1 | 521 return 1 |
560 print e | 522 print e |
561 return 1 | 523 return 1 |
562 return 0 | 524 return 0 |
563 | 525 |
564 | 526 |
565 if __name__ == "__main__": | 527 if __name__ == "__main__": |
566 sys.exit(TryChange(None, [], False)) | 528 sys.exit(TryChange(None, [], False)) |
OLD | NEW |