internal/task: make Tag idempotent

Proceed if we're trying to write a tag that already exists and matches
the desired commit. That way, if we fail just after writing a tag, or
need to entirely restart a workflow for whatever reason, we can keep
going.

For golang/go#51797.

Change-Id: Ib2fdb13eea6f8cd6d3dd83cdc6cf7d97f12ca6f5
Reviewed-on: https://go-review.googlesource.com/c/build/+/411195
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
Run-TryBot: Heschi Kreinick <heschi@google.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
Auto-Submit: Heschi Kreinick <heschi@google.com>
diff --git a/gerrit/gerrit.go b/gerrit/gerrit.go
index 3ced0aa..ed707a8 100644
--- a/gerrit/gerrit.go
+++ b/gerrit/gerrit.go
@@ -774,15 +774,14 @@
 	return c.do(ctx, nil, "POST", "/changes/"+changeID+"/edit:publish", wantResStatus(http.StatusNoContent))
 }
 
-// ErrProjectNotExist is returned when a project doesn't exist.
+// ErrXNotExist is returned when the requested X doesn't exist.
 // It is not necessarily returned unless a method is documented as
 // returning it.
-var ErrProjectNotExist = errors.New("gerrit: requested project does not exist")
-
-// ErrChangeNotExist is returned when a change doesn't exist.
-// It is not necessarily returned unless a method is documented as
-// returning it.
-var ErrChangeNotExist = errors.New("gerrit: requested change does not exist")
+var (
+	ErrProjectNotExist = errors.New("gerrit: requested project does not exist")
+	ErrChangeNotExist  = errors.New("gerrit: requested change does not exist")
+	ErrTagNotExist     = errors.New("gerrit: requested tag does not exist")
+)
 
 // GetProjectInfo returns info about a project.
 // If the project doesn't exist, the error will be ErrProjectNotExist.
@@ -883,6 +882,19 @@
 	return m, nil
 }
 
+// GetTag returns a particular tag on project. If the tag doesn't exist, the
+// error will be ErrTagNotExist.
+//
+// See https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#get-tag.
+func (c *Client) GetTag(ctx context.Context, project, tag string) (TagInfo, error) {
+	var res TagInfo
+	err := c.do(ctx, &res, "GET", fmt.Sprintf("/projects/%s/tags/%s", project, tag))
+	if he, ok := err.(*HTTPError); ok && he.Res.StatusCode == 404 {
+		return TagInfo{}, ErrTagNotExist
+	}
+	return res, err
+}
+
 // TagInput contains information for creating a tag.
 // See https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#tag-input
 type TagInput struct {
diff --git a/internal/task/gerrit.go b/internal/task/gerrit.go
index 8b4dcee..16bf0d2 100644
--- a/internal/task/gerrit.go
+++ b/internal/task/gerrit.go
@@ -85,7 +85,20 @@
 }
 
 func (c *RealGerritClient) Tag(ctx context.Context, project, tag, commit string) error {
-	_, err := c.Client.CreateTag(ctx, project, tag, gerrit.TagInput{
+	info, err := c.Client.GetTag(ctx, project, tag)
+	if err != nil && err != gerrit.ErrTagNotExist {
+		return fmt.Errorf("checking if tag already exists: %v", err)
+	}
+	if err == nil {
+		if info.Revision != commit {
+			return fmt.Errorf("tag %q already exists on revision %q rather than our %q", tag, info.Revision, commit)
+		} else {
+			// Nothing to do.
+			return nil
+		}
+	}
+
+	_, err = c.Client.CreateTag(ctx, project, tag, gerrit.TagInput{
 		Revision: commit,
 	})
 	return err