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

Side by Side Diff: upload.py

Issue 414035: Update upload.py to revision 488 and remove HOSTED support. (Closed)
Patch Set: Created 11 years, 1 month 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 | « no previous file | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 #!/usr/bin/env python 1 #!/usr/bin/env python
2 # 2 #
3 # Copyright 2007 Google Inc. 3 # Copyright 2007 Google Inc.
4 # 4 #
5 # Licensed under the Apache License, Version 2.0 (the "License"); 5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with 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 7 # You may obtain a copy of the License at
8 # 8 #
9 # http://www.apache.org/licenses/LICENSE-2.0 9 # http://www.apache.org/licenses/LICENSE-2.0
10 # 10 #
(...skipping 27 matching lines...) Expand all
38 import optparse 38 import optparse
39 import os 39 import os
40 import re 40 import re
41 import socket 41 import socket
42 import subprocess 42 import subprocess
43 import sys 43 import sys
44 import urllib 44 import urllib
45 import urllib2 45 import urllib2
46 import urlparse 46 import urlparse
47 47
48 # Work-around for md5 module deprecation warning in python 2.5+: 48 # The md5 module was deprecated in Python 2.5.
49 try: 49 try:
50 # Try to load hashlib (python 2.5+)
51 from hashlib import md5 50 from hashlib import md5
52 except ImportError: 51 except ImportError:
53 # If hashlib cannot be imported, load md5.new instead. 52 from md5 import md5
54 from md5 import new as md5
55 53
56 try: 54 try:
57 import readline 55 import readline
58 except ImportError: 56 except ImportError:
59 pass 57 pass
60 58
61 # The logging verbosity: 59 # The logging verbosity:
62 # 0: Errors only. 60 # 0: Errors only.
63 # 1: Status messages. 61 # 1: Status messages.
64 # 2: Info logs. 62 # 2: Info logs.
65 # 3: Debug logs. 63 # 3: Debug logs.
66 verbosity = 1 64 verbosity = 1
67 65
68 # Max size of patch or base file. 66 # Max size of patch or base file.
69 MAX_UPLOAD_SIZE = 900 * 1024 67 MAX_UPLOAD_SIZE = 900 * 1024
70 68
69 # Constants for version control names. Used by GuessVCSName.
70 VCS_GIT = "Git"
71 VCS_MERCURIAL = "Mercurial"
72 VCS_SUBVERSION = "Subversion"
73 VCS_UNKNOWN = "Unknown"
71 74
72 def GetEmail(): 75 # whitelist for non-binary filetypes which do not start with "text/"
76 # .mm (Objective-C) shows up as application/x-freemind on my Linux box.
77 TEXT_MIMETYPES = ['application/javascript', 'application/x-javascript',
78 'application/x-freemind']
79
80 VCS_ABBREVIATIONS = {
81 VCS_MERCURIAL.lower(): VCS_MERCURIAL,
82 "hg": VCS_MERCURIAL,
83 VCS_SUBVERSION.lower(): VCS_SUBVERSION,
84 "svn": VCS_SUBVERSION,
85 VCS_GIT.lower(): VCS_GIT,
86 }
87
88
89 def GetEmail(prompt):
73 """Prompts the user for their email address and returns it. 90 """Prompts the user for their email address and returns it.
74 91
75 The last used email address is saved to a file and offered up as a suggestion 92 The last used email address is saved to a file and offered up as a suggestion
76 to the user. If the user presses enter without typing in anything the last 93 to the user. If the user presses enter without typing in anything the last
77 used email address is used. If the user enters a new address, it is saved 94 used email address is used. If the user enters a new address, it is saved
78 for next time we prompt. 95 for next time we prompt.
79 96
80 """ 97 """
81 last_email_file_name = os.path.expanduser( 98 last_email_file_name = os.path.expanduser("~/.last_codereview_email_address")
82 os.path.join("~", ".last_codereview_email_address"))
83 last_email = "" 99 last_email = ""
84 prompt = "Email: "
85 if os.path.exists(last_email_file_name): 100 if os.path.exists(last_email_file_name):
86 try: 101 try:
87 last_email_file = open(last_email_file_name, "r") 102 last_email_file = open(last_email_file_name, "r")
88 last_email = last_email_file.readline().strip("\n") 103 last_email = last_email_file.readline().strip("\n")
89 last_email_file.close() 104 last_email_file.close()
90 prompt = "Email [%s]: " % last_email 105 prompt += " [%s]" % last_email
91 except IOError, e: 106 except IOError, e:
92 pass 107 pass
93 email = raw_input(prompt).strip() 108 email = raw_input(prompt + ": ").strip()
94 if email: 109 if email:
95 try: 110 try:
96 last_email_file = open(last_email_file_name, "w") 111 last_email_file = open(last_email_file_name, "w")
97 last_email_file.write(email) 112 last_email_file.write(email)
98 last_email_file.close() 113 last_email_file.close()
99 except IOError, e: 114 except IOError, e:
100 pass 115 pass
101 else: 116 else:
102 email = last_email 117 email = last_email
103 return email 118 return email
(...skipping 82 matching lines...) Expand 10 before | Expand all | Expand 10 after
186 password: The user's password 201 password: The user's password
187 202
188 Raises: 203 Raises:
189 ClientLoginError: If there was an error authenticating with ClientLogin. 204 ClientLoginError: If there was an error authenticating with ClientLogin.
190 HTTPError: If there was some other form of HTTP error. 205 HTTPError: If there was some other form of HTTP error.
191 206
192 Returns: 207 Returns:
193 The authentication token returned by ClientLogin. 208 The authentication token returned by ClientLogin.
194 """ 209 """
195 account_type = "GOOGLE" 210 account_type = "GOOGLE"
196 if self.host.endswith(".google.com"):
197 # Needed for use inside Google.
198 account_type = "HOSTED"
199 req = self._CreateRequest( 211 req = self._CreateRequest(
200 url="https://www.google.com/accounts/ClientLogin", 212 url="https://www.google.com/accounts/ClientLogin",
201 data=urllib.urlencode({ 213 data=urllib.urlencode({
202 "Email": email, 214 "Email": email,
203 "Passwd": password, 215 "Passwd": password,
204 "service": "ah", 216 "service": "ah",
205 "source": "rietveld-codereview-upload", 217 "source": "rietveld-codereview-upload",
206 "accountType": account_type, 218 "accountType": account_type,
207 }), 219 }),
208 ) 220 )
(...skipping 41 matching lines...) Expand 10 before | Expand all | Expand 10 after
250 262
251 The authentication process works as follows: 263 The authentication process works as follows:
252 1) We get a username and password from the user 264 1) We get a username and password from the user
253 2) We use ClientLogin to obtain an AUTH token for the user 265 2) We use ClientLogin to obtain an AUTH token for the user
254 (see http://code.google.com/apis/accounts/AuthForInstalledApps.html). 266 (see http://code.google.com/apis/accounts/AuthForInstalledApps.html).
255 3) We pass the auth token to /_ah/login on the server to obtain an 267 3) We pass the auth token to /_ah/login on the server to obtain an
256 authentication cookie. If login was successful, it tries to redirect 268 authentication cookie. If login was successful, it tries to redirect
257 us to the URL we provided. 269 us to the URL we provided.
258 270
259 If we attempt to access the upload API without first obtaining an 271 If we attempt to access the upload API without first obtaining an
260 authentication cookie, it returns a 401 response and directs us to 272 authentication cookie, it returns a 401 response (or a 302) and
261 authenticate ourselves with ClientLogin. 273 directs us to authenticate ourselves with ClientLogin.
262 """ 274 """
263 for i in range(3): 275 for i in range(3):
264 credentials = self.auth_function() 276 credentials = self.auth_function()
265 try: 277 try:
266 auth_token = self._GetAuthToken(credentials[0], credentials[1]) 278 auth_token = self._GetAuthToken(credentials[0], credentials[1])
267 except ClientLoginError, e: 279 except ClientLoginError, e:
268 if e.reason == "BadAuthentication": 280 if e.reason == "BadAuthentication":
269 print >>sys.stderr, "Invalid username or password." 281 print >>sys.stderr, "Invalid username or password."
270 continue 282 continue
271 if e.reason == "CaptchaRequired": 283 if e.reason == "CaptchaRequired":
(...skipping 60 matching lines...) Expand 10 before | Expand all | Expand 10 after
332 req = self._CreateRequest(url=url, data=payload) 344 req = self._CreateRequest(url=url, data=payload)
333 req.add_header("Content-Type", content_type) 345 req.add_header("Content-Type", content_type)
334 try: 346 try:
335 f = self.opener.open(req) 347 f = self.opener.open(req)
336 response = f.read() 348 response = f.read()
337 f.close() 349 f.close()
338 return response 350 return response
339 except urllib2.HTTPError, e: 351 except urllib2.HTTPError, e:
340 if tries > 3: 352 if tries > 3:
341 raise 353 raise
342 elif e.code == 401: 354 elif e.code == 401 or e.code == 302:
343 self._Authenticate() 355 self._Authenticate()
344 ## elif e.code >= 500 and e.code < 600: 356 ## elif e.code >= 500 and e.code < 600:
345 ## # Server Error - try again. 357 ## # Server Error - try again.
346 ## continue 358 ## continue
347 else: 359 else:
348 raise 360 raise
349 finally: 361 finally:
350 socket.setdefaulttimeout(old_timeout) 362 socket.setdefaulttimeout(old_timeout)
351 363
352 364
(...skipping 14 matching lines...) Expand all
367 A urllib2.OpenerDirector object. 379 A urllib2.OpenerDirector object.
368 """ 380 """
369 opener = urllib2.OpenerDirector() 381 opener = urllib2.OpenerDirector()
370 opener.add_handler(urllib2.ProxyHandler()) 382 opener.add_handler(urllib2.ProxyHandler())
371 opener.add_handler(urllib2.UnknownHandler()) 383 opener.add_handler(urllib2.UnknownHandler())
372 opener.add_handler(urllib2.HTTPHandler()) 384 opener.add_handler(urllib2.HTTPHandler())
373 opener.add_handler(urllib2.HTTPDefaultErrorHandler()) 385 opener.add_handler(urllib2.HTTPDefaultErrorHandler())
374 opener.add_handler(urllib2.HTTPSHandler()) 386 opener.add_handler(urllib2.HTTPSHandler())
375 opener.add_handler(urllib2.HTTPErrorProcessor()) 387 opener.add_handler(urllib2.HTTPErrorProcessor())
376 if self.save_cookies: 388 if self.save_cookies:
377 self.cookie_file = os.path.expanduser( 389 self.cookie_file = os.path.expanduser("~/.codereview_upload_cookies")
378 os.path.join("~", ".codereview_upload_cookies"))
379 self.cookie_jar = cookielib.MozillaCookieJar(self.cookie_file) 390 self.cookie_jar = cookielib.MozillaCookieJar(self.cookie_file)
380 if os.path.exists(self.cookie_file): 391 if os.path.exists(self.cookie_file):
381 try: 392 try:
382 self.cookie_jar.load() 393 self.cookie_jar.load()
383 self.authenticated = True 394 self.authenticated = True
384 StatusUpdate("Loaded authentication cookies from %s" % 395 StatusUpdate("Loaded authentication cookies from %s" %
385 self.cookie_file) 396 self.cookie_file)
386 except (cookielib.LoadError, IOError): 397 except (cookielib.LoadError, IOError):
387 # Failed to load cookies - just ignore them. 398 # Failed to load cookies - just ignore them.
388 pass 399 pass
(...skipping 22 matching lines...) Expand all
411 dest="verbose", default=1, 422 dest="verbose", default=1,
412 help="Print info level logs (default).") 423 help="Print info level logs (default).")
413 group.add_option("--noisy", action="store_const", const=3, 424 group.add_option("--noisy", action="store_const", const=3,
414 dest="verbose", help="Print all logs.") 425 dest="verbose", help="Print all logs.")
415 # Review server 426 # Review server
416 group = parser.add_option_group("Review server options") 427 group = parser.add_option_group("Review server options")
417 group.add_option("-s", "--server", action="store", dest="server", 428 group.add_option("-s", "--server", action="store", dest="server",
418 default="codereview.appspot.com", 429 default="codereview.appspot.com",
419 metavar="SERVER", 430 metavar="SERVER",
420 help=("The server to upload to. The format is host[:port]. " 431 help=("The server to upload to. The format is host[:port]. "
421 "Defaults to 'codereview.appspot.com'.")) 432 "Defaults to '%default'."))
422 group.add_option("-e", "--email", action="store", dest="email", 433 group.add_option("-e", "--email", action="store", dest="email",
423 metavar="EMAIL", default=None, 434 metavar="EMAIL", default=None,
424 help="The username to use. Will prompt if omitted.") 435 help="The username to use. Will prompt if omitted.")
425 group.add_option("-H", "--host", action="store", dest="host", 436 group.add_option("-H", "--host", action="store", dest="host",
426 metavar="HOST", default=None, 437 metavar="HOST", default=None,
427 help="Overrides the Host header sent with all RPCs.") 438 help="Overrides the Host header sent with all RPCs.")
428 group.add_option("--no_cookies", action="store_false", 439 group.add_option("--no_cookies", action="store_false",
429 dest="save_cookies", default=True, 440 dest="save_cookies", default=True,
430 help="Do not save authentication cookies to local disk.") 441 help="Do not save authentication cookies to local disk.")
431 # Issue 442 # Issue
432 group = parser.add_option_group("Issue options") 443 group = parser.add_option_group("Issue options")
433 group.add_option("-d", "--description", action="store", dest="description", 444 group.add_option("-d", "--description", action="store", dest="description",
434 metavar="DESCRIPTION", default=None, 445 metavar="DESCRIPTION", default=None,
435 help="Optional description when creating an issue.") 446 help="Optional description when creating an issue.")
436 group.add_option("-f", "--description_file", action="store", 447 group.add_option("-f", "--description_file", action="store",
437 dest="description_file", metavar="DESCRIPTION_FILE", 448 dest="description_file", metavar="DESCRIPTION_FILE",
438 default=None, 449 default=None,
439 help="Optional path of a file that contains " 450 help="Optional path of a file that contains "
440 "the description when creating an issue.") 451 "the description when creating an issue.")
441 group.add_option("-r", "--reviewers", action="store", dest="reviewers", 452 group.add_option("-r", "--reviewers", action="store", dest="reviewers",
442 metavar="REVIEWERS", default=None, 453 metavar="REVIEWERS", default=None,
443 help="Add reviewers (comma separated email addresses).") 454 help="Add reviewers (comma separated email addresses).")
444 group.add_option("--cc", action="store", dest="cc", 455 group.add_option("--cc", action="store", dest="cc",
445 metavar="CC", default=None, 456 metavar="CC", default=None,
446 help="Add CC (comma separated email addresses).") 457 help="Add CC (comma separated email addresses).")
458 group.add_option("--private", action="store_true", dest="private",
459 default=False,
460 help="Make the issue restricted to reviewers and those CCed")
447 # Upload options 461 # Upload options
448 group = parser.add_option_group("Patch options") 462 group = parser.add_option_group("Patch options")
449 group.add_option("-m", "--message", action="store", dest="message", 463 group.add_option("-m", "--message", action="store", dest="message",
450 metavar="MESSAGE", default=None, 464 metavar="MESSAGE", default=None,
451 help="A message to identify the patch. " 465 help="A message to identify the patch. "
452 "Will prompt if omitted.") 466 "Will prompt if omitted.")
453 group.add_option("-i", "--issue", type="int", action="store", 467 group.add_option("-i", "--issue", type="int", action="store",
454 metavar="ISSUE", default=None, 468 metavar="ISSUE", default=None,
455 help="Issue number to which to add. Defaults to new issue.") 469 help="Issue number to which to add. Defaults to new issue.")
456 group.add_option("--download_base", action="store_true", 470 group.add_option("--download_base", action="store_true",
457 dest="download_base", default=False, 471 dest="download_base", default=False,
458 help="Base files will be downloaded by the server " 472 help="Base files will be downloaded by the server "
459 "(side-by-side diffs may not work on files with CRs).") 473 "(side-by-side diffs may not work on files with CRs).")
460 group.add_option("--rev", action="store", dest="revision", 474 group.add_option("--rev", action="store", dest="revision",
461 metavar="REV", default=None, 475 metavar="REV", default=None,
462 help="Branch/tree/revision to diff against (used by DVCS).") 476 help="Branch/tree/revision to diff against (used by DVCS).")
463 group.add_option("--send_mail", action="store_true", 477 group.add_option("--send_mail", action="store_true",
464 dest="send_mail", default=False, 478 dest="send_mail", default=False,
465 help="Send notification email to reviewers.") 479 help="Send notification email to reviewers.")
480 group.add_option("--vcs", action="store", dest="vcs",
481 metavar="VCS", default=None,
482 help=("Version control system (optional, usually upload.py "
483 "already guesses the right VCS)."))
466 484
467 485
468 def GetRpcServer(options): 486 def GetRpcServer(options):
469 """Returns an instance of an AbstractRpcServer. 487 """Returns an instance of an AbstractRpcServer.
470 488
471 Returns: 489 Returns:
472 A new AbstractRpcServer, on which RPC calls can be made. 490 A new AbstractRpcServer, on which RPC calls can be made.
473 """ 491 """
474 492
475 rpc_server_class = HttpRpcServer 493 rpc_server_class = HttpRpcServer
476 494
477 def GetUserCredentials(): 495 def GetUserCredentials():
478 """Prompts the user for a username and password.""" 496 """Prompts the user for a username and password."""
479 email = options.email 497 email = options.email
480 if email is None: 498 if email is None:
481 email = GetEmail() 499 email = GetEmail("Email (login for uploading to %s)" % options.server)
482 password = getpass.getpass("Password for %s: " % email) 500 password = getpass.getpass("Password for %s: " % email)
483 return (email, password) 501 return (email, password)
484 502
485 # If this is the dev_appserver, use fake authentication. 503 # If this is the dev_appserver, use fake authentication.
486 host = (options.host or options.server).lower() 504 host = (options.host or options.server).lower()
487 if host == "localhost" or host.startswith("localhost:"): 505 if host == "localhost" or host.startswith("localhost:"):
488 email = options.email 506 email = options.email
489 if email is None: 507 if email is None:
490 email = "test@example.com" 508 email = "test@example.com"
491 logging.info("Using debug user %s. Override with --email" % email) 509 logging.info("Using debug user %s. Override with --email" % email)
(...skipping 50 matching lines...) Expand 10 before | Expand all | Expand 10 after
542 560
543 def GetContentType(filename): 561 def GetContentType(filename):
544 """Helper to guess the content-type from the filename.""" 562 """Helper to guess the content-type from the filename."""
545 return mimetypes.guess_type(filename)[0] or 'application/octet-stream' 563 return mimetypes.guess_type(filename)[0] or 'application/octet-stream'
546 564
547 565
548 # Use a shell for subcommands on Windows to get a PATH search. 566 # Use a shell for subcommands on Windows to get a PATH search.
549 use_shell = sys.platform.startswith("win") 567 use_shell = sys.platform.startswith("win")
550 568
551 def RunShellWithReturnCode(command, print_output=False, 569 def RunShellWithReturnCode(command, print_output=False,
552 universal_newlines=True): 570 universal_newlines=True,
571 env=os.environ):
553 """Executes a command and returns the output from stdout and the return code. 572 """Executes a command and returns the output from stdout and the return code.
554 573
555 Args: 574 Args:
556 command: Command to execute. 575 command: Command to execute.
557 print_output: If True, the output is printed to stdout. 576 print_output: If True, the output is printed to stdout.
558 If False, both stdout and stderr are ignored. 577 If False, both stdout and stderr are ignored.
559 universal_newlines: Use universal_newlines flag (default: True). 578 universal_newlines: Use universal_newlines flag (default: True).
560 579
561 Returns: 580 Returns:
562 Tuple (output, return code) 581 Tuple (output, return code)
563 """ 582 """
564 logging.info("Running %s", command) 583 logging.info("Running %s", command)
565 p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, 584 p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
566 shell=use_shell, universal_newlines=universal_newlines) 585 shell=use_shell, universal_newlines=universal_newlines,
586 env=env)
567 if print_output: 587 if print_output:
568 output_array = [] 588 output_array = []
569 while True: 589 while True:
570 line = p.stdout.readline() 590 line = p.stdout.readline()
571 if not line: 591 if not line:
572 break 592 break
573 print line.strip("\n") 593 print line.strip("\n")
574 output_array.append(line) 594 output_array.append(line)
575 output = "".join(output_array) 595 output = "".join(output_array)
576 else: 596 else:
577 output = p.stdout.read() 597 output = p.stdout.read()
578 p.wait() 598 p.wait()
579 errout = p.stderr.read() 599 errout = p.stderr.read()
580 if print_output and errout: 600 if print_output and errout:
581 print >>sys.stderr, errout 601 print >>sys.stderr, errout
582 p.stdout.close() 602 p.stdout.close()
583 p.stderr.close() 603 p.stderr.close()
584 return output, p.returncode 604 return output, p.returncode
585 605
586 606
587 def RunShell(command, silent_ok=False, universal_newlines=True, 607 def RunShell(command, silent_ok=False, universal_newlines=True,
588 print_output=False): 608 print_output=False, env=os.environ):
589 data, retcode = RunShellWithReturnCode(command, print_output, 609 data, retcode = RunShellWithReturnCode(command, print_output,
590 universal_newlines) 610 universal_newlines, env)
591 if retcode: 611 if retcode:
592 ErrorExit("Got error status from %s:\n%s" % (command, data)) 612 ErrorExit("Got error status from %s:\n%s" % (command, data))
593 if not silent_ok and not data: 613 if not silent_ok and not data:
594 ErrorExit("No output from %s" % command) 614 ErrorExit("No output from %s" % command)
595 return data 615 return data
596 616
597 617
598 class VersionControlSystem(object): 618 class VersionControlSystem(object):
599 """Abstract base class providing an interface to the VCS.""" 619 """Abstract base class providing an interface to the VCS."""
600 620
(...skipping 119 matching lines...) Expand 10 before | Expand all | Expand 10 after
720 if new_content != None: 740 if new_content != None:
721 UploadFile(filename, file_id, new_content, is_binary, status, False) 741 UploadFile(filename, file_id, new_content, is_binary, status, False)
722 742
723 def IsImage(self, filename): 743 def IsImage(self, filename):
724 """Returns true if the filename has an image extension.""" 744 """Returns true if the filename has an image extension."""
725 mimetype = mimetypes.guess_type(filename)[0] 745 mimetype = mimetypes.guess_type(filename)[0]
726 if not mimetype: 746 if not mimetype:
727 return False 747 return False
728 return mimetype.startswith("image/") 748 return mimetype.startswith("image/")
729 749
750 def IsBinary(self, filename):
751 """Returns true if the guessed mimetyped isnt't in text group."""
752 mimetype = mimetypes.guess_type(filename)[0]
753 if not mimetype:
754 return False # e.g. README, "real" binaries usually have an extension
755 # special case for text files which don't start with text/
756 if mimetype in TEXT_MIMETYPES:
757 return False
758 return not mimetype.startswith("text/")
759
730 760
731 class SubversionVCS(VersionControlSystem): 761 class SubversionVCS(VersionControlSystem):
732 """Implementation of the VersionControlSystem interface for Subversion.""" 762 """Implementation of the VersionControlSystem interface for Subversion."""
733 763
734 def __init__(self, options): 764 def __init__(self, options):
735 super(SubversionVCS, self).__init__(options) 765 super(SubversionVCS, self).__init__(options)
736 if self.options.revision: 766 if self.options.revision:
737 match = re.match(r"(\d+)(:(\d+))?", self.options.revision) 767 match = re.match(r"(\d+)(:(\d+))?", self.options.revision)
738 if not match: 768 if not match:
739 ErrorExit("Invalid Subversion revision %s." % self.options.revision) 769 ErrorExit("Invalid Subversion revision %s." % self.options.revision)
(...skipping 173 matching lines...) Expand 10 before | Expand all | Expand 10 after
913 # If a file is copied its status will be "A +", which signifies 943 # If a file is copied its status will be "A +", which signifies
914 # "addition-with-history". See "svn st" for more information. We need to 944 # "addition-with-history". See "svn st" for more information. We need to
915 # upload the original file or else diff parsing will fail if the file was 945 # upload the original file or else diff parsing will fail if the file was
916 # edited. 946 # edited.
917 if status[0] == "A" and status[3] != "+": 947 if status[0] == "A" and status[3] != "+":
918 # We'll need to upload the new content if we're adding a binary file 948 # We'll need to upload the new content if we're adding a binary file
919 # since diff's output won't contain it. 949 # since diff's output won't contain it.
920 mimetype = RunShell(["svn", "propget", "svn:mime-type", filename], 950 mimetype = RunShell(["svn", "propget", "svn:mime-type", filename],
921 silent_ok=True) 951 silent_ok=True)
922 base_content = "" 952 base_content = ""
923 is_binary = mimetype and not mimetype.startswith("text/") 953 is_binary = bool(mimetype) and not mimetype.startswith("text/")
924 if is_binary and self.IsImage(filename): 954 if is_binary and self.IsImage(filename):
925 new_content = self.ReadFile(filename) 955 new_content = self.ReadFile(filename)
926 elif (status[0] in ("M", "D", "R") or 956 elif (status[0] in ("M", "D", "R") or
927 (status[0] == "A" and status[3] == "+") or # Copied file. 957 (status[0] == "A" and status[3] == "+") or # Copied file.
928 (status[0] == " " and status[1] == "M")): # Property change. 958 (status[0] == " " and status[1] == "M")): # Property change.
929 args = [] 959 args = []
930 if self.options.revision: 960 if self.options.revision:
931 url = "%s/%s@%s" % (self.svn_base, filename, self.rev_start) 961 url = "%s/%s@%s" % (self.svn_base, filename, self.rev_start)
932 else: 962 else:
933 # Don't change filename, it's needed later. 963 # Don't change filename, it's needed later.
934 url = filename 964 url = filename
935 args += ["-r", "BASE"] 965 args += ["-r", "BASE"]
936 cmd = ["svn"] + args + ["propget", "svn:mime-type", url] 966 cmd = ["svn"] + args + ["propget", "svn:mime-type", url]
937 mimetype, returncode = RunShellWithReturnCode(cmd) 967 mimetype, returncode = RunShellWithReturnCode(cmd)
938 if returncode: 968 if returncode:
939 # File does not exist in the requested revision. 969 # File does not exist in the requested revision.
940 # Reset mimetype, it contains an error message. 970 # Reset mimetype, it contains an error message.
941 mimetype = "" 971 mimetype = ""
942 get_base = False 972 get_base = False
943 is_binary = mimetype and not mimetype.startswith("text/") 973 is_binary = bool(mimetype) and not mimetype.startswith("text/")
944 if status[0] == " ": 974 if status[0] == " ":
945 # Empty base content just to force an upload. 975 # Empty base content just to force an upload.
946 base_content = "" 976 base_content = ""
947 elif is_binary: 977 elif is_binary:
948 if self.IsImage(filename): 978 if self.IsImage(filename):
949 get_base = True 979 get_base = True
950 if status[0] == "M": 980 if status[0] == "M":
951 if not self.rev_end: 981 if not self.rev_end:
952 new_content = self.ReadFile(filename) 982 new_content = self.ReadFile(filename)
953 else: 983 else:
(...skipping 36 matching lines...) Expand 10 before | Expand all | Expand 10 after
990 StatusUpdate("svn status returned unexpected output: %s" % status) 1020 StatusUpdate("svn status returned unexpected output: %s" % status)
991 sys.exit(1) 1021 sys.exit(1)
992 return base_content, new_content, is_binary, status[0:5] 1022 return base_content, new_content, is_binary, status[0:5]
993 1023
994 1024
995 class GitVCS(VersionControlSystem): 1025 class GitVCS(VersionControlSystem):
996 """Implementation of the VersionControlSystem interface for Git.""" 1026 """Implementation of the VersionControlSystem interface for Git."""
997 1027
998 def __init__(self, options): 1028 def __init__(self, options):
999 super(GitVCS, self).__init__(options) 1029 super(GitVCS, self).__init__(options)
1000 # Map of filename -> hash of base file. 1030 # Map of filename -> (hash before, hash after) of base file.
1001 self.base_hashes = {} 1031 # Hashes for "no such file" are represented as None.
1032 self.hashes = {}
1033 # Map of new filename -> old filename for renames.
1034 self.renames = {}
1002 1035
1003 def GenerateDiff(self, extra_args): 1036 def GenerateDiff(self, extra_args):
1004 # This is more complicated than svn's GenerateDiff because we must convert 1037 # This is more complicated than svn's GenerateDiff because we must convert
1005 # the diff output to include an svn-style "Index:" line as well as record 1038 # the diff output to include an svn-style "Index:" line as well as record
1006 # the hashes of the base files, so we can upload them along with our diff. 1039 # the hashes of the files, so we can upload them along with our diff.
1040
1041 # Special used by git to indicate "no such content".
1042 NULL_HASH = "0"*40
1043
1044 extra_args = extra_args[:]
1007 if self.options.revision: 1045 if self.options.revision:
1008 extra_args = [self.options.revision] + extra_args 1046 extra_args = [self.options.revision] + extra_args
1009 gitdiff = RunShell(["git", "diff", "--no-ext-diff", "--full-index"] + 1047
1010 extra_args) 1048 # --no-ext-diff is broken in some versions of Git, so try to work around
1049 # this by overriding the environment (but there is still a problem if the
1050 # git config key "diff.external" is used).
1051 env = os.environ.copy()
1052 if 'GIT_EXTERNAL_DIFF' in env: del env['GIT_EXTERNAL_DIFF']
1053 gitdiff = RunShell(["git", "diff", "--no-ext-diff", "--full-index", "-M"]
1054 + extra_args, env=env)
1011 svndiff = [] 1055 svndiff = []
1012 filecount = 0 1056 filecount = 0
1013 filename = None 1057 filename = None
1014 for line in gitdiff.splitlines(): 1058 for line in gitdiff.splitlines():
1015 match = re.match(r"diff --git a/(.*) b/.*$", line) 1059 match = re.match(r"diff --git a/(.*) b/(.*)$", line)
1016 if match: 1060 if match:
1017 filecount += 1 1061 filecount += 1
1018 filename = match.group(1) 1062 # Intentionally use the "after" filename so we can show renames.
1063 filename = match.group(2)
1019 svndiff.append("Index: %s\n" % filename) 1064 svndiff.append("Index: %s\n" % filename)
1065 if match.group(1) != match.group(2):
1066 self.renames[match.group(2)] = match.group(1)
1020 else: 1067 else:
1021 # The "index" line in a git diff looks like this (long hashes elided): 1068 # The "index" line in a git diff looks like this (long hashes elided):
1022 # index 82c0d44..b2cee3f 100755 1069 # index 82c0d44..b2cee3f 100755
1023 # We want to save the left hash, as that identifies the base file. 1070 # We want to save the left hash, as that identifies the base file.
1024 match = re.match(r"index (\w+)\.\.", line) 1071 match = re.match(r"index (\w+)\.\.(\w+)", line)
1025 if match: 1072 if match:
1026 self.base_hashes[filename] = match.group(1) 1073 before, after = (match.group(1), match.group(2))
1074 if before == NULL_HASH:
1075 before = None
1076 if after == NULL_HASH:
1077 after = None
1078 self.hashes[filename] = (before, after)
1027 svndiff.append(line + "\n") 1079 svndiff.append(line + "\n")
1028 if not filecount: 1080 if not filecount:
1029 ErrorExit("No valid patches found in output from git diff") 1081 ErrorExit("No valid patches found in output from git diff")
1030 return "".join(svndiff) 1082 return "".join(svndiff)
1031 1083
1032 def GetUnknownFiles(self): 1084 def GetUnknownFiles(self):
1033 status = RunShell(["git", "ls-files", "--exclude-standard", "--others"], 1085 status = RunShell(["git", "ls-files", "--exclude-standard", "--others"],
1034 silent_ok=True) 1086 silent_ok=True)
1035 return status.splitlines() 1087 return status.splitlines()
1036 1088
1089 def GetFileContent(self, file_hash, is_binary):
1090 """Returns the content of a file identified by its git hash."""
1091 data, retcode = RunShellWithReturnCode(["git", "show", file_hash],
1092 universal_newlines=not is_binary)
1093 if retcode:
1094 ErrorExit("Got error status from 'git show %s'" % file_hash)
1095 return data
1096
1037 def GetBaseFile(self, filename): 1097 def GetBaseFile(self, filename):
1038 hash = self.base_hashes[filename] 1098 hash_before, hash_after = self.hashes.get(filename, (None,None))
1039 base_content = None 1099 base_content = None
1040 new_content = None 1100 new_content = None
1041 is_binary = False 1101 is_binary = self.IsBinary(filename)
1042 if hash == "0" * 40: # All-zero hash indicates no base file. 1102 status = None
1103
1104 if filename in self.renames:
1105 status = "A +" # Match svn attribute name for renames.
1106 if filename not in self.hashes:
1107 # If a rename doesn't change the content, we never get a hash.
1108 base_content = RunShell(["git", "show", filename])
1109 elif not hash_before:
1043 status = "A" 1110 status = "A"
1044 base_content = "" 1111 base_content = ""
1112 elif not hash_after:
1113 status = "D"
1045 else: 1114 else:
1046 status = "M" 1115 status = "M"
1047 base_content = RunShell(["git", "show", hash]) 1116
1117 is_image = self.IsImage(filename)
1118
1119 # Grab the before/after content if we need it.
1120 # We should include file contents if it's text or it's an image.
1121 if not is_binary or is_image:
1122 # Grab the base content if we don't have it already.
1123 if base_content is None and hash_before:
1124 base_content = self.GetFileContent(hash_before, is_binary)
1125 # Only include the "after" file if it's an image; otherwise it
1126 # it is reconstructed from the diff.
1127 if is_image and hash_after:
1128 new_content = self.GetFileContent(hash_after, is_binary)
1129
1048 return (base_content, new_content, is_binary, status) 1130 return (base_content, new_content, is_binary, status)
1049 1131
1050 1132
1051 class MercurialVCS(VersionControlSystem): 1133 class MercurialVCS(VersionControlSystem):
1052 """Implementation of the VersionControlSystem interface for Mercurial.""" 1134 """Implementation of the VersionControlSystem interface for Mercurial."""
1053 1135
1054 def __init__(self, options, repo_dir): 1136 def __init__(self, options, repo_dir):
1055 super(MercurialVCS, self).__init__(options) 1137 super(MercurialVCS, self).__init__(options)
1056 # Absolute path to repository (we can be in a subdir) 1138 # Absolute path to repository (we can be in a subdir)
1057 self.repo_dir = os.path.normpath(repo_dir) 1139 self.repo_dir = os.path.normpath(repo_dir)
1058 # Compute the subdir 1140 # Compute the subdir
1059 cwd = os.path.normpath(os.getcwd()) 1141 cwd = os.path.normpath(os.getcwd())
1060 assert cwd.startswith(self.repo_dir) 1142 assert cwd.startswith(self.repo_dir)
1061 self.subdir = cwd[len(self.repo_dir):].lstrip(r"\/") 1143 self.subdir = cwd[len(self.repo_dir):].lstrip(r"\/")
1062 if self.options.revision: 1144 if self.options.revision:
1063 self.base_rev = self.options.revision 1145 self.base_rev = self.options.revision
1064 else: 1146 else:
1065 self.base_rev = RunShell(["hg", "parent", "-q"]).split(':')[1].strip() 1147 self.base_rev = RunShell(["hg", "parent", "-q"]).split(':')[1].strip()
1066 1148
1067 def _GetRelPath(self, filename): 1149 def _GetRelPath(self, filename):
1068 """Get relative path of a file according to the current directory, 1150 """Get relative path of a file according to the current directory,
1069 given its logical path in the repo.""" 1151 given its logical path in the repo."""
1070 assert filename.startswith(self.subdir), filename 1152 assert filename.startswith(self.subdir), (filename, self.subdir)
1071 return filename[len(self.subdir):].lstrip(r"\/") 1153 return filename[len(self.subdir):].lstrip(r"\/")
1072 1154
1073 def GenerateDiff(self, extra_args): 1155 def GenerateDiff(self, extra_args):
1074 # If no file specified, restrict to the current subdir 1156 # If no file specified, restrict to the current subdir
1075 extra_args = extra_args or ["."] 1157 extra_args = extra_args or ["."]
1076 cmd = ["hg", "diff", "--git", "-r", self.base_rev] + extra_args 1158 cmd = ["hg", "diff", "--git", "-r", self.base_rev] + extra_args
1077 data = RunShell(cmd, silent_ok=True) 1159 data = RunShell(cmd, silent_ok=True)
1078 svndiff = [] 1160 svndiff = []
1079 filecount = 0 1161 filecount = 0
1080 for line in data.splitlines(): 1162 for line in data.splitlines():
(...skipping 42 matching lines...) Expand 10 before | Expand all | Expand 10 after
1123 # the working copy 1205 # the working copy
1124 if out[0].startswith('%s: ' % relpath): 1206 if out[0].startswith('%s: ' % relpath):
1125 out = out[1:] 1207 out = out[1:]
1126 if len(out) > 1: 1208 if len(out) > 1:
1127 # Moved/copied => considered as modified, use old filename to 1209 # Moved/copied => considered as modified, use old filename to
1128 # retrieve base contents 1210 # retrieve base contents
1129 oldrelpath = out[1].strip() 1211 oldrelpath = out[1].strip()
1130 status = "M" 1212 status = "M"
1131 else: 1213 else:
1132 status, _ = out[0].split(' ', 1) 1214 status, _ = out[0].split(' ', 1)
1215 if ":" in self.base_rev:
1216 base_rev = self.base_rev.split(":", 1)[0]
1217 else:
1218 base_rev = self.base_rev
1133 if status != "A": 1219 if status != "A":
1134 base_content = RunShell(["hg", "cat", "-r", self.base_rev, oldrelpath], 1220 base_content = RunShell(["hg", "cat", "-r", base_rev, oldrelpath],
1135 silent_ok=True) 1221 silent_ok=True)
1136 is_binary = "\0" in base_content # Mercurial's heuristic 1222 is_binary = "\0" in base_content # Mercurial's heuristic
1137 if status != "R": 1223 if status != "R":
1138 new_content = open(relpath, "rb").read() 1224 new_content = open(relpath, "rb").read()
1139 is_binary = is_binary or "\0" in new_content 1225 is_binary = is_binary or "\0" in new_content
1140 if is_binary and base_content: 1226 if is_binary and base_content:
1141 # Fetch again without converting newlines 1227 # Fetch again without converting newlines
1142 base_content = RunShell(["hg", "cat", "-r", self.base_rev, oldrelpath], 1228 base_content = RunShell(["hg", "cat", "-r", base_rev, oldrelpath],
1143 silent_ok=True, universal_newlines=False) 1229 silent_ok=True, universal_newlines=False)
1144 if not is_binary or not self.IsImage(relpath): 1230 if not is_binary or not self.IsImage(relpath):
1145 new_content = None 1231 new_content = None
1146 return base_content, new_content, is_binary, status 1232 return base_content, new_content, is_binary, status
1147 1233
1148 1234
1149 # NOTE: The SplitPatch function is duplicated in engine.py, keep them in sync. 1235 # NOTE: The SplitPatch function is duplicated in engine.py, keep them in sync.
1150 def SplitPatch(data): 1236 def SplitPatch(data):
1151 """Splits a patch into separate pieces for each file. 1237 """Splits a patch into separate pieces for each file.
1152 1238
(...skipping 55 matching lines...) Expand 10 before | Expand all | Expand 10 after
1208 print "Uploading patch for " + patch[0] 1294 print "Uploading patch for " + patch[0]
1209 response_body = rpc_server.Send(url, body, content_type=ctype) 1295 response_body = rpc_server.Send(url, body, content_type=ctype)
1210 lines = response_body.splitlines() 1296 lines = response_body.splitlines()
1211 if not lines or lines[0] != "OK": 1297 if not lines or lines[0] != "OK":
1212 StatusUpdate(" --> %s" % response_body) 1298 StatusUpdate(" --> %s" % response_body)
1213 sys.exit(1) 1299 sys.exit(1)
1214 rv.append([lines[1], patch[0]]) 1300 rv.append([lines[1], patch[0]])
1215 return rv 1301 return rv
1216 1302
1217 1303
1218 def GuessVCS(options): 1304 def GuessVCSName():
1219 """Helper to guess the version control system. 1305 """Helper to guess the version control system.
1220 1306
1221 This examines the current directory, guesses which VersionControlSystem 1307 This examines the current directory, guesses which VersionControlSystem
1222 we're using, and returns an instance of the appropriate class. Exit with an 1308 we're using, and returns an string indicating which VCS is detected.
1223 error if we can't figure it out.
1224 1309
1225 Returns: 1310 Returns:
1226 A VersionControlSystem instance. Exits if the VCS can't be guessed. 1311 A pair (vcs, output). vcs is a string indicating which VCS was detected
1312 and is one of VCS_GIT, VCS_MERCURIAL, VCS_SUBVERSION, or VCS_UNKNOWN.
1313 output is a string containing any interesting output from the vcs
1314 detection routine, or None if there is nothing interesting.
1227 """ 1315 """
1228 # Mercurial has a command to get the base directory of a repository 1316 # Mercurial has a command to get the base directory of a repository
1229 # Try running it, but don't die if we don't have hg installed. 1317 # Try running it, but don't die if we don't have hg installed.
1230 # NOTE: we try Mercurial first as it can sit on top of an SVN working copy. 1318 # NOTE: we try Mercurial first as it can sit on top of an SVN working copy.
1231 try: 1319 try:
1232 out, returncode = RunShellWithReturnCode(["hg", "root"]) 1320 out, returncode = RunShellWithReturnCode(["hg", "root"])
1233 if returncode == 0: 1321 if returncode == 0:
1234 return MercurialVCS(options, out.strip()) 1322 return (VCS_MERCURIAL, out.strip())
1235 except OSError, (errno, message): 1323 except OSError, (errno, message):
1236 if errno != 2: # ENOENT -- they don't have hg installed. 1324 if errno != 2: # ENOENT -- they don't have hg installed.
1237 raise 1325 raise
1238 1326
1239 # Subversion has a .svn in all working directories. 1327 # Subversion has a .svn in all working directories.
1240 if os.path.isdir('.svn'): 1328 if os.path.isdir('.svn'):
1241 logging.info("Guessed VCS = Subversion") 1329 logging.info("Guessed VCS = Subversion")
1242 return SubversionVCS(options) 1330 return (VCS_SUBVERSION, None)
1243 1331
1244 # Git has a command to test if you're in a git tree. 1332 # Git has a command to test if you're in a git tree.
1245 # Try running it, but don't die if we don't have git installed. 1333 # Try running it, but don't die if we don't have git installed.
1246 try: 1334 try:
1247 out, returncode = RunShellWithReturnCode(["git", "rev-parse", 1335 out, returncode = RunShellWithReturnCode(["git", "rev-parse",
1248 "--is-inside-work-tree"]) 1336 "--is-inside-work-tree"])
1249 if returncode == 0: 1337 if returncode == 0:
1250 return GitVCS(options) 1338 return (VCS_GIT, None)
1251 except OSError, (errno, message): 1339 except OSError, (errno, message):
1252 if errno != 2: # ENOENT -- they don't have git installed. 1340 if errno != 2: # ENOENT -- they don't have git installed.
1253 raise 1341 raise
1254 1342
1343 return (VCS_UNKNOWN, None)
1344
1345
1346 def GuessVCS(options):
1347 """Helper to guess the version control system.
1348
1349 This verifies any user-specified VersionControlSystem (by command line
1350 or environment variable). If the user didn't specify one, this examines
1351 the current directory, guesses which VersionControlSystem we're using,
1352 and returns an instance of the appropriate class. Exit with an error
1353 if we can't figure it out.
1354
1355 Returns:
1356 A VersionControlSystem instance. Exits if the VCS can't be guessed.
1357 """
1358 vcs = options.vcs
1359 if not vcs:
1360 vcs = os.environ.get("CODEREVIEW_VCS")
1361 if vcs:
1362 v = VCS_ABBREVIATIONS.get(vcs.lower())
1363 if v is None:
1364 ErrorExit("Unknown version control system %r specified." % vcs)
1365 (vcs, extra_output) = (v, None)
1366 else:
1367 (vcs, extra_output) = GuessVCSName()
1368
1369 if vcs == VCS_MERCURIAL:
1370 if extra_output is None:
1371 extra_output = RunShell(["hg", "root"]).strip()
1372 return MercurialVCS(options, extra_output)
1373 elif vcs == VCS_SUBVERSION:
1374 return SubversionVCS(options)
1375 elif vcs == VCS_GIT:
1376 return GitVCS(options)
1377
1255 ErrorExit(("Could not guess version control system. " 1378 ErrorExit(("Could not guess version control system. "
1256 "Are you in a working copy directory?")) 1379 "Are you in a working copy directory?"))
1257 1380
1258 1381
1382 def CheckReviewer(reviewer):
1383 """Validate a reviewer -- either a nickname or an email addres.
1384
1385 Args:
1386 reviewer: A nickname or an email address.
1387
1388 Calls ErrorExit() if it is an invalid email address.
1389 """
1390 if "@" not in reviewer:
1391 return # Assume nickname
1392 parts = reviewer.split("@")
1393 if len(parts) > 2:
1394 ErrorExit("Invalid email address: %r" % reviewer)
1395 assert len(parts) == 2
1396 if "." not in parts[1]:
1397 ErrorExit("Invalid email address: %r" % reviewer)
1398
1399
1259 def RealMain(argv, data=None): 1400 def RealMain(argv, data=None):
1401 """The real main function.
1402
1403 Args:
1404 argv: Command line arguments.
1405 data: Diff contents. If None (default) the diff is generated by
1406 the VersionControlSystem implementation returned by GuessVCS().
1407
1408 Returns:
1409 A 2-tuple (issue id, patchset id).
1410 The patchset id is None if the base files are not uploaded by this
1411 script (applies only to SVN checkouts).
1412 """
1260 logging.basicConfig(format=("%(asctime).19s %(levelname)s %(filename)s:" 1413 logging.basicConfig(format=("%(asctime).19s %(levelname)s %(filename)s:"
1261 "%(lineno)s %(message)s ")) 1414 "%(lineno)s %(message)s "))
1262 os.environ['LC_ALL'] = 'C' 1415 os.environ['LC_ALL'] = 'C'
1263 options, args = parser.parse_args(argv[1:]) 1416 options, args = parser.parse_args(argv[1:])
1264 global verbosity 1417 global verbosity
1265 verbosity = options.verbose 1418 verbosity = options.verbose
1266 if verbosity >= 3: 1419 if verbosity >= 3:
1267 logging.getLogger().setLevel(logging.DEBUG) 1420 logging.getLogger().setLevel(logging.DEBUG)
1268 elif verbosity >= 2: 1421 elif verbosity >= 2:
1269 logging.getLogger().setLevel(logging.INFO) 1422 logging.getLogger().setLevel(logging.INFO)
(...skipping 24 matching lines...) Expand all
1294 rpc_server = GetRpcServer(options) 1447 rpc_server = GetRpcServer(options)
1295 form_fields = [("subject", message)] 1448 form_fields = [("subject", message)]
1296 if base: 1449 if base:
1297 form_fields.append(("base", base)) 1450 form_fields.append(("base", base))
1298 if options.issue: 1451 if options.issue:
1299 form_fields.append(("issue", str(options.issue))) 1452 form_fields.append(("issue", str(options.issue)))
1300 if options.email: 1453 if options.email:
1301 form_fields.append(("user", options.email)) 1454 form_fields.append(("user", options.email))
1302 if options.reviewers: 1455 if options.reviewers:
1303 for reviewer in options.reviewers.split(','): 1456 for reviewer in options.reviewers.split(','):
1304 if "@" in reviewer and not reviewer.split("@")[1].count(".") == 1: 1457 CheckReviewer(reviewer)
1305 ErrorExit("Invalid email address: %s" % reviewer)
1306 form_fields.append(("reviewers", options.reviewers)) 1458 form_fields.append(("reviewers", options.reviewers))
1307 if options.cc: 1459 if options.cc:
1308 for cc in options.cc.split(','): 1460 for cc in options.cc.split(','):
1309 if "@" in cc and not cc.split("@")[1].count(".") == 1: 1461 CheckReviewer(cc)
1310 ErrorExit("Invalid email address: %s" % cc)
1311 form_fields.append(("cc", options.cc)) 1462 form_fields.append(("cc", options.cc))
1312 description = options.description 1463 description = options.description
1313 if options.description_file: 1464 if options.description_file:
1314 if options.description: 1465 if options.description:
1315 ErrorExit("Can't specify description and description_file") 1466 ErrorExit("Can't specify description and description_file")
1316 file = open(options.description_file, 'r') 1467 file = open(options.description_file, 'r')
1317 description = file.read() 1468 description = file.read()
1318 file.close() 1469 file.close()
1319 if description: 1470 if description:
1320 form_fields.append(("description", description)) 1471 form_fields.append(("description", description))
1321 # Send a hash of all the base file so the server can determine if a copy 1472 # Send a hash of all the base file so the server can determine if a copy
1322 # already exists in an earlier patchset. 1473 # already exists in an earlier patchset.
1323 base_hashes = "" 1474 base_hashes = ""
1324 for file, info in files.iteritems(): 1475 for file, info in files.iteritems():
1325 if not info[0] is None: 1476 if not info[0] is None:
1326 checksum = md5(info[0]).hexdigest() 1477 checksum = md5(info[0]).hexdigest()
1327 if base_hashes: 1478 if base_hashes:
1328 base_hashes += "|" 1479 base_hashes += "|"
1329 base_hashes += checksum + ":" + file 1480 base_hashes += checksum + ":" + file
1330 form_fields.append(("base_hashes", base_hashes)) 1481 form_fields.append(("base_hashes", base_hashes))
1482 if options.private:
1483 if options.issue:
1484 print "Warning: Private flag ignored when updating an existing issue."
1485 else:
1486 form_fields.append(("private", "1"))
1331 # If we're uploading base files, don't send the email before the uploads, so 1487 # If we're uploading base files, don't send the email before the uploads, so
1332 # that it contains the file status. 1488 # that it contains the file status.
1333 if options.send_mail and options.download_base: 1489 if options.send_mail and options.download_base:
1334 form_fields.append(("send_mail", "1")) 1490 form_fields.append(("send_mail", "1"))
1335 if not options.download_base: 1491 if not options.download_base:
1336 form_fields.append(("content_upload", "1")) 1492 form_fields.append(("content_upload", "1"))
1337 if len(data) > MAX_UPLOAD_SIZE: 1493 if len(data) > MAX_UPLOAD_SIZE:
1338 print "Patch is large, so uploading file patches separately." 1494 print "Patch is large, so uploading file patches separately."
1339 uploaded_diff_file = [] 1495 uploaded_diff_file = []
1340 form_fields.append(("separate_patches", "1")) 1496 form_fields.append(("separate_patches", "1"))
1341 else: 1497 else:
1342 uploaded_diff_file = [("data", "data.diff", data)] 1498 uploaded_diff_file = [("data", "data.diff", data)]
1343 ctype, body = EncodeMultipartFormData(form_fields, uploaded_diff_file) 1499 ctype, body = EncodeMultipartFormData(form_fields, uploaded_diff_file)
1344 response_body = rpc_server.Send("/upload", body, content_type=ctype) 1500 response_body = rpc_server.Send("/upload", body, content_type=ctype)
1501 patchset = None
1345 if not options.download_base or not uploaded_diff_file: 1502 if not options.download_base or not uploaded_diff_file:
1346 lines = response_body.splitlines() 1503 lines = response_body.splitlines()
1347 if len(lines) >= 2: 1504 if len(lines) >= 2:
1348 msg = lines[0] 1505 msg = lines[0]
1349 patchset = lines[1].strip() 1506 patchset = lines[1].strip()
1350 patches = [x.split(" ", 1) for x in lines[2:]] 1507 patches = [x.split(" ", 1) for x in lines[2:]]
1351 for patch_pair in patches:
1352 # On Windows if a file has property changes its filename uses '\'
1353 # instead of '/'. Perhaps this change should be made (also) on the
1354 # server when it is decoding the patch file sent by the client, but
1355 # we do it here as well to be safe.
1356 patch_pair[1] = patch_pair[1].replace('\\', '/')
1357 else: 1508 else:
1358 msg = response_body 1509 msg = response_body
1359 else: 1510 else:
1360 msg = response_body 1511 msg = response_body
1361 StatusUpdate(msg) 1512 StatusUpdate(msg)
1362 if not response_body.startswith("Issue created.") and \ 1513 if not response_body.startswith("Issue created.") and \
1363 not response_body.startswith("Issue updated."): 1514 not response_body.startswith("Issue updated."):
1364 sys.exit(0) 1515 sys.exit(0)
1365 issue = msg[msg.rfind("/")+1:] 1516 issue = msg[msg.rfind("/")+1:]
1366 1517
(...skipping 13 matching lines...) Expand all
1380 try: 1531 try:
1381 RealMain(sys.argv) 1532 RealMain(sys.argv)
1382 except KeyboardInterrupt: 1533 except KeyboardInterrupt:
1383 print 1534 print
1384 StatusUpdate("Interrupted.") 1535 StatusUpdate("Interrupted.")
1385 sys.exit(1) 1536 sys.exit(1)
1386 1537
1387 1538
1388 if __name__ == "__main__": 1539 if __name__ == "__main__":
1389 main() 1540 main()
OLDNEW
« no previous file with comments | « no previous file | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698