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 }