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