2023-12-23 12:50:47 +00:00
|
|
|
package cli
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2024-01-03 08:08:57 +00:00
|
|
|
"errors"
|
2023-12-23 12:50:47 +00:00
|
|
|
"fmt"
|
2023-12-26 10:11:32 +00:00
|
|
|
"os"
|
|
|
|
"os/signal"
|
|
|
|
"syscall"
|
2023-12-23 12:50:47 +00:00
|
|
|
"time"
|
|
|
|
|
2023-12-23 13:39:16 +00:00
|
|
|
"git.numtide.com/numtide/treefmt/internal/cache"
|
|
|
|
"git.numtide.com/numtide/treefmt/internal/format"
|
2023-12-23 12:50:47 +00:00
|
|
|
|
|
|
|
"github.com/charmbracelet/log"
|
|
|
|
"golang.org/x/sync/errgroup"
|
|
|
|
)
|
|
|
|
|
|
|
|
type Format struct{}
|
|
|
|
|
|
|
|
func (f *Format) Run() error {
|
|
|
|
start := time.Now()
|
|
|
|
|
2023-12-25 12:26:18 +00:00
|
|
|
Cli.Configure()
|
2023-12-23 12:50:47 +00:00
|
|
|
|
|
|
|
l := log.WithPrefix("format")
|
|
|
|
|
|
|
|
defer func() {
|
|
|
|
if err := cache.Close(); err != nil {
|
|
|
|
l.Errorf("failed to close cache: %v", err)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
// create an overall context
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
// read config
|
|
|
|
cfg, err := format.ReadConfigFile(Cli.ConfigFile)
|
|
|
|
if err != nil {
|
2024-01-02 10:33:50 +00:00
|
|
|
return fmt.Errorf("%w: failed to read config file", err)
|
2023-12-23 12:50:47 +00:00
|
|
|
}
|
|
|
|
|
2024-01-02 12:12:47 +00:00
|
|
|
globalExcludes, err := format.CompileGlobs(cfg.Global.Excludes)
|
|
|
|
|
2023-12-25 12:26:18 +00:00
|
|
|
// create optional formatter filter set
|
|
|
|
formatterSet := make(map[string]bool)
|
|
|
|
for _, name := range Cli.Formatters {
|
|
|
|
_, ok := cfg.Formatters[name]
|
|
|
|
if !ok {
|
2024-01-02 10:33:50 +00:00
|
|
|
return fmt.Errorf("%w: formatter not found in config: %v", err, name)
|
2023-12-25 12:26:18 +00:00
|
|
|
}
|
|
|
|
formatterSet[name] = true
|
|
|
|
}
|
|
|
|
|
|
|
|
includeFormatter := func(name string) bool {
|
|
|
|
if len(formatterSet) == 0 {
|
|
|
|
return true
|
|
|
|
} else {
|
|
|
|
_, include := formatterSet[name]
|
|
|
|
return include
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-23 12:50:47 +00:00
|
|
|
// init formatters
|
|
|
|
for name, formatter := range cfg.Formatters {
|
2023-12-25 12:26:18 +00:00
|
|
|
if !includeFormatter(name) {
|
|
|
|
// remove this formatter
|
|
|
|
delete(cfg.Formatters, name)
|
|
|
|
l.Debugf("formatter %v is not in formatter list %v, skipping", name, Cli.Formatters)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2024-01-02 12:12:47 +00:00
|
|
|
err = formatter.Init(name, globalExcludes)
|
2024-01-03 08:08:57 +00:00
|
|
|
if errors.Is(err, format.ErrFormatterNotFound) && Cli.AllowMissingFormatter {
|
2023-12-23 15:00:39 +00:00
|
|
|
l.Debugf("formatter not found: %v", name)
|
|
|
|
// remove this formatter
|
|
|
|
delete(cfg.Formatters, name)
|
|
|
|
} else if err != nil {
|
2024-01-02 10:33:50 +00:00
|
|
|
return fmt.Errorf("%w: failed to initialise formatter: %v", err, name)
|
2023-12-23 12:50:47 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
ctx = format.RegisterFormatters(ctx, cfg.Formatters)
|
|
|
|
|
2024-01-03 08:08:57 +00:00
|
|
|
if err = cache.Open(Cli.TreeRoot, Cli.ClearCache, cfg.Formatters); err != nil {
|
2023-12-23 12:50:47 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
//
|
|
|
|
pendingCh := make(chan string, 1024)
|
|
|
|
completedCh := make(chan string, 1024)
|
|
|
|
|
|
|
|
ctx = format.SetCompletedChannel(ctx, completedCh)
|
|
|
|
|
|
|
|
//
|
|
|
|
eg, ctx := errgroup.WithContext(ctx)
|
|
|
|
|
|
|
|
// start the formatters
|
|
|
|
for name := range cfg.Formatters {
|
|
|
|
formatter := cfg.Formatters[name]
|
|
|
|
eg.Go(func() error {
|
|
|
|
return formatter.Run(ctx)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// determine paths to be formatted
|
|
|
|
pathsCh := make(chan string, 1024)
|
|
|
|
|
|
|
|
// update cache as paths are completed
|
|
|
|
eg.Go(func() error {
|
|
|
|
batchSize := 1024
|
2024-01-03 10:39:55 +00:00
|
|
|
batch := make([]string, 0, batchSize)
|
2023-12-23 12:50:47 +00:00
|
|
|
|
2023-12-23 13:31:08 +00:00
|
|
|
var pending, completed, changes int
|
2023-12-23 12:50:47 +00:00
|
|
|
|
|
|
|
LOOP:
|
|
|
|
for {
|
|
|
|
select {
|
2024-01-03 08:08:57 +00:00
|
|
|
case <-ctx.Done():
|
|
|
|
return ctx.Err()
|
2023-12-23 12:50:47 +00:00
|
|
|
case _, ok := <-pendingCh:
|
|
|
|
if ok {
|
|
|
|
pending += 1
|
|
|
|
} else if pending == completed {
|
|
|
|
break LOOP
|
|
|
|
}
|
|
|
|
|
|
|
|
case path, ok := <-completedCh:
|
|
|
|
if !ok {
|
|
|
|
break LOOP
|
|
|
|
}
|
|
|
|
batch = append(batch, path)
|
|
|
|
if len(batch) == batchSize {
|
2023-12-23 13:31:08 +00:00
|
|
|
count, err := cache.Update(batch)
|
|
|
|
if err != nil {
|
2023-12-23 12:50:47 +00:00
|
|
|
return err
|
|
|
|
}
|
2023-12-23 13:31:08 +00:00
|
|
|
changes += count
|
2023-12-23 12:50:47 +00:00
|
|
|
batch = batch[:0]
|
|
|
|
}
|
|
|
|
|
|
|
|
completed += 1
|
|
|
|
|
|
|
|
if completed == pending {
|
|
|
|
close(completedCh)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// final flush
|
2023-12-23 13:31:08 +00:00
|
|
|
count, err := cache.Update(batch)
|
|
|
|
if err != nil {
|
2023-12-23 12:50:47 +00:00
|
|
|
return err
|
|
|
|
}
|
2023-12-23 13:31:08 +00:00
|
|
|
changes += count
|
2023-12-23 12:50:47 +00:00
|
|
|
|
2023-12-25 12:26:18 +00:00
|
|
|
fmt.Printf("%v files changed in %v", changes, time.Now().Sub(start))
|
2023-12-23 12:50:47 +00:00
|
|
|
return nil
|
|
|
|
})
|
|
|
|
|
|
|
|
eg.Go(func() error {
|
|
|
|
count := 0
|
|
|
|
|
|
|
|
for path := range pathsCh {
|
|
|
|
for _, formatter := range cfg.Formatters {
|
|
|
|
if formatter.Wants(path) {
|
|
|
|
pendingCh <- path
|
|
|
|
count += 1
|
|
|
|
formatter.Put(path)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, formatter := range cfg.Formatters {
|
|
|
|
formatter.Close()
|
|
|
|
}
|
|
|
|
|
|
|
|
if count == 0 {
|
|
|
|
close(completedCh)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
|
|
|
|
eg.Go(func() error {
|
|
|
|
defer close(pathsCh)
|
|
|
|
return cache.ChangeSet(ctx, Cli.TreeRoot, pathsCh)
|
|
|
|
})
|
|
|
|
|
2023-12-26 10:11:32 +00:00
|
|
|
// listen for shutdown and call cancel if required
|
|
|
|
go func() {
|
|
|
|
exit := make(chan os.Signal, 1)
|
|
|
|
signal.Notify(exit, os.Interrupt, syscall.SIGTERM)
|
|
|
|
<-exit
|
|
|
|
cancel()
|
|
|
|
}()
|
2023-12-23 15:00:39 +00:00
|
|
|
|
2023-12-23 12:50:47 +00:00
|
|
|
return eg.Wait()
|
|
|
|
}
|