| 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 27 matching lines...) Expand all Loading... |
| 38 import optparse | 38 import optparse |
| 39 import os | 39 import os |
| 40 import re | 40 import re |
| 41 import socket | 41 import socket |
| 42 import subprocess | 42 import subprocess |
| 43 import sys | 43 import sys |
| 44 import urllib | 44 import urllib |
| 45 import urllib2 | 45 import urllib2 |
| 46 import urlparse | 46 import urlparse |
| 47 | 47 |
| 48 # Work-around for md5 module deprecation warning in python 2.5+: | 48 # The md5 module was deprecated in Python 2.5. |
| 49 try: | 49 try: |
| 50 # Try to load hashlib (python 2.5+) | |
| 51 from hashlib import md5 | 50 from hashlib import md5 |
| 52 except ImportError: | 51 except ImportError: |
| 53 # If hashlib cannot be imported, load md5.new instead. | 52 from md5 import md5 |
| 54 from md5 import new as md5 | |
| 55 | 53 |
| 56 try: | 54 try: |
| 57 import readline | 55 import readline |
| 58 except ImportError: | 56 except ImportError: |
| 59 pass | 57 pass |
| 60 | 58 |
| 61 # The logging verbosity: | 59 # The logging verbosity: |
| 62 # 0: Errors only. | 60 # 0: Errors only. |
| 63 # 1: Status messages. | 61 # 1: Status messages. |
| 64 # 2: Info logs. | 62 # 2: Info logs. |
| 65 # 3: Debug logs. | 63 # 3: Debug logs. |
| 66 verbosity = 1 | 64 verbosity = 1 |
| 67 | 65 |
| 68 # Max size of patch or base file. | 66 # Max size of patch or base file. |
| 69 MAX_UPLOAD_SIZE = 900 * 1024 | 67 MAX_UPLOAD_SIZE = 900 * 1024 |
| 70 | 68 |
| 69 # Constants for version control names. Used by GuessVCSName. |
| 70 VCS_GIT = "Git" |
| 71 VCS_MERCURIAL = "Mercurial" |
| 72 VCS_SUBVERSION = "Subversion" |
| 73 VCS_UNKNOWN = "Unknown" |
| 71 | 74 |
| 72 def GetEmail(): | 75 # 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. |
| 77 TEXT_MIMETYPES = ['application/javascript', 'application/x-javascript', |
| 78 'application/x-freemind'] |
| 79 |
| 80 VCS_ABBREVIATIONS = { |
| 81 VCS_MERCURIAL.lower(): VCS_MERCURIAL, |
| 82 "hg": VCS_MERCURIAL, |
| 83 VCS_SUBVERSION.lower(): VCS_SUBVERSION, |
| 84 "svn": VCS_SUBVERSION, |
| 85 VCS_GIT.lower(): VCS_GIT, |
| 86 } |
| 87 |
| 88 |
| 89 def GetEmail(prompt): |
| 73 """Prompts the user for their email address and returns it. | 90 """Prompts the user for their email address and returns it. |
| 74 | 91 |
| 75 The last used email address is saved to a file and offered up as a suggestion | 92 The last used email address is saved to a file and offered up as a suggestion |
| 76 to the user. If the user presses enter without typing in anything the last | 93 to the user. If the user presses enter without typing in anything the last |
| 77 used email address is used. If the user enters a new address, it is saved | 94 used email address is used. If the user enters a new address, it is saved |
| 78 for next time we prompt. | 95 for next time we prompt. |
| 79 | 96 |
| 80 """ | 97 """ |
| 81 last_email_file_name = os.path.expanduser( | 98 last_email_file_name = os.path.expanduser("~/.last_codereview_email_address") |
| 82 os.path.join("~", ".last_codereview_email_address")) | |
| 83 last_email = "" | 99 last_email = "" |
| 84 prompt = "Email: " | |
| 85 if os.path.exists(last_email_file_name): | 100 if os.path.exists(last_email_file_name): |
| 86 try: | 101 try: |
| 87 last_email_file = open(last_email_file_name, "r") | 102 last_email_file = open(last_email_file_name, "r") |
| 88 last_email = last_email_file.readline().strip("\n") | 103 last_email = last_email_file.readline().strip("\n") |
| 89 last_email_file.close() | 104 last_email_file.close() |
| 90 prompt = "Email [%s]: " % last_email | 105 prompt += " [%s]" % last_email |
| 91 except IOError, e: | 106 except IOError, e: |
| 92 pass | 107 pass |
| 93 email = raw_input(prompt).strip() | 108 email = raw_input(prompt + ": ").strip() |
| 94 if email: | 109 if email: |
| 95 try: | 110 try: |
| 96 last_email_file = open(last_email_file_name, "w") | 111 last_email_file = open(last_email_file_name, "w") |
| 97 last_email_file.write(email) | 112 last_email_file.write(email) |
| 98 last_email_file.close() | 113 last_email_file.close() |
| 99 except IOError, e: | 114 except IOError, e: |
| 100 pass | 115 pass |
| 101 else: | 116 else: |
| 102 email = last_email | 117 email = last_email |
| 103 return email | 118 return email |
| (...skipping 82 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 186 password: The user's password | 201 password: The user's password |
| 187 | 202 |
| 188 Raises: | 203 Raises: |
| 189 ClientLoginError: If there was an error authenticating with ClientLogin. | 204 ClientLoginError: If there was an error authenticating with ClientLogin. |
| 190 HTTPError: If there was some other form of HTTP error. | 205 HTTPError: If there was some other form of HTTP error. |
| 191 | 206 |
| 192 Returns: | 207 Returns: |
| 193 The authentication token returned by ClientLogin. | 208 The authentication token returned by ClientLogin. |
| 194 """ | 209 """ |
| 195 account_type = "GOOGLE" | 210 account_type = "GOOGLE" |
| 196 if self.host.endswith(".google.com"): | |
| 197 # Needed for use inside Google. | |
| 198 account_type = "HOSTED" | |
| 199 req = self._CreateRequest( | 211 req = self._CreateRequest( |
| 200 url="https://www.google.com/accounts/ClientLogin", | 212 url="https://www.google.com/accounts/ClientLogin", |
| 201 data=urllib.urlencode({ | 213 data=urllib.urlencode({ |
| 202 "Email": email, | 214 "Email": email, |
| 203 "Passwd": password, | 215 "Passwd": password, |
| 204 "service": "ah", | 216 "service": "ah", |
| 205 "source": "rietveld-codereview-upload", | 217 "source": "rietveld-codereview-upload", |
| 206 "accountType": account_type, | 218 "accountType": account_type, |
| 207 }), | 219 }), |
| 208 ) | 220 ) |
| (...skipping 41 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 250 | 262 |
| 251 The authentication process works as follows: | 263 The authentication process works as follows: |
| 252 1) We get a username and password from the user | 264 1) We get a username and password from the user |
| 253 2) We use ClientLogin to obtain an AUTH token for the user | 265 2) We use ClientLogin to obtain an AUTH token for the user |
| 254 (see http://code.google.com/apis/accounts/AuthForInstalledApps.html). | 266 (see http://code.google.com/apis/accounts/AuthForInstalledApps.html). |
| 255 3) We pass the auth token to /_ah/login on the server to obtain an | 267 3) We pass the auth token to /_ah/login on the server to obtain an |
| 256 authentication cookie. If login was successful, it tries to redirect | 268 authentication cookie. If login was successful, it tries to redirect |
| 257 us to the URL we provided. | 269 us to the URL we provided. |
| 258 | 270 |
| 259 If we attempt to access the upload API without first obtaining an | 271 If we attempt to access the upload API without first obtaining an |
| 260 authentication cookie, it returns a 401 response and directs us to | 272 authentication cookie, it returns a 401 response (or a 302) and |
| 261 authenticate ourselves with ClientLogin. | 273 directs us to authenticate ourselves with ClientLogin. |
| 262 """ | 274 """ |
| 263 for i in range(3): | 275 for i in range(3): |
| 264 credentials = self.auth_function() | 276 credentials = self.auth_function() |
| 265 try: | 277 try: |
| 266 auth_token = self._GetAuthToken(credentials[0], credentials[1]) | 278 auth_token = self._GetAuthToken(credentials[0], credentials[1]) |
| 267 except ClientLoginError, e: | 279 except ClientLoginError, e: |
| 268 if e.reason == "BadAuthentication": | 280 if e.reason == "BadAuthentication": |
| 269 print >>sys.stderr, "Invalid username or password." | 281 print >>sys.stderr, "Invalid username or password." |
| 270 continue | 282 continue |
| 271 if e.reason == "CaptchaRequired": | 283 if e.reason == "CaptchaRequired": |
| (...skipping 60 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 332 req = self._CreateRequest(url=url, data=payload) | 344 req = self._CreateRequest(url=url, data=payload) |
| 333 req.add_header("Content-Type", content_type) | 345 req.add_header("Content-Type", content_type) |
| 334 try: | 346 try: |
| 335 f = self.opener.open(req) | 347 f = self.opener.open(req) |
| 336 response = f.read() | 348 response = f.read() |
| 337 f.close() | 349 f.close() |
| 338 return response | 350 return response |
| 339 except urllib2.HTTPError, e: | 351 except urllib2.HTTPError, e: |
| 340 if tries > 3: | 352 if tries > 3: |
| 341 raise | 353 raise |
| 342 elif e.code == 401: | 354 elif e.code == 401 or e.code == 302: |
| 343 self._Authenticate() | 355 self._Authenticate() |
| 344 ## elif e.code >= 500 and e.code < 600: | 356 ## elif e.code >= 500 and e.code < 600: |
| 345 ## # Server Error - try again. | 357 ## # Server Error - try again. |
| 346 ## continue | 358 ## continue |
| 347 else: | 359 else: |
| 348 raise | 360 raise |
| 349 finally: | 361 finally: |
| 350 socket.setdefaulttimeout(old_timeout) | 362 socket.setdefaulttimeout(old_timeout) |
| 351 | 363 |
| 352 | 364 |
| (...skipping 14 matching lines...) Expand all Loading... |
| 367 A urllib2.OpenerDirector object. | 379 A urllib2.OpenerDirector object. |
| 368 """ | 380 """ |
| 369 opener = urllib2.OpenerDirector() | 381 opener = urllib2.OpenerDirector() |
| 370 opener.add_handler(urllib2.ProxyHandler()) | 382 opener.add_handler(urllib2.ProxyHandler()) |
| 371 opener.add_handler(urllib2.UnknownHandler()) | 383 opener.add_handler(urllib2.UnknownHandler()) |
| 372 opener.add_handler(urllib2.HTTPHandler()) | 384 opener.add_handler(urllib2.HTTPHandler()) |
| 373 opener.add_handler(urllib2.HTTPDefaultErrorHandler()) | 385 opener.add_handler(urllib2.HTTPDefaultErrorHandler()) |
| 374 opener.add_handler(urllib2.HTTPSHandler()) | 386 opener.add_handler(urllib2.HTTPSHandler()) |
| 375 opener.add_handler(urllib2.HTTPErrorProcessor()) | 387 opener.add_handler(urllib2.HTTPErrorProcessor()) |
| 376 if self.save_cookies: | 388 if self.save_cookies: |
| 377 self.cookie_file = os.path.expanduser( | 389 self.cookie_file = os.path.expanduser("~/.codereview_upload_cookies") |
| 378 os.path.join("~", ".codereview_upload_cookies")) | |
| 379 self.cookie_jar = cookielib.MozillaCookieJar(self.cookie_file) | 390 self.cookie_jar = cookielib.MozillaCookieJar(self.cookie_file) |
| 380 if os.path.exists(self.cookie_file): | 391 if os.path.exists(self.cookie_file): |
| 381 try: | 392 try: |
| 382 self.cookie_jar.load() | 393 self.cookie_jar.load() |
| 383 self.authenticated = True | 394 self.authenticated = True |
| 384 StatusUpdate("Loaded authentication cookies from %s" % | 395 StatusUpdate("Loaded authentication cookies from %s" % |
| 385 self.cookie_file) | 396 self.cookie_file) |
| 386 except (cookielib.LoadError, IOError): | 397 except (cookielib.LoadError, IOError): |
| 387 # Failed to load cookies - just ignore them. | 398 # Failed to load cookies - just ignore them. |
| 388 pass | 399 pass |
| (...skipping 22 matching lines...) Expand all Loading... |
| 411 dest="verbose", default=1, | 422 dest="verbose", default=1, |
| 412 help="Print info level logs (default).") | 423 help="Print info level logs (default).") |
| 413 group.add_option("--noisy", action="store_const", const=3, | 424 group.add_option("--noisy", action="store_const", const=3, |
| 414 dest="verbose", help="Print all logs.") | 425 dest="verbose", help="Print all logs.") |
| 415 # Review server | 426 # Review server |
| 416 group = parser.add_option_group("Review server options") | 427 group = parser.add_option_group("Review server options") |
| 417 group.add_option("-s", "--server", action="store", dest="server", | 428 group.add_option("-s", "--server", action="store", dest="server", |
| 418 default="codereview.appspot.com", | 429 default="codereview.appspot.com", |
| 419 metavar="SERVER", | 430 metavar="SERVER", |
| 420 help=("The server to upload to. The format is host[:port]. " | 431 help=("The server to upload to. The format is host[:port]. " |
| 421 "Defaults to 'codereview.appspot.com'.")) | 432 "Defaults to '%default'.")) |
| 422 group.add_option("-e", "--email", action="store", dest="email", | 433 group.add_option("-e", "--email", action="store", dest="email", |
| 423 metavar="EMAIL", default=None, | 434 metavar="EMAIL", default=None, |
| 424 help="The username to use. Will prompt if omitted.") | 435 help="The username to use. Will prompt if omitted.") |
| 425 group.add_option("-H", "--host", action="store", dest="host", | 436 group.add_option("-H", "--host", action="store", dest="host", |
| 426 metavar="HOST", default=None, | 437 metavar="HOST", default=None, |
| 427 help="Overrides the Host header sent with all RPCs.") | 438 help="Overrides the Host header sent with all RPCs.") |
| 428 group.add_option("--no_cookies", action="store_false", | 439 group.add_option("--no_cookies", action="store_false", |
| 429 dest="save_cookies", default=True, | 440 dest="save_cookies", default=True, |
| 430 help="Do not save authentication cookies to local disk.") | 441 help="Do not save authentication cookies to local disk.") |
| 431 # Issue | 442 # Issue |
| 432 group = parser.add_option_group("Issue options") | 443 group = parser.add_option_group("Issue options") |
| 433 group.add_option("-d", "--description", action="store", dest="description", | 444 group.add_option("-d", "--description", action="store", dest="description", |
| 434 metavar="DESCRIPTION", default=None, | 445 metavar="DESCRIPTION", default=None, |
| 435 help="Optional description when creating an issue.") | 446 help="Optional description when creating an issue.") |
| 436 group.add_option("-f", "--description_file", action="store", | 447 group.add_option("-f", "--description_file", action="store", |
| 437 dest="description_file", metavar="DESCRIPTION_FILE", | 448 dest="description_file", metavar="DESCRIPTION_FILE", |
| 438 default=None, | 449 default=None, |
| 439 help="Optional path of a file that contains " | 450 help="Optional path of a file that contains " |
| 440 "the description when creating an issue.") | 451 "the description when creating an issue.") |
| 441 group.add_option("-r", "--reviewers", action="store", dest="reviewers", | 452 group.add_option("-r", "--reviewers", action="store", dest="reviewers", |
| 442 metavar="REVIEWERS", default=None, | 453 metavar="REVIEWERS", default=None, |
| 443 help="Add reviewers (comma separated email addresses).") | 454 help="Add reviewers (comma separated email addresses).") |
| 444 group.add_option("--cc", action="store", dest="cc", | 455 group.add_option("--cc", action="store", dest="cc", |
| 445 metavar="CC", default=None, | 456 metavar="CC", default=None, |
| 446 help="Add CC (comma separated email addresses).") | 457 help="Add CC (comma separated email addresses).") |
| 458 group.add_option("--private", action="store_true", dest="private", |
| 459 default=False, |
| 460 help="Make the issue restricted to reviewers and those CCed") |
| 447 # Upload options | 461 # Upload options |
| 448 group = parser.add_option_group("Patch options") | 462 group = parser.add_option_group("Patch options") |
| 449 group.add_option("-m", "--message", action="store", dest="message", | 463 group.add_option("-m", "--message", action="store", dest="message", |
| 450 metavar="MESSAGE", default=None, | 464 metavar="MESSAGE", default=None, |
| 451 help="A message to identify the patch. " | 465 help="A message to identify the patch. " |
| 452 "Will prompt if omitted.") | 466 "Will prompt if omitted.") |
| 453 group.add_option("-i", "--issue", type="int", action="store", | 467 group.add_option("-i", "--issue", type="int", action="store", |
| 454 metavar="ISSUE", default=None, | 468 metavar="ISSUE", default=None, |
| 455 help="Issue number to which to add. Defaults to new issue.") | 469 help="Issue number to which to add. Defaults to new issue.") |
| 456 group.add_option("--download_base", action="store_true", | 470 group.add_option("--download_base", action="store_true", |
| 457 dest="download_base", default=False, | 471 dest="download_base", default=False, |
| 458 help="Base files will be downloaded by the server " | 472 help="Base files will be downloaded by the server " |
| 459 "(side-by-side diffs may not work on files with CRs).") | 473 "(side-by-side diffs may not work on files with CRs).") |
| 460 group.add_option("--rev", action="store", dest="revision", | 474 group.add_option("--rev", action="store", dest="revision", |
| 461 metavar="REV", default=None, | 475 metavar="REV", default=None, |
| 462 help="Branch/tree/revision to diff against (used by DVCS).") | 476 help="Branch/tree/revision to diff against (used by DVCS).") |
| 463 group.add_option("--send_mail", action="store_true", | 477 group.add_option("--send_mail", action="store_true", |
| 464 dest="send_mail", default=False, | 478 dest="send_mail", default=False, |
| 465 help="Send notification email to reviewers.") | 479 help="Send notification email to reviewers.") |
| 480 group.add_option("--vcs", action="store", dest="vcs", |
| 481 metavar="VCS", default=None, |
| 482 help=("Version control system (optional, usually upload.py " |
| 483 "already guesses the right VCS).")) |
| 466 | 484 |
| 467 | 485 |
| 468 def GetRpcServer(options): | 486 def GetRpcServer(options): |
| 469 """Returns an instance of an AbstractRpcServer. | 487 """Returns an instance of an AbstractRpcServer. |
| 470 | 488 |
| 471 Returns: | 489 Returns: |
| 472 A new AbstractRpcServer, on which RPC calls can be made. | 490 A new AbstractRpcServer, on which RPC calls can be made. |
| 473 """ | 491 """ |
| 474 | 492 |
| 475 rpc_server_class = HttpRpcServer | 493 rpc_server_class = HttpRpcServer |
| 476 | 494 |
| 477 def GetUserCredentials(): | 495 def GetUserCredentials(): |
| 478 """Prompts the user for a username and password.""" | 496 """Prompts the user for a username and password.""" |
| 479 email = options.email | 497 email = options.email |
| 480 if email is None: | 498 if email is None: |
| 481 email = GetEmail() | 499 email = GetEmail("Email (login for uploading to %s)" % options.server) |
| 482 password = getpass.getpass("Password for %s: " % email) | 500 password = getpass.getpass("Password for %s: " % email) |
| 483 return (email, password) | 501 return (email, password) |
| 484 | 502 |
| 485 # If this is the dev_appserver, use fake authentication. | 503 # If this is the dev_appserver, use fake authentication. |
| 486 host = (options.host or options.server).lower() | 504 host = (options.host or options.server).lower() |
| 487 if host == "localhost" or host.startswith("localhost:"): | 505 if host == "localhost" or host.startswith("localhost:"): |
| 488 email = options.email | 506 email = options.email |
| 489 if email is None: | 507 if email is None: |
| 490 email = "test@example.com" | 508 email = "test@example.com" |
| 491 logging.info("Using debug user %s. Override with --email" % email) | 509 logging.info("Using debug user %s. Override with --email" % email) |
| (...skipping 50 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 542 | 560 |
| 543 def GetContentType(filename): | 561 def GetContentType(filename): |
| 544 """Helper to guess the content-type from the filename.""" | 562 """Helper to guess the content-type from the filename.""" |
| 545 return mimetypes.guess_type(filename)[0] or 'application/octet-stream' | 563 return mimetypes.guess_type(filename)[0] or 'application/octet-stream' |
| 546 | 564 |
| 547 | 565 |
| 548 # Use a shell for subcommands on Windows to get a PATH search. | 566 # Use a shell for subcommands on Windows to get a PATH search. |
| 549 use_shell = sys.platform.startswith("win") | 567 use_shell = sys.platform.startswith("win") |
| 550 | 568 |
| 551 def RunShellWithReturnCode(command, print_output=False, | 569 def RunShellWithReturnCode(command, print_output=False, |
| 552 universal_newlines=True): | 570 universal_newlines=True, |
| 571 env=os.environ): |
| 553 """Executes a command and returns the output from stdout and the return code. | 572 """Executes a command and returns the output from stdout and the return code. |
| 554 | 573 |
| 555 Args: | 574 Args: |
| 556 command: Command to execute. | 575 command: Command to execute. |
| 557 print_output: If True, the output is printed to stdout. | 576 print_output: If True, the output is printed to stdout. |
| 558 If False, both stdout and stderr are ignored. | 577 If False, both stdout and stderr are ignored. |
| 559 universal_newlines: Use universal_newlines flag (default: True). | 578 universal_newlines: Use universal_newlines flag (default: True). |
| 560 | 579 |
| 561 Returns: | 580 Returns: |
| 562 Tuple (output, return code) | 581 Tuple (output, return code) |
| 563 """ | 582 """ |
| 564 logging.info("Running %s", command) | 583 logging.info("Running %s", command) |
| 565 p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, | 584 p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, |
| 566 shell=use_shell, universal_newlines=universal_newlines) | 585 shell=use_shell, universal_newlines=universal_newlines, |
| 586 env=env) |
| 567 if print_output: | 587 if print_output: |
| 568 output_array = [] | 588 output_array = [] |
| 569 while True: | 589 while True: |
| 570 line = p.stdout.readline() | 590 line = p.stdout.readline() |
| 571 if not line: | 591 if not line: |
| 572 break | 592 break |
| 573 print line.strip("\n") | 593 print line.strip("\n") |
| 574 output_array.append(line) | 594 output_array.append(line) |
| 575 output = "".join(output_array) | 595 output = "".join(output_array) |
| 576 else: | 596 else: |
| 577 output = p.stdout.read() | 597 output = p.stdout.read() |
| 578 p.wait() | 598 p.wait() |
| 579 errout = p.stderr.read() | 599 errout = p.stderr.read() |
| 580 if print_output and errout: | 600 if print_output and errout: |
| 581 print >>sys.stderr, errout | 601 print >>sys.stderr, errout |
| 582 p.stdout.close() | 602 p.stdout.close() |
| 583 p.stderr.close() | 603 p.stderr.close() |
| 584 return output, p.returncode | 604 return output, p.returncode |
| 585 | 605 |
| 586 | 606 |
| 587 def RunShell(command, silent_ok=False, universal_newlines=True, | 607 def RunShell(command, silent_ok=False, universal_newlines=True, |
| 588 print_output=False): | 608 print_output=False, env=os.environ): |
| 589 data, retcode = RunShellWithReturnCode(command, print_output, | 609 data, retcode = RunShellWithReturnCode(command, print_output, |
| 590 universal_newlines) | 610 universal_newlines, env) |
| 591 if retcode: | 611 if retcode: |
| 592 ErrorExit("Got error status from %s:\n%s" % (command, data)) | 612 ErrorExit("Got error status from %s:\n%s" % (command, data)) |
| 593 if not silent_ok and not data: | 613 if not silent_ok and not data: |
| 594 ErrorExit("No output from %s" % command) | 614 ErrorExit("No output from %s" % command) |
| 595 return data | 615 return data |
| 596 | 616 |
| 597 | 617 |
| 598 class VersionControlSystem(object): | 618 class VersionControlSystem(object): |
| 599 """Abstract base class providing an interface to the VCS.""" | 619 """Abstract base class providing an interface to the VCS.""" |
| 600 | 620 |
| (...skipping 119 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 720 if new_content != None: | 740 if new_content != None: |
| 721 UploadFile(filename, file_id, new_content, is_binary, status, False) | 741 UploadFile(filename, file_id, new_content, is_binary, status, False) |
| 722 | 742 |
| 723 def IsImage(self, filename): | 743 def IsImage(self, filename): |
| 724 """Returns true if the filename has an image extension.""" | 744 """Returns true if the filename has an image extension.""" |
| 725 mimetype = mimetypes.guess_type(filename)[0] | 745 mimetype = mimetypes.guess_type(filename)[0] |
| 726 if not mimetype: | 746 if not mimetype: |
| 727 return False | 747 return False |
| 728 return mimetype.startswith("image/") | 748 return mimetype.startswith("image/") |
| 729 | 749 |
| 750 def IsBinary(self, filename): |
| 751 """Returns true if the guessed mimetyped isnt't in text group.""" |
| 752 mimetype = mimetypes.guess_type(filename)[0] |
| 753 if not mimetype: |
| 754 return False # e.g. README, "real" binaries usually have an extension |
| 755 # special case for text files which don't start with text/ |
| 756 if mimetype in TEXT_MIMETYPES: |
| 757 return False |
| 758 return not mimetype.startswith("text/") |
| 759 |
| 730 | 760 |
| 731 class SubversionVCS(VersionControlSystem): | 761 class SubversionVCS(VersionControlSystem): |
| 732 """Implementation of the VersionControlSystem interface for Subversion.""" | 762 """Implementation of the VersionControlSystem interface for Subversion.""" |
| 733 | 763 |
| 734 def __init__(self, options): | 764 def __init__(self, options): |
| 735 super(SubversionVCS, self).__init__(options) | 765 super(SubversionVCS, self).__init__(options) |
| 736 if self.options.revision: | 766 if self.options.revision: |
| 737 match = re.match(r"(\d+)(:(\d+))?", self.options.revision) | 767 match = re.match(r"(\d+)(:(\d+))?", self.options.revision) |
| 738 if not match: | 768 if not match: |
| 739 ErrorExit("Invalid Subversion revision %s." % self.options.revision) | 769 ErrorExit("Invalid Subversion revision %s." % self.options.revision) |
| (...skipping 173 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 913 # If a file is copied its status will be "A +", which signifies | 943 # If a file is copied its status will be "A +", which signifies |
| 914 # "addition-with-history". See "svn st" for more information. We need to | 944 # "addition-with-history". See "svn st" for more information. We need to |
| 915 # upload the original file or else diff parsing will fail if the file was | 945 # upload the original file or else diff parsing will fail if the file was |
| 916 # edited. | 946 # edited. |
| 917 if status[0] == "A" and status[3] != "+": | 947 if status[0] == "A" and status[3] != "+": |
| 918 # We'll need to upload the new content if we're adding a binary file | 948 # We'll need to upload the new content if we're adding a binary file |
| 919 # since diff's output won't contain it. | 949 # since diff's output won't contain it. |
| 920 mimetype = RunShell(["svn", "propget", "svn:mime-type", filename], | 950 mimetype = RunShell(["svn", "propget", "svn:mime-type", filename], |
| 921 silent_ok=True) | 951 silent_ok=True) |
| 922 base_content = "" | 952 base_content = "" |
| 923 is_binary = mimetype and not mimetype.startswith("text/") | 953 is_binary = bool(mimetype) and not mimetype.startswith("text/") |
| 924 if is_binary and self.IsImage(filename): | 954 if is_binary and self.IsImage(filename): |
| 925 new_content = self.ReadFile(filename) | 955 new_content = self.ReadFile(filename) |
| 926 elif (status[0] in ("M", "D", "R") or | 956 elif (status[0] in ("M", "D", "R") or |
| 927 (status[0] == "A" and status[3] == "+") or # Copied file. | 957 (status[0] == "A" and status[3] == "+") or # Copied file. |
| 928 (status[0] == " " and status[1] == "M")): # Property change. | 958 (status[0] == " " and status[1] == "M")): # Property change. |
| 929 args = [] | 959 args = [] |
| 930 if self.options.revision: | 960 if self.options.revision: |
| 931 url = "%s/%s@%s" % (self.svn_base, filename, self.rev_start) | 961 url = "%s/%s@%s" % (self.svn_base, filename, self.rev_start) |
| 932 else: | 962 else: |
| 933 # Don't change filename, it's needed later. | 963 # Don't change filename, it's needed later. |
| 934 url = filename | 964 url = filename |
| 935 args += ["-r", "BASE"] | 965 args += ["-r", "BASE"] |
| 936 cmd = ["svn"] + args + ["propget", "svn:mime-type", url] | 966 cmd = ["svn"] + args + ["propget", "svn:mime-type", url] |
| 937 mimetype, returncode = RunShellWithReturnCode(cmd) | 967 mimetype, returncode = RunShellWithReturnCode(cmd) |
| 938 if returncode: | 968 if returncode: |
| 939 # File does not exist in the requested revision. | 969 # File does not exist in the requested revision. |
| 940 # Reset mimetype, it contains an error message. | 970 # Reset mimetype, it contains an error message. |
| 941 mimetype = "" | 971 mimetype = "" |
| 942 get_base = False | 972 get_base = False |
| 943 is_binary = mimetype and not mimetype.startswith("text/") | 973 is_binary = bool(mimetype) and not mimetype.startswith("text/") |
| 944 if status[0] == " ": | 974 if status[0] == " ": |
| 945 # Empty base content just to force an upload. | 975 # Empty base content just to force an upload. |
| 946 base_content = "" | 976 base_content = "" |
| 947 elif is_binary: | 977 elif is_binary: |
| 948 if self.IsImage(filename): | 978 if self.IsImage(filename): |
| 949 get_base = True | 979 get_base = True |
| 950 if status[0] == "M": | 980 if status[0] == "M": |
| 951 if not self.rev_end: | 981 if not self.rev_end: |
| 952 new_content = self.ReadFile(filename) | 982 new_content = self.ReadFile(filename) |
| 953 else: | 983 else: |
| (...skipping 36 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 990 StatusUpdate("svn status returned unexpected output: %s" % status) | 1020 StatusUpdate("svn status returned unexpected output: %s" % status) |
| 991 sys.exit(1) | 1021 sys.exit(1) |
| 992 return base_content, new_content, is_binary, status[0:5] | 1022 return base_content, new_content, is_binary, status[0:5] |
| 993 | 1023 |
| 994 | 1024 |
| 995 class GitVCS(VersionControlSystem): | 1025 class GitVCS(VersionControlSystem): |
| 996 """Implementation of the VersionControlSystem interface for Git.""" | 1026 """Implementation of the VersionControlSystem interface for Git.""" |
| 997 | 1027 |
| 998 def __init__(self, options): | 1028 def __init__(self, options): |
| 999 super(GitVCS, self).__init__(options) | 1029 super(GitVCS, self).__init__(options) |
| 1000 # Map of filename -> hash of base file. | 1030 # Map of filename -> (hash before, hash after) of base file. |
| 1001 self.base_hashes = {} | 1031 # Hashes for "no such file" are represented as None. |
| 1032 self.hashes = {} |
| 1033 # Map of new filename -> old filename for renames. |
| 1034 self.renames = {} |
| 1002 | 1035 |
| 1003 def GenerateDiff(self, extra_args): | 1036 def GenerateDiff(self, extra_args): |
| 1004 # This is more complicated than svn's GenerateDiff because we must convert | 1037 # This is more complicated than svn's GenerateDiff because we must convert |
| 1005 # the diff output to include an svn-style "Index:" line as well as record | 1038 # the diff output to include an svn-style "Index:" line as well as record |
| 1006 # the hashes of the base files, so we can upload them along with our diff. | 1039 # the hashes of the files, so we can upload them along with our diff. |
| 1040 |
| 1041 # Special used by git to indicate "no such content". |
| 1042 NULL_HASH = "0"*40 |
| 1043 |
| 1044 extra_args = extra_args[:] |
| 1007 if self.options.revision: | 1045 if self.options.revision: |
| 1008 extra_args = [self.options.revision] + extra_args | 1046 extra_args = [self.options.revision] + extra_args |
| 1009 gitdiff = RunShell(["git", "diff", "--no-ext-diff", "--full-index"] + | 1047 |
| 1010 extra_args) | 1048 # --no-ext-diff is broken in some versions of Git, so try to work around |
| 1049 # this by overriding the environment (but there is still a problem if the |
| 1050 # git config key "diff.external" is used). |
| 1051 env = os.environ.copy() |
| 1052 if 'GIT_EXTERNAL_DIFF' in env: del env['GIT_EXTERNAL_DIFF'] |
| 1053 gitdiff = RunShell(["git", "diff", "--no-ext-diff", "--full-index", "-M"] |
| 1054 + extra_args, env=env) |
| 1011 svndiff = [] | 1055 svndiff = [] |
| 1012 filecount = 0 | 1056 filecount = 0 |
| 1013 filename = None | 1057 filename = None |
| 1014 for line in gitdiff.splitlines(): | 1058 for line in gitdiff.splitlines(): |
| 1015 match = re.match(r"diff --git a/(.*) b/.*$", line) | 1059 match = re.match(r"diff --git a/(.*) b/(.*)$", line) |
| 1016 if match: | 1060 if match: |
| 1017 filecount += 1 | 1061 filecount += 1 |
| 1018 filename = match.group(1) | 1062 # Intentionally use the "after" filename so we can show renames. |
| 1063 filename = match.group(2) |
| 1019 svndiff.append("Index: %s\n" % filename) | 1064 svndiff.append("Index: %s\n" % filename) |
| 1065 if match.group(1) != match.group(2): |
| 1066 self.renames[match.group(2)] = match.group(1) |
| 1020 else: | 1067 else: |
| 1021 # The "index" line in a git diff looks like this (long hashes elided): | 1068 # The "index" line in a git diff looks like this (long hashes elided): |
| 1022 # index 82c0d44..b2cee3f 100755 | 1069 # index 82c0d44..b2cee3f 100755 |
| 1023 # We want to save the left hash, as that identifies the base file. | 1070 # We want to save the left hash, as that identifies the base file. |
| 1024 match = re.match(r"index (\w+)\.\.", line) | 1071 match = re.match(r"index (\w+)\.\.(\w+)", line) |
| 1025 if match: | 1072 if match: |
| 1026 self.base_hashes[filename] = match.group(1) | 1073 before, after = (match.group(1), match.group(2)) |
| 1074 if before == NULL_HASH: |
| 1075 before = None |
| 1076 if after == NULL_HASH: |
| 1077 after = None |
| 1078 self.hashes[filename] = (before, after) |
| 1027 svndiff.append(line + "\n") | 1079 svndiff.append(line + "\n") |
| 1028 if not filecount: | 1080 if not filecount: |
| 1029 ErrorExit("No valid patches found in output from git diff") | 1081 ErrorExit("No valid patches found in output from git diff") |
| 1030 return "".join(svndiff) | 1082 return "".join(svndiff) |
| 1031 | 1083 |
| 1032 def GetUnknownFiles(self): | 1084 def GetUnknownFiles(self): |
| 1033 status = RunShell(["git", "ls-files", "--exclude-standard", "--others"], | 1085 status = RunShell(["git", "ls-files", "--exclude-standard", "--others"], |
| 1034 silent_ok=True) | 1086 silent_ok=True) |
| 1035 return status.splitlines() | 1087 return status.splitlines() |
| 1036 | 1088 |
| 1089 def GetFileContent(self, file_hash, is_binary): |
| 1090 """Returns the content of a file identified by its git hash.""" |
| 1091 data, retcode = RunShellWithReturnCode(["git", "show", file_hash], |
| 1092 universal_newlines=not is_binary) |
| 1093 if retcode: |
| 1094 ErrorExit("Got error status from 'git show %s'" % file_hash) |
| 1095 return data |
| 1096 |
| 1037 def GetBaseFile(self, filename): | 1097 def GetBaseFile(self, filename): |
| 1038 hash = self.base_hashes[filename] | 1098 hash_before, hash_after = self.hashes.get(filename, (None,None)) |
| 1039 base_content = None | 1099 base_content = None |
| 1040 new_content = None | 1100 new_content = None |
| 1041 is_binary = False | 1101 is_binary = self.IsBinary(filename) |
| 1042 if hash == "0" * 40: # All-zero hash indicates no base file. | 1102 status = None |
| 1103 |
| 1104 if filename in self.renames: |
| 1105 status = "A +" # Match svn attribute name for renames. |
| 1106 if filename not in self.hashes: |
| 1107 # If a rename doesn't change the content, we never get a hash. |
| 1108 base_content = RunShell(["git", "show", filename]) |
| 1109 elif not hash_before: |
| 1043 status = "A" | 1110 status = "A" |
| 1044 base_content = "" | 1111 base_content = "" |
| 1112 elif not hash_after: |
| 1113 status = "D" |
| 1045 else: | 1114 else: |
| 1046 status = "M" | 1115 status = "M" |
| 1047 base_content = RunShell(["git", "show", hash]) | 1116 |
| 1117 is_image = self.IsImage(filename) |
| 1118 |
| 1119 # Grab the before/after content if we need it. |
| 1120 # We should include file contents if it's text or it's an image. |
| 1121 if not is_binary or is_image: |
| 1122 # Grab the base content if we don't have it already. |
| 1123 if base_content is None and hash_before: |
| 1124 base_content = self.GetFileContent(hash_before, is_binary) |
| 1125 # Only include the "after" file if it's an image; otherwise it |
| 1126 # it is reconstructed from the diff. |
| 1127 if is_image and hash_after: |
| 1128 new_content = self.GetFileContent(hash_after, is_binary) |
| 1129 |
| 1048 return (base_content, new_content, is_binary, status) | 1130 return (base_content, new_content, is_binary, status) |
| 1049 | 1131 |
| 1050 | 1132 |
| 1051 class MercurialVCS(VersionControlSystem): | 1133 class MercurialVCS(VersionControlSystem): |
| 1052 """Implementation of the VersionControlSystem interface for Mercurial.""" | 1134 """Implementation of the VersionControlSystem interface for Mercurial.""" |
| 1053 | 1135 |
| 1054 def __init__(self, options, repo_dir): | 1136 def __init__(self, options, repo_dir): |
| 1055 super(MercurialVCS, self).__init__(options) | 1137 super(MercurialVCS, self).__init__(options) |
| 1056 # Absolute path to repository (we can be in a subdir) | 1138 # Absolute path to repository (we can be in a subdir) |
| 1057 self.repo_dir = os.path.normpath(repo_dir) | 1139 self.repo_dir = os.path.normpath(repo_dir) |
| 1058 # Compute the subdir | 1140 # Compute the subdir |
| 1059 cwd = os.path.normpath(os.getcwd()) | 1141 cwd = os.path.normpath(os.getcwd()) |
| 1060 assert cwd.startswith(self.repo_dir) | 1142 assert cwd.startswith(self.repo_dir) |
| 1061 self.subdir = cwd[len(self.repo_dir):].lstrip(r"\/") | 1143 self.subdir = cwd[len(self.repo_dir):].lstrip(r"\/") |
| 1062 if self.options.revision: | 1144 if self.options.revision: |
| 1063 self.base_rev = self.options.revision | 1145 self.base_rev = self.options.revision |
| 1064 else: | 1146 else: |
| 1065 self.base_rev = RunShell(["hg", "parent", "-q"]).split(':')[1].strip() | 1147 self.base_rev = RunShell(["hg", "parent", "-q"]).split(':')[1].strip() |
| 1066 | 1148 |
| 1067 def _GetRelPath(self, filename): | 1149 def _GetRelPath(self, filename): |
| 1068 """Get relative path of a file according to the current directory, | 1150 """Get relative path of a file according to the current directory, |
| 1069 given its logical path in the repo.""" | 1151 given its logical path in the repo.""" |
| 1070 assert filename.startswith(self.subdir), filename | 1152 assert filename.startswith(self.subdir), (filename, self.subdir) |
| 1071 return filename[len(self.subdir):].lstrip(r"\/") | 1153 return filename[len(self.subdir):].lstrip(r"\/") |
| 1072 | 1154 |
| 1073 def GenerateDiff(self, extra_args): | 1155 def GenerateDiff(self, extra_args): |
| 1074 # If no file specified, restrict to the current subdir | 1156 # If no file specified, restrict to the current subdir |
| 1075 extra_args = extra_args or ["."] | 1157 extra_args = extra_args or ["."] |
| 1076 cmd = ["hg", "diff", "--git", "-r", self.base_rev] + extra_args | 1158 cmd = ["hg", "diff", "--git", "-r", self.base_rev] + extra_args |
| 1077 data = RunShell(cmd, silent_ok=True) | 1159 data = RunShell(cmd, silent_ok=True) |
| 1078 svndiff = [] | 1160 svndiff = [] |
| 1079 filecount = 0 | 1161 filecount = 0 |
| 1080 for line in data.splitlines(): | 1162 for line in data.splitlines(): |
| (...skipping 42 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 1123 # the working copy | 1205 # the working copy |
| 1124 if out[0].startswith('%s: ' % relpath): | 1206 if out[0].startswith('%s: ' % relpath): |
| 1125 out = out[1:] | 1207 out = out[1:] |
| 1126 if len(out) > 1: | 1208 if len(out) > 1: |
| 1127 # Moved/copied => considered as modified, use old filename to | 1209 # Moved/copied => considered as modified, use old filename to |
| 1128 # retrieve base contents | 1210 # retrieve base contents |
| 1129 oldrelpath = out[1].strip() | 1211 oldrelpath = out[1].strip() |
| 1130 status = "M" | 1212 status = "M" |
| 1131 else: | 1213 else: |
| 1132 status, _ = out[0].split(' ', 1) | 1214 status, _ = out[0].split(' ', 1) |
| 1215 if ":" in self.base_rev: |
| 1216 base_rev = self.base_rev.split(":", 1)[0] |
| 1217 else: |
| 1218 base_rev = self.base_rev |
| 1133 if status != "A": | 1219 if status != "A": |
| 1134 base_content = RunShell(["hg", "cat", "-r", self.base_rev, oldrelpath], | 1220 base_content = RunShell(["hg", "cat", "-r", base_rev, oldrelpath], |
| 1135 silent_ok=True) | 1221 silent_ok=True) |
| 1136 is_binary = "\0" in base_content # Mercurial's heuristic | 1222 is_binary = "\0" in base_content # Mercurial's heuristic |
| 1137 if status != "R": | 1223 if status != "R": |
| 1138 new_content = open(relpath, "rb").read() | 1224 new_content = open(relpath, "rb").read() |
| 1139 is_binary = is_binary or "\0" in new_content | 1225 is_binary = is_binary or "\0" in new_content |
| 1140 if is_binary and base_content: | 1226 if is_binary and base_content: |
| 1141 # Fetch again without converting newlines | 1227 # Fetch again without converting newlines |
| 1142 base_content = RunShell(["hg", "cat", "-r", self.base_rev, oldrelpath], | 1228 base_content = RunShell(["hg", "cat", "-r", base_rev, oldrelpath], |
| 1143 silent_ok=True, universal_newlines=False) | 1229 silent_ok=True, universal_newlines=False) |
| 1144 if not is_binary or not self.IsImage(relpath): | 1230 if not is_binary or not self.IsImage(relpath): |
| 1145 new_content = None | 1231 new_content = None |
| 1146 return base_content, new_content, is_binary, status | 1232 return base_content, new_content, is_binary, status |
| 1147 | 1233 |
| 1148 | 1234 |
| 1149 # NOTE: The SplitPatch function is duplicated in engine.py, keep them in sync. | 1235 # NOTE: The SplitPatch function is duplicated in engine.py, keep them in sync. |
| 1150 def SplitPatch(data): | 1236 def SplitPatch(data): |
| 1151 """Splits a patch into separate pieces for each file. | 1237 """Splits a patch into separate pieces for each file. |
| 1152 | 1238 |
| (...skipping 55 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 1208 print "Uploading patch for " + patch[0] | 1294 print "Uploading patch for " + patch[0] |
| 1209 response_body = rpc_server.Send(url, body, content_type=ctype) | 1295 response_body = rpc_server.Send(url, body, content_type=ctype) |
| 1210 lines = response_body.splitlines() | 1296 lines = response_body.splitlines() |
| 1211 if not lines or lines[0] != "OK": | 1297 if not lines or lines[0] != "OK": |
| 1212 StatusUpdate(" --> %s" % response_body) | 1298 StatusUpdate(" --> %s" % response_body) |
| 1213 sys.exit(1) | 1299 sys.exit(1) |
| 1214 rv.append([lines[1], patch[0]]) | 1300 rv.append([lines[1], patch[0]]) |
| 1215 return rv | 1301 return rv |
| 1216 | 1302 |
| 1217 | 1303 |
| 1218 def GuessVCS(options): | 1304 def GuessVCSName(): |
| 1219 """Helper to guess the version control system. | 1305 """Helper to guess the version control system. |
| 1220 | 1306 |
| 1221 This examines the current directory, guesses which VersionControlSystem | 1307 This examines the current directory, guesses which VersionControlSystem |
| 1222 we're using, and returns an instance of the appropriate class. Exit with an | 1308 we're using, and returns an string indicating which VCS is detected. |
| 1223 error if we can't figure it out. | |
| 1224 | 1309 |
| 1225 Returns: | 1310 Returns: |
| 1226 A VersionControlSystem instance. Exits if the VCS can't be guessed. | 1311 A pair (vcs, output). vcs is a string indicating which VCS was detected |
| 1312 and is one of VCS_GIT, VCS_MERCURIAL, VCS_SUBVERSION, or VCS_UNKNOWN. |
| 1313 output is a string containing any interesting output from the vcs |
| 1314 detection routine, or None if there is nothing interesting. |
| 1227 """ | 1315 """ |
| 1228 # Mercurial has a command to get the base directory of a repository | 1316 # Mercurial has a command to get the base directory of a repository |
| 1229 # Try running it, but don't die if we don't have hg installed. | 1317 # Try running it, but don't die if we don't have hg installed. |
| 1230 # NOTE: we try Mercurial first as it can sit on top of an SVN working copy. | 1318 # NOTE: we try Mercurial first as it can sit on top of an SVN working copy. |
| 1231 try: | 1319 try: |
| 1232 out, returncode = RunShellWithReturnCode(["hg", "root"]) | 1320 out, returncode = RunShellWithReturnCode(["hg", "root"]) |
| 1233 if returncode == 0: | 1321 if returncode == 0: |
| 1234 return MercurialVCS(options, out.strip()) | 1322 return (VCS_MERCURIAL, out.strip()) |
| 1235 except OSError, (errno, message): | 1323 except OSError, (errno, message): |
| 1236 if errno != 2: # ENOENT -- they don't have hg installed. | 1324 if errno != 2: # ENOENT -- they don't have hg installed. |
| 1237 raise | 1325 raise |
| 1238 | 1326 |
| 1239 # Subversion has a .svn in all working directories. | 1327 # Subversion has a .svn in all working directories. |
| 1240 if os.path.isdir('.svn'): | 1328 if os.path.isdir('.svn'): |
| 1241 logging.info("Guessed VCS = Subversion") | 1329 logging.info("Guessed VCS = Subversion") |
| 1242 return SubversionVCS(options) | 1330 return (VCS_SUBVERSION, None) |
| 1243 | 1331 |
| 1244 # Git has a command to test if you're in a git tree. | 1332 # Git has a command to test if you're in a git tree. |
| 1245 # Try running it, but don't die if we don't have git installed. | 1333 # Try running it, but don't die if we don't have git installed. |
| 1246 try: | 1334 try: |
| 1247 out, returncode = RunShellWithReturnCode(["git", "rev-parse", | 1335 out, returncode = RunShellWithReturnCode(["git", "rev-parse", |
| 1248 "--is-inside-work-tree"]) | 1336 "--is-inside-work-tree"]) |
| 1249 if returncode == 0: | 1337 if returncode == 0: |
| 1250 return GitVCS(options) | 1338 return (VCS_GIT, None) |
| 1251 except OSError, (errno, message): | 1339 except OSError, (errno, message): |
| 1252 if errno != 2: # ENOENT -- they don't have git installed. | 1340 if errno != 2: # ENOENT -- they don't have git installed. |
| 1253 raise | 1341 raise |
| 1254 | 1342 |
| 1343 return (VCS_UNKNOWN, None) |
| 1344 |
| 1345 |
| 1346 def GuessVCS(options): |
| 1347 """Helper to guess the version control system. |
| 1348 |
| 1349 This verifies any user-specified VersionControlSystem (by command line |
| 1350 or environment variable). If the user didn't specify one, this examines |
| 1351 the current directory, guesses which VersionControlSystem we're using, |
| 1352 and returns an instance of the appropriate class. Exit with an error |
| 1353 if we can't figure it out. |
| 1354 |
| 1355 Returns: |
| 1356 A VersionControlSystem instance. Exits if the VCS can't be guessed. |
| 1357 """ |
| 1358 vcs = options.vcs |
| 1359 if not vcs: |
| 1360 vcs = os.environ.get("CODEREVIEW_VCS") |
| 1361 if vcs: |
| 1362 v = VCS_ABBREVIATIONS.get(vcs.lower()) |
| 1363 if v is None: |
| 1364 ErrorExit("Unknown version control system %r specified." % vcs) |
| 1365 (vcs, extra_output) = (v, None) |
| 1366 else: |
| 1367 (vcs, extra_output) = GuessVCSName() |
| 1368 |
| 1369 if vcs == VCS_MERCURIAL: |
| 1370 if extra_output is None: |
| 1371 extra_output = RunShell(["hg", "root"]).strip() |
| 1372 return MercurialVCS(options, extra_output) |
| 1373 elif vcs == VCS_SUBVERSION: |
| 1374 return SubversionVCS(options) |
| 1375 elif vcs == VCS_GIT: |
| 1376 return GitVCS(options) |
| 1377 |
| 1255 ErrorExit(("Could not guess version control system. " | 1378 ErrorExit(("Could not guess version control system. " |
| 1256 "Are you in a working copy directory?")) | 1379 "Are you in a working copy directory?")) |
| 1257 | 1380 |
| 1258 | 1381 |
| 1382 def CheckReviewer(reviewer): |
| 1383 """Validate a reviewer -- either a nickname or an email addres. |
| 1384 |
| 1385 Args: |
| 1386 reviewer: A nickname or an email address. |
| 1387 |
| 1388 Calls ErrorExit() if it is an invalid email address. |
| 1389 """ |
| 1390 if "@" not in reviewer: |
| 1391 return # Assume nickname |
| 1392 parts = reviewer.split("@") |
| 1393 if len(parts) > 2: |
| 1394 ErrorExit("Invalid email address: %r" % reviewer) |
| 1395 assert len(parts) == 2 |
| 1396 if "." not in parts[1]: |
| 1397 ErrorExit("Invalid email address: %r" % reviewer) |
| 1398 |
| 1399 |
| 1259 def RealMain(argv, data=None): | 1400 def RealMain(argv, data=None): |
| 1401 """The real main function. |
| 1402 |
| 1403 Args: |
| 1404 argv: Command line arguments. |
| 1405 data: Diff contents. If None (default) the diff is generated by |
| 1406 the VersionControlSystem implementation returned by GuessVCS(). |
| 1407 |
| 1408 Returns: |
| 1409 A 2-tuple (issue id, patchset id). |
| 1410 The patchset id is None if the base files are not uploaded by this |
| 1411 script (applies only to SVN checkouts). |
| 1412 """ |
| 1260 logging.basicConfig(format=("%(asctime).19s %(levelname)s %(filename)s:" | 1413 logging.basicConfig(format=("%(asctime).19s %(levelname)s %(filename)s:" |
| 1261 "%(lineno)s %(message)s ")) | 1414 "%(lineno)s %(message)s ")) |
| 1262 os.environ['LC_ALL'] = 'C' | 1415 os.environ['LC_ALL'] = 'C' |
| 1263 options, args = parser.parse_args(argv[1:]) | 1416 options, args = parser.parse_args(argv[1:]) |
| 1264 global verbosity | 1417 global verbosity |
| 1265 verbosity = options.verbose | 1418 verbosity = options.verbose |
| 1266 if verbosity >= 3: | 1419 if verbosity >= 3: |
| 1267 logging.getLogger().setLevel(logging.DEBUG) | 1420 logging.getLogger().setLevel(logging.DEBUG) |
| 1268 elif verbosity >= 2: | 1421 elif verbosity >= 2: |
| 1269 logging.getLogger().setLevel(logging.INFO) | 1422 logging.getLogger().setLevel(logging.INFO) |
| (...skipping 24 matching lines...) Expand all Loading... |
| 1294 rpc_server = GetRpcServer(options) | 1447 rpc_server = GetRpcServer(options) |
| 1295 form_fields = [("subject", message)] | 1448 form_fields = [("subject", message)] |
| 1296 if base: | 1449 if base: |
| 1297 form_fields.append(("base", base)) | 1450 form_fields.append(("base", base)) |
| 1298 if options.issue: | 1451 if options.issue: |
| 1299 form_fields.append(("issue", str(options.issue))) | 1452 form_fields.append(("issue", str(options.issue))) |
| 1300 if options.email: | 1453 if options.email: |
| 1301 form_fields.append(("user", options.email)) | 1454 form_fields.append(("user", options.email)) |
| 1302 if options.reviewers: | 1455 if options.reviewers: |
| 1303 for reviewer in options.reviewers.split(','): | 1456 for reviewer in options.reviewers.split(','): |
| 1304 if "@" in reviewer and not reviewer.split("@")[1].count(".") == 1: | 1457 CheckReviewer(reviewer) |
| 1305 ErrorExit("Invalid email address: %s" % reviewer) | |
| 1306 form_fields.append(("reviewers", options.reviewers)) | 1458 form_fields.append(("reviewers", options.reviewers)) |
| 1307 if options.cc: | 1459 if options.cc: |
| 1308 for cc in options.cc.split(','): | 1460 for cc in options.cc.split(','): |
| 1309 if "@" in cc and not cc.split("@")[1].count(".") == 1: | 1461 CheckReviewer(cc) |
| 1310 ErrorExit("Invalid email address: %s" % cc) | |
| 1311 form_fields.append(("cc", options.cc)) | 1462 form_fields.append(("cc", options.cc)) |
| 1312 description = options.description | 1463 description = options.description |
| 1313 if options.description_file: | 1464 if options.description_file: |
| 1314 if options.description: | 1465 if options.description: |
| 1315 ErrorExit("Can't specify description and description_file") | 1466 ErrorExit("Can't specify description and description_file") |
| 1316 file = open(options.description_file, 'r') | 1467 file = open(options.description_file, 'r') |
| 1317 description = file.read() | 1468 description = file.read() |
| 1318 file.close() | 1469 file.close() |
| 1319 if description: | 1470 if description: |
| 1320 form_fields.append(("description", description)) | 1471 form_fields.append(("description", description)) |
| 1321 # Send a hash of all the base file so the server can determine if a copy | 1472 # Send a hash of all the base file so the server can determine if a copy |
| 1322 # already exists in an earlier patchset. | 1473 # already exists in an earlier patchset. |
| 1323 base_hashes = "" | 1474 base_hashes = "" |
| 1324 for file, info in files.iteritems(): | 1475 for file, info in files.iteritems(): |
| 1325 if not info[0] is None: | 1476 if not info[0] is None: |
| 1326 checksum = md5(info[0]).hexdigest() | 1477 checksum = md5(info[0]).hexdigest() |
| 1327 if base_hashes: | 1478 if base_hashes: |
| 1328 base_hashes += "|" | 1479 base_hashes += "|" |
| 1329 base_hashes += checksum + ":" + file | 1480 base_hashes += checksum + ":" + file |
| 1330 form_fields.append(("base_hashes", base_hashes)) | 1481 form_fields.append(("base_hashes", base_hashes)) |
| 1482 if options.private: |
| 1483 if options.issue: |
| 1484 print "Warning: Private flag ignored when updating an existing issue." |
| 1485 else: |
| 1486 form_fields.append(("private", "1")) |
| 1331 # If we're uploading base files, don't send the email before the uploads, so | 1487 # If we're uploading base files, don't send the email before the uploads, so |
| 1332 # that it contains the file status. | 1488 # that it contains the file status. |
| 1333 if options.send_mail and options.download_base: | 1489 if options.send_mail and options.download_base: |
| 1334 form_fields.append(("send_mail", "1")) | 1490 form_fields.append(("send_mail", "1")) |
| 1335 if not options.download_base: | 1491 if not options.download_base: |
| 1336 form_fields.append(("content_upload", "1")) | 1492 form_fields.append(("content_upload", "1")) |
| 1337 if len(data) > MAX_UPLOAD_SIZE: | 1493 if len(data) > MAX_UPLOAD_SIZE: |
| 1338 print "Patch is large, so uploading file patches separately." | 1494 print "Patch is large, so uploading file patches separately." |
| 1339 uploaded_diff_file = [] | 1495 uploaded_diff_file = [] |
| 1340 form_fields.append(("separate_patches", "1")) | 1496 form_fields.append(("separate_patches", "1")) |
| 1341 else: | 1497 else: |
| 1342 uploaded_diff_file = [("data", "data.diff", data)] | 1498 uploaded_diff_file = [("data", "data.diff", data)] |
| 1343 ctype, body = EncodeMultipartFormData(form_fields, uploaded_diff_file) | 1499 ctype, body = EncodeMultipartFormData(form_fields, uploaded_diff_file) |
| 1344 response_body = rpc_server.Send("/upload", body, content_type=ctype) | 1500 response_body = rpc_server.Send("/upload", body, content_type=ctype) |
| 1501 patchset = None |
| 1345 if not options.download_base or not uploaded_diff_file: | 1502 if not options.download_base or not uploaded_diff_file: |
| 1346 lines = response_body.splitlines() | 1503 lines = response_body.splitlines() |
| 1347 if len(lines) >= 2: | 1504 if len(lines) >= 2: |
| 1348 msg = lines[0] | 1505 msg = lines[0] |
| 1349 patchset = lines[1].strip() | 1506 patchset = lines[1].strip() |
| 1350 patches = [x.split(" ", 1) for x in lines[2:]] | 1507 patches = [x.split(" ", 1) for x in lines[2:]] |
| 1351 for patch_pair in patches: | |
| 1352 # On Windows if a file has property changes its filename uses '\' | |
| 1353 # instead of '/'. Perhaps this change should be made (also) on the | |
| 1354 # server when it is decoding the patch file sent by the client, but | |
| 1355 # we do it here as well to be safe. | |
| 1356 patch_pair[1] = patch_pair[1].replace('\\', '/') | |
| 1357 else: | 1508 else: |
| 1358 msg = response_body | 1509 msg = response_body |
| 1359 else: | 1510 else: |
| 1360 msg = response_body | 1511 msg = response_body |
| 1361 StatusUpdate(msg) | 1512 StatusUpdate(msg) |
| 1362 if not response_body.startswith("Issue created.") and \ | 1513 if not response_body.startswith("Issue created.") and \ |
| 1363 not response_body.startswith("Issue updated."): | 1514 not response_body.startswith("Issue updated."): |
| 1364 sys.exit(0) | 1515 sys.exit(0) |
| 1365 issue = msg[msg.rfind("/")+1:] | 1516 issue = msg[msg.rfind("/")+1:] |
| 1366 | 1517 |
| (...skipping 13 matching lines...) Expand all Loading... |
| 1380 try: | 1531 try: |
| 1381 RealMain(sys.argv) | 1532 RealMain(sys.argv) |
| 1382 except KeyboardInterrupt: | 1533 except KeyboardInterrupt: |
| 1383 print | 1534 print |
| 1384 StatusUpdate("Interrupted.") | 1535 StatusUpdate("Interrupted.") |
| 1385 sys.exit(1) | 1536 sys.exit(1) |
| 1386 | 1537 |
| 1387 | 1538 |
| 1388 if __name__ == "__main__": | 1539 if __name__ == "__main__": |
| 1389 main() | 1540 main() |
| OLD | NEW |