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