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"
|
|
|
|
)
|
|
|
|
|
2023-12-23 15:00:39 +00:00
|
|
|
const (
|
2023-12-24 11:59:05 +00:00
|
|
|
// ErrFormatterNotFound is returned when the Command for a Formatter is not available.
|
2023-12-23 15:00:39 +00:00
|
|
|
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
|
|
|
|
2023-12-23 15:00:39 +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
|
2023-12-23 15:00:39 +00:00
|
|
|
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 {
|
2023-12-23 13:20:11 +00:00
|
|
|
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 {
|
2023-12-23 13:20:11 +00:00
|
|
|
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 {
|
2023-12-23 13:20:11 +00:00
|
|
|
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
|
|
|
}
|
2023-12-23 13:20:11 +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)
|
|
|
|
}
|