blob: 3f1b93b094b9707f020550172060a12607ac2f55 [file] [log] [blame]
// Copyright 2020 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package cqdepend parses CQ-Depend directives in CL description.
//
// CQ-Depend directives are special footer lines in CL descriptions.
// Footers are roughly "Key: arbitrary value" lines in the last paragraph of CL
// description, see more at:
// https://commondatastorage.googleapis.com/chrome-infra-docs/flat/depot_tools/docs/html/git-footers.html
//
// CQ-Depend footer starts with "CQ-Depend:" prefix and contains comma
// separated values:
//
// CQ-Depend: VALUE[,VALUE]*
//
// where each VALUE is:
//
// [SUBDOMAIN:]NUMBER
//
// where if SUBDOMAIN is specified, it is a subdomain w/o "-review" suffix of a
// Gerrit host, e.g., just "chrome-internal" for
// https://chrome-internal-review.googlesource.com Gerrit host.
//
// For example, if a CL on chromium-review.googlesource.com has the following
// description:
//
// Example CL with explicit dependencies.
//
// This: is footer, because it is in the last paragraph.
// It may have non "key: value" lines,
// the next line says CL depends on chromium-review.googlesource.com/123.
// CQ-Depend: 123
// and multiple CQ-Depend lines are allowed, such as next line saying
// this CL also depends on pdfium-review.googlesource.com/987.
// CQ-Depend: pdfium:987
// Prefix is just "pdfium" and "-review" is implicitly inferred.
// For practicality, the following is not valid:
// CQ-Depend: pdfium-review:456
// It's OK to specify multiple comma separated CQ-Depend values:
// CQ-Depend: 123,pdfium:987
//
//
// Error handling:
//
// * Valid CQ-Depend lines are ignored entirely if not in the last paragraph
// of CL's description (because they aren't Git/Gerrit footers).
// For example, in this description CQ-Depend is ignored:
//
// Title.
//
// CQ-Depend: valid:123
// But this isn't last paragraph.
//
// Change-Id: Ideadbeef
// This-is: the last paragraph.
//
// * Each CQ-Depend line is parsed independently; any invalid ones are
// silently ignored. For example,
// CQ-Depend: not valid, and so it is ignored.
// CQ-Depend: valid:123
//
// * In a single CQ-Depend line, each value is processed independently,
// and invalid values are ignored. For example:
// CQ-Depend: x:123, bad, 456
// will result in just 2 dependencies: "x:123" and "456".
//
// See also Lint method, which reports all such misuses of CQ-Depend.
package cqdepend
import (
"regexp"
"sort"
"strconv"
"strings"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/common/git/footer"
)
// cqDependKey is normalized key as used by git footer library.
const cqDependKey = "Cq-Depend"
// Dep is a Gerrit Change representing a dependency.
type Dep struct {
// Subdomain is set if change is on a different Gerrit host.
Subdomain string
Change int64
}
func (l *Dep) cmp(r Dep) int {
switch {
case l.Subdomain < r.Subdomain:
return 1
case l.Subdomain > r.Subdomain:
return -1
case l.Change < r.Change:
return 1
case l.Change > r.Change:
return -1
default:
return 0
}
}
// Parse returns all valid dependencies without duplicates.
func Parse(description string) (deps []Dep) {
for _, footerValue := range footer.ParseMessage(description)[cqDependKey] {
for _, v := range strings.Split(footerValue, ",") {
if dep, err := parseSingleDep(v); err == nil {
deps = append(deps, dep)
}
}
}
if len(deps) <= 1 {
return deps
}
sort.Slice(deps, func(i, j int) bool { return deps[i].cmp(deps[j]) == 1 })
// Remove duplicates. We don't use the map in the first place, because
// duplicates are highly unlikely in practice and sorting is nice for
// determinism.
l := 0
for i := 1; i < len(deps); i++ {
if d := deps[i]; d.cmp(deps[l]) != 0 {
l += 1
deps[l] = d
}
}
return deps[:l+1]
}
var valueRegexp = regexp.MustCompile(`^\s*(\w[-\w]*:)?(\d+)\s*$`)
func parseSingleDep(v string) (Dep, error) {
const errPrefix = "invalid format of CQ-Depend value %q: "
dep := Dep{}
switch res := valueRegexp.FindStringSubmatch(v); {
case len(res) != 3:
return dep, errors.Reason(errPrefix+"must match %q", v, valueRegexp.String()).Err()
case strings.HasSuffix(res[1], "-review:"):
return dep, errors.Reason(errPrefix+"must not include '-review'", v).Err()
case strings.HasSuffix(res[1], ":"):
dep.Subdomain = strings.ToLower(res[1][:len(res[1])-1])
fallthrough
default:
c, err := strconv.ParseInt(res[2], 10, 64)
if err != nil {
return dep, errors.Reason(errPrefix+"change number too large", v).Err()
}
dep.Change = c
return dep, nil
}
}
// Lint reports warnings or errors regarding CQ-Depend use.
func Lint(description string) error {
return errors.New("Not implemented")
}