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 }