Make `git map-branches` detect and report cycles

I just made a bunch of branches disappear from `git map-branches` by
creating a big cycle (none of them were a root, thus none was listed at
all). This CL makes `git map-branches` check for such cycles and report
them as warnings.

R=iannucci@chromium.org

Change-Id: I723b327282a1c90bc9da5438777653e48f8bfd1d
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/tools/depot_tools/+/1528985
Auto-Submit: Clemens Hammacher <clemensh@chromium.org>
Reviewed-by: Robbie Iannucci <iannucci@chromium.org>
Commit-Queue: Robbie Iannucci <iannucci@chromium.org>
diff --git a/git_map_branches.py b/git_map_branches.py
index 00e204b..290f90f 100755
--- a/git_map_branches.py
+++ b/git_map_branches.py
@@ -150,6 +150,8 @@
         continue
 
       parent = branch_info.upstream
+      if self.__check_cycle(branch):
+        continue
       if not self.__branches_info[parent]:
         branch_upstream = upstream(branch)
         # If git can't find the upstream, mark the upstream as gone.
@@ -174,6 +176,20 @@
       no_branches.append('No User Branches')
       self.output.append(no_branches)
 
+  def __check_cycle(self, branch):
+    # Maximum length of the cycle is `num_branches`. This limit avoids running
+    # into a cycle which does *not* contain `branch`.
+    num_branches = len(self.__branches_info)
+    cycle = [branch]
+    while len(cycle) < num_branches and self.__branches_info[cycle[-1]]:
+      parent = self.__branches_info[cycle[-1]].upstream
+      cycle.append(parent)
+      if parent == branch:
+        print >> sys.stderr, 'Warning: Detected cycle in branches: {}'.format(
+            ' -> '.join(cycle))
+        return True
+    return False
+
   def __is_invalid_parent(self, parent):
     return not parent or parent in self.__gone_branches