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 |