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