Fix "git rebase-update" when multiple branch are stale.

Fix a bug that left branches tracking a dead branch if both their parent
and grand-parent were left with no changes after a "rebase-update" step.

Given the following initial state:
  $ git map-branches -v
  origin/master
    a
      b
        c *        [ ahead 1 ]

without this patch, a "git rebase-update" on this tree state would
leave the branch "c" as tracking a non-existing branch "a":

  $ git recursive-rebase
  a up-to-date
  b up-to-date
  c up-to-date
  Reparented c to track a (was tracking b)
  Deleted branch b (was 448d1da).
  Deleted branch a (was 448d1da).
  $ git map-branches -v
  {a:GONE}
    c *

with the patch, we record that the branch "c" is tracking must be
updated twice and we end up in a state were "c" is correctly tracking
"origin/master":

  $ git recursive-rebase
  a up-to-date
  b up-to-date
  c up-to-date
  Reparented c to track origin/master (was tracking b)
  Deleted branch b (was 448d1da).
  Deleted branch a (was 448d1da).
  $ git map-branches -v
  origin/master
    c *            [ ahead 1 ]

BUG=456806

Review URL: https://codereview.chromium.org/1482753002

git-svn-id: svn://svn.chromium.org/chrome/trunk/tools/depot_tools@297792 0039d316-1c4b-4281-b951-d872f2087c98
diff --git a/git_rebase_update.py b/git_rebase_update.py
index 992bb19..9d5755e 100755
--- a/git_rebase_update.py
+++ b/git_rebase_update.py
@@ -87,32 +87,54 @@
   tag_set = git.tags()
   ensure_root_checkout = git.once(lambda: git.run('checkout', git.root()))
 
-  deletions = set()
+  deletions = {}
+  reparents = {}
   downstreams = collections.defaultdict(list)
   for branch, parent in git.topo_iter(branch_tree, top_down=False):
     downstreams[parent].append(branch)
 
+    # If branch and parent have the same state, then branch has to be marked
+    # for deletion and its children and grand-children reparented to parent.
     if git.hash_one(branch) == git.hash_one(parent):
       ensure_root_checkout()
 
       logging.debug('branch %s merged to %s', branch, parent)
 
+      # Mark branch for deletion while remembering the ordering, then add all
+      # its children as grand-children of its parent and record reparenting
+      # information if necessary.
+      deletions[branch] = len(deletions)
+
       for down in downstreams[branch]:
         if down in deletions:
           continue
 
-        if parent in tag_set:
-          git.set_branch_config(down, 'remote', '.')
-          git.set_branch_config(down, 'merge', 'refs/tags/%s' % parent)
-          print ('Reparented %s to track %s [tag] (was tracking %s)'
-                 % (down, parent, branch))
+        # Record the new and old parent for down, or update such a record
+        # if it already exists. Keep track of the ordering so that reparenting
+        # happen in topological order.
+        downstreams[parent].append(down)
+        if down not in reparents:
+          reparents[down] = (len(reparents), parent, branch)
         else:
-          git.run('branch', '--set-upstream-to', parent, down)
-          print ('Reparented %s to track %s (was tracking %s)'
-                 % (down, parent, branch))
+          order, _, old_parent = reparents[down]
+          reparents[down] = (order, parent, old_parent)
 
-      deletions.add(branch)
-      print git.run('branch', '-d', branch)
+  # Apply all reparenting recorded, in order.
+  for branch, value in sorted(reparents.iteritems(), key=lambda x:x[1][0]):
+    _, parent, old_parent = value
+    if parent in tag_set:
+      git.set_branch_config(branch, 'remote', '.')
+      git.set_branch_config(branch, 'merge', 'refs/tags/%s' % parent)
+      print ('Reparented %s to track %s [tag] (was tracking %s)'
+             % (branch, parent, old_parent))
+    else:
+      git.run('branch', '--set-upstream-to', parent, branch)
+      print ('Reparented %s to track %s (was tracking %s)'
+             % (branch, parent, old_parent))
+
+  # Apply all deletions recorded, in order.
+  for branch, _ in sorted(deletions.iteritems(), key=lambda x: x[1]):
+    print git.run('branch', '-d', branch)
 
 
 def rebase_branch(branch, parent, start_hash):