code: func parseCL(s string) (*pb.GerritChange, error) {
  	ret := &pb.GerritChange{}
  	var change, patchSet string
  	if m := regexCLCRRev.FindStringSubmatch(s); m != nil {
  		ret.Host = "chromium-review.googlesource.com"
  		if m[1] == "i" {
  			ret.Host = "chrome-internal-review.googlesource.com"
  		}
  		change = m[2]
  		patchSet = m[4]
  	} else if m := regexCL.FindStringSubmatch(s); m != nil {
  		ret.Host = m[1]
  		ret.Project = m[4]
  		change = m[5]
  		patchSet = m[7]
  	}
  
  	if ret.Host == "" {
  		return nil, fmt.Errorf("does not match r%q or r%q", regexCL, regexCLCRRev)
  	}
  
  	var err error
  	ret.Change, err = strconv.ParseInt(change, 10, 64)
  	if err != nil {
  		return nil, fmt.Errorf("invalid change %q: %s", change, err)
  	}
  
  	if patchSet != "" {
  		ret.Patchset, err = strconv.ParseInt(patchSet, 10, 64)
  		if err != nil {
  			return nil, fmt.Errorf("invalid patchset %q: %s", patchSet, err)
  		}
  	}
  
  	return ret, nil
  }
docs: // parseCL tries to retrieve a CL info from a string.
  //
  // It is not strict and can consume noisy strings, e.g.
  // https://chromium-review.googlesource.com/c/infra/luci/luci-go/+/1541677/7/buildbucket/cmd/bb/base_command.go
  // or incomplete strings, e.g.
  // https://chromium-review.googlesource.com/c/1541677
  //
  // Supports crrev.com.
  //
  // If err is nil, returned change is guaranteed to have Host and Change.