go.fuchsia.dev/infra@v0.0.0-20240507153436-9b593402251b/cmd/cl-util/create_cl.go (about)

     1  // Copyright 2020 The Fuchsia Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style license that can be
     3  // found in the LICENSE file.
     4  
     5  package main
     6  
     7  import (
     8  	"context"
     9  	"encoding/json"
    10  	"errors"
    11  	"fmt"
    12  	"os"
    13  	"strings"
    14  
    15  	"github.com/maruel/subcommands"
    16  	"go.chromium.org/luci/auth"
    17  	"go.chromium.org/luci/common/logging"
    18  	"go.chromium.org/luci/common/logging/gologger"
    19  
    20  	gerritpb "go.chromium.org/luci/common/proto/gerrit"
    21  	"go.fuchsia.dev/infra/gerrit"
    22  	"go.fuchsia.dev/infra/gitiles"
    23  )
    24  
    25  func cmdCreateCL(authOpts auth.Options) *subcommands.Command {
    26  	return &subcommands.Command{
    27  		UsageLine: "create-cl -host <gerrit-host> -project <gerrit-project> -subject <cl-subject> -file-edit <filepath1>:<contents1>, ... -json-output <json-output> [-ref <ref>]",
    28  		ShortDesc: "Create a CL with file edits.",
    29  		LongDesc:  "Create a CL with file edits.",
    30  		CommandRun: func() subcommands.CommandRun {
    31  			c := &createCLRun{}
    32  			c.Init(authOpts)
    33  			return c
    34  		},
    35  	}
    36  }
    37  
    38  type createCLRun struct {
    39  	commonFlags
    40  	edits      fileEdits
    41  	subject    string
    42  	jsonOutput string
    43  	ref        string
    44  }
    45  
    46  func (c *createCLRun) Init(defaultAuthOpts auth.Options) {
    47  	c.commonFlags.Init(defaultAuthOpts)
    48  	c.Flags.StringVar(&c.subject, "subject", "", "CL subject.")
    49  	c.Flags.Var(&c.edits, "file-edit", "filepath:content pairs. Repeatable.")
    50  	c.Flags.StringVar(&c.jsonOutput, "json-output", "", "Path to write gerrit.ChangeInfo to.")
    51  	c.Flags.StringVar(&c.ref, "ref", "refs/heads/main", "Ref to author CL against.")
    52  }
    53  
    54  func (c *createCLRun) Parse(a subcommands.Application, args []string) error {
    55  	if err := c.commonFlags.Parse(); err != nil {
    56  		return err
    57  	}
    58  	if c.subject == "" {
    59  		return errors.New("-subject is required")
    60  	}
    61  	if len(c.edits) == 0 {
    62  		return errors.New("at least one -file-edit is required")
    63  	}
    64  	if c.jsonOutput == "" {
    65  		return errors.New("-json-output is required")
    66  	}
    67  	return nil
    68  }
    69  
    70  // fileEdit represents a file to edit in a repository.
    71  type fileEdit struct {
    72  	filepath string
    73  	contents string
    74  }
    75  
    76  // String returns a string representation of the file edit.
    77  func (e *fileEdit) String() string {
    78  	return fmt.Sprintf("%s:%s", e.filepath, e.contents)
    79  }
    80  
    81  // newFileEdit returns a fileEdit for a filepath:contents string.
    82  func newFileEdit(editStr string) (*fileEdit, error) {
    83  	fp, c, found := strings.Cut(editStr, ":")
    84  	if !found {
    85  		return nil, fmt.Errorf("%q is not of format filepath:contents", editStr)
    86  	}
    87  	return &fileEdit{filepath: fp, contents: c}, nil
    88  }
    89  
    90  // fileEdits is a flag.Getter implementation representing a []*fileEdit.
    91  type fileEdits []*fileEdit
    92  
    93  // String returns a comma-separated string representation of the flag file edits.
    94  func (f fileEdits) String() string {
    95  	strs := make([]string, len(f))
    96  	for i, edit := range f {
    97  		strs[i] = edit.String()
    98  	}
    99  	return strings.Join(strs, ", ")
   100  }
   101  
   102  // Set records seeing a flag value.
   103  func (f *fileEdits) Set(val string) error {
   104  	fe, err := newFileEdit(val)
   105  	if err != nil {
   106  		return err
   107  	}
   108  	*f = append(*f, fe)
   109  	return nil
   110  }
   111  
   112  // Get retrieves the flag value.
   113  func (f fileEdits) Get() any {
   114  	return []*fileEdit(f)
   115  }
   116  
   117  func (c *createCLRun) main(a subcommands.Application) error {
   118  	ctx := context.Background()
   119  	ctx = logging.SetLevel(ctx, c.logLevel)
   120  	ctx = gologger.StdConfig.Use(ctx)
   121  	authClient, err := newAuthClient(ctx, c.parsedAuthOpts)
   122  	if err != nil {
   123  		return err
   124  	}
   125  	gitilesClient, err := gitiles.NewClient(strings.Replace(c.gerritHost, "-review", "", 1), c.gerritProject, authClient)
   126  	if err != nil {
   127  		return err
   128  	}
   129  	gerritClient, err := gerrit.NewClient(c.gerritHost, c.gerritProject, authClient)
   130  	if err != nil {
   131  		return err
   132  	}
   133  	commit, err := gitilesClient.LatestCommit(ctx, c.ref)
   134  	if err != nil {
   135  		return err
   136  	}
   137  	changeInfo, err := gerritClient.CreateChange(ctx, c.subject, commit, c.ref)
   138  	if err != nil {
   139  		return err
   140  	}
   141  	for _, edit := range c.edits {
   142  		if err := gerritClient.EditFile(ctx, changeInfo.Number, edit.filepath, edit.contents); err != nil {
   143  			return err
   144  		}
   145  	}
   146  	if err := gerritClient.PublishEdits(ctx, changeInfo.Number); err != nil {
   147  		return err
   148  	}
   149  	// Grab post-edit ChangeInfo prior to dumping to JSON.
   150  	changeInfo, err = gerritClient.GetChange(ctx, changeInfo.Number, gerritpb.QueryOption_ALL_REVISIONS)
   151  	if err != nil {
   152  		return err
   153  	}
   154  	out := os.Stdout
   155  	if c.jsonOutput != "-" {
   156  		out, err = os.Create(c.jsonOutput)
   157  		if err != nil {
   158  			return err
   159  		}
   160  		defer out.Close()
   161  	}
   162  	if err := json.NewEncoder(out).Encode(changeInfo); err != nil {
   163  		return fmt.Errorf("failed to encode: %w", err)
   164  	}
   165  	return nil
   166  }
   167  
   168  func (c *createCLRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
   169  	if err := c.Parse(a, args); err != nil {
   170  		fmt.Fprintf(a.GetErr(), "%s: %s\n", a.GetName(), err)
   171  		return 1
   172  	}
   173  
   174  	if err := c.main(a); err != nil {
   175  		fmt.Fprintf(a.GetErr(), "%s: %s\n", a.GetName(), err)
   176  		return 1
   177  	}
   178  	return 0
   179  }