Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code | Sign in
(1812)

Side by Side Diff: MoinMoin/search/upload.py

Issue 4539114: Whoosh indexing work
Patch Set: Created 13 years, 10 months ago
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments. Please Sign in to add in-line comments.
Jump to:
View unified diff | Download patch
« MoinMoin/search/analyzers.py ('K') | « MoinMoin/search/analyzers.py ('k') | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 #!/usr/bin/env python
ThomasWaldmann 2011/06/06 16:48:10 you don't add upload.py to the repo. hg rm --forc
2 #
3 # Copyright 2007 Google Inc.
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
16
17 """Tool for uploading diffs from a version control system to the codereview app.
18
19 Usage summary: upload.py [options] [-- diff_options] [path...]
20
21 Diff options are passed to the diff command of the underlying system.
22
23 Supported version control systems:
24 Git
25 Mercurial
26 Subversion
27 Perforce
28 CVS
29
30 It is important for Git/Mercurial users to specify a tree/node/branch to diff
31 against by using the '--rev' option.
32 """
33 # This code is derived from appcfg.py in the App Engine SDK (open source),
34 # and from ASPN recipe #146306.
35
36 import ConfigParser
37 import cookielib
38 import errno
39 import fnmatch
40 import getpass
41 import logging
42 import marshal
43 import mimetypes
44 import optparse
45 import os
46 import re
47 import socket
48 import subprocess
49 import sys
50 import urllib
51 import urllib2
52 import urlparse
53
54 # The md5 module was deprecated in Python 2.5.
55 try:
56 from hashlib import md5
57 except ImportError:
58 from md5 import md5
59
60 try:
61 import readline
62 except ImportError:
63 pass
64
65 try:
66 import keyring
67 except ImportError:
68 keyring = None
69
70 # The logging verbosity:
71 # 0: Errors only.
72 # 1: Status messages.
73 # 2: Info logs.
74 # 3: Debug logs.
75 verbosity = 1
76
77 # The account type used for authentication.
78 # This line could be changed by the review server (see handler for
79 # upload.py).
80 AUTH_ACCOUNT_TYPE = "GOOGLE"
81
82 # URL of the default review server. As for AUTH_ACCOUNT_TYPE, this line could be
83 # changed by the review server (see handler for upload.py).
84 DEFAULT_REVIEW_SERVER = "codereview.appspot.com"
85
86 # Max size of patch or base file.
87 MAX_UPLOAD_SIZE = 900 * 1024
88
89 # Constants for version control names. Used by GuessVCSName.
90 VCS_GIT = "Git"
91 VCS_MERCURIAL = "Mercurial"
92 VCS_SUBVERSION = "Subversion"
93 VCS_PERFORCE = "Perforce"
94 VCS_CVS = "CVS"
95 VCS_UNKNOWN = "Unknown"
96
97 # whitelist for non-binary filetypes which do not start with "text/"
98 # .mm (Objective-C) shows up as application/x-freemind on my Linux box.
99 TEXT_MIMETYPES = ['application/javascript', 'application/json',
100 'application/x-javascript', 'application/xml',
101 'application/x-freemind', 'application/x-sh',
102 'application/x-ruby', 'application/x-httpd-php']
103
104 VCS_ABBREVIATIONS = {
105 VCS_MERCURIAL.lower(): VCS_MERCURIAL,
106 "hg": VCS_MERCURIAL,
107 VCS_SUBVERSION.lower(): VCS_SUBVERSION,
108 "svn": VCS_SUBVERSION,
109 VCS_PERFORCE.lower(): VCS_PERFORCE,
110 "p4": VCS_PERFORCE,
111 VCS_GIT.lower(): VCS_GIT,
112 VCS_CVS.lower(): VCS_CVS,
113 }
114
115 # The result of parsing Subversion's [auto-props] setting.
116 svn_auto_props_map = None
117
118 def GetEmail(prompt):
119 """Prompts the user for their email address and returns it.
120
121 The last used email address is saved to a file and offered up as a suggestion
122 to the user. If the user presses enter without typing in anything the last
123 used email address is used. If the user enters a new address, it is saved
124 for next time we prompt.
125
126 """
127 last_email_file_name = os.path.expanduser("~/.last_codereview_email_address")
128 last_email = ""
129 if os.path.exists(last_email_file_name):
130 try:
131 last_email_file = open(last_email_file_name, "r")
132 last_email = last_email_file.readline().strip("\n")
133 last_email_file.close()
134 prompt += " [%s]" % last_email
135 except IOError, e:
136 pass
137 email = raw_input(prompt + ": ").strip()
138 if email:
139 try:
140 last_email_file = open(last_email_file_name, "w")
141 last_email_file.write(email)
142 last_email_file.close()
143 except IOError, e:
144 pass
145 else:
146 email = last_email
147 return email
148
149
150 def StatusUpdate(msg):
151 """Print a status message to stdout.
152
153 If 'verbosity' is greater than 0, print the message.
154
155 Args:
156 msg: The string to print.
157 """
158 if verbosity > 0:
159 print msg
160
161
162 def ErrorExit(msg):
163 """Print an error message to stderr and exit."""
164 print >>sys.stderr, msg
165 sys.exit(1)
166
167
168 class ClientLoginError(urllib2.HTTPError):
169 """Raised to indicate there was an error authenticating with ClientLogin."""
170
171 def __init__(self, url, code, msg, headers, args):
172 urllib2.HTTPError.__init__(self, url, code, msg, headers, None)
173 self.args = args
174 self.reason = args["Error"]
175 self.info = args.get("Info", None)
176
177
178 class AbstractRpcServer(object):
179 """Provides a common interface for a simple RPC server."""
180
181 def __init__(self, host, auth_function, host_override=None, extra_headers={},
182 save_cookies=False, account_type=AUTH_ACCOUNT_TYPE):
183 """Creates a new HttpRpcServer.
184
185 Args:
186 host: The host to send requests to.
187 auth_function: A function that takes no arguments and returns an
188 (email, password) tuple when called. Will be called if authentication
189 is required.
190 host_override: The host header to send to the server (defaults to host).
191 extra_headers: A dict of extra headers to append to every request.
192 save_cookies: If True, save the authentication cookies to local disk.
193 If False, use an in-memory cookiejar instead. Subclasses must
194 implement this functionality. Defaults to False.
195 account_type: Account type used for authentication. Defaults to
196 AUTH_ACCOUNT_TYPE.
197 """
198 self.host = host
199 if (not self.host.startswith("http://") and
200 not self.host.startswith("https://")):
201 self.host = "http://" + self.host
202 self.host_override = host_override
203 self.auth_function = auth_function
204 self.authenticated = False
205 self.extra_headers = extra_headers
206 self.save_cookies = save_cookies
207 self.account_type = account_type
208 self.opener = self._GetOpener()
209 if self.host_override:
210 logging.info("Server: %s; Host: %s", self.host, self.host_override)
211 else:
212 logging.info("Server: %s", self.host)
213
214 def _GetOpener(self):
215 """Returns an OpenerDirector for making HTTP requests.
216
217 Returns:
218 A urllib2.OpenerDirector object.
219 """
220 raise NotImplementedError()
221
222 def _CreateRequest(self, url, data=None):
223 """Creates a new urllib request."""
224 logging.debug("Creating request for: '%s' with payload:\n%s", url, data)
225 req = urllib2.Request(url, data=data)
226 if self.host_override:
227 req.add_header("Host", self.host_override)
228 for key, value in self.extra_headers.iteritems():
229 req.add_header(key, value)
230 return req
231
232 def _GetAuthToken(self, email, password):
233 """Uses ClientLogin to authenticate the user, returning an auth token.
234
235 Args:
236 email: The user's email address
237 password: The user's password
238
239 Raises:
240 ClientLoginError: If there was an error authenticating with ClientLogin.
241 HTTPError: If there was some other form of HTTP error.
242
243 Returns:
244 The authentication token returned by ClientLogin.
245 """
246 account_type = self.account_type
247 if self.host.endswith(".google.com"):
248 # Needed for use inside Google.
249 account_type = "HOSTED"
250 req = self._CreateRequest(
251 url="https://www.google.com/accounts/ClientLogin",
252 data=urllib.urlencode({
253 "Email": email,
254 "Passwd": password,
255 "service": "ah",
256 "source": "rietveld-codereview-upload",
257 "accountType": account_type,
258 }),
259 )
260 try:
261 response = self.opener.open(req)
262 response_body = response.read()
263 response_dict = dict(x.split("=")
264 for x in response_body.split("\n") if x)
265 return response_dict["Auth"]
266 except urllib2.HTTPError, e:
267 if e.code == 403:
268 body = e.read()
269 response_dict = dict(x.split("=", 1) for x in body.split("\n") if x)
270 raise ClientLoginError(req.get_full_url(), e.code, e.msg,
271 e.headers, response_dict)
272 else:
273 raise
274
275 def _GetAuthCookie(self, auth_token):
276 """Fetches authentication cookies for an authentication token.
277
278 Args:
279 auth_token: The authentication token returned by ClientLogin.
280
281 Raises:
282 HTTPError: If there was an error fetching the authentication cookies.
283 """
284 # This is a dummy value to allow us to identify when we're successful.
285 continue_location = "http://localhost/"
286 args = {"continue": continue_location, "auth": auth_token}
287 req = self._CreateRequest("%s/_ah/login?%s" %
288 (self.host, urllib.urlencode(args)))
289 try:
290 response = self.opener.open(req)
291 except urllib2.HTTPError, e:
292 response = e
293 if (response.code != 302 or
294 response.info()["location"] != continue_location):
295 raise urllib2.HTTPError(req.get_full_url(), response.code, response.msg,
296 response.headers, response.fp)
297 self.authenticated = True
298
299 def _Authenticate(self):
300 """Authenticates the user.
301
302 The authentication process works as follows:
303 1) We get a username and password from the user
304 2) We use ClientLogin to obtain an AUTH token for the user
305 (see http://code.google.com/apis/accounts/AuthForInstalledApps.html).
306 3) We pass the auth token to /_ah/login on the server to obtain an
307 authentication cookie. If login was successful, it tries to redirect
308 us to the URL we provided.
309
310 If we attempt to access the upload API without first obtaining an
311 authentication cookie, it returns a 401 response (or a 302) and
312 directs us to authenticate ourselves with ClientLogin.
313 """
314 for i in range(3):
315 credentials = self.auth_function()
316 try:
317 auth_token = self._GetAuthToken(credentials[0], credentials[1])
318 except ClientLoginError, e:
319 print >>sys.stderr, ''
320 if e.reason == "BadAuthentication":
321 if e.info == "InvalidSecondFactor":
322 print >>sys.stderr, (
323 "Use an application-specific password instead "
324 "of your regular account password.\n"
325 "See http://www.google.com/"
326 "support/accounts/bin/answer.py?answer=185833")
327 else:
328 print >>sys.stderr, "Invalid username or password."
329 elif e.reason == "CaptchaRequired":
330 print >>sys.stderr, (
331 "Please go to\n"
332 "https://www.google.com/accounts/DisplayUnlockCaptcha\n"
333 "and verify you are a human. Then try again.\n"
334 "If you are using a Google Apps account the URL is:\n"
335 "https://www.google.com/a/yourdomain.com/UnlockCaptcha")
336 elif e.reason == "NotVerified":
337 print >>sys.stderr, "Account not verified."
338 elif e.reason == "TermsNotAgreed":
339 print >>sys.stderr, "User has not agreed to TOS."
340 elif e.reason == "AccountDeleted":
341 print >>sys.stderr, "The user account has been deleted."
342 elif e.reason == "AccountDisabled":
343 print >>sys.stderr, "The user account has been disabled."
344 break
345 elif e.reason == "ServiceDisabled":
346 print >>sys.stderr, ("The user's access to the service has been "
347 "disabled.")
348 elif e.reason == "ServiceUnavailable":
349 print >>sys.stderr, "The service is not available; try again later."
350 else:
351 # Unknown error.
352 raise
353 print >>sys.stderr, ''
354 continue
355 self._GetAuthCookie(auth_token)
356 return
357
358 def Send(self, request_path, payload=None,
359 content_type="application/octet-stream",
360 timeout=None,
361 extra_headers=None,
362 **kwargs):
363 """Sends an RPC and returns the response.
364
365 Args:
366 request_path: The path to send the request to, eg /api/appversion/create.
367 payload: The body of the request, or None to send an empty request.
368 content_type: The Content-Type header to use.
369 timeout: timeout in seconds; default None i.e. no timeout.
370 (Note: for large requests on OS X, the timeout doesn't work right.)
371 extra_headers: Dict containing additional HTTP headers that should be
372 included in the request (string header names mapped to their values),
373 or None to not include any additional headers.
374 kwargs: Any keyword arguments are converted into query string parameters.
375
376 Returns:
377 The response body, as a string.
378 """
379 # TODO: Don't require authentication. Let the server say
380 # whether it is necessary.
381 if not self.authenticated:
382 self._Authenticate()
383
384 old_timeout = socket.getdefaulttimeout()
385 socket.setdefaulttimeout(timeout)
386 try:
387 tries = 0
388 while True:
389 tries += 1
390 args = dict(kwargs)
391 url = "%s%s" % (self.host, request_path)
392 if args:
393 url += "?" + urllib.urlencode(args)
394 req = self._CreateRequest(url=url, data=payload)
395 req.add_header("Content-Type", content_type)
396 if extra_headers:
397 for header, value in extra_headers.items():
398 req.add_header(header, value)
399 try:
400 f = self.opener.open(req)
401 response = f.read()
402 f.close()
403 return response
404 except urllib2.HTTPError, e:
405 if tries > 3:
406 raise
407 elif e.code == 401 or e.code == 302:
408 self._Authenticate()
409 ## elif e.code >= 500 and e.code < 600:
410 ## # Server Error - try again.
411 ## continue
412 elif e.code == 301:
413 # Handle permanent redirect manually.
414 url = e.info()["location"]
415 url_loc = urlparse.urlparse(url)
416 self.host = '%s://%s' % (url_loc[0], url_loc[1])
417 else:
418 raise
419 finally:
420 socket.setdefaulttimeout(old_timeout)
421
422
423 class HttpRpcServer(AbstractRpcServer):
424 """Provides a simplified RPC-style interface for HTTP requests."""
425
426 def _Authenticate(self):
427 """Save the cookie jar after authentication."""
428 super(HttpRpcServer, self)._Authenticate()
429 if self.save_cookies:
430 StatusUpdate("Saving authentication cookies to %s" % self.cookie_file)
431 self.cookie_jar.save()
432
433 def _GetOpener(self):
434 """Returns an OpenerDirector that supports cookies and ignores redirects.
435
436 Returns:
437 A urllib2.OpenerDirector object.
438 """
439 opener = urllib2.OpenerDirector()
440 opener.add_handler(urllib2.ProxyHandler())
441 opener.add_handler(urllib2.UnknownHandler())
442 opener.add_handler(urllib2.HTTPHandler())
443 opener.add_handler(urllib2.HTTPDefaultErrorHandler())
444 opener.add_handler(urllib2.HTTPSHandler())
445 opener.add_handler(urllib2.HTTPErrorProcessor())
446 if self.save_cookies:
447 self.cookie_file = os.path.expanduser("~/.codereview_upload_cookies")
448 self.cookie_jar = cookielib.MozillaCookieJar(self.cookie_file)
449 if os.path.exists(self.cookie_file):
450 try:
451 self.cookie_jar.load()
452 self.authenticated = True
453 StatusUpdate("Loaded authentication cookies from %s" %
454 self.cookie_file)
455 except (cookielib.LoadError, IOError):
456 # Failed to load cookies - just ignore them.
457 pass
458 else:
459 # Create an empty cookie file with mode 600
460 fd = os.open(self.cookie_file, os.O_CREAT, 0600)
461 os.close(fd)
462 # Always chmod the cookie file
463 os.chmod(self.cookie_file, 0600)
464 else:
465 # Don't save cookies across runs of update.py.
466 self.cookie_jar = cookielib.CookieJar()
467 opener.add_handler(urllib2.HTTPCookieProcessor(self.cookie_jar))
468 return opener
469
470
471 parser = optparse.OptionParser(
472 usage="%prog [options] [-- diff_options] [path...]")
473 parser.add_option("-y", "--assume_yes", action="store_true",
474 dest="assume_yes", default=False,
475 help="Assume that the answer to yes/no questions is 'yes'.")
476 # Logging
477 group = parser.add_option_group("Logging options")
478 group.add_option("-q", "--quiet", action="store_const", const=0,
479 dest="verbose", help="Print errors only.")
480 group.add_option("-v", "--verbose", action="store_const", const=2,
481 dest="verbose", default=1,
482 help="Print info level logs.")
483 group.add_option("--noisy", action="store_const", const=3,
484 dest="verbose", help="Print all logs.")
485 group.add_option("--print_diffs", dest="print_diffs", action="store_true",
486 help="Print full diffs.")
487 # Review server
488 group = parser.add_option_group("Review server options")
489 group.add_option("-s", "--server", action="store", dest="server",
490 default=DEFAULT_REVIEW_SERVER,
491 metavar="SERVER",
492 help=("The server to upload to. The format is host[:port]. "
493 "Defaults to '%default'."))
494 group.add_option("-e", "--email", action="store", dest="email",
495 metavar="EMAIL", default=None,
496 help="The username to use. Will prompt if omitted.")
497 group.add_option("-H", "--host", action="store", dest="host",
498 metavar="HOST", default=None,
499 help="Overrides the Host header sent with all RPCs.")
500 group.add_option("--no_cookies", action="store_false",
501 dest="save_cookies", default=True,
502 help="Do not save authentication cookies to local disk.")
503 group.add_option("--account_type", action="store", dest="account_type",
504 metavar="TYPE", default=AUTH_ACCOUNT_TYPE,
505 choices=["GOOGLE", "HOSTED"],
506 help=("Override the default account type "
507 "(defaults to '%default', "
508 "valid choices are 'GOOGLE' and 'HOSTED')."))
509 # Issue
510 group = parser.add_option_group("Issue options")
511 group.add_option("-d", "--description", action="store", dest="description",
512 metavar="DESCRIPTION", default=None,
513 help="Optional description when creating an issue.")
514 group.add_option("-f", "--description_file", action="store",
515 dest="description_file", metavar="DESCRIPTION_FILE",
516 default=None,
517 help="Optional path of a file that contains "
518 "the description when creating an issue.")
519 group.add_option("-r", "--reviewers", action="store", dest="reviewers",
520 metavar="REVIEWERS", default=None,
521 help="Add reviewers (comma separated email addresses).")
522 group.add_option("--cc", action="store", dest="cc",
523 metavar="CC", default=None,
524 help="Add CC (comma separated email addresses).")
525 group.add_option("--private", action="store_true", dest="private",
526 default=False,
527 help="Make the issue restricted to reviewers and those CCed")
528 # Upload options
529 group = parser.add_option_group("Patch options")
530 group.add_option("-m", "--message", action="store", dest="message",
531 metavar="MESSAGE", default=None,
532 help="A message to identify the patch. "
533 "Will prompt if omitted.")
534 group.add_option("-i", "--issue", type="int", action="store",
535 metavar="ISSUE", default=None,
536 help="Issue number to which to add. Defaults to new issue.")
537 group.add_option("--base_url", action="store", dest="base_url", default=None,
538 help="Base repository URL (listed as \"Base URL\" when "
539 "viewing issue). If omitted, will be guessed automatically "
540 "for SVN repos and left blank for others.")
541 group.add_option("--download_base", action="store_true",
542 dest="download_base", default=False,
543 help="Base files will be downloaded by the server "
544 "(side-by-side diffs may not work on files with CRs).")
545 group.add_option("--rev", action="store", dest="revision",
546 metavar="REV", default=None,
547 help="Base revision/branch/tree to diff against. Use "
548 "rev1:rev2 range to review already committed changeset.")
549 group.add_option("--send_mail", action="store_true",
550 dest="send_mail", default=False,
551 help="Send notification email to reviewers.")
552 group.add_option("--vcs", action="store", dest="vcs",
553 metavar="VCS", default=None,
554 help=("Version control system (optional, usually upload.py "
555 "already guesses the right VCS)."))
556 group.add_option("--emulate_svn_auto_props", action="store_true",
557 dest="emulate_svn_auto_props", default=False,
558 help=("Emulate Subversion's auto properties feature."))
559 # Perforce-specific
560 group = parser.add_option_group("Perforce-specific options "
561 "(overrides P4 environment variables)")
562 group.add_option("--p4_port", action="store", dest="p4_port",
563 metavar="P4_PORT", default=None,
564 help=("Perforce server and port (optional)"))
565 group.add_option("--p4_changelist", action="store", dest="p4_changelist",
566 metavar="P4_CHANGELIST", default=None,
567 help=("Perforce changelist id"))
568 group.add_option("--p4_client", action="store", dest="p4_client",
569 metavar="P4_CLIENT", default=None,
570 help=("Perforce client/workspace"))
571 group.add_option("--p4_user", action="store", dest="p4_user",
572 metavar="P4_USER", default=None,
573 help=("Perforce user"))
574
575 def GetRpcServer(server, email=None, host_override=None, save_cookies=True,
576 account_type=AUTH_ACCOUNT_TYPE):
577 """Returns an instance of an AbstractRpcServer.
578
579 Args:
580 server: String containing the review server URL.
581 email: String containing user's email address.
582 host_override: If not None, string containing an alternate hostname to use
583 in the host header.
584 save_cookies: Whether authentication cookies should be saved to disk.
585 account_type: Account type for authentication, either 'GOOGLE'
586 or 'HOSTED'. Defaults to AUTH_ACCOUNT_TYPE.
587
588 Returns:
589 A new AbstractRpcServer, on which RPC calls can be made.
590 """
591
592 rpc_server_class = HttpRpcServer
593
594 # If this is the dev_appserver, use fake authentication.
595 host = (host_override or server).lower()
596 if re.match(r'(http://)?localhost([:/]|$)', host):
597 if email is None:
598 email = "test@example.com"
599 logging.info("Using debug user %s. Override with --email" % email)
600 server = rpc_server_class(
601 server,
602 lambda: (email, "password"),
603 host_override=host_override,
604 extra_headers={"Cookie":
605 'dev_appserver_login="%s:False"' % email},
606 save_cookies=save_cookies,
607 account_type=account_type)
608 # Don't try to talk to ClientLogin.
609 server.authenticated = True
610 return server
611
612 def GetUserCredentials():
613 """Prompts the user for a username and password."""
614 # Create a local alias to the email variable to avoid Python's crazy
615 # scoping rules.
616 local_email = email
617 if local_email is None:
618 local_email = GetEmail("Email (login for uploading to %s)" % server)
619 password = None
620 if keyring:
621 password = keyring.get_password(host, local_email)
622 if password is not None:
623 print "Using password from system keyring."
624 else:
625 password = getpass.getpass("Password for %s: " % local_email)
626 if keyring:
627 answer = raw_input("Store password in system keyring?(y/N) ").strip()
628 if answer == "y":
629 keyring.set_password(host, local_email, password)
630 return (local_email, password)
631
632 return rpc_server_class(server,
633 GetUserCredentials,
634 host_override=host_override,
635 save_cookies=save_cookies)
636
637
638 def EncodeMultipartFormData(fields, files):
639 """Encode form fields for multipart/form-data.
640
641 Args:
642 fields: A sequence of (name, value) elements for regular form fields.
643 files: A sequence of (name, filename, value) elements for data to be
644 uploaded as files.
645 Returns:
646 (content_type, body) ready for httplib.HTTP instance.
647
648 Source:
649 http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/146306
650 """
651 BOUNDARY = '-M-A-G-I-C---B-O-U-N-D-A-R-Y-'
652 CRLF = '\r\n'
653 lines = []
654 for (key, value) in fields:
655 lines.append('--' + BOUNDARY)
656 lines.append('Content-Disposition: form-data; name="%s"' % key)
657 lines.append('')
658 if isinstance(value, unicode):
659 value = value.encode('utf-8')
660 lines.append(value)
661 for (key, filename, value) in files:
662 lines.append('--' + BOUNDARY)
663 lines.append('Content-Disposition: form-data; name="%s"; filename="%s"' %
664 (key, filename))
665 lines.append('Content-Type: %s' % GetContentType(filename))
666 lines.append('')
667 if isinstance(value, unicode):
668 value = value.encode('utf-8')
669 lines.append(value)
670 lines.append('--' + BOUNDARY + '--')
671 lines.append('')
672 body = CRLF.join(lines)
673 content_type = 'multipart/form-data; boundary=%s' % BOUNDARY
674 return content_type, body
675
676
677 def GetContentType(filename):
678 """Helper to guess the content-type from the filename."""
679 return mimetypes.guess_type(filename)[0] or 'application/octet-stream'
680
681
682 # Use a shell for subcommands on Windows to get a PATH search.
683 use_shell = sys.platform.startswith("win")
684
685 def RunShellWithReturnCodeAndStderr(command, print_output=False,
686 universal_newlines=True,
687 env=os.environ):
688 """Executes a command and returns the output from stdout, stderr and the retur n code.
689
690 Args:
691 command: Command to execute.
692 print_output: If True, the output is printed to stdout.
693 If False, both stdout and stderr are ignored.
694 universal_newlines: Use universal_newlines flag (default: True).
695
696 Returns:
697 Tuple (stdout, stderr, return code)
698 """
699 logging.info("Running %s", command)
700 env = env.copy()
701 env['LC_MESSAGES'] = 'C'
702 p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
703 shell=use_shell, universal_newlines=universal_newlines,
704 env=env)
705 if print_output:
706 output_array = []
707 while True:
708 line = p.stdout.readline()
709 if not line:
710 break
711 print line.strip("\n")
712 output_array.append(line)
713 output = "".join(output_array)
714 else:
715 output = p.stdout.read()
716 p.wait()
717 errout = p.stderr.read()
718 if print_output and errout:
719 print >>sys.stderr, errout
720 p.stdout.close()
721 p.stderr.close()
722 return output, errout, p.returncode
723
724 def RunShellWithReturnCode(command, print_output=False,
725 universal_newlines=True,
726 env=os.environ):
727 """Executes a command and returns the output from stdout and the return code." ""
728 out, err, retcode = RunShellWithReturnCodeAndStderr(command, print_output,
729 universal_newlines, env)
730 return out, retcode
731
732 def RunShell(command, silent_ok=False, universal_newlines=True,
733 print_output=False, env=os.environ):
734 data, retcode = RunShellWithReturnCode(command, print_output,
735 universal_newlines, env)
736 if retcode:
737 ErrorExit("Got error status from %s:\n%s" % (command, data))
738 if not silent_ok and not data:
739 ErrorExit("No output from %s" % command)
740 return data
741
742
743 class VersionControlSystem(object):
744 """Abstract base class providing an interface to the VCS."""
745
746 def __init__(self, options):
747 """Constructor.
748
749 Args:
750 options: Command line options.
751 """
752 self.options = options
753
754 def PostProcessDiff(self, diff):
755 """Return the diff with any special post processing this VCS needs, e.g.
756 to include an svn-style "Index:"."""
757 return diff
758
759 def GenerateDiff(self, args):
760 """Return the current diff as a string.
761
762 Args:
763 args: Extra arguments to pass to the diff command.
764 """
765 raise NotImplementedError(
766 "abstract method -- subclass %s must override" % self.__class__)
767
768 def GetUnknownFiles(self):
769 """Return a list of files unknown to the VCS."""
770 raise NotImplementedError(
771 "abstract method -- subclass %s must override" % self.__class__)
772
773 def CheckForUnknownFiles(self):
774 """Show an "are you sure?" prompt if there are unknown files."""
775 unknown_files = self.GetUnknownFiles()
776 if unknown_files:
777 print "The following files are not added to version control:"
778 for line in unknown_files:
779 print line
780 prompt = "Are you sure to continue?(y/N) "
781 answer = raw_input(prompt).strip()
782 if answer != "y":
783 ErrorExit("User aborted")
784
785 def GetBaseFile(self, filename):
786 """Get the content of the upstream version of a file.
787
788 Returns:
789 A tuple (base_content, new_content, is_binary, status)
790 base_content: The contents of the base file.
791 new_content: For text files, this is empty. For binary files, this is
792 the contents of the new file, since the diff output won't contain
793 information to reconstruct the current file.
794 is_binary: True iff the file is binary.
795 status: The status of the file.
796 """
797
798 raise NotImplementedError(
799 "abstract method -- subclass %s must override" % self.__class__)
800
801
802 def GetBaseFiles(self, diff):
803 """Helper that calls GetBase file for each file in the patch.
804
805 Returns:
806 A dictionary that maps from filename to GetBaseFile's tuple. Filenames
807 are retrieved based on lines that start with "Index:" or
808 "Property changes on:".
809 """
810 files = {}
811 for line in diff.splitlines(True):
812 if line.startswith('Index:') or line.startswith('Property changes on:'):
813 unused, filename = line.split(':', 1)
814 # On Windows if a file has property changes its filename uses '\'
815 # instead of '/'.
816 filename = filename.strip().replace('\\', '/')
817 files[filename] = self.GetBaseFile(filename)
818 return files
819
820
821 def UploadBaseFiles(self, issue, rpc_server, patch_list, patchset, options,
822 files):
823 """Uploads the base files (and if necessary, the current ones as well)."""
824
825 def UploadFile(filename, file_id, content, is_binary, status, is_base):
826 """Uploads a file to the server."""
827 file_too_large = False
828 if is_base:
829 type = "base"
830 else:
831 type = "current"
832 if len(content) > MAX_UPLOAD_SIZE:
833 print ("Not uploading the %s file for %s because it's too large." %
834 (type, filename))
835 file_too_large = True
836 content = ""
837 checksum = md5(content).hexdigest()
838 if options.verbose > 0 and not file_too_large:
839 print "Uploading %s file for %s" % (type, filename)
840 url = "/%d/upload_content/%d/%d" % (int(issue), int(patchset), file_id)
841 form_fields = [("filename", filename),
842 ("status", status),
843 ("checksum", checksum),
844 ("is_binary", str(is_binary)),
845 ("is_current", str(not is_base)),
846 ]
847 if file_too_large:
848 form_fields.append(("file_too_large", "1"))
849 if options.email:
850 form_fields.append(("user", options.email))
851 ctype, body = EncodeMultipartFormData(form_fields,
852 [("data", filename, content)])
853 response_body = rpc_server.Send(url, body,
854 content_type=ctype)
855 if not response_body.startswith("OK"):
856 StatusUpdate(" --> %s" % response_body)
857 sys.exit(1)
858
859 patches = dict()
860 [patches.setdefault(v, k) for k, v in patch_list]
861 for filename in patches.keys():
862 base_content, new_content, is_binary, status = files[filename]
863 file_id_str = patches.get(filename)
864 if file_id_str.find("nobase") != -1:
865 base_content = None
866 file_id_str = file_id_str[file_id_str.rfind("_") + 1:]
867 file_id = int(file_id_str)
868 if base_content != None:
869 UploadFile(filename, file_id, base_content, is_binary, status, True)
870 if new_content != None:
871 UploadFile(filename, file_id, new_content, is_binary, status, False)
872
873 def IsImage(self, filename):
874 """Returns true if the filename has an image extension."""
875 mimetype = mimetypes.guess_type(filename)[0]
876 if not mimetype:
877 return False
878 return mimetype.startswith("image/")
879
880 def IsBinary(self, filename):
881 """Returns true if the guessed mimetyped isnt't in text group."""
882 mimetype = mimetypes.guess_type(filename)[0]
883 if not mimetype:
884 return False # e.g. README, "real" binaries usually have an extension
885 # special case for text files which don't start with text/
886 if mimetype in TEXT_MIMETYPES:
887 return False
888 return not mimetype.startswith("text/")
889
890
891 class SubversionVCS(VersionControlSystem):
892 """Implementation of the VersionControlSystem interface for Subversion."""
893
894 def __init__(self, options):
895 super(SubversionVCS, self).__init__(options)
896 if self.options.revision:
897 match = re.match(r"(\d+)(:(\d+))?", self.options.revision)
898 if not match:
899 ErrorExit("Invalid Subversion revision %s." % self.options.revision)
900 self.rev_start = match.group(1)
901 self.rev_end = match.group(3)
902 else:
903 self.rev_start = self.rev_end = None
904 # Cache output from "svn list -r REVNO dirname".
905 # Keys: dirname, Values: 2-tuple (ouput for start rev and end rev).
906 self.svnls_cache = {}
907 # Base URL is required to fetch files deleted in an older revision.
908 # Result is cached to not guess it over and over again in GetBaseFile().
909 required = self.options.download_base or self.options.revision is not None
910 self.svn_base = self._GuessBase(required)
911
912 def GuessBase(self, required):
913 """Wrapper for _GuessBase."""
914 return self.svn_base
915
916 def _GuessBase(self, required):
917 """Returns base URL for current diff.
918
919 Args:
920 required: If true, exits if the url can't be guessed, otherwise None is
921 returned.
922 """
923 info = RunShell(["svn", "info"])
924 for line in info.splitlines():
925 if line.startswith("URL: "):
926 url = line.split()[1]
927 scheme, netloc, path, params, query, fragment = urlparse.urlparse(url)
928 guess = ""
929 if netloc == "svn.python.org" and scheme == "svn+ssh":
930 path = "projects" + path
931 scheme = "http"
932 guess = "Python "
933 elif netloc.endswith(".googlecode.com"):
934 scheme = "http"
935 guess = "Google Code "
936 path = path + "/"
937 base = urlparse.urlunparse((scheme, netloc, path, params,
938 query, fragment))
939 logging.info("Guessed %sbase = %s", guess, base)
940 return base
941 if required:
942 ErrorExit("Can't find URL in output from svn info")
943 return None
944
945 def GenerateDiff(self, args):
946 cmd = ["svn", "diff"]
947 if self.options.revision:
948 cmd += ["-r", self.options.revision]
949 cmd.extend(args)
950 data = RunShell(cmd)
951 count = 0
952 for line in data.splitlines():
953 if line.startswith("Index:") or line.startswith("Property changes on:"):
954 count += 1
955 logging.info(line)
956 if not count:
957 ErrorExit("No valid patches found in output from svn diff")
958 return data
959
960 def _CollapseKeywords(self, content, keyword_str):
961 """Collapses SVN keywords."""
962 # svn cat translates keywords but svn diff doesn't. As a result of this
963 # behavior patching.PatchChunks() fails with a chunk mismatch error.
964 # This part was originally written by the Review Board development team
965 # who had the same problem (http://reviews.review-board.org/r/276/).
966 # Mapping of keywords to known aliases
967 svn_keywords = {
968 # Standard keywords
969 'Date': ['Date', 'LastChangedDate'],
970 'Revision': ['Revision', 'LastChangedRevision', 'Rev'],
971 'Author': ['Author', 'LastChangedBy'],
972 'HeadURL': ['HeadURL', 'URL'],
973 'Id': ['Id'],
974
975 # Aliases
976 'LastChangedDate': ['LastChangedDate', 'Date'],
977 'LastChangedRevision': ['LastChangedRevision', 'Rev', 'Revision'],
978 'LastChangedBy': ['LastChangedBy', 'Author'],
979 'URL': ['URL', 'HeadURL'],
980 }
981
982 def repl(m):
983 if m.group(2):
984 return "$%s::%s$" % (m.group(1), " " * len(m.group(3)))
985 return "$%s$" % m.group(1)
986 keywords = [keyword
987 for name in keyword_str.split(" ")
988 for keyword in svn_keywords.get(name, [])]
989 return re.sub(r"\$(%s):(:?)([^\$]+)\$" % '|'.join(keywords), repl, content)
990
991 def GetUnknownFiles(self):
992 status = RunShell(["svn", "status", "--ignore-externals"], silent_ok=True)
993 unknown_files = []
994 for line in status.split("\n"):
995 if line and line[0] == "?":
996 unknown_files.append(line)
997 return unknown_files
998
999 def ReadFile(self, filename):
1000 """Returns the contents of a file."""
1001 file = open(filename, 'rb')
1002 result = ""
1003 try:
1004 result = file.read()
1005 finally:
1006 file.close()
1007 return result
1008
1009 def GetStatus(self, filename):
1010 """Returns the status of a file."""
1011 if not self.options.revision:
1012 status = RunShell(["svn", "status", "--ignore-externals", filename])
1013 if not status:
1014 ErrorExit("svn status returned no output for %s" % filename)
1015 status_lines = status.splitlines()
1016 # If file is in a cl, the output will begin with
1017 # "\n--- Changelist 'cl_name':\n". See
1018 # http://svn.collab.net/repos/svn/trunk/notes/changelist-design.txt
1019 if (len(status_lines) == 3 and
1020 not status_lines[0] and
1021 status_lines[1].startswith("--- Changelist")):
1022 status = status_lines[2]
1023 else:
1024 status = status_lines[0]
1025 # If we have a revision to diff against we need to run "svn list"
1026 # for the old and the new revision and compare the results to get
1027 # the correct status for a file.
1028 else:
1029 dirname, relfilename = os.path.split(filename)
1030 if dirname not in self.svnls_cache:
1031 cmd = ["svn", "list", "-r", self.rev_start, dirname or "."]
1032 out, err, returncode = RunShellWithReturnCodeAndStderr(cmd)
1033 if returncode:
1034 # Directory might not yet exist at start revison
1035 # svn: Unable to find repository location for 'abc' in revision nnn
1036 if re.match('^svn: Unable to find repository location for .+ in revisi on \d+', err):
1037 old_files = ()
1038 else:
1039 ErrorExit("Failed to get status for %s:\n%s" % (filename, err))
1040 else:
1041 old_files = out.splitlines()
1042 args = ["svn", "list"]
1043 if self.rev_end:
1044 args += ["-r", self.rev_end]
1045 cmd = args + [dirname or "."]
1046 out, returncode = RunShellWithReturnCode(cmd)
1047 if returncode:
1048 ErrorExit("Failed to run command %s" % cmd)
1049 self.svnls_cache[dirname] = (old_files, out.splitlines())
1050 old_files, new_files = self.svnls_cache[dirname]
1051 if relfilename in old_files and relfilename not in new_files:
1052 status = "D "
1053 elif relfilename in old_files and relfilename in new_files:
1054 status = "M "
1055 else:
1056 status = "A "
1057 return status
1058
1059 def GetBaseFile(self, filename):
1060 status = self.GetStatus(filename)
1061 base_content = None
1062 new_content = None
1063
1064 # If a file is copied its status will be "A +", which signifies
1065 # "addition-with-history". See "svn st" for more information. We need to
1066 # upload the original file or else diff parsing will fail if the file was
1067 # edited.
1068 if status[0] == "A" and status[3] != "+":
1069 # We'll need to upload the new content if we're adding a binary file
1070 # since diff's output won't contain it.
1071 mimetype = RunShell(["svn", "propget", "svn:mime-type", filename],
1072 silent_ok=True)
1073 base_content = ""
1074 is_binary = bool(mimetype) and not mimetype.startswith("text/")
1075 if is_binary and self.IsImage(filename):
1076 new_content = self.ReadFile(filename)
1077 elif (status[0] in ("M", "D", "R") or
1078 (status[0] == "A" and status[3] == "+") or # Copied file.
1079 (status[0] == " " and status[1] == "M")): # Property change.
1080 args = []
1081 if self.options.revision:
1082 url = "%s/%s@%s" % (self.svn_base, filename, self.rev_start)
1083 else:
1084 # Don't change filename, it's needed later.
1085 url = filename
1086 args += ["-r", "BASE"]
1087 cmd = ["svn"] + args + ["propget", "svn:mime-type", url]
1088 mimetype, returncode = RunShellWithReturnCode(cmd)
1089 if returncode:
1090 # File does not exist in the requested revision.
1091 # Reset mimetype, it contains an error message.
1092 mimetype = ""
1093 else:
1094 mimetype = mimetype.strip()
1095 get_base = False
1096 is_binary = (bool(mimetype) and
1097 not mimetype.startswith("text/") and
1098 not mimetype in TEXT_MIMETYPES)
1099 if status[0] == " ":
1100 # Empty base content just to force an upload.
1101 base_content = ""
1102 elif is_binary:
1103 if self.IsImage(filename):
1104 get_base = True
1105 if status[0] == "M":
1106 if not self.rev_end:
1107 new_content = self.ReadFile(filename)
1108 else:
1109 url = "%s/%s@%s" % (self.svn_base, filename, self.rev_end)
1110 new_content = RunShell(["svn", "cat", url],
1111 universal_newlines=True, silent_ok=True)
1112 else:
1113 base_content = ""
1114 else:
1115 get_base = True
1116
1117 if get_base:
1118 if is_binary:
1119 universal_newlines = False
1120 else:
1121 universal_newlines = True
1122 if self.rev_start:
1123 # "svn cat -r REV delete_file.txt" doesn't work. cat requires
1124 # the full URL with "@REV" appended instead of using "-r" option.
1125 url = "%s/%s@%s" % (self.svn_base, filename, self.rev_start)
1126 base_content = RunShell(["svn", "cat", url],
1127 universal_newlines=universal_newlines,
1128 silent_ok=True)
1129 else:
1130 base_content, ret_code = RunShellWithReturnCode(
1131 ["svn", "cat", filename], universal_newlines=universal_newlines)
1132 if ret_code and status[0] == "R":
1133 # It's a replaced file without local history (see issue208).
1134 # The base file needs to be fetched from the server.
1135 url = "%s/%s" % (self.svn_base, filename)
1136 base_content = RunShell(["svn", "cat", url],
1137 universal_newlines=universal_newlines,
1138 silent_ok=True)
1139 elif ret_code:
1140 ErrorExit("Got error status from 'svn cat %s'" % filename)
1141 if not is_binary:
1142 args = []
1143 if self.rev_start:
1144 url = "%s/%s@%s" % (self.svn_base, filename, self.rev_start)
1145 else:
1146 url = filename
1147 args += ["-r", "BASE"]
1148 cmd = ["svn"] + args + ["propget", "svn:keywords", url]
1149 keywords, returncode = RunShellWithReturnCode(cmd)
1150 if keywords and not returncode:
1151 base_content = self._CollapseKeywords(base_content, keywords)
1152 else:
1153 StatusUpdate("svn status returned unexpected output: %s" % status)
1154 sys.exit(1)
1155 return base_content, new_content, is_binary, status[0:5]
1156
1157
1158 class GitVCS(VersionControlSystem):
1159 """Implementation of the VersionControlSystem interface for Git."""
1160
1161 def __init__(self, options):
1162 super(GitVCS, self).__init__(options)
1163 # Map of filename -> (hash before, hash after) of base file.
1164 # Hashes for "no such file" are represented as None.
1165 self.hashes = {}
1166 # Map of new filename -> old filename for renames.
1167 self.renames = {}
1168
1169 def PostProcessDiff(self, gitdiff):
1170 """Converts the diff output to include an svn-style "Index:" line as well
1171 as record the hashes of the files, so we can upload them along with our
1172 diff."""
1173 # Special used by git to indicate "no such content".
1174 NULL_HASH = "0"*40
1175
1176 def IsFileNew(filename):
1177 return filename in self.hashes and self.hashes[filename][0] is None
1178
1179 def AddSubversionPropertyChange(filename):
1180 """Add svn's property change information into the patch if given file is
1181 new file.
1182
1183 We use Subversion's auto-props setting to retrieve its property.
1184 See http://svnbook.red-bean.com/en/1.1/ch07.html#svn-ch-7-sect-1.3.2 for
1185 Subversion's [auto-props] setting.
1186 """
1187 if self.options.emulate_svn_auto_props and IsFileNew(filename):
1188 svnprops = GetSubversionPropertyChanges(filename)
1189 if svnprops:
1190 svndiff.append("\n" + svnprops + "\n")
1191
1192 svndiff = []
1193 filecount = 0
1194 filename = None
1195 for line in gitdiff.splitlines():
1196 match = re.match(r"diff --git a/(.*) b/(.*)$", line)
1197 if match:
1198 # Add auto property here for previously seen file.
1199 if filename is not None:
1200 AddSubversionPropertyChange(filename)
1201 filecount += 1
1202 # Intentionally use the "after" filename so we can show renames.
1203 filename = match.group(2)
1204 svndiff.append("Index: %s\n" % filename)
1205 if match.group(1) != match.group(2):
1206 self.renames[match.group(2)] = match.group(1)
1207 else:
1208 # The "index" line in a git diff looks like this (long hashes elided):
1209 # index 82c0d44..b2cee3f 100755
1210 # We want to save the left hash, as that identifies the base file.
1211 match = re.match(r"index (\w+)\.\.(\w+)", line)
1212 if match:
1213 before, after = (match.group(1), match.group(2))
1214 if before == NULL_HASH:
1215 before = None
1216 if after == NULL_HASH:
1217 after = None
1218 self.hashes[filename] = (before, after)
1219 svndiff.append(line + "\n")
1220 if not filecount:
1221 ErrorExit("No valid patches found in output from git diff")
1222 # Add auto property for the last seen file.
1223 assert filename is not None
1224 AddSubversionPropertyChange(filename)
1225 return "".join(svndiff)
1226
1227 def GenerateDiff(self, extra_args):
1228 extra_args = extra_args[:]
1229 if self.options.revision:
1230 if ":" in self.options.revision:
1231 extra_args = self.options.revision.split(":", 1) + extra_args
1232 else:
1233 extra_args = [self.options.revision] + extra_args
1234
1235 # --no-ext-diff is broken in some versions of Git, so try to work around
1236 # this by overriding the environment (but there is still a problem if the
1237 # git config key "diff.external" is used).
1238 env = os.environ.copy()
1239 if 'GIT_EXTERNAL_DIFF' in env: del env['GIT_EXTERNAL_DIFF']
1240 return RunShell(["git", "diff", "--no-ext-diff", "--full-index", "-M"]
1241 + extra_args, env=env)
1242
1243 def GetUnknownFiles(self):
1244 status = RunShell(["git", "ls-files", "--exclude-standard", "--others"],
1245 silent_ok=True)
1246 return status.splitlines()
1247
1248 def GetFileContent(self, file_hash, is_binary):
1249 """Returns the content of a file identified by its git hash."""
1250 data, retcode = RunShellWithReturnCode(["git", "show", file_hash],
1251 universal_newlines=not is_binary)
1252 if retcode:
1253 ErrorExit("Got error status from 'git show %s'" % file_hash)
1254 return data
1255
1256 def GetBaseFile(self, filename):
1257 hash_before, hash_after = self.hashes.get(filename, (None,None))
1258 base_content = None
1259 new_content = None
1260 is_binary = self.IsBinary(filename)
1261 status = None
1262
1263 if filename in self.renames:
1264 status = "A +" # Match svn attribute name for renames.
1265 if filename not in self.hashes:
1266 # If a rename doesn't change the content, we never get a hash.
1267 base_content = RunShell(["git", "show", "HEAD:" + filename])
1268 elif not hash_before:
1269 status = "A"
1270 base_content = ""
1271 elif not hash_after:
1272 status = "D"
1273 else:
1274 status = "M"
1275
1276 is_image = self.IsImage(filename)
1277
1278 # Grab the before/after content if we need it.
1279 # We should include file contents if it's text or it's an image.
1280 if not is_binary or is_image:
1281 # Grab the base content if we don't have it already.
1282 if base_content is None and hash_before:
1283 base_content = self.GetFileContent(hash_before, is_binary)
1284 # Only include the "after" file if it's an image; otherwise it
1285 # it is reconstructed from the diff.
1286 if is_image and hash_after:
1287 new_content = self.GetFileContent(hash_after, is_binary)
1288
1289 return (base_content, new_content, is_binary, status)
1290
1291
1292 class CVSVCS(VersionControlSystem):
1293 """Implementation of the VersionControlSystem interface for CVS."""
1294
1295 def __init__(self, options):
1296 super(CVSVCS, self).__init__(options)
1297
1298 def GetOriginalContent_(self, filename):
1299 RunShell(["cvs", "up", filename], silent_ok=True)
1300 # TODO need detect file content encoding
1301 content = open(filename).read()
1302 return content.replace("\r\n", "\n")
1303
1304 def GetBaseFile(self, filename):
1305 base_content = None
1306 new_content = None
1307 is_binary = False
1308 status = "A"
1309
1310 output, retcode = RunShellWithReturnCode(["cvs", "status", filename])
1311 if retcode:
1312 ErrorExit("Got error status from 'cvs status %s'" % filename)
1313
1314 if output.find("Status: Locally Modified") != -1:
1315 status = "M"
1316 temp_filename = "%s.tmp123" % filename
1317 os.rename(filename, temp_filename)
1318 base_content = self.GetOriginalContent_(filename)
1319 os.rename(temp_filename, filename)
1320 elif output.find("Status: Locally Added"):
1321 status = "A"
1322 base_content = ""
1323 elif output.find("Status: Needs Checkout"):
1324 status = "D"
1325 base_content = self.GetOriginalContent_(filename)
1326
1327 return (base_content, new_content, is_binary, status)
1328
1329 def GenerateDiff(self, extra_args):
1330 cmd = ["cvs", "diff", "-u", "-N"]
1331 if self.options.revision:
1332 cmd += ["-r", self.options.revision]
1333
1334 cmd.extend(extra_args)
1335 data, retcode = RunShellWithReturnCode(cmd)
1336 count = 0
1337 if retcode in [0, 1]:
1338 for line in data.splitlines():
1339 if line.startswith("Index:"):
1340 count += 1
1341 logging.info(line)
1342
1343 if not count:
1344 ErrorExit("No valid patches found in output from cvs diff")
1345
1346 return data
1347
1348 def GetUnknownFiles(self):
1349 data, retcode = RunShellWithReturnCode(["cvs", "diff"])
1350 if retcode not in [0, 1]:
1351 ErrorExit("Got error status from 'cvs diff':\n%s" % (data,))
1352 unknown_files = []
1353 for line in data.split("\n"):
1354 if line and line[0] == "?":
1355 unknown_files.append(line)
1356 return unknown_files
1357
1358 class MercurialVCS(VersionControlSystem):
1359 """Implementation of the VersionControlSystem interface for Mercurial."""
1360
1361 def __init__(self, options, repo_dir):
1362 super(MercurialVCS, self).__init__(options)
1363 # Absolute path to repository (we can be in a subdir)
1364 self.repo_dir = os.path.normpath(repo_dir)
1365 # Compute the subdir
1366 cwd = os.path.normpath(os.getcwd())
1367 assert cwd.startswith(self.repo_dir)
1368 self.subdir = cwd[len(self.repo_dir):].lstrip(r"\/")
1369 if self.options.revision:
1370 self.base_rev = self.options.revision
1371 else:
1372 self.base_rev = RunShell(["hg", "parent", "-q"]).split(':')[1].strip()
1373
1374 def _GetRelPath(self, filename):
1375 """Get relative path of a file according to the current directory,
1376 given its logical path in the repo."""
1377 assert filename.startswith(self.subdir), (filename, self.subdir)
1378 return filename[len(self.subdir):].lstrip(r"\/")
1379
1380 def GenerateDiff(self, extra_args):
1381 cmd = ["hg", "diff", "--git", "-r", self.base_rev] + extra_args
1382 data = RunShell(cmd, silent_ok=True)
1383 svndiff = []
1384 filecount = 0
1385 for line in data.splitlines():
1386 m = re.match("diff --git a/(\S+) b/(\S+)", line)
1387 if m:
1388 # Modify line to make it look like as it comes from svn diff.
1389 # With this modification no changes on the server side are required
1390 # to make upload.py work with Mercurial repos.
1391 # NOTE: for proper handling of moved/copied files, we have to use
1392 # the second filename.
1393 filename = m.group(2)
1394 svndiff.append("Index: %s" % filename)
1395 svndiff.append("=" * 67)
1396 filecount += 1
1397 logging.info(line)
1398 else:
1399 svndiff.append(line)
1400 if not filecount:
1401 ErrorExit("No valid patches found in output from hg diff")
1402 return "\n".join(svndiff) + "\n"
1403
1404 def GetUnknownFiles(self):
1405 """Return a list of files unknown to the VCS."""
1406 args = []
1407 status = RunShell(["hg", "status", "--rev", self.base_rev, "-u", "."],
1408 silent_ok=True)
1409 unknown_files = []
1410 for line in status.splitlines():
1411 st, fn = line.split(" ", 1)
1412 if st == "?":
1413 unknown_files.append(fn)
1414 return unknown_files
1415
1416 def GetBaseFile(self, filename):
1417 # "hg status" and "hg cat" both take a path relative to the current subdir
1418 # rather than to the repo root, but "hg diff" has given us the full path
1419 # to the repo root.
1420 base_content = ""
1421 new_content = None
1422 is_binary = False
1423 oldrelpath = relpath = self._GetRelPath(filename)
1424 # "hg status -C" returns two lines for moved/copied files, one otherwise
1425 out = RunShell(["hg", "status", "-C", "--rev", self.base_rev, relpath])
1426 out = out.splitlines()
1427 # HACK: strip error message about missing file/directory if it isn't in
1428 # the working copy
1429 if out[0].startswith('%s: ' % relpath):
1430 out = out[1:]
1431 status, _ = out[0].split(' ', 1)
1432 if len(out) > 1 and status == "A":
1433 # Moved/copied => considered as modified, use old filename to
1434 # retrieve base contents
1435 oldrelpath = out[1].strip()
1436 status = "M"
1437 if ":" in self.base_rev:
1438 base_rev = self.base_rev.split(":", 1)[0]
1439 else:
1440 base_rev = self.base_rev
1441 if status != "A":
1442 base_content = RunShell(["hg", "cat", "-r", base_rev, oldrelpath],
1443 silent_ok=True)
1444 is_binary = "\0" in base_content # Mercurial's heuristic
1445 if status != "R":
1446 new_content = open(relpath, "rb").read()
1447 is_binary = is_binary or "\0" in new_content
1448 if is_binary and base_content:
1449 # Fetch again without converting newlines
1450 base_content = RunShell(["hg", "cat", "-r", base_rev, oldrelpath],
1451 silent_ok=True, universal_newlines=False)
1452 if not is_binary or not self.IsImage(relpath):
1453 new_content = None
1454 return base_content, new_content, is_binary, status
1455
1456
1457 class PerforceVCS(VersionControlSystem):
1458 """Implementation of the VersionControlSystem interface for Perforce."""
1459
1460 def __init__(self, options):
1461 ····
1462 def ConfirmLogin():
1463 # Make sure we have a valid perforce session
1464 while True:
1465 data, retcode = self.RunPerforceCommandWithReturnCode(
1466 ["login", "-s"], marshal_output=True)
1467 if not data:
1468 ErrorExit("Error checking perforce login")
1469 if not retcode and (not "code" in data or data["code"] != "error"):
1470 break
1471 print "Enter perforce password: "
1472 self.RunPerforceCommandWithReturnCode(["login"])
1473 ······
1474 super(PerforceVCS, self).__init__(options)
1475 ····
1476 self.p4_changelist = options.p4_changelist
1477 if not self.p4_changelist:
1478 ErrorExit("A changelist id is required")
1479 if (options.revision):
1480 ErrorExit("--rev is not supported for perforce")
1481 ····
1482 self.p4_port = options.p4_port
1483 self.p4_client = options.p4_client
1484 self.p4_user = options.p4_user
1485 ····
1486 ConfirmLogin()
1487 ····
1488 if not options.message:
1489 description = self.RunPerforceCommand(["describe", self.p4_changelist],
1490 marshal_output=True)
1491 if description and "desc" in description:
1492 # Rietveld doesn't support multi-line descriptions
1493 raw_message = description["desc"].strip()
1494 lines = raw_message.splitlines()
1495 if len(lines):
1496 options.message = lines[0]
1497 ··
1498 def RunPerforceCommandWithReturnCode(self, extra_args, marshal_output=False,
1499 universal_newlines=True):
1500 args = ["p4"]
1501 if marshal_output:
1502 # -G makes perforce format its output as marshalled python objects
1503 args.extend(["-G"])
1504 if self.p4_port:
1505 args.extend(["-p", self.p4_port])
1506 if self.p4_client:
1507 args.extend(["-c", self.p4_client])
1508 if self.p4_user:
1509 args.extend(["-u", self.p4_user])
1510 args.extend(extra_args)
1511 ····
1512 data, retcode = RunShellWithReturnCode(
1513 args, print_output=False, universal_newlines=universal_newlines)
1514 if marshal_output and data:
1515 data = marshal.loads(data)
1516 return data, retcode
1517 ····
1518 def RunPerforceCommand(self, extra_args, marshal_output=False,
1519 universal_newlines=True):
1520 # This might be a good place to cache call results, since things like
1521 # describe or fstat might get called repeatedly.
1522 data, retcode = self.RunPerforceCommandWithReturnCode(
1523 extra_args, marshal_output, universal_newlines)
1524 if retcode:
1525 ErrorExit("Got error status from %s:\n%s" % (extra_args, data))
1526 return data
1527
1528 def GetFileProperties(self, property_key_prefix = "", command = "describe"):
1529 description = self.RunPerforceCommand(["describe", self.p4_changelist],
1530 marshal_output=True)
1531 ····
1532 changed_files = {}
1533 file_index = 0
1534 # Try depotFile0, depotFile1, ... until we don't find a match
1535 while True:
1536 file_key = "depotFile%d" % file_index
1537 if file_key in description:
1538 filename = description[file_key]
1539 change_type = description[property_key_prefix + str(file_index)]
1540 changed_files[filename] = change_type
1541 file_index += 1
1542 else:
1543 break
1544 return changed_files
1545
1546 def GetChangedFiles(self):
1547 return self.GetFileProperties("action")
1548
1549 def GetUnknownFiles(self):
1550 # Perforce doesn't detect new files, they have to be explicitly added
1551 return []
1552
1553 def IsBaseBinary(self, filename):
1554 base_filename = self.GetBaseFilename(filename)
1555 return self.IsBinaryHelper(base_filename, "files")
1556
1557 def IsPendingBinary(self, filename):
1558 return self.IsBinaryHelper(filename, "describe")
1559
1560 def IsBinary(self, filename):
1561 ErrorExit("IsBinary is not safe: call IsBaseBinary or IsPendingBinary")
1562
1563 def IsBinaryHelper(self, filename, command):
1564 file_types = self.GetFileProperties("type", command)
1565 if not filename in file_types:
1566 ErrorExit("Trying to check binary status of unknown file %s." % filename)
1567 # This treats symlinks, macintosh resource files, temporary objects, and
1568 # unicode as binary. See the Perforce docs for more details:
1569 # http://www.perforce.com/perforce/doc.current/manuals/cmdref/o.ftypes.html
1570 return not file_types[filename].endswith("text")
1571
1572 def GetFileContent(self, filename, revision, is_binary):
1573 file_arg = filename
1574 if revision:
1575 file_arg += "#" + revision
1576 # -q suppresses the initial line that displays the filename and revision
1577 return self.RunPerforceCommand(["print", "-q", file_arg],
1578 universal_newlines=not is_binary)
1579
1580 def GetBaseFilename(self, filename):
1581 actionsWithDifferentBases = [
1582 "move/add", # p4 move
1583 "branch", # p4 integrate (to a new file), similar to hg "add"
1584 "add", # p4 integrate (to a new file), after modifying the new file
1585 ]
1586 ····
1587 # We only see a different base for "add" if this is a downgraded branch
1588 # after a file was branched (integrated), then edited.·
1589 if self.GetAction(filename) in actionsWithDifferentBases:
1590 # -Or shows information about pending integrations/moves
1591 fstat_result = self.RunPerforceCommand(["fstat", "-Or", filename],
1592 marshal_output=True)
1593 ······
1594 baseFileKey = "resolveFromFile0" # I think it's safe to use only file0
1595 if baseFileKey in fstat_result:
1596 return fstat_result[baseFileKey]
1597 ····
1598 return filename
1599
1600 def GetBaseRevision(self, filename):
1601 base_filename = self.GetBaseFilename(filename)
1602 ····
1603 have_result = self.RunPerforceCommand(["have", base_filename],
1604 marshal_output=True)
1605 if "haveRev" in have_result:
1606 return have_result["haveRev"]
1607 ····
1608 def GetLocalFilename(self, filename):
1609 where = self.RunPerforceCommand(["where", filename], marshal_output=True)
1610 if "path" in where:
1611 return where["path"]
1612
1613 def GenerateDiff(self, args):·
1614 class DiffData:
1615 def __init__(self, perforceVCS, filename, action):
1616 self.perforceVCS = perforceVCS
1617 self.filename = filename
1618 self.action = action
1619 self.base_filename = perforceVCS.GetBaseFilename(filename)
1620 ········
1621 self.file_body = None
1622 self.base_rev = None
1623 self.prefix = None
1624 self.working_copy = True
1625 self.change_summary = None
1626 ·········
1627 def GenerateDiffHeader(diffData):
1628 header = []
1629 header.append("Index: %s" % diffData.filename)
1630 header.append("=" * 67)
1631 ······
1632 if diffData.base_filename != diffData.filename:
1633 if diffData.action.startswith("move"):
1634 verb = "rename"
1635 else:
1636 verb = "copy"
1637 header.append("%s from %s" % (verb, diffData.base_filename))
1638 header.append("%s to %s" % (verb, diffData.filename))
1639 ········
1640 suffix = "\t(revision %s)" % diffData.base_rev
1641 header.append("--- " + diffData.base_filename + suffix)
1642 if diffData.working_copy:
1643 suffix = "\t(working copy)"
1644 header.append("+++ " + diffData.filename + suffix)
1645 if diffData.change_summary:
1646 header.append(diffData.change_summary)
1647 return header
1648 ··
1649 def GenerateMergeDiff(diffData, args):
1650 # -du generates a unified diff, which is nearly svn format
1651 diffData.file_body = self.RunPerforceCommand(
1652 ["diff", "-du", diffData.filename] + args)
1653 diffData.base_rev = self.GetBaseRevision(diffData.filename)
1654 diffData.prefix = ""
1655 ······
1656 # We have to replace p4's file status output (the lines starting
1657 # with +++ or ---) to match svn's diff format
1658 lines = diffData.file_body.splitlines()
1659 first_good_line = 0
1660 while (first_good_line < len(lines) and
1661 not lines[first_good_line].startswith("@@")):
1662 first_good_line += 1
1663 diffData.file_body = "\n".join(lines[first_good_line:])
1664 return diffData
1665
1666 def GenerateAddDiff(diffData):
1667 fstat = self.RunPerforceCommand(["fstat", diffData.filename],
1668 marshal_output=True)
1669 if "headRev" in fstat:
1670 diffData.base_rev = fstat["headRev"] # Re-adding a deleted file
1671 else:
1672 diffData.base_rev = "0" # Brand new file
1673 diffData.working_copy = False
1674 rel_path = self.GetLocalFilename(diffData.filename)
1675 diffData.file_body = open(rel_path, 'r').read()
1676 # Replicate svn's list of changed lines
1677 line_count = len(diffData.file_body.splitlines())
1678 diffData.change_summary = "@@ -0,0 +1"
1679 if line_count > 1:
1680 diffData.change_summary += ",%d" % line_count
1681 diffData.change_summary += " @@"
1682 diffData.prefix = "+"
1683 return diffData
1684 ····
1685 def GenerateDeleteDiff(diffData):
1686 diffData.base_rev = self.GetBaseRevision(diffData.filename)
1687 is_base_binary = self.IsBaseBinary(diffData.filename)
1688 # For deletes, base_filename == filename
1689 diffData.file_body = self.GetFileContent(diffData.base_filename,·
1690 None,·
1691 is_base_binary)
1692 # Replicate svn's list of changed lines
1693 line_count = len(diffData.file_body.splitlines())
1694 diffData.change_summary = "@@ -1"
1695 if line_count > 1:
1696 diffData.change_summary += ",%d" % line_count
1697 diffData.change_summary += " +0,0 @@"
1698 diffData.prefix = "-"
1699 return diffData
1700 ··
1701 changed_files = self.GetChangedFiles()
1702 ····
1703 svndiff = []
1704 filecount = 0
1705 for (filename, action) in changed_files.items():
1706 svn_status = self.PerforceActionToSvnStatus(action)
1707 if svn_status == "SKIP":
1708 continue
1709 ····
1710 diffData = DiffData(self, filename, action)
1711 # Is it possible to diff a branched file? Stackoverflow says no:
1712 # http://stackoverflow.com/questions/1771314/in-perforce-command-line-how- to-diff-a-file-reopened-for-add
1713 if svn_status == "M":
1714 diffData = GenerateMergeDiff(diffData, args)
1715 elif svn_status == "A":
1716 diffData = GenerateAddDiff(diffData)
1717 elif svn_status == "D":
1718 diffData = GenerateDeleteDiff(diffData)
1719 else:
1720 ErrorExit("Unknown file action %s (svn action %s)." % \
1721 (action, svn_status))
1722 ······
1723 svndiff += GenerateDiffHeader(diffData)
1724 ······
1725 for line in diffData.file_body.splitlines():
1726 svndiff.append(diffData.prefix + line)
1727 filecount += 1
1728 if not filecount:
1729 ErrorExit("No valid patches found in output from p4 diff")
1730 return "\n".join(svndiff) + "\n"
1731
1732 def PerforceActionToSvnStatus(self, status):
1733 # Mirroring the list at http://permalink.gmane.org/gmane.comp.version-contro l.mercurial.devel/28717
1734 # Is there something more official?
1735 return {
1736 "add" : "A",
1737 "branch" : "A",
1738 "delete" : "D",
1739 "edit" : "M", # Also includes changing file types.
1740 "integrate" : "M",
1741 "move/add" : "M",
1742 "move/delete": "SKIP",
1743 "purge" : "D", # How does a file's status become "purge"?
1744 }[status]
1745
1746 def GetAction(self, filename):
1747 changed_files = self.GetChangedFiles()
1748 if not filename in changed_files:
1749 ErrorExit("Trying to get base version of unknown file %s." % filename)
1750 ······
1751 return changed_files[filename]
1752
1753 def GetBaseFile(self, filename):
1754 base_filename = self.GetBaseFilename(filename)
1755 base_content = ""
1756 new_content = None
1757 ····
1758 status = self.PerforceActionToSvnStatus(self.GetAction(filename))
1759 ····
1760 if status != "A":
1761 revision = self.GetBaseRevision(base_filename)
1762 if not revision:
1763 ErrorExit("Couldn't find base revision for file %s" % filename)
1764 is_base_binary = self.IsBaseBinary(base_filename)
1765 base_content = self.GetFileContent(base_filename,
1766 revision,
1767 is_base_binary)
1768 ····
1769 is_binary = self.IsPendingBinary(filename)
1770 if status != "D" and status != "SKIP":
1771 relpath = self.GetLocalFilename(filename)
1772 if is_binary and self.IsImage(relpath):
1773 new_content = open(relpath, "rb").read()
1774 ····
1775 return base_content, new_content, is_binary, status
1776
1777 # NOTE: The SplitPatch function is duplicated in engine.py, keep them in sync.
1778 def SplitPatch(data):
1779 """Splits a patch into separate pieces for each file.
1780
1781 Args:
1782 data: A string containing the output of svn diff.
1783
1784 Returns:
1785 A list of 2-tuple (filename, text) where text is the svn diff output
1786 pertaining to filename.
1787 """
1788 patches = []
1789 filename = None
1790 diff = []
1791 for line in data.splitlines(True):
1792 new_filename = None
1793 if line.startswith('Index:'):
1794 unused, new_filename = line.split(':', 1)
1795 new_filename = new_filename.strip()
1796 elif line.startswith('Property changes on:'):
1797 unused, temp_filename = line.split(':', 1)
1798 # When a file is modified, paths use '/' between directories, however
1799 # when a property is modified '\' is used on Windows. Make them the same
1800 # otherwise the file shows up twice.
1801 temp_filename = temp_filename.strip().replace('\\', '/')
1802 if temp_filename != filename:
1803 # File has property changes but no modifications, create a new diff.
1804 new_filename = temp_filename
1805 if new_filename:
1806 if filename and diff:
1807 patches.append((filename, ''.join(diff)))
1808 filename = new_filename
1809 diff = [line]
1810 continue
1811 if diff is not None:
1812 diff.append(line)
1813 if filename and diff:
1814 patches.append((filename, ''.join(diff)))
1815 return patches
1816
1817
1818 def UploadSeparatePatches(issue, rpc_server, patchset, data, options):
1819 """Uploads a separate patch for each file in the diff output.
1820
1821 Returns a list of [patch_key, filename] for each file.
1822 """
1823 patches = SplitPatch(data)
1824 rv = []
1825 for patch in patches:
1826 if len(patch[1]) > MAX_UPLOAD_SIZE:
1827 print ("Not uploading the patch for " + patch[0] +
1828 " because the file is too large.")
1829 continue
1830 form_fields = [("filename", patch[0])]
1831 if not options.download_base:
1832 form_fields.append(("content_upload", "1"))
1833 files = [("data", "data.diff", patch[1])]
1834 ctype, body = EncodeMultipartFormData(form_fields, files)
1835 url = "/%d/upload_patch/%d" % (int(issue), int(patchset))
1836 print "Uploading patch for " + patch[0]
1837 response_body = rpc_server.Send(url, body, content_type=ctype)
1838 lines = response_body.splitlines()
1839 if not lines or lines[0] != "OK":
1840 StatusUpdate(" --> %s" % response_body)
1841 sys.exit(1)
1842 rv.append([lines[1], patch[0]])
1843 return rv
1844
1845
1846 def GuessVCSName(options):
1847 """Helper to guess the version control system.
1848
1849 This examines the current directory, guesses which VersionControlSystem
1850 we're using, and returns an string indicating which VCS is detected.
1851
1852 Returns:
1853 A pair (vcs, output). vcs is a string indicating which VCS was detected
1854 and is one of VCS_GIT, VCS_MERCURIAL, VCS_SUBVERSION, VCS_PERFORCE,
1855 VCS_CVS, or VCS_UNKNOWN.
1856 Since local perforce repositories can't be easily detected, this method
1857 will only guess VCS_PERFORCE if any perforce options have been specified.
1858 output is a string containing any interesting output from the vcs
1859 detection routine, or None if there is nothing interesting.
1860 """
1861 for attribute, value in options.__dict__.iteritems():
1862 if attribute.startswith("p4") and value != None:
1863 return (VCS_PERFORCE, None)
1864 ··
1865 def RunDetectCommand(vcs_type, command):
1866 """Helper to detect VCS by executing command.
1867 ····
1868 Returns:
1869 A pair (vcs, output) or None. Throws exception on error.
1870 """
1871 try:
1872 out, returncode = RunShellWithReturnCode(command)
1873 if returncode == 0:
1874 return (vcs_type, out.strip())
1875 except OSError, (errcode, message):
1876 if errcode != errno.ENOENT: # command not found code
1877 raise
1878 ··
1879 # Mercurial has a command to get the base directory of a repository
1880 # Try running it, but don't die if we don't have hg installed.
1881 # NOTE: we try Mercurial first as it can sit on top of an SVN working copy.
1882 res = RunDetectCommand(VCS_MERCURIAL, ["hg", "root"])
1883 if res != None:
1884 return res
1885
1886 # Subversion has a .svn in all working directories.
1887 if os.path.isdir('.svn'):
1888 logging.info("Guessed VCS = Subversion")
1889 return (VCS_SUBVERSION, None)
1890
1891 # Git has a command to test if you're in a git tree.
1892 # Try running it, but don't die if we don't have git installed.
1893 res = RunDetectCommand(VCS_GIT, ["git", "rev-parse",
1894 "--is-inside-work-tree"])
1895 if res != None:
1896 return res
1897
1898 # detect CVS repos use `cvs status && $? == 0` rules
1899 res = RunDetectCommand(VCS_CVS, ["cvs", "status"])
1900 if res != None:
1901 return res
1902
1903 return (VCS_UNKNOWN, None)
1904
1905
1906 def GuessVCS(options):
1907 """Helper to guess the version control system.
1908
1909 This verifies any user-specified VersionControlSystem (by command line
1910 or environment variable). If the user didn't specify one, this examines
1911 the current directory, guesses which VersionControlSystem we're using,
1912 and returns an instance of the appropriate class. Exit with an error
1913 if we can't figure it out.
1914
1915 Returns:
1916 A VersionControlSystem instance. Exits if the VCS can't be guessed.
1917 """
1918 vcs = options.vcs
1919 if not vcs:
1920 vcs = os.environ.get("CODEREVIEW_VCS")
1921 if vcs:
1922 v = VCS_ABBREVIATIONS.get(vcs.lower())
1923 if v is None:
1924 ErrorExit("Unknown version control system %r specified." % vcs)
1925 (vcs, extra_output) = (v, None)
1926 else:
1927 (vcs, extra_output) = GuessVCSName(options)
1928
1929 if vcs == VCS_MERCURIAL:
1930 if extra_output is None:
1931 extra_output = RunShell(["hg", "root"]).strip()
1932 return MercurialVCS(options, extra_output)
1933 elif vcs == VCS_SUBVERSION:
1934 return SubversionVCS(options)
1935 elif vcs == VCS_PERFORCE:
1936 return PerforceVCS(options)
1937 elif vcs == VCS_GIT:
1938 return GitVCS(options)
1939 elif vcs == VCS_CVS:
1940 return CVSVCS(options)
1941
1942 ErrorExit(("Could not guess version control system. "
1943 "Are you in a working copy directory?"))
1944
1945
1946 def CheckReviewer(reviewer):
1947 """Validate a reviewer -- either a nickname or an email addres.
1948
1949 Args:
1950 reviewer: A nickname or an email address.
1951
1952 Calls ErrorExit() if it is an invalid email address.
1953 """
1954 if "@" not in reviewer:
1955 return # Assume nickname
1956 parts = reviewer.split("@")
1957 if len(parts) > 2:
1958 ErrorExit("Invalid email address: %r" % reviewer)
1959 assert len(parts) == 2
1960 if "." not in parts[1]:
1961 ErrorExit("Invalid email address: %r" % reviewer)
1962
1963
1964 def LoadSubversionAutoProperties():
1965 """Returns the content of [auto-props] section of Subversion's config file as
1966 a dictionary.
1967
1968 Returns:
1969 A dictionary whose key-value pair corresponds the [auto-props] section's
1970 key-value pair.
1971 In following cases, returns empty dictionary:
1972 - config file doesn't exist, or
1973 - 'enable-auto-props' is not set to 'true-like-value' in [miscellany].
1974 """
1975 if os.name == 'nt':
1976 subversion_config = os.environ.get("APPDATA") + "\\Subversion\\config"
1977 else:
1978 subversion_config = os.path.expanduser("~/.subversion/config")
1979 if not os.path.exists(subversion_config):
1980 return {}
1981 config = ConfigParser.ConfigParser()
1982 config.read(subversion_config)
1983 if (config.has_section("miscellany") and
1984 config.has_option("miscellany", "enable-auto-props") and
1985 config.getboolean("miscellany", "enable-auto-props") and
1986 config.has_section("auto-props")):
1987 props = {}
1988 for file_pattern in config.options("auto-props"):
1989 props[file_pattern] = ParseSubversionPropertyValues(
1990 config.get("auto-props", file_pattern))
1991 return props
1992 else:
1993 return {}
1994
1995 def ParseSubversionPropertyValues(props):
1996 """Parse the given property value which comes from [auto-props] section and
1997 returns a list whose element is a (svn_prop_key, svn_prop_value) pair.
1998
1999 See the following doctest for example.
2000
2001 >>> ParseSubversionPropertyValues('svn:eol-style=LF')
2002 [('svn:eol-style', 'LF')]
2003 >>> ParseSubversionPropertyValues('svn:mime-type=image/jpeg')
2004 [('svn:mime-type', 'image/jpeg')]
2005 >>> ParseSubversionPropertyValues('svn:eol-style=LF;svn:executable')
2006 [('svn:eol-style', 'LF'), ('svn:executable', '*')]
2007 """
2008 key_value_pairs = []
2009 for prop in props.split(";"):
2010 key_value = prop.split("=")
2011 assert len(key_value) <= 2
2012 if len(key_value) == 1:
2013 # If value is not given, use '*' as a Subversion's convention.
2014 key_value_pairs.append((key_value[0], "*"))
2015 else:
2016 key_value_pairs.append((key_value[0], key_value[1]))
2017 return key_value_pairs
2018
2019
2020 def GetSubversionPropertyChanges(filename):
2021 """Return a Subversion's 'Property changes on ...' string, which is used in
2022 the patch file.
2023
2024 Args:
2025 filename: filename whose property might be set by [auto-props] config.
2026
2027 Returns:
2028 A string like 'Property changes on |filename| ...' if given |filename|
2029 matches any entries in [auto-props] section. None, otherwise.
2030 """
2031 global svn_auto_props_map
2032 if svn_auto_props_map is None:
2033 svn_auto_props_map = LoadSubversionAutoProperties()
2034
2035 all_props = []
2036 for file_pattern, props in svn_auto_props_map.items():
2037 if fnmatch.fnmatch(filename, file_pattern):
2038 all_props.extend(props)
2039 if all_props:
2040 return FormatSubversionPropertyChanges(filename, all_props)
2041 return None
2042
2043
2044 def FormatSubversionPropertyChanges(filename, props):
2045 """Returns Subversion's 'Property changes on ...' strings using given filename
2046 and properties.
2047
2048 Args:
2049 filename: filename
2050 props: A list whose element is a (svn_prop_key, svn_prop_value) pair.
2051
2052 Returns:
2053 A string which can be used in the patch file for Subversion.
2054
2055 See the following doctest for example.
2056
2057 >>> print FormatSubversionPropertyChanges('foo.cc', [('svn:eol-style', 'LF')])
2058 Property changes on: foo.cc
2059 ___________________________________________________________________
2060 Added: svn:eol-style
2061 + LF
2062 <BLANKLINE>
2063 """
2064 prop_changes_lines = [
2065 "Property changes on: %s" % filename,
2066 "___________________________________________________________________"]
2067 for key, value in props:
2068 prop_changes_lines.append("Added: " + key)
2069 prop_changes_lines.append(" + " + value)
2070 return "\n".join(prop_changes_lines) + "\n"
2071
2072
2073 def RealMain(argv, data=None):
2074 """The real main function.
2075
2076 Args:
2077 argv: Command line arguments.
2078 data: Diff contents. If None (default) the diff is generated by
2079 the VersionControlSystem implementation returned by GuessVCS().
2080
2081 Returns:
2082 A 2-tuple (issue id, patchset id).
2083 The patchset id is None if the base files are not uploaded by this
2084 script (applies only to SVN checkouts).
2085 """
2086 options, args = parser.parse_args(argv[1:])
2087 global verbosity
2088 verbosity = options.verbose
2089 if verbosity >= 3:
2090 logging.getLogger().setLevel(logging.DEBUG)
2091 elif verbosity >= 2:
2092 logging.getLogger().setLevel(logging.INFO)
2093
2094 vcs = GuessVCS(options)
2095
2096 base = options.base_url
2097 if isinstance(vcs, SubversionVCS):
2098 # Guessing the base field is only supported for Subversion.
2099 # Note: Fetching base files may become deprecated in future releases.
2100 guessed_base = vcs.GuessBase(options.download_base)
2101 if base:
2102 if guessed_base and base != guessed_base:
2103 print "Using base URL \"%s\" from --base_url instead of \"%s\"" % \
2104 (base, guessed_base)
2105 else:
2106 base = guessed_base
2107
2108 if not base and options.download_base:
2109 options.download_base = True
2110 logging.info("Enabled upload of base file")
2111 if not options.assume_yes:
2112 vcs.CheckForUnknownFiles()
2113 if data is None:
2114 data = vcs.GenerateDiff(args)
2115 data = vcs.PostProcessDiff(data)
2116 if options.print_diffs:
2117 print "Rietveld diff start:*****"
2118 print data
2119 print "Rietveld diff end:*****"
2120 files = vcs.GetBaseFiles(data)
2121 if verbosity >= 1:
2122 print "Upload server:", options.server, "(change with -s/--server)"
2123 if options.issue:
2124 prompt = "Message describing this patch set: "
2125 else:
2126 prompt = "New issue subject: "
2127 message = options.message or raw_input(prompt).strip()
2128 if not message:
2129 ErrorExit("A non-empty message is required")
2130 rpc_server = GetRpcServer(options.server,
2131 options.email,
2132 options.host,
2133 options.save_cookies,
2134 options.account_type)
2135 form_fields = [("subject", message)]
2136 if base:
2137 b = urlparse.urlparse(base)
2138 username, netloc = urllib.splituser(b.netloc)
2139 if username:
2140 logging.info("Removed username from base URL")
2141 base = urlparse.urlunparse((b.scheme, netloc, b.path, b.params,
2142 b.query, b.fragment))
2143 form_fields.append(("base", base))
2144 if options.issue:
2145 form_fields.append(("issue", str(options.issue)))
2146 if options.email:
2147 form_fields.append(("user", options.email))
2148 if options.reviewers:
2149 for reviewer in options.reviewers.split(','):
2150 CheckReviewer(reviewer)
2151 form_fields.append(("reviewers", options.reviewers))
2152 if options.cc:
2153 for cc in options.cc.split(','):
2154 CheckReviewer(cc)
2155 form_fields.append(("cc", options.cc))
2156 description = options.description
2157 if options.description_file:
2158 if options.description:
2159 ErrorExit("Can't specify description and description_file")
2160 file = open(options.description_file, 'r')
2161 description = file.read()
2162 file.close()
2163 if description:
2164 form_fields.append(("description", description))
2165 # Send a hash of all the base file so the server can determine if a copy
2166 # already exists in an earlier patchset.
2167 base_hashes = ""
2168 for file, info in files.iteritems():
2169 if not info[0] is None:
2170 checksum = md5(info[0]).hexdigest()
2171 if base_hashes:
2172 base_hashes += "|"
2173 base_hashes += checksum + ":" + file
2174 form_fields.append(("base_hashes", base_hashes))
2175 if options.private:
2176 if options.issue:
2177 print "Warning: Private flag ignored when updating an existing issue."
2178 else:
2179 form_fields.append(("private", "1"))
2180 # If we're uploading base files, don't send the email before the uploads, so
2181 # that it contains the file status.
2182 if options.send_mail and options.download_base:
2183 form_fields.append(("send_mail", "1"))
2184 if not options.download_base:
2185 form_fields.append(("content_upload", "1"))
2186 if len(data) > MAX_UPLOAD_SIZE:
2187 print "Patch is large, so uploading file patches separately."
2188 uploaded_diff_file = []
2189 form_fields.append(("separate_patches", "1"))
2190 else:
2191 uploaded_diff_file = [("data", "data.diff", data)]
2192 ctype, body = EncodeMultipartFormData(form_fields, uploaded_diff_file)
2193 response_body = rpc_server.Send("/upload", body, content_type=ctype)
2194 patchset = None
2195 if not options.download_base or not uploaded_diff_file:
2196 lines = response_body.splitlines()
2197 if len(lines) >= 2:
2198 msg = lines[0]
2199 patchset = lines[1].strip()
2200 patches = [x.split(" ", 1) for x in lines[2:]]
2201 else:
2202 msg = response_body
2203 else:
2204 msg = response_body
2205 StatusUpdate(msg)
2206 if not response_body.startswith("Issue created.") and \
2207 not response_body.startswith("Issue updated."):
2208 sys.exit(0)
2209 issue = msg[msg.rfind("/")+1:]
2210
2211 if not uploaded_diff_file:
2212 result = UploadSeparatePatches(issue, rpc_server, patchset, data, options)
2213 if not options.download_base:
2214 patches = result
2215
2216 if not options.download_base:
2217 vcs.UploadBaseFiles(issue, rpc_server, patches, patchset, options, files)
2218 if options.send_mail:
2219 rpc_server.Send("/" + issue + "/mail", payload="")
2220 return issue, patchset
2221
2222
2223 def main():
2224 try:
2225 logging.basicConfig(format=("%(asctime).19s %(levelname)s %(filename)s:"
2226 "%(lineno)s %(message)s "))
2227 os.environ['LC_ALL'] = 'C'
2228 RealMain(sys.argv)
2229 except KeyboardInterrupt:
2230 print
2231 StatusUpdate("Interrupted.")
2232 sys.exit(1)
2233
2234
2235 if __name__ == "__main__":
2236 main()
OLDNEW
« MoinMoin/search/analyzers.py ('K') | « MoinMoin/search/analyzers.py ('k') | no next file » | no next file with comments »

Powered by Google App Engine
RSS Feeds Recent Issues | This issue
This is Rietveld f62528b