stacked-changes: Skip empty branches.

Changes:
- _UploadAllPrecheck() returns a list of cls to upload that skips any empty commits in the stack.
- If the current branch is empty, we throw an error.
- UploadAllSquashed() now computes the parent to use for the squashed and cherry-pick flow.
  - for cherry-pick it uses the gerrit_squash_hash of the next cl in `cls`. This means it could be the gerrit_squash_hash of the direct ancestor OR the closest non-empty ancestor branch.
  - for multiple squashed commits, the first parent passed in is either:
     - the closest ancestor with a gerrit_squash_hash OR the common ancestor shared with the root of the tree.
- PrepareSquashedCommit() and PrepareCherryPick() now both require a parent passed in.

Bug:1411878, b/265929888
Change-Id: I7dba289defb40ed0464eabdb7e90810353ef155f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/tools/depot_tools/+/4220412
Reviewed-by: Josip Sokcevic <sokcevic@chromium.org>
Commit-Queue: Joanna Wang <jojwang@chromium.org>
Reviewed-by: Gavin Mak <gavinmak@google.com>
diff --git a/git_cl.py b/git_cl.py
index e827a33..29c0326 100755
--- a/git_cl.py
+++ b/git_cl.py
@@ -1001,6 +1001,7 @@
                         'approval', 'disapproval'])
 
 
+# TODO(b/265929888): Change `parent` to `pushed_commit_base`.
 _NewUpload = collections.namedtuple('NewUpload', [
     'reviewers', 'ccs', 'commit_to_push', 'new_last_uploaded_commit', 'parent',
     'change_desc'
@@ -1673,23 +1674,9 @@
     # branch/change.
     return refspec_opts
 
-  def PrepareSquashedCommit(self, options, parent=None, end_commit=None):
-    # type: (optparse.Values, Optional[str], Optional[str]) -> _NewUpload()
+  def PrepareSquashedCommit(self, options, parent, end_commit=None):
+    # type: (optparse.Values, str, Optional[str]) -> _NewUpload()
     """Create a squashed commit to upload."""
-    if parent is None:
-      origin, upstream_branch_ref = self.FetchUpstreamTuple(self.GetBranch())
-      upstream_branch = scm.GIT.ShortBranchName(upstream_branch_ref)
-      if origin == '.':
-        # upstream is another local branch.
-        # Assume we want to upload from upstream's last upload.
-        parent = scm.GIT.GetBranchConfig(settings.GetRoot(), upstream_branch,
-                                         GERRIT_SQUASH_HASH_CONFIG_KEY)
-        assert parent, ('upstream branch %s not configured correctly. '
-                        'Could not fetch latest gerrit upload from git '
-                        'config.')
-      else:
-        # upstream is the root of the tree.
-        parent = self.GetCommonAncestorWithUpstream()
 
     if end_commit is None:
       end_commit = RunGit(['rev-parse', self.branchref]).strip()
@@ -1706,40 +1693,38 @@
     return _NewUpload(reviewers, ccs, commit_to_push, end_commit, parent,
                       change_desc)
 
-  def PrepareCherryPickSquashedCommit(self, options):
-    # type: (optparse.Values) -> _NewUpload()
+  def PrepareCherryPickSquashedCommit(self, options, parent):
+    # type: (optparse.Values, str) -> _NewUpload()
     """Create a commit cherry-picked on parent to push."""
 
-    parent = self.GetCommonAncestorWithUpstream()
-    reviewers, ccs, change_desc = self._PrepareChange(options, parent,
+    # The `parent` is what we will cherry-pick on top of.
+    # The `cherry_pick_base` is the beginning range of what
+    # we are cherry-picking.
+    cherry_pick_base = self.GetCommonAncestorWithUpstream()
+    reviewers, ccs, change_desc = self._PrepareChange(options, cherry_pick_base,
                                                       self.branchref)
 
     new_upload_hash = RunGit(['rev-parse', self.branchref]).strip()
     latest_tree = RunGit(['rev-parse', self.branchref + ':']).strip()
     with gclient_utils.temporary_file() as desc_tempfile:
       gclient_utils.FileWrite(desc_tempfile, change_desc.description)
-      commit_to_cp = RunGit(
-          ['commit-tree', latest_tree, '-p', parent, '-F',
-           desc_tempfile]).strip()
+      commit_to_cp = RunGit([
+          'commit-tree', latest_tree, '-p', cherry_pick_base, '-F',
+          desc_tempfile
+      ]).strip()
 
-    _, upstream_branch_ref = self.FetchUpstreamTuple(self.GetBranch())
-
-    upstream_branch = scm.GIT.ShortBranchName(upstream_branch_ref)
-    upstream_squashed_upload = scm.GIT.GetBranchConfig(
-        settings.GetRoot(), upstream_branch, GERRIT_SQUASH_HASH_CONFIG_KEY)
-
-    RunGit(['checkout', '-q', upstream_squashed_upload])
+    RunGit(['checkout', '-q', parent])
     ret, _out = RunGitWithCode(['cherry-pick', commit_to_cp])
     if ret:
       RunGit(['cherry-pick', '--abort'])
       RunGit(['checkout', '-q', self.branch])
       DieWithError('Could not cleanly cherry-pick')
 
-    commit_to_push = RunGit(['rev-parse', 'HEAD'])
+    commit_to_push = RunGit(['rev-parse', 'HEAD']).strip()
     RunGit(['checkout', '-q', self.branch])
 
-    return _NewUpload(reviewers, ccs, commit_to_push, new_upload_hash, parent,
-                      change_desc)
+    return _NewUpload(reviewers, ccs, commit_to_push, new_upload_hash,
+                      cherry_pick_base, change_desc)
 
   def _PrepareChange(self, options, parent, end_commit):
     # type: (optparse.Values, str, str) ->
@@ -4836,9 +4821,28 @@
     new_upload = cls[0].PrepareCherryPickSquashedCommit(options, parent)
     uploads_by_cl.append((cls[0], new_upload))
   else:
-    parent = None
     ordered_cls = list(reversed(cls))
 
+    parent = None
+    origin = '.'
+    cl = ordered_cls[0]
+    branch = cl.GetBranch()
+    while origin == '.':
+      # Search for cl's closest ancestor with a gerrit hash.
+      origin, upstream_branch_ref = Changelist.FetchUpstreamTuple(branch)
+      if origin == '.':
+        upstream_branch = scm.GIT.ShortBranchName(upstream_branch_ref)
+        parent = scm.GIT.GetBranchConfig(settings.GetRoot(), upstream_branch,
+                                         GERRIT_SQUASH_HASH_CONFIG_KEY)
+        if parent:
+          break
+        branch = upstream_branch
+    else:
+      # Either the root of the tree is the cl's direct parent and the while
+      # loop above only found empty branches between cl and the root of the
+      # tree.
+      parent = cl.GetCommonAncestorWithUpstream()
+
     for i, cl in enumerate(ordered_cls):
       # If we're in the middle of the stack, set end_commit to downstream's
       # direct ancestor.
@@ -4847,10 +4851,9 @@
       else:
         child_base_commit = None
       new_upload = cl.PrepareSquashedCommit(options,
-                                            parent=parent,
+                                            parent,
                                             end_commit=child_base_commit)
       uploads_by_cl.append((cl, new_upload))
-
       parent = new_upload.commit_to_push
 
   # Create refspec options
@@ -4908,6 +4911,7 @@
   branch_ref = None
   cls = []
   must_upload_upstream = False
+  first_pass = True
 
   Changelist._GerritCommitMsgHookCheck(offer_removal=not options.force)
 
@@ -4920,28 +4924,42 @@
           (_MAX_STACKED_BRANCHES_UPLOAD))
 
     cl = Changelist(branchref=branch_ref)
-    cls.append(cl)
 
+    # Only add CL if it has anything to commit.
+    base_commit = cl.GetCommonAncestorWithUpstream()
+    end_commit = RunGit(['rev-parse', cl.GetBranchRef()]).strip()
+
+    diff = RunGitSilent(['diff', '%s..%s' % (base_commit, end_commit)])
+    if diff:
+      cls.append(cl)
+      if (not first_pass and
+          cl._GitGetBranchConfigValue(GERRIT_SQUASH_HASH_CONFIG_KEY) is None):
+        # We are mid-stack and the user must upload their upstream branches.
+        must_upload_upstream = True
+    elif first_pass:  # The current branch has nothing to commit. Exit.
+      DieWithError('Branch %s has nothing to commit' % cl.GetBranch())
+    # Else: A mid-stack branch has nothing to commit. We do not add it to cls.
+    first_pass = False
+
+    # Cases below determine if we should continue to traverse up the tree.
     origin, upstream_branch_ref = Changelist.FetchUpstreamTuple(cl.GetBranch())
-    upstream_branch = scm.GIT.ShortBranchName(upstream_branch_ref)
     branch_ref = upstream_branch_ref  # set branch for next run.
 
+    upstream_branch = scm.GIT.ShortBranchName(upstream_branch_ref)
+    upstream_last_upload = scm.GIT.GetBranchConfig(settings.GetRoot(),
+                                                   upstream_branch,
+                                                   LAST_UPLOAD_HASH_CONFIG_KEY)
+
     # Case 1: We've reached the beginning of the tree.
     if origin != '.':
       break
 
-    upstream_last_upload = scm.GIT.GetBranchConfig(settings.GetRoot(),
-                                                   upstream_branch,
-                                                   LAST_UPLOAD_HASH_CONFIG_KEY)
-
     # Case 2: If any upstream branches have never been uploaded,
-    # the user MUST upload them.
+    # the user MUST upload them unless they are empty. Continue to
+    # next loop to add upstream if it is not empty.
     if not upstream_last_upload:
-      must_upload_upstream = True
       continue
 
-    base_commit = cl.GetCommonAncestorWithUpstream()
-
     # Case 3: If upstream's last_upload == cl.base_commit we do
     # not need to upload any more upstreams from this point on.
     # (Even if there may be diverged branches higher up the tree)
@@ -4973,17 +4991,18 @@
   cherry_pick = False
   if len(cls) > 1:
     message = ''
+    branches = ', '.join([cl.branch for cl in cls])
     if len(orig_args):
       message = ('options %s will be used for all uploads.\n' % orig_args)
     if must_upload_upstream:
       confirm_or_exit('\n' + message +
-                      'There are upstream branches that must be uploaded.\n')
+                      'Branches `%s` must be uploaded.\n' % branches)
     else:
       answer = gclient_utils.AskForData(
           '\n' + message +
           'Press enter to update branches %s.\nOr type `n` to upload only '
           '`%s` cherry-picked on %s\'s last upload:' %
-          ([cl.branch for cl in cls], cls[0].branch, cls[1].branch))
+          (branches, cls[0].branch, cls[1].branch))
       if answer.lower() == 'n':
         cherry_pick = True
   return cls, cherry_pick