go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/buildbucket/protoutil/tag.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 protoutil
    16  
    17  import (
    18  	"fmt"
    19  	"sort"
    20  	"strconv"
    21  	"strings"
    22  
    23  	"google.golang.org/protobuf/proto"
    24  
    25  	"go.chromium.org/luci/common/data/strpair"
    26  
    27  	pb "go.chromium.org/luci/buildbucket/proto"
    28  )
    29  
    30  // StringPairs converts a strpair.Map to a slice of StringPair messages.
    31  func StringPairs(m strpair.Map) []*pb.StringPair {
    32  	ret := make([]*pb.StringPair, 0, len(m))
    33  	for k, vs := range m {
    34  		for _, v := range vs {
    35  			ret = append(ret, &pb.StringPair{Key: k, Value: v})
    36  		}
    37  	}
    38  	SortStringPairs(ret)
    39  	return ret
    40  }
    41  
    42  // SortStringPairs sorts string pairs.
    43  func SortStringPairs(pairs []*pb.StringPair) {
    44  	sort.Slice(pairs, func(i, j int) bool {
    45  		switch {
    46  		case pairs[i].Key < pairs[j].Key:
    47  			return true
    48  		case pairs[i].Key > pairs[j].Key:
    49  			return false
    50  		default:
    51  			return pairs[i].Value < pairs[j].Value
    52  		}
    53  	})
    54  }
    55  
    56  // StringPairMap converts a slice of StringPair messages to a strpair.Map.
    57  func StringPairMap(pairs []*pb.StringPair) strpair.Map {
    58  	m := make(strpair.Map)
    59  	for _, pair := range pairs {
    60  		m.Add(pair.Key, pair.Value)
    61  	}
    62  	return m
    63  }
    64  
    65  // BuildSets returns all of the buildsets of the build.
    66  func BuildSets(b *pb.Build) []string {
    67  	var result []string
    68  	for _, tag := range b.Tags {
    69  		if tag.Key == "buildset" {
    70  			result = append(result, tag.Value)
    71  		}
    72  	}
    73  	return result
    74  }
    75  
    76  // GerritBuildSet returns a buildset representation of c,
    77  // e.g. "patch/gerrit/chromium-review.googlesource.com/677784/5"
    78  func GerritBuildSet(c *pb.GerritChange) string {
    79  	return fmt.Sprintf("patch/gerrit/%s/%d/%d", c.Host, c.Change, c.Patchset)
    80  }
    81  
    82  // GerritChangeURL returns URL of the change.
    83  func GerritChangeURL(c *pb.GerritChange) string {
    84  	return fmt.Sprintf("https://%s/c/%d/%d", c.Host, c.Change, c.Patchset)
    85  }
    86  
    87  // GitilesBuildSet returns a buildset representation of c.
    88  // e.g. "commit/gitiles/chromium.googlesource.com/infra/luci/luci-go/+/b7a757f457487cd5cfe2dae83f65c5bc10e288b7"
    89  //
    90  // Returns an empty string if not all of {Host, Project, Id} are set.
    91  func GitilesBuildSet(c *pb.GitilesCommit) string {
    92  	if c.Host == "" || c.Project == "" || c.Id == "" {
    93  		return ""
    94  	}
    95  	return fmt.Sprintf("commit/gitiles/%s/%s/+/%s", c.Host, c.Project, c.Id)
    96  }
    97  
    98  // GitilesRepoURL returns the URL for the gitiles repo.
    99  // e.g. "https://chromium.googlesource.com/chromium/src"
   100  func GitilesRepoURL(c *pb.GitilesCommit) string {
   101  	return fmt.Sprintf("https://%s/%s", c.Host, c.Project)
   102  }
   103  
   104  // GitilesCommitURL returns the URL for the gitiles commit.
   105  // e.g. "https://chromium.googlesource.com/chromium/src/+/b7a757f457487cd5cfe2dae83f65c5bc10e288b7"
   106  // or "https://chromium.googlesource.com/chromium/src/+/refs/heads/master"
   107  // if id is not available.
   108  func GitilesCommitURL(c *pb.GitilesCommit) string {
   109  	suffix := c.Id
   110  	if suffix == "" {
   111  		suffix = c.Ref
   112  	}
   113  	return fmt.Sprintf("%s/+/%s", GitilesRepoURL(c), suffix)
   114  }
   115  
   116  // ParseBuildSet tries to parse buildset as one of the known formats.
   117  // May return *pb.GerritChange, *pb.GitilesCommit
   118  // or nil.
   119  func ParseBuildSet(buildSet string) proto.Message {
   120  	// fmt.Sscanf cannot be used for this parsing because
   121  	//   var a, b string
   122  	//   fmt.Scanf("a/b", "%s/%s", &a, &b)
   123  	//   a == "a/b", b == ""
   124  
   125  	p := strings.Split(buildSet, "/")
   126  	for _, c := range p {
   127  		if c == "" {
   128  			return nil
   129  		}
   130  	}
   131  	n := len(p)
   132  	switch {
   133  	case n == 5 && p[0] == "patch" && p[1] == "gerrit":
   134  		gerrit := &pb.GerritChange{
   135  			Host: p[2],
   136  		}
   137  		var err error
   138  		if gerrit.Change, err = strconv.ParseInt(p[3], 10, 64); err != nil {
   139  			return nil
   140  		}
   141  		if gerrit.Patchset, err = strconv.ParseInt(p[4], 10, 64); err != nil {
   142  			return nil
   143  		}
   144  		return gerrit
   145  
   146  	case n >= 5 && p[0] == "commit" && p[1] == "gitiles":
   147  		if p[n-2] != "+" || !looksLikeSha1(p[n-1]) {
   148  			return nil
   149  		}
   150  		return &pb.GitilesCommit{
   151  			Host:    p[2],
   152  			Project: strings.Join(p[3:n-2], "/"), // exclude plus
   153  			Id:      p[n-1],
   154  		}
   155  
   156  	default:
   157  		return nil
   158  	}
   159  }
   160  
   161  func looksLikeSha1(s string) bool {
   162  	if len(s) != 40 {
   163  		return false
   164  	}
   165  	for _, c := range s {
   166  		switch {
   167  		case '0' <= c && c <= '9':
   168  		case 'a' <= c && c <= 'f':
   169  		default:
   170  			return false
   171  		}
   172  	}
   173  	return true
   174  }