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