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/internal/format/format.go

182 lines
4.5 KiB
Go
Raw Normal View History

2023-12-23 12:50:47 +00:00
package format
import (
"context"
"os/exec"
"time"
"github.com/charmbracelet/log"
"github.com/gobwas/glob"
"github.com/juju/errors"
)
const (
2023-12-24 11:59:05 +00:00
// ErrFormatterNotFound is returned when the Command for a Formatter is not available.
ErrFormatterNotFound = errors.ConstError("formatter not found")
)
2023-12-24 11:59:05 +00:00
// Formatter represents a command which should be applied to a filesystem.
2023-12-23 12:50:47 +00:00
type Formatter struct {
2023-12-24 11:59:05 +00:00
// Command is the command invoke when applying this Formatter.
Command string
// Options are an optional list of args to be passed to Command.
Options []string
// Includes is a list of glob patterns used to determine whether this Formatter should be applied against a path.
2023-12-23 12:50:47 +00:00
Includes []string
2023-12-24 11:59:05 +00:00
// Excludes is an optional list of glob patterns used to exclude certain files from this Formatter.
2023-12-23 12:50:47 +00:00
Excludes []string
2023-12-24 11:59:05 +00:00
name string
log *log.Logger
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
2023-12-24 11:59:05 +00:00
// inbox is used to accept new paths for formatting.
2023-12-23 12:50:47 +00:00
inbox chan string
2023-12-24 11:59:05 +00:00
// Entries from inbox are batched according to batchSize and stored in batch for processing when the batchSize has
// been reached or Close is invoked.
2023-12-23 12:50:47 +00:00
batch []string
batchSize int
}
func (f *Formatter) Init(name string) error {
2023-12-24 11:59:05 +00:00
// capture the name from the config file
f.name = name
2023-12-23 12:50:47 +00:00
// test if the formatter is available
if err := exec.Command(f.Command, "--help").Run(); err != nil {
return ErrFormatterNotFound
}
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
2023-12-24 11:59:05 +00:00
f.inbox = make(chan string, f.batchSize)
2023-12-23 12:50:47 +00:00
f.batch = make([]string, f.batchSize)
f.batch = f.batch[:0]
// todo refactor common code below
if len(f.Includes) > 0 {
for _, pattern := range f.Includes {
g, err := glob.Compile("**/" + pattern)
2023-12-23 12:50:47 +00:00
if err != nil {
2023-12-24 11:59:05 +00:00
return errors.Annotatef(err, "failed to compile include pattern '%v' for formatter '%v'", pattern, f.name)
2023-12-23 12:50:47 +00:00
}
f.includes = append(f.includes, g)
}
}
if len(f.Excludes) > 0 {
for _, pattern := range f.Excludes {
g, err := glob.Compile("**/" + pattern)
2023-12-23 12:50:47 +00:00
if err != nil {
2023-12-24 11:59:05 +00:00
return errors.Annotatef(err, "failed to compile exclude pattern '%v' for formatter '%v'", pattern, f.name)
2023-12-23 12:50:47 +00:00
}
f.excludes = append(f.excludes, g)
}
}
return nil
}
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 {
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
}
2023-12-24 11:59:05 +00:00
// Put add path into this Formatter's inbox for processing.
2023-12-23 12:50:47 +00:00
func (f *Formatter) Put(path string) {
f.inbox <- path
}
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) {
LOOP:
2023-12-24 11:59:05 +00:00
// keep processing until ctx has been cancelled or inbox 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.inbox:
2023-12-24 11:59:05 +00:00
// check if the inbox 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.Options
// 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.Command, args...)
if _, err := cmd.CombinedOutput(); err != nil {
// todo log output
return err
}
f.log.Infof("%v files processed in %v", len(f.batch), time.Now().Sub(start))
2023-12-24 11:59:05 +00:00
// mark each path in this batch as completed
for _, path := range f.batch {
MarkFormatComplete(ctx, 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.inbox)
}