Update prebuilt.py to upload correctly when tracking branch is a revision.

This contains the following two commits.

1. Update prebuilt.py to sync to latest revision before pushing.

This fixes a bug where prebuilt.py is unable to push if it is using
manifest-versions to sync to an old revision. This bug occurs because
manifest-versions is pointing at an old version of the repository so
the prebuilt pusher is never able to push.

BUG=chrome-os-partner:3959
TEST=Test with --dry-run push. Also run unit test suite.

Reviewed-on: http://gerrit.chromium.org/gerrit/1239
Reviewed-by: Chris Sosa <sosa@chromium.org>
Tested-by: David James <davidjames@chromium.org>
(cherry picked from commit 1b6e67aef79696d74bcee3d2fce252c8397e592a)

2. Update prebuilt.py to upload correctly when tracking branch is a revision.

If the tracking branch is a revision, we shouldn't push there. We need to
look in the manifest for the default branch and use that instead.

BUG=chrome-os-partner:4169
TEST=Run prebuilt.py when tracking branch is a revision and verify it pushes
     to the right branch (i.e. the default branch defined in the manifest).

Reviewed-on: http://gerrit.chromium.org/gerrit/1702
Reviewed-by: Nick Sanders <nsanders@chromium.org>
Tested-by: David James <davidjames@chromium.org>
(cherry picked from commit d611f3d89a40ca74bb9218f341259a9a1b6f351b)

Change-Id: I553fc0931052ff51a5dcfd6fd5862da7115b4cb2
Reviewed-on: http://gerrit.chromium.org/gerrit/1699
Reviewed-by: Chris Sosa <sosa@chromium.org>
Tested-by: David James <davidjames@chromium.org>
diff --git a/buildbot/prebuilt.py b/buildbot/prebuilt.py
index 23180e0..2b874a6 100755
--- a/buildbot/prebuilt.py
+++ b/buildbot/prebuilt.py
@@ -1,5 +1,5 @@
 #!/usr/bin/python
-# Copyright (c) 2010 The Chromium OS Authors. All rights reserved.
+# Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
@@ -17,8 +17,7 @@
   sys.path.append(constants.SOURCE_ROOT)
 
 from chromite.lib import cros_build_lib
-from chromite.lib.binpkg import (GrabLocalPackageIndex, GrabRemotePackageIndex,
-                                 PackageIndex)
+from chromite.lib.binpkg import (GrabLocalPackageIndex, GrabRemotePackageIndex)
 """
 This script is used to upload host prebuilts as well as board BINHOSTS.
 
@@ -80,10 +79,6 @@
   """Raised when a function finds an unknown board format."""
   pass
 
-class GitPushFailed(Exception):
-  """Raised when a git push failed after retry."""
-  pass
-
 
 def UpdateLocalFile(filename, value, key='PORTAGE_BINHOST'):
   """Update the key in file with the value passed.
@@ -131,28 +126,6 @@
   new_file_fh.close()
 
 
-def RevGitPushWithRetry(retries=5):
-  """Repo sync and then push git changes in flight.
-
-    Args:
-      retries: The number of times to retry before giving up, default: 5
-
-    Raises:
-      GitPushFailed if push was unsuccessful after retries
-  """
-  for retry in range(1, retries + 1):
-    try:
-      cros_build_lib.RunCommand(['repo', 'sync', '.'])
-      cros_build_lib.RunCommand(['git', 'push'])
-      break
-    except cros_build_lib.RunCommandError:
-      if retry < retries:
-        print 'Error pushing changes trying again (%s/%s)' % (retry, retries)
-        time.sleep(5 * retry)
-  else:
-    raise GitPushFailed('Failed to push change after %s retries' % retries)
-
-
 def RevGitFile(filename, value, retries=5, key='PORTAGE_BINHOST'):
   """Update and push the git file.
 
@@ -165,30 +138,30 @@
         (Default: PORTAGE_BINHOST)
   """
   prebuilt_branch = 'prebuilt_branch'
-  old_cwd = os.getcwd()
-  os.chdir(os.path.dirname(filename))
-
-  commit = cros_build_lib.RunCommand(['git', 'rev-parse', 'HEAD'],
+  cwd = os.path.abspath(os.path.dirname(filename))
+  commit = cros_build_lib.RunCommand(['git', 'rev-parse', 'HEAD'], cwd=cwd,
                                      redirect_stdout=True).output.rstrip()
-  cros_build_lib.RunCommand(['git', 'remote', 'update'])
-  cros_build_lib.RunCommand(['repo', 'start', prebuilt_branch, '.'])
   git_ssh_config_cmd = [
       'git',
       'config',
-      'url.ssh://git@gitrw.chromium.org:9222.pushinsteadof',
-      'http://git.chromium.org/git' ]
-  cros_build_lib.RunCommand(git_ssh_config_cmd)
+      'url.ssh://gerrit.chromium.org:29418.pushinsteadof',
+      'http://git.chromium.org']
+  cros_build_lib.RunCommand(git_ssh_config_cmd, cwd=cwd)
+  cros_build_lib.RunCommand(['git', 'remote', 'update'], cwd=cwd)
+  cros_build_lib.RunCommand(['repo', 'start', prebuilt_branch, '.'], cwd=cwd)
   description = 'Update %s="%s" in %s' % (key, value, filename)
   print description
   try:
     UpdateLocalFile(filename, value, key)
-    cros_build_lib.RunCommand(['git', 'config', 'push.default', 'tracking'])
-    cros_build_lib.RunCommand(['git', 'commit', '-am', description])
-    RevGitPushWithRetry(retries)
+    cros_build_lib.RunCommand(['git', 'config', 'push.default', 'tracking'],
+                              cwd=cwd)
+    cros_build_lib.RunCommand(['git', 'add', filename], cwd=cwd)
+    cros_build_lib.RunCommand(['git', 'commit', '-m', description], cwd=cwd)
+    cros_build_lib.GitPushWithRetry(prebuilt_branch, cwd=cwd)
   finally:
-    cros_build_lib.RunCommand(['repo', 'abandon', 'prebuilt_branch', '.'])
-    cros_build_lib.RunCommand(['git', 'checkout', commit])
-    os.chdir(old_cwd)
+    cros_build_lib.RunCommand(['git', 'checkout', commit], cwd=cwd)
+    cros_build_lib.RunCommand(['repo', 'abandon', 'prebuilt_branch', '.'],
+                              cwd=cwd)
 
 
 def GetVersion():
@@ -424,7 +397,7 @@
     config_file = file(path, 'w')
     config_file.close()
   UpdateLocalFile(path, value, key)
-  cros_build_lib.RunCommand(['git', 'add', filename],  cwd=cwd)
+  cros_build_lib.RunCommand(['git', 'add', filename], cwd=cwd)
   description = 'Update %s=%s in %s' % (key, value, filename)
   cros_build_lib.RunCommand(['git', 'commit', '-m', description], cwd=cwd)
 
@@ -676,6 +649,8 @@
     usage(parser, 'Error: you need provide a chroot path')
   if not options.upload:
     usage(parser, 'Error: you need to provide an upload location using -u')
+  if args:
+    usage(parser, 'Error: invalid arguments passed to prebuilt.py: %r' % args)
 
 
   if options.upload_board_tarball and not options.upload.startswith('gs://'):
diff --git a/lib/cros_build_lib.py b/lib/cros_build_lib.py
index 9e35dbc..37a5673 100644
--- a/lib/cros_build_lib.py
+++ b/lib/cros_build_lib.py
@@ -10,11 +10,15 @@
 import signal
 import subprocess
 import sys
+import time
 from terminal import Color
 
 
 _STDOUT_IS_TTY = hasattr(sys.stdout, 'isatty') and sys.stdout.isatty()
 
+class GitPushFailed(Exception):
+  """Raised when a git push failed after retry."""
+  pass
 
 class CommandResult(object):
   """An object to store various attributes of a child process."""
@@ -326,6 +330,64 @@
   return new_path
 
 
+def GetPushBranch(branch, cwd):
+  """Gets the appropriate push branch for the specified branch / directory.
+
+  If branch has a valid tracking branch, we should push to that branch. If
+  the tracking branch is a revision, we can't push to that, so we should look
+  at the default branch from the manifest.
+
+  Args:
+    branch: Branch to examine for tracking branch.
+    cwd: Directory to look in.
+  """
+  info = {}
+  for key in ('remote', 'merge'):
+    cmd = ['git', 'config', 'branch.%s.%s' % (branch, key)]
+    info[key] = RunCommand(cmd, redirect_stdout=True, cwd=cwd).output.strip()
+  if not info['merge'].startswith('refs/heads/'):
+    output = RunCommand(['repo', 'manifest', '-o', '-'], redirect_stdout=True,
+                        cwd=cwd).output
+    m = re.search(r'<default[^>]*revision="(refs/heads/[^"]*)"', output)
+    assert m
+    info['merge'] = m.group(1)
+  assert info['merge'].startswith('refs/heads/')
+  return info['remote'], info['merge'].replace('refs/heads/', '')
+
+
+def GitPushWithRetry(branch, cwd, dryrun=False, retries=5):
+  """General method to push local git changes.
+
+    Args:
+      branch: Local branch to push.  Branch should have already been created
+        with a local change committed ready to push to the remote branch.  Must
+        also already be checked out to that branch.
+      cwd: Directory to push in.
+      dryrun: Git push --dry-run if set to True.
+      retries: The number of times to retry before giving up, default: 5
+
+    Raises:
+      GitPushFailed if push was unsuccessful after retries
+  """
+  remote, push_branch = GetPushBranch(branch, cwd)
+  for retry in range(1, retries + 1):
+    try:
+      RunCommand(['git', 'remote', 'update'], cwd=cwd)
+      RunCommand(['git', 'rebase', '%s/%s' % (remote, push_branch)], cwd=cwd)
+      push_command = ['git', 'push', remote, '%s:%s' % (branch, push_branch)]
+      if dryrun:
+        push_command.append('--dry-run')
+
+      RunCommand(push_command, cwd=cwd)
+      break
+    except RunCommandError:
+      if retry < retries:
+        print 'Error pushing changes trying again (%s/%s)' % (retry, retries)
+        time.sleep(5 * retry)
+  else:
+    raise GitPushFailed('Failed to push change after %s retries' % retries)
+
+
 def GetCallerName():
   """Returns the name of the calling module with __main__."""
   top_frame = inspect.stack()[-1][0]