| OLD | NEW |
| 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 Loading... |
| 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 Loading... |
| 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 Loading... |
| 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 Loading... |
| 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 Loading... |
| 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 Loading... |
| 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 Loading... |
| 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 Loading... |
| 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 Loading... |
| 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 Loading... |
| 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() |
| OLD | NEW |