go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/changelist/external_id.go (about)

     1  // Copyright 2021 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 changelist
    16  
    17  import (
    18  	"fmt"
    19  	"strconv"
    20  	"strings"
    21  
    22  	"go.chromium.org/luci/common/errors"
    23  )
    24  
    25  // ExternalID is a unique CL ID deterministically constructed based on CL data.
    26  //
    27  // Currently, only Gerrit is supported.
    28  type ExternalID string
    29  
    30  // GobID makes an ExternalID for a Gerrit CL.
    31  //
    32  // Host is typically "something-review.googlesource.com".
    33  // Change is a number, e.g. 2515619 for
    34  // https://chromium-review.googlesource.com/c/infra/luci/luci-go/+/2515619
    35  func GobID(host string, change int64) (ExternalID, error) {
    36  	if strings.ContainsRune(host, '/') {
    37  		return "", errors.Reason("invalid host %q: must not contain /", host).Err()
    38  	}
    39  	return ExternalID(fmt.Sprintf("gerrit/%s/%d", host, change)), nil
    40  }
    41  
    42  // MustGobID is like GobID but panics on error.
    43  func MustGobID(host string, change int64) ExternalID {
    44  	ret, err := GobID(host, change)
    45  	if err != nil {
    46  		panic(err)
    47  	}
    48  	return ret
    49  }
    50  
    51  // ParseGobID returns Gerrit host and change if this is a GobID.
    52  func (eid ExternalID) ParseGobID() (host string, change int64, err error) {
    53  	parts := strings.Split(string(eid), "/")
    54  	if len(parts) != 3 || parts[0] != "gerrit" {
    55  		err = errors.Reason("%q is not a valid GobID", eid).Err()
    56  		return
    57  	}
    58  	host = parts[1]
    59  	change, err = strconv.ParseInt(parts[2], 10, 63)
    60  	if err != nil {
    61  		err = errors.Annotate(err, "%q is not a valid GobID", eid).Err()
    62  	}
    63  	return
    64  }
    65  
    66  // URL returns URL of the CL.
    67  func (eid ExternalID) URL() (string, error) {
    68  	parts := strings.Split(string(eid), "/")
    69  	if len(parts) < 2 {
    70  		return "", errors.Reason("invalid ExternalID: %q", eid).Err()
    71  	}
    72  	switch kind := parts[0]; kind {
    73  	case "gerrit":
    74  		return fmt.Sprintf("https://%s/c/%s", parts[1], parts[2]), nil
    75  	default:
    76  		return "", errors.Reason("unrecognized ExternalID: %q", eid).Err()
    77  	}
    78  }
    79  
    80  // MustURL is like `URL()` but panic on err.
    81  func (eid ExternalID) MustURL() string {
    82  	ret, err := eid.URL()
    83  	if err != nil {
    84  		panic(err)
    85  	}
    86  	return ret
    87  }
    88  
    89  func (eid ExternalID) kind() (string, error) {
    90  	s := string(eid)
    91  	idx := strings.IndexRune(s, '/')
    92  	if idx <= 0 {
    93  		return "", errors.Reason("invalid ExternalID: %q", s).Err()
    94  	}
    95  	return s[:idx], nil
    96  }
    97  
    98  // JoinExternalURLs the URL of given ExternalIDs.
    99  //
   100  // Panics if any of the ExternalIDs is invalid.
   101  func JoinExternalURLs(ids []ExternalID, sep string) string {
   102  	var s strings.Builder
   103  	for i, id := range ids {
   104  		fmt.Fprint(&s, id.MustURL())
   105  		if i != len(ids)-1 {
   106  			fmt.Fprint(&s, sep)
   107  		}
   108  	}
   109  	return s.String()
   110  }