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

236 lines
5.5 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
before string
child *Formatter
parent *Formatter
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
// inboxCh is used to accept new paths for formatting.
inboxCh chan string
// completedCh is used to wait for this formatter to finish all processing.
completedCh chan interface{}
2023-12-23 12:50:47 +00:00
// Entries from inboxCh are batched according to batchSize and stored in batch for processing when the batchSize has
2023-12-24 11:59:05 +00:00
// been reached or Close is invoked.
2023-12-23 12:50:47 +00:00
batch []string
batchSize int
}
func (f *Formatter) Before() string {
return f.before
}
func (f *Formatter) ResetBefore() {
f.before = ""
}
// Executable returns the path to the executable defined by Command
func (f *Formatter) Executable() string {
return f.executable
}
// 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{}
2023-12-24 11:59:05 +00:00
// capture the name from the config file
f.name = name
f.config = config
f.before = config.Before
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.batchSize = 1024
f.batch = make([]string, 0, f.batchSize)
f.inboxCh = make(chan string, f.batchSize)
f.completedCh = make(chan interface{}, 1)
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
}
func (f *Formatter) SetParent(formatter *Formatter) {
f.parent = formatter
}
func (f *Formatter) Parent() *Formatter {
return f.parent
}
func (f *Formatter) SetChild(formatter *Formatter) {
f.child = formatter
2023-12-23 12:50:47 +00:00
}
2023-12-24 11:59:05 +00:00
// 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.
2023-12-23 12:50:47 +00:00
func (f *Formatter) Wants(path string) bool {
if f.parent != nil {
// we don't accept this path directly, our parent will forward it
return false
}
match := !PathMatches(path, f.excludes) && PathMatches(path, f.includes)
if match {
f.log.Debugf("match: %v", path)
2023-12-23 12:50:47 +00:00
}
return match
2023-12-23 12:50:47 +00:00
}
// Put add path into this Formatter's inboxCh for processing.
2023-12-23 12:50:47 +00:00
func (f *Formatter) Put(path string) {
f.inboxCh <- path
2023-12-23 12:50:47 +00:00
}
2023-12-24 11:59:05 +00:00
// Run is the main processing loop for this Formatter.
// It accepts a context which is used to lookup certain dependencies and for cancellation.
2023-12-23 12:50:47 +00:00
func (f *Formatter) Run(ctx context.Context) (err error) {
defer func() {
if f.child != nil {
// indicate no further processing for the child formatter
f.child.Close()
}
// indicate this formatter has finished processing
f.completedCh <- nil
}()
2023-12-23 12:50:47 +00:00
LOOP:
// keep processing until ctx has been cancelled or inboxCh has been closed
2023-12-23 12:50:47 +00:00
for {
select {
2023-12-24 11:59:05 +00:00
2023-12-23 12:50:47 +00:00
case <-ctx.Done():
2023-12-24 11:59:05 +00:00
// ctx has been cancelled
2023-12-23 12:50:47 +00:00
err = ctx.Err()
break LOOP
case path, ok := <-f.inboxCh:
// check if the inboxCh has been closed
2023-12-23 12:50:47 +00:00
if !ok {
break LOOP
}
2023-12-24 11:59:05 +00:00
// add path to the current batch
2023-12-23 12:50:47 +00:00
f.batch = append(f.batch, path)
if len(f.batch) == f.batchSize {
// drain immediately
if err := f.apply(ctx); err != nil {
break LOOP
}
}
}
}
2023-12-24 11:59:05 +00:00
// check if LOOP was exited due to an error
2023-12-23 12:50:47 +00:00
if err != nil {
return
}
2023-12-24 11:59:05 +00:00
// processing any lingering batch
2023-12-23 12:50:47 +00:00
return f.apply(ctx)
}
2023-12-24 11:59:05 +00:00
// apply executes Command against the latest batch of paths.
// It accepts a context which is used to lookup certain dependencies and for cancellation.
2023-12-23 12:50:47 +00:00
func (f *Formatter) apply(ctx context.Context) error {
// empty check
if len(f.batch) == 0 {
return nil
}
// construct args, starting with config
args := f.config.Options
2023-12-23 12:50:47 +00:00
// append each file path
for _, path := range f.batch {
args = append(args, path)
}
2023-12-24 11:59:05 +00:00
// execute
2023-12-23 12:50:47 +00:00
start := time.Now()
cmd := exec.CommandContext(ctx, f.config.Command, args...)
2023-12-23 12:50:47 +00:00
if out, err := cmd.CombinedOutput(); err != nil {
f.log.Debugf("\n%v", string(out))
2023-12-23 12:50:47 +00:00
// todo log output
return err
}
f.log.Infof("%v files processed in %v", len(f.batch), time.Now().Sub(start))
if f.child == nil {
// mark each path in this batch as completed
for _, path := range f.batch {
MarkPathComplete(ctx, path)
}
} else {
// otherwise forward each path onto the next formatter for processing
for _, path := range f.batch {
f.child.Put(path)
}
2023-12-23 12:50:47 +00:00
}
// reset batch
f.batch = f.batch[:0]
return nil
}
2023-12-24 11:59:05 +00:00
// Close is used to indicate that a Formatter should process any remaining paths and then stop it's processing loop.
2023-12-23 12:50:47 +00:00
func (f *Formatter) Close() {
close(f.inboxCh)
}
func (f *Formatter) AwaitCompletion() {
// todo support a timeout
<-f.completedCh
2023-12-23 12:50:47 +00:00
}