go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/buildbucket/cli/cl.go (about) 1 // Copyright 2019 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 cli 16 17 import ( 18 "context" 19 "flag" 20 "fmt" 21 "net/http" 22 "regexp" 23 "strconv" 24 25 "go.chromium.org/luci/common/api/gerrit" 26 "go.chromium.org/luci/common/sync/parallel" 27 28 pb "go.chromium.org/luci/buildbucket/proto" 29 luciflag "go.chromium.org/luci/common/flag" 30 gerritpb "go.chromium.org/luci/common/proto/gerrit" 31 ) 32 33 type clsFlag struct { 34 cls []string 35 } 36 37 func (f *clsFlag) Register(fs *flag.FlagSet, help string) { 38 fs.Var(luciflag.StringSlice(&f.cls), "cl", help) 39 } 40 41 const kRequirePatchset = true 42 43 // retrieveCLs retrieves GerritChange objects from f.cls. 44 // Makes Gerrit RPCs if necessary, in parallel. 45 func (f *clsFlag) retrieveCLs(ctx context.Context, httpClient *http.Client, requirePatchset bool) ([]*pb.GerritChange, error) { 46 ret := make([]*pb.GerritChange, len(f.cls)) 47 return ret, parallel.FanOutIn(func(work chan<- func() error) { 48 for i, cl := range f.cls { 49 i := i 50 cl := cl 51 work <- func() error { 52 change, err := f.retrieveCL(ctx, cl, httpClient, requirePatchset) 53 if err != nil { 54 return fmt.Errorf("CL %q: %s", cl, err) 55 } 56 ret[i] = change 57 return nil 58 } 59 } 60 }) 61 } 62 63 // retrieveCL retrieves a GerritChange from a string. 64 // Makes a Gerrit RPC if necessary. 65 func (f *clsFlag) retrieveCL(ctx context.Context, cl string, httpClient *http.Client, requirePatchset bool) (*pb.GerritChange, error) { 66 ret, err := parseCL(cl) 67 switch { 68 case err != nil: 69 return nil, err 70 case requirePatchset && ret.Patchset == 0: 71 return nil, fmt.Errorf("missing patchset number") 72 case ret.Project != "" && ret.Patchset != 0: 73 return ret, nil 74 } 75 76 // Fetch CL info from Gerrit. 77 client, err := gerrit.NewRESTClient(httpClient, ret.Host, true) 78 if err != nil { 79 return nil, err 80 } 81 change, err := client.GetChange(ctx, &gerritpb.GetChangeRequest{ 82 Number: ret.Change, 83 Options: []gerritpb.QueryOption{gerritpb.QueryOption_CURRENT_REVISION}, 84 }) 85 if err != nil { 86 return nil, fmt.Errorf("failed to fetch CL %d from %q: %s", ret.Change, ret.Host, err) 87 } 88 89 ret.Project = change.Project 90 if ret.Patchset == 0 { 91 ret.Patchset = int64(change.Revisions[change.CurrentRevision].Number) 92 } 93 return ret, nil 94 } 95 96 var regexCL = regexp.MustCompile(`((\w+-)+review\.googlesource\.com)/(#/)?c/(([^\+]+)/\+/)?(\d+)(/(\d+))?`) 97 var regexCLCRRev = regexp.MustCompile(`crrev\.com/([ci])/(\d+)(/(\d+))?`) 98 99 // parseCL tries to retrieve a CL info from a string. 100 // 101 // It is not strict and can consume noisy strings, e.g. 102 // https://chromium-review.googlesource.com/c/infra/luci/luci-go/+/1541677/7/buildbucket/cmd/bb/base_command.go 103 // or incomplete strings, e.g. 104 // https://chromium-review.googlesource.com/c/1541677 105 // 106 // Supports crrev.com. 107 // 108 // If err is nil, returned change is guaranteed to have Host and Change. 109 func parseCL(s string) (*pb.GerritChange, error) { 110 ret := &pb.GerritChange{} 111 var change, patchSet string 112 if m := regexCLCRRev.FindStringSubmatch(s); m != nil { 113 ret.Host = "chromium-review.googlesource.com" 114 if m[1] == "i" { 115 ret.Host = "chrome-internal-review.googlesource.com" 116 } 117 change = m[2] 118 patchSet = m[4] 119 } else if m := regexCL.FindStringSubmatch(s); m != nil { 120 ret.Host = m[1] 121 ret.Project = m[5] 122 change = m[6] 123 patchSet = m[8] 124 } 125 126 if ret.Host == "" { 127 return nil, fmt.Errorf("does not match r%q or r%q", regexCL, regexCLCRRev) 128 } 129 130 var err error 131 ret.Change, err = strconv.ParseInt(change, 10, 64) 132 if err != nil { 133 return nil, fmt.Errorf("invalid change %q: %s", change, err) 134 } 135 136 if patchSet != "" { 137 ret.Patchset, err = strconv.ParseInt(patchSet, 10, 64) 138 if err != nil { 139 return nil, fmt.Errorf("invalid patchset %q: %s", patchSet, err) 140 } 141 } 142 143 return ret, nil 144 }