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  }