| LEFT | RIGHT |
|---|---|
| 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 # |
| 11 # Unless required by applicable law or agreed to in writing, software | 11 # Unless required by applicable law or agreed to in writing, software |
| 12 # distributed under the License is distributed on an "AS IS" BASIS, | 12 # distributed under the License is distributed on an "AS IS" BASIS, |
| 13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 14 # See the License for the specific language governing permissions and | 14 # See the License for the specific language governing permissions and |
| 15 # limitations under the License. | 15 # limitations under the License. |
| 16 | 16 |
| 17 """Tool for uploading diffs to the codereview app. | 17 """Tool for uploading diffs from a version control system to the codereview app. |
|
Andi Albrecht
2008/08/19 04:59:03
I think we should make clear that it uploads diffs
| |
| 18 | 18 |
| 19 Usage summary: upload.py [options] [-- diff_options] | 19 Usage summary: upload.py [options] [-- diff_options] |
| 20 | 20 |
| 21 With Subversion, diff_options are passed to "svn diff". | 21 Diff options are passed to the diff command of the underlying system. |
| 22 With Git, diff_options are passed to "git diff". | 22 |
|
Andi Albrecht
2008/08/19 04:59:03
I'd unify this message to something like "diff_opt
| |
| 23 Supported version control systems: | |
| 24 Git | |
| 25 Subversion | |
| 26 | |
| 27 (It is important for Git users to specify a tree-ish to diff against.) | |
| 23 """ | 28 """ |
| 24 # This code is derived from appcfg.py in the App Engine SDK (open source), | 29 # This code is derived from appcfg.py in the App Engine SDK (open source), |
| 25 # and from ASPN recipe #146306. | 30 # and from ASPN recipe #146306. |
| 26 | 31 |
| 27 import cookielib | 32 import cookielib |
| 28 import getpass | 33 import getpass |
| 29 import logging | 34 import logging |
| 30 import md5 | 35 import md5 |
| 31 import mimetypes | 36 import mimetypes |
| 32 import optparse | 37 import optparse |
| 33 import os | 38 import os |
| 34 import re | 39 import re |
| 35 import socket | 40 import socket |
| 41 import subprocess | |
| 36 import sys | 42 import sys |
| 37 import urllib | 43 import urllib |
| 38 import urllib2 | 44 import urllib2 |
| 39 import urlparse | 45 import urlparse |
| 40 | 46 |
| 41 try: | 47 try: |
| 42 import readline | 48 import readline |
| 43 except ImportError: | 49 except ImportError: |
| 44 pass | 50 pass |
| 45 | 51 |
| (...skipping 440 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 486 """Helper to guess the content-type from the filename.""" | 492 """Helper to guess the content-type from the filename.""" |
| 487 return mimetypes.guess_type(filename)[0] or 'application/octet-stream' | 493 return mimetypes.guess_type(filename)[0] or 'application/octet-stream' |
| 488 | 494 |
| 489 | 495 |
| 490 def RunShell(command, args=(), silent_ok=False): | 496 def RunShell(command, args=(), silent_ok=False): |
| 491 command = "%s %s" % (command, " ".join(args)) | 497 command = "%s %s" % (command, " ".join(args)) |
| 492 logging.info("Running %s", command) | 498 logging.info("Running %s", command) |
| 493 stream = os.popen(command, "r") | 499 stream = os.popen(command, "r") |
| 494 data = stream.read() | 500 data = stream.read() |
| 495 if stream.close(): | 501 if stream.close(): |
| 496 ErrorExit("Got error status from %s" % (command + str(args))) | 502 ErrorExit("Got error status from %s" % command) |
| 497 if not silent_ok and not data: | 503 if not silent_ok and not data: |
| 498 ErrorExit("No output from %s" % command) | 504 ErrorExit("No output from %s" % command) |
| 499 return data | 505 return data |
| 500 | 506 |
| 501 | 507 |
| 502 class VersionControlSystem(object): | 508 class VersionControlSystem(object): |
| 503 """Abstract base class providing an interface to the VCS.""" | 509 """Abstract base class providing an interface to the VCS.""" |
| 504 | 510 |
| 505 def GenerateDiff(self, args): | 511 def GenerateDiff(self, args): |
| 506 """Return the current diff as a string. | 512 """Return the current diff as a string. |
| (...skipping 81 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 588 Args: | 594 Args: |
| 589 required: If true, exits if the url can't be guessed, otherwise None is | 595 required: If true, exits if the url can't be guessed, otherwise None is |
| 590 returned. | 596 returned. |
| 591 """ | 597 """ |
| 592 info = RunShell("svn info") | 598 info = RunShell("svn info") |
| 593 for line in info.splitlines(): | 599 for line in info.splitlines(): |
| 594 words = line.split() | 600 words = line.split() |
| 595 if len(words) == 2 and words[0] == "URL:": | 601 if len(words) == 2 and words[0] == "URL:": |
| 596 url = words[1] | 602 url = words[1] |
| 597 scheme, netloc, path, params, query, fragment = urlparse.urlparse(url) | 603 scheme, netloc, path, params, query, fragment = urlparse.urlparse(url) |
| 604 username, netloc = urllib.splituser(netloc) | |
| 605 if username: | |
| 606 logging.info("Removed username from base URL") | |
| 598 if netloc.endswith("svn.python.org"): | 607 if netloc.endswith("svn.python.org"): |
| 599 if netloc == "svn.python.org": | 608 if netloc == "svn.python.org": |
| 600 if path.startswith("/projects/"): | 609 if path.startswith("/projects/"): |
| 601 path = path[9:] | 610 path = path[9:] |
| 602 elif netloc != "pythondev@svn.python.org": | 611 elif netloc != "pythondev@svn.python.org": |
| 603 ErrorExit("Unrecognized Python URL: %s" % url) | 612 ErrorExit("Unrecognized Python URL: %s" % url) |
| 604 base = "http://svn.python.org/view/*checkout*%s/" % path | 613 base = "http://svn.python.org/view/*checkout*%s/" % path |
| 605 logging.info("Guessed Python base = %s", base) | 614 logging.info("Guessed Python base = %s", base) |
| 606 elif netloc.endswith("svn.collab.net"): | 615 elif netloc.endswith("svn.collab.net"): |
| 607 if path.startswith("/repos/"): | 616 if path.startswith("/repos/"): |
| 608 path = path[6:] | 617 path = path[6:] |
| 609 base = "http://svn.collab.net/viewvc/*checkout*%s/" % path | 618 base = "http://svn.collab.net/viewvc/*checkout*%s/" % path |
| 610 logging.info("Guessed CollabNet base = %s", base) | 619 logging.info("Guessed CollabNet base = %s", base) |
| 611 elif netloc.endswith(".googlecode.com"): | 620 elif netloc.endswith(".googlecode.com"): |
| 612 base = url + "/" | 621 path = path + "/" |
| 613 if base.startswith("https"): | 622 base = urlparse.urlunparse(("http", netloc, path, params, |
| 614 base = "http" + base[5:] | 623 query, fragment)) |
| 615 logging.info("Guessed Google Code base = %s", base) | 624 logging.info("Guessed Google Code base = %s", base) |
| 616 else: | 625 else: |
| 617 base = url + "/" | 626 path = path + "/" |
| 627 base = urlparse.urlunparse((scheme, netloc, path, params, | |
| 628 query, fragment)) | |
| 618 logging.info("Guessed base = %s", base) | 629 logging.info("Guessed base = %s", base) |
| 619 return base | 630 return base |
| 620 if required: | 631 if required: |
| 621 ErrorExit("Can't find URL in output from svn info") | 632 ErrorExit("Can't find URL in output from svn info") |
| 622 return None | 633 return None |
| 623 | 634 |
| 624 def GenerateDiff(self, args): | 635 def GenerateDiff(self, args): |
| 625 cmd = "svn diff" | 636 cmd = "svn diff" |
| 626 if not sys.platform.startswith("win"): | 637 if not sys.platform.startswith("win"): |
| 627 cmd += " --diff-cmd=diff" | 638 cmd += " --diff-cmd=diff" |
| (...skipping 94 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 722 | 733 |
| 723 def GenerateDiff(self, extra_args): | 734 def GenerateDiff(self, extra_args): |
| 724 # This is more complicated than svn's GenerateDiff because we must convert | 735 # This is more complicated than svn's GenerateDiff because we must convert |
| 725 # the diff output to include an svn-style "Index:" line as well as record | 736 # the diff output to include an svn-style "Index:" line as well as record |
| 726 # the hashes of the base files, so we can upload them along with our diff. | 737 # the hashes of the base files, so we can upload them along with our diff. |
| 727 gitdiff = RunShell("git diff", ["--full-index"] + extra_args) | 738 gitdiff = RunShell("git diff", ["--full-index"] + extra_args) |
| 728 svndiff = [] | 739 svndiff = [] |
| 729 filecount = 0 | 740 filecount = 0 |
| 730 filename = None | 741 filename = None |
| 731 for line in gitdiff.splitlines(): | 742 for line in gitdiff.splitlines(): |
| 732 match = re.match(r"diff --git a/(.*) b/.*", line) | 743 match = re.match(r"diff --git a/(.*) b/.*$", line) |
|
Andi Albrecht
2008/08/19 04:59:03
We should wrap the regular expression in '^...$' t
| |
| 733 if match: | 744 if match: |
| 734 filecount += 1 | 745 filecount += 1 |
| 735 filename = match.group(1) | 746 filename = match.group(1) |
| 736 svndiff.append("Index: %s\n" % filename) | 747 svndiff.append("Index: %s\n" % filename) |
| 737 else: | 748 else: |
| 738 # The "index" line in a git diff looks like this (long hashes elided): | 749 # The "index" line in a git diff looks like this (long hashes elided): |
| 739 # index 82c0d44..b2cee3f 100755 | 750 # index 82c0d44..b2cee3f 100755 |
| 740 # We want to save the left hash, as that identifies the base file. | 751 # We want to save the left hash, as that identifies the base file. |
| 741 match = re.match(r"index (\w+)\.\.", line) | 752 match = re.match(r"index (\w+)\.\.", line) |
|
Andi Albrecht
2008/08/19 04:59:03
'^...$' here too.
| |
| 742 if match: | 753 if match: |
| 743 self.base_hashes[filename] = match.group(1) | 754 self.base_hashes[filename] = match.group(1) |
| 744 svndiff.append(line + "\n") | 755 svndiff.append(line + "\n") |
| 745 if not filecount: | 756 if not filecount: |
|
Andi Albrecht
2008/08/19 04:59:03
Do we need to run an additional sanity check here?
| |
| 746 ErrorExit("No valid patches found in output from git diff") | 757 ErrorExit("No valid patches found in output from git diff") |
| 747 return "".join(svndiff) | 758 return "".join(svndiff) |
| 748 | 759 |
| 749 def GetUnknownFiles(self): | 760 def GetUnknownFiles(self): |
| 750 status = RunShell("git ls-files --others", silent_ok=True) | 761 status = RunShell("git ls-files --others", silent_ok=True) |
| 751 return status.splitlines() | 762 return status.splitlines() |
| 752 | 763 |
| 753 def GetBaseFile(self, filename): | 764 def GetBaseFile(self, filename): |
| 754 hash = self.base_hashes[filename] | 765 hash = self.base_hashes[filename] |
| 755 if hash == "0" * 40: # All-zero hash indicates no base file. | 766 if hash == "0" * 40: # All-zero hash indicates no base file. |
| (...skipping 69 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 825 sys.exit(False) | 836 sys.exit(False) |
| 826 rv.append([lines[1], patch[0]]) | 837 rv.append([lines[1], patch[0]]) |
| 827 return rv | 838 return rv |
| 828 | 839 |
| 829 | 840 |
| 830 def GuessVCS(): | 841 def GuessVCS(): |
| 831 """Helper to guess the version control system. | 842 """Helper to guess the version control system. |
| 832 | 843 |
| 833 This examines the current directory, guesses which VersionControlSystem | 844 This examines the current directory, guesses which VersionControlSystem |
| 834 we're using, and returns an instance of the appropriate class. Exit with an | 845 we're using, and returns an instance of the appropriate class. Exit with an |
| 835 error if we can't figure it out. | 846 error if we can't figure it out. |
|
Andi Albrecht
2008/08/19 19:48:34
Thanks for adding some more docstrings and comment
| |
| 836 | 847 |
| 837 Returns: | 848 Returns: |
| 838 A VersionControlSystem instance. Exits if the VCS can't be guessed. | 849 A VersionControlSystem instance. Exits if the VCS can't be guessed. |
| 839 """ | 850 """ |
| 840 | |
| 841 # Subversion has a .svn in all working directories. | 851 # Subversion has a .svn in all working directories. |
| 842 if os.path.isdir(os.path.join(os.getcwd(), '.svn')): | 852 if os.path.isdir('.svn'): |
| 843 logging.info("Guessed VCS = Subversion") | 853 logging.info("Guessed VCS = Subversion") |
| 844 return SubversionVCS() | 854 return SubversionVCS() |
| 845 | 855 |
| 846 # Git has a command to test if you're in a git tree. | 856 # Git has a command to test if you're in a git tree. |
| 847 # This will silently fail if you don't have git installed. | 857 # Try running it, but don't die if we don't have git installed. |
| 848 if os.system("git rev-parse --is-inside-work-tree >/dev/null 2>&1") == 0: | 858 try: |
|
Andi Albrecht
2008/08/19 04:59:03
What about Windows users? I'm not sure how the syn
| |
| 849 return GitVCS() | 859 subproc = subprocess.Popen(["git", "rev-parse", "--is-inside-work-tree"], |
| 860 stdout=subprocess.PIPE, stderr=subprocess.PIPE) | |
| 861 if subproc.wait() == 0: | |
| 862 return GitVCS() | |
| 863 except OSError, (errno, message): | |
| 864 if errno != 2: # ENOENT -- they don't have git installed. | |
| 865 raise | |
| 850 | 866 |
| 851 ErrorExit(("Could not guess version control system. " | 867 ErrorExit(("Could not guess version control system. " |
| 852 "Are you in a working copy directory?")) | 868 "Are you in a working copy directory?")) |
| 853 | 869 |
| 854 | 870 |
| 855 def RealMain(argv, data=None): | 871 def RealMain(argv, data=None): |
| 856 logging.basicConfig(format=("%(asctime).19s %(levelname)s %(filename)s:" | 872 logging.basicConfig(format=("%(asctime).19s %(levelname)s %(filename)s:" |
| 857 "%(lineno)s %(message)s ")) | 873 "%(lineno)s %(message)s ")) |
| 858 os.environ['LC_ALL'] = 'C' | 874 os.environ['LC_ALL'] = 'C' |
| 859 options, args = parser.parse_args(argv[1:]) | 875 options, args = parser.parse_args(argv[1:]) |
| 860 global verbosity | 876 global verbosity |
| 861 verbosity = options.verbose | 877 verbosity = options.verbose |
| 862 if verbosity >= 3: | 878 if verbosity >= 3: |
| 863 logging.getLogger().setLevel(logging.DEBUG) | 879 logging.getLogger().setLevel(logging.DEBUG) |
| 864 elif verbosity >= 2: | 880 elif verbosity >= 2: |
| 865 logging.getLogger().setLevel(logging.INFO) | 881 logging.getLogger().setLevel(logging.INFO) |
| 866 vcs = GuessVCS() | 882 vcs = GuessVCS() |
| 867 if isinstance(vcs, SubversionVCS): | 883 if isinstance(vcs, SubversionVCS): |
| 868 # base field is only allowed for Subversion. | 884 # base field is only allowed for Subversion. |
| 869 # Note: Fetching base files may become deprecated in future releases. | 885 # Note: Fetching base files may become deprecated in future releases. |
| 870 base = vcs.GuessBase(not options.local_base) | 886 base = vcs.GuessBase(not options.local_base) |
| 871 else: | 887 else: |
| 872 base = None | 888 base = None |
| 873 if not base and not options.local_base: | 889 if not base and not options.local_base: |
| 890 # TODO(andi): Enable local_base for other VCS by default. | |
| 874 # For future use. SubversionVCS.GuessBase() already checks this | 891 # For future use. SubversionVCS.GuessBase() already checks this |
|
Andi Albrecht
2008/08/19 19:48:34
I think we can enable local_base here now for all
| |
| 875 # condition. | 892 # condition. |
| 876 ErrorExit("Use '--local_base' to upload base files.") | 893 ErrorExit("Use '--local_base' to upload base files.") |
| 877 if not options.assume_yes: | 894 if not options.assume_yes: |
| 878 vcs.CheckForUnknownFiles() | 895 vcs.CheckForUnknownFiles() |
| 879 if not data: | 896 if not data: |
| 880 data = vcs.GenerateDiff(args) | 897 data = vcs.GenerateDiff(args) |
| 881 if verbosity >= 1: | 898 if verbosity >= 1: |
| 882 print "Upload server:", options.server, "(change with -s/--server)" | 899 print "Upload server:", options.server, "(change with -s/--server)" |
| 883 if options.issue: | 900 if options.issue: |
| 884 prompt = "Message describing this patch set: " | 901 prompt = "Message describing this patch set: " |
| (...skipping 53 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 938 msg = response_body | 955 msg = response_body |
| 939 else: | 956 else: |
| 940 msg = response_body | 957 msg = response_body |
| 941 StatusUpdate(msg) | 958 StatusUpdate(msg) |
| 942 if not response_body.startswith("Issue created.") and \ | 959 if not response_body.startswith("Issue created.") and \ |
| 943 not response_body.startswith("Issue updated."): | 960 not response_body.startswith("Issue updated."): |
| 944 sys.exit(0) | 961 sys.exit(0) |
| 945 issue = msg[msg.rfind("/")+1:] | 962 issue = msg[msg.rfind("/")+1:] |
| 946 | 963 |
| 947 if not files: | 964 if not files: |
| 948 rv = UploadSeparatePatches(issue, rpc_server, patchset, data, options) | 965 result = UploadSeparatePatches(issue, rpc_server, patchset, data, options) |
| 949 if options.local_base: | 966 if options.local_base: |
| 950 patches = rv | 967 patches = result |
| 951 | 968 |
| 952 if options.local_base: | 969 if options.local_base: |
| 953 vcs.UploadBaseFiles(issue, rpc_server, patches, patchset, options) | 970 vcs.UploadBaseFiles(issue, rpc_server, patches, patchset, options) |
| 954 if options.send_mail: | 971 if options.send_mail: |
| 955 rpc_server.Send("/" + issue + "/mail") | 972 rpc_server.Send("/" + issue + "/mail") |
| 956 return issue | 973 return issue |
| 957 | 974 |
| 958 | 975 |
| 959 def main(): | 976 def main(): |
| 960 try: | 977 try: |
| 961 RealMain(sys.argv) | 978 RealMain(sys.argv) |
| 962 except KeyboardInterrupt: | 979 except KeyboardInterrupt: |
| 963 print | 980 print |
| 964 StatusUpdate("Interrupted.") | 981 StatusUpdate("Interrupted.") |
| 965 sys.exit(1) | 982 sys.exit(1) |
| 966 | 983 |
| 967 | 984 |
| 968 if __name__ == "__main__": | 985 if __name__ == "__main__": |
| 969 main() | 986 main() |
| LEFT | RIGHT |