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