bisect-kit: Improve strategy to get actions between branched CROS specs

When `prev_spec` and `next_spec` not on the same branch, we should
follow the branch stated in `next_spec` instead of `prev_spec` to
build the action list.

BUG=b:147575652
TEST=python3 -m unittest bisect_kit/cros_util_test.py
TEST=python3 -m unittest bisect_kit/git_util_test.py
TEST=./bisect_cr_localbuild_internal.py init --old 81.0.3995.0 --new 81.0.3999.0
TEST=./bisect_cros_version.py init --board samus-kernelnext --old R80-12739.10.0 --new R80-12739.11.0
TEST=./bisect_cros_repo.py init --board samus-kernelnext --old R80-12739.0.0 --new R80-12739.1.0
TEST=./bisect_cros_repo.py init --board samus-kernelnext --old R80-12739.10.0 --new R80-12739.11.0
TEST=./bisect_cros_repo.py init --board samus-kernelnext --old R80-12739.0.0 --new R80-12739.11.0
TEST=./bisect_cros_repo.py init --board samus-kernelnext --old R80-12730.0.0 --new R80-12739.0.0
TEST=./bisect_cros_repo.py init --board samus-kernelnext --old R80-12738.0.0-24800 --new R80-12739.11.0

Change-Id: I055527df79903c7ddc879024f2c9bf1dc8fa42b5
diff --git a/bisect_kit/codechange.py b/bisect_kit/codechange.py
index 3e4f3a1..900a5a0 100644
--- a/bisect_kit/codechange.py
+++ b/bisect_kit/codechange.py
@@ -585,9 +585,26 @@
     # a local checkout), there is no need to convert the name.
     return git_util.get_rev_by_time(git_root, timestamp, spec[path].at)
 
-  def get_actions_between_two_commit(self, spec, path, old, new):
+  def get_actions_between_two_commit(self,
+                                     spec,
+                                     path,
+                                     old,
+                                     new,
+                                     ignore_not_ancestor=False):
     git_root = self.cached_git_root(spec[path].repo_url)
     result = []
+    # not in the same branch, regard as an atomic operation
+    # this situation happens when
+    # 1. new is branched from old and
+    # 2. commit timestamp is not reliable(i.e. commit time != merged time)
+    # old and new might not have ancestor relation
+    if ignore_not_ancestor and old != new and not git_util.is_ancestor_commit(
+        git_root, old, new):
+      timestamp = git_util.get_commit_time(git_root, new)
+      result.append(
+          GitCheckoutCommit(timestamp, path, spec[path].repo_url, new))
+      return result
+
     for timestamp, git_rev in git_util.list_commits_between_commits(
         git_root, old, new):
       result.append(
@@ -637,6 +654,15 @@
     groups = []
     last_group = ActionGroup(next_float.timestamp)
     is_removed = set()
+
+    # `branch_between_float_specs` is currently a chromeos-only logic,
+    # and branch behavior is not verified for android and chrome now.
+    is_chromeos_branched = False
+    if hasattr(self.spec_manager, 'branch_between_float_specs'
+              ) and self.spec_manager.branch_between_float_specs(
+                  prev_float, next_float):
+      is_chromeos_branched = True
+
     # Sort alphabetically, so parent directories are handled before children
     # directories.
     for path in sorted(set(prev_float.entries) | set(next_float.entries)):
@@ -653,20 +679,43 @@
                        next_at))
         continue
 
-      # Existing path is floating, enumerates commits until next spec.
-      #
-      #                prev_at                 till_at
-      # prev branch ---> o --------> o --------> o --------> o --------> ...
-      #                       ^                        ^
-      #                 prev_float.timestamp        next_float.timestamp
+      # Existing path is floating.
       if not prev_float[path].is_static():
+        # Enumerates commits until next spec. Get `prev_at` and `till_at`
+        # by prev_float and next_float's timestamp.
+        #
+        # 1. Non-branched case:
+        #
+        #                prev_at                 till_at
+        # prev branch ---> o --------> o --------> o --------> o --------> ...
+        #                       ^                        ^
+        #                 prev_float.timestamp        next_float.timestamp
+        #
+        # building an image between prev_at and till_at should follow
+        # prev_float's spec.
+        #
+        # 2. Branched case:
+        #
+        #                     till_at
+        #              /------->o---------->
+        #             /            ^ next_float.timestamp
+        #            / prev_at
+        # ---------->o---------------------->
+        #                ^prev_float.timestamp
+        #
+        # building an image between prev_at and till_at should follow
+        # next_float's spec.
+        #
         prev_at = self.code_storage.get_rev_by_time(prev_float, path,
                                                     prev_float.timestamp)
-        till_at = self.code_storage.get_rev_by_time(prev_float, path,
-                                                    next_float.timestamp)
-
+        if is_chromeos_branched:
+          till_at = self.code_storage.get_rev_by_time(next_float, path,
+                                                      next_float.timestamp)
+        else:
+          till_at = self.code_storage.get_rev_by_time(prev_float, path,
+                                                      next_float.timestamp)
         actions = self.code_storage.get_actions_between_two_commit(
-            prev_float, path, prev_at, till_at)
+            prev_float, path, prev_at, till_at, ignore_not_ancestor=True)
 
         # Assume commits with the same timestamp as manifest/DEPS change are
         # atomic.