treewide: Move device_tree to commonlib
[coreboot2.git] / util / scripts / maintainers.go
blob53beb2ccf73da10d7d92094c14a986fa31716076
1 /* SPDX-License-Identifier: GPL-2.0-only */
3 package main
5 import (
6 "bufio"
7 "flag"
8 "fmt"
9 "log"
10 "os"
11 "os/exec"
12 "regexp"
13 "strings"
16 type subsystem struct {
17 name string
18 maintainer []string
19 paths []string
20 globs []*regexp.Regexp
23 var subsystems []subsystem
25 func get_git_files() ([]string, error) {
26 var files []string
28 /* Read in list of all files in the git repository */
29 cmd := exec.Command("git", "ls-files")
30 out, err := cmd.StdoutPipe()
31 if err != nil {
32 log.Fatalf("git ls-files failed: %v", err)
33 return files, err
35 if err := cmd.Start(); err != nil {
36 log.Fatalf("Could not start %v: %v", cmd, err)
37 return files, err
40 r := bufio.NewScanner(out)
41 for r.Scan() {
42 /* Cut out leading tab */
43 files = append(files, r.Text())
46 cmd.Wait()
48 return files, nil
51 func get_maintainers() ([]string, error) {
52 var maintainers []string
54 /* Read in all maintainers */
55 file, err := os.Open("MAINTAINERS")
56 if err != nil {
57 log.Fatalf("Can't open MAINTAINERS file: %v", err)
58 log.Fatalf("Are you running from the top-level directory?")
59 return maintainers, err
61 defer file.Close()
63 keep := false
64 s := bufio.NewScanner(file)
65 for s.Scan() {
66 /* Are we in the "data" section and have a non-empty line? */
67 if keep && s.Text() != "" {
68 maintainers = append(maintainers, s.Text())
70 /* Skip everything before the delimiter */
71 if s.Text() == "\t\t-----------------------------------" {
72 keep = true
76 return maintainers, nil
79 func path_to_regexstr(path string) string {
80 /* Add missing trailing slash if path is a directory */
81 if path[len(path)-1] != '/' {
82 fileInfo, err := os.Stat(path)
83 if err == nil && fileInfo.IsDir() {
84 path += "/"
88 regexstr := glob_to_regex(path)
90 /* Handle path with trailing '/' as prefix */
91 if regexstr[len(regexstr)-2:] == "/$" {
92 regexstr = regexstr[:len(regexstr)-1] + ".*$"
95 return regexstr;
98 func path_to_regex(path string) *regexp.Regexp {
99 regexstr := path_to_regexstr(path)
100 return regexp.MustCompile(regexstr)
103 func build_maintainers(maintainers []string) {
104 var current *subsystem
105 for _, line := range maintainers {
106 if line[1] != ':' {
107 /* Create new subsystem entry */
108 var tmp subsystem
109 subsystems = append(subsystems, tmp)
110 current = &subsystems[len(subsystems)-1]
111 current.name = line
112 } else {
113 switch line[0] {
114 case 'R', 'M':
115 /* Add subsystem maintainer */
116 current.maintainer = append(current.maintainer, line[3:len(line)])
117 case 'F':
118 // add files
119 current.paths = append(current.paths, line[3:len(line)])
120 current.globs = append(current.globs, path_to_regex(line[3:len(line)]))
121 break
122 case 'L', 'S', 'T', 'W': // ignore
123 default:
124 fmt.Println("No such specifier: ", line)
130 func print_maintainers() {
131 for _, subsystem := range subsystems {
132 fmt.Println(subsystem.name)
133 fmt.Println(" ", subsystem.maintainer)
134 fmt.Println(" ", subsystem.paths)
138 func match_file(fname string, component subsystem) bool {
139 for _, glob := range component.globs {
140 if glob.Match([]byte(fname)) {
141 return true
144 return false
147 func find_maintainer(fname string) {
148 var success bool
150 for _, subsystem := range subsystems {
151 matched := match_file(fname, subsystem)
152 if matched {
153 success = true
154 fmt.Println(fname, "is in subsystem",
155 subsystem.name)
156 fmt.Println("Maintainers: ", strings.Join(subsystem.maintainer, ", "))
159 if !success {
160 fmt.Println(fname, "has no subsystem defined in MAINTAINERS")
164 func find_unmaintained(fname string) {
165 var success bool
167 for _, subsystem := range subsystems {
168 matched := match_file(fname, subsystem)
169 if matched {
170 success = true
171 fmt.Println(fname, "is in subsystem",
172 subsystem.name)
175 if !success {
176 fmt.Println(fname, "has no subsystem defined in MAINTAINERS")
180 // taken from https://github.com/zyedidia/glob/blob/master/glob.go which is
181 // Copyright (c) 2016: Zachary Yedidia.
182 // and was published under the MIT "Expat" license.
184 // only change: return the string, instead of a compiled golang regex
185 func glob_to_regex(glob string) string {
186 regex := ""
187 inGroup := 0
188 inClass := 0
189 firstIndexInClass := -1
190 arr := []byte(glob)
192 for i := 0; i < len(arr); i++ {
193 ch := arr[i]
195 switch ch {
196 case '\\':
198 if i >= len(arr) {
199 regex += "\\"
200 } else {
201 next := arr[i]
202 switch next {
203 case ',':
204 // Nothing
205 case 'Q', 'E':
206 regex += "\\\\"
207 default:
208 regex += "\\"
210 regex += string(next)
212 case '*':
213 if inClass == 0 {
214 regex += "[^/]*"
215 } else {
216 regex += "*"
218 case '?':
219 if inClass == 0 {
220 regex += "."
221 } else {
222 regex += "?"
224 case '[':
225 inClass++
226 firstIndexInClass = i + 1
227 regex += "["
228 case ']':
229 inClass--
230 regex += "]"
231 case '.', '(', ')', '+', '|', '^', '$', '@', '%':
232 if inClass == 0 || (firstIndexInClass == i && ch == '^') {
233 regex += "\\"
235 regex += string(ch)
236 case '!':
237 if firstIndexInClass == i {
238 regex += "^"
239 } else {
240 regex += "!"
242 case '{':
243 inGroup++
244 regex += "("
245 case '}':
246 inGroup--
247 regex += ")"
248 case ',':
249 if inGroup > 0 {
250 regex += "|"
251 } else {
252 regex += ","
254 default:
255 regex += string(ch)
258 return "^" + regex + "$"
261 var is_email *regexp.Regexp
263 func extract_maintainer(maintainer string) string {
264 if is_email == nil {
265 is_email = regexp.MustCompile("<[^>]*>")
268 if match := is_email.FindStringSubmatch(maintainer); match != nil {
269 return match[0][1 : len(match[0])-1]
271 return maintainer
274 func do_print_gerrit_rules() {
275 for _, subsystem := range subsystems {
276 if len(subsystem.paths) == 0 || len(subsystem.maintainer) == 0 {
277 continue
279 fmt.Println("#", subsystem.name)
280 for _, path := range subsystem.paths {
281 fmt.Println("[filter \"file:\\\"" + path_to_regexstr(path) + "\\\"\"]")
282 for _, maint := range subsystem.maintainer {
283 fmt.Println(" reviewer =", extract_maintainer(maint))
286 fmt.Println()
290 func main() {
291 var (
292 files []string
293 err error
294 print_gerrit_rules = flag.Bool("print-gerrit-rules", false, "emit the MAINTAINERS rules in a format suitable for Gerrit's reviewers plugin")
295 debug = flag.Bool("debug", false, "emit additional debug output")
297 flag.Parse()
299 /* get and build subsystem database */
300 maintainers, err := get_maintainers()
301 if err != nil {
302 log.Fatalf("Oops.")
303 return
305 build_maintainers(maintainers)
307 if *debug {
308 print_maintainers()
311 if *print_gerrit_rules {
312 do_print_gerrit_rules()
313 return
316 args := flag.Args()
317 if len(args) == 0 {
318 /* get the filenames */
319 files, err = get_git_files()
320 if err != nil {
321 log.Fatalf("Oops.")
322 return
324 for _, file := range files {
325 find_unmaintained(file)
327 } else {
328 files = args
330 /* Find maintainers for each file */
331 for _, file := range files {
332 find_maintainer(file)