This repository has been archived on 2024-05-03. You can view files and clone it, but cannot push or open issues or pull requests.
treefmt/format/formatter.go

113 lines
2.7 KiB
Go
Raw Normal View History

2023-12-23 12:50:47 +00:00
package format
import (
"context"
"errors"
"fmt"
2023-12-23 12:50:47 +00:00
"os/exec"
"time"
"git.numtide.com/numtide/treefmt/config"
2023-12-23 12:50:47 +00:00
"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
2023-12-23 12:50:47 +00:00
log *log.Logger
executable string // path to the executable described by Command
2023-12-23 12:50:47 +00:00
2023-12-24 11:59:05 +00:00
// internal compiled versions of Includes and Excludes.
2023-12-23 12:50:47 +00:00
includes []glob.Glob
excludes []glob.Glob
}
// Executable returns the path to the executable defined by Command
func (f *Formatter) Executable() string {
return f.executable
}
func (f *Formatter) Apply(ctx context.Context, paths []string) error {
// only apply if the resultant batch is not empty
if len(paths) > 0 {
// construct args, starting with config
args := f.config.Options
// append each file path
for _, path := range paths {
args = append(args, path)
}
// execute
start := time.Now()
cmd := exec.CommandContext(ctx, f.config.Command, args...)
if out, err := cmd.CombinedOutput(); err != nil {
f.log.Debugf("\n%v", string(out))
// todo log output
return err
}
f.log.Infof("%v files processed in %v", len(paths), time.Now().Sub(start))
}
return nil
}
// Wants is used to test if a Formatter wants 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(path string) bool {
match := !PathMatches(path, f.excludes) && PathMatches(path, f.includes)
if match {
f.log.Debugf("match: %v", path)
}
return match
}
// NewFormatter is used to create a new Formatter.
func NewFormatter(
name string,
config *config.Formatter,
globalExcludes []glob.Glob,
) (*Formatter, error) {
var err error
f := Formatter{}
// capture config and the formatter's name
2023-12-24 11:59:05 +00:00
f.name = name
f.config = config
2023-12-23 12:50:47 +00:00
// test if the formatter is available
executable, err := exec.LookPath(config.Command)
if errors.Is(err, exec.ErrNotFound) {
return nil, ErrCommandNotFound
} else if err != nil {
return nil, err
}
f.executable = executable
2023-12-24 11:59:05 +00:00
// initialise internal state
f.log = log.WithPrefix("format | " + name)
2023-12-23 12:50:47 +00:00
f.includes, err = CompileGlobs(config.Includes)
if err != nil {
return nil, fmt.Errorf("%w: formatter '%v' includes", err, f.name)
2023-12-23 12:50:47 +00:00
}
f.excludes, err = CompileGlobs(config.Excludes)
if err != nil {
return nil, fmt.Errorf("%w: formatter '%v' excludes", err, f.name)
2023-12-23 12:50:47 +00:00
}
f.excludes = append(f.excludes, globalExcludes...)
2023-12-23 12:50:47 +00:00
return &f, nil
}