[cl-util] Add abandon-cl subcommand

Add a subcommand to abandon a CL. This may be used to clean up CLs
after they've outlived their use.

Change-Id: I792f5f5803b19323a8106134c6d1ce1cbf3b1009
Bug: 52176
Reviewed-on: https://fuchsia-review.googlesource.com/c/infra/infra/+/389665
Commit-Queue: Anthony Fandrianto <atyfto@google.com>
Reviewed-by: Marc-Antoine Ruel <maruel@google.com>
diff --git a/cmd/cl-util/abandon_cl.go b/cmd/cl-util/abandon_cl.go
new file mode 100644
index 0000000..e67e2eb
--- /dev/null
+++ b/cmd/cl-util/abandon_cl.go
@@ -0,0 +1,69 @@
+// Copyright 2020 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package main
+
+import (
+	"context"
+	"errors"
+	"fmt"
+
+	"github.com/maruel/subcommands"
+	"go.chromium.org/luci/auth"
+)
+
+func cmdAbandonCL(authOpts auth.Options) *subcommands.Command {
+	return &subcommands.Command{
+		UsageLine: "abandon-cl -host <gerrit-host> -project <gerrit-project> -change-num <change-num>",
+		ShortDesc: "Abandon a CL.",
+		LongDesc:  "Abandon a CL.",
+		CommandRun: func() subcommands.CommandRun {
+			c := &abandonCLRun{}
+			c.Init(authOpts)
+			return c
+		},
+	}
+}
+
+type abandonCLRun struct {
+	commonFlags
+	changeNum int64
+}
+
+func (c *abandonCLRun) Init(defaultAuthOpts auth.Options) {
+	c.commonFlags.Init(defaultAuthOpts)
+	c.Flags.Int64Var(&c.changeNum, "change-num", 0, "Gerrit change number.")
+}
+
+func (c *abandonCLRun) Parse(a subcommands.Application, args []string) error {
+	if err := c.commonFlags.Parse(); err != nil {
+		return err
+	}
+	if c.changeNum == 0 {
+		return errors.New("-change-num is required")
+	}
+	return nil
+}
+
+func (c *abandonCLRun) main(a subcommands.Application) error {
+	ctx := context.Background()
+	gerritClient, err := newGerritClient(ctx, c.gerritHost, c.gerritProject, c.parsedAuthOpts)
+	if err != nil {
+		return err
+	}
+	return gerritClient.abandonChange(ctx, c.changeNum)
+}
+
+func (c *abandonCLRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
+	if err := c.Parse(a, args); err != nil {
+		fmt.Fprintf(a.GetErr(), "%s: %s\n", a.GetName(), err)
+		return 1
+	}
+
+	if err := c.main(a); err != nil {
+		fmt.Fprintf(a.GetErr(), "%s: %s\n", a.GetName(), err)
+		return 1
+	}
+	return 0
+}
diff --git a/cmd/cl-util/create_cl.go b/cmd/cl-util/create_cl.go
index 086da14..8459f2b 100644
--- a/cmd/cl-util/create_cl.go
+++ b/cmd/cl-util/create_cl.go
@@ -6,6 +6,7 @@
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"strings"
 
@@ -43,10 +44,10 @@
 		return err
 	}
 	if c.subject == "" {
-		return fmt.Errorf("-subject is required")
+		return errors.New("-subject is required")
 	}
 	if len(c.edits) == 0 {
-		return fmt.Errorf("at least one -file-edit is required")
+		return errors.New("at least one -file-edit is required")
 	}
 	return nil
 }
diff --git a/cmd/cl-util/gerrit.go b/cmd/cl-util/gerrit.go
index 671da7a..5221364 100644
--- a/cmd/cl-util/gerrit.go
+++ b/cmd/cl-util/gerrit.go
@@ -117,3 +117,12 @@
 	}
 	return nil
 }
+
+// abandonChange abandons a Gerrit change.
+func (c *gerritClientWrapper) abandonChange(ctx context.Context, changeNum int64) error {
+	_, err := c.client.AbandonChange(ctx, &gerritpb.AbandonChangeRequest{
+		Number:  changeNum,
+		Project: c.project,
+	})
+	return err
+}
diff --git a/cmd/cl-util/gerrit_test.go b/cmd/cl-util/gerrit_test.go
index 087ef80..3571c3a 100644
--- a/cmd/cl-util/gerrit_test.go
+++ b/cmd/cl-util/gerrit_test.go
@@ -410,3 +410,36 @@
 		}
 	}
 }
+
+func TestAbandonChange(t *testing.T) {
+	t.Parallel()
+	var tests = []struct {
+		project   string
+		changeNum int64
+	}{
+		{
+			project:   "test-project",
+			changeNum: int64(123456),
+		},
+	}
+	for _, test := range tests {
+		ctrl := gomock.NewController(t)
+		defer ctrl.Finish()
+		mockGerritClient := gerritpb.NewMockGerritClient(ctrl)
+		req := gerritpb.AbandonChangeRequest{
+			Number:  test.changeNum,
+			Project: test.project,
+		}
+		resp := gerritpb.ChangeInfo{}
+		mockGerritClient.EXPECT().AbandonChange(gomock.Any(), &req).Return(&resp, nil)
+		mockGerritClientWrapper := gerritClientWrapper{
+			client:  mockGerritClient,
+			project: test.project,
+		}
+		ctx := context.Background()
+		err := mockGerritClientWrapper.abandonChange(ctx, test.changeNum)
+		if err != nil {
+			t.Fatalf("got unexpected err: %v", err)
+		}
+	}
+}
diff --git a/cmd/cl-util/main.go b/cmd/cl-util/main.go
index c0525e9..4dd6ae3 100644
--- a/cmd/cl-util/main.go
+++ b/cmd/cl-util/main.go
@@ -19,7 +19,7 @@
 )
 
 // Version must be updated on functional change (behavior, arguments, supported commands).
-const version = "0.0.1"
+const version = "0.0.2"
 
 func getApplication(defaultAuthOpts auth.Options) *subcommands.DefaultApplication {
 	defaultAuthOpts.Scopes = []string{gitiles.OAuthScope, gerrit.OAuthScope}
@@ -29,6 +29,7 @@
 		Commands: []*subcommands.Command{
 			cmdCreateCL(defaultAuthOpts),
 			cmdTriggerCQ(defaultAuthOpts),
+			cmdAbandonCL(defaultAuthOpts),
 			authcli.SubcommandInfo(defaultAuthOpts, "whoami", false),
 			authcli.SubcommandLogin(defaultAuthOpts, "login", false),
 			authcli.SubcommandLogout(defaultAuthOpts, "logout", false),
diff --git a/cmd/cl-util/trigger_cq.go b/cmd/cl-util/trigger_cq.go
index 3557530..9cfbd0f 100644
--- a/cmd/cl-util/trigger_cq.go
+++ b/cmd/cl-util/trigger_cq.go
@@ -6,6 +6,7 @@
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"time"
 
@@ -49,7 +50,7 @@
 		return err
 	}
 	if c.changeNum == 0 {
-		return fmt.Errorf("-change-num is required")
+		return errors.New("-change-num is required")
 	}
 	return nil
 }