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 |