go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/led/ledcli/edit_cl.go (about) 1 // Copyright 2020 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 ledcli 16 17 import ( 18 "context" 19 "net/http" 20 "net/url" 21 "strconv" 22 "strings" 23 24 "google.golang.org/grpc/codes" 25 "google.golang.org/grpc/status" 26 27 "github.com/maruel/subcommands" 28 29 "go.chromium.org/luci/auth" 30 bbpb "go.chromium.org/luci/buildbucket/proto" 31 gerritapi "go.chromium.org/luci/common/api/gerrit" 32 "go.chromium.org/luci/common/errors" 33 "go.chromium.org/luci/common/logging" 34 gerritpb "go.chromium.org/luci/common/proto/gerrit" 35 "go.chromium.org/luci/led/job" 36 ) 37 38 func editGerritCLCmd(opts cmdBaseOptions) *subcommands.Command { 39 return &subcommands.Command{ 40 UsageLine: "edit-gerrit-cl [-remove|-no-implicit-clear] URL_TO_CHANGELIST", 41 ShortDesc: "sets Gerrit CL-related properties on this JobDefinition (for experimenting with tryjobs)", 42 LongDesc: `This allows you to edit a JobDefinition and associate a changelist with 43 it, as if the job was triggered via Gerrit. 44 45 Recognized URL forms: 46 https://<gerrit_host>/<change> 47 https://<gerrit_host>/c/<path/to/project>/+/<change> 48 https://<gerrit_host>/c/<path/to/project>/+/<change>/<patchset> 49 50 If you provide URLs in one of the first two forms and <gerrit_host> has public read 51 access, this will fill in the missing information for the change. Otherwise, this will 52 fail and ask you to provide the full URL containing host, project, change and patchset. 53 54 By default, when adding a CL, this will clear all existing CLs on the job, unless 55 you pass -no-implicit-clear. Most jobs only expect one CL, so this implicit clearing 56 behavior is for CLI ergonomic reasons. 57 `, 58 59 CommandRun: func() subcommands.CommandRun { 60 ret := &cmdEditCl{} 61 ret.initFlags(opts) 62 return ret 63 }, 64 } 65 } 66 67 func editCrCLCmd(opts cmdBaseOptions) *subcommands.Command { 68 return &subcommands.Command{ 69 UsageLine: "edit-cr-cl [-remove|-no-implicit-clear] URL_TO_CHANGELIST", 70 ShortDesc: "DEPRECATED: sets Gerrit CL-related properties on this JobDefinition (for experimenting with tryjobs)", 71 LongDesc: `This command functions identically to edit-gerrit-cl but has a Chrome-specific 72 name for historical reasons. It's kept for backwards compatibility. Prefer to use edit-gerrit-cl instead. 73 `, 74 Advanced: true, 75 CommandRun: func() subcommands.CommandRun { 76 ret := &cmdEditCl{} 77 ret.initFlags(opts) 78 ret.printDeprecationWarning = true 79 return ret 80 }, 81 } 82 } 83 84 type cmdEditCl struct { 85 cmdBase 86 87 gerritChange *bbpb.GerritChange 88 remove bool 89 noImplicitClear bool 90 printDeprecationWarning bool 91 } 92 93 func (c *cmdEditCl) initFlags(opts cmdBaseOptions) { 94 c.Flags.BoolVar(&c.remove, "remove", false, "If provided, will remove the given CL instead of adding it.") 95 c.Flags.BoolVar(&c.noImplicitClear, "no-implicit-clear", false, 96 "If provided, will not clear existing CLs when adding a new one.") 97 c.cmdBase.initFlags(opts) 98 } 99 100 func (c *cmdEditCl) jobInput() bool { return true } 101 func (c *cmdEditCl) positionalRange() (min, max int) { return 1, 1 } 102 103 type changeResolver func(host string, change int64) (proj string, ps int64, err error) 104 105 func parseCrChangeListURL(clURL string, resolveChange changeResolver) (*bbpb.GerritChange, error) { 106 p, err := url.Parse(clURL) 107 if err != nil { 108 return nil, errors.Annotate(err, "URL_TO_CHANGELIST").Err() 109 } 110 p.Host = strings.ReplaceAll(p.Host, ".git.corp.google.com", ".googlesource.com") 111 if !strings.HasSuffix(p.Hostname(), "-review.googlesource.com") { 112 return nil, errors.New("only *-review.googlesource.com URLs are supported") 113 } 114 115 var toks []string 116 if trimPath := strings.Trim(p.Path, "/"); len(trimPath) > 0 { 117 toks = strings.Split(trimPath, "/") 118 } 119 120 if len(toks) == 0 { 121 // https://<gerrit_host>/#/c/<change> 122 // https://<gerrit_host>/#/c/<change>/<patchset> 123 return nil, errors.Reason("old/empty gerrit URL: %q", clURL).Err() 124 } 125 126 var projectToks []string 127 var changePatchsetToks []string 128 129 if toks[0] == "c" { 130 toks = toks[1:] // remove "c" 131 // toks == v----------------------------------v 132 // https://<gerrit_host>/c/<change> 133 // https://<gerrit_host>/c/<change>/<patchset> 134 // https://<gerrit_host>/c/<project/path>/+/<change> 135 // https://<gerrit_host>/c/<project/path>/+/<change>/<patchset> 136 for i, tok := range toks { 137 if tok == "+" { 138 projectToks, changePatchsetToks = toks[:i], toks[i+1:] 139 break 140 } 141 } 142 if len(projectToks) == 0 { 143 return nil, errors.Reason("gerrit URL missing project: %q", clURL).Err() 144 } 145 } else if len(toks) == 1 { 146 // toks == v------v 147 // https://<gerrit_host>/<change> 148 changePatchsetToks = toks 149 } else { 150 return nil, errors.Reason("Unknown changelist URL format: %q", clURL).Err() 151 } 152 153 if len(changePatchsetToks) == 0 { 154 return nil, errors.Reason("gerrit URL missing change/patchset: %q", clURL).Err() 155 } 156 157 ret := &bbpb.GerritChange{ 158 Host: p.Hostname(), 159 Project: strings.Join(projectToks, "/"), 160 } 161 ret.Change, err = strconv.ParseInt(changePatchsetToks[0], 10, 64) 162 if err != nil { 163 return nil, errors.Reason("gerrit URL parsing change %q from %q", changePatchsetToks[0], clURL).Err() 164 } 165 if len(changePatchsetToks) > 1 { 166 ret.Patchset, err = strconv.ParseInt(changePatchsetToks[1], 10, 64) 167 if err != nil { 168 return nil, errors.Reason("gerrit URL parsing patchset %q from %q", changePatchsetToks[1], clURL).Err() 169 } 170 } else { 171 ret.Project, ret.Patchset, err = resolveChange(ret.Host, ret.Change) 172 if err != nil { 173 return nil, errors.Annotate( 174 err, "resolving patchset from Gerrit Url %q", clURL).Err() 175 } 176 } 177 178 return ret, nil 179 } 180 181 func gerritResolver(ctx context.Context) changeResolver { 182 return func(host string, change int64) (string, int64, error) { 183 // TODO(crbug/1211623): allow authentication for internal hosts. 184 gc, err := gerritapi.NewRESTClient(http.DefaultClient, host, false) 185 if err != nil { 186 return "", 0, errors.Annotate(err, "creating new gerrit client").Err() 187 } 188 ci, err := gc.GetChange(ctx, &gerritpb.GetChangeRequest{ 189 Number: change, 190 Options: []gerritpb.QueryOption{ 191 gerritpb.QueryOption_CURRENT_REVISION, 192 }, 193 }) 194 if status.Code(err) == codes.Unauthenticated { 195 return "", 0, errors.Annotate(err, 196 "Gerrit host %q requires authentication and URL did not include project and/or patchset. "+ 197 "Please include the project and patchset you want in your URL (patchset can be "+ 198 "ignored by setting it to `0`).", host, 199 ).Err() 200 } 201 if err != nil { 202 return "", 0, errors.Annotate(err, "GetChange").Err() 203 } 204 205 // There's only one. 206 for _, rd := range ci.Revisions { 207 return ci.Project, int64(rd.Number), nil 208 } 209 panic("impossible") 210 } 211 } 212 213 func (c *cmdEditCl) validateFlags(ctx context.Context, positionals []string, _ subcommands.Env) (err error) { 214 if c.remove && c.noImplicitClear { 215 return errors.New("cannot specify both -remove and -no-implicit-clear") 216 } 217 218 c.gerritChange, err = parseCrChangeListURL(positionals[0], gerritResolver(ctx)) 219 return errors.Annotate(err, "invalid URL_TO_CHANGESET").Err() 220 } 221 222 func (c *cmdEditCl) execute(ctx context.Context, _ *http.Client, _ auth.Options, inJob *job.Definition) (out any, err error) { 223 if c.printDeprecationWarning { 224 logging.Warningf(ctx, "'edit-cr-cl' is a deprecated alias, please use 'edit-gerrit-cl'.") 225 } 226 return inJob, inJob.HighLevelEdit(func(je job.HighLevelEditor) { 227 if c.remove { 228 je.RemoveGerritChange(c.gerritChange) 229 } else { 230 if !c.noImplicitClear { 231 je.ClearGerritChanges() 232 } 233 je.AddGerritChange(c.gerritChange) 234 } 235 }) 236 } 237 238 func (c *cmdEditCl) Run(a subcommands.Application, args []string, env subcommands.Env) int { 239 return c.doContextExecute(a, c, args, env) 240 }