Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code | Sign in
(28)

Side by Side Diff: static/upload.py

Issue 2602: git support (Closed) SVN Base: http://rietveld.googlecode.com/svn/trunk/
Patch Set: try two Created 1 year, 3 months ago
Use n/p to move between diff chunks; N/P to move between comments. Please Sign in to add in-line comments.
Jump to:
View unified diff | Download patch
« no previous file | no next file » | Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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
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
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
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
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()
OLDNEW
« no previous file | no next file » | Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')

Powered by Google App Engine
RSS Feeds Recent Issues | This issue
This is Rietveld r497