cmd/gopherbot: remove the wait-author hashtags from CLs when author replies

Updates golang/go#24836

Change-Id: I65dd57290634b31b112062dca9fafa76b2cc7153
Reviewed-on: https://go-review.googlesource.com/108218
Reviewed-by: Andrew Bonventre <andybons@golang.org>
diff --git a/cmd/gopherbot/gopherbot.go b/cmd/gopherbot/gopherbot.go
index 56fe140..2effcec 100644
--- a/cmd/gopherbot/gopherbot.go
+++ b/cmd/gopherbot/gopherbot.go
@@ -225,6 +225,7 @@
 	{"check cherry picks", (*gopherbot).checkCherryPicks},
 	{"update needs", (*gopherbot).updateNeeds},
 	{"congratulate new contributors", (*gopherbot).congratulateNewContributors},
+	{"un-wait CLs", (*gopherbot).unwaitCLs},
 }
 
 func (b *gopherbot) initCorpus() {
@@ -851,6 +852,84 @@
 	return nil
 }
 
+// unwaitCLs removes wait-* hashtags from CLs.
+func (b *gopherbot) unwaitCLs(ctx context.Context) error {
+	return b.corpus.Gerrit().ForeachProjectUnsorted(func(gp *maintner.GerritProject) error {
+		if gp.Server() != "go.googlesource.com" {
+			return nil
+		}
+		return gp.ForeachOpenCL(func(cl *maintner.GerritCL) error {
+			tags := cl.Meta.Hashtags()
+			if tags.Len() == 0 {
+				return nil
+			}
+			// If the CL is tagged "wait-author", remove
+			// that tag if the author has replied since
+			// the last time the "wait-author" tag was
+			// added.
+			if tags.Contains("wait-author") {
+				// Figure out othe last index at which "wait-author" was added.
+				waitAuthorIndex := -1
+				for i := len(cl.Metas) - 1; i >= 0; i-- {
+					if cl.Metas[i].HashtagsAdded().Contains("wait-author") {
+						waitAuthorIndex = i
+						break
+					}
+				}
+
+				// Find the author has replied since
+				author := cl.Metas[0].Commit.Author.Str
+				hasReplied := false
+				for _, m := range cl.Metas[waitAuthorIndex+1:] {
+					if m.Commit.Author.Str == author {
+						hasReplied = true
+						break
+					}
+				}
+				if hasReplied {
+					log.Printf("https://golang.org/cl/%d -- remove wait-author; reply from %s", cl.Number, author)
+					err := b.onLatestCL(ctx, cl, func() error {
+						if *dryRun {
+							log.Printf("[dry run] would remove hashtag 'wait-author' from CL %d", cl.Number)
+							return nil
+						}
+						_, err := b.gerrit.RemoveHashtags(ctx, fmt.Sprint(cl.Number), "wait-author")
+						if err != nil {
+							log.Printf("https://golang.org/cl/%d: error removing wait-author: %v", cl.Number, err)
+							return err
+						}
+						log.Printf("https://golang.org/cl/%d: removed wait-author", cl.Number)
+						return nil
+					})
+					if err != nil {
+						return err
+					}
+				}
+			}
+			return nil
+		})
+	})
+}
+
+// onLatestCL checks whether cl's metadata is in sync with Gerrit's
+// upstream data and, if so, returns f(). If it's out of sync, it does
+// nothing more and returns nil.
+func (b *gopherbot) onLatestCL(ctx context.Context, cl *maintner.GerritCL, f func() error) error {
+	ci, err := b.gerrit.GetChangeDetail(ctx, fmt.Sprint(cl.Number), gerrit.QueryChangesOpt{Fields: []string{"MESSAGES"}})
+	if err != nil {
+		return err
+	}
+	if len(ci.Messages) == 0 {
+		log.Printf("onLatestCL: CL %v has no messages. Odd. Ignoring.")
+		return nil
+	}
+	if ci.Messages[len(ci.Messages)-1].ID == cl.Meta.Commit.Hash.String() {
+		return f()
+	}
+	log.Printf("onLatestCL: maintner metadata for CL %d is behind; skipping action for now.", cl.Number)
+	return nil
+}
+
 // errStopIteration is used to stop iteration over issues or comments.
 // It has no special meaning.
 var errStopIteration = errors.New("stop iteration")