Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(269)

Side by Side Diff: third_party/upload.py

Issue 2027008: Update upload.py to r529 (Closed)
Patch Set: Created 10 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « no previous file | 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
1 #!/usr/bin/env python 1 #!/usr/bin/env python
2 # 2 #
3 # Copyright 2007 Google Inc. 3 # Copyright 2007 Google Inc.
4 # 4 #
5 # Licensed under the Apache License, Version 2.0 (the "License"); 5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with 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 7 # You may obtain a copy of the License at
8 # 8 #
9 # http://www.apache.org/licenses/LICENSE-2.0 9 # http://www.apache.org/licenses/LICENSE-2.0
10 # 10 #
(...skipping 13 matching lines...) Expand all
24 Git 24 Git
25 Mercurial 25 Mercurial
26 Subversion 26 Subversion
27 27
28 It is important for Git/Mercurial users to specify a tree/node/branch to diff 28 It is important for Git/Mercurial users to specify a tree/node/branch to diff
29 against by using the '--rev' option. 29 against by using the '--rev' option.
30 """ 30 """
31 # This code is derived from appcfg.py in the App Engine SDK (open source), 31 # This code is derived from appcfg.py in the App Engine SDK (open source),
32 # and from ASPN recipe #146306. 32 # and from ASPN recipe #146306.
33 33
34 import ConfigParser
34 import cookielib 35 import cookielib
36 import fnmatch
35 import getpass 37 import getpass
36 import logging 38 import logging
37 import mimetypes 39 import mimetypes
38 import optparse 40 import optparse
39 import os 41 import os
40 import re 42 import re
41 import socket 43 import socket
42 import subprocess 44 import subprocess
43 import sys 45 import sys
44 import urllib 46 import urllib
(...skipping 23 matching lines...) Expand all
68 70
69 # Constants for version control names. Used by GuessVCSName. 71 # Constants for version control names. Used by GuessVCSName.
70 VCS_GIT = "Git" 72 VCS_GIT = "Git"
71 VCS_MERCURIAL = "Mercurial" 73 VCS_MERCURIAL = "Mercurial"
72 VCS_SUBVERSION = "Subversion" 74 VCS_SUBVERSION = "Subversion"
73 VCS_UNKNOWN = "Unknown" 75 VCS_UNKNOWN = "Unknown"
74 76
75 # whitelist for non-binary filetypes which do not start with "text/" 77 # whitelist for non-binary filetypes which do not start with "text/"
76 # .mm (Objective-C) shows up as application/x-freemind on my Linux box. 78 # .mm (Objective-C) shows up as application/x-freemind on my Linux box.
77 TEXT_MIMETYPES = ['application/javascript', 'application/x-javascript', 79 TEXT_MIMETYPES = ['application/javascript', 'application/x-javascript',
78 'application/x-freemind'] 80 'application/xml', 'application/x-freemind']
79 81
80 VCS_ABBREVIATIONS = { 82 VCS_ABBREVIATIONS = {
81 VCS_MERCURIAL.lower(): VCS_MERCURIAL, 83 VCS_MERCURIAL.lower(): VCS_MERCURIAL,
82 "hg": VCS_MERCURIAL, 84 "hg": VCS_MERCURIAL,
83 VCS_SUBVERSION.lower(): VCS_SUBVERSION, 85 VCS_SUBVERSION.lower(): VCS_SUBVERSION,
84 "svn": VCS_SUBVERSION, 86 "svn": VCS_SUBVERSION,
85 VCS_GIT.lower(): VCS_GIT, 87 VCS_GIT.lower(): VCS_GIT,
86 } 88 }
87 89
90 # The result of parsing Subversion's [auto-props] setting.
91 svn_auto_props_map = None
88 92
89 def GetEmail(prompt): 93 def GetEmail(prompt):
90 """Prompts the user for their email address and returns it. 94 """Prompts the user for their email address and returns it.
91 95
92 The last used email address is saved to a file and offered up as a suggestion 96 The last used email address is saved to a file and offered up as a suggestion
93 to the user. If the user presses enter without typing in anything the last 97 to the user. If the user presses enter without typing in anything the last
94 used email address is used. If the user enters a new address, it is saved 98 used email address is used. If the user enters a new address, it is saved
95 for next time we prompt. 99 for next time we prompt.
96 100
97 """ 101 """
(...skipping 106 matching lines...) Expand 10 before | Expand all | Expand 10 after
204 password: The user's password 208 password: The user's password
205 209
206 Raises: 210 Raises:
207 ClientLoginError: If there was an error authenticating with ClientLogin. 211 ClientLoginError: If there was an error authenticating with ClientLogin.
208 HTTPError: If there was some other form of HTTP error. 212 HTTPError: If there was some other form of HTTP error.
209 213
210 Returns: 214 Returns:
211 The authentication token returned by ClientLogin. 215 The authentication token returned by ClientLogin.
212 """ 216 """
213 account_type = "GOOGLE" 217 account_type = "GOOGLE"
218 if self.host.endswith(".google.com"):
219 # Needed for use inside Google.
220 account_type = "HOSTED"
214 req = self._CreateRequest( 221 req = self._CreateRequest(
215 url="https://www.google.com/accounts/ClientLogin", 222 url="https://www.google.com/accounts/ClientLogin",
216 data=urllib.urlencode({ 223 data=urllib.urlencode({
217 "Email": email, 224 "Email": email,
218 "Passwd": password, 225 "Passwd": password,
219 "service": "ah", 226 "service": "ah",
220 "source": "rietveld-codereview-upload", 227 "source": "rietveld-codereview-upload",
221 "accountType": account_type, 228 "accountType": account_type,
222 }), 229 }),
223 ) 230 )
224 try: 231 try:
225 response = self.opener.open(req) 232 response = self.opener.open(req)
226 response_body = response.read() 233 response_body = response.read()
227 response_dict = dict(x.split("=") 234 response_dict = dict(x.split("=")
228 for x in response_body.split("\n") if x) 235 for x in response_body.split("\n") if x)
229 return response_dict["Auth"] 236 return response_dict["Auth"]
230 except urllib2.HTTPError, e: 237 except urllib2.HTTPError, e:
231 if e.code == 403: 238 if e.code == 403:
232 # Try a temporary workaround.
233 if self.host.endswith(".google.com"):
234 account_type = "HOSTED"
235 req = self._CreateRequest(
236 url="https://www.google.com/accounts/ClientLogin",
237 data=urllib.urlencode({
238 "Email": email,
239 "Passwd": password,
240 "service": "ah",
241 "source": "rietveld-codereview-upload",
242 "accountType": account_type,
243 }),
244 )
245 try:
246 response = self.opener.open(req)
247 response_body = response.read()
248 response_dict = dict(x.split("=")
249 for x in response_body.split("\n") if x)
250 return response_dict["Auth"]
251 except urllib2.HTTPError, e:
252 if e.code == 403:
253 body = e.read()
254 response_dict = dict(x.split("=", 1) for x in body.split("\n")
255 if x)
256 raise ClientLoginError(req.get_full_url(), e.code, e.msg,
257 e.headers, response_dict)
258 else:
259 raise
260
261 body = e.read() 239 body = e.read()
262 response_dict = dict(x.split("=", 1) for x in body.split("\n") if x) 240 response_dict = dict(x.split("=", 1) for x in body.split("\n") if x)
263 raise ClientLoginError(req.get_full_url(), e.code, e.msg, 241 raise ClientLoginError(req.get_full_url(), e.code, e.msg,
264 e.headers, response_dict) 242 e.headers, response_dict)
265 else: 243 else:
266 raise 244 raise
267 245
268 def _GetAuthCookie(self, auth_token): 246 def _GetAuthCookie(self, auth_token):
269 """Fetches authentication cookies for an authentication token. 247 """Fetches authentication cookies for an authentication token.
270 248
(...skipping 66 matching lines...) Expand 10 before | Expand all | Expand 10 after
337 if e.reason == "ServiceUnavailable": 315 if e.reason == "ServiceUnavailable":
338 print >>sys.stderr, "The service is not available; try again later." 316 print >>sys.stderr, "The service is not available; try again later."
339 break 317 break
340 raise 318 raise
341 self._GetAuthCookie(auth_token) 319 self._GetAuthCookie(auth_token)
342 return 320 return
343 321
344 def Send(self, request_path, payload=None, 322 def Send(self, request_path, payload=None,
345 content_type="application/octet-stream", 323 content_type="application/octet-stream",
346 timeout=None, 324 timeout=None,
325 extra_headers=None,
347 **kwargs): 326 **kwargs):
348 """Sends an RPC and returns the response. 327 """Sends an RPC and returns the response.
349 328
350 Args: 329 Args:
351 request_path: The path to send the request to, eg /api/appversion/create. 330 request_path: The path to send the request to, eg /api/appversion/create.
352 payload: The body of the request, or None to send an empty request. 331 payload: The body of the request, or None to send an empty request.
353 content_type: The Content-Type header to use. 332 content_type: The Content-Type header to use.
354 timeout: timeout in seconds; default None i.e. no timeout. 333 timeout: timeout in seconds; default None i.e. no timeout.
355 (Note: for large requests on OS X, the timeout doesn't work right.) 334 (Note: for large requests on OS X, the timeout doesn't work right.)
335 extra_headers: Dict containing additional HTTP headers that should be
336 included in the request (string header names mapped to their values),
337 or None to not include any additional headers.
356 kwargs: Any keyword arguments are converted into query string parameters. 338 kwargs: Any keyword arguments are converted into query string parameters.
357 339
358 Returns: 340 Returns:
359 The response body, as a string. 341 The response body, as a string.
360 """ 342 """
361 # TODO: Don't require authentication. Let the server say 343 # TODO: Don't require authentication. Let the server say
362 # whether it is necessary. 344 # whether it is necessary.
363 if not self.authenticated: 345 if not self.authenticated:
364 self._Authenticate() 346 self._Authenticate()
365 347
366 old_timeout = socket.getdefaulttimeout() 348 old_timeout = socket.getdefaulttimeout()
367 socket.setdefaulttimeout(timeout) 349 socket.setdefaulttimeout(timeout)
368 try: 350 try:
369 tries = 0 351 tries = 0
370 while True: 352 while True:
371 tries += 1 353 tries += 1
372 args = dict(kwargs) 354 args = dict(kwargs)
373 url = "%s%s" % (self.host, request_path) 355 url = "%s%s" % (self.host, request_path)
374 if args: 356 if args:
375 url += "?" + urllib.urlencode(args) 357 url += "?" + urllib.urlencode(args)
376 req = self._CreateRequest(url=url, data=payload) 358 req = self._CreateRequest(url=url, data=payload)
377 req.add_header("Content-Type", content_type) 359 req.add_header("Content-Type", content_type)
360 if extra_headers:
361 for header, value in extra_headers.items():
362 req.add_header(header, value)
378 try: 363 try:
379 f = self.opener.open(req) 364 f = self.opener.open(req)
380 response = f.read() 365 response = f.read()
381 f.close() 366 f.close()
382 return response 367 return response
383 except urllib2.HTTPError, e: 368 except urllib2.HTTPError, e:
384 if tries > 3: 369 if tries > 3:
385 raise 370 raise
386 elif e.code == 401 or e.code == 302: 371 elif e.code == 401 or e.code == 302:
387 self._Authenticate() 372 self._Authenticate()
(...skipping 104 matching lines...) Expand 10 before | Expand all | Expand 10 after
492 help="Make the issue restricted to reviewers and those CCed") 477 help="Make the issue restricted to reviewers and those CCed")
493 # Upload options 478 # Upload options
494 group = parser.add_option_group("Patch options") 479 group = parser.add_option_group("Patch options")
495 group.add_option("-m", "--message", action="store", dest="message", 480 group.add_option("-m", "--message", action="store", dest="message",
496 metavar="MESSAGE", default=None, 481 metavar="MESSAGE", default=None,
497 help="A message to identify the patch. " 482 help="A message to identify the patch. "
498 "Will prompt if omitted.") 483 "Will prompt if omitted.")
499 group.add_option("-i", "--issue", type="int", action="store", 484 group.add_option("-i", "--issue", type="int", action="store",
500 metavar="ISSUE", default=None, 485 metavar="ISSUE", default=None,
501 help="Issue number to which to add. Defaults to new issue.") 486 help="Issue number to which to add. Defaults to new issue.")
487 group.add_option("--base_url", action="store", dest="base_url", default=None,
488 help="Base repository URL (listed as \"Base URL\" when "
489 "viewing issue). If omitted, will be guessed automatically "
490 "for SVN repos and left blank for others.")
502 group.add_option("--download_base", action="store_true", 491 group.add_option("--download_base", action="store_true",
503 dest="download_base", default=False, 492 dest="download_base", default=False,
504 help="Base files will be downloaded by the server " 493 help="Base files will be downloaded by the server "
505 "(side-by-side diffs may not work on files with CRs).") 494 "(side-by-side diffs may not work on files with CRs).")
506 group.add_option("--rev", action="store", dest="revision", 495 group.add_option("--rev", action="store", dest="revision",
507 metavar="REV", default=None, 496 metavar="REV", default=None,
508 help="Branch/tree/revision to diff against (used by DVCS).") 497 help="Base revision/branch/tree to diff against. Use "
498 "rev1:rev2 range to review already committed changeset.")
509 group.add_option("--send_mail", action="store_true", 499 group.add_option("--send_mail", action="store_true",
510 dest="send_mail", default=False, 500 dest="send_mail", default=False,
511 help="Send notification email to reviewers.") 501 help="Send notification email to reviewers.")
512 group.add_option("--vcs", action="store", dest="vcs", 502 group.add_option("--vcs", action="store", dest="vcs",
513 metavar="VCS", default=None, 503 metavar="VCS", default=None,
514 help=("Version control system (optional, usually upload.py " 504 help=("Version control system (optional, usually upload.py "
515 "already guesses the right VCS).")) 505 "already guesses the right VCS)."))
506 group.add_option("--emulate_svn_auto_props", action="store_true",
507 dest="emulate_svn_auto_props", default=False,
508 help=("Emulate Subversion's auto properties feature."))
516 509
517 510
518 def GetRpcServer(options): 511 def GetRpcServer(server, email=None, host_override=None, save_cookies=True):
519 """Returns an instance of an AbstractRpcServer. 512 """Returns an instance of an AbstractRpcServer.
520 513
514 Args:
515 server: String containing the review server URL.
516 email: String containing user's email address.
517 host_override: If not None, string containing an alternate hostname to use
518 in the host header.
519 save_cookies: Whether authentication cookies should be saved to disk.
520
521 Returns: 521 Returns:
522 A new AbstractRpcServer, on which RPC calls can be made. 522 A new AbstractRpcServer, on which RPC calls can be made.
523 """ 523 """
524 524
525 rpc_server_class = HttpRpcServer 525 rpc_server_class = HttpRpcServer
526 526
527 def GetUserCredentials():
528 """Prompts the user for a username and password."""
529 email = options.email
530 if email is None:
531 email = GetEmail("Email (login for uploading to %s)" % options.server)
532 password = getpass.getpass("Password for %s: " % email)
533 return (email, password)
534
535 # If this is the dev_appserver, use fake authentication. 527 # If this is the dev_appserver, use fake authentication.
536 host = (options.host or options.server).lower() 528 host = (host_override or server).lower()
537 if host == "localhost" or host.startswith("localhost:"): 529 if host == "localhost" or host.startswith("localhost:"):
538 email = options.email
539 if email is None: 530 if email is None:
540 email = "test@example.com" 531 email = "test@example.com"
541 logging.info("Using debug user %s. Override with --email" % email) 532 logging.info("Using debug user %s. Override with --email" % email)
542 server = rpc_server_class( 533 server = rpc_server_class(
543 options.server, 534 server,
544 lambda: (email, "password"), 535 lambda: (email, "password"),
545 host_override=options.host, 536 host_override=host_override,
546 extra_headers={"Cookie": 537 extra_headers={"Cookie":
547 'dev_appserver_login="%s:False"' % email}, 538 'dev_appserver_login="%s:False"' % email},
548 save_cookies=options.save_cookies) 539 save_cookies=save_cookies)
549 # Don't try to talk to ClientLogin. 540 # Don't try to talk to ClientLogin.
550 server.authenticated = True 541 server.authenticated = True
551 return server 542 return server
552 543
553 return rpc_server_class(options.server, GetUserCredentials, 544 def GetUserCredentials():
554 host_override=options.host, 545 """Prompts the user for a username and password."""
555 save_cookies=options.save_cookies) 546 # Create a local alias to the email variable to avoid Python's crazy
547 # scoping rules.
548 local_email = email
549 if local_email is None:
550 local_email = GetEmail("Email (login for uploading to %s)" % server)
551 password = getpass.getpass("Password for %s: " % local_email)
552 return (local_email, password)
553
554 return rpc_server_class(server,
555 GetUserCredentials,
556 host_override=host_override,
557 save_cookies=save_cookies)
556 558
557 559
558 def EncodeMultipartFormData(fields, files): 560 def EncodeMultipartFormData(fields, files):
559 """Encode form fields for multipart/form-data. 561 """Encode form fields for multipart/form-data.
560 562
561 Args: 563 Args:
562 fields: A sequence of (name, value) elements for regular form fields. 564 fields: A sequence of (name, value) elements for regular form fields.
563 files: A sequence of (name, filename, value) elements for data to be 565 files: A sequence of (name, filename, value) elements for data to be
564 uploaded as files. 566 uploaded as files.
565 Returns: 567 Returns:
(...skipping 85 matching lines...) Expand 10 before | Expand all | Expand 10 after
651 """Abstract base class providing an interface to the VCS.""" 653 """Abstract base class providing an interface to the VCS."""
652 654
653 def __init__(self, options): 655 def __init__(self, options):
654 """Constructor. 656 """Constructor.
655 657
656 Args: 658 Args:
657 options: Command line options. 659 options: Command line options.
658 """ 660 """
659 self.options = options 661 self.options = options
660 662
663 def PostProcessDiff(self, diff):
664 """Return the diff with any special post processing this VCS needs, e.g.
665 to include an svn-style "Index:"."""
666 return diff
667
661 def GenerateDiff(self, args): 668 def GenerateDiff(self, args):
662 """Return the current diff as a string. 669 """Return the current diff as a string.
663 670
664 Args: 671 Args:
665 args: Extra arguments to pass to the diff command. 672 args: Extra arguments to pass to the diff command.
666 """ 673 """
667 raise NotImplementedError( 674 raise NotImplementedError(
668 "abstract method -- subclass %s must override" % self.__class__) 675 "abstract method -- subclass %s must override" % self.__class__)
669 676
670 def GetUnknownFiles(self): 677 def GetUnknownFiles(self):
(...skipping 128 matching lines...) Expand 10 before | Expand all | Expand 10 after
799 match = re.match(r"(\d+)(:(\d+))?", self.options.revision) 806 match = re.match(r"(\d+)(:(\d+))?", self.options.revision)
800 if not match: 807 if not match:
801 ErrorExit("Invalid Subversion revision %s." % self.options.revision) 808 ErrorExit("Invalid Subversion revision %s." % self.options.revision)
802 self.rev_start = match.group(1) 809 self.rev_start = match.group(1)
803 self.rev_end = match.group(3) 810 self.rev_end = match.group(3)
804 else: 811 else:
805 self.rev_start = self.rev_end = None 812 self.rev_start = self.rev_end = None
806 # Cache output from "svn list -r REVNO dirname". 813 # Cache output from "svn list -r REVNO dirname".
807 # Keys: dirname, Values: 2-tuple (ouput for start rev and end rev). 814 # Keys: dirname, Values: 2-tuple (ouput for start rev and end rev).
808 self.svnls_cache = {} 815 self.svnls_cache = {}
809 # SVN base URL is required to fetch files deleted in an older revision. 816 # Base URL is required to fetch files deleted in an older revision.
810 # Result is cached to not guess it over and over again in GetBaseFile(). 817 # Result is cached to not guess it over and over again in GetBaseFile().
811 required = self.options.download_base or self.options.revision is not None 818 required = self.options.download_base or self.options.revision is not None
812 self.svn_base = self._GuessBase(required) 819 self.svn_base = self._GuessBase(required)
813 820
814 def GuessBase(self, required): 821 def GuessBase(self, required):
815 """Wrapper for _GuessBase.""" 822 """Wrapper for _GuessBase."""
816 return self.svn_base 823 return self.svn_base
817 824
818 def _GuessBase(self, required): 825 def _GuessBase(self, required):
819 """Returns the SVN base URL. 826 """Returns the SVN base URL.
(...skipping 238 matching lines...) Expand 10 before | Expand all | Expand 10 after
1058 """Implementation of the VersionControlSystem interface for Git.""" 1065 """Implementation of the VersionControlSystem interface for Git."""
1059 1066
1060 def __init__(self, options): 1067 def __init__(self, options):
1061 super(GitVCS, self).__init__(options) 1068 super(GitVCS, self).__init__(options)
1062 # Map of filename -> (hash before, hash after) of base file. 1069 # Map of filename -> (hash before, hash after) of base file.
1063 # Hashes for "no such file" are represented as None. 1070 # Hashes for "no such file" are represented as None.
1064 self.hashes = {} 1071 self.hashes = {}
1065 # Map of new filename -> old filename for renames. 1072 # Map of new filename -> old filename for renames.
1066 self.renames = {} 1073 self.renames = {}
1067 1074
1068 def GenerateDiff(self, extra_args): 1075 def PostProcessDiff(self, gitdiff):
1069 # This is more complicated than svn's GenerateDiff because we must convert 1076 """Converts the diff output to include an svn-style "Index:" line as well
1070 # the diff output to include an svn-style "Index:" line as well as record 1077 as record the hashes of the files, so we can upload them along with our
1071 # the hashes of the files, so we can upload them along with our diff. 1078 diff."""
1072
1073 # Special used by git to indicate "no such content". 1079 # Special used by git to indicate "no such content".
1074 NULL_HASH = "0"*40 1080 NULL_HASH = "0"*40
1075 1081
1076 extra_args = extra_args[:] 1082 def IsFileNew(filename):
1077 if self.options.revision: 1083 return filename in self.hashes and self.hashes[filename][0] is None
1078 extra_args = [self.options.revision] + extra_args
1079 1084
1080 # --no-ext-diff is broken in some versions of Git, so try to work around 1085 def AddSubversionPropertyChange(filename):
1081 # this by overriding the environment (but there is still a problem if the 1086 """Add svn's property change information into the patch if given file is
1082 # git config key "diff.external" is used). 1087 new file.
1083 env = os.environ.copy() 1088
1084 if 'GIT_EXTERNAL_DIFF' in env: del env['GIT_EXTERNAL_DIFF'] 1089 We use Subversion's auto-props setting to retrieve its property.
1085 gitdiff = RunShell(["git", "diff", "--no-ext-diff", "--full-index", "-M"] 1090 See http://svnbook.red-bean.com/en/1.1/ch07.html#svn-ch-7-sect-1.3.2 for
1086 + extra_args, env=env) 1091 Subversion's [auto-props] setting.
1092 """
1093 if self.options.emulate_svn_auto_props and IsFileNew(filename):
1094 svnprops = GetSubversionPropertyChanges(filename)
1095 if svnprops:
1096 svndiff.append("\n" + svnprops + "\n")
1097
1087 svndiff = [] 1098 svndiff = []
1088 filecount = 0 1099 filecount = 0
1089 filename = None 1100 filename = None
1090 for line in gitdiff.splitlines(): 1101 for line in gitdiff.splitlines():
1091 match = re.match(r"diff --git a/(.*) b/(.*)$", line) 1102 match = re.match(r"diff --git a/(.*) b/(.*)$", line)
1092 if match: 1103 if match:
1104 # Add auto property here for previously seen file.
1105 if filename is not None:
1106 AddSubversionPropertyChange(filename)
1093 filecount += 1 1107 filecount += 1
1094 # Intentionally use the "after" filename so we can show renames. 1108 # Intentionally use the "after" filename so we can show renames.
1095 filename = match.group(2) 1109 filename = match.group(2)
1096 svndiff.append("Index: %s\n" % filename) 1110 svndiff.append("Index: %s\n" % filename)
1097 if match.group(1) != match.group(2): 1111 if match.group(1) != match.group(2):
1098 self.renames[match.group(2)] = match.group(1) 1112 self.renames[match.group(2)] = match.group(1)
1099 else: 1113 else:
1100 # The "index" line in a git diff looks like this (long hashes elided): 1114 # The "index" line in a git diff looks like this (long hashes elided):
1101 # index 82c0d44..b2cee3f 100755 1115 # index 82c0d44..b2cee3f 100755
1102 # We want to save the left hash, as that identifies the base file. 1116 # We want to save the left hash, as that identifies the base file.
1103 match = re.match(r"index (\w+)\.\.(\w+)", line) 1117 match = re.match(r"index (\w+)\.\.(\w+)", line)
1104 if match: 1118 if match:
1105 before, after = (match.group(1), match.group(2)) 1119 before, after = (match.group(1), match.group(2))
1106 if before == NULL_HASH: 1120 if before == NULL_HASH:
1107 before = None 1121 before = None
1108 if after == NULL_HASH: 1122 if after == NULL_HASH:
1109 after = None 1123 after = None
1110 self.hashes[filename] = (before, after) 1124 self.hashes[filename] = (before, after)
1111 svndiff.append(line + "\n") 1125 svndiff.append(line + "\n")
1112 if not filecount: 1126 if not filecount:
1113 ErrorExit("No valid patches found in output from git diff") 1127 ErrorExit("No valid patches found in output from git diff")
1128 # Add auto property for the last seen file.
1129 assert filename is not None
1130 AddSubversionPropertyChange(filename)
1114 return "".join(svndiff) 1131 return "".join(svndiff)
1115 1132
1133 def GenerateDiff(self, extra_args):
1134 extra_args = extra_args[:]
1135 if self.options.revision:
1136 extra_args = [self.options.revision] + extra_args
1137
1138 # --no-ext-diff is broken in some versions of Git, so try to work around
1139 # this by overriding the environment (but there is still a problem if the
1140 # git config key "diff.external" is used).
1141 env = os.environ.copy()
1142 if 'GIT_EXTERNAL_DIFF' in env: del env['GIT_EXTERNAL_DIFF']
1143 return RunShell(["git", "diff", "--no-ext-diff", "--full-index", "-M"]
1144 + extra_args, env=env)
1145
1116 def GetUnknownFiles(self): 1146 def GetUnknownFiles(self):
1117 status = RunShell(["git", "ls-files", "--exclude-standard", "--others"], 1147 status = RunShell(["git", "ls-files", "--exclude-standard", "--others"],
1118 silent_ok=True) 1148 silent_ok=True)
1119 return status.splitlines() 1149 return status.splitlines()
1120 1150
1121 def GetFileContent(self, file_hash, is_binary): 1151 def GetFileContent(self, file_hash, is_binary):
1122 """Returns the content of a file identified by its git hash.""" 1152 """Returns the content of a file identified by its git hash."""
1123 data, retcode = RunShellWithReturnCode(["git", "show", file_hash], 1153 data, retcode = RunShellWithReturnCode(["git", "show", file_hash],
1124 universal_newlines=not is_binary) 1154 universal_newlines=not is_binary)
1125 if retcode: 1155 if retcode:
1126 ErrorExit("Got error status from 'git show %s'" % file_hash) 1156 ErrorExit("Got error status from 'git show %s'" % file_hash)
1127 return data 1157 return data
1128 1158
1129 def GetBaseFile(self, filename): 1159 def GetBaseFile(self, filename):
1130 hash_before, hash_after = self.hashes.get(filename, (None,None)) 1160 hash_before, hash_after = self.hashes.get(filename, (None,None))
1131 base_content = None 1161 base_content = None
1132 new_content = None 1162 new_content = None
1133 is_binary = self.IsBinary(filename) 1163 is_binary = self.IsBinary(filename)
1134 status = None 1164 status = None
1135 1165
1136 if filename in self.renames: 1166 if filename in self.renames:
1137 status = "A +" # Match svn attribute name for renames. 1167 status = "A +" # Match svn attribute name for renames.
1138 if filename not in self.hashes: 1168 if filename not in self.hashes:
1139 # If a rename doesn't change the content, we never get a hash. 1169 # If a rename doesn't change the content, we never get a hash.
1140 base_content = RunShell(["git", "show", filename]) 1170 base_content = RunShell(["git", "show", "HEAD:" + filename])
1141 elif not hash_before: 1171 elif not hash_before:
1142 status = "A" 1172 status = "A"
1143 base_content = "" 1173 base_content = ""
1144 elif not hash_after: 1174 elif not hash_after:
1145 status = "D" 1175 status = "D"
1146 else: 1176 else:
1147 status = "M" 1177 status = "M"
1148 1178
1149 is_image = self.IsImage(filename) 1179 is_image = self.IsImage(filename)
1150 1180
(...skipping 271 matching lines...) Expand 10 before | Expand all | Expand 10 after
1422 if "@" not in reviewer: 1452 if "@" not in reviewer:
1423 return # Assume nickname 1453 return # Assume nickname
1424 parts = reviewer.split("@") 1454 parts = reviewer.split("@")
1425 if len(parts) > 2: 1455 if len(parts) > 2:
1426 ErrorExit("Invalid email address: %r" % reviewer) 1456 ErrorExit("Invalid email address: %r" % reviewer)
1427 assert len(parts) == 2 1457 assert len(parts) == 2
1428 if "." not in parts[1]: 1458 if "." not in parts[1]:
1429 ErrorExit("Invalid email address: %r" % reviewer) 1459 ErrorExit("Invalid email address: %r" % reviewer)
1430 1460
1431 1461
1462 def LoadSubversionAutoProperties():
1463 """Returns the content of [auto-props] section of Subversion's config file as
1464 a dictionary.
1465
1466 Returns:
1467 A dictionary whose key-value pair corresponds the [auto-props] section's
1468 key-value pair.
1469 In following cases, returns empty dictionary:
1470 - config file doesn't exist, or
1471 - 'enable-auto-props' is not set to 'true-like-value' in [miscellany].
1472 """
1473 # Todo(hayato): Windows users might use different path for configuration file.
1474 subversion_config = os.path.expanduser("~/.subversion/config")
1475 if not os.path.exists(subversion_config):
1476 return {}
1477 config = ConfigParser.ConfigParser()
1478 config.read(subversion_config)
1479 if (config.has_section("miscellany") and
1480 config.has_option("miscellany", "enable-auto-props") and
1481 config.getboolean("miscellany", "enable-auto-props") and
1482 config.has_section("auto-props")):
1483 props = {}
1484 for file_pattern in config.options("auto-props"):
1485 props[file_pattern] = ParseSubversionPropertyValues(
1486 config.get("auto-props", file_pattern))
1487 return props
1488 else:
1489 return {}
1490
1491 def ParseSubversionPropertyValues(props):
1492 """Parse the given property value which comes from [auto-props] section and
1493 returns a list whose element is a (svn_prop_key, svn_prop_value) pair.
1494
1495 See the following doctest for example.
1496
1497 >>> ParseSubversionPropertyValues('svn:eol-style=LF')
1498 [('svn:eol-style', 'LF')]
1499 >>> ParseSubversionPropertyValues('svn:mime-type=image/jpeg')
1500 [('svn:mime-type', 'image/jpeg')]
1501 >>> ParseSubversionPropertyValues('svn:eol-style=LF;svn:executable')
1502 [('svn:eol-style', 'LF'), ('svn:executable', '*')]
1503 """
1504 key_value_pairs = []
1505 for prop in props.split(";"):
1506 key_value = prop.split("=")
1507 assert len(key_value) <= 2
1508 if len(key_value) == 1:
1509 # If value is not given, use '*' as a Subversion's convention.
1510 key_value_pairs.append((key_value[0], "*"))
1511 else:
1512 key_value_pairs.append((key_value[0], key_value[1]))
1513 return key_value_pairs
1514
1515
1516 def GetSubversionPropertyChanges(filename):
1517 """Return a Subversion's 'Property changes on ...' string, which is used in
1518 the patch file.
1519
1520 Args:
1521 filename: filename whose property might be set by [auto-props] config.
1522
1523 Returns:
1524 A string like 'Property changes on |filename| ...' if given |filename|
1525 matches any entries in [auto-props] section. None, otherwise.
1526 """
1527 global svn_auto_props_map
1528 if svn_auto_props_map is None:
1529 svn_auto_props_map = LoadSubversionAutoProperties()
1530
1531 all_props = []
1532 for file_pattern, props in svn_auto_props_map.items():
1533 if fnmatch.fnmatch(filename, file_pattern):
1534 all_props.extend(props)
1535 if all_props:
1536 return FormatSubversionPropertyChanges(filename, all_props)
1537 return None
1538
1539
1540 def FormatSubversionPropertyChanges(filename, props):
1541 """Returns Subversion's 'Property changes on ...' strings using given filename
1542 and properties.
1543
1544 Args:
1545 filename: filename
1546 props: A list whose element is a (svn_prop_key, svn_prop_value) pair.
1547
1548 Returns:
1549 A string which can be used in the patch file for Subversion.
1550
1551 See the following doctest for example.
1552
1553 >>> print FormatSubversionPropertyChanges('foo.cc', [('svn:eol-style', 'LF')])
1554 Property changes on: foo.cc
1555 ___________________________________________________________________
1556 Added: svn:eol-style
1557 + LF
1558 <BLANKLINE>
1559 """
1560 prop_changes_lines = [
1561 "Property changes on: %s" % filename,
1562 "___________________________________________________________________"]
1563 for key, value in props:
1564 prop_changes_lines.append("Added: " + key)
1565 prop_changes_lines.append(" + " + value)
1566 return "\n".join(prop_changes_lines) + "\n"
1567
1568
1432 def RealMain(argv, data=None): 1569 def RealMain(argv, data=None):
1433 """The real main function. 1570 """The real main function.
1434 1571
1435 Args: 1572 Args:
1436 argv: Command line arguments. 1573 argv: Command line arguments.
1437 data: Diff contents. If None (default) the diff is generated by 1574 data: Diff contents. If None (default) the diff is generated by
1438 the VersionControlSystem implementation returned by GuessVCS(). 1575 the VersionControlSystem implementation returned by GuessVCS().
1439 1576
1440 Returns: 1577 Returns:
1441 A 2-tuple (issue id, patchset id). 1578 A 2-tuple (issue id, patchset id).
1442 The patchset id is None if the base files are not uploaded by this 1579 The patchset id is None if the base files are not uploaded by this
1443 script (applies only to SVN checkouts). 1580 script (applies only to SVN checkouts).
1444 """ 1581 """
1445 logging.basicConfig(format=("%(asctime).19s %(levelname)s %(filename)s:" 1582 logging.basicConfig(format=("%(asctime).19s %(levelname)s %(filename)s:"
1446 "%(lineno)s %(message)s ")) 1583 "%(lineno)s %(message)s "))
1447 os.environ['LC_ALL'] = 'C' 1584 os.environ['LC_ALL'] = 'C'
1448 options, args = parser.parse_args(argv[1:]) 1585 options, args = parser.parse_args(argv[1:])
1449 global verbosity 1586 global verbosity
1450 verbosity = options.verbose 1587 verbosity = options.verbose
1451 if verbosity >= 3: 1588 if verbosity >= 3:
1452 logging.getLogger().setLevel(logging.DEBUG) 1589 logging.getLogger().setLevel(logging.DEBUG)
1453 elif verbosity >= 2: 1590 elif verbosity >= 2:
1454 logging.getLogger().setLevel(logging.INFO) 1591 logging.getLogger().setLevel(logging.INFO)
1592
1455 vcs = GuessVCS(options) 1593 vcs = GuessVCS(options)
1594
1595 base = options.base_url
1456 if isinstance(vcs, SubversionVCS): 1596 if isinstance(vcs, SubversionVCS):
1457 # base field is only allowed for Subversion. 1597 # Guessing the base field is only supported for Subversion.
1458 # Note: Fetching base files may become deprecated in future releases. 1598 # Note: Fetching base files may become deprecated in future releases.
1459 base = vcs.GuessBase(options.download_base) 1599 guessed_base = vcs.GuessBase(options.download_base)
1460 else: 1600 if base:
1461 base = None 1601 if guessed_base and base != guessed_base:
1602 print "Using base URL \"%s\" from --base_url instead of \"%s\"" % \
1603 (base, guessed_base)
1604 else:
1605 base = guessed_base
1606
1462 if not base and options.download_base: 1607 if not base and options.download_base:
1463 options.download_base = True 1608 options.download_base = True
1464 logging.info("Enabled upload of base file") 1609 logging.info("Enabled upload of base file")
1465 if not options.assume_yes: 1610 if not options.assume_yes:
1466 vcs.CheckForUnknownFiles() 1611 vcs.CheckForUnknownFiles()
1467 if data is None: 1612 if data is None:
1468 data = vcs.GenerateDiff(args) 1613 data = vcs.GenerateDiff(args)
1614 data = vcs.PostProcessDiff(data)
1469 files = vcs.GetBaseFiles(data) 1615 files = vcs.GetBaseFiles(data)
1470 if verbosity >= 1: 1616 if verbosity >= 1:
1471 print "Upload server:", options.server, "(change with -s/--server)" 1617 print "Upload server:", options.server, "(change with -s/--server)"
1472 if options.issue: 1618 if options.issue:
1473 prompt = "Message describing this patch set: " 1619 prompt = "Message describing this patch set: "
1474 else: 1620 else:
1475 prompt = "New issue subject: " 1621 prompt = "New issue subject: "
1476 message = options.message or raw_input(prompt).strip() 1622 message = options.message or raw_input(prompt).strip()
1477 if not message: 1623 if not message:
1478 ErrorExit("A non-empty message is required") 1624 ErrorExit("A non-empty message is required")
1479 rpc_server = GetRpcServer(options) 1625 rpc_server = GetRpcServer(options.server,
1626 options.email,
1627 options.host,
1628 options.save_cookies)
1480 form_fields = [("subject", message)] 1629 form_fields = [("subject", message)]
1481 if base: 1630 if base:
1482 form_fields.append(("base", base)) 1631 form_fields.append(("base", base))
1483 if options.issue: 1632 if options.issue:
1484 form_fields.append(("issue", str(options.issue))) 1633 form_fields.append(("issue", str(options.issue)))
1485 if options.email: 1634 if options.email:
1486 form_fields.append(("user", options.email)) 1635 form_fields.append(("user", options.email))
1487 if options.reviewers: 1636 if options.reviewers:
1488 for reviewer in options.reviewers.split(','): 1637 for reviewer in options.reviewers.split(','):
1489 CheckReviewer(reviewer) 1638 CheckReviewer(reviewer)
(...skipping 73 matching lines...) Expand 10 before | Expand all | Expand 10 after
1563 try: 1712 try:
1564 RealMain(sys.argv) 1713 RealMain(sys.argv)
1565 except KeyboardInterrupt: 1714 except KeyboardInterrupt:
1566 print 1715 print
1567 StatusUpdate("Interrupted.") 1716 StatusUpdate("Interrupted.")
1568 sys.exit(1) 1717 sys.exit(1)
1569 1718
1570 1719
1571 if __name__ == "__main__": 1720 if __name__ == "__main__":
1572 main() 1721 main()
OLDNEW
« no previous file with comments | « no previous file | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698