[Updater] Optionally commit and upload change

Test: ./updater.sh update --force --branch_and_commit --push_change googletest
Test: aosp/889542
Change-Id: I1025e2ded65000ce34e2f02f84052e7871e425d3
diff --git a/external_updater.py b/external_updater.py
index 0ee2e86..bc464ac 100644
--- a/external_updater.py
+++ b/external_updater.py
@@ -26,9 +26,10 @@
 
 from google.protobuf import text_format    # pylint: disable=import-error
 
-import fileutils
 from git_updater import GitUpdater
 from github_archive_updater import GithubArchiveUpdater
+import fileutils
+import git_utils
 import updater_utils
 
 
@@ -79,6 +80,11 @@
     return updater.get_current_version() != updater.get_latest_version()
 
 
+def _message_for_calledprocesserror(error):
+    return '\n'.join([error.stdout.decode('utf-8'),
+                      error.stderr.decode('utf-8')])
+
+
 def check_update(proj_path):
     """Checks updates for a project. Prints result on console.
 
@@ -104,11 +110,8 @@
                               err))
         return (updater, str(err))
     except subprocess.CalledProcessError as err:
-        msg = 'stdout: {}\nstderr: {}.'.format(
-            err.stdout,
-            err.stderr)
-        print('{} {}.'.format(color_string('Failed.', 'ERROR'),
-                              msg))
+        msg = _message_for_calledprocesserror(err)
+        print('{}\n{}'.format(msg, color_string('Failed.', 'ERROR')))
         return (updater, msg)
 
 
@@ -157,12 +160,42 @@
 
 def update(args):
     """Handler for update command."""
+    try:
+        _do_update(args)
+    except subprocess.CalledProcessError as err:
+        msg = _message_for_calledprocesserror(err)
+        print('{}\n{}'.format(msg, color_string('Failed to upgrade.', 'ERROR')))
 
+
+TMP_BRANCH_NAME = 'tmp_auto_upgrade'
+
+
+def _do_update(args):
     updater, err = check_update(args.path)
     if updater is None:
         return
-    if has_new_version(updater) or args.force:
-        updater.update()
+    if not has_new_version(updater) and not args.force:
+        return
+
+    full_path = fileutils.get_absolute_project_path(args.path)
+    if args.branch_and_commit:
+        git_utils.checkout(full_path, args.remote_name + '/master')
+        try:
+            git_utils.delete_branch(full_path, TMP_BRANCH_NAME)
+        except subprocess.CalledProcessError as err:
+            # Still continue if the branch doesn't exist.
+            pass
+        git_utils.start_branch(full_path, TMP_BRANCH_NAME)
+
+    updater.update()
+
+    if args.branch_and_commit:
+        msg = 'Upgrade {} to {}\n\nTest: None'.format(
+            args.path, updater.get_latest_version())
+        git_utils.commit(full_path, msg)
+
+    if args.push_change:
+        git_utils.push(full_path, args.remote_name)
 
 
 def parse_args():
@@ -201,6 +234,15 @@
         '--force',
         help='Run update even if there\'s no new version.',
         action='store_true')
+    update_parser.add_argument(
+        '--branch_and_commit', action='store_true',
+        help='Starts a new branch and commit changes.')
+    update_parser.add_argument(
+        '--push_change', action='store_true',
+        help='Pushes change to Gerrit.')
+    update_parser.add_argument(
+        '--remote_name', default='aosp', required=False,
+        help='Upstream remote name.')
     update_parser.set_defaults(func=update)
 
     return parser.parse_args()
diff --git a/git_updater.py b/git_updater.py
index 81ef922..81ee93d 100644
--- a/git_updater.py
+++ b/git_updater.py
@@ -135,7 +135,8 @@
             print('{} is {} commits behind of {}.'.format(
                 self.merge_from, len(commits), upstream_branch))
 
-        self._write_metadata(self.proj_path)
         print("Running `git merge {merge_branch}`..."
               .format(merge_branch=self.merge_from))
         git_utils.merge(self.proj_path, self.merge_from)
+        self._write_metadata(self.proj_path)
+        git_utils.add_file(self.proj_path, 'METADATA')
diff --git a/git_utils.py b/git_utils.py
index 67f4500..7b78b21 100644
--- a/git_utils.py
+++ b/git_utils.py
@@ -18,9 +18,10 @@
 import subprocess
 
 
-def _run(cmd, cwd):
+def _run(cmd, cwd, redirect=True):
     """Runs a command with stdout and stderr redirected."""
-    return subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
+    out = subprocess.PIPE if redirect else None
+    return subprocess.run(cmd, stdout=out, stderr=out,
                           check=True, cwd=cwd)
 
 
@@ -117,4 +118,41 @@
 
 def merge(proj_path, branch):
     """Merges a branch."""
-    _run(['git', 'merge', branch], cwd=proj_path)
+    try:
+        out = _run(['git', 'merge', branch, '--no-commit'],
+                   cwd=proj_path)
+    except subprocess.CalledProcessError:
+        # Merge failed. Error is already written to console.
+        subprocess.run(['git', 'merge', '--abort'], cwd=proj_path)
+        raise
+
+
+def add_file(proj_path, file_name):
+    """Stages a file."""
+    _run(['git', 'add', file_name], cwd=proj_path)
+
+
+def delete_branch(proj_path, branch_name):
+    """Force delete a branch."""
+    _run(['git', 'branch', '-D', branch_name], cwd=proj_path)
+
+
+def start_branch(proj_path, branch_name):
+    """Starts a new repo branch."""
+    _run(['repo', 'start', branch_name], cwd=proj_path)
+
+
+def commit(proj_path, message):
+    """Commits changes."""
+    _run(['git', 'commit', '-m', message], cwd=proj_path)
+
+
+def checkout(proj_path, branch_name):
+    """Checkouts a branch."""
+    _run(['git', 'checkout', branch_name], cwd=proj_path)
+
+
+def push(proj_path, remote_name):
+    """Pushes change to remote."""
+    return _run(['git', 'push', remote_name, 'HEAD:refs/for/master'],
+                cwd=proj_path, redirect=False)