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 }