mirror of
https://github.com/numtide/treefmt.git
synced 2024-08-17 04:40:27 +03:00
For each path we determine the list of formatters that are interested in formatting it. From there, we sort the list of formatters first by priority (lower value, higher priority) and then by name (lexicographically). With this information we create a batch key which is based on the unique sequence of formatters. When enough paths with the same sequence is ready we apply them in order to each formatter. By default, with no special configuration, this model guarantees that a given path will only be processed by one formatter at a time. If a user wishes to influence the order in which formatters are applied they can use the priority field. Signed-off-by: Brian McGee <brian@bmcgee.ie>
143 lines
3.3 KiB
Go
143 lines
3.3 KiB
Go
package format
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"time"
|
|
|
|
"git.numtide.com/numtide/treefmt/walk"
|
|
|
|
"git.numtide.com/numtide/treefmt/config"
|
|
|
|
"github.com/charmbracelet/log"
|
|
"github.com/gobwas/glob"
|
|
)
|
|
|
|
// ErrCommandNotFound is returned when the Command for a Formatter is not available.
|
|
var ErrCommandNotFound = errors.New("formatter command not found in PATH")
|
|
|
|
// Formatter represents a command which should be applied to a filesystem.
|
|
type Formatter struct {
|
|
name string
|
|
config *config.Formatter
|
|
|
|
log *log.Logger
|
|
executable string // path to the executable described by Command
|
|
workingDir string
|
|
|
|
// internal compiled versions of Includes and Excludes.
|
|
includes []glob.Glob
|
|
excludes []glob.Glob
|
|
|
|
batch []string
|
|
}
|
|
|
|
// Executable returns the path to the executable defined by Command
|
|
func (f *Formatter) Executable() string {
|
|
return f.executable
|
|
}
|
|
|
|
func (f *Formatter) Name() string {
|
|
return f.name
|
|
}
|
|
|
|
func (f *Formatter) Priority() int {
|
|
return f.config.Priority
|
|
}
|
|
|
|
func (f *Formatter) Apply(ctx context.Context, tasks []*Task) error {
|
|
start := time.Now()
|
|
|
|
// construct args, starting with config
|
|
args := f.config.Options
|
|
|
|
// exit early if nothing to process
|
|
if len(tasks) == 0 {
|
|
return nil
|
|
}
|
|
|
|
// append paths to the args
|
|
for _, task := range tasks {
|
|
args = append(args, task.File.RelPath)
|
|
}
|
|
|
|
// execute the command
|
|
cmd := exec.CommandContext(ctx, f.executable, args...)
|
|
cmd.Dir = f.workingDir
|
|
|
|
// log out the command being executed
|
|
f.log.Debugf("executing: %s", cmd.String())
|
|
|
|
if out, err := cmd.CombinedOutput(); err != nil {
|
|
if len(out) > 0 {
|
|
_, _ = fmt.Fprintf(os.Stderr, "%s error:\n%s\n", f.name, out)
|
|
}
|
|
return fmt.Errorf("formatter '%s' with options '%v' failed to apply: %w", f.config.Command, f.config.Options, err)
|
|
}
|
|
|
|
//
|
|
|
|
f.log.Infof("%v files processed in %v", len(tasks), time.Now().Sub(start))
|
|
|
|
return nil
|
|
}
|
|
|
|
// Wants is used to test if a Formatter wants a path based on it's configured Includes and Excludes patterns.
|
|
// Returns true if the Formatter should be applied to path, false otherwise.
|
|
func (f *Formatter) Wants(file *walk.File) bool {
|
|
match := !PathMatches(file.RelPath, f.excludes) && PathMatches(file.RelPath, f.includes)
|
|
if match {
|
|
f.log.Debugf("match: %v", file)
|
|
}
|
|
return match
|
|
}
|
|
|
|
// NewFormatter is used to create a new Formatter.
|
|
func NewFormatter(
|
|
name string,
|
|
treeRoot string,
|
|
cfg *config.Formatter,
|
|
globalExcludes []glob.Glob,
|
|
) (*Formatter, error) {
|
|
var err error
|
|
|
|
f := Formatter{}
|
|
|
|
// capture config and the formatter's name
|
|
f.name = name
|
|
f.config = cfg
|
|
f.workingDir = treeRoot
|
|
|
|
// test if the formatter is available
|
|
executable, err := exec.LookPath(cfg.Command)
|
|
if errors.Is(err, exec.ErrNotFound) {
|
|
return nil, ErrCommandNotFound
|
|
} else if err != nil {
|
|
return nil, err
|
|
}
|
|
f.executable = executable
|
|
|
|
// initialise internal state
|
|
if cfg.Priority > 0 {
|
|
f.log = log.WithPrefix(fmt.Sprintf("format | %s[%d]", name, cfg.Priority))
|
|
} else {
|
|
f.log = log.WithPrefix(fmt.Sprintf("format | %s", name))
|
|
}
|
|
|
|
f.includes, err = CompileGlobs(cfg.Includes)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to compile formatter '%v' includes: %w", f.name, err)
|
|
}
|
|
|
|
f.excludes, err = CompileGlobs(cfg.Excludes)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to compile formatter '%v' excludes: %w", f.name, err)
|
|
}
|
|
f.excludes = append(f.excludes, globalExcludes...)
|
|
|
|
return &f, nil
|
|
}
|