2023-12-23 15:50:47 +03:00
|
|
|
package format
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2024-01-02 13:33:50 +03:00
|
|
|
"errors"
|
|
|
|
"fmt"
|
2024-04-25 14:16:04 +03:00
|
|
|
"os"
|
2023-12-23 15:50:47 +03:00
|
|
|
"os/exec"
|
|
|
|
"time"
|
|
|
|
|
2024-05-01 21:03:26 +03:00
|
|
|
"git.numtide.com/numtide/treefmt/walk"
|
|
|
|
|
2024-02-15 12:20:01 +03:00
|
|
|
"git.numtide.com/numtide/treefmt/config"
|
2024-01-15 13:46:49 +03:00
|
|
|
|
2023-12-23 15:50:47 +03:00
|
|
|
"github.com/charmbracelet/log"
|
|
|
|
"github.com/gobwas/glob"
|
|
|
|
)
|
|
|
|
|
2024-01-15 13:46:49 +03:00
|
|
|
// ErrCommandNotFound is returned when the Command for a Formatter is not available.
|
|
|
|
var ErrCommandNotFound = errors.New("formatter command not found in PATH")
|
2024-01-12 14:46:04 +03:00
|
|
|
|
|
|
|
// Formatter represents a command which should be applied to a filesystem.
|
|
|
|
type Formatter struct {
|
|
|
|
name string
|
2024-01-15 13:46:49 +03:00
|
|
|
config *config.Formatter
|
2023-12-23 15:50:47 +03:00
|
|
|
|
2023-12-26 14:01:35 +03:00
|
|
|
log *log.Logger
|
|
|
|
executable string // path to the executable described by Command
|
2024-04-25 14:16:04 +03:00
|
|
|
workingDir string
|
2023-12-23 15:50:47 +03:00
|
|
|
|
2023-12-24 14:59:05 +03:00
|
|
|
// internal compiled versions of Includes and Excludes.
|
2023-12-23 15:50:47 +03:00
|
|
|
includes []glob.Glob
|
|
|
|
excludes []glob.Glob
|
2024-04-25 11:17:51 +03:00
|
|
|
|
|
|
|
batch []string
|
2023-12-23 15:50:47 +03:00
|
|
|
}
|
|
|
|
|
2024-04-19 12:57:41 +03:00
|
|
|
// Executable returns the path to the executable defined by Command
|
|
|
|
func (f *Formatter) Executable() string {
|
|
|
|
return f.executable
|
2024-01-12 14:46:04 +03:00
|
|
|
}
|
|
|
|
|
2024-05-24 13:42:40 +03:00
|
|
|
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 {
|
2024-04-25 14:16:04 +03:00
|
|
|
start := time.Now()
|
|
|
|
|
2024-04-25 11:17:51 +03:00
|
|
|
// construct args, starting with config
|
|
|
|
args := f.config.Options
|
|
|
|
|
2024-05-24 13:42:40 +03:00
|
|
|
// exit early if nothing to process
|
|
|
|
if len(tasks) == 0 {
|
|
|
|
return nil
|
|
|
|
}
|
2024-04-19 12:57:41 +03:00
|
|
|
|
2024-05-24 13:42:40 +03:00
|
|
|
// append paths to the args
|
|
|
|
for _, task := range tasks {
|
|
|
|
args = append(args, task.File.RelPath)
|
2024-04-25 11:17:51 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// execute the command
|
2024-05-17 11:49:41 +03:00
|
|
|
cmd := exec.CommandContext(ctx, f.executable, args...)
|
2024-04-25 14:16:04 +03:00
|
|
|
cmd.Dir = f.workingDir
|
2024-04-25 11:17:51 +03:00
|
|
|
|
2024-05-17 11:56:36 +03:00
|
|
|
// log out the command being executed
|
|
|
|
f.log.Debugf("executing: %s", cmd.String())
|
|
|
|
|
2024-04-25 11:17:51 +03:00
|
|
|
if out, err := cmd.CombinedOutput(); err != nil {
|
2024-04-25 14:16:04 +03:00
|
|
|
if len(out) > 0 {
|
|
|
|
_, _ = fmt.Fprintf(os.Stderr, "%s error:\n%s\n", f.name, out)
|
|
|
|
}
|
2024-05-10 13:43:28 +03:00
|
|
|
return fmt.Errorf("formatter '%s' with options '%v' failed to apply: %w", f.config.Command, f.config.Options, err)
|
2024-04-19 12:57:41 +03:00
|
|
|
}
|
|
|
|
|
2024-04-25 14:16:04 +03:00
|
|
|
//
|
|
|
|
|
2024-05-24 13:42:40 +03:00
|
|
|
f.log.Infof("%v files processed in %v", len(tasks), time.Now().Sub(start))
|
2024-04-25 11:17:51 +03:00
|
|
|
|
2024-04-19 12:57:41 +03:00
|
|
|
return nil
|
2024-01-12 14:46:04 +03:00
|
|
|
}
|
|
|
|
|
2024-04-26 12:27:11 +03:00
|
|
|
// Wants is used to test if a Formatter wants a path based on it's configured Includes and Excludes patterns.
|
2024-04-19 12:57:41 +03:00
|
|
|
// Returns true if the Formatter should be applied to path, false otherwise.
|
2024-05-01 21:03:26 +03:00
|
|
|
func (f *Formatter) Wants(file *walk.File) bool {
|
|
|
|
match := !PathMatches(file.RelPath, f.excludes) && PathMatches(file.RelPath, f.includes)
|
2024-04-19 12:57:41 +03:00
|
|
|
if match {
|
2024-05-01 21:03:26 +03:00
|
|
|
f.log.Debugf("match: %v", file)
|
2024-04-19 12:57:41 +03:00
|
|
|
}
|
|
|
|
return match
|
2023-12-26 14:01:35 +03:00
|
|
|
}
|
|
|
|
|
2024-01-12 14:46:04 +03:00
|
|
|
// NewFormatter is used to create a new Formatter.
|
2024-04-19 12:57:41 +03:00
|
|
|
func NewFormatter(
|
|
|
|
name string,
|
2024-04-25 14:16:04 +03:00
|
|
|
treeRoot string,
|
2024-05-02 12:56:32 +03:00
|
|
|
cfg *config.Formatter,
|
2024-04-19 12:57:41 +03:00
|
|
|
globalExcludes []glob.Glob,
|
|
|
|
) (*Formatter, error) {
|
2024-01-02 15:12:47 +03:00
|
|
|
var err error
|
|
|
|
|
2024-01-12 14:46:04 +03:00
|
|
|
f := Formatter{}
|
2024-04-19 12:57:41 +03:00
|
|
|
|
|
|
|
// capture config and the formatter's name
|
2023-12-24 14:59:05 +03:00
|
|
|
f.name = name
|
2024-05-02 12:56:32 +03:00
|
|
|
f.config = cfg
|
2024-04-25 14:16:04 +03:00
|
|
|
f.workingDir = treeRoot
|
2023-12-23 15:50:47 +03:00
|
|
|
|
2023-12-23 18:00:39 +03:00
|
|
|
// test if the formatter is available
|
2024-05-02 12:56:32 +03:00
|
|
|
executable, err := exec.LookPath(cfg.Command)
|
2023-12-26 14:01:35 +03:00
|
|
|
if errors.Is(err, exec.ErrNotFound) {
|
2024-01-15 13:46:49 +03:00
|
|
|
return nil, ErrCommandNotFound
|
2023-12-26 14:01:35 +03:00
|
|
|
} else if err != nil {
|
2024-01-12 14:46:04 +03:00
|
|
|
return nil, err
|
2023-12-23 18:00:39 +03:00
|
|
|
}
|
2023-12-26 14:01:35 +03:00
|
|
|
f.executable = executable
|
2023-12-23 18:00:39 +03:00
|
|
|
|
2023-12-24 14:59:05 +03:00
|
|
|
// initialise internal state
|
2024-05-24 13:42:40 +03:00
|
|
|
if cfg.Priority > 0 {
|
|
|
|
f.log = log.WithPrefix(fmt.Sprintf("format | %s[%d]", name, cfg.Priority))
|
2024-04-25 11:17:51 +03:00
|
|
|
} else {
|
2024-05-24 13:42:40 +03:00
|
|
|
f.log = log.WithPrefix(fmt.Sprintf("format | %s", name))
|
2024-04-25 11:17:51 +03:00
|
|
|
}
|
2023-12-23 15:50:47 +03:00
|
|
|
|
2024-05-02 12:56:32 +03:00
|
|
|
f.includes, err = CompileGlobs(cfg.Includes)
|
2024-01-02 15:12:47 +03:00
|
|
|
if err != nil {
|
2024-05-02 13:40:49 +03:00
|
|
|
return nil, fmt.Errorf("failed to compile formatter '%v' includes: %w", f.name, err)
|
2023-12-23 15:50:47 +03:00
|
|
|
}
|
|
|
|
|
2024-05-02 12:56:32 +03:00
|
|
|
f.excludes, err = CompileGlobs(cfg.Excludes)
|
2024-01-02 15:12:47 +03:00
|
|
|
if err != nil {
|
2024-05-02 13:40:49 +03:00
|
|
|
return nil, fmt.Errorf("failed to compile formatter '%v' excludes: %w", f.name, err)
|
2023-12-23 15:50:47 +03:00
|
|
|
}
|
2024-01-02 15:12:47 +03:00
|
|
|
f.excludes = append(f.excludes, globalExcludes...)
|
2023-12-23 15:50:47 +03:00
|
|
|
|
2024-01-12 14:46:04 +03:00
|
|
|
return &f, nil
|
|
|
|
}
|