code.gitea.io/gitea@v1.21.7/build/code-batch-process.go (about) 1 // Copyright 2021 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 //go:build ignore 5 6 package main 7 8 import ( 9 "fmt" 10 "log" 11 "os" 12 "os/exec" 13 "path/filepath" 14 "regexp" 15 "strconv" 16 "strings" 17 18 "code.gitea.io/gitea/build/codeformat" 19 ) 20 21 // Windows has a limitation for command line arguments, the size can not exceed 32KB. 22 // So we have to feed the files to some tools (like gofmt) batch by batch 23 24 // We also introduce a `gitea-fmt` command, it does better import formatting than gofmt/goimports. `gitea-fmt` calls `gofmt` internally. 25 26 var optionLogVerbose bool 27 28 func logVerbose(msg string, args ...any) { 29 if optionLogVerbose { 30 log.Printf(msg, args...) 31 } 32 } 33 34 func passThroughCmd(cmd string, args []string) error { 35 foundCmd, err := exec.LookPath(cmd) 36 if err != nil { 37 log.Fatalf("can not find cmd: %s", cmd) 38 } 39 c := exec.Cmd{ 40 Path: foundCmd, 41 Args: append([]string{cmd}, args...), 42 Stdin: os.Stdin, 43 Stdout: os.Stdout, 44 Stderr: os.Stderr, 45 } 46 return c.Run() 47 } 48 49 type fileCollector struct { 50 dirs []string 51 includePatterns []*regexp.Regexp 52 excludePatterns []*regexp.Regexp 53 batchSize int 54 } 55 56 func newFileCollector(fileFilter string, batchSize int) (*fileCollector, error) { 57 co := &fileCollector{batchSize: batchSize} 58 if fileFilter == "go-own" { 59 co.dirs = []string{ 60 "build", 61 "cmd", 62 "contrib", 63 "tests", 64 "models", 65 "modules", 66 "routers", 67 "services", 68 } 69 co.includePatterns = append(co.includePatterns, regexp.MustCompile(`.*\.go$`)) 70 71 co.excludePatterns = append(co.excludePatterns, regexp.MustCompile(`.*\bbindata\.go$`)) 72 co.excludePatterns = append(co.excludePatterns, regexp.MustCompile(`tests/gitea-repositories-meta`)) 73 co.excludePatterns = append(co.excludePatterns, regexp.MustCompile(`tests/integration/migration-test`)) 74 co.excludePatterns = append(co.excludePatterns, regexp.MustCompile(`modules/git/tests`)) 75 co.excludePatterns = append(co.excludePatterns, regexp.MustCompile(`models/fixtures`)) 76 co.excludePatterns = append(co.excludePatterns, regexp.MustCompile(`models/migrations/fixtures`)) 77 co.excludePatterns = append(co.excludePatterns, regexp.MustCompile(`services/gitdiff/testdata`)) 78 } 79 80 if co.dirs == nil { 81 return nil, fmt.Errorf("unknown file-filter: %s", fileFilter) 82 } 83 return co, nil 84 } 85 86 func (fc *fileCollector) matchPatterns(path string, regexps []*regexp.Regexp) bool { 87 path = strings.ReplaceAll(path, "\\", "/") 88 for _, re := range regexps { 89 if re.MatchString(path) { 90 return true 91 } 92 } 93 return false 94 } 95 96 func (fc *fileCollector) collectFiles() (res [][]string, err error) { 97 var batch []string 98 for _, dir := range fc.dirs { 99 err = filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error { 100 include := len(fc.includePatterns) == 0 || fc.matchPatterns(path, fc.includePatterns) 101 exclude := fc.matchPatterns(path, fc.excludePatterns) 102 process := include && !exclude 103 if !process { 104 if d.IsDir() { 105 if exclude { 106 logVerbose("exclude dir %s", path) 107 return filepath.SkipDir 108 } 109 // for a directory, if it is not excluded explicitly, we should walk into 110 return nil 111 } 112 // for a file, we skip it if it shouldn't be processed 113 logVerbose("skip process %s", path) 114 return nil 115 } 116 if d.IsDir() { 117 // skip dir, we don't add dirs to the file list now 118 return nil 119 } 120 if len(batch) >= fc.batchSize { 121 res = append(res, batch) 122 batch = nil 123 } 124 batch = append(batch, path) 125 return nil 126 }) 127 if err != nil { 128 return nil, err 129 } 130 } 131 res = append(res, batch) 132 return res, nil 133 } 134 135 // substArgFiles expands the {file-list} to a real file list for commands 136 func substArgFiles(args, files []string) []string { 137 for i, s := range args { 138 if s == "{file-list}" { 139 newArgs := append(args[:i], files...) 140 newArgs = append(newArgs, args[i+1:]...) 141 return newArgs 142 } 143 } 144 return args 145 } 146 147 func exitWithCmdErrors(subCmd string, subArgs []string, cmdErrors []error) { 148 for _, err := range cmdErrors { 149 if err != nil { 150 if exitError, ok := err.(*exec.ExitError); ok { 151 exitCode := exitError.ExitCode() 152 log.Printf("run command failed (code=%d): %s %v", exitCode, subCmd, subArgs) 153 os.Exit(exitCode) 154 } else { 155 log.Fatalf("run command failed (err=%s) %s %v", err, subCmd, subArgs) 156 } 157 } 158 } 159 } 160 161 func parseArgs() (mainOptions map[string]string, subCmd string, subArgs []string) { 162 mainOptions = map[string]string{} 163 for i := 1; i < len(os.Args); i++ { 164 arg := os.Args[i] 165 if arg == "" { 166 break 167 } 168 if arg[0] == '-' { 169 arg = strings.TrimPrefix(arg, "-") 170 arg = strings.TrimPrefix(arg, "-") 171 fields := strings.SplitN(arg, "=", 2) 172 if len(fields) == 1 { 173 mainOptions[fields[0]] = "1" 174 } else { 175 mainOptions[fields[0]] = fields[1] 176 } 177 } else { 178 subCmd = arg 179 subArgs = os.Args[i+1:] 180 break 181 } 182 } 183 return 184 } 185 186 func showUsage() { 187 fmt.Printf(`Usage: %[1]s [options] {command} [arguments] 188 189 Options: 190 --verbose 191 --file-filter=go-own 192 --batch-size=100 193 194 Commands: 195 %[1]s gofmt ... 196 197 Arguments: 198 {file-list} the file list 199 200 Example: 201 %[1]s gofmt -s -d {file-list} 202 203 `, "file-batch-exec") 204 } 205 206 func getGoVersion() string { 207 goModFile, err := os.ReadFile("go.mod") 208 if err != nil { 209 log.Fatalf(`Faild to read "go.mod": %v`, err) 210 os.Exit(1) 211 } 212 goModVersionRegex := regexp.MustCompile(`go \d+\.\d+`) 213 goModVersionLine := goModVersionRegex.Find(goModFile) 214 return string(goModVersionLine[3:]) 215 } 216 217 func newFileCollectorFromMainOptions(mainOptions map[string]string) (fc *fileCollector, err error) { 218 fileFilter := mainOptions["file-filter"] 219 if fileFilter == "" { 220 fileFilter = "go-own" 221 } 222 batchSize, _ := strconv.Atoi(mainOptions["batch-size"]) 223 if batchSize == 0 { 224 batchSize = 100 225 } 226 227 return newFileCollector(fileFilter, batchSize) 228 } 229 230 func containsString(a []string, s string) bool { 231 for _, v := range a { 232 if v == s { 233 return true 234 } 235 } 236 return false 237 } 238 239 func giteaFormatGoImports(files []string, doWriteFile bool) error { 240 for _, file := range files { 241 if err := codeformat.FormatGoImports(file, doWriteFile); err != nil { 242 log.Printf("failed to format go imports: %s, err=%v", file, err) 243 return err 244 } 245 } 246 return nil 247 } 248 249 func main() { 250 mainOptions, subCmd, subArgs := parseArgs() 251 if subCmd == "" { 252 showUsage() 253 os.Exit(1) 254 } 255 optionLogVerbose = mainOptions["verbose"] != "" 256 257 fc, err := newFileCollectorFromMainOptions(mainOptions) 258 if err != nil { 259 log.Fatalf("can not create file collector: %s", err.Error()) 260 } 261 262 fileBatches, err := fc.collectFiles() 263 if err != nil { 264 log.Fatalf("can not collect files: %s", err.Error()) 265 } 266 267 processed := 0 268 var cmdErrors []error 269 for _, files := range fileBatches { 270 if len(files) == 0 { 271 break 272 } 273 substArgs := substArgFiles(subArgs, files) 274 logVerbose("batch cmd: %s %v", subCmd, substArgs) 275 switch subCmd { 276 case "gitea-fmt": 277 if containsString(subArgs, "-d") { 278 log.Print("the -d option is not supported by gitea-fmt") 279 } 280 cmdErrors = append(cmdErrors, giteaFormatGoImports(files, containsString(subArgs, "-w"))) 281 cmdErrors = append(cmdErrors, passThroughCmd("go", append([]string{"run", os.Getenv("GOFUMPT_PACKAGE"), "-extra", "-lang", getGoVersion()}, substArgs...))) 282 default: 283 log.Fatalf("unknown cmd: %s %v", subCmd, subArgs) 284 } 285 processed += len(files) 286 } 287 288 logVerbose("processed %d files", processed) 289 exitWithCmdErrors(subCmd, subArgs, cmdErrors) 290 }