xref: /aosp_15_r20/external/coreboot/util/scripts/maintainers.go (revision b9411a12aaaa7e1e6a6fb7c5e057f44ee179a49c)
1/* SPDX-License-Identifier: GPL-2.0-only */
2
3package main
4
5import (
6	"bufio"
7	"flag"
8	"fmt"
9	"log"
10	"os"
11	"os/exec"
12	"regexp"
13	"strings"
14)
15
16type subsystem struct {
17	name       string
18	maintainer []string
19	paths      []string
20	globs      []*regexp.Regexp
21}
22
23var subsystems []subsystem
24
25func get_git_files() ([]string, error) {
26	var files []string
27
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
34	}
35	if err := cmd.Start(); err != nil {
36		log.Fatalf("Could not start %v: %v", cmd, err)
37		return files, err
38	}
39
40	r := bufio.NewScanner(out)
41	for r.Scan() {
42		/* Cut out leading tab */
43		files = append(files, r.Text())
44	}
45
46	cmd.Wait()
47
48	return files, nil
49}
50
51func get_maintainers() ([]string, error) {
52	var maintainers []string
53
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
60	}
61	defer file.Close()
62
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())
69		}
70		/* Skip everything before the delimiter */
71		if s.Text() == "\t\t-----------------------------------" {
72			keep = true
73		}
74	}
75
76	return maintainers, nil
77}
78
79func 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 += "/"
85		}
86	}
87
88	regexstr := glob_to_regex(path)
89
90	/* Handle path with trailing '/' as prefix */
91	if regexstr[len(regexstr)-2:] == "/$" {
92		regexstr = regexstr[:len(regexstr)-1] + ".*$"
93	}
94
95	return regexstr;
96}
97
98func path_to_regex(path string) *regexp.Regexp {
99	regexstr := path_to_regexstr(path)
100	return regexp.MustCompile(regexstr)
101}
102
103func 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)
125			}
126		}
127	}
128}
129
130func print_maintainers() {
131	for _, subsystem := range subsystems {
132		fmt.Println(subsystem.name)
133		fmt.Println("  ", subsystem.maintainer)
134		fmt.Println("  ", subsystem.paths)
135	}
136}
137
138func match_file(fname string, component subsystem) bool {
139	for _, glob := range component.globs {
140		if glob.Match([]byte(fname)) {
141			return true
142		}
143	}
144	return false
145}
146
147func find_maintainer(fname string) {
148	var success bool
149
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, ", "))
157		}
158	}
159	if !success {
160		fmt.Println(fname, "has no subsystem defined in MAINTAINERS")
161	}
162}
163
164func find_unmaintained(fname string) {
165	var success bool
166
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)
173		}
174	}
175	if !success {
176		fmt.Println(fname, "has no subsystem defined in MAINTAINERS")
177	}
178}
179
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.
183//
184// only change: return the string, instead of a compiled golang regex
185func glob_to_regex(glob string) string {
186	regex := ""
187	inGroup := 0
188	inClass := 0
189	firstIndexInClass := -1
190	arr := []byte(glob)
191
192	for i := 0; i < len(arr); i++ {
193		ch := arr[i]
194
195		switch ch {
196		case '\\':
197			i++
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 += "\\"
209				}
210				regex += string(next)
211			}
212		case '*':
213			if inClass == 0 {
214				regex += "[^/]*"
215			} else {
216				regex += "*"
217			}
218		case '?':
219			if inClass == 0 {
220				regex += "."
221			} else {
222				regex += "?"
223			}
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 += "\\"
234			}
235			regex += string(ch)
236		case '!':
237			if firstIndexInClass == i {
238				regex += "^"
239			} else {
240				regex += "!"
241			}
242		case '{':
243			inGroup++
244			regex += "("
245		case '}':
246			inGroup--
247			regex += ")"
248		case ',':
249			if inGroup > 0 {
250				regex += "|"
251			} else {
252				regex += ","
253			}
254		default:
255			regex += string(ch)
256		}
257	}
258	return "^" + regex + "$"
259}
260
261var is_email *regexp.Regexp
262
263func extract_maintainer(maintainer string) string {
264	if is_email == nil {
265		is_email = regexp.MustCompile("<[^>]*>")
266	}
267
268	if match := is_email.FindStringSubmatch(maintainer); match != nil {
269		return match[0][1 : len(match[0])-1]
270	}
271	return maintainer
272}
273
274func do_print_gerrit_rules() {
275	for _, subsystem := range subsystems {
276		if len(subsystem.paths) == 0 || len(subsystem.maintainer) == 0 {
277			continue
278		}
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))
284			}
285		}
286		fmt.Println()
287	}
288}
289
290func 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")
296	)
297	flag.Parse()
298
299	/* get and build subsystem database */
300	maintainers, err := get_maintainers()
301	if err != nil {
302		log.Fatalf("Oops.")
303		return
304	}
305	build_maintainers(maintainers)
306
307	if *debug {
308		print_maintainers()
309	}
310
311	if *print_gerrit_rules {
312		do_print_gerrit_rules()
313		return
314	}
315
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
323		}
324		for _, file := range files {
325			find_unmaintained(file)
326		}
327	} else {
328		files = args
329
330		/* Find maintainers for each file */
331		for _, file := range files {
332			find_maintainer(file)
333		}
334	}
335}
336