media/webrtc/trunk/testing/gtest/scripts/upload.py

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rwxr-xr-x

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

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

mercurial