| OLD | NEW | 
|     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 40 matching lines...) Expand 10 before | Expand all | Expand 10 after  Loading... | 
|    51 try: |    51 try: | 
|    52   from hashlib import md5 |    52   from hashlib import md5 | 
|    53 except ImportError: |    53 except ImportError: | 
|    54   from md5 import md5 |    54   from md5 import md5 | 
|    55  |    55  | 
|    56 try: |    56 try: | 
|    57   import readline |    57   import readline | 
|    58 except ImportError: |    58 except ImportError: | 
|    59   pass |    59   pass | 
|    60  |    60  | 
 |    61 try: | 
 |    62   import keyring | 
 |    63 except ImportError: | 
 |    64   keyring = None | 
 |    65  | 
|    61 # The logging verbosity: |    66 # The logging verbosity: | 
|    62 #  0: Errors only. |    67 #  0: Errors only. | 
|    63 #  1: Status messages. |    68 #  1: Status messages. | 
|    64 #  2: Info logs. |    69 #  2: Info logs. | 
|    65 #  3: Debug logs. |    70 #  3: Debug logs. | 
|    66 verbosity = 1 |    71 verbosity = 1 | 
|    67  |    72  | 
 |    73 # The account type used for authentication. | 
 |    74 # This line could be changed by the review server (see handler for | 
 |    75 # upload.py). | 
 |    76 AUTH_ACCOUNT_TYPE = "GOOGLE" | 
 |    77  | 
 |    78 # URL of the default review server. As for AUTH_ACCOUNT_TYPE, this line could be | 
 |    79 # changed by the review server (see handler for upload.py). | 
 |    80 DEFAULT_REVIEW_SERVER = "codereview.appspot.com" | 
 |    81  | 
|    68 # Max size of patch or base file. |    82 # Max size of patch or base file. | 
|    69 MAX_UPLOAD_SIZE = 900 * 1024 |    83 MAX_UPLOAD_SIZE = 900 * 1024 | 
|    70  |    84  | 
|    71 # Constants for version control names.  Used by GuessVCSName. |    85 # Constants for version control names.  Used by GuessVCSName. | 
|    72 VCS_GIT = "Git" |    86 VCS_GIT = "Git" | 
|    73 VCS_MERCURIAL = "Mercurial" |    87 VCS_MERCURIAL = "Mercurial" | 
|    74 VCS_SUBVERSION = "Subversion" |    88 VCS_SUBVERSION = "Subversion" | 
|    75 VCS_UNKNOWN = "Unknown" |    89 VCS_UNKNOWN = "Unknown" | 
|    76  |    90  | 
|    77 # whitelist for non-binary filetypes which do not start with "text/" |    91 # whitelist for non-binary filetypes which do not start with "text/" | 
|    78 # .mm (Objective-C) shows up as application/x-freemind on my Linux box. |    92 # .mm (Objective-C) shows up as application/x-freemind on my Linux box. | 
|    79 TEXT_MIMETYPES = ['application/javascript', 'application/x-javascript', |    93 TEXT_MIMETYPES = ['application/javascript', 'application/x-javascript', | 
|    80                   'application/xml', 'application/x-freemind'] |    94                   'application/xml', 'application/x-freemind',  | 
 |    95                   'application/x-sh'] | 
|    81  |    96  | 
|    82 VCS_ABBREVIATIONS = { |    97 VCS_ABBREVIATIONS = { | 
|    83   VCS_MERCURIAL.lower(): VCS_MERCURIAL, |    98   VCS_MERCURIAL.lower(): VCS_MERCURIAL, | 
|    84   "hg": VCS_MERCURIAL, |    99   "hg": VCS_MERCURIAL, | 
|    85   VCS_SUBVERSION.lower(): VCS_SUBVERSION, |   100   VCS_SUBVERSION.lower(): VCS_SUBVERSION, | 
|    86   "svn": VCS_SUBVERSION, |   101   "svn": VCS_SUBVERSION, | 
|    87   VCS_GIT.lower(): VCS_GIT, |   102   VCS_GIT.lower(): VCS_GIT, | 
|    88 } |   103 } | 
|    89  |   104  | 
|    90 # The result of parsing Subversion's [auto-props] setting. |   105 # The result of parsing Subversion's [auto-props] setting. | 
| (...skipping 55 matching lines...) Expand 10 before | Expand all | Expand 10 after  Loading... | 
|   146   def __init__(self, url, code, msg, headers, args): |   161   def __init__(self, url, code, msg, headers, args): | 
|   147     urllib2.HTTPError.__init__(self, url, code, msg, headers, None) |   162     urllib2.HTTPError.__init__(self, url, code, msg, headers, None) | 
|   148     self.args = args |   163     self.args = args | 
|   149     self.reason = args["Error"] |   164     self.reason = args["Error"] | 
|   150  |   165  | 
|   151  |   166  | 
|   152 class AbstractRpcServer(object): |   167 class AbstractRpcServer(object): | 
|   153   """Provides a common interface for a simple RPC server.""" |   168   """Provides a common interface for a simple RPC server.""" | 
|   154  |   169  | 
|   155   def __init__(self, host, auth_function, host_override=None, extra_headers={}, |   170   def __init__(self, host, auth_function, host_override=None, extra_headers={}, | 
|   156                save_cookies=False): |   171                save_cookies=False, account_type=AUTH_ACCOUNT_TYPE): | 
|   157     """Creates a new HttpRpcServer. |   172     """Creates a new HttpRpcServer. | 
|   158  |   173  | 
|   159     Args: |   174     Args: | 
|   160       host: The host to send requests to. |   175       host: The host to send requests to. | 
|   161       auth_function: A function that takes no arguments and returns an |   176       auth_function: A function that takes no arguments and returns an | 
|   162         (email, password) tuple when called. Will be called if authentication |   177         (email, password) tuple when called. Will be called if authentication | 
|   163         is required. |   178         is required. | 
|   164       host_override: The host header to send to the server (defaults to host). |   179       host_override: The host header to send to the server (defaults to host). | 
|   165       extra_headers: A dict of extra headers to append to every request. |   180       extra_headers: A dict of extra headers to append to every request. | 
|   166       save_cookies: If True, save the authentication cookies to local disk. |   181       save_cookies: If True, save the authentication cookies to local disk. | 
|   167         If False, use an in-memory cookiejar instead.  Subclasses must |   182         If False, use an in-memory cookiejar instead.  Subclasses must | 
|   168         implement this functionality.  Defaults to False. |   183         implement this functionality.  Defaults to False. | 
 |   184       account_type: Account type used for authentication. Defaults to | 
 |   185         AUTH_ACCOUNT_TYPE. | 
|   169     """ |   186     """ | 
|   170     self.host = host |   187     self.host = host | 
|   171     if (not self.host.startswith("http://") and |   188     if (not self.host.startswith("http://") and | 
|   172         not self.host.startswith("https://")): |   189         not self.host.startswith("https://")): | 
|   173       self.host = "http://" + self.host |   190       self.host = "http://" + self.host | 
|   174     self.host_override = host_override |   191     self.host_override = host_override | 
|   175     self.auth_function = auth_function |   192     self.auth_function = auth_function | 
|   176     self.authenticated = False |   193     self.authenticated = False | 
|   177     self.extra_headers = extra_headers |   194     self.extra_headers = extra_headers | 
|   178     self.save_cookies = save_cookies |   195     self.save_cookies = save_cookies | 
 |   196     self.account_type = account_type | 
|   179     self.opener = self._GetOpener() |   197     self.opener = self._GetOpener() | 
|   180     if self.host_override: |   198     if self.host_override: | 
|   181       logging.info("Server: %s; Host: %s", self.host, self.host_override) |   199       logging.info("Server: %s; Host: %s", self.host, self.host_override) | 
|   182     else: |   200     else: | 
|   183       logging.info("Server: %s", self.host) |   201       logging.info("Server: %s", self.host) | 
|   184  |   202  | 
|   185   def _GetOpener(self): |   203   def _GetOpener(self): | 
|   186     """Returns an OpenerDirector for making HTTP requests. |   204     """Returns an OpenerDirector for making HTTP requests. | 
|   187  |   205  | 
|   188     Returns: |   206     Returns: | 
| (...skipping 18 matching lines...) Expand all  Loading... | 
|   207       email:    The user's email address |   225       email:    The user's email address | 
|   208       password: The user's password |   226       password: The user's password | 
|   209  |   227  | 
|   210     Raises: |   228     Raises: | 
|   211       ClientLoginError: If there was an error authenticating with ClientLogin. |   229       ClientLoginError: If there was an error authenticating with ClientLogin. | 
|   212       HTTPError: If there was some other form of HTTP error. |   230       HTTPError: If there was some other form of HTTP error. | 
|   213  |   231  | 
|   214     Returns: |   232     Returns: | 
|   215       The authentication token returned by ClientLogin. |   233       The authentication token returned by ClientLogin. | 
|   216     """ |   234     """ | 
|   217     account_type = "GOOGLE" |   235     account_type = self.account_type | 
|   218     if self.host.endswith(".google.com"): |   236     if self.host.endswith(".google.com"): | 
|   219       # Needed for use inside Google. |   237       # Needed for use inside Google. | 
|   220       account_type = "HOSTED" |   238       account_type = "HOSTED" | 
|   221     req = self._CreateRequest( |   239     req = self._CreateRequest( | 
|   222         url="https://www.google.com/accounts/ClientLogin", |   240         url="https://www.google.com/accounts/ClientLogin", | 
|   223         data=urllib.urlencode({ |   241         data=urllib.urlencode({ | 
|   224             "Email": email, |   242             "Email": email, | 
|   225             "Passwd": password, |   243             "Passwd": password, | 
|   226             "service": "ah", |   244             "service": "ah", | 
|   227             "source": "rietveld-codereview-upload", |   245             "source": "rietveld-codereview-upload", | 
| (...skipping 59 matching lines...) Expand 10 before | Expand all | Expand 10 after  Loading... | 
|   287       try: |   305       try: | 
|   288         auth_token = self._GetAuthToken(credentials[0], credentials[1]) |   306         auth_token = self._GetAuthToken(credentials[0], credentials[1]) | 
|   289       except ClientLoginError, e: |   307       except ClientLoginError, e: | 
|   290         if e.reason == "BadAuthentication": |   308         if e.reason == "BadAuthentication": | 
|   291           print >>sys.stderr, "Invalid username or password." |   309           print >>sys.stderr, "Invalid username or password." | 
|   292           continue |   310           continue | 
|   293         if e.reason == "CaptchaRequired": |   311         if e.reason == "CaptchaRequired": | 
|   294           print >>sys.stderr, ( |   312           print >>sys.stderr, ( | 
|   295               "Please go to\n" |   313               "Please go to\n" | 
|   296               "https://www.google.com/accounts/DisplayUnlockCaptcha\n" |   314               "https://www.google.com/accounts/DisplayUnlockCaptcha\n" | 
|   297               "and verify you are a human.  Then try again.") |   315               "and verify you are a human.  Then try again.\n" | 
 |   316               "If you are using a Google Apps account the URL is:\n" | 
 |   317               "https://www.google.com/a/yourdomain.com/UnlockCaptcha") | 
|   298           break |   318           break | 
|   299         if e.reason == "NotVerified": |   319         if e.reason == "NotVerified": | 
|   300           print >>sys.stderr, "Account not verified." |   320           print >>sys.stderr, "Account not verified." | 
|   301           break |   321           break | 
|   302         if e.reason == "TermsNotAgreed": |   322         if e.reason == "TermsNotAgreed": | 
|   303           print >>sys.stderr, "User has not agreed to TOS." |   323           print >>sys.stderr, "User has not agreed to TOS." | 
|   304           break |   324           break | 
|   305         if e.reason == "AccountDeleted": |   325         if e.reason == "AccountDeleted": | 
|   306           print >>sys.stderr, "The user account has been deleted." |   326           print >>sys.stderr, "The user account has been deleted." | 
|   307           break |   327           break | 
| (...skipping 128 matching lines...) Expand 10 before | Expand all | Expand 10 after  Loading... | 
|   436 group.add_option("-q", "--quiet", action="store_const", const=0, |   456 group.add_option("-q", "--quiet", action="store_const", const=0, | 
|   437                  dest="verbose", help="Print errors only.") |   457                  dest="verbose", help="Print errors only.") | 
|   438 group.add_option("-v", "--verbose", action="store_const", const=2, |   458 group.add_option("-v", "--verbose", action="store_const", const=2, | 
|   439                  dest="verbose", default=1, |   459                  dest="verbose", default=1, | 
|   440                  help="Print info level logs (default).") |   460                  help="Print info level logs (default).") | 
|   441 group.add_option("--noisy", action="store_const", const=3, |   461 group.add_option("--noisy", action="store_const", const=3, | 
|   442                  dest="verbose", help="Print all logs.") |   462                  dest="verbose", help="Print all logs.") | 
|   443 # Review server |   463 # Review server | 
|   444 group = parser.add_option_group("Review server options") |   464 group = parser.add_option_group("Review server options") | 
|   445 group.add_option("-s", "--server", action="store", dest="server", |   465 group.add_option("-s", "--server", action="store", dest="server", | 
|   446                  default="codereview.appspot.com", |   466                  default=DEFAULT_REVIEW_SERVER, | 
|   447                  metavar="SERVER", |   467                  metavar="SERVER", | 
|   448                  help=("The server to upload to. The format is host[:port]. " |   468                  help=("The server to upload to. The format is host[:port]. " | 
|   449                        "Defaults to '%default'.")) |   469                        "Defaults to '%default'.")) | 
|   450 group.add_option("-e", "--email", action="store", dest="email", |   470 group.add_option("-e", "--email", action="store", dest="email", | 
|   451                  metavar="EMAIL", default=None, |   471                  metavar="EMAIL", default=None, | 
|   452                  help="The username to use. Will prompt if omitted.") |   472                  help="The username to use. Will prompt if omitted.") | 
|   453 group.add_option("-H", "--host", action="store", dest="host", |   473 group.add_option("-H", "--host", action="store", dest="host", | 
|   454                  metavar="HOST", default=None, |   474                  metavar="HOST", default=None, | 
|   455                  help="Overrides the Host header sent with all RPCs.") |   475                  help="Overrides the Host header sent with all RPCs.") | 
|   456 group.add_option("--no_cookies", action="store_false", |   476 group.add_option("--no_cookies", action="store_false", | 
|   457                  dest="save_cookies", default=True, |   477                  dest="save_cookies", default=True, | 
|   458                  help="Do not save authentication cookies to local disk.") |   478                  help="Do not save authentication cookies to local disk.") | 
 |   479 group.add_option("--account_type", action="store", dest="account_type", | 
 |   480                  metavar="TYPE", default=AUTH_ACCOUNT_TYPE, | 
 |   481                  choices=["GOOGLE", "HOSTED"], | 
 |   482                  help=("Override the default account type " | 
 |   483                        "(defaults to '%default', " | 
 |   484                        "valid choices are 'GOOGLE' and 'HOSTED').")) | 
|   459 # Issue |   485 # Issue | 
|   460 group = parser.add_option_group("Issue options") |   486 group = parser.add_option_group("Issue options") | 
|   461 group.add_option("-d", "--description", action="store", dest="description", |   487 group.add_option("-d", "--description", action="store", dest="description", | 
|   462                  metavar="DESCRIPTION", default=None, |   488                  metavar="DESCRIPTION", default=None, | 
|   463                  help="Optional description when creating an issue.") |   489                  help="Optional description when creating an issue.") | 
|   464 group.add_option("-f", "--description_file", action="store", |   490 group.add_option("-f", "--description_file", action="store", | 
|   465                  dest="description_file", metavar="DESCRIPTION_FILE", |   491                  dest="description_file", metavar="DESCRIPTION_FILE", | 
|   466                  default=None, |   492                  default=None, | 
|   467                  help="Optional path of a file that contains " |   493                  help="Optional path of a file that contains " | 
|   468                       "the description when creating an issue.") |   494                       "the description when creating an issue.") | 
| (...skipping 32 matching lines...) Expand 10 before | Expand all | Expand 10 after  Loading... | 
|   501                  help="Send notification email to reviewers.") |   527                  help="Send notification email to reviewers.") | 
|   502 group.add_option("--vcs", action="store", dest="vcs", |   528 group.add_option("--vcs", action="store", dest="vcs", | 
|   503                  metavar="VCS", default=None, |   529                  metavar="VCS", default=None, | 
|   504                  help=("Version control system (optional, usually upload.py " |   530                  help=("Version control system (optional, usually upload.py " | 
|   505                        "already guesses the right VCS).")) |   531                        "already guesses the right VCS).")) | 
|   506 group.add_option("--emulate_svn_auto_props", action="store_true", |   532 group.add_option("--emulate_svn_auto_props", action="store_true", | 
|   507                  dest="emulate_svn_auto_props", default=False, |   533                  dest="emulate_svn_auto_props", default=False, | 
|   508                  help=("Emulate Subversion's auto properties feature.")) |   534                  help=("Emulate Subversion's auto properties feature.")) | 
|   509  |   535  | 
|   510  |   536  | 
|   511 def GetRpcServer(server, email=None, host_override=None, save_cookies=True): |   537 def GetRpcServer(server, email=None, host_override=None, save_cookies=True, | 
 |   538                  account_type=AUTH_ACCOUNT_TYPE): | 
|   512   """Returns an instance of an AbstractRpcServer. |   539   """Returns an instance of an AbstractRpcServer. | 
|   513  |   540  | 
|   514   Args: |   541   Args: | 
|   515     server: String containing the review server URL. |   542     server: String containing the review server URL. | 
|   516     email: String containing user's email address. |   543     email: String containing user's email address. | 
|   517     host_override: If not None, string containing an alternate hostname to use |   544     host_override: If not None, string containing an alternate hostname to use | 
|   518       in the host header. |   545       in the host header. | 
|   519     save_cookies: Whether authentication cookies should be saved to disk. |   546     save_cookies: Whether authentication cookies should be saved to disk. | 
 |   547     account_type: Account type for authentication, either 'GOOGLE' | 
 |   548       or 'HOSTED'. Defaults to AUTH_ACCOUNT_TYPE. | 
|   520  |   549  | 
|   521   Returns: |   550   Returns: | 
|   522     A new AbstractRpcServer, on which RPC calls can be made. |   551     A new AbstractRpcServer, on which RPC calls can be made. | 
|   523   """ |   552   """ | 
|   524  |   553  | 
|   525   rpc_server_class = HttpRpcServer |   554   rpc_server_class = HttpRpcServer | 
|   526  |   555  | 
|   527   # If this is the dev_appserver, use fake authentication. |   556   # If this is the dev_appserver, use fake authentication. | 
|   528   host = (host_override or server).lower() |   557   host = (host_override or server).lower() | 
|   529   if host == "localhost" or host.startswith("localhost:"): |   558   if host == "localhost" or host.startswith("localhost:"): | 
|   530     if email is None: |   559     if email is None: | 
|   531       email = "test@example.com" |   560       email = "test@example.com" | 
|   532       logging.info("Using debug user %s.  Override with --email" % email) |   561       logging.info("Using debug user %s.  Override with --email" % email) | 
|   533     server = rpc_server_class( |   562     server = rpc_server_class( | 
|   534         server, |   563         server, | 
|   535         lambda: (email, "password"), |   564         lambda: (email, "password"), | 
|   536         host_override=host_override, |   565         host_override=host_override, | 
|   537         extra_headers={"Cookie": |   566         extra_headers={"Cookie": | 
|   538                        'dev_appserver_login="%s:False"' % email}, |   567                        'dev_appserver_login="%s:False"' % email}, | 
|   539         save_cookies=save_cookies) |   568         save_cookies=save_cookies, | 
 |   569         account_type=account_type) | 
|   540     # Don't try to talk to ClientLogin. |   570     # Don't try to talk to ClientLogin. | 
|   541     server.authenticated = True |   571     server.authenticated = True | 
|   542     return server |   572     return server | 
|   543  |   573  | 
|   544   def GetUserCredentials(): |   574   def GetUserCredentials(): | 
|   545     """Prompts the user for a username and password.""" |   575     """Prompts the user for a username and password.""" | 
|   546     # Create a local alias to the email variable to avoid Python's crazy |   576     # Create a local alias to the email variable to avoid Python's crazy | 
|   547     # scoping rules. |   577     # scoping rules. | 
|   548     local_email = email |   578     local_email = email | 
|   549     if local_email is None: |   579     if local_email is None: | 
|   550       local_email = GetEmail("Email (login for uploading to %s)" % server) |   580       local_email = GetEmail("Email (login for uploading to %s)" % server) | 
|   551     password = getpass.getpass("Password for %s: " % local_email) |   581     password = None | 
 |   582     if keyring: | 
 |   583       password = keyring.get_password(host, local_email) | 
 |   584     if password is not None: | 
 |   585       print "Using password from system keyring." | 
 |   586     else: | 
 |   587       password = getpass.getpass("Password for %s: " % local_email) | 
 |   588       if keyring: | 
 |   589         answer = raw_input("Store password in system keyring?(y/N) ").strip() | 
 |   590         if answer == "y": | 
 |   591           keyring.set_password(host, local_email, password) | 
|   552     return (local_email, password) |   592     return (local_email, password) | 
|   553  |   593  | 
|   554   return rpc_server_class(server, |   594   return rpc_server_class(server, | 
|   555                           GetUserCredentials, |   595                           GetUserCredentials, | 
|   556                           host_override=host_override, |   596                           host_override=host_override, | 
|   557                           save_cookies=save_cookies) |   597                           save_cookies=save_cookies) | 
|   558  |   598  | 
|   559  |   599  | 
|   560 def EncodeMultipartFormData(fields, files): |   600 def EncodeMultipartFormData(fields, files): | 
|   561   """Encode form fields for multipart/form-data. |   601   """Encode form fields for multipart/form-data. | 
|   562  |   602  | 
|   563   Args: |   603   Args: | 
|   564     fields: A sequence of (name, value) elements for regular form fields. |   604     fields: A sequence of (name, value) elements for regular form fields. | 
|   565     files: A sequence of (name, filename, value) elements for data to be |   605     files: A sequence of (name, filename, value) elements for data to be | 
|   566            uploaded as files. |   606            uploaded as files. | 
|   567   Returns: |   607   Returns: | 
|   568     (content_type, body) ready for httplib.HTTP instance. |   608     (content_type, body) ready for httplib.HTTP instance. | 
|   569  |   609  | 
|   570   Source: |   610   Source: | 
|   571     http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/146306 |   611     http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/146306 | 
|   572   """ |   612   """ | 
|   573   BOUNDARY = '-M-A-G-I-C---B-O-U-N-D-A-R-Y-' |   613   BOUNDARY = '-M-A-G-I-C---B-O-U-N-D-A-R-Y-' | 
|   574   CRLF = '\r\n' |   614   CRLF = '\r\n' | 
|   575   lines = [] |   615   lines = [] | 
|   576   for (key, value) in fields: |   616   for (key, value) in fields: | 
|   577     lines.append('--' + BOUNDARY) |   617     lines.append('--' + BOUNDARY) | 
|   578     lines.append('Content-Disposition: form-data; name="%s"' % key) |   618     lines.append('Content-Disposition: form-data; name="%s"' % key) | 
|   579     lines.append('') |   619     lines.append('') | 
 |   620     if isinstance(value, unicode): | 
 |   621       value = value.encode('utf-8') | 
|   580     lines.append(value) |   622     lines.append(value) | 
|   581   for (key, filename, value) in files: |   623   for (key, filename, value) in files: | 
|   582     lines.append('--' + BOUNDARY) |   624     lines.append('--' + BOUNDARY) | 
|   583     lines.append('Content-Disposition: form-data; name="%s"; filename="%s"' % |   625     lines.append('Content-Disposition: form-data; name="%s"; filename="%s"' % | 
|   584              (key, filename)) |   626              (key, filename)) | 
|   585     lines.append('Content-Type: %s' % GetContentType(filename)) |   627     lines.append('Content-Type: %s' % GetContentType(filename)) | 
|   586     lines.append('') |   628     lines.append('') | 
 |   629     if isinstance(value, unicode): | 
 |   630       value = value.encode('utf-8') | 
|   587     lines.append(value) |   631     lines.append(value) | 
|   588   lines.append('--' + BOUNDARY + '--') |   632   lines.append('--' + BOUNDARY + '--') | 
|   589   lines.append('') |   633   lines.append('') | 
|   590   body = CRLF.join(lines) |   634   body = CRLF.join(lines) | 
|   591   content_type = 'multipart/form-data; boundary=%s' % BOUNDARY |   635   content_type = 'multipart/form-data; boundary=%s' % BOUNDARY | 
|   592   return content_type, body |   636   return content_type, body | 
|   593  |   637  | 
|   594  |   638  | 
|   595 def GetContentType(filename): |   639 def GetContentType(filename): | 
|   596   """Helper to guess the content-type from the filename.""" |   640   """Helper to guess the content-type from the filename.""" | 
| (...skipping 437 matching lines...) Expand 10 before | Expand all | Expand 10 after  Loading... | 
|  1034         else: |  1078         else: | 
|  1035           universal_newlines = True |  1079           universal_newlines = True | 
|  1036         if self.rev_start: |  1080         if self.rev_start: | 
|  1037           # "svn cat -r REV delete_file.txt" doesn't work. cat requires |  1081           # "svn cat -r REV delete_file.txt" doesn't work. cat requires | 
|  1038           # the full URL with "@REV" appended instead of using "-r" option. |  1082           # the full URL with "@REV" appended instead of using "-r" option. | 
|  1039           url = "%s/%s@%s" % (self.svn_base, filename, self.rev_start) |  1083           url = "%s/%s@%s" % (self.svn_base, filename, self.rev_start) | 
|  1040           base_content = RunShell(["svn", "cat", url], |  1084           base_content = RunShell(["svn", "cat", url], | 
|  1041                                   universal_newlines=universal_newlines, |  1085                                   universal_newlines=universal_newlines, | 
|  1042                                   silent_ok=True) |  1086                                   silent_ok=True) | 
|  1043         else: |  1087         else: | 
|  1044           base_content = RunShell(["svn", "cat", filename], |  1088           base_content, ret_code = RunShellWithReturnCode( | 
|  1045                                   universal_newlines=universal_newlines, |  1089             ["svn", "cat", filename], universal_newlines=universal_newlines) | 
|  1046                                   silent_ok=True) |  1090           if ret_code and status[0] == "R": | 
 |  1091             # It's a replaced file without local history (see issue208). | 
 |  1092             # The base file needs to be fetched from the server. | 
 |  1093             url = "%s/%s" % (self.svn_base, filename) | 
 |  1094             base_content = RunShell(["svn", "cat", url], | 
 |  1095                                     universal_newlines=universal_newlines, | 
 |  1096                                     silent_ok=True) | 
 |  1097           elif ret_code: | 
 |  1098             ErrorExit("Got error status from 'svn cat %s'", filename) | 
|  1047         if not is_binary: |  1099         if not is_binary: | 
|  1048           args = [] |  1100           args = [] | 
|  1049           if self.rev_start: |  1101           if self.rev_start: | 
|  1050             url = "%s/%s@%s" % (self.svn_base, filename, self.rev_start) |  1102             url = "%s/%s@%s" % (self.svn_base, filename, self.rev_start) | 
|  1051           else: |  1103           else: | 
|  1052             url = filename |  1104             url = filename | 
|  1053             args += ["-r", "BASE"] |  1105             args += ["-r", "BASE"] | 
|  1054           cmd = ["svn"] + args + ["propget", "svn:keywords", url] |  1106           cmd = ["svn"] + args + ["propget", "svn:keywords", url] | 
|  1055           keywords, returncode = RunShellWithReturnCode(cmd) |  1107           keywords, returncode = RunShellWithReturnCode(cmd) | 
|  1056           if keywords and not returncode: |  1108           if keywords and not returncode: | 
| (...skipping 561 matching lines...) Expand 10 before | Expand all | Expand 10 after  Loading... | 
|  1618   if options.issue: |  1670   if options.issue: | 
|  1619     prompt = "Message describing this patch set: " |  1671     prompt = "Message describing this patch set: " | 
|  1620   else: |  1672   else: | 
|  1621     prompt = "New issue subject: " |  1673     prompt = "New issue subject: " | 
|  1622   message = options.message or raw_input(prompt).strip() |  1674   message = options.message or raw_input(prompt).strip() | 
|  1623   if not message: |  1675   if not message: | 
|  1624     ErrorExit("A non-empty message is required") |  1676     ErrorExit("A non-empty message is required") | 
|  1625   rpc_server = GetRpcServer(options.server, |  1677   rpc_server = GetRpcServer(options.server, | 
|  1626                             options.email, |  1678                             options.email, | 
|  1627                             options.host, |  1679                             options.host, | 
|  1628                             options.save_cookies) |  1680                             options.save_cookies, | 
 |  1681                             options.account_type) | 
|  1629   form_fields = [("subject", message)] |  1682   form_fields = [("subject", message)] | 
|  1630   if base: |  1683   if base: | 
|  1631     form_fields.append(("base", base)) |  1684     form_fields.append(("base", base)) | 
|  1632   if options.issue: |  1685   if options.issue: | 
|  1633     form_fields.append(("issue", str(options.issue))) |  1686     form_fields.append(("issue", str(options.issue))) | 
|  1634   if options.email: |  1687   if options.email: | 
|  1635     form_fields.append(("user", options.email)) |  1688     form_fields.append(("user", options.email)) | 
|  1636   if options.reviewers: |  1689   if options.reviewers: | 
|  1637     for reviewer in options.reviewers.split(','): |  1690     for reviewer in options.reviewers.split(','): | 
|  1638       CheckReviewer(reviewer) |  1691       CheckReviewer(reviewer) | 
| (...skipping 73 matching lines...) Expand 10 before | Expand all | Expand 10 after  Loading... | 
|  1712   try: |  1765   try: | 
|  1713     RealMain(sys.argv) |  1766     RealMain(sys.argv) | 
|  1714   except KeyboardInterrupt: |  1767   except KeyboardInterrupt: | 
|  1715     print |  1768     print | 
|  1716     StatusUpdate("Interrupted.") |  1769     StatusUpdate("Interrupted.") | 
|  1717     sys.exit(1) |  1770     sys.exit(1) | 
|  1718  |  1771  | 
|  1719  |  1772  | 
|  1720 if __name__ == "__main__": |  1773 if __name__ == "__main__": | 
|  1721   main() |  1774   main() | 
| OLD | NEW |