Add minimal Gerrit support to 'git cl config' and 'git cl upload'

This is the bare beginnings of Gerrit support for (non-repo) depot_tools,
based on Roland's work.
Differnt from http://codereview.chromium.org/8826015/
 it read codereview.settings and if it has GERRTI_HOST and GERRIT_PORT,
 then "git cl config" configured it for gerrit.
   installs hooks/commit-msg
   git config gerrit.host $GERRIT_HOST
   git config gerrit.port $GERRIT_PORT

 if it has gerrit.host config, "git cl upload" will upload a change
 to gerrit as
   "git push --receive-pack=... origin master"
 it scans description and extract reviewers from R= line.

Review URL: http://codereview.chromium.org/9264065

git-svn-id: svn://svn.chromium.org/chrome/trunk/tools/depot_tools@120276 0039d316-1c4b-4281-b951-d872f2087c98
diff --git a/git_cl.py b/git_cl.py
index c24fdb6..f78839b 100755
--- a/git_cl.py
+++ b/git_cl.py
@@ -147,6 +147,7 @@
     self.viewvc_url = None
     self.updated = False
     self.did_migrate_check = False
+    self.is_gerrit = None
 
   def LazyUpdateIfNeeded(self):
     """Updates the settings from a codereview.settings file, if available."""
@@ -264,6 +265,12 @@
   def GetDefaultCCList(self):
     return self._GetConfig('rietveld.cc', error_ok=True)
 
+  def GetIsGerrit(self):
+    """Return true if this repo is assosiated with gerrit code review system."""
+    if self.is_gerrit is None:
+      self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
+    return self.is_gerrit
+
   def _GetConfig(self, param, **kwargs):
     self.LazyUpdateIfNeeded()
     return RunGit(['config', param], **kwargs).strip()
@@ -604,6 +611,7 @@
 
 def GetCodereviewSettingsInteractively():
   """Prompt the user for settings."""
+  # TODO(ukai): ask code review system is rietveld or gerrit?
   server = settings.GetDefaultServerUrl(error_ok=True)
   prompt = 'Rietveld server (host[:port])'
   prompt += ' [%s]' % (server or DEFAULT_SERVER)
@@ -666,9 +674,9 @@
     content = re.compile(r'^#.*$', re.MULTILINE).sub('', content).strip()
     if not content:
       DieWithError('No CL description, aborting')
-    self._ParseDescription(content)
+    self.ParseDescription(content)
 
-  def _ParseDescription(self, description):
+  def ParseDescription(self, description):
     """Updates the list of reviewers and subject from the description."""
     if not description:
       self.description = description
@@ -723,6 +731,15 @@
   SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
   SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
 
+  if 'GERRIT_HOST' in keyvals and 'GERRIT_PORT' in keyvals:
+    RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
+    RunGit(['config', 'gerrit.port', keyvals['GERRIT_PORT']])
+    # Install the standard commit-msg hook.
+    RunCommand(['scp', '-p', '-P', keyvals['GERRIT_PORT'],
+                '%s:hooks/commit-msg' % keyvals['GERRIT_HOST'],
+                os.path.join(settings.GetRoot(),
+                             '.git', 'hooks', 'commit-msg')])
+
   if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
     #should be of the form
     #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
@@ -863,64 +880,49 @@
   return 0
 
 
-@usage('[args to "git diff"]')
-def CMDupload(parser, args):
-  """upload the current changelist to codereview"""
-  parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
-                    help='bypass upload presubmit hook')
-  parser.add_option('-f', action='store_true', dest='force',
-                    help="force yes to questions (don't prompt)")
-  parser.add_option('-m', dest='message', help='message for patch')
-  parser.add_option('-r', '--reviewers',
-                    help='reviewer email addresses')
-  parser.add_option('--cc',
-                    help='cc email addresses')
-  parser.add_option('--send-mail', action='store_true',
-                    help='send email to reviewer immediately')
-  parser.add_option("--emulate_svn_auto_props", action="store_true",
-                    dest="emulate_svn_auto_props",
-                    help="Emulate Subversion's auto properties feature.")
-  parser.add_option("--desc_from_logs", action="store_true",
-                    dest="from_logs",
-                    help="""Squashes git commit logs into change description and
-                            uses message as subject""")
-  parser.add_option('-c', '--use-commit-queue', action='store_true',
-                    help='tell the commit queue to commit this patchset')
-  (options, args) = parser.parse_args(args)
+def GerritUpload(options, args, cl):
+  """upload the current branch to gerrit."""
+  # We assume the remote called "origin" is the one we want.
+  # It is probably not worthwhile to support different workflows.
+  remote = 'origin'
+  branch = 'master'
+  if options.target_branch:
+    branch = options.target_branch
 
-  # Make sure index is up-to-date before running diff-index.
-  RunGit(['update-index', '--refresh', '-q'], error_ok=True)
-  if RunGit(['diff-index', 'HEAD']):
-    print 'Cannot upload with a dirty tree.  You must commit locally first.'
+  log_desc = CreateDescriptionFromLog(args)
+  if options.reviewers:
+    log_desc += '\nR=' + options.reviewers
+  change_desc = ChangeDescription(options.message, log_desc,
+                                  options.reviewers)
+  change_desc.ParseDescription(log_desc)
+  if change_desc.IsEmpty():
+    print "Description is empty; aborting."
     return 1
 
-  cl = Changelist()
-  if args:
-    base_branch = args[0]
-  else:
-    # Default to diffing against the "upstream" branch.
-    base_branch = cl.GetUpstreamBranch()
-    args = [base_branch + "..."]
+  receive_options = []
+  cc = cl.GetCCList().split(',')
+  if options.cc:
+    cc += options.cc.split(',')
+  cc = filter(None, cc)
+  if cc:
+    receive_options += ['--cc=' + email for email in cc]
+  if change_desc.reviewers:
+    reviewers = filter(None, change_desc.reviewers.split(','))
+    if reviewers:
+      receive_options += ['--reviewer=' + email for email in reviewers]
 
-  if not options.bypass_hooks:
-    hook_results = cl.RunHook(committing=False, upstream_branch=base_branch,
-                              may_prompt=not options.force,
-                              verbose=options.verbose,
-                              author=None)
-    if not hook_results.should_continue():
-      return 1
-    if not options.reviewers and hook_results.reviewers:
-      options.reviewers = hook_results.reviewers
+  git_command = ['push']
+  if receive_options:
+    git_command.append('--receive-pack="git receive-pack %s"' %
+                       ' '.join(receive_options))
+  git_command += [remote, 'HEAD:refs/for/' + branch]
+  RunGit(git_command)
+  # TODO(ukai): parse Change-Id: and set issue number?
+  return 0
 
-  # --no-ext-diff is broken in some versions of Git, so try to work around
-  # this by overriding the environment (but there is still a problem if the
-  # git config key "diff.external" is used).
-  env = os.environ.copy()
-  if 'GIT_EXTERNAL_DIFF' in env:
-    del env['GIT_EXTERNAL_DIFF']
-  subprocess2.call(
-      ['git', 'diff', '--no-ext-diff', '--stat', '-M'] + args, env=env)
 
+def RietveldUpload(options, args, cl):
+  """upload the patch to rietveld."""
   upload_args = ['--assume_yes']  # Don't ask about untracked files.
   upload_args.extend(['--server', cl.GetRietveldServer()])
   if options.emulate_svn_auto_props:
@@ -1002,6 +1004,73 @@
   return 0
 
 
+@usage('[args to "git diff"]')
+def CMDupload(parser, args):
+  """upload the current changelist to codereview"""
+  parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
+                    help='bypass upload presubmit hook')
+  parser.add_option('-f', action='store_true', dest='force',
+                    help="force yes to questions (don't prompt)")
+  parser.add_option('-m', dest='message', help='message for patch')
+  parser.add_option('-r', '--reviewers',
+                    help='reviewer email addresses')
+  parser.add_option('--cc',
+                    help='cc email addresses')
+  parser.add_option('--send-mail', action='store_true',
+                    help='send email to reviewer immediately')
+  parser.add_option("--emulate_svn_auto_props", action="store_true",
+                    dest="emulate_svn_auto_props",
+                    help="Emulate Subversion's auto properties feature.")
+  parser.add_option("--desc_from_logs", action="store_true",
+                    dest="from_logs",
+                    help="""Squashes git commit logs into change description and
+                            uses message as subject""")
+  parser.add_option('-c', '--use-commit-queue', action='store_true',
+                    help='tell the commit queue to commit this patchset')
+  if settings.GetIsGerrit():
+    parser.add_option('--target_branch', dest='target_branch', default='master',
+                      help='target branch to upload')
+  (options, args) = parser.parse_args(args)
+
+  # Make sure index is up-to-date before running diff-index.
+  RunGit(['update-index', '--refresh', '-q'], error_ok=True)
+  if RunGit(['diff-index', 'HEAD']):
+    print 'Cannot upload with a dirty tree.  You must commit locally first.'
+    return 1
+
+  cl = Changelist()
+  if args:
+    # TODO(ukai): is it ok for gerrit case?
+    base_branch = args[0]
+  else:
+    # Default to diffing against the "upstream" branch.
+    base_branch = cl.GetUpstreamBranch()
+    args = [base_branch + "..."]
+
+  if not options.bypass_hooks:
+    hook_results = cl.RunHook(committing=False, upstream_branch=base_branch,
+                              may_prompt=not options.force,
+                              verbose=options.verbose,
+                              author=None)
+    if not hook_results.should_continue():
+      return 1
+    if not options.reviewers and hook_results.reviewers:
+      options.reviewers = hook_results.reviewers
+
+  # --no-ext-diff is broken in some versions of Git, so try to work around
+  # this by overriding the environment (but there is still a problem if the
+  # git config key "diff.external" is used).
+  env = os.environ.copy()
+  if 'GIT_EXTERNAL_DIFF' in env:
+    del env['GIT_EXTERNAL_DIFF']
+  subprocess2.call(
+      ['git', 'diff', '--no-ext-diff', '--stat', '-M'] + args, env=env)
+
+  if settings.GetIsGerrit():
+    return GerritUpload(options, args, cl)
+  return RietveldUpload(options, args, cl)
+
+
 def SendUpstream(parser, args, cmd):
   """Common code for CmdPush and CmdDCommit
 
@@ -1229,6 +1298,7 @@
   issue_arg = args[0]
 
   # TODO(maruel): Use apply_issue.py
+  # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
 
   if re.match(r'\d+', issue_arg):
     # Input is an issue id.  Figure out the URL.