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

Delta Between Two Patch Sets: static/upload.py

Issue 2602: git support (Closed) SVN Base: http://rietveld.googlecode.com/svn/trunk/
Left Patch Set: retry with fixed GetBaseFile Created 1 year, 3 months ago
Right 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:
Left: Side by side diff | Download
Right: Side by side diff | Download
« no previous file | no next file » | Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
LEFTRIGHT
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
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
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
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
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
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()
LEFTRIGHT
« no previous file | no next file » | Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Toggle Comments ('s')

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