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

     1  // Copyright 2020 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 cqdepend parses CQ-Depend directives in CL description.
    16  //
    17  // CQ-Depend directives are special footer lines in CL descriptions.
    18  // Footers are roughly "Key: arbitrary value" lines in the last paragraph of CL
    19  // description, see more at:
    20  // https://commondatastorage.googleapis.com/chrome-infra-docs/flat/depot_tools/docs/html/git-footers.html
    21  //
    22  // CQ-Depend footer starts with "CQ-Depend:" prefix and contains comma
    23  // separated values:
    24  //
    25  //	CQ-Depend: VALUE[,VALUE]*
    26  //
    27  // where each VALUE is:
    28  //
    29  //	[SUBDOMAIN:]NUMBER
    30  //
    31  // where if SUBDOMAIN is specified, it is a subdomain w/o "-review" suffix of a
    32  // Gerrit host, e.g., just "chrome-internal" for
    33  // https://chrome-internal-review.googlesource.com Gerrit host.
    34  //
    35  // For example, if a CL on chromium-review.googlesource.com has the following
    36  // description:
    37  //
    38  //	Example CL with explicit dependencies.
    39  //
    40  //	This: is footer, because it is in the last paragraph.
    41  //	  It may have non "key: value" lines,
    42  //	  the next line says CL depends on chromium-review.googlesource.com/123.
    43  //	CQ-Depend: 123
    44  //	  and multiple CQ-Depend lines are allowed, such as next line saying
    45  //	  this CL also depends on pdfium-review.googlesource.com/987.
    46  //	CQ-Depend: pdfium:987
    47  //	  Prefix is just "pdfium" and "-review" is implicitly inferred.
    48  //	  For practicality, the following is not valid:
    49  //	CQ-Depend: pdfium-review:456
    50  //	  It's OK to specify multiple comma separated CQ-Depend values:
    51  //	CQ-Depend: 123,pdfium:987
    52  //
    53  // Error handling:
    54  //
    55  //   - Valid CQ-Depend lines are ignored entirely if not in the last paragraph
    56  //     of CL's description (because they aren't Git/Gerrit footers).
    57  //     For example, in this description CQ-Depend is ignored:
    58  //
    59  //     Title.
    60  //
    61  //     CQ-Depend: valid:123
    62  //     But this isn't last paragraph.
    63  //
    64  //     Change-Id: Ideadbeef
    65  //     This-is: the last paragraph.
    66  //
    67  //   - Each CQ-Depend line is parsed independently; any invalid ones are
    68  //     silently ignored. For example,
    69  //     CQ-Depend: not valid, and so it is ignored.
    70  //     CQ-Depend: valid:123
    71  //
    72  //   - In a single CQ-Depend line, each value is processed independently,
    73  //     and invalid values are ignored. For example:
    74  //     CQ-Depend: x:123, bad, 456
    75  //     will result in just 2 dependencies: "x:123" and "456".
    76  //
    77  // See also Lint method, which reports all such misuses of CQ-Depend.
    78  package cqdepend
    79  
    80  import (
    81  	"regexp"
    82  	"sort"
    83  	"strconv"
    84  	"strings"
    85  
    86  	"go.chromium.org/luci/common/errors"
    87  	"go.chromium.org/luci/common/git/footer"
    88  )
    89  
    90  // cqDependKey is normalized key as used by git footer library.
    91  const cqDependKey = "Cq-Depend"
    92  
    93  // Dep is a Gerrit Change representing a dependency.
    94  type Dep struct {
    95  	// Subdomain is set if change is on a different Gerrit host.
    96  	Subdomain string
    97  	Change    int64
    98  }
    99  
   100  func (l *Dep) cmp(r Dep) int {
   101  	switch {
   102  	case l.Subdomain < r.Subdomain:
   103  		return 1
   104  	case l.Subdomain > r.Subdomain:
   105  		return -1
   106  	case l.Change < r.Change:
   107  		return 1
   108  	case l.Change > r.Change:
   109  		return -1
   110  	default:
   111  		return 0
   112  	}
   113  }
   114  
   115  // Parse returns all valid dependencies without duplicates.
   116  func Parse(description string) (deps []Dep) {
   117  	for _, footerValue := range footer.ParseMessage(description)[cqDependKey] {
   118  		for _, v := range strings.Split(footerValue, ",") {
   119  			if dep, err := parseSingleDep(v); err == nil {
   120  				deps = append(deps, dep)
   121  			}
   122  		}
   123  	}
   124  	if len(deps) <= 1 {
   125  		return deps
   126  	}
   127  	sort.Slice(deps, func(i, j int) bool { return deps[i].cmp(deps[j]) == 1 })
   128  	// Remove duplicates. We don't use the map in the first place, because
   129  	// duplicates are highly unlikely in practice and sorting is nice for
   130  	// determinism.
   131  	l := 0
   132  	for i := 1; i < len(deps); i++ {
   133  		if d := deps[i]; d.cmp(deps[l]) != 0 {
   134  			l += 1
   135  			deps[l] = d
   136  		}
   137  	}
   138  	return deps[:l+1]
   139  }
   140  
   141  var valueRegexp = regexp.MustCompile(`^\s*(\w[-\w]*:)?(\d+)\s*$`)
   142  
   143  func parseSingleDep(v string) (Dep, error) {
   144  	const errPrefix = "invalid format of CQ-Depend value %q: "
   145  	dep := Dep{}
   146  	switch res := valueRegexp.FindStringSubmatch(v); {
   147  	case len(res) != 3:
   148  		return dep, errors.Reason(errPrefix+"must match %q", v, valueRegexp.String()).Err()
   149  	case strings.HasSuffix(res[1], "-review:"):
   150  		return dep, errors.Reason(errPrefix+"must not include '-review'", v).Err()
   151  	case strings.HasSuffix(res[1], ":"):
   152  		dep.Subdomain = strings.ToLower(res[1][:len(res[1])-1])
   153  		fallthrough
   154  	default:
   155  		c, err := strconv.ParseInt(res[2], 10, 64)
   156  		if err != nil {
   157  			return dep, errors.Reason(errPrefix+"change number too large", v).Err()
   158  		}
   159  		dep.Change = c
   160  		return dep, nil
   161  	}
   162  }
   163  
   164  // Lint reports warnings or errors regarding CQ-Depend use.
   165  func Lint(description string) error {
   166  	return errors.New("Not implemented")
   167  }