Support Gerrit2's ssh:// based upload

In Gerrit2 uploads are sent over "git push ssh://...", as this
is a more efficient transport and is easier to code from external
scripts and/or direct command line usage by an end-user.

Gerrit1's HTTP POST based format is assumed if the review server
does not have the /ssh_info URL available on it.

Signed-off-by: Shawn O. Pearce <sop@google.com>
diff --git a/git_config.py b/git_config.py
index 9ddb2ed..ed5a44a 100644
--- a/git_config.py
+++ b/git_config.py
@@ -16,7 +16,8 @@
 import os
 import re
 import sys
-from error import GitError
+from urllib2 import urlopen, HTTPError
+from error import GitError, UploadError
 from git_command import GitCommand
 
 R_HEADS = 'refs/heads/'
@@ -261,6 +262,45 @@
     self.projectname = self._Get('projectname')
     self.fetch = map(lambda x: RefSpec.FromString(x),
                      self._Get('fetch', all=True))
+    self._review_protocol = None
+
+  @property
+  def ReviewProtocol(self):
+    if self._review_protocol is None:
+      if self.review is None:
+        return None
+
+      u = self.review
+      if not u.startswith('http:') and not u.startswith('https:'):
+        u = 'http://%s' % u
+      if not u.endswith('/'):
+        u += '/'
+      u += 'ssh_info'
+
+      try:
+        info = urlopen(u).read()
+        if info == 'NOT_AVAILABLE':
+          raise UploadError('Upload over ssh unavailable')
+
+        self._review_protocol = 'ssh'
+        self._review_host = info.split(" ")[0]
+        self._review_port = info.split(" ")[1]
+
+      except HTTPError, e:
+        if e.code == 404:
+          self._review_protocol = 'http-post'
+        else:
+          raise UploadError('Cannot guess Gerrit version')
+    return self._review_protocol
+
+  def SshReviewUrl(self, userEmail):
+    if self.ReviewProtocol != 'ssh':
+      return None
+    return 'ssh://%s@%s:%s/%s' % (
+      userEmail.split("@")[0],
+      self._review_host,
+      self._review_port,
+      self.projectname)
 
   def ToLocal(self, rev):
     """Convert a remote revision string to something we have locally.
diff --git a/project.py b/project.py
index 7743ca1..5d036c3 100644
--- a/project.py
+++ b/project.py
@@ -46,6 +46,8 @@
 def not_rev(r):
   return '^' + r
 
+def sq(r):
+  return "'" + r.replace("'", "'\''") + "'"
 
 hook_list = None
 def repo_hooks():
@@ -475,33 +477,58 @@
     if not dest_branch.startswith(R_HEADS):
       dest_branch = R_HEADS + dest_branch
 
-    base_list = []
-    for name, id in self._allrefs.iteritems():
-      if branch.remote.WritesTo(name):
-        base_list.append(not_rev(name))
-    if not base_list:
-      raise GitError('no base refs, cannot upload %s' % branch.name)
-
     if not branch.remote.projectname:
       branch.remote.projectname = self.name
       branch.remote.Save()
 
-    print >>sys.stderr, ''
-    _info("Uploading %s to %s:", branch.name, self.name)
-    try:
-      UploadBundle(project = self,
-                   server = branch.remote.review,
-                   email = self.UserEmail,
-                   dest_project = branch.remote.projectname,
-                   dest_branch = dest_branch,
-                   src_branch = R_HEADS + branch.name,
-                   bases = base_list,
-                   people = people,
-                   replace_changes = replace_changes)
-    except proto_client.ClientLoginError:
-      raise UploadError('Login failure')
-    except urllib2.HTTPError, e:
-      raise UploadError('HTTP error %d' % e.code)
+    if branch.remote.ReviewProtocol == 'http-post':
+      base_list = []
+      for name, id in self._allrefs.iteritems():
+        if branch.remote.WritesTo(name):
+          base_list.append(not_rev(name))
+      if not base_list:
+        raise GitError('no base refs, cannot upload %s' % branch.name)
+
+      print >>sys.stderr, ''
+      _info("Uploading %s to %s:", branch.name, self.name)
+      try:
+        UploadBundle(project = self,
+                     server = branch.remote.review,
+                     email = self.UserEmail,
+                     dest_project = branch.remote.projectname,
+                     dest_branch = dest_branch,
+                     src_branch = R_HEADS + branch.name,
+                     bases = base_list,
+                     people = people,
+                     replace_changes = replace_changes)
+      except proto_client.ClientLoginError:
+        raise UploadError('Login failure')
+      except urllib2.HTTPError, e:
+        raise UploadError('HTTP error %d' % e.code)
+
+    elif branch.remote.ReviewProtocol == 'ssh':
+      if dest_branch.startswith(R_HEADS):
+        dest_branch = dest_branch[len(R_HEADS):]
+
+      rp = ['gerrit receive-pack']
+      for e in people[0]:
+        rp.append('--reviewer=%s' % sq(e))
+      for e in people[1]:
+        rp.append('--cc=%s' % sq(e))
+
+      cmd = ['push']
+      cmd.append('--receive-pack=%s' % " ".join(rp))
+      cmd.append(branch.remote.SshReviewUrl(self.UserEmail))
+      cmd.append('%s:refs/for/%s' % (R_HEADS + branch.name, dest_branch))
+      if replace_changes:
+        for change_id,commit_id in replace_changes.iteritems():
+          cmd.append('%s:refs/changes/%s/new' % (commit_id, change_id))
+      if GitCommand(self, cmd, bare = True).Wait() != 0:
+        raise UploadError('Upload failed')
+
+    else:
+        raise UploadError('Unsupported protocol %s' \
+          % branch.remote.review)
 
     msg = "posted to %s for %s" % (branch.remote.review, dest_branch)
     self.bare_git.UpdateRef(R_PUB + branch.name,