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

Side by Side Diff: depot_tools/upload.py

Issue 92087: Create the Next Generation of depot_tools. Eh. (Closed) Base URL: svn://chrome-svn.corp.google.com/chrome/trunk/tools/
Patch Set: Created 11 years, 8 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 | « depot_tools/trychange.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')
Property Changes:
Added: svn:executable
+ *
Added: svn:eol-style
+ LF
OLDNEW
(Empty)
1 #!/usr/bin/env python
2 #
3 # Copyright 2007 Google Inc.
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
16
17 """Tool for uploading diffs from a version control system to the codereview app.
18
19 Usage summary: upload.py [options] [-- diff_options]
20
21 Diff options are passed to the diff command of the underlying system.
22
23 Supported version control systems:
24 Git
25 Mercurial
26 Subversion
27
28 It is important for Git/Mercurial users to specify a tree/node/branch to diff
29 against by using the '--rev' option.
30 """
31 # This code is derived from appcfg.py in the App Engine SDK (open source),
32 # and from ASPN recipe #146306.
33
34 import cookielib
35 import getpass
36 import logging
37 import md5
38 import mimetypes
39 import optparse
40 import os
41 import re
42 import socket
43 import subprocess
44 import sys
45 import urllib
46 import urllib2
47 import urlparse
48
49 try:
50 import readline
51 except ImportError:
52 pass
53
54 # The logging verbosity:
55 # 0: Errors only.
56 # 1: Status messages.
57 # 2: Info logs.
58 # 3: Debug logs.
59 verbosity = 1
60
61 # Max size of patch or base file.
62 MAX_UPLOAD_SIZE = 900 * 1024
63
64
65 def GetEmail():
66 """Prompts the user for their email address and returns it.
67
68 The last used email address is saved to a file and offered up as a suggestion
69 to the user. If the user presses enter without typing in anything the last
70 used email address is used. If the user enters a new address, it is saved
71 for next time we prompt.
72
73 """
74 last_email_file_name = os.path.expanduser("~/.last_codereview_email_address")
75 last_email = ""
76 prompt = "Email: "
77 if os.path.exists(last_email_file_name):
78 try:
79 last_email_file = open(last_email_file_name, "r")
80 last_email = last_email_file.readline().strip("\n")
81 last_email_file.close()
82 prompt = "Email [%s]: " % last_email
83 except IOError, e:
84 pass
85 email = raw_input(prompt).strip()
86 if email:
87 try:
88 last_email_file = open(last_email_file_name, "w")
89 last_email_file.write(email)
90 last_email_file.close()
91 except IOError, e:
92 pass
93 else:
94 email = last_email
95 return email
96
97
98 def StatusUpdate(msg):
99 """Print a status message to stdout.
100
101 If 'verbosity' is greater than 0, print the message.
102
103 Args:
104 msg: The string to print.
105 """
106 if verbosity > 0:
107 print msg
108
109
110 def ErrorExit(msg):
111 """Print an error message to stderr and exit."""
112 print >>sys.stderr, msg
113 sys.exit(1)
114
115
116 class ClientLoginError(urllib2.HTTPError):
117 """Raised to indicate there was an error authenticating with ClientLogin."""
118
119 def __init__(self, url, code, msg, headers, args):
120 urllib2.HTTPError.__init__(self, url, code, msg, headers, None)
121 self.args = args
122 self.reason = args["Error"]
123
124
125 class AbstractRpcServer(object):
126 """Provides a common interface for a simple RPC server."""
127
128 def __init__(self, host, auth_function, host_override=None, extra_headers={},
129 save_cookies=False):
130 """Creates a new HttpRpcServer.
131
132 Args:
133 host: The host to send requests to.
134 auth_function: A function that takes no arguments and returns an
135 (email, password) tuple when called. Will be called if authentication
136 is required.
137 host_override: The host header to send to the server (defaults to host).
138 extra_headers: A dict of extra headers to append to every request.
139 save_cookies: If True, save the authentication cookies to local disk.
140 If False, use an in-memory cookiejar instead. Subclasses must
141 implement this functionality. Defaults to False.
142 """
143 self.host = host
144 self.host_override = host_override
145 self.auth_function = auth_function
146 self.authenticated = False
147 self.extra_headers = extra_headers
148 self.save_cookies = save_cookies
149 self.opener = self._GetOpener()
150 if self.host_override:
151 logging.info("Server: %s; Host: %s", self.host, self.host_override)
152 else:
153 logging.info("Server: %s", self.host)
154
155 def _GetOpener(self):
156 """Returns an OpenerDirector for making HTTP requests.
157
158 Returns:
159 A urllib2.OpenerDirector object.
160 """
161 raise NotImplementedError()
162
163 def _CreateRequest(self, url, data=None):
164 """Creates a new urllib request."""
165 logging.debug("Creating request for: '%s' with payload:\n%s", url, data)
166 req = urllib2.Request(url, data=data)
167 if self.host_override:
168 req.add_header("Host", self.host_override)
169 for key, value in self.extra_headers.iteritems():
170 req.add_header(key, value)
171 return req
172
173 def _GetAuthToken(self, email, password):
174 """Uses ClientLogin to authenticate the user, returning an auth token.
175
176 Args:
177 email: The user's email address
178 password: The user's password
179
180 Raises:
181 ClientLoginError: If there was an error authenticating with ClientLogin.
182 HTTPError: If there was some other form of HTTP error.
183
184 Returns:
185 The authentication token returned by ClientLogin.
186 """
187 account_type = "GOOGLE"
188 if self.host.endswith(".google.com"):
189 # Needed for use inside Google.
190 account_type = "HOSTED"
191 req = self._CreateRequest(
192 url="https://www.google.com/accounts/ClientLogin",
193 data=urllib.urlencode({
194 "Email": email,
195 "Passwd": password,
196 "service": "ah",
197 "source": "rietveld-codereview-upload",
198 "accountType": account_type,
199 }),
200 )
201 try:
202 response = self.opener.open(req)
203 response_body = response.read()
204 response_dict = dict(x.split("=")
205 for x in response_body.split("\n") if x)
206 return response_dict["Auth"]
207 except urllib2.HTTPError, e:
208 if e.code == 403:
209 body = e.read()
210 response_dict = dict(x.split("=", 1) for x in body.split("\n") if x)
211 raise ClientLoginError(req.get_full_url(), e.code, e.msg,
212 e.headers, response_dict)
213 else:
214 raise
215
216 def _GetAuthCookie(self, auth_token):
217 """Fetches authentication cookies for an authentication token.
218
219 Args:
220 auth_token: The authentication token returned by ClientLogin.
221
222 Raises:
223 HTTPError: If there was an error fetching the authentication cookies.
224 """
225 # This is a dummy value to allow us to identify when we're successful.
226 continue_location = "http://localhost/"
227 args = {"continue": continue_location, "auth": auth_token}
228 req = self._CreateRequest("http://%s/_ah/login?%s" %
229 (self.host, urllib.urlencode(args)))
230 try:
231 response = self.opener.open(req)
232 except urllib2.HTTPError, e:
233 response = e
234 if (response.code != 302 or
235 response.info()["location"] != continue_location):
236 raise urllib2.HTTPError(req.get_full_url(), response.code, response.msg,
237 response.headers, response.fp)
238 self.authenticated = True
239
240 def _Authenticate(self):
241 """Authenticates the user.
242
243 The authentication process works as follows:
244 1) We get a username and password from the user
245 2) We use ClientLogin to obtain an AUTH token for the user
246 (see http://code.google.com/apis/accounts/AuthForInstalledApps.html).
247 3) We pass the auth token to /_ah/login on the server to obtain an
248 authentication cookie. If login was successful, it tries to redirect
249 us to the URL we provided.
250
251 If we attempt to access the upload API without first obtaining an
252 authentication cookie, it returns a 401 response and directs us to
253 authenticate ourselves with ClientLogin.
254 """
255 for i in range(3):
256 credentials = self.auth_function()
257 try:
258 auth_token = self._GetAuthToken(credentials[0], credentials[1])
259 except ClientLoginError, e:
260 if e.reason == "BadAuthentication":
261 print >>sys.stderr, "Invalid username or password."
262 continue
263 if e.reason == "CaptchaRequired":
264 print >>sys.stderr, (
265 "Please go to\n"
266 "https://www.google.com/accounts/DisplayUnlockCaptcha\n"
267 "and verify you are a human. Then try again.")
268 break
269 if e.reason == "NotVerified":
270 print >>sys.stderr, "Account not verified."
271 break
272 if e.reason == "TermsNotAgreed":
273 print >>sys.stderr, "User has not agreed to TOS."
274 break
275 if e.reason == "AccountDeleted":
276 print >>sys.stderr, "The user account has been deleted."
277 break
278 if e.reason == "AccountDisabled":
279 print >>sys.stderr, "The user account has been disabled."
280 break
281 if e.reason == "ServiceDisabled":
282 print >>sys.stderr, ("The user's access to the service has been "
283 "disabled.")
284 break
285 if e.reason == "ServiceUnavailable":
286 print >>sys.stderr, "The service is not available; try again later."
287 break
288 raise
289 self._GetAuthCookie(auth_token)
290 return
291
292 def Send(self, request_path, payload=None,
293 content_type="application/octet-stream",
294 timeout=None,
295 **kwargs):
296 """Sends an RPC and returns the response.
297
298 Args:
299 request_path: The path to send the request to, eg /api/appversion/create.
300 payload: The body of the request, or None to send an empty request.
301 content_type: The Content-Type header to use.
302 timeout: timeout in seconds; default None i.e. no timeout.
303 (Note: for large requests on OS X, the timeout doesn't work right.)
304 kwargs: Any keyword arguments are converted into query string parameters.
305
306 Returns:
307 The response body, as a string.
308 """
309 # TODO: Don't require authentication. Let the server say
310 # whether it is necessary.
311 if not self.authenticated:
312 self._Authenticate()
313
314 old_timeout = socket.getdefaulttimeout()
315 socket.setdefaulttimeout(timeout)
316 try:
317 tries = 0
318 while True:
319 tries += 1
320 args = dict(kwargs)
321 url = "http://%s%s" % (self.host, request_path)
322 if args:
323 url += "?" + urllib.urlencode(args)
324 req = self._CreateRequest(url=url, data=payload)
325 req.add_header("Content-Type", content_type)
326 try:
327 f = self.opener.open(req)
328 response = f.read()
329 f.close()
330 return response
331 except urllib2.HTTPError, e:
332 if tries > 3:
333 raise
334 elif e.code == 401:
335 self._Authenticate()
336 ## elif e.code >= 500 and e.code < 600:
337 ## # Server Error - try again.
338 ## continue
339 else:
340 raise
341 finally:
342 socket.setdefaulttimeout(old_timeout)
343
344
345 class HttpRpcServer(AbstractRpcServer):
346 """Provides a simplified RPC-style interface for HTTP requests."""
347
348 def _Authenticate(self):
349 """Save the cookie jar after authentication."""
350 super(HttpRpcServer, self)._Authenticate()
351 if self.save_cookies:
352 StatusUpdate("Saving authentication cookies to %s" % self.cookie_file)
353 self.cookie_jar.save()
354
355 def _GetOpener(self):
356 """Returns an OpenerDirector that supports cookies and ignores redirects.
357
358 Returns:
359 A urllib2.OpenerDirector object.
360 """
361 opener = urllib2.OpenerDirector()
362 opener.add_handler(urllib2.ProxyHandler())
363 opener.add_handler(urllib2.UnknownHandler())
364 opener.add_handler(urllib2.HTTPHandler())
365 opener.add_handler(urllib2.HTTPDefaultErrorHandler())
366 opener.add_handler(urllib2.HTTPSHandler())
367 opener.add_handler(urllib2.HTTPErrorProcessor())
368 if self.save_cookies:
369 self.cookie_file = os.path.expanduser("~/.codereview_upload_cookies")
370 self.cookie_jar = cookielib.MozillaCookieJar(self.cookie_file)
371 if os.path.exists(self.cookie_file):
372 try:
373 self.cookie_jar.load()
374 self.authenticated = True
375 StatusUpdate("Loaded authentication cookies from %s" %
376 self.cookie_file)
377 except (cookielib.LoadError, IOError):
378 # Failed to load cookies - just ignore them.
379 pass
380 else:
381 # Create an empty cookie file with mode 600
382 fd = os.open(self.cookie_file, os.O_CREAT, 0600)
383 os.close(fd)
384 # Always chmod the cookie file
385 os.chmod(self.cookie_file, 0600)
386 else:
387 # Don't save cookies across runs of update.py.
388 self.cookie_jar = cookielib.CookieJar()
389 opener.add_handler(urllib2.HTTPCookieProcessor(self.cookie_jar))
390 return opener
391
392
393 parser = optparse.OptionParser(usage="%prog [options] [-- diff_options]")
394 parser.add_option("-y", "--assume_yes", action="store_true",
395 dest="assume_yes", default=False,
396 help="Assume that the answer to yes/no questions is 'yes'.")
397 # Logging
398 group = parser.add_option_group("Logging options")
399 group.add_option("-q", "--quiet", action="store_const", const=0,
400 dest="verbose", help="Print errors only.")
401 group.add_option("-v", "--verbose", action="store_const", const=2,
402 dest="verbose", default=1,
403 help="Print info level logs (default).")
404 group.add_option("--noisy", action="store_const", const=3,
405 dest="verbose", help="Print all logs.")
406 # Review server
407 group = parser.add_option_group("Review server options")
408 group.add_option("-s", "--server", action="store", dest="server",
409 default="codereview.appspot.com",
410 metavar="SERVER",
411 help=("The server to upload to. The format is host[:port]. "
412 "Defaults to 'codereview.appspot.com'."))
413 group.add_option("-e", "--email", action="store", dest="email",
414 metavar="EMAIL", default=None,
415 help="The username to use. Will prompt if omitted.")
416 group.add_option("-H", "--host", action="store", dest="host",
417 metavar="HOST", default=None,
418 help="Overrides the Host header sent with all RPCs.")
419 group.add_option("--no_cookies", action="store_false",
420 dest="save_cookies", default=True,
421 help="Do not save authentication cookies to local disk.")
422 # Issue
423 group = parser.add_option_group("Issue options")
424 group.add_option("-d", "--description", action="store", dest="description",
425 metavar="DESCRIPTION", default=None,
426 help="Optional description when creating an issue.")
427 group.add_option("-f", "--description_file", action="store",
428 dest="description_file", metavar="DESCRIPTION_FILE",
429 default=None,
430 help="Optional path of a file that contains "
431 "the description when creating an issue.")
432 group.add_option("-r", "--reviewers", action="store", dest="reviewers",
433 metavar="REVIEWERS", default=None,
434 help="Add reviewers (comma separated email addresses).")
435 group.add_option("--cc", action="store", dest="cc",
436 metavar="CC", default=None,
437 help="Add CC (comma separated email addresses).")
438 # Upload options
439 group = parser.add_option_group("Patch options")
440 group.add_option("-m", "--message", action="store", dest="message",
441 metavar="MESSAGE", default=None,
442 help="A message to identify the patch. "
443 "Will prompt if omitted.")
444 group.add_option("-i", "--issue", type="int", action="store",
445 metavar="ISSUE", default=None,
446 help="Issue number to which to add. Defaults to new issue.")
447 group.add_option("--download_base", action="store_true",
448 dest="download_base", default=False,
449 help="Base files will be downloaded by the server "
450 "(side-by-side diffs may not work on files with CRs).")
451 group.add_option("--rev", action="store", dest="revision",
452 metavar="REV", default=None,
453 help="Branch/tree/revision to diff against (used by DVCS).")
454 group.add_option("--send_mail", action="store_true",
455 dest="send_mail", default=False,
456 help="Send notification email to reviewers.")
457
458
459 def GetRpcServer(options):
460 """Returns an instance of an AbstractRpcServer.
461
462 Returns:
463 A new AbstractRpcServer, on which RPC calls can be made.
464 """
465
466 rpc_server_class = HttpRpcServer
467
468 def GetUserCredentials():
469 """Prompts the user for a username and password."""
470 email = options.email
471 if email is None:
472 email = GetEmail()
473 password = getpass.getpass("Password for %s: " % email)
474 return (email, password)
475
476 # If this is the dev_appserver, use fake authentication.
477 host = (options.host or options.server).lower()
478 if host == "localhost" or host.startswith("localhost:"):
479 email = options.email
480 if email is None:
481 email = "test@example.com"
482 logging.info("Using debug user %s. Override with --email" % email)
483 server = rpc_server_class(
484 options.server,
485 lambda: (email, "password"),
486 host_override=options.host,
487 extra_headers={"Cookie":
488 'dev_appserver_login="%s:False"' % email},
489 save_cookies=options.save_cookies)
490 # Don't try to talk to ClientLogin.
491 server.authenticated = True
492 return server
493
494 return rpc_server_class(options.server, GetUserCredentials,
495 host_override=options.host,
496 save_cookies=options.save_cookies)
497
498
499 def EncodeMultipartFormData(fields, files):
500 """Encode form fields for multipart/form-data.
501
502 Args:
503 fields: A sequence of (name, value) elements for regular form fields.
504 files: A sequence of (name, filename, value) elements for data to be
505 uploaded as files.
506 Returns:
507 (content_type, body) ready for httplib.HTTP instance.
508
509 Source:
510 http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/146306
511 """
512 BOUNDARY = '-M-A-G-I-C---B-O-U-N-D-A-R-Y-'
513 CRLF = '\r\n'
514 lines = []
515 for (key, value) in fields:
516 lines.append('--' + BOUNDARY)
517 lines.append('Content-Disposition: form-data; name="%s"' % key)
518 lines.append('')
519 lines.append(value)
520 for (key, filename, value) in files:
521 lines.append('--' + BOUNDARY)
522 lines.append('Content-Disposition: form-data; name="%s"; filename="%s"' %
523 (key, filename))
524 lines.append('Content-Type: %s' % GetContentType(filename))
525 lines.append('')
526 lines.append(value)
527 lines.append('--' + BOUNDARY + '--')
528 lines.append('')
529 body = CRLF.join(lines)
530 content_type = 'multipart/form-data; boundary=%s' % BOUNDARY
531 return content_type, body
532
533
534 def GetContentType(filename):
535 """Helper to guess the content-type from the filename."""
536 return mimetypes.guess_type(filename)[0] or 'application/octet-stream'
537
538
539 # Use a shell for subcommands on Windows to get a PATH search.
540 use_shell = sys.platform.startswith("win")
541
542 def RunShellWithReturnCode(command, print_output=False,
543 universal_newlines=True):
544 """Executes a command and returns the output from stdout and the return code.
545
546 Args:
547 command: Command to execute.
548 print_output: If True, the output is printed to stdout.
549 If False, both stdout and stderr are ignored.
550 universal_newlines: Use universal_newlines flag (default: True).
551
552 Returns:
553 Tuple (output, return code)
554 """
555 logging.info("Running %s", command)
556 p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
557 shell=use_shell, universal_newlines=universal_newlines)
558 if print_output:
559 output_array = []
560 while True:
561 line = p.stdout.readline()
562 if not line:
563 break
564 print line.strip("\n")
565 output_array.append(line)
566 output = "".join(output_array)
567 else:
568 output = p.stdout.read()
569 p.wait()
570 errout = p.stderr.read()
571 if print_output and errout:
572 print >>sys.stderr, errout
573 p.stdout.close()
574 p.stderr.close()
575 return output, p.returncode
576
577
578 def RunShell(command, silent_ok=False, universal_newlines=True,
579 print_output=False):
580 data, retcode = RunShellWithReturnCode(command, print_output,
581 universal_newlines)
582 if retcode:
583 ErrorExit("Got error status from %s:\n%s" % (command, data))
584 if not silent_ok and not data:
585 ErrorExit("No output from %s" % command)
586 return data
587
588
589 class VersionControlSystem(object):
590 """Abstract base class providing an interface to the VCS."""
591
592 def __init__(self, options):
593 """Constructor.
594
595 Args:
596 options: Command line options.
597 """
598 self.options = options
599
600 def GenerateDiff(self, args):
601 """Return the current diff as a string.
602
603 Args:
604 args: Extra arguments to pass to the diff command.
605 """
606 raise NotImplementedError(
607 "abstract method -- subclass %s must override" % self.__class__)
608
609 def GetUnknownFiles(self):
610 """Return a list of files unknown to the VCS."""
611 raise NotImplementedError(
612 "abstract method -- subclass %s must override" % self.__class__)
613
614 def CheckForUnknownFiles(self):
615 """Show an "are you sure?" prompt if there are unknown files."""
616 unknown_files = self.GetUnknownFiles()
617 if unknown_files:
618 print "The following files are not added to version control:"
619 for line in unknown_files:
620 print line
621 prompt = "Are you sure to continue?(y/N) "
622 answer = raw_input(prompt).strip()
623 if answer != "y":
624 ErrorExit("User aborted")
625
626 def GetBaseFile(self, filename):
627 """Get the content of the upstream version of a file.
628
629 Returns:
630 A tuple (base_content, new_content, is_binary, status)
631 base_content: The contents of the base file.
632 new_content: For text files, this is empty. For binary files, this is
633 the contents of the new file, since the diff output won't contain
634 information to reconstruct the current file.
635 is_binary: True iff the file is binary.
636 status: The status of the file.
637 """
638
639 raise NotImplementedError(
640 "abstract method -- subclass %s must override" % self.__class__)
641
642
643 def GetBaseFiles(self, diff):
644 """Helper that calls GetBase file for each file in the patch.
645
646 Returns:
647 A dictionary that maps from filename to GetBaseFile's tuple. Filenames
648 are retrieved based on lines that start with "Index:" or
649 "Property changes on:".
650 """
651 files = {}
652 for line in diff.splitlines(True):
653 if line.startswith('Index:') or line.startswith('Property changes on:'):
654 unused, filename = line.split(':', 1)
655 # On Windows if a file has property changes its filename uses '\'
656 # instead of '/'.
657 filename = filename.strip().replace('\\', '/')
658 files[filename] = self.GetBaseFile(filename)
659 return files
660
661
662 def UploadBaseFiles(self, issue, rpc_server, patch_list, patchset, options,
663 files):
664 """Uploads the base files (and if necessary, the current ones as well)."""
665
666 def UploadFile(filename, file_id, content, is_binary, status, is_base):
667 """Uploads a file to the server."""
668 file_too_large = False
669 if is_base:
670 type = "base"
671 else:
672 type = "current"
673 if len(content) > MAX_UPLOAD_SIZE:
674 print ("Not uploading the %s file for %s because it's too large." %
675 (type, filename))
676 file_too_large = True
677 content = ""
678 checksum = md5.new(content).hexdigest()
679 if options.verbose > 0 and not file_too_large:
680 print "Uploading %s file for %s" % (type, filename)
681 url = "/%d/upload_content/%d/%d" % (int(issue), int(patchset), file_id)
682 form_fields = [("filename", filename),
683 ("status", status),
684 ("checksum", checksum),
685 ("is_binary", str(is_binary)),
686 ("is_current", str(not is_base)),
687 ]
688 if file_too_large:
689 form_fields.append(("file_too_large", "1"))
690 if options.email:
691 form_fields.append(("user", options.email))
692 ctype, body = EncodeMultipartFormData(form_fields,
693 [("data", filename, content)])
694 response_body = rpc_server.Send(url, body,
695 content_type=ctype)
696 if not response_body.startswith("OK"):
697 StatusUpdate(" --> %s" % response_body)
698 sys.exit(1)
699
700 patches = dict()
701 [patches.setdefault(v, k) for k, v in patch_list]
702 for filename in patches.keys():
703 base_content, new_content, is_binary, status = files[filename]
704 file_id_str = patches.get(filename)
705 if file_id_str.find("nobase") != -1:
706 base_content = None
707 file_id_str = file_id_str[file_id_str.rfind("_") + 1:]
708 file_id = int(file_id_str)
709 if base_content != None:
710 UploadFile(filename, file_id, base_content, is_binary, status, True)
711 if new_content != None:
712 UploadFile(filename, file_id, new_content, is_binary, status, False)
713
714 def IsImage(self, filename):
715 """Returns true if the filename has an image extension."""
716 mimetype = mimetypes.guess_type(filename)[0]
717 if not mimetype:
718 return False
719 return mimetype.startswith("image/")
720
721
722 class SubversionVCS(VersionControlSystem):
723 """Implementation of the VersionControlSystem interface for Subversion."""
724
725 def __init__(self, options):
726 super(SubversionVCS, self).__init__(options)
727 if self.options.revision:
728 match = re.match(r"(\d+)(:(\d+))?", self.options.revision)
729 if not match:
730 ErrorExit("Invalid Subversion revision %s." % self.options.revision)
731 self.rev_start = match.group(1)
732 self.rev_end = match.group(3)
733 else:
734 self.rev_start = self.rev_end = None
735 # Cache output from "svn list -r REVNO dirname".
736 # Keys: dirname, Values: 2-tuple (ouput for start rev and end rev).
737 self.svnls_cache = {}
738 # SVN base URL is required to fetch files deleted in an older revision.
739 # Result is cached to not guess it over and over again in GetBaseFile().
740 required = self.options.download_base or self.options.revision is not None
741 self.svn_base = self._GuessBase(required)
742
743 def GuessBase(self, required):
744 """Wrapper for _GuessBase."""
745 return self.svn_base
746
747 def _GuessBase(self, required):
748 """Returns the SVN base URL.
749
750 Args:
751 required: If true, exits if the url can't be guessed, otherwise None is
752 returned.
753 """
754 info = RunShell(["svn", "info"])
755 for line in info.splitlines():
756 words = line.split()
757 if len(words) == 2 and words[0] == "URL:":
758 url = words[1]
759 scheme, netloc, path, params, query, fragment = urlparse.urlparse(url)
760 username, netloc = urllib.splituser(netloc)
761 if username:
762 logging.info("Removed username from base URL")
763 if netloc.endswith("svn.python.org"):
764 if netloc == "svn.python.org":
765 if path.startswith("/projects/"):
766 path = path[9:]
767 elif netloc != "pythondev@svn.python.org":
768 ErrorExit("Unrecognized Python URL: %s" % url)
769 base = "http://svn.python.org/view/*checkout*%s/" % path
770 logging.info("Guessed Python base = %s", base)
771 elif netloc.endswith("svn.collab.net"):
772 if path.startswith("/repos/"):
773 path = path[6:]
774 base = "http://svn.collab.net/viewvc/*checkout*%s/" % path
775 logging.info("Guessed CollabNet base = %s", base)
776 elif netloc.endswith(".googlecode.com"):
777 path = path + "/"
778 base = urlparse.urlunparse(("http", netloc, path, params,
779 query, fragment))
780 logging.info("Guessed Google Code base = %s", base)
781 else:
782 path = path + "/"
783 base = urlparse.urlunparse((scheme, netloc, path, params,
784 query, fragment))
785 logging.info("Guessed base = %s", base)
786 return base
787 if required:
788 ErrorExit("Can't find URL in output from svn info")
789 return None
790
791 def GenerateDiff(self, args):
792 cmd = ["svn", "diff"]
793 if self.options.revision:
794 cmd += ["-r", self.options.revision]
795 cmd.extend(args)
796 data = RunShell(cmd)
797 count = 0
798 for line in data.splitlines():
799 if line.startswith("Index:") or line.startswith("Property changes on:"):
800 count += 1
801 logging.info(line)
802 if not count:
803 ErrorExit("No valid patches found in output from svn diff")
804 return data
805
806 def _CollapseKeywords(self, content, keyword_str):
807 """Collapses SVN keywords."""
808 # svn cat translates keywords but svn diff doesn't. As a result of this
809 # behavior patching.PatchChunks() fails with a chunk mismatch error.
810 # This part was originally written by the Review Board development team
811 # who had the same problem (http://reviews.review-board.org/r/276/).
812 # Mapping of keywords to known aliases
813 svn_keywords = {
814 # Standard keywords
815 'Date': ['Date', 'LastChangedDate'],
816 'Revision': ['Revision', 'LastChangedRevision', 'Rev'],
817 'Author': ['Author', 'LastChangedBy'],
818 'HeadURL': ['HeadURL', 'URL'],
819 'Id': ['Id'],
820
821 # Aliases
822 'LastChangedDate': ['LastChangedDate', 'Date'],
823 'LastChangedRevision': ['LastChangedRevision', 'Rev', 'Revision'],
824 'LastChangedBy': ['LastChangedBy', 'Author'],
825 'URL': ['URL', 'HeadURL'],
826 }
827
828 def repl(m):
829 if m.group(2):
830 return "$%s::%s$" % (m.group(1), " " * len(m.group(3)))
831 return "$%s$" % m.group(1)
832 keywords = [keyword
833 for name in keyword_str.split(" ")
834 for keyword in svn_keywords.get(name, [])]
835 return re.sub(r"\$(%s):(:?)([^\$]+)\$" % '|'.join(keywords), repl, content)
836
837 def GetUnknownFiles(self):
838 status = RunShell(["svn", "status", "--ignore-externals"], silent_ok=True)
839 unknown_files = []
840 for line in status.split("\n"):
841 if line and line[0] == "?":
842 unknown_files.append(line)
843 return unknown_files
844
845 def ReadFile(self, filename):
846 """Returns the contents of a file."""
847 file = open(filename, 'rb')
848 result = ""
849 try:
850 result = file.read()
851 finally:
852 file.close()
853 return result
854
855 def GetStatus(self, filename):
856 """Returns the status of a file."""
857 if not self.options.revision:
858 status = RunShell(["svn", "status", "--ignore-externals", filename])
859 if not status:
860 ErrorExit("svn status returned no output for %s" % filename)
861 status_lines = status.splitlines()
862 # If file is in a cl, the output will begin with
863 # "\n--- Changelist 'cl_name':\n". See
864 # http://svn.collab.net/repos/svn/trunk/notes/changelist-design.txt
865 if (len(status_lines) == 3 and
866 not status_lines[0] and
867 status_lines[1].startswith("--- Changelist")):
868 status = status_lines[2]
869 else:
870 status = status_lines[0]
871 # If we have a revision to diff against we need to run "svn list"
872 # for the old and the new revision and compare the results to get
873 # the correct status for a file.
874 else:
875 dirname, relfilename = os.path.split(filename)
876 if dirname not in self.svnls_cache:
877 cmd = ["svn", "list", "-r", self.rev_start, dirname or "."]
878 out, returncode = RunShellWithReturnCode(cmd)
879 if returncode:
880 ErrorExit("Failed to get status for %s." % filename)
881 old_files = out.splitlines()
882 args = ["svn", "list"]
883 if self.rev_end:
884 args += ["-r", self.rev_end]
885 cmd = args + [dirname or "."]
886 out, returncode = RunShellWithReturnCode(cmd)
887 if returncode:
888 ErrorExit("Failed to run command %s" % cmd)
889 self.svnls_cache[dirname] = (old_files, out.splitlines())
890 old_files, new_files = self.svnls_cache[dirname]
891 if relfilename in old_files and relfilename not in new_files:
892 status = "D "
893 elif relfilename in old_files and relfilename in new_files:
894 status = "M "
895 else:
896 status = "A "
897 return status
898
899 def GetBaseFile(self, filename):
900 status = self.GetStatus(filename)
901 base_content = None
902 new_content = None
903
904 # If a file is copied its status will be "A +", which signifies
905 # "addition-with-history". See "svn st" for more information. We need to
906 # upload the original file or else diff parsing will fail if the file was
907 # edited.
908 if status[0] == "A" and status[3] != "+":
909 # We'll need to upload the new content if we're adding a binary file
910 # since diff's output won't contain it.
911 mimetype = RunShell(["svn", "propget", "svn:mime-type", filename],
912 silent_ok=True)
913 base_content = ""
914 is_binary = mimetype and not mimetype.startswith("text/")
915 if is_binary and self.IsImage(filename):
916 new_content = self.ReadFile(filename)
917 elif (status[0] in ("M", "D", "R") or
918 (status[0] == "A" and status[3] == "+") or # Copied file.
919 (status[0] == " " and status[1] == "M")): # Property change.
920 args = []
921 if self.options.revision:
922 url = "%s/%s@%s" % (self.svn_base, filename, self.rev_start)
923 else:
924 # Don't change filename, it's needed later.
925 url = filename
926 args += ["-r", "BASE"]
927 cmd = ["svn"] + args + ["propget", "svn:mime-type", url]
928 mimetype, returncode = RunShellWithReturnCode(cmd)
929 if returncode:
930 # File does not exist in the requested revision.
931 # Reset mimetype, it contains an error message.
932 mimetype = ""
933 get_base = False
934 is_binary = mimetype and not mimetype.startswith("text/")
935 if status[0] == " ":
936 # Empty base content just to force an upload.
937 base_content = ""
938 elif is_binary:
939 if self.IsImage(filename):
940 get_base = True
941 if status[0] == "M":
942 if not self.rev_end:
943 new_content = self.ReadFile(filename)
944 else:
945 url = "%s/%s@%s" % (self.svn_base, filename, self.rev_end)
946 new_content = RunShell(["svn", "cat", url],
947 universal_newlines=True, silent_ok=True)
948 else:
949 base_content = ""
950 else:
951 get_base = True
952
953 if get_base:
954 if is_binary:
955 universal_newlines = False
956 else:
957 universal_newlines = True
958 if self.rev_start:
959 # "svn cat -r REV delete_file.txt" doesn't work. cat requires
960 # the full URL with "@REV" appended instead of using "-r" option.
961 url = "%s/%s@%s" % (self.svn_base, filename, self.rev_start)
962 base_content = RunShell(["svn", "cat", url],
963 universal_newlines=universal_newlines,
964 silent_ok=True)
965 else:
966 base_content = RunShell(["svn", "cat", filename],
967 universal_newlines=universal_newlines,
968 silent_ok=True)
969 if not is_binary:
970 args = []
971 if self.rev_start:
972 url = "%s/%s@%s" % (self.svn_base, filename, self.rev_start)
973 else:
974 url = filename
975 args += ["-r", "BASE"]
976 cmd = ["svn"] + args + ["propget", "svn:keywords", url]
977 keywords, returncode = RunShellWithReturnCode(cmd)
978 if keywords and not returncode:
979 base_content = self._CollapseKeywords(base_content, keywords)
980 else:
981 StatusUpdate("svn status returned unexpected output: %s" % status)
982 sys.exit(1)
983 return base_content, new_content, is_binary, status[0:5]
984
985
986 class GitVCS(VersionControlSystem):
987 """Implementation of the VersionControlSystem interface for Git."""
988
989 def __init__(self, options):
990 super(GitVCS, self).__init__(options)
991 # Map of filename -> hash of base file.
992 self.base_hashes = {}
993
994 def GenerateDiff(self, extra_args):
995 # This is more complicated than svn's GenerateDiff because we must convert
996 # the diff output to include an svn-style "Index:" line as well as record
997 # the hashes of the base files, so we can upload them along with our diff.
998 if self.options.revision:
999 extra_args = [self.options.revision] + extra_args
1000 gitdiff = RunShell(["git", "diff", "--full-index"] + extra_args)
1001 svndiff = []
1002 filecount = 0
1003 filename = None
1004 for line in gitdiff.splitlines():
1005 match = re.match(r"diff --git a/(.*) b/.*$", line)
1006 if match:
1007 filecount += 1
1008 filename = match.group(1)
1009 svndiff.append("Index: %s\n" % filename)
1010 else:
1011 # The "index" line in a git diff looks like this (long hashes elided):
1012 # index 82c0d44..b2cee3f 100755
1013 # We want to save the left hash, as that identifies the base file.
1014 match = re.match(r"index (\w+)\.\.", line)
1015 if match:
1016 self.base_hashes[filename] = match.group(1)
1017 svndiff.append(line + "\n")
1018 if not filecount:
1019 ErrorExit("No valid patches found in output from git diff")
1020 return "".join(svndiff)
1021
1022 def GetUnknownFiles(self):
1023 status = RunShell(["git", "ls-files", "--exclude-standard", "--others"],
1024 silent_ok=True)
1025 return status.splitlines()
1026
1027 def GetBaseFile(self, filename):
1028 hash = self.base_hashes[filename]
1029 base_content = None
1030 new_content = None
1031 is_binary = False
1032 if hash == "0" * 40: # All-zero hash indicates no base file.
1033 status = "A"
1034 base_content = ""
1035 else:
1036 status = "M"
1037 base_content = RunShell(["git", "show", hash])
1038 return (base_content, new_content, is_binary, status)
1039
1040
1041 class MercurialVCS(VersionControlSystem):
1042 """Implementation of the VersionControlSystem interface for Mercurial."""
1043
1044 def __init__(self, options, repo_dir):
1045 super(MercurialVCS, self).__init__(options)
1046 # Absolute path to repository (we can be in a subdir)
1047 self.repo_dir = os.path.normpath(repo_dir)
1048 # Compute the subdir
1049 cwd = os.path.normpath(os.getcwd())
1050 assert cwd.startswith(self.repo_dir)
1051 self.subdir = cwd[len(self.repo_dir):].lstrip(r"\/")
1052 if self.options.revision:
1053 self.base_rev = self.options.revision
1054 else:
1055 self.base_rev = RunShell(["hg", "parent", "-q"]).split(':')[1].strip()
1056
1057 def _GetRelPath(self, filename):
1058 """Get relative path of a file according to the current directory,
1059 given its logical path in the repo."""
1060 assert filename.startswith(self.subdir), filename
1061 return filename[len(self.subdir):].lstrip(r"\/")
1062
1063 def GenerateDiff(self, extra_args):
1064 # If no file specified, restrict to the current subdir
1065 extra_args = extra_args or ["."]
1066 cmd = ["hg", "diff", "--git", "-r", self.base_rev] + extra_args
1067 data = RunShell(cmd, silent_ok=True)
1068 svndiff = []
1069 filecount = 0
1070 for line in data.splitlines():
1071 m = re.match("diff --git a/(\S+) b/(\S+)", line)
1072 if m:
1073 # Modify line to make it look like as it comes from svn diff.
1074 # With this modification no changes on the server side are required
1075 # to make upload.py work with Mercurial repos.
1076 # NOTE: for proper handling of moved/copied files, we have to use
1077 # the second filename.
1078 filename = m.group(2)
1079 svndiff.append("Index: %s" % filename)
1080 svndiff.append("=" * 67)
1081 filecount += 1
1082 logging.info(line)
1083 else:
1084 svndiff.append(line)
1085 if not filecount:
1086 ErrorExit("No valid patches found in output from hg diff")
1087 return "\n".join(svndiff) + "\n"
1088
1089 def GetUnknownFiles(self):
1090 """Return a list of files unknown to the VCS."""
1091 args = []
1092 status = RunShell(["hg", "status", "--rev", self.base_rev, "-u", "."],
1093 silent_ok=True)
1094 unknown_files = []
1095 for line in status.splitlines():
1096 st, fn = line.split(" ", 1)
1097 if st == "?":
1098 unknown_files.append(fn)
1099 return unknown_files
1100
1101 def GetBaseFile(self, filename):
1102 # "hg status" and "hg cat" both take a path relative to the current subdir
1103 # rather than to the repo root, but "hg diff" has given us the full path
1104 # to the repo root.
1105 base_content = ""
1106 new_content = None
1107 is_binary = False
1108 oldrelpath = relpath = self._GetRelPath(filename)
1109 # "hg status -C" returns two lines for moved/copied files, one otherwise
1110 out = RunShell(["hg", "status", "-C", "--rev", self.base_rev, relpath])
1111 out = out.splitlines()
1112 # HACK: strip error message about missing file/directory if it isn't in
1113 # the working copy
1114 if out[0].startswith('%s: ' % relpath):
1115 out = out[1:]
1116 if len(out) > 1:
1117 # Moved/copied => considered as modified, use old filename to
1118 # retrieve base contents
1119 oldrelpath = out[1].strip()
1120 status = "M"
1121 else:
1122 status, _ = out[0].split(' ', 1)
1123 if status != "A":
1124 base_content = RunShell(["hg", "cat", "-r", self.base_rev, oldrelpath],
1125 silent_ok=True)
1126 is_binary = "\0" in base_content # Mercurial's heuristic
1127 if status != "R":
1128 new_content = open(relpath, "rb").read()
1129 is_binary = is_binary or "\0" in new_content
1130 if is_binary and base_content:
1131 # Fetch again without converting newlines
1132 base_content = RunShell(["hg", "cat", "-r", self.base_rev, oldrelpath],
1133 silent_ok=True, universal_newlines=False)
1134 if not is_binary or not self.IsImage(relpath):
1135 new_content = None
1136 return base_content, new_content, is_binary, status
1137
1138
1139 # NOTE: The SplitPatch function is duplicated in engine.py, keep them in sync.
1140 def SplitPatch(data):
1141 """Splits a patch into separate pieces for each file.
1142
1143 Args:
1144 data: A string containing the output of svn diff.
1145
1146 Returns:
1147 A list of 2-tuple (filename, text) where text is the svn diff output
1148 pertaining to filename.
1149 """
1150 patches = []
1151 filename = None
1152 diff = []
1153 for line in data.splitlines(True):
1154 new_filename = None
1155 if line.startswith('Index:'):
1156 unused, new_filename = line.split(':', 1)
1157 new_filename = new_filename.strip()
1158 elif line.startswith('Property changes on:'):
1159 unused, temp_filename = line.split(':', 1)
1160 # When a file is modified, paths use '/' between directories, however
1161 # when a property is modified '\' is used on Windows. Make them the same
1162 # otherwise the file shows up twice.
1163 temp_filename = temp_filename.strip().replace('\\', '/')
1164 if temp_filename != filename:
1165 # File has property changes but no modifications, create a new diff.
1166 new_filename = temp_filename
1167 if new_filename:
1168 if filename and diff:
1169 patches.append((filename, ''.join(diff)))
1170 filename = new_filename
1171 diff = [line]
1172 continue
1173 if diff is not None:
1174 diff.append(line)
1175 if filename and diff:
1176 patches.append((filename, ''.join(diff)))
1177 return patches
1178
1179
1180 def UploadSeparatePatches(issue, rpc_server, patchset, data, options):
1181 """Uploads a separate patch for each file in the diff output.
1182
1183 Returns a list of [patch_key, filename] for each file.
1184 """
1185 patches = SplitPatch(data)
1186 rv = []
1187 for patch in patches:
1188 if len(patch[1]) > MAX_UPLOAD_SIZE:
1189 print ("Not uploading the patch for " + patch[0] +
1190 " because the file is too large.")
1191 continue
1192 form_fields = [("filename", patch[0])]
1193 if not options.download_base:
1194 form_fields.append(("content_upload", "1"))
1195 files = [("data", "data.diff", patch[1])]
1196 ctype, body = EncodeMultipartFormData(form_fields, files)
1197 url = "/%d/upload_patch/%d" % (int(issue), int(patchset))
1198 print "Uploading patch for " + patch[0]
1199 response_body = rpc_server.Send(url, body, content_type=ctype)
1200 lines = response_body.splitlines()
1201 if not lines or lines[0] != "OK":
1202 StatusUpdate(" --> %s" % response_body)
1203 sys.exit(1)
1204 rv.append([lines[1], patch[0]])
1205 return rv
1206
1207
1208 def GuessVCS(options):
1209 """Helper to guess the version control system.
1210
1211 This examines the current directory, guesses which VersionControlSystem
1212 we're using, and returns an instance of the appropriate class. Exit with an
1213 error if we can't figure it out.
1214
1215 Returns:
1216 A VersionControlSystem instance. Exits if the VCS can't be guessed.
1217 """
1218 # Mercurial has a command to get the base directory of a repository
1219 # Try running it, but don't die if we don't have hg installed.
1220 # NOTE: we try Mercurial first as it can sit on top of an SVN working copy.
1221 try:
1222 out, returncode = RunShellWithReturnCode(["hg", "root"])
1223 if returncode == 0:
1224 return MercurialVCS(options, out.strip())
1225 except OSError, (errno, message):
1226 if errno != 2: # ENOENT -- they don't have hg installed.
1227 raise
1228
1229 # Subversion has a .svn in all working directories.
1230 if os.path.isdir('.svn'):
1231 logging.info("Guessed VCS = Subversion")
1232 return SubversionVCS(options)
1233
1234 # Git has a command to test if you're in a git tree.
1235 # Try running it, but don't die if we don't have git installed.
1236 try:
1237 out, returncode = RunShellWithReturnCode(["git", "rev-parse",
1238 "--is-inside-work-tree"])
1239 if returncode == 0:
1240 return GitVCS(options)
1241 except OSError, (errno, message):
1242 if errno != 2: # ENOENT -- they don't have git installed.
1243 raise
1244
1245 ErrorExit(("Could not guess version control system. "
1246 "Are you in a working copy directory?"))
1247
1248
1249 def RealMain(argv, data=None):
1250 logging.basicConfig(format=("%(asctime).19s %(levelname)s %(filename)s:"
1251 "%(lineno)s %(message)s "))
1252 os.environ['LC_ALL'] = 'C'
1253 options, args = parser.parse_args(argv[1:])
1254 global verbosity
1255 verbosity = options.verbose
1256 if verbosity >= 3:
1257 logging.getLogger().setLevel(logging.DEBUG)
1258 elif verbosity >= 2:
1259 logging.getLogger().setLevel(logging.INFO)
1260 vcs = GuessVCS(options)
1261 if isinstance(vcs, SubversionVCS):
1262 # base field is only allowed for Subversion.
1263 # Note: Fetching base files may become deprecated in future releases.
1264 base = vcs.GuessBase(options.download_base)
1265 else:
1266 base = None
1267 if not base and options.download_base:
1268 options.download_base = True
1269 logging.info("Enabled upload of base file")
1270 if not options.assume_yes:
1271 vcs.CheckForUnknownFiles()
1272 if data is None:
1273 data = vcs.GenerateDiff(args)
1274 files = vcs.GetBaseFiles(data)
1275 if verbosity >= 1:
1276 print "Upload server:", options.server, "(change with -s/--server)"
1277 if options.issue:
1278 prompt = "Message describing this patch set: "
1279 else:
1280 prompt = "New issue subject: "
1281 message = options.message or raw_input(prompt).strip()
1282 if not message:
1283 ErrorExit("A non-empty message is required")
1284 rpc_server = GetRpcServer(options)
1285 form_fields = [("subject", message)]
1286 if base:
1287 form_fields.append(("base", base))
1288 if options.issue:
1289 form_fields.append(("issue", str(options.issue)))
1290 if options.email:
1291 form_fields.append(("user", options.email))
1292 if options.reviewers:
1293 for reviewer in options.reviewers.split(','):
1294 if "@" in reviewer and not reviewer.split("@")[1].count(".") == 1:
1295 ErrorExit("Invalid email address: %s" % reviewer)
1296 form_fields.append(("reviewers", options.reviewers))
1297 if options.cc:
1298 for cc in options.cc.split(','):
1299 if "@" in cc and not cc.split("@")[1].count(".") == 1:
1300 ErrorExit("Invalid email address: %s" % cc)
1301 form_fields.append(("cc", options.cc))
1302 description = options.description
1303 if options.description_file:
1304 if options.description:
1305 ErrorExit("Can't specify description and description_file")
1306 file = open(options.description_file, 'r')
1307 description = file.read()
1308 file.close()
1309 if description:
1310 form_fields.append(("description", description))
1311 # Send a hash of all the base file so the server can determine if a copy
1312 # already exists in an earlier patchset.
1313 base_hashes = ""
1314 for file, info in files.iteritems():
1315 if not info[0] is None:
1316 checksum = md5.new(info[0]).hexdigest()
1317 if base_hashes:
1318 base_hashes += "|"
1319 base_hashes += checksum + ":" + file
1320 form_fields.append(("base_hashes", base_hashes))
1321 # If we're uploading base files, don't send the email before the uploads, so
1322 # that it contains the file status.
1323 if options.send_mail and options.download_base:
1324 form_fields.append(("send_mail", "1"))
1325 if not options.download_base:
1326 form_fields.append(("content_upload", "1"))
1327 if len(data) > MAX_UPLOAD_SIZE:
1328 print "Patch is large, so uploading file patches separately."
1329 uploaded_diff_file = []
1330 form_fields.append(("separate_patches", "1"))
1331 else:
1332 uploaded_diff_file = [("data", "data.diff", data)]
1333 ctype, body = EncodeMultipartFormData(form_fields, uploaded_diff_file)
1334 response_body = rpc_server.Send("/upload", body, content_type=ctype)
1335 if not options.download_base or not uploaded_diff_file:
1336 lines = response_body.splitlines()
1337 if len(lines) >= 2:
1338 msg = lines[0]
1339 patchset = lines[1].strip()
1340 patches = [x.split(" ", 1) for x in lines[2:]]
1341 else:
1342 msg = response_body
1343 else:
1344 msg = response_body
1345 StatusUpdate(msg)
1346 if not response_body.startswith("Issue created.") and \
1347 not response_body.startswith("Issue updated."):
1348 sys.exit(0)
1349 issue = msg[msg.rfind("/")+1:]
1350
1351 if not uploaded_diff_file:
1352 result = UploadSeparatePatches(issue, rpc_server, patchset, data, options)
1353 if not options.download_base:
1354 patches = result
1355
1356 if not options.download_base:
1357 vcs.UploadBaseFiles(issue, rpc_server, patches, patchset, options, files)
1358 if options.send_mail:
1359 rpc_server.Send("/" + issue + "/mail", payload="")
1360 return issue, patchset
1361
1362
1363 def main():
1364 try:
1365 RealMain(sys.argv)
1366 except KeyboardInterrupt:
1367 print
1368 StatusUpdate("Interrupted.")
1369 sys.exit(1)
1370
1371
1372 if __name__ == "__main__":
1373 main()
OLDNEW
« no previous file with comments | « depot_tools/trychange.py ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698