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 }