go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/client/cmd/gerrit/change.go (about)

     1  // Copyright 2017 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package main
    16  
    17  import (
    18  	"context"
    19  	"encoding/json"
    20  	"fmt"
    21  	"os"
    22  
    23  	"github.com/maruel/subcommands"
    24  
    25  	"go.chromium.org/luci/auth"
    26  	"go.chromium.org/luci/common/api/gerrit"
    27  	"go.chromium.org/luci/common/errors"
    28  	"go.chromium.org/luci/common/retry/transient"
    29  )
    30  
    31  type apiCallInput struct {
    32  	ChangeID   string `json:"change_id,omitempty"`
    33  	ProjectID  string `json:"project_id,omitempty"`
    34  	RevisionID string `json:"revision_id,omitempty"`
    35  	JSONInput  any    `json:"input,omitempty"`
    36  	QueryInput any    `json:"params,omitempty"`
    37  }
    38  
    39  type apiCall func(context.Context, *gerrit.Client, *apiCallInput) (any, error)
    40  
    41  type changeRunOptions struct {
    42  	// These booleans indicate whether a value is required in a subcommand's JSON
    43  	// input.
    44  	changeID   bool
    45  	projectID  bool
    46  	revisionID bool
    47  	jsonInput  any
    48  	queryInput any
    49  }
    50  
    51  type changeRun struct {
    52  	commonFlags
    53  	changeRunOptions
    54  	inputLocation string
    55  	input         apiCallInput
    56  	apiFunc       apiCall
    57  }
    58  
    59  type failureOutput struct {
    60  	Message   string `json:"message"`
    61  	Transient bool   `json:"transient"`
    62  }
    63  
    64  func newChangeRun(authOpts auth.Options, cmdOpts changeRunOptions, apiFunc apiCall) *changeRun {
    65  	c := changeRun{
    66  		changeRunOptions: cmdOpts,
    67  		apiFunc:          apiFunc,
    68  	}
    69  	c.commonFlags.Init(authOpts)
    70  	c.Flags.StringVar(&c.inputLocation, "input", "", "(required) Path to file containing json input for the request (use '-' for stdin).")
    71  	return &c
    72  }
    73  
    74  func (c *changeRun) Parse(a subcommands.Application, args []string) error {
    75  	if err := c.commonFlags.Parse(); err != nil {
    76  		return err
    77  	}
    78  	if len(args) != 0 {
    79  		return errors.New("position arguments not expected")
    80  	}
    81  	if c.host == "" {
    82  		return errors.New("must specify a host")
    83  	}
    84  	if c.inputLocation == "" {
    85  		return errors.New("must specify input")
    86  	}
    87  
    88  	// Copy inputs from options to json-decodable input.
    89  	c.input.JSONInput = c.changeRunOptions.jsonInput
    90  	c.input.QueryInput = c.changeRunOptions.queryInput
    91  
    92  	// Load json from file and decode.
    93  	input := os.Stdin
    94  	if c.inputLocation != "-" {
    95  		f, err := os.Open(c.inputLocation)
    96  		if err != nil {
    97  			return err
    98  		}
    99  		defer f.Close()
   100  		input = f
   101  	}
   102  	if err := json.NewDecoder(input).Decode(&c.input); err != nil {
   103  		return errors.Annotate(err, "failed to decode input").Err()
   104  	}
   105  
   106  	// Verify we have a change ID if the command requires one.
   107  	if c.changeID && len(c.input.ChangeID) == 0 {
   108  		return errors.New("change_id is required")
   109  	}
   110  
   111  	// Verify we have a project ID if the command requires one.
   112  	if c.projectID && len(c.input.ProjectID) == 0 {
   113  		return errors.New("project_id is required")
   114  	}
   115  
   116  	// Verify we have a revision ID if the command requires one.
   117  	if c.revisionID && len(c.input.RevisionID) == 0 {
   118  		return errors.New("revision_id is required")
   119  	}
   120  	return nil
   121  }
   122  
   123  func (c *changeRun) writeOutput(v any) error {
   124  	out := os.Stdout
   125  	var err error
   126  	if c.jsonOutput != "-" {
   127  		out, err = os.Create(c.jsonOutput)
   128  		if err != nil {
   129  			return err
   130  		}
   131  		defer out.Close()
   132  	}
   133  	data, err := json.MarshalIndent(v, "", "  ")
   134  	if err != nil {
   135  		return err
   136  	}
   137  	_, err = out.Write(data)
   138  	return err
   139  }
   140  
   141  func (c *changeRun) main(a subcommands.Application) error {
   142  	// Create auth client and context.
   143  	authCl, err := c.createAuthClient()
   144  	if err != nil {
   145  		return err
   146  	}
   147  	ctx := c.defaultFlags.MakeLoggingContext(os.Stderr)
   148  
   149  	// Create gerrit client and make call.
   150  	g, err := gerrit.NewClient(authCl, c.host)
   151  	if err != nil {
   152  		return err
   153  	}
   154  	v, err := c.apiFunc(ctx, g, &c.input)
   155  	if err != nil {
   156  		c.writeOutput(failureOutput{
   157  			Message:   err.Error(),
   158  			Transient: transient.Tag.In(err),
   159  		})
   160  		return err
   161  	}
   162  	return c.writeOutput(v)
   163  }
   164  
   165  func (c *changeRun) Run(a subcommands.Application, args []string, _ subcommands.Env) int {
   166  	if err := c.Parse(a, args); err != nil {
   167  		fmt.Fprintf(a.GetErr(), "%s: %s\n", a.GetName(), err)
   168  		return 1
   169  	}
   170  	if err := c.main(a); err != nil {
   171  		fmt.Fprintf(a.GetErr(), "%s: %s\n", a.GetName(), err)
   172  		return 1
   173  	}
   174  	return 0
   175  }
   176  
   177  func cmdCreateBranch(authOpts auth.Options) *subcommands.Command {
   178  	runner := func(ctx context.Context, client *gerrit.Client, input *apiCallInput) (any, error) {
   179  		bi := input.JSONInput.(*gerrit.BranchInput)
   180  		return client.CreateBranch(ctx, input.ProjectID, bi)
   181  	}
   182  	return &subcommands.Command{
   183  		UsageLine: "create-branch <options>",
   184  		ShortDesc: "creates a branch",
   185  		LongDesc: `Creates a branch.
   186  
   187  Input should contain a project ID and a JSON payload, e.g.
   188  {
   189  	"project_id": <project-id>,
   190  	"input": <JSON payload>
   191  }
   192  
   193  More information on creating branches may be found here:
   194  https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#create-branch`,
   195  		CommandRun: func() subcommands.CommandRun {
   196  			return newChangeRun(authOpts, changeRunOptions{
   197  				projectID: true,
   198  				jsonInput: &gerrit.BranchInput{},
   199  			}, runner)
   200  		},
   201  	}
   202  }
   203  
   204  func cmdChangeAbandon(authOpts auth.Options) *subcommands.Command {
   205  	runner := func(ctx context.Context, client *gerrit.Client, input *apiCallInput) (any, error) {
   206  		ai := input.JSONInput.(*gerrit.AbandonInput)
   207  		return client.AbandonChange(ctx, input.ChangeID, ai)
   208  	}
   209  	return &subcommands.Command{
   210  		UsageLine: "change-abandon <options>",
   211  		ShortDesc: "abandons a change",
   212  		LongDesc: `Abandons a change in Gerrit.
   213  
   214  Input should contain a change ID and optionally a JSON payload, e.g.
   215  {
   216    "change_id": <change-id>,
   217    "input": <JSON payload>
   218  }
   219  
   220  For more information on change-id, see
   221  https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-id
   222  
   223  More information on abandoning changes may be found here:
   224  https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#abandon-change`,
   225  		CommandRun: func() subcommands.CommandRun {
   226  			return newChangeRun(authOpts, changeRunOptions{
   227  				changeID:  true,
   228  				jsonInput: &gerrit.AbandonInput{},
   229  			}, runner)
   230  		},
   231  	}
   232  }
   233  
   234  func cmdChangeCreate(authOpts auth.Options) *subcommands.Command {
   235  	runner := func(ctx context.Context, client *gerrit.Client, input *apiCallInput) (any, error) {
   236  		ci := input.JSONInput.(*gerrit.ChangeInput)
   237  		return client.CreateChange(ctx, ci)
   238  	}
   239  	return &subcommands.Command{
   240  		UsageLine: "change-create <options>",
   241  		ShortDesc: "creates a new change",
   242  		LongDesc: `Creates a new change in Gerrit.
   243  
   244  Input should contain a JSON payload, e.g. {"input": <JSON payload>}.
   245  
   246  For more information, see https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#create-change`,
   247  		CommandRun: func() subcommands.CommandRun {
   248  			return newChangeRun(authOpts, changeRunOptions{
   249  				jsonInput: &gerrit.ChangeInput{},
   250  			}, runner)
   251  		},
   252  	}
   253  }
   254  
   255  func cmdChangeQuery(authOpts auth.Options) *subcommands.Command {
   256  	runner := func(ctx context.Context, client *gerrit.Client, input *apiCallInput) (any, error) {
   257  		req := input.QueryInput.(*gerrit.ChangeQueryParams)
   258  		changes, _, err := client.ChangeQuery(ctx, *req)
   259  		return changes, err
   260  	}
   261  	return &subcommands.Command{
   262  		UsageLine: "change-query <options>",
   263  		ShortDesc: "queries Gerrit for changes",
   264  		LongDesc: `Queries Gerrit for changes.
   265  
   266  Input should contain query options, e.g. {"params": <query parameters as JSON>}
   267  
   268  For more information on valid query parameters, see
   269  https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#query-changes`,
   270  		CommandRun: func() subcommands.CommandRun {
   271  			return newChangeRun(authOpts, changeRunOptions{
   272  				queryInput: &gerrit.ChangeQueryParams{},
   273  			}, runner)
   274  		},
   275  	}
   276  }
   277  
   278  func cmdChangeDetail(authOpts auth.Options) *subcommands.Command {
   279  	runner := func(ctx context.Context, client *gerrit.Client, input *apiCallInput) (any, error) {
   280  		opts := input.QueryInput.(*gerrit.ChangeDetailsParams)
   281  		return client.ChangeDetails(ctx, input.ChangeID, *opts)
   282  	}
   283  	return &subcommands.Command{
   284  		UsageLine: "change-detail <options>",
   285  		ShortDesc: "gets details about a single change with optional fields",
   286  		LongDesc: `Gets details about a single change with optional fields.
   287  
   288  Input should contain a change ID and optionally query parameters, e.g.
   289  {
   290    "change_id": <change-id>,
   291    "params": <query parameters as JSON>
   292  }
   293  
   294  For more information on change-id, see
   295  https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-id
   296  
   297  For more information on valid query parameters, see
   298  https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#list-changes`,
   299  		CommandRun: func() subcommands.CommandRun {
   300  			return newChangeRun(authOpts, changeRunOptions{
   301  				changeID:   true,
   302  				queryInput: &gerrit.ChangeDetailsParams{},
   303  			}, runner)
   304  		},
   305  	}
   306  }
   307  
   308  func cmdListChangeComments(authOpts auth.Options) *subcommands.Command {
   309  	runner := func(ctx context.Context, client *gerrit.Client, input *apiCallInput) (any, error) {
   310  		result, err := client.ListChangeComments(ctx, input.ChangeID, input.RevisionID)
   311  		if err != nil {
   312  			return nil, err
   313  		}
   314  		return result, nil
   315  	}
   316  	return &subcommands.Command{
   317  		UsageLine: "list-change-comments <options>",
   318  		ShortDesc: "gets all comments on a single change",
   319  		LongDesc: `Gets all comments on a single change.
   320  
   321  Input should contain a change ID, e.g.
   322  {
   323    "change_id": <change-id>,
   324  }
   325  
   326  For more information on change-id, see
   327  https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-id`,
   328  		CommandRun: func() subcommands.CommandRun {
   329  			return newChangeRun(authOpts, changeRunOptions{
   330  				changeID: true,
   331  			}, runner)
   332  		},
   333  	}
   334  }
   335  
   336  func cmdListRobotComments(authOpts auth.Options) *subcommands.Command {
   337  	runner := func(ctx context.Context, client *gerrit.Client, input *apiCallInput) (any, error) {
   338  		result, err := client.ListRobotComments(ctx, input.ChangeID, input.RevisionID)
   339  		if err != nil {
   340  			return nil, err
   341  		}
   342  		return result, nil
   343  	}
   344  	return &subcommands.Command{
   345  		UsageLine: "list-robot-comments <options>",
   346  		ShortDesc: "gets all robot comments on a single change",
   347  		LongDesc: `Gets all robot comments on a single change.
   348  
   349  Input should contain a change ID, e.g.
   350  {
   351    "change_id": <change-id>,
   352  }
   353  
   354  For more information on change-id, see
   355  https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-id`,
   356  		CommandRun: func() subcommands.CommandRun {
   357  			return newChangeRun(authOpts, changeRunOptions{
   358  				changeID: true,
   359  			}, runner)
   360  		},
   361  	}
   362  }
   363  
   364  func cmdChangesSubmittedTogether(authOpts auth.Options) *subcommands.Command {
   365  	runner := func(ctx context.Context, client *gerrit.Client, input *apiCallInput) (any, error) {
   366  		opts := input.QueryInput.(*gerrit.ChangeDetailsParams)
   367  		return client.ChangesSubmittedTogether(ctx, input.ChangeID, *opts)
   368  	}
   369  	return &subcommands.Command{
   370  		UsageLine: "changes-submitted-together <options>",
   371  		ShortDesc: "lists Gerrit changes which are submitted together when Submit is called for a change",
   372  		LongDesc: `Lists Gerrit changes which are submitted together when Submit is called for a change.
   373  
   374  Input should contain a change ID and optionally query parameters, e.g.
   375  {
   376    "change_id": <change-id>,
   377    "params": <query parameters as JSON>
   378  }
   379  
   380  For more information on change-id, see
   381  https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-id
   382  
   383  For more information on valid query parameters, see
   384  https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#list-changes`,
   385  		CommandRun: func() subcommands.CommandRun {
   386  			return newChangeRun(authOpts, changeRunOptions{
   387  				changeID:   true,
   388  				queryInput: &gerrit.ChangeDetailsParams{},
   389  			}, runner)
   390  		},
   391  	}
   392  }
   393  
   394  func cmdSetReview(authOpts auth.Options) *subcommands.Command {
   395  	runner := func(ctx context.Context, client *gerrit.Client, input *apiCallInput) (any, error) {
   396  		ri := input.JSONInput.(*gerrit.ReviewInput)
   397  		return client.SetReview(ctx, input.ChangeID, input.RevisionID, ri)
   398  	}
   399  	return &subcommands.Command{
   400  		UsageLine: "set-review <options>",
   401  		ShortDesc: "sets the review on a revision of a change",
   402  		LongDesc: `Sets the review on a revision of a change.
   403  
   404  Input should contain a change ID, a revision ID, and a JSON payload, e.g.
   405  {
   406    "change_id": <change-id>,
   407    "revision_id": <revision-id>,
   408    "input": <JSON payload>
   409  }
   410  
   411  For more information on change-id, see
   412  https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-id
   413  
   414  For more information on revision-id, see
   415  https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#revision-id
   416  
   417  More information on "set review" may be found here:
   418  https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#set-review`,
   419  		CommandRun: func() subcommands.CommandRun {
   420  			return newChangeRun(authOpts, changeRunOptions{
   421  				changeID:   true,
   422  				revisionID: true,
   423  				jsonInput:  &gerrit.ReviewInput{},
   424  			}, runner)
   425  		},
   426  	}
   427  }
   428  
   429  func cmdGetMergeable(authOpts auth.Options) *subcommands.Command {
   430  	runner := func(ctx context.Context, client *gerrit.Client, input *apiCallInput) (any, error) {
   431  		return client.GetMergeable(ctx, input.ChangeID, input.RevisionID)
   432  	}
   433  	return &subcommands.Command{
   434  		UsageLine: "get-mergeable <options>",
   435  		ShortDesc: "Checks if this change and revision are mergeable",
   436  		LongDesc: `Does the mergeability check on a change and revision.
   437  
   438  Input should contain a change ID, a revision ID, and a JSON payload, e.g.
   439  {
   440    "change_id": <change-id>,
   441    "revision_id": <revision-id>
   442  }
   443  
   444  For more information on change-id, see
   445  https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-id
   446  
   447  For more information on revision-id, see
   448  https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#revision-id
   449  
   450  More information on "get mergeable" may be found here:
   451  https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#get-mergeable`,
   452  		CommandRun: func() subcommands.CommandRun {
   453  			return newChangeRun(authOpts, changeRunOptions{
   454  				changeID:   true,
   455  				revisionID: true,
   456  			}, runner)
   457  		},
   458  	}
   459  }
   460  
   461  func cmdSubmit(authOpts auth.Options) *subcommands.Command {
   462  	runner := func(ctx context.Context, client *gerrit.Client, input *apiCallInput) (any, error) {
   463  		si := input.JSONInput.(*gerrit.SubmitInput)
   464  		return client.Submit(ctx, input.ChangeID, si)
   465  	}
   466  	return &subcommands.Command{
   467  		UsageLine: "submit <options>",
   468  		ShortDesc: "submit a change",
   469  		LongDesc: `Submit a change.
   470  
   471  Input should contain a change ID, e.g.
   472  {
   473    "change_id": <change-id>,
   474  }
   475  
   476  For more information on change-id, see
   477  https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-id`,
   478  		CommandRun: func() subcommands.CommandRun {
   479  			return newChangeRun(authOpts, changeRunOptions{
   480  				changeID:  true,
   481  				jsonInput: &gerrit.SubmitInput{},
   482  			}, runner)
   483  		},
   484  	}
   485  }
   486  
   487  func cmdRebase(authOpts auth.Options) *subcommands.Command {
   488  	runner := func(ctx context.Context, client *gerrit.Client, input *apiCallInput) (any, error) {
   489  		ri := input.JSONInput.(*gerrit.RebaseInput)
   490  		return client.RebaseChange(ctx, input.ChangeID, ri)
   491  	}
   492  	return &subcommands.Command{
   493  		UsageLine: "rebase <options>",
   494  		ShortDesc: "rebases a change",
   495  		LongDesc: `rebases a change.
   496  
   497  Input should contain a change ID, and optionally a JSON payload, e.g.
   498  {
   499    "change_id": <change-id>,
   500    "input": <JSON payload>
   501  }
   502  
   503  For more information on change-id, see
   504  https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-id
   505  
   506  More information on "rebase" may be found here:
   507  https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#rebase-change`,
   508  		CommandRun: func() subcommands.CommandRun {
   509  			return newChangeRun(authOpts, changeRunOptions{
   510  				changeID:  true,
   511  				jsonInput: &gerrit.RebaseInput{},
   512  			}, runner)
   513  		},
   514  	}
   515  }
   516  
   517  func cmdRestore(authOpts auth.Options) *subcommands.Command {
   518  	runner := func(ctx context.Context, client *gerrit.Client, input *apiCallInput) (any, error) {
   519  		ri := input.JSONInput.(*gerrit.RestoreInput)
   520  		return client.RestoreChange(ctx, input.ChangeID, ri)
   521  	}
   522  	return &subcommands.Command{
   523  		UsageLine: "restore <options>",
   524  		ShortDesc: "restores a change",
   525  		LongDesc: `restores a change.
   526  
   527  Input should contain a change ID, and optionally a JSON payload, e.g.
   528  {
   529    "change_id": <change-id>,
   530    "input": <JSON payload>
   531  }
   532  
   533  For more information on change-id, see
   534  https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-id
   535  
   536  More information on "restore" may be found here:
   537  https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#restore-change`,
   538  		CommandRun: func() subcommands.CommandRun {
   539  			return newChangeRun(authOpts, changeRunOptions{
   540  				changeID:  true,
   541  				jsonInput: &gerrit.RestoreInput{},
   542  			}, runner)
   543  		},
   544  	}
   545  }
   546  
   547  func cmdAccountQuery(authOpts auth.Options) *subcommands.Command {
   548  	runner := func(ctx context.Context, client *gerrit.Client, input *apiCallInput) (any, error) {
   549  		req := input.QueryInput.(*gerrit.AccountQueryParams)
   550  		changes, _, err := client.AccountQuery(ctx, *req)
   551  		return changes, err
   552  	}
   553  	return &subcommands.Command{
   554  		UsageLine: "account-query <options>",
   555  		ShortDesc: "queries Gerrit for accounts",
   556  		LongDesc: `Queries Gerrit for accounts.
   557  
   558  Input should contain query options, e.g. {"params": <query parameters as JSON>}
   559  
   560  For more information on valid query parameters, see
   561  https://gerrit-review.googlesource.com/Documentation/user-search-accounts.html#_search_operators`,
   562  		CommandRun: func() subcommands.CommandRun {
   563  			return newChangeRun(authOpts, changeRunOptions{
   564  				queryInput: &gerrit.AccountQueryParams{},
   565  			}, runner)
   566  		},
   567  	}
   568  }