Script for syncing all branches in a chain of CLs

Change-Id: I1852e22b043ce2c8ff4634d725157731686742ce
diff --git a/tools/git_sync_cl_chain.py b/tools/git_sync_cl_chain.py
new file mode 100755
index 0000000..97dcfb7
--- /dev/null
+++ b/tools/git_sync_cl_chain.py
@@ -0,0 +1,125 @@
+#!/usr/bin/env python
+# Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
+# for details. All rights reserved. Use of this source code is governed by a
+# BSD-style license that can be found in the LICENSE file.
+
+# Script that automatically pulls and uploads all upstream direct and indirect
+# branches into the current branch.
+#
+# Example:
+#
+#   $ git branch -vv
+#   * feature_final    xxxxxxxxx [feature_prereq_c: ...] ...
+#     feature_prereq_c xxxxxxxxx [feature_prereq_b: ...] ...
+#     feature_prereq_b xxxxxxxxx [feature_prereq_a: ...] ...
+#     feature_prereq_a xxxxxxxxx [master: ...] ...
+#     master           xxxxxxxxx [origin/master] ...
+#
+# Executing `git_sync_cl_chain.py -m <message>` causes the following chain of
+# commands to be executed:
+#
+#   $ git checkout feature_prereq_a; git pull; git cl upload -m <message>
+#   $ git checkout feature_prereq_b; git pull; git cl upload -m <message>
+#   $ git checkout feature_prereq_c; git pull; git cl upload -m <message>
+#   $ git checkout feature_final;    git pull; git cl upload -m <message>
+
+import optparse
+import os
+import sys
+
+import defines
+import utils
+
+REPO_ROOT = defines.REPO_ROOT
+
+class Repo(object):
+  def __init__(self, name, is_current, upstream):
+    self.name = name
+    self.is_current = is_current
+    self.upstream = upstream
+
+def ParseOptions(argv):
+  result = optparse.OptionParser()
+  result.add_option('--message', '-m', help='Message for patchset')
+  result.add_option('--rebase',
+                    help='To use `git pull --rebase` instead of `git pull`',
+                    action='store_true')
+  (options, args) = result.parse_args(argv)
+  print(options)
+  assert options.message, 'A message for the patchset is required.'
+  assert len(args) == 0
+  return options
+
+def main(argv):
+  options = ParseOptions(argv)
+  rebase_args = ['--rebase'] if options.rebase else []
+  with utils.ChangedWorkingDirectory(REPO_ROOT, quiet=True):
+    branches = [
+        parse(line)
+        for line in utils.RunCmd(['git', 'branch', '-vv'], quiet=True)]
+
+    current_branch = None
+    for branch in branches:
+      if branch.is_current:
+        current_branch = branch
+        break
+    assert current_branch is not None
+
+    if current_branch.upstream == None:
+      print('Nothing to sync')
+      return
+
+    stack = []
+    while True:
+      stack.append(current_branch)
+      if current_branch.upstream is None or current_branch.upstream == 'master':
+        break
+      current_branch = get_branch_with_name(current_branch.upstream, branches)
+
+    while len(stack) > 0:
+      branch = stack.pop()
+      print('Syncing ' + branch.name)
+      utils.RunCmd(['git', 'checkout', branch.name], quiet=True)
+      utils.RunCmd(['git', 'pull'] + rebase_args, quiet=True)
+      utils.RunCmd(['git', 'cl', 'upload', '-m', options.message], quiet=True)
+
+def get_branch_with_name(name, branches):
+  for branch in branches:
+    if branch.name == name:
+      return branch
+  return None
+
+# Parses a line from the output of `git branch -vv`.
+#
+# Example output ('*' denotes the current branch):
+#
+#   $ git branch -vv
+#   * feature_final    xxxxxxxxx [feature_prereq_c: ...] ...
+#     feature_prereq_c xxxxxxxxx [feature_prereq_b: ...] ...
+#     feature_prereq_b xxxxxxxxx [feature_prereq_a: ...] ...
+#     feature_prereq_a xxxxxxxxx [master: ...] ...
+#     master           xxxxxxxxx [origin/master] ...
+def parse(line):
+  is_current = False
+  if line.startswith('*'):
+    is_current = True
+    line = line[1:].lstrip()
+  else:
+    line = line.lstrip()
+
+  name_end_index = line.index(' ')
+  name = line[:name_end_index]
+  line = line[name_end_index:].lstrip()
+
+  if ('[') not in line or ':' not in line:
+    return Repo(name, is_current, None)
+
+  upstream_start_index = line.index('[')
+  line = line[upstream_start_index+1:]
+  upstream_end_index = line.index(':')
+  upstream = line[:upstream_end_index]
+
+  return Repo(name, is_current, upstream)
+
+if __name__ == '__main__':
+  sys.exit(main(sys.argv[1:]))