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  }