blob: 3e1e14287ddaf99bc5bcb0817d9f64cf1f220aed [file] [log] [blame]
// Copyright 2017 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"context"
"encoding/json"
"fmt"
"os"
"github.com/maruel/subcommands"
"go.chromium.org/luci/auth"
"go.chromium.org/luci/common/api/gerrit"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/common/retry/transient"
)
type apiCallInput struct {
ChangeID string `json:"change_id,omitempty"`
ProjectID string `json:"project_id,omitempty"`
RevisionID string `json:"revision_id,omitempty"`
JSONInput any `json:"input,omitempty"`
QueryInput any `json:"params,omitempty"`
}
type apiCall func(context.Context, *gerrit.Client, *apiCallInput) (any, error)
type changeRunOptions struct {
// These booleans indicate whether a value is required in a subcommand's JSON
// input.
changeID bool
projectID bool
revisionID bool
jsonInput any
queryInput any
}
type changeRun struct {
commonFlags
changeRunOptions
inputLocation string
input apiCallInput
apiFunc apiCall
}
type failureOutput struct {
Message string `json:"message"`
Transient bool `json:"transient"`
}
func newChangeRun(authOpts auth.Options, cmdOpts changeRunOptions, apiFunc apiCall) *changeRun {
c := changeRun{
changeRunOptions: cmdOpts,
apiFunc: apiFunc,
}
c.commonFlags.Init(authOpts)
c.Flags.StringVar(&c.inputLocation, "input", "", "(required) Path to file containing json input for the request (use '-' for stdin).")
return &c
}
func (c *changeRun) Parse(a subcommands.Application, args []string) error {
if err := c.commonFlags.Parse(); err != nil {
return err
}
if len(args) != 0 {
return errors.New("position arguments not expected")
}
if c.host == "" {
return errors.New("must specify a host")
}
if c.inputLocation == "" {
return errors.New("must specify input")
}
// Copy inputs from options to json-decodable input.
c.input.JSONInput = c.changeRunOptions.jsonInput
c.input.QueryInput = c.changeRunOptions.queryInput
// Load json from file and decode.
input := os.Stdin
if c.inputLocation != "-" {
f, err := os.Open(c.inputLocation)
if err != nil {
return err
}
defer f.Close()
input = f
}
if err := json.NewDecoder(input).Decode(&c.input); err != nil {
return errors.Annotate(err, "failed to decode input").Err()
}
// Verify we have a change ID if the command requires one.
if c.changeID && len(c.input.ChangeID) == 0 {
return errors.New("change_id is required")
}
// Verify we have a project ID if the command requires one.
if c.projectID && len(c.input.ProjectID) == 0 {
return errors.New("project_id is required")
}
// Verify we have a revision ID if the command requires one.
if c.revisionID && len(c.input.RevisionID) == 0 {
return errors.New("revision_id is required")
}
return nil
}
func (c *changeRun) writeOutput(v any) error {
out := os.Stdout
var err error
if c.jsonOutput != "-" {
out, err = os.Create(c.jsonOutput)
if err != nil {
return err
}
defer out.Close()
}
data, err := json.MarshalIndent(v, "", " ")
if err != nil {
return err
}
_, err = out.Write(data)
return err
}
func (c *changeRun) main(a subcommands.Application) error {
// Create auth client and context.
authCl, err := c.createAuthClient()
if err != nil {
return err
}
ctx := c.defaultFlags.MakeLoggingContext(os.Stderr)
// Create gerrit client and make call.
g, err := gerrit.NewClient(authCl, c.host)
if err != nil {
return err
}
v, err := c.apiFunc(ctx, g, &c.input)
if err != nil {
c.writeOutput(failureOutput{
Message: err.Error(),
Transient: transient.Tag.In(err),
})
return err
}
return c.writeOutput(v)
}
func (c *changeRun) Run(a subcommands.Application, args []string, _ 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
}
func cmdCreateBranch(authOpts auth.Options) *subcommands.Command {
runner := func(ctx context.Context, client *gerrit.Client, input *apiCallInput) (any, error) {
bi := input.JSONInput.(*gerrit.BranchInput)
return client.CreateBranch(ctx, input.ProjectID, bi)
}
return &subcommands.Command{
UsageLine: "create-branch <options>",
ShortDesc: "creates a branch",
LongDesc: `Creates a branch.
Input should contain a project ID and a JSON payload, e.g.
{
"project_id": <project-id>,
"input": <JSON payload>
}
More information on creating branches may be found here:
https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#create-branch`,
CommandRun: func() subcommands.CommandRun {
return newChangeRun(authOpts, changeRunOptions{
projectID: true,
jsonInput: &gerrit.BranchInput{},
}, runner)
},
}
}
func cmdChangeAbandon(authOpts auth.Options) *subcommands.Command {
runner := func(ctx context.Context, client *gerrit.Client, input *apiCallInput) (any, error) {
ai := input.JSONInput.(*gerrit.AbandonInput)
return client.AbandonChange(ctx, input.ChangeID, ai)
}
return &subcommands.Command{
UsageLine: "change-abandon <options>",
ShortDesc: "abandons a change",
LongDesc: `Abandons a change in Gerrit.
Input should contain a change ID and optionally a JSON payload, e.g.
{
"change_id": <change-id>,
"input": <JSON payload>
}
For more information on change-id, see
https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-id
More information on abandoning changes may be found here:
https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#abandon-change`,
CommandRun: func() subcommands.CommandRun {
return newChangeRun(authOpts, changeRunOptions{
changeID: true,
jsonInput: &gerrit.AbandonInput{},
}, runner)
},
}
}
func cmdChangeCreate(authOpts auth.Options) *subcommands.Command {
runner := func(ctx context.Context, client *gerrit.Client, input *apiCallInput) (any, error) {
ci := input.JSONInput.(*gerrit.ChangeInput)
return client.CreateChange(ctx, ci)
}
return &subcommands.Command{
UsageLine: "change-create <options>",
ShortDesc: "creates a new change",
LongDesc: `Creates a new change in Gerrit.
Input should contain a JSON payload, e.g. {"input": <JSON payload>}.
For more information, see https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#create-change`,
CommandRun: func() subcommands.CommandRun {
return newChangeRun(authOpts, changeRunOptions{
jsonInput: &gerrit.ChangeInput{},
}, runner)
},
}
}
func cmdChangeQuery(authOpts auth.Options) *subcommands.Command {
runner := func(ctx context.Context, client *gerrit.Client, input *apiCallInput) (any, error) {
req := input.QueryInput.(*gerrit.ChangeQueryParams)
changes, _, err := client.ChangeQuery(ctx, *req)
return changes, err
}
return &subcommands.Command{
UsageLine: "change-query <options>",
ShortDesc: "queries Gerrit for changes",
LongDesc: `Queries Gerrit for changes.
Input should contain query options, e.g. {"params": <query parameters as JSON>}
For more information on valid query parameters, see
https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#query-changes`,
CommandRun: func() subcommands.CommandRun {
return newChangeRun(authOpts, changeRunOptions{
queryInput: &gerrit.ChangeQueryParams{},
}, runner)
},
}
}
func cmdChangeDetail(authOpts auth.Options) *subcommands.Command {
runner := func(ctx context.Context, client *gerrit.Client, input *apiCallInput) (any, error) {
opts := input.QueryInput.(*gerrit.ChangeDetailsParams)
return client.ChangeDetails(ctx, input.ChangeID, *opts)
}
return &subcommands.Command{
UsageLine: "change-detail <options>",
ShortDesc: "gets details about a single change with optional fields",
LongDesc: `Gets details about a single change with optional fields.
Input should contain a change ID and optionally query parameters, e.g.
{
"change_id": <change-id>,
"params": <query parameters as JSON>
}
For more information on change-id, see
https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-id
For more information on valid query parameters, see
https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#list-changes`,
CommandRun: func() subcommands.CommandRun {
return newChangeRun(authOpts, changeRunOptions{
changeID: true,
queryInput: &gerrit.ChangeDetailsParams{},
}, runner)
},
}
}
func cmdListChangeComments(authOpts auth.Options) *subcommands.Command {
runner := func(ctx context.Context, client *gerrit.Client, input *apiCallInput) (any, error) {
result, err := client.ListChangeComments(ctx, input.ChangeID, input.RevisionID)
if err != nil {
return nil, err
}
return result, nil
}
return &subcommands.Command{
UsageLine: "list-change-comments <options>",
ShortDesc: "gets all comments on a single change",
LongDesc: `Gets all comments on a single change.
Input should contain a change ID, e.g.
{
"change_id": <change-id>,
}
For more information on change-id, see
https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-id`,
CommandRun: func() subcommands.CommandRun {
return newChangeRun(authOpts, changeRunOptions{
changeID: true,
}, runner)
},
}
}
func cmdListRobotComments(authOpts auth.Options) *subcommands.Command {
runner := func(ctx context.Context, client *gerrit.Client, input *apiCallInput) (any, error) {
result, err := client.ListRobotComments(ctx, input.ChangeID, input.RevisionID)
if err != nil {
return nil, err
}
return result, nil
}
return &subcommands.Command{
UsageLine: "list-robot-comments <options>",
ShortDesc: "gets all robot comments on a single change",
LongDesc: `Gets all robot comments on a single change.
Input should contain a change ID, e.g.
{
"change_id": <change-id>,
}
For more information on change-id, see
https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-id`,
CommandRun: func() subcommands.CommandRun {
return newChangeRun(authOpts, changeRunOptions{
changeID: true,
}, runner)
},
}
}
func cmdChangesSubmittedTogether(authOpts auth.Options) *subcommands.Command {
runner := func(ctx context.Context, client *gerrit.Client, input *apiCallInput) (any, error) {
opts := input.QueryInput.(*gerrit.ChangeDetailsParams)
return client.ChangesSubmittedTogether(ctx, input.ChangeID, *opts)
}
return &subcommands.Command{
UsageLine: "changes-submitted-together <options>",
ShortDesc: "lists Gerrit changes which are submitted together when Submit is called for a change",
LongDesc: `Lists Gerrit changes which are submitted together when Submit is called for a change.
Input should contain a change ID and optionally query parameters, e.g.
{
"change_id": <change-id>,
"params": <query parameters as JSON>
}
For more information on change-id, see
https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-id
For more information on valid query parameters, see
https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#list-changes`,
CommandRun: func() subcommands.CommandRun {
return newChangeRun(authOpts, changeRunOptions{
changeID: true,
queryInput: &gerrit.ChangeDetailsParams{},
}, runner)
},
}
}
func cmdSetReview(authOpts auth.Options) *subcommands.Command {
runner := func(ctx context.Context, client *gerrit.Client, input *apiCallInput) (any, error) {
ri := input.JSONInput.(*gerrit.ReviewInput)
return client.SetReview(ctx, input.ChangeID, input.RevisionID, ri)
}
return &subcommands.Command{
UsageLine: "set-review <options>",
ShortDesc: "sets the review on a revision of a change",
LongDesc: `Sets the review on a revision of a change.
Input should contain a change ID, a revision ID, and a JSON payload, e.g.
{
"change_id": <change-id>,
"revision_id": <revision-id>,
"input": <JSON payload>
}
For more information on change-id, see
https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-id
For more information on revision-id, see
https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#revision-id
More information on "set review" may be found here:
https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#set-review`,
CommandRun: func() subcommands.CommandRun {
return newChangeRun(authOpts, changeRunOptions{
changeID: true,
revisionID: true,
jsonInput: &gerrit.ReviewInput{},
}, runner)
},
}
}
func cmdGetMergeable(authOpts auth.Options) *subcommands.Command {
runner := func(ctx context.Context, client *gerrit.Client, input *apiCallInput) (any, error) {
return client.GetMergeable(ctx, input.ChangeID, input.RevisionID)
}
return &subcommands.Command{
UsageLine: "get-mergeable <options>",
ShortDesc: "Checks if this change and revision are mergeable",
LongDesc: `Does the mergeability check on a change and revision.
Input should contain a change ID, a revision ID, and a JSON payload, e.g.
{
"change_id": <change-id>,
"revision_id": <revision-id>
}
For more information on change-id, see
https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-id
For more information on revision-id, see
https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#revision-id
More information on "get mergeable" may be found here:
https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#get-mergeable`,
CommandRun: func() subcommands.CommandRun {
return newChangeRun(authOpts, changeRunOptions{
changeID: true,
revisionID: true,
}, runner)
},
}
}
func cmdSubmit(authOpts auth.Options) *subcommands.Command {
runner := func(ctx context.Context, client *gerrit.Client, input *apiCallInput) (any, error) {
si := input.JSONInput.(*gerrit.SubmitInput)
return client.Submit(ctx, input.ChangeID, si)
}
return &subcommands.Command{
UsageLine: "submit <options>",
ShortDesc: "submit a change",
LongDesc: `Submit a change.
Input should contain a change ID, e.g.
{
"change_id": <change-id>,
}
For more information on change-id, see
https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-id`,
CommandRun: func() subcommands.CommandRun {
return newChangeRun(authOpts, changeRunOptions{
changeID: true,
jsonInput: &gerrit.SubmitInput{},
}, runner)
},
}
}
func cmdRebase(authOpts auth.Options) *subcommands.Command {
runner := func(ctx context.Context, client *gerrit.Client, input *apiCallInput) (any, error) {
ri := input.JSONInput.(*gerrit.RebaseInput)
return client.RebaseChange(ctx, input.ChangeID, ri)
}
return &subcommands.Command{
UsageLine: "rebase <options>",
ShortDesc: "rebases a change",
LongDesc: `rebases a change.
Input should contain a change ID, and optionally a JSON payload, e.g.
{
"change_id": <change-id>,
"input": <JSON payload>
}
For more information on change-id, see
https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-id
More information on "rebase" may be found here:
https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#rebase-change`,
CommandRun: func() subcommands.CommandRun {
return newChangeRun(authOpts, changeRunOptions{
changeID: true,
jsonInput: &gerrit.RebaseInput{},
}, runner)
},
}
}
func cmdRestore(authOpts auth.Options) *subcommands.Command {
runner := func(ctx context.Context, client *gerrit.Client, input *apiCallInput) (any, error) {
ri := input.JSONInput.(*gerrit.RestoreInput)
return client.RestoreChange(ctx, input.ChangeID, ri)
}
return &subcommands.Command{
UsageLine: "restore <options>",
ShortDesc: "restores a change",
LongDesc: `restores a change.
Input should contain a change ID, and optionally a JSON payload, e.g.
{
"change_id": <change-id>,
"input": <JSON payload>
}
For more information on change-id, see
https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-id
More information on "restore" may be found here:
https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#restore-change`,
CommandRun: func() subcommands.CommandRun {
return newChangeRun(authOpts, changeRunOptions{
changeID: true,
jsonInput: &gerrit.RestoreInput{},
}, runner)
},
}
}
func cmdAccountQuery(authOpts auth.Options) *subcommands.Command {
runner := func(ctx context.Context, client *gerrit.Client, input *apiCallInput) (any, error) {
req := input.QueryInput.(*gerrit.AccountQueryParams)
changes, _, err := client.AccountQuery(ctx, *req)
return changes, err
}
return &subcommands.Command{
UsageLine: "account-query <options>",
ShortDesc: "queries Gerrit for accounts",
LongDesc: `Queries Gerrit for accounts.
Input should contain query options, e.g. {"params": <query parameters as JSON>}
For more information on valid query parameters, see
https://gerrit-review.googlesource.com/Documentation/user-search-accounts.html#_search_operators`,
CommandRun: func() subcommands.CommandRun {
return newChangeRun(authOpts, changeRunOptions{
queryInput: &gerrit.AccountQueryParams{},
}, runner)
},
}
}