// Copyright 2021 The Gitea Authors. All rights reserved.// SPDX-License-Identifier: MIT //go:build ignore package main import ( "fmt" "log" "os" "os/exec" "path/filepath" "regexp" "strconv" "strings" "forgejo.org/build/codeformat") // Windows has a limitation for command line arguments, the size can not exceed 32KB.// So we have to feed the files to some tools (like gofmt) batch by batch // We also introduce a `gitea-fmt` command, it does better import formatting than gofmt/goimports. `gitea-fmt` calls `gofmt` internally. var optionLogVerbose bool func logVerbose(msg string, args ...any) { if optionLogVerbose { log.Printf(msg, args...) }} func passThroughCmd(cmd string, args []string) error { foundCmd, err := exec.LookPath(cmd) if err != nil { log.Fatalf("can not find cmd: %s", cmd) } c := exec.Cmd{ Path: foundCmd, Args: append([]string{cmd}, args...), Stdin: os.Stdin, Stdout: os.Stdout, Stderr: os.Stderr, } return c.Run()} type fileCollector struct { dirs []string includePatterns []*regexp.Regexp excludePatterns []*regexp.Regexp batchSize int} func newFileCollector(fileFilter string, batchSize int) (*fileCollector, error) { co := &fileCollector{batchSize: batchSize} if fileFilter == "go-own" { co.dirs = []string{ "build", "cmd", "contrib", "tests", "models", "modules", "routers", "services", } co.includePatterns = append(co.includePatterns, regexp.MustCompile(`.*\.go$`)) co.excludePatterns = append(co.excludePatterns, regexp.MustCompile(`.*\bbindata\.go$`)) co.excludePatterns = append(co.excludePatterns, regexp.MustCompile(`\.pb\.go$`)) co.excludePatterns = append(co.excludePatterns, regexp.MustCompile(`tests/gitea-repositories-meta`)) co.excludePatterns = append(co.excludePatterns, regexp.MustCompile(`tests/integration/migration-test`)) co.excludePatterns = append(co.excludePatterns, regexp.MustCompile(`modules/git/tests`)) co.excludePatterns = append(co.excludePatterns, regexp.MustCompile(`models/fixtures`)) co.excludePatterns = append(co.excludePatterns, regexp.MustCompile(`models/gitea_migrations/fixtures`)) co.excludePatterns = append(co.excludePatterns, regexp.MustCompile(`services/gitdiff/testdata`)) } if co.dirs == nil { return nil, fmt.Errorf("unknown file-filter: %s", fileFilter) } return co, nil} func (fc *fileCollector) matchPatterns(path string, regexps []*regexp.Regexp) bool { path = strings.ReplaceAll(path, "\\", "/") for _, re := range regexps { if re.MatchString(path) { return true } } return false} func (fc *fileCollector) collectFiles() (res [][]string, err error) { var batch []string for _, dir := range fc.dirs { err = filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error { include := len(fc.includePatterns) == 0 || fc.matchPatterns(path, fc.includePatterns) exclude := fc.matchPatterns(path, fc.excludePatterns) process := include && !exclude if !process { if d.IsDir() { if exclude { logVerbose("exclude dir %s", path) return filepath.SkipDir } // for a directory, if it is not excluded explicitly, we should walk into return nil } // for a file, we skip it if it shouldn't be processed logVerbose("skip process %s", path) return nil } if d.IsDir() { // skip dir, we don't add dirs to the file list now return nil } if len(batch) >= fc.batchSize { res = append(res, batch) batch = nil } batch = append(batch, path) return nil }) if err != nil { return nil, err } } res = append(res, batch) return res, nil} // substArgFiles expands the {file-list} to a real file list for commandsfunc substArgFiles(args, files []string) []string { for i, s := range args { if s == "{file-list}" { newArgs := append(args[:i], files...) newArgs = append(newArgs, args[i+1:]...) return newArgs } } return args} func exitWithCmdErrors(subCmd string, subArgs []string, cmdErrors []error) { for _, err := range cmdErrors { if err != nil { if exitError, ok := err.(*exec.ExitError); ok { exitCode := exitError.ExitCode() log.Printf("run command failed (code=%d): %s %v", exitCode, subCmd, subArgs) os.Exit(exitCode) } else { log.Fatalf("run command failed (err=%s) %s %v", err, subCmd, subArgs) } } }} func parseArgs() (mainOptions map[string]string, subCmd string, subArgs []string) { mainOptions = map[string]string{} for i := 1; i < len(os.Args); i++ { arg := os.Args[i] if arg == "" { break } if arg[0] == '-' { arg = strings.TrimPrefix(arg, "-") arg = strings.TrimPrefix(arg, "-") fields := strings.SplitN(arg, "=", 2) if len(fields) == 1 { mainOptions[fields[0]] = "1" } else { mainOptions[fields[0]] = fields[1] } } else { subCmd = arg subArgs = os.Args[i+1:] break } } return mainOptions, subCmd, subArgs} func showUsage() { fmt.Printf(`Usage: %[1]s [options] {command} [arguments] Options: --verbose --file-filter=go-own --batch-size=100 Commands: %[1]s gofmt ... Arguments: {file-list} the file list Example: %[1]s gofmt -s -d {file-list} `, "file-batch-exec")} func newFileCollectorFromMainOptions(mainOptions map[string]string) (fc *fileCollector, err error) { fileFilter := mainOptions["file-filter"] if fileFilter == "" { fileFilter = "go-own" } batchSize, _ := strconv.Atoi(mainOptions["batch-size"]) if batchSize == 0 { batchSize = 100 } return newFileCollector(fileFilter, batchSize)} func containsString(a []string, s string) bool { for _, v := range a { if v == s { return true } } return false} func giteaFormatGoImports(files []string, doWriteFile bool) error { for _, file := range files { if err := codeformat.FormatGoImports(file, doWriteFile); err != nil { log.Printf("failed to format go imports: %s, err=%v", file, err) return err } } return nil} func main() { mainOptions, subCmd, subArgs := parseArgs() if subCmd == "" { showUsage() os.Exit(1) } optionLogVerbose = mainOptions["verbose"] != "" fc, err := newFileCollectorFromMainOptions(mainOptions) if err != nil { log.Fatalf("can not create file collector: %s", err.Error()) } fileBatches, err := fc.collectFiles() if err != nil { log.Fatalf("can not collect files: %s", err.Error()) } processed := 0 var cmdErrors []error for _, files := range fileBatches { if len(files) == 0 { break } substArgs := substArgFiles(subArgs, files) logVerbose("batch cmd: %s %v", subCmd, substArgs) switch subCmd { case "gitea-fmt": if containsString(subArgs, "-d") { log.Print("the -d option is not supported by gitea-fmt") } cmdErrors = append(cmdErrors, giteaFormatGoImports(files, containsString(subArgs, "-w"))) cmdErrors = append(cmdErrors, passThroughCmd("gofmt", append([]string{"-w", "-r", "interface{} -> any"}, substArgs...))) cmdErrors = append(cmdErrors, passThroughCmd("go", append([]string{"run", os.Getenv("GOFUMPT_PACKAGE"), "-extra"}, substArgs...))) default: log.Fatalf("unknown cmd: %s %v", subCmd, subArgs) } processed += len(files) } logVerbose("processed %d files", processed) exitWithCmdErrors(subCmd, subArgs, cmdErrors)}
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build ignore
package main
import (
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
"forgejo.org/build/codeformat"
)
// Windows has a limitation for command line arguments, the size can not exceed 32KB.
// So we have to feed the files to some tools (like gofmt) batch by batch
// We also introduce a `gitea-fmt` command, it does better import formatting than gofmt/goimports. `gitea-fmt` calls `gofmt` internally.
var optionLogVerbose bool
func logVerbose(msg string, args ...any) {
if optionLogVerbose {
log.Printf(msg, args...)
}
}
func passThroughCmd(cmd string, args []string) error {
foundCmd, err := exec.LookPath(cmd)
if err != nil {
log.Fatalf("can not find cmd: %s", cmd)
}
c := exec.Cmd{
Path: foundCmd,
Args: append([]string{cmd}, args...),
Stdin: os.Stdin,
Stdout: os.Stdout,
Stderr: os.Stderr,
}
return c.Run()
}
type fileCollector struct {
dirs []string
includePatterns []*regexp.Regexp
excludePatterns []*regexp.Regexp
batchSize int
}
func newFileCollector(fileFilter string, batchSize int) (*fileCollector, error) {
co := &fileCollector{batchSize: batchSize}
if fileFilter == "go-own" {
co.dirs = []string{
"build",
"cmd",
"contrib",
"tests",
"models",
"modules",
"routers",
"services",
}
co.includePatterns = append(co.includePatterns, regexp.MustCompile(`.*\.go$`))
co.excludePatterns = append(co.excludePatterns, regexp.MustCompile(`.*\bbindata\.go$`))
co.excludePatterns = append(co.excludePatterns, regexp.MustCompile(`\.pb\.go$`))
co.excludePatterns = append(co.excludePatterns, regexp.MustCompile(`tests/gitea-repositories-meta`))
co.excludePatterns = append(co.excludePatterns, regexp.MustCompile(`tests/integration/migration-test`))
co.excludePatterns = append(co.excludePatterns, regexp.MustCompile(`modules/git/tests`))
co.excludePatterns = append(co.excludePatterns, regexp.MustCompile(`models/fixtures`))
co.excludePatterns = append(co.excludePatterns, regexp.MustCompile(`models/gitea_migrations/fixtures`))
co.excludePatterns = append(co.excludePatterns, regexp.MustCompile(`services/gitdiff/testdata`))
}
if co.dirs == nil {
return nil, fmt.Errorf("unknown file-filter: %s", fileFilter)
}
return co, nil
}
func (fc *fileCollector) matchPatterns(path string, regexps []*regexp.Regexp) bool {
path = strings.ReplaceAll(path, "\\", "/")
for _, re := range regexps {
if re.MatchString(path) {
return true
}
}
return false
}
func (fc *fileCollector) collectFiles() (res [][]string, err error) {
var batch []string
for _, dir := range fc.dirs {
err = filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error {
include := len(fc.includePatterns) == 0 || fc.matchPatterns(path, fc.includePatterns)
exclude := fc.matchPatterns(path, fc.excludePatterns)
process := include && !exclude
if !process {
if d.IsDir() {
if exclude {
logVerbose("exclude dir %s", path)
return filepath.SkipDir
}
// for a directory, if it is not excluded explicitly, we should walk into
return nil
}
// for a file, we skip it if it shouldn't be processed
logVerbose("skip process %s", path)
return nil
}
if d.IsDir() {
// skip dir, we don't add dirs to the file list now
return nil
}
if len(batch) >= fc.batchSize {
res = append(res, batch)
batch = nil
}
batch = append(batch, path)
return nil
})
if err != nil {
return nil, err
}
}
res = append(res, batch)
return res, nil
}
// substArgFiles expands the {file-list} to a real file list for commands
func substArgFiles(args, files []string) []string {
for i, s := range args {
if s == "{file-list}" {
newArgs := append(args[:i], files...)
newArgs = append(newArgs, args[i+1:]...)
return newArgs
}
}
return args
}
func exitWithCmdErrors(subCmd string, subArgs []string, cmdErrors []error) {
for _, err := range cmdErrors {
if err != nil {
if exitError, ok := err.(*exec.ExitError); ok {
exitCode := exitError.ExitCode()
log.Printf("run command failed (code=%d): %s %v", exitCode, subCmd, subArgs)
os.Exit(exitCode)
} else {
log.Fatalf("run command failed (err=%s) %s %v", err, subCmd, subArgs)
}
}
}
}
func parseArgs() (mainOptions map[string]string, subCmd string, subArgs []string) {
mainOptions = map[string]string{}
for i := 1; i < len(os.Args); i++ {
arg := os.Args[i]
if arg == "" {
break
}
if arg[0] == '-' {
arg = strings.TrimPrefix(arg, "-")
arg = strings.TrimPrefix(arg, "-")
fields := strings.SplitN(arg, "=", 2)
if len(fields) == 1 {
mainOptions[fields[0]] = "1"
} else {
mainOptions[fields[0]] = fields[1]
}
} else {
subCmd = arg
subArgs = os.Args[i+1:]
break
}
}
return mainOptions, subCmd, subArgs
}
func showUsage() {
fmt.Printf(`Usage: %[1]s [options] {command} [arguments]
Options:
--verbose
--file-filter=go-own
--batch-size=100
Commands:
%[1]s gofmt ...
Arguments:
{file-list} the file list
Example:
%[1]s gofmt -s -d {file-list}
`, "file-batch-exec")
}
func newFileCollectorFromMainOptions(mainOptions map[string]string) (fc *fileCollector, err error) {
fileFilter := mainOptions["file-filter"]
if fileFilter == "" {
fileFilter = "go-own"
}
batchSize, _ := strconv.Atoi(mainOptions["batch-size"])
if batchSize == 0 {
batchSize = 100
}
return newFileCollector(fileFilter, batchSize)
}
func containsString(a []string, s string) bool {
for _, v := range a {
if v == s {
return true
}
}
return false
}
func giteaFormatGoImports(files []string, doWriteFile bool) error {
for _, file := range files {
if err := codeformat.FormatGoImports(file, doWriteFile); err != nil {
log.Printf("failed to format go imports: %s, err=%v", file, err)
return err
}
}
return nil
}
func main() {
mainOptions, subCmd, subArgs := parseArgs()
if subCmd == "" {
showUsage()
os.Exit(1)
}
optionLogVerbose = mainOptions["verbose"] != ""
fc, err := newFileCollectorFromMainOptions(mainOptions)
if err != nil {
log.Fatalf("can not create file collector: %s", err.Error())
}
fileBatches, err := fc.collectFiles()
if err != nil {
log.Fatalf("can not collect files: %s", err.Error())
}
processed := 0
var cmdErrors []error
for _, files := range fileBatches {
if len(files) == 0 {
break
}
substArgs := substArgFiles(subArgs, files)
logVerbose("batch cmd: %s %v", subCmd, substArgs)
switch subCmd {
case "gitea-fmt":
if containsString(subArgs, "-d") {
log.Print("the -d option is not supported by gitea-fmt")
}
cmdErrors = append(cmdErrors, giteaFormatGoImports(files, containsString(subArgs, "-w")))
cmdErrors = append(cmdErrors, passThroughCmd("gofmt", append([]string{"-w", "-r", "interface{} -> any"}, substArgs...)))
cmdErrors = append(cmdErrors, passThroughCmd("go", append([]string{"run", os.Getenv("GOFUMPT_PACKAGE"), "-extra"}, substArgs...)))
default:
log.Fatalf("unknown cmd: %s %v", subCmd, subArgs)
}
processed += len(files)
}
logVerbose("processed %d files", processed)
exitWithCmdErrors(subCmd, subArgs, cmdErrors)
}