cmd/gopherbot: autosubmit stacks in the right order

When a change in a stack is labelled with Auto-Submit+1, only submit
the change if all of the changes below have already been merged (or
been abandoned.) Additionally, if any of the parent changes are based on
a revision of their parent which is no longer the current revision (i.e.
in the Gerrit UI it is marked as 'Not current'), we will not submit the
change. This makes sure that changes are submitted in the expected
order.

Updates #48021

Change-Id: I9a178551843f64f8b752468ce0a091cb3cf2577d
Reviewed-on: https://go-review.googlesource.com/c/build/+/399043
Reviewed-by: Russ Cox <rsc@golang.org>
Reviewed-by: Bryan Mills <bcmills@google.com>
Run-TryBot: Roland Shoemaker <roland@golang.org>
TryBot-Result: Gopher Robot <gobot@golang.org>
diff --git a/cmd/gopherbot/gopherbot.go b/cmd/gopherbot/gopherbot.go
index 5d2c13b..28c4c29 100644
--- a/cmd/gopherbot/gopherbot.go
+++ b/cmd/gopherbot/gopherbot.go
@@ -2462,6 +2462,41 @@
 				return nil
 			}
 
+			// If this change is part of a stack, we'd like to merge the stack
+			// in the correct order (i.e. from the bottom of the stack to the
+			// top), so we'll only merge the current change if every change
+			// below it in the stack is either merged, or abandoned. If a parent
+			// change is no longer current (the revision of the change that the
+			// child change is based on is no longer the current revision of
+			// that change) we won't merge the child. GetRelatedChanges gives us
+			// the stack from top to bottom (the order of the git commits, from
+			// newest to oldest, see Gerrit documentation for
+			// RelatedChangesInfo), so first we find our change in the stack,
+			// then  check everything below it.
+			relatedChanges, err := b.gerrit.GetRelatedChanges(ctx, fmt.Sprint(cl.Number), "current")
+			if err != nil {
+				return err
+			}
+			if len(relatedChanges.Changes) > 0 {
+				var parentChanges bool
+				for _, ci := range relatedChanges.Changes {
+					if !parentChanges {
+						// Skip everything before the change we are checking, as
+						// they are the children of this change, and we only care
+						// about the parents.
+						parentChanges = ci.ChangeNumber == cl.Number
+						continue
+					}
+					if ci.Status != gerrit.ChangeStatusAbandoned &&
+						ci.Status != gerrit.ChangeStatusMerged {
+						return nil
+					}
+					if ci.CurrentRevisionNumber != ci.RevisionNumber {
+						return nil
+					}
+				}
+			}
+
 			if *dryRun {
 				log.Printf("[dry-run] would've submitted CL https://golang.org/cl/%d ...", cl.Number)
 				return nil
diff --git a/gerrit/gerrit.go b/gerrit/gerrit.go
index 4b6cccc..93de748 100644
--- a/gerrit/gerrit.go
+++ b/gerrit/gerrit.go
@@ -1045,3 +1045,31 @@
 	err := c.do(ctx, &actions, "GET", "/changes/"+changeID+"/revisions/"+revision+"/actions")
 	return actions, err
 }
+
+// RelatedChangeAndCommitInfo contains information about a particular
+// change at a particular commit.
+//
+// See https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#related-change-and-commit-info.
+type RelatedChangeAndCommitInfo struct {
+	Project               string     `json:"project"`
+	ChangeID              string     `json:"change_id"`
+	ChangeNumber          int32      `json:"_change_number"`
+	Commit                CommitInfo `json:"commit"`
+	Status                string     `json:"status"`
+	RevisionNumber        int32      `json:"_revision_number"`
+	CurrentRevisionNumber int32      `json:"_current_revision_number"`
+}
+
+// RelatedChangesInfo contains information about a set of related changes.
+//
+// See https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#related-changes-info.
+type RelatedChangesInfo struct {
+	Changes []RelatedChangeAndCommitInfo `json:"changes"`
+}
+
+// GetRelatedChanges retrieves information about a set of related changes.
+func (c *Client) GetRelatedChanges(ctx context.Context, changeID, revision string) (*RelatedChangesInfo, error) {
+	var changes *RelatedChangesInfo
+	err := c.do(ctx, &changes, "GET", "/changes/"+changeID+"/revisions/"+revision+"/related")
+	return changes, err
+}