| 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 # |
| 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 subversion diffs to the codereview app. | 17 """Tool for uploading diffs from a version control system to the codereview app. |
| 18 | 18 |
| 19 Usage summary: upload.py [options] [-- svn_diff_options] | 19 Usage summary: upload.py [options] [-- diff_options] |
| 20 | |
| 21 Diff options are passed to the diff command of the underlying system. | |
| 22 | |
| 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.) | |
| 20 """ | 28 """ |
| 21 # 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), |
| 22 # and from ASPN recipe #146306. | 30 # and from ASPN recipe #146306. |
| 23 | 31 |
| 24 import cookielib | 32 import cookielib |
| 25 import getpass | 33 import getpass |
| 26 import logging | 34 import logging |
| 27 import md5 | 35 import md5 |
| 28 import mimetypes | 36 import mimetypes |
| 29 import optparse | 37 import optparse |
| 30 import os | 38 import os |
| 31 import re | 39 import re |
| 32 import socket | 40 import socket |
| 41 import subprocess | |
| 33 import sys | 42 import sys |
| 34 import urllib | 43 import urllib |
| 35 import urllib2 | 44 import urllib2 |
| 36 import urlparse | 45 import urlparse |
| 37 | 46 |
| 38 try: | 47 try: |
| 39 import readline | 48 import readline |
| 40 except ImportError: | 49 except ImportError: |
| 41 pass | 50 pass |
| 42 | 51 |
| (...skipping 292 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 335 os.close(fd) | 344 os.close(fd) |
| 336 # Always chmod the cookie file | 345 # Always chmod the cookie file |
| 337 os.chmod(self.cookie_file, 0600) | 346 os.chmod(self.cookie_file, 0600) |
| 338 else: | 347 else: |
| 339 # Don't save cookies across runs of update.py. | 348 # Don't save cookies across runs of update.py. |
| 340 self.cookie_jar = cookielib.CookieJar() | 349 self.cookie_jar = cookielib.CookieJar() |
| 341 opener.add_handler(urllib2.HTTPCookieProcessor(self.cookie_jar)) | 350 opener.add_handler(urllib2.HTTPCookieProcessor(self.cookie_jar)) |
| 342 return opener | 351 return opener |
| 343 | 352 |
| 344 | 353 |
| 345 parser = optparse.OptionParser(usage="%prog [options] [-- svn_diff_options]") | 354 parser = optparse.OptionParser(usage="%prog [options] [-- diff_options]") |
| 346 parser.add_option("-y", "--assume_yes", action="store_true", | 355 parser.add_option("-y", "--assume_yes", action="store_true", |
| 347 dest="assume_yes", default=False, | 356 dest="assume_yes", default=False, |
| 348 help="Assume that the answer to yes/no questions is 'yes'.") | 357 help="Assume that the answer to yes/no questions is 'yes'.") |
| 349 # Logging | 358 # Logging |
| 350 group = parser.add_option_group("Logging options") | 359 group = parser.add_option_group("Logging options") |
| 351 group.add_option("-q", "--quiet", action="store_const", const=0, | 360 group.add_option("-q", "--quiet", action="store_const", const=0, |
| 352 dest="verbose", help="Print errors only.") | 361 dest="verbose", help="Print errors only.") |
| 353 group.add_option("-v", "--verbose", action="store_const", const=2, | 362 group.add_option("-v", "--verbose", action="store_const", const=2, |
| 354 dest="verbose", default=1, | 363 dest="verbose", default=1, |
| 355 help="Print info level logs (default).") | 364 help="Print info level logs (default).") |
| (...skipping 350 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 706 else: | 715 else: |
| 707 content = RunShell("svn cat", [filename]) | 716 content = RunShell("svn cat", [filename]) |
| 708 keywords = RunShell("svn -rBASE propget svn:keywords", [filename], | 717 keywords = RunShell("svn -rBASE propget svn:keywords", [filename], |
| 709 silent_ok=True) | 718 silent_ok=True) |
| 710 if keywords: | 719 if keywords: |
| 711 content = self._CollapseKeywords(content, keywords) | 720 content = self._CollapseKeywords(content, keywords) |
| 712 else: | 721 else: |
| 713 StatusUpdate("svn status returned unexpected output: %s" % status) | 722 StatusUpdate("svn status returned unexpected output: %s" % status) |
| 714 sys.exit(False) | 723 sys.exit(False) |
| 715 return content, status[0:5] | 724 return content, status[0:5] |
| 725 | |
| 726 | |
| 727 class GitVCS(VersionControlSystem): | |
| 728 """Implementation of the VersionControlSystem interface for Git.""" | |
| 729 | |
| 730 def __init__(self): | |
| 731 # Map of filename -> hash of base file. | |
| 732 self.base_hashes = {} | |
| 733 | |
| 734 def GenerateDiff(self, extra_args): | |
| 735 # This is more complicated than svn's GenerateDiff because we must convert | |
| 736 # the diff output to include an svn-style "Index:" line as well as record | |
| 737 # the hashes of the base files, so we can upload them along with our diff. | |
| 738 gitdiff = RunShell("git diff", ["--full-index"] + extra_args) | |
| 739 svndiff = [] | |
| 740 filecount = 0 | |
| 741 filename = None | |
| 742 for line in gitdiff.splitlines(): | |
| 743 match = re.match(r"diff --git a/(.*) b/.*$", line) | |
| 744 if match: | |
| 745 filecount += 1 | |
| 746 filename = match.group(1) | |
| 747 svndiff.append("Index: %s\n" % filename) | |
| 748 else: | |
| 749 # The "index" line in a git diff looks like this (long hashes elided): | |
| 750 # index 82c0d44..b2cee3f 100755 | |
| 751 # We want to save the left hash, as that identifies the base file. | |
| 752 match = re.match(r"index (\w+)\.\.", line) | |
| 753 if match: | |
| 754 self.base_hashes[filename] = match.group(1) | |
| 755 svndiff.append(line + "\n") | |
| 756 if not filecount: | |
| 757 ErrorExit("No valid patches found in output from git diff") | |
| 758 return "".join(svndiff) | |
| 759 | |
| 760 def GetUnknownFiles(self): | |
| 761 status = RunShell("git ls-files --others", silent_ok=True) | |
| 762 return status.splitlines() | |
| 763 | |
| 764 def GetBaseFile(self, filename): | |
| 765 hash = self.base_hashes[filename] | |
| 766 if hash == "0" * 40: # All-zero hash indicates no base file. | |
| 767 return ("", "A") | |
| 768 else: | |
| 769 return (RunShell("git show", [hash]), "M") | |
| 716 | 770 |
| 717 | 771 |
| 718 # NOTE: this function is duplicated in engine.py, keep them in sync. | 772 # NOTE: this function is duplicated in engine.py, keep them in sync. |
| 719 def SplitPatch(data): | 773 def SplitPatch(data): |
| 720 """Splits a patch into separate pieces for each file. | 774 """Splits a patch into separate pieces for each file. |
| 721 | 775 |
| 722 Args: | 776 Args: |
| 723 data: A string containing the output of svn diff. | 777 data: A string containing the output of svn diff. |
| 724 | 778 |
| 725 Returns: | 779 Returns: |
| (...skipping 53 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 779 lines = response_body.splitlines() | 833 lines = response_body.splitlines() |
| 780 if not lines or lines[0] != "OK": | 834 if not lines or lines[0] != "OK": |
| 781 StatusUpdate(" --> %s" % response_body) | 835 StatusUpdate(" --> %s" % response_body) |
| 782 sys.exit(False) | 836 sys.exit(False) |
| 783 rv.append([lines[1], patch[0]]) | 837 rv.append([lines[1], patch[0]]) |
| 784 return rv | 838 return rv |
| 785 | 839 |
| 786 | 840 |
| 787 def GuessVCS(): | 841 def GuessVCS(): |
| 788 """Helper to guess the version control system. | 842 """Helper to guess the version control system. |
| 843 | |
| 844 This examines the current directory, guesses which VersionControlSystem | |
| 845 we're using, and returns an instance of the appropriate class. Exit with an | |
| 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
| |
| 789 | 847 |
| 790 Returns: | 848 Returns: |
| 791 A VersionControlSystem instance. Exits if the VCS can't be guessed. | 849 A VersionControlSystem instance. Exits if the VCS can't be guessed. |
| 792 """ | 850 """ |
| 851 # Subversion has a .svn in all working directories. | |
| 793 if os.path.isdir('.svn'): | 852 if os.path.isdir('.svn'): |
| 794 logging.info("Guessed VCS = Subversion") | 853 logging.info("Guessed VCS = Subversion") |
| 795 return SubversionVCS() | 854 return SubversionVCS() |
| 855 | |
| 856 # Git has a command to test if you're in a git tree. | |
| 857 # Try running it, but don't die if we don't have git installed. | |
| 858 try: | |
| 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 | |
| 866 | |
| 796 ErrorExit(("Could not guess version control system. " | 867 ErrorExit(("Could not guess version control system. " |
| 797 "Are you in a working copy directory?")) | 868 "Are you in a working copy directory?")) |
| 798 | 869 |
| 799 | 870 |
| 800 def RealMain(argv, data=None): | 871 def RealMain(argv, data=None): |
| 801 logging.basicConfig(format=("%(asctime).19s %(levelname)s %(filename)s:" | 872 logging.basicConfig(format=("%(asctime).19s %(levelname)s %(filename)s:" |
| 802 "%(lineno)s %(message)s ")) | 873 "%(lineno)s %(message)s ")) |
| 803 os.environ['LC_ALL'] = 'C' | 874 os.environ['LC_ALL'] = 'C' |
| 804 options, args = parser.parse_args(argv[1:]) | 875 options, args = parser.parse_args(argv[1:]) |
| 805 global verbosity | 876 global verbosity |
| 806 verbosity = options.verbose | 877 verbosity = options.verbose |
| 807 if verbosity >= 3: | 878 if verbosity >= 3: |
| 808 logging.getLogger().setLevel(logging.DEBUG) | 879 logging.getLogger().setLevel(logging.DEBUG) |
| 809 elif verbosity >= 2: | 880 elif verbosity >= 2: |
| 810 logging.getLogger().setLevel(logging.INFO) | 881 logging.getLogger().setLevel(logging.INFO) |
| 811 vcs = GuessVCS() | 882 vcs = GuessVCS() |
| 812 if isinstance(vcs, SubversionVCS): | 883 if isinstance(vcs, SubversionVCS): |
| 813 # base field is only allowed for Subversion. | 884 # base field is only allowed for Subversion. |
| 814 # Note: Fetching base files may become deprecated in future releases. | 885 # Note: Fetching base files may become deprecated in future releases. |
| 815 base = vcs.GuessBase(not options.local_base) | 886 base = vcs.GuessBase(not options.local_base) |
| 816 else: | 887 else: |
| 817 base = None | 888 base = None |
| 818 if not base and not options.local_base: | 889 if not base and not options.local_base: |
| 819 # TODO(andi): Enable local_base for other VCS by default. | 890 # TODO(andi): Enable local_base for other VCS by default. |
| 820 # 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
| |
| 821 # condition. | 892 # condition. |
| 822 ErrorExit("Use '--local_base' to upload base files.") | 893 ErrorExit("Use '--local_base' to upload base files.") |
| 823 if not options.assume_yes: | 894 if not options.assume_yes: |
| 824 vcs.CheckForUnknownFiles() | 895 vcs.CheckForUnknownFiles() |
| 825 if not data: | 896 if not data: |
| 826 data = vcs.GenerateDiff(args) | 897 data = vcs.GenerateDiff(args) |
| 827 if verbosity >= 1: | 898 if verbosity >= 1: |
| 828 print "Upload server:", options.server, "(change with -s/--server)" | 899 print "Upload server:", options.server, "(change with -s/--server)" |
| 829 if options.issue: | 900 if options.issue: |
| 830 prompt = "Message describing this patch set: " | 901 prompt = "Message describing this patch set: " |
| (...skipping 75 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 906 try: | 977 try: |
| 907 RealMain(sys.argv) | 978 RealMain(sys.argv) |
| 908 except KeyboardInterrupt: | 979 except KeyboardInterrupt: |
| 909 print | 980 print |
| 910 StatusUpdate("Interrupted.") | 981 StatusUpdate("Interrupted.") |
| 911 sys.exit(1) | 982 sys.exit(1) |
| 912 | 983 |
| 913 | 984 |
| 914 if __name__ == "__main__": | 985 if __name__ == "__main__": |
| 915 main() | 986 main() |
| OLD | NEW |