Compare commits
28 Commits
65276c05bb
...
ad3e66166a
Author | SHA1 | Date | |
---|---|---|---|
ad3e66166a | |||
8e840a9a96 | |||
6070b6f6e4 | |||
4d60509e13 | |||
d9ee97a0b7 | |||
f1c80edcf1 | |||
17f354b880 | |||
2f557e364a | |||
577c6086c0 | |||
6c66daf26b | |||
311e46a409 | |||
95c6cdbd57 | |||
7cc49e2275 | |||
894f90f084 | |||
e252f6bd0c | |||
fc8d8f31d1 | |||
07989c7a04 | |||
097b701792 | |||
482e4976fd | |||
73722737f6 | |||
b86d0f897f | |||
397d051088 | |||
404b0a951a | |||
ed10f976f8 | |||
c720e414ac | |||
2eaf999a0e | |||
fb9493884c | |||
5a5c1ea03e |
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -10,3 +10,8 @@ repl-result-*
|
||||||
|
|
||||||
# devshell
|
# devshell
|
||||||
/.data
|
/.data
|
||||||
|
|
||||||
|
# docs
|
||||||
|
node_modules
|
||||||
|
docs/.vitepress/cache
|
||||||
|
docs/.vitepress/dist
|
86
cache/cache.go
vendored
86
cache/cache.go
vendored
|
@ -5,9 +5,7 @@ import (
|
||||||
"crypto/sha1"
|
"crypto/sha1"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
"runtime"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -57,25 +55,25 @@ func Open(treeRoot string, clean bool, formatters map[string]*format.Formatter)
|
||||||
name := hex.EncodeToString(digest)
|
name := hex.EncodeToString(digest)
|
||||||
path, err := xdg.CacheFile(fmt.Sprintf("treefmt/eval-cache/%v.db", name))
|
path, err := xdg.CacheFile(fmt.Sprintf("treefmt/eval-cache/%v.db", name))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%w: could not resolve local path for the cache", err)
|
return fmt.Errorf("could not resolve local path for the cache: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
db, err = bolt.Open(path, 0o600, nil)
|
db, err = bolt.Open(path, 0o600, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%w: failed to open cache", err)
|
return fmt.Errorf("failed to open cache at %v: %w", path, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = db.Update(func(tx *bolt.Tx) error {
|
err = db.Update(func(tx *bolt.Tx) error {
|
||||||
// create bucket for tracking paths
|
// create bucket for tracking paths
|
||||||
pathsBucket, err := tx.CreateBucketIfNotExists([]byte(pathsBucket))
|
pathsBucket, err := tx.CreateBucketIfNotExists([]byte(pathsBucket))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%w: failed to create paths bucket", err)
|
return fmt.Errorf("failed to create paths bucket: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// create bucket for tracking formatters
|
// create bucket for tracking formatters
|
||||||
formattersBucket, err := tx.CreateBucketIfNotExists([]byte(formattersBucket))
|
formattersBucket, err := tx.CreateBucketIfNotExists([]byte(formattersBucket))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%w: failed to create formatters bucket", err)
|
return fmt.Errorf("failed to create formatters bucket: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// check for any newly configured or modified formatters
|
// check for any newly configured or modified formatters
|
||||||
|
@ -83,12 +81,12 @@ func Open(treeRoot string, clean bool, formatters map[string]*format.Formatter)
|
||||||
|
|
||||||
stat, err := os.Lstat(formatter.Executable())
|
stat, err := os.Lstat(formatter.Executable())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%w: failed to state formatter executable", err)
|
return fmt.Errorf("failed to stat formatter executable %v: %w", formatter.Executable(), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
entry, err := getEntry(formattersBucket, name)
|
entry, err := getEntry(formattersBucket, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%w: failed to retrieve entry for formatter", err)
|
return fmt.Errorf("failed to retrieve cache entry for formatter %v: %w", name, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
clean = clean || entry == nil || !(entry.Size == stat.Size() && entry.Modified == stat.ModTime())
|
clean = clean || entry == nil || !(entry.Size == stat.Size() && entry.Modified == stat.ModTime())
|
||||||
|
@ -107,7 +105,7 @@ func Open(treeRoot string, clean bool, formatters map[string]*format.Formatter)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = putEntry(formattersBucket, name, entry); err != nil {
|
if err = putEntry(formattersBucket, name, entry); err != nil {
|
||||||
return fmt.Errorf("%w: failed to write formatter entry", err)
|
return fmt.Errorf("failed to write cache entry for formatter %v: %w", name, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,14 +115,14 @@ func Open(treeRoot string, clean bool, formatters map[string]*format.Formatter)
|
||||||
if !ok {
|
if !ok {
|
||||||
// remove the formatter entry from the cache
|
// remove the formatter entry from the cache
|
||||||
if err = formattersBucket.Delete(key); err != nil {
|
if err = formattersBucket.Delete(key); err != nil {
|
||||||
return fmt.Errorf("%w: failed to remove formatter entry", err)
|
return fmt.Errorf("failed to remove cache entry for formatter %v: %w", key, err)
|
||||||
}
|
}
|
||||||
// indicate a clean is required
|
// indicate a clean is required
|
||||||
clean = true
|
clean = true
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return fmt.Errorf("%w: failed to check for removed formatters", err)
|
return fmt.Errorf("failed to check cache for removed formatters: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if clean {
|
if clean {
|
||||||
|
@ -132,7 +130,7 @@ func Open(treeRoot string, clean bool, formatters map[string]*format.Formatter)
|
||||||
c := pathsBucket.Cursor()
|
c := pathsBucket.Cursor()
|
||||||
for k, v := c.First(); !(k == nil && v == nil); k, v = c.Next() {
|
for k, v := c.First(); !(k == nil && v == nil); k, v = c.Next() {
|
||||||
if err = c.Delete(); err != nil {
|
if err = c.Delete(); err != nil {
|
||||||
return fmt.Errorf("%w: failed to remove path entry", err)
|
return fmt.Errorf("failed to remove path entry: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -157,7 +155,7 @@ func getEntry(bucket *bolt.Bucket, path string) (*Entry, error) {
|
||||||
if b != nil {
|
if b != nil {
|
||||||
var cached Entry
|
var cached Entry
|
||||||
if err := msgpack.Unmarshal(b, &cached); err != nil {
|
if err := msgpack.Unmarshal(b, &cached); err != nil {
|
||||||
return nil, fmt.Errorf("%w: failed to unmarshal cache info for path '%v'", err, path)
|
return nil, fmt.Errorf("failed to unmarshal cache info for path '%v': %w", path, err)
|
||||||
}
|
}
|
||||||
return &cached, nil
|
return &cached, nil
|
||||||
} else {
|
} else {
|
||||||
|
@ -169,18 +167,18 @@ func getEntry(bucket *bolt.Bucket, path string) (*Entry, error) {
|
||||||
func putEntry(bucket *bolt.Bucket, path string, entry *Entry) error {
|
func putEntry(bucket *bolt.Bucket, path string, entry *Entry) error {
|
||||||
bytes, err := msgpack.Marshal(entry)
|
bytes, err := msgpack.Marshal(entry)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%w: failed to marshal cache entry", err)
|
return fmt.Errorf("failed to marshal cache path %v: %w", path, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = bucket.Put([]byte(path), bytes); err != nil {
|
if err = bucket.Put([]byte(path), bytes); err != nil {
|
||||||
return fmt.Errorf("%w: failed to put cache entry", err)
|
return fmt.Errorf("failed to put cache path %v: %w", path, err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ChangeSet is used to walk a filesystem, starting at root, and outputting any new or changed paths using pathsCh.
|
// ChangeSet is used to walk a filesystem, starting at root, and outputting any new or changed paths using pathsCh.
|
||||||
// It determines if a path is new or has changed by comparing against cache entries.
|
// It determines if a path is new or has changed by comparing against cache entries.
|
||||||
func ChangeSet(ctx context.Context, walker walk.Walker, pathsCh chan<- string) error {
|
func ChangeSet(ctx context.Context, walker walk.Walker, filesCh chan<- *walk.File) error {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
|
@ -198,24 +196,21 @@ func ChangeSet(ctx context.Context, walker walk.Walker, pathsCh chan<- string) e
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// for quick removal of tree root from paths
|
return walker.Walk(ctx, func(file *walk.File, err error) error {
|
||||||
relPathOffset := len(walker.Root()) + 1
|
|
||||||
|
|
||||||
return walker.Walk(ctx, func(path string, info fs.FileInfo, err error) error {
|
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
default:
|
default:
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%w: failed to walk path", err)
|
return fmt.Errorf("failed to walk path: %w", err)
|
||||||
} else if info.IsDir() {
|
} else if file.Info.IsDir() {
|
||||||
// ignore directories
|
// ignore directories
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ignore symlinks
|
// ignore symlinks
|
||||||
if info.Mode()&os.ModeSymlink == os.ModeSymlink {
|
if file.Info.Mode()&os.ModeSymlink == os.ModeSymlink {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -224,18 +219,17 @@ func ChangeSet(ctx context.Context, walker walk.Walker, pathsCh chan<- string) e
|
||||||
if tx == nil {
|
if tx == nil {
|
||||||
tx, err = db.Begin(false)
|
tx, err = db.Begin(false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%w: failed to open a new read tx", err)
|
return fmt.Errorf("failed to open a new cache read tx: %w", err)
|
||||||
}
|
}
|
||||||
bucket = tx.Bucket([]byte(pathsBucket))
|
bucket = tx.Bucket([]byte(pathsBucket))
|
||||||
}
|
}
|
||||||
|
|
||||||
relPath := path[relPathOffset:]
|
cached, err := getEntry(bucket, file.RelPath)
|
||||||
cached, err := getEntry(bucket, relPath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
changedOrNew := cached == nil || !(cached.Modified == info.ModTime() && cached.Size == info.Size())
|
changedOrNew := cached == nil || !(cached.Modified == file.Info.ModTime() && cached.Size == file.Info.Size())
|
||||||
|
|
||||||
stats.Add(stats.Traversed, 1)
|
stats.Add(stats.Traversed, 1)
|
||||||
if !changedOrNew {
|
if !changedOrNew {
|
||||||
|
@ -250,7 +244,7 @@ func ChangeSet(ctx context.Context, walker walk.Walker, pathsCh chan<- string) e
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
default:
|
default:
|
||||||
pathsCh <- relPath
|
filesCh <- file
|
||||||
}
|
}
|
||||||
|
|
||||||
// close the current tx if we have reached the batch size
|
// close the current tx if we have reached the batch size
|
||||||
|
@ -266,47 +260,35 @@ func ChangeSet(ctx context.Context, walker walk.Walker, pathsCh chan<- string) e
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update is used to record updated cache information for the specified list of paths.
|
// Update is used to record updated cache information for the specified list of paths.
|
||||||
func Update(treeRoot string, paths []string) (int, error) {
|
func Update(files []*walk.File) error {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
defer func() {
|
defer func() {
|
||||||
logger.Infof("finished updating %v paths in %v", len(paths), time.Since(start))
|
logger.Infof("finished processing %v paths in %v", len(files), time.Since(start))
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if len(paths) == 0 {
|
if len(files) == 0 {
|
||||||
return 0, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var changes int
|
return db.Update(func(tx *bolt.Tx) error {
|
||||||
|
|
||||||
return changes, db.Update(func(tx *bolt.Tx) error {
|
|
||||||
bucket := tx.Bucket([]byte(pathsBucket))
|
bucket := tx.Bucket([]byte(pathsBucket))
|
||||||
|
|
||||||
for _, path := range paths {
|
for _, f := range files {
|
||||||
cached, err := getEntry(bucket, path)
|
currentInfo, err := os.Stat(f.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
pathInfo, err := os.Stat(filepath.Join(treeRoot, path))
|
if !(f.Info.ModTime() == currentInfo.ModTime() && f.Info.Size() == currentInfo.Size()) {
|
||||||
if err != nil {
|
stats.Add(stats.Formatted, 1)
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if cached == nil || !(cached.Modified == pathInfo.ModTime() && cached.Size == pathInfo.Size()) {
|
|
||||||
changes += 1
|
|
||||||
} else {
|
|
||||||
// no change to write
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
stats.Add(stats.Formatted, 1)
|
|
||||||
|
|
||||||
entry := Entry{
|
entry := Entry{
|
||||||
Size: pathInfo.Size(),
|
Size: currentInfo.Size(),
|
||||||
Modified: pathInfo.ModTime(),
|
Modified: currentInfo.ModTime(),
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = putEntry(bucket, path, &entry); err != nil {
|
if err = putEntry(bucket, f.RelPath, &entry); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,14 +26,14 @@ type Format struct {
|
||||||
Stdin bool `help:"Format the context passed in via stdin"`
|
Stdin bool `help:"Format the context passed in via stdin"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Format) Configure() {
|
func ConfigureLogging() {
|
||||||
log.SetReportTimestamp(false)
|
log.SetReportTimestamp(false)
|
||||||
|
|
||||||
if f.Verbosity == 0 {
|
if Cli.Verbosity == 0 {
|
||||||
log.SetLevel(log.WarnLevel)
|
log.SetLevel(log.WarnLevel)
|
||||||
} else if f.Verbosity == 1 {
|
} else if Cli.Verbosity == 1 {
|
||||||
log.SetLevel(log.InfoLevel)
|
log.SetLevel(log.InfoLevel)
|
||||||
} else if f.Verbosity > 1 {
|
} else if Cli.Verbosity > 1 {
|
||||||
log.SetLevel(log.DebugLevel)
|
log.SetLevel(log.DebugLevel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
264
cli/format.go
264
cli/format.go
|
@ -5,13 +5,10 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"slices"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
|
@ -35,19 +32,17 @@ var (
|
||||||
globalExcludes []glob.Glob
|
globalExcludes []glob.Glob
|
||||||
formatters map[string]*format.Formatter
|
formatters map[string]*format.Formatter
|
||||||
pipelines map[string]*format.Pipeline
|
pipelines map[string]*format.Pipeline
|
||||||
pathsCh chan string
|
filesCh chan *walk.File
|
||||||
processedCh chan string
|
processedCh chan *walk.File
|
||||||
|
|
||||||
ErrFailOnChange = errors.New("unexpected changes detected, --fail-on-change is enabled")
|
ErrFailOnChange = errors.New("unexpected changes detected, --fail-on-change is enabled")
|
||||||
)
|
)
|
||||||
|
|
||||||
func (f *Format) Run() (err error) {
|
func (f *Format) Run() (err error) {
|
||||||
stats.Init()
|
// create a prefixed logger
|
||||||
|
|
||||||
Cli.Configure()
|
|
||||||
|
|
||||||
l := log.WithPrefix("format")
|
l := log.WithPrefix("format")
|
||||||
|
|
||||||
|
// ensure cache is closed on return
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := cache.Close(); err != nil {
|
if err := cache.Close(); err != nil {
|
||||||
l.Errorf("failed to close cache: %v", err)
|
l.Errorf("failed to close cache: %v", err)
|
||||||
|
@ -55,46 +50,23 @@ func (f *Format) Run() (err error) {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// read config
|
// read config
|
||||||
cfg, err := config.ReadFile(Cli.ConfigFile)
|
cfg, err := config.ReadFile(Cli.ConfigFile, Cli.Formatters)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%w: failed to read config file", err)
|
return fmt.Errorf("failed to read config file %v: %w", Cli.ConfigFile, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// compile global exclude globs
|
||||||
if globalExcludes, err = format.CompileGlobs(cfg.Global.Excludes); err != nil {
|
if globalExcludes, err = format.CompileGlobs(cfg.Global.Excludes); err != nil {
|
||||||
return fmt.Errorf("%w: failed to compile global globs", err)
|
return fmt.Errorf("failed to compile global excludes: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// initialise pipelines
|
||||||
pipelines = make(map[string]*format.Pipeline)
|
pipelines = make(map[string]*format.Pipeline)
|
||||||
formatters = make(map[string]*format.Formatter)
|
formatters = make(map[string]*format.Formatter)
|
||||||
|
|
||||||
// filter formatters
|
// iterate the formatters in lexicographical order
|
||||||
if len(Cli.Formatters) > 0 {
|
for _, name := range cfg.Names {
|
||||||
// first check the cli formatter list is valid
|
// init formatter
|
||||||
for _, name := range Cli.Formatters {
|
|
||||||
_, ok := cfg.Formatters[name]
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("formatter not found in config: %v", name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// next we remove any formatter configs that were not specified
|
|
||||||
for name := range cfg.Formatters {
|
|
||||||
if !slices.Contains(Cli.Formatters, name) {
|
|
||||||
delete(cfg.Formatters, name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// sort the formatter names so that, as we construct pipelines, we add formatters in a determinstic fashion. This
|
|
||||||
// ensures a deterministic order even when all priority values are the same e.g. 0
|
|
||||||
|
|
||||||
names := make([]string, 0, len(cfg.Formatters))
|
|
||||||
for name := range cfg.Formatters {
|
|
||||||
names = append(names, name)
|
|
||||||
}
|
|
||||||
sort.Strings(names)
|
|
||||||
|
|
||||||
// init formatters
|
|
||||||
for _, name := range names {
|
|
||||||
formatterCfg := cfg.Formatters[name]
|
formatterCfg := cfg.Formatters[name]
|
||||||
formatter, err := format.NewFormatter(name, Cli.TreeRoot, formatterCfg, globalExcludes)
|
formatter, err := format.NewFormatter(name, Cli.TreeRoot, formatterCfg, globalExcludes)
|
||||||
if errors.Is(err, format.ErrCommandNotFound) && Cli.AllowMissingFormatter {
|
if errors.Is(err, format.ErrCommandNotFound) && Cli.AllowMissingFormatter {
|
||||||
|
@ -104,8 +76,12 @@ func (f *Format) Run() (err error) {
|
||||||
return fmt.Errorf("%w: failed to initialise formatter: %v", err, name)
|
return fmt.Errorf("%w: failed to initialise formatter: %v", err, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// store formatter by name
|
||||||
formatters[name] = formatter
|
formatters[name] = formatter
|
||||||
|
|
||||||
|
// If no pipeline is configured, we add the formatter to a nominal pipeline of size 1 with the key being the
|
||||||
|
// formatter's name. If a pipeline is configured, we add the formatter to a pipeline keyed by
|
||||||
|
// 'p:<pipeline_name>' in which it is sorted by priority.
|
||||||
if formatterCfg.Pipeline == "" {
|
if formatterCfg.Pipeline == "" {
|
||||||
pipeline := format.Pipeline{}
|
pipeline := format.Pipeline{}
|
||||||
pipeline.Add(formatter)
|
pipeline.Add(formatter)
|
||||||
|
@ -137,17 +113,20 @@ func (f *Format) Run() (err error) {
|
||||||
cancel()
|
cancel()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// create some groups for concurrent processing and control flow
|
// initialise stats collection
|
||||||
|
stats.Init()
|
||||||
|
|
||||||
|
// create an overall error group for executing high level tasks concurrently
|
||||||
eg, ctx := errgroup.WithContext(ctx)
|
eg, ctx := errgroup.WithContext(ctx)
|
||||||
|
|
||||||
// create a channel for paths to be processed
|
// create a channel for files needing to be processed
|
||||||
// we use a multiple of batch size here to allow for greater concurrency
|
// we use a multiple of batch size here as a rudimentary concurrency optimization based on the host machine
|
||||||
pathsCh = make(chan string, BatchSize*runtime.NumCPU())
|
filesCh = make(chan *walk.File, BatchSize*runtime.NumCPU())
|
||||||
|
|
||||||
// create a channel for tracking paths that have been processed
|
// create a channel for files that have been processed
|
||||||
processedCh = make(chan string, cap(pathsCh))
|
processedCh = make(chan *walk.File, cap(filesCh))
|
||||||
|
|
||||||
// start concurrent processing tasks
|
// start concurrent processing tasks in reverse order
|
||||||
eg.Go(updateCache(ctx))
|
eg.Go(updateCache(ctx))
|
||||||
eg.Go(applyFormatters(ctx))
|
eg.Go(applyFormatters(ctx))
|
||||||
eg.Go(walkFilesystem(ctx))
|
eg.Go(walkFilesystem(ctx))
|
||||||
|
@ -156,76 +135,15 @@ func (f *Format) Run() (err error) {
|
||||||
return eg.Wait()
|
return eg.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
func walkFilesystem(ctx context.Context) func() error {
|
|
||||||
return func() error {
|
|
||||||
paths := Cli.Paths
|
|
||||||
|
|
||||||
if len(paths) == 0 && Cli.Stdin {
|
|
||||||
|
|
||||||
cwd, err := os.Getwd()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%w: failed to determine current working directory", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// read in all the paths
|
|
||||||
scanner := bufio.NewScanner(os.Stdin)
|
|
||||||
for scanner.Scan() {
|
|
||||||
path := scanner.Text()
|
|
||||||
if !strings.HasPrefix(path, "/") {
|
|
||||||
// append the cwd
|
|
||||||
path = filepath.Join(cwd, path)
|
|
||||||
}
|
|
||||||
|
|
||||||
paths = append(paths, path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
walker, err := walk.New(Cli.Walk, Cli.TreeRoot, paths)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create walker: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
defer close(pathsCh)
|
|
||||||
|
|
||||||
if Cli.NoCache {
|
|
||||||
return walker.Walk(ctx, func(path string, info fs.FileInfo, err error) error {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return ctx.Err()
|
|
||||||
default:
|
|
||||||
// ignore symlinks and directories
|
|
||||||
if !(info.IsDir() || info.Mode()&os.ModeSymlink == os.ModeSymlink) {
|
|
||||||
stats.Add(stats.Traversed, 1)
|
|
||||||
stats.Add(stats.Emitted, 1)
|
|
||||||
pathsCh <- path
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = cache.ChangeSet(ctx, walker, pathsCh); err != nil {
|
|
||||||
return fmt.Errorf("failed to generate change set: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateCache(ctx context.Context) func() error {
|
func updateCache(ctx context.Context) func() error {
|
||||||
return func() error {
|
return func() error {
|
||||||
batch := make([]string, 0, BatchSize)
|
// used to batch updates for more efficient txs
|
||||||
|
batch := make([]*walk.File, 0, BatchSize)
|
||||||
var changes int
|
|
||||||
|
|
||||||
|
// apply a batch
|
||||||
processBatch := func() error {
|
processBatch := func() error {
|
||||||
if Cli.NoCache {
|
if err := cache.Update(batch); err != nil {
|
||||||
changes += len(batch)
|
return err
|
||||||
} else {
|
|
||||||
count, err := cache.Update(Cli.TreeRoot, batch)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
changes += count
|
|
||||||
}
|
}
|
||||||
batch = batch[:0]
|
batch = batch[:0]
|
||||||
return nil
|
return nil
|
||||||
|
@ -234,13 +152,17 @@ func updateCache(ctx context.Context) func() error {
|
||||||
LOOP:
|
LOOP:
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
|
// detect ctx cancellation
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
case path, ok := <-processedCh:
|
// respond to processed files
|
||||||
|
case file, ok := <-processedCh:
|
||||||
if !ok {
|
if !ok {
|
||||||
|
// channel has been closed, no further files to process
|
||||||
break LOOP
|
break LOOP
|
||||||
}
|
}
|
||||||
batch = append(batch, path)
|
// append to batch and process if we have enough
|
||||||
|
batch = append(batch, file)
|
||||||
if len(batch) == BatchSize {
|
if len(batch) == BatchSize {
|
||||||
if err := processBatch(); err != nil {
|
if err := processBatch(); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -254,48 +176,124 @@ func updateCache(ctx context.Context) func() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if Cli.FailOnChange && changes != 0 {
|
// if fail on change has been enabled, check that no files were actually formatted, throwing an error if so
|
||||||
|
if Cli.FailOnChange && stats.Value(stats.Formatted) != 0 {
|
||||||
return ErrFailOnChange
|
return ErrFailOnChange
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// print stats to stdout
|
||||||
stats.Print()
|
stats.Print()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func walkFilesystem(ctx context.Context) func() error {
|
||||||
|
return func() error {
|
||||||
|
paths := Cli.Paths
|
||||||
|
|
||||||
|
// we read paths from stdin if the cli flag has been set and no paths were provided as cli args
|
||||||
|
if len(paths) == 0 && Cli.Stdin {
|
||||||
|
|
||||||
|
// determine the current working directory
|
||||||
|
cwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to determine current working directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// read in all the paths
|
||||||
|
scanner := bufio.NewScanner(os.Stdin)
|
||||||
|
for scanner.Scan() {
|
||||||
|
path := scanner.Text()
|
||||||
|
if !strings.HasPrefix(path, "/") {
|
||||||
|
// append the cwd
|
||||||
|
path = filepath.Join(cwd, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// append the fully qualified path to our paths list
|
||||||
|
paths = append(paths, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a filesystem walker
|
||||||
|
walker, err := walk.New(Cli.Walk, Cli.TreeRoot, paths)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create walker: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// close the files channel when we're done walking the file system
|
||||||
|
defer close(filesCh)
|
||||||
|
|
||||||
|
// if no cache has been configured, we invoke the walker directly
|
||||||
|
if Cli.NoCache {
|
||||||
|
return walker.Walk(ctx, func(file *walk.File, err error) error {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
default:
|
||||||
|
// ignore symlinks and directories
|
||||||
|
if !(file.Info.IsDir() || file.Info.Mode()&os.ModeSymlink == os.ModeSymlink) {
|
||||||
|
stats.Add(stats.Traversed, 1)
|
||||||
|
stats.Add(stats.Emitted, 1)
|
||||||
|
filesCh <- file
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise we pass the walker to the cache and have it generate files for processing based on whether or not
|
||||||
|
// they have been added/changed since the last invocation
|
||||||
|
if err = cache.ChangeSet(ctx, walker, filesCh); err != nil {
|
||||||
|
return fmt.Errorf("failed to generate change set: %w", err)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyFormatters(ctx context.Context) func() error {
|
func applyFormatters(ctx context.Context) func() error {
|
||||||
|
// create our own errgroup for concurrent formatting tasks
|
||||||
fg, ctx := errgroup.WithContext(ctx)
|
fg, ctx := errgroup.WithContext(ctx)
|
||||||
batches := make(map[string][]string)
|
|
||||||
|
|
||||||
tryApply := func(key string, path string) {
|
// pre-initialise batches keyed by pipeline
|
||||||
batch, ok := batches[key]
|
batches := make(map[string][]*walk.File)
|
||||||
if !ok {
|
for key := range pipelines {
|
||||||
batch = make([]string, 0, BatchSize)
|
batches[key] = make([]*walk.File, 0, BatchSize)
|
||||||
}
|
}
|
||||||
batch = append(batch, path)
|
|
||||||
batches[key] = batch
|
|
||||||
|
|
||||||
|
// for a given pipeline key, add the provided file to the current batch and trigger a format if the batch size has
|
||||||
|
// been reached
|
||||||
|
tryApply := func(key string, file *walk.File) {
|
||||||
|
// append to batch
|
||||||
|
batches[key] = append(batches[key], file)
|
||||||
|
|
||||||
|
// check if the batch is full
|
||||||
|
batch := batches[key]
|
||||||
if len(batch) == BatchSize {
|
if len(batch) == BatchSize {
|
||||||
|
// get the pipeline
|
||||||
pipeline := pipelines[key]
|
pipeline := pipelines[key]
|
||||||
|
|
||||||
// copy the batch
|
// copy the batch
|
||||||
paths := make([]string, len(batch))
|
files := make([]*walk.File, len(batch))
|
||||||
copy(paths, batch)
|
copy(files, batch)
|
||||||
|
|
||||||
|
// apply to the pipeline
|
||||||
fg.Go(func() error {
|
fg.Go(func() error {
|
||||||
if err := pipeline.Apply(ctx, paths); err != nil {
|
if err := pipeline.Apply(ctx, files); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, path := range paths {
|
for _, path := range files {
|
||||||
processedCh <- path
|
processedCh <- path
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// reset the batch
|
||||||
batches[key] = batch[:0]
|
batches[key] = batch[:0]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// format any partial batches
|
||||||
flushBatches := func() {
|
flushBatches := func() {
|
||||||
for key, pipeline := range pipelines {
|
for key, pipeline := range pipelines {
|
||||||
|
|
||||||
|
@ -322,17 +320,21 @@ func applyFormatters(ctx context.Context) func() error {
|
||||||
close(processedCh)
|
close(processedCh)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
for path := range pathsCh {
|
// iterate the files channel, checking if any pipeline wants it, and attempting to apply if so.
|
||||||
|
for file := range filesCh {
|
||||||
var matched bool
|
var matched bool
|
||||||
for key, pipeline := range pipelines {
|
for key, pipeline := range pipelines {
|
||||||
if !pipeline.Wants(path) {
|
if !pipeline.Wants(file) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
matched = true
|
matched = true
|
||||||
tryApply(key, path)
|
tryApply(key, file)
|
||||||
}
|
}
|
||||||
if matched {
|
if matched {
|
||||||
stats.Add(stats.Matched, 1)
|
stats.Add(stats.Matched, 1)
|
||||||
|
} else {
|
||||||
|
// no match, so we send it direct to the processed channel
|
||||||
|
processedCh <- file
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -51,42 +51,42 @@ func TestSpecifyingFormatters(t *testing.T) {
|
||||||
test.WriteConfig(t, configPath, config2.Config{
|
test.WriteConfig(t, configPath, config2.Config{
|
||||||
Formatters: map[string]*config2.Formatter{
|
Formatters: map[string]*config2.Formatter{
|
||||||
"elm": {
|
"elm": {
|
||||||
Command: "echo",
|
Command: "touch",
|
||||||
Includes: []string{"*.elm"},
|
Includes: []string{"*.elm"},
|
||||||
},
|
},
|
||||||
"nix": {
|
"nix": {
|
||||||
Command: "echo",
|
Command: "touch",
|
||||||
Includes: []string{"*.nix"},
|
Includes: []string{"*.nix"},
|
||||||
},
|
},
|
||||||
"ruby": {
|
"ruby": {
|
||||||
Command: "echo",
|
Command: "touch",
|
||||||
Includes: []string{"*.rb"},
|
Includes: []string{"*.rb"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
out, err := cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir)
|
_, err := cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir)
|
||||||
as.NoError(err)
|
as.NoError(err)
|
||||||
assertFormatted(t, as, out, 3)
|
assertStats(t, as, 31, 31, 3, 3)
|
||||||
|
|
||||||
out, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir, "--formatters", "elm,nix")
|
_, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir, "--formatters", "elm,nix")
|
||||||
as.NoError(err)
|
as.NoError(err)
|
||||||
assertFormatted(t, as, out, 2)
|
assertStats(t, as, 31, 31, 2, 2)
|
||||||
|
|
||||||
out, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir, "--formatters", "ruby,nix")
|
_, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir, "--formatters", "ruby,nix")
|
||||||
as.NoError(err)
|
as.NoError(err)
|
||||||
assertFormatted(t, as, out, 2)
|
assertStats(t, as, 31, 31, 2, 2)
|
||||||
|
|
||||||
out, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir, "--formatters", "nix")
|
_, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir, "--formatters", "nix")
|
||||||
as.NoError(err)
|
as.NoError(err)
|
||||||
assertFormatted(t, as, out, 1)
|
assertStats(t, as, 31, 31, 1, 1)
|
||||||
|
|
||||||
// test bad names
|
// test bad names
|
||||||
|
|
||||||
out, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir, "--formatters", "foo")
|
_, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir, "--formatters", "foo")
|
||||||
as.Errorf(err, "formatter not found in config: foo")
|
as.Errorf(err, "formatter not found in config: foo")
|
||||||
|
|
||||||
out, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir, "--formatters", "bar,foo")
|
_, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir, "--formatters", "bar,foo")
|
||||||
as.Errorf(err, "formatter not found in config: bar")
|
as.Errorf(err, "formatter not found in config: bar")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,7 +94,7 @@ func TestIncludesAndExcludes(t *testing.T) {
|
||||||
as := require.New(t)
|
as := require.New(t)
|
||||||
|
|
||||||
tempDir := test.TempExamples(t)
|
tempDir := test.TempExamples(t)
|
||||||
configPath := tempDir + "/echo.toml"
|
configPath := tempDir + "/touch.toml"
|
||||||
|
|
||||||
// test without any excludes
|
// test without any excludes
|
||||||
cfg := config2.Config{
|
cfg := config2.Config{
|
||||||
|
@ -107,25 +107,25 @@ func TestIncludesAndExcludes(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
test.WriteConfig(t, configPath, cfg)
|
test.WriteConfig(t, configPath, cfg)
|
||||||
out, err := cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir)
|
_, err := cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir)
|
||||||
as.NoError(err)
|
as.NoError(err)
|
||||||
assertFormatted(t, as, out, 31)
|
assertStats(t, as, 31, 31, 31, 0)
|
||||||
|
|
||||||
// globally exclude nix files
|
// globally exclude nix files
|
||||||
cfg.Global.Excludes = []string{"*.nix"}
|
cfg.Global.Excludes = []string{"*.nix"}
|
||||||
|
|
||||||
test.WriteConfig(t, configPath, cfg)
|
test.WriteConfig(t, configPath, cfg)
|
||||||
out, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir)
|
_, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir)
|
||||||
as.NoError(err)
|
as.NoError(err)
|
||||||
assertFormatted(t, as, out, 30)
|
assertStats(t, as, 31, 31, 30, 0)
|
||||||
|
|
||||||
// add haskell files to the global exclude
|
// add haskell files to the global exclude
|
||||||
cfg.Global.Excludes = []string{"*.nix", "*.hs"}
|
cfg.Global.Excludes = []string{"*.nix", "*.hs"}
|
||||||
|
|
||||||
test.WriteConfig(t, configPath, cfg)
|
test.WriteConfig(t, configPath, cfg)
|
||||||
out, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir)
|
_, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir)
|
||||||
as.NoError(err)
|
as.NoError(err)
|
||||||
assertFormatted(t, as, out, 24)
|
assertStats(t, as, 31, 31, 24, 0)
|
||||||
|
|
||||||
echo := cfg.Formatters["echo"]
|
echo := cfg.Formatters["echo"]
|
||||||
|
|
||||||
|
@ -133,40 +133,40 @@ func TestIncludesAndExcludes(t *testing.T) {
|
||||||
echo.Excludes = []string{"*.py"}
|
echo.Excludes = []string{"*.py"}
|
||||||
|
|
||||||
test.WriteConfig(t, configPath, cfg)
|
test.WriteConfig(t, configPath, cfg)
|
||||||
out, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir)
|
_, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir)
|
||||||
as.NoError(err)
|
as.NoError(err)
|
||||||
assertFormatted(t, as, out, 22)
|
assertStats(t, as, 31, 31, 22, 0)
|
||||||
|
|
||||||
// remove go files from the echo formatter
|
// remove go files from the echo formatter
|
||||||
echo.Excludes = []string{"*.py", "*.go"}
|
echo.Excludes = []string{"*.py", "*.go"}
|
||||||
|
|
||||||
test.WriteConfig(t, configPath, cfg)
|
test.WriteConfig(t, configPath, cfg)
|
||||||
out, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir)
|
_, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir)
|
||||||
as.NoError(err)
|
as.NoError(err)
|
||||||
assertFormatted(t, as, out, 21)
|
assertStats(t, as, 31, 31, 21, 0)
|
||||||
|
|
||||||
// adjust the includes for echo to only include elm files
|
// adjust the includes for echo to only include elm files
|
||||||
echo.Includes = []string{"*.elm"}
|
echo.Includes = []string{"*.elm"}
|
||||||
|
|
||||||
test.WriteConfig(t, configPath, cfg)
|
test.WriteConfig(t, configPath, cfg)
|
||||||
out, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir)
|
_, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir)
|
||||||
as.NoError(err)
|
as.NoError(err)
|
||||||
assertFormatted(t, as, out, 1)
|
assertStats(t, as, 31, 31, 1, 0)
|
||||||
|
|
||||||
// add js files to echo formatter
|
// add js files to echo formatter
|
||||||
echo.Includes = []string{"*.elm", "*.js"}
|
echo.Includes = []string{"*.elm", "*.js"}
|
||||||
|
|
||||||
test.WriteConfig(t, configPath, cfg)
|
test.WriteConfig(t, configPath, cfg)
|
||||||
out, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir)
|
_, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir)
|
||||||
as.NoError(err)
|
as.NoError(err)
|
||||||
assertFormatted(t, as, out, 2)
|
assertStats(t, as, 31, 31, 2, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCache(t *testing.T) {
|
func TestCache(t *testing.T) {
|
||||||
as := require.New(t)
|
as := require.New(t)
|
||||||
|
|
||||||
tempDir := test.TempExamples(t)
|
tempDir := test.TempExamples(t)
|
||||||
configPath := tempDir + "/echo.toml"
|
configPath := tempDir + "/touch.toml"
|
||||||
|
|
||||||
// test without any excludes
|
// test without any excludes
|
||||||
cfg := config2.Config{
|
cfg := config2.Config{
|
||||||
|
@ -181,7 +181,7 @@ func TestCache(t *testing.T) {
|
||||||
test.WriteConfig(t, configPath, cfg)
|
test.WriteConfig(t, configPath, cfg)
|
||||||
out, err := cmd(t, "--config-file", configPath, "--tree-root", tempDir)
|
out, err := cmd(t, "--config-file", configPath, "--tree-root", tempDir)
|
||||||
as.NoError(err)
|
as.NoError(err)
|
||||||
assertFormatted(t, as, out, 31)
|
assertStats(t, as, 31, 31, 31, 0)
|
||||||
|
|
||||||
out, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir)
|
out, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir)
|
||||||
as.NoError(err)
|
as.NoError(err)
|
||||||
|
@ -190,7 +190,7 @@ func TestCache(t *testing.T) {
|
||||||
// clear cache
|
// clear cache
|
||||||
out, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir, "-c")
|
out, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir, "-c")
|
||||||
as.NoError(err)
|
as.NoError(err)
|
||||||
assertFormatted(t, as, out, 31)
|
assertStats(t, as, 31, 31, 31, 0)
|
||||||
|
|
||||||
out, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir)
|
out, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir)
|
||||||
as.NoError(err)
|
as.NoError(err)
|
||||||
|
@ -199,7 +199,7 @@ func TestCache(t *testing.T) {
|
||||||
// clear cache
|
// clear cache
|
||||||
out, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir, "-c")
|
out, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir, "-c")
|
||||||
as.NoError(err)
|
as.NoError(err)
|
||||||
assertFormatted(t, as, out, 31)
|
assertStats(t, as, 31, 31, 31, 0)
|
||||||
|
|
||||||
out, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir)
|
out, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir)
|
||||||
as.NoError(err)
|
as.NoError(err)
|
||||||
|
@ -208,7 +208,7 @@ func TestCache(t *testing.T) {
|
||||||
// no cache
|
// no cache
|
||||||
out, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir, "--no-cache")
|
out, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir, "--no-cache")
|
||||||
as.NoError(err)
|
as.NoError(err)
|
||||||
assertStats(t, as, out, 31, 31, 31, 0)
|
assertStats(t, as, 31, 31, 31, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestChangeWorkingDirectory(t *testing.T) {
|
func TestChangeWorkingDirectory(t *testing.T) {
|
||||||
|
@ -240,22 +240,22 @@ func TestChangeWorkingDirectory(t *testing.T) {
|
||||||
|
|
||||||
// by default, we look for ./treefmt.toml and use the cwd for the tree root
|
// by default, we look for ./treefmt.toml and use the cwd for the tree root
|
||||||
// this should fail if the working directory hasn't been changed first
|
// this should fail if the working directory hasn't been changed first
|
||||||
out, err := cmd(t, "-C", tempDir)
|
_, err = cmd(t, "-C", tempDir)
|
||||||
as.NoError(err)
|
as.NoError(err)
|
||||||
assertFormatted(t, as, out, 31)
|
assertStats(t, as, 31, 31, 31, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFailOnChange(t *testing.T) {
|
func TestFailOnChange(t *testing.T) {
|
||||||
as := require.New(t)
|
as := require.New(t)
|
||||||
|
|
||||||
tempDir := test.TempExamples(t)
|
tempDir := test.TempExamples(t)
|
||||||
configPath := tempDir + "/echo.toml"
|
configPath := tempDir + "/touch.toml"
|
||||||
|
|
||||||
// test without any excludes
|
// test without any excludes
|
||||||
cfg := config2.Config{
|
cfg := config2.Config{
|
||||||
Formatters: map[string]*config2.Formatter{
|
Formatters: map[string]*config2.Formatter{
|
||||||
"echo": {
|
"touch": {
|
||||||
Command: "echo",
|
Command: "touch",
|
||||||
Includes: []string{"*"},
|
Includes: []string{"*"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -270,7 +270,7 @@ func TestBustCacheOnFormatterChange(t *testing.T) {
|
||||||
as := require.New(t)
|
as := require.New(t)
|
||||||
|
|
||||||
tempDir := test.TempExamples(t)
|
tempDir := test.TempExamples(t)
|
||||||
configPath := tempDir + "/echo.toml"
|
configPath := tempDir + "/touch.toml"
|
||||||
|
|
||||||
// symlink some formatters into temp dir, so we can mess with their mod times
|
// symlink some formatters into temp dir, so we can mess with their mod times
|
||||||
binPath := tempDir + "/bin"
|
binPath := tempDir + "/bin"
|
||||||
|
@ -304,33 +304,33 @@ func TestBustCacheOnFormatterChange(t *testing.T) {
|
||||||
|
|
||||||
test.WriteConfig(t, configPath, cfg)
|
test.WriteConfig(t, configPath, cfg)
|
||||||
args := []string{"--config-file", configPath, "--tree-root", tempDir}
|
args := []string{"--config-file", configPath, "--tree-root", tempDir}
|
||||||
out, err := cmd(t, args...)
|
_, err := cmd(t, args...)
|
||||||
as.NoError(err)
|
as.NoError(err)
|
||||||
assertFormatted(t, as, out, 3)
|
assertStats(t, as, 31, 31, 3, 0)
|
||||||
|
|
||||||
// tweak mod time of elm formatter
|
// tweak mod time of elm formatter
|
||||||
as.NoError(test.RecreateSymlink(t, binPath+"/"+"elm-format"))
|
as.NoError(test.RecreateSymlink(t, binPath+"/"+"elm-format"))
|
||||||
|
|
||||||
out, err = cmd(t, args...)
|
_, err = cmd(t, args...)
|
||||||
as.NoError(err)
|
as.NoError(err)
|
||||||
assertFormatted(t, as, out, 3)
|
assertStats(t, as, 31, 31, 3, 0)
|
||||||
|
|
||||||
// check cache is working
|
// check cache is working
|
||||||
out, err = cmd(t, args...)
|
_, err = cmd(t, args...)
|
||||||
as.NoError(err)
|
as.NoError(err)
|
||||||
assertFormatted(t, as, out, 0)
|
assertStats(t, as, 31, 0, 0, 0)
|
||||||
|
|
||||||
// tweak mod time of python formatter
|
// tweak mod time of python formatter
|
||||||
as.NoError(test.RecreateSymlink(t, binPath+"/"+"black"))
|
as.NoError(test.RecreateSymlink(t, binPath+"/"+"black"))
|
||||||
|
|
||||||
out, err = cmd(t, args...)
|
_, err = cmd(t, args...)
|
||||||
as.NoError(err)
|
as.NoError(err)
|
||||||
assertFormatted(t, as, out, 3)
|
assertStats(t, as, 31, 31, 3, 0)
|
||||||
|
|
||||||
// check cache is working
|
// check cache is working
|
||||||
out, err = cmd(t, args...)
|
_, err = cmd(t, args...)
|
||||||
as.NoError(err)
|
as.NoError(err)
|
||||||
assertFormatted(t, as, out, 0)
|
assertStats(t, as, 31, 0, 0, 0)
|
||||||
|
|
||||||
// add go formatter
|
// add go formatter
|
||||||
cfg.Formatters["go"] = &config2.Formatter{
|
cfg.Formatters["go"] = &config2.Formatter{
|
||||||
|
@ -340,40 +340,40 @@ func TestBustCacheOnFormatterChange(t *testing.T) {
|
||||||
}
|
}
|
||||||
test.WriteConfig(t, configPath, cfg)
|
test.WriteConfig(t, configPath, cfg)
|
||||||
|
|
||||||
out, err = cmd(t, args...)
|
_, err = cmd(t, args...)
|
||||||
as.NoError(err)
|
as.NoError(err)
|
||||||
assertFormatted(t, as, out, 4)
|
assertStats(t, as, 31, 31, 4, 0)
|
||||||
|
|
||||||
// check cache is working
|
// check cache is working
|
||||||
out, err = cmd(t, args...)
|
_, err = cmd(t, args...)
|
||||||
as.NoError(err)
|
as.NoError(err)
|
||||||
assertFormatted(t, as, out, 0)
|
assertStats(t, as, 31, 0, 0, 0)
|
||||||
|
|
||||||
// remove python formatter
|
// remove python formatter
|
||||||
delete(cfg.Formatters, "python")
|
delete(cfg.Formatters, "python")
|
||||||
test.WriteConfig(t, configPath, cfg)
|
test.WriteConfig(t, configPath, cfg)
|
||||||
|
|
||||||
out, err = cmd(t, args...)
|
_, err = cmd(t, args...)
|
||||||
as.NoError(err)
|
as.NoError(err)
|
||||||
assertFormatted(t, as, out, 2)
|
assertStats(t, as, 31, 31, 2, 0)
|
||||||
|
|
||||||
// check cache is working
|
// check cache is working
|
||||||
out, err = cmd(t, args...)
|
_, err = cmd(t, args...)
|
||||||
as.NoError(err)
|
as.NoError(err)
|
||||||
assertFormatted(t, as, out, 0)
|
assertStats(t, as, 31, 0, 0, 0)
|
||||||
|
|
||||||
// remove elm formatter
|
// remove elm formatter
|
||||||
delete(cfg.Formatters, "elm")
|
delete(cfg.Formatters, "elm")
|
||||||
test.WriteConfig(t, configPath, cfg)
|
test.WriteConfig(t, configPath, cfg)
|
||||||
|
|
||||||
out, err = cmd(t, args...)
|
_, err = cmd(t, args...)
|
||||||
as.NoError(err)
|
as.NoError(err)
|
||||||
assertFormatted(t, as, out, 1)
|
assertStats(t, as, 31, 31, 1, 0)
|
||||||
|
|
||||||
// check cache is working
|
// check cache is working
|
||||||
out, err = cmd(t, args...)
|
_, err = cmd(t, args...)
|
||||||
as.NoError(err)
|
as.NoError(err)
|
||||||
assertFormatted(t, as, out, 0)
|
assertStats(t, as, 31, 0, 0, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGitWorktree(t *testing.T) {
|
func TestGitWorktree(t *testing.T) {
|
||||||
|
@ -407,28 +407,28 @@ func TestGitWorktree(t *testing.T) {
|
||||||
wt, err := repo.Worktree()
|
wt, err := repo.Worktree()
|
||||||
as.NoError(err, "failed to get git worktree")
|
as.NoError(err, "failed to get git worktree")
|
||||||
|
|
||||||
run := func(formatted int) {
|
run := func(traversed int, emitted int, matched int, formatted int) {
|
||||||
out, err := cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir)
|
out, err := cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir)
|
||||||
as.NoError(err)
|
as.NoError(err)
|
||||||
assertFormatted(t, as, out, formatted)
|
assertFormatted(t, as, out, formatted)
|
||||||
}
|
}
|
||||||
|
|
||||||
// run before adding anything to the worktree
|
// run before adding anything to the worktree
|
||||||
run(0)
|
run(0, 0, 0, 0)
|
||||||
|
|
||||||
// add everything to the worktree
|
// add everything to the worktree
|
||||||
as.NoError(wt.AddGlob("."))
|
as.NoError(wt.AddGlob("."))
|
||||||
as.NoError(err)
|
as.NoError(err)
|
||||||
run(31)
|
run(31, 31, 31, 0)
|
||||||
|
|
||||||
// remove python directory
|
// remove python directory
|
||||||
as.NoError(wt.RemoveGlob("python/*"))
|
as.NoError(wt.RemoveGlob("python/*"))
|
||||||
run(28)
|
run(28, 28, 28, 0)
|
||||||
|
|
||||||
// walk with filesystem instead of git
|
// walk with filesystem instead of git
|
||||||
out, err := cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir, "--walk", "filesystem")
|
_, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir, "--walk", "filesystem")
|
||||||
as.NoError(err)
|
as.NoError(err)
|
||||||
assertFormatted(t, as, out, 59)
|
assertStats(t, as, 59, 59, 59, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPathsArg(t *testing.T) {
|
func TestPathsArg(t *testing.T) {
|
||||||
|
@ -461,17 +461,17 @@ func TestPathsArg(t *testing.T) {
|
||||||
test.WriteConfig(t, configPath, cfg)
|
test.WriteConfig(t, configPath, cfg)
|
||||||
|
|
||||||
// without any path args
|
// without any path args
|
||||||
out, err := cmd(t, "-C", tempDir)
|
_, err = cmd(t, "-C", tempDir)
|
||||||
as.NoError(err)
|
as.NoError(err)
|
||||||
assertFormatted(t, as, out, 31)
|
assertStats(t, as, 31, 31, 31, 0)
|
||||||
|
|
||||||
// specify some explicit paths
|
// specify some explicit paths
|
||||||
out, err = cmd(t, "-C", tempDir, "-c", "elm/elm.json", "haskell/Nested/Foo.hs")
|
_, err = cmd(t, "-C", tempDir, "-c", "elm/elm.json", "haskell/Nested/Foo.hs")
|
||||||
as.NoError(err)
|
as.NoError(err)
|
||||||
assertFormatted(t, as, out, 2)
|
assertStats(t, as, 4, 4, 4, 0)
|
||||||
|
|
||||||
// specify a bad path
|
// specify a bad path
|
||||||
out, err = cmd(t, "-C", tempDir, "-c", "elm/elm.json", "haskell/Nested/Bar.hs")
|
_, err = cmd(t, "-C", tempDir, "-c", "elm/elm.json", "haskell/Nested/Bar.hs")
|
||||||
as.ErrorContains(err, "no such file or directory")
|
as.ErrorContains(err, "no such file or directory")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -526,9 +526,9 @@ go/main.go
|
||||||
_, _ = stdin.Seek(0, 0)
|
_, _ = stdin.Seek(0, 0)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
out, err := cmd(t, "-C", tempDir, "--stdin")
|
_, err = cmd(t, "-C", tempDir, "--stdin")
|
||||||
as.NoError(err)
|
as.NoError(err)
|
||||||
assertFormatted(t, as, out, 3)
|
assertStats(t, as, 6, 6, 6, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDeterministicOrderingInPipeline(t *testing.T) {
|
func TestDeterministicOrderingInPipeline(t *testing.T) {
|
||||||
|
|
|
@ -7,6 +7,8 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"git.numtide.com/numtide/treefmt/stats"
|
||||||
|
|
||||||
"git.numtide.com/numtide/treefmt/test"
|
"git.numtide.com/numtide/treefmt/test"
|
||||||
|
|
||||||
"github.com/alecthomas/kong"
|
"github.com/alecthomas/kong"
|
||||||
|
@ -55,12 +57,12 @@ func cmd(t *testing.T, args ...string) ([]byte, error) {
|
||||||
|
|
||||||
// reset and read the temporary output
|
// reset and read the temporary output
|
||||||
if _, err = tempOut.Seek(0, 0); err != nil {
|
if _, err = tempOut.Seek(0, 0); err != nil {
|
||||||
return nil, fmt.Errorf("%w: failed to reset temp output for reading", err)
|
return nil, fmt.Errorf("failed to reset temp output for reading: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
out, err := io.ReadAll(tempOut)
|
out, err := io.ReadAll(tempOut)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%w: failed to read temp output", err)
|
return nil, fmt.Errorf("failed to read temp output: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// swap outputs back
|
// swap outputs back
|
||||||
|
@ -70,12 +72,12 @@ func cmd(t *testing.T, args ...string) ([]byte, error) {
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func assertStats(t *testing.T, as *require.Assertions, output []byte, traversed int32, emitted int32, matched int32, formatted int32) {
|
func assertStats(t *testing.T, as *require.Assertions, traversed int32, emitted int32, matched int32, formatted int32) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
as.Contains(string(output), fmt.Sprintf("traversed %d files", traversed))
|
as.Equal(traversed, stats.Value(stats.Traversed))
|
||||||
as.Contains(string(output), fmt.Sprintf("emitted %d files", emitted))
|
as.Equal(emitted, stats.Value(stats.Emitted))
|
||||||
as.Contains(string(output), fmt.Sprintf("matched %d files", matched))
|
as.Equal(matched, stats.Value(stats.Matched))
|
||||||
as.Contains(string(output), fmt.Sprintf("formatted %d files", formatted))
|
as.Equal(formatted, stats.Value(stats.Formatted))
|
||||||
}
|
}
|
||||||
|
|
||||||
func assertFormatted(t *testing.T, as *require.Assertions, output []byte, count int) {
|
func assertFormatted(t *testing.T, as *require.Assertions, output []byte, count int) {
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import "github.com/BurntSushi/toml"
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/BurntSushi/toml"
|
||||||
|
)
|
||||||
|
|
||||||
// Config is used to represent the list of configured Formatters.
|
// Config is used to represent the list of configured Formatters.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
@ -8,11 +13,39 @@ type Config struct {
|
||||||
// Excludes is an optional list of glob patterns used to exclude certain files from all formatters.
|
// Excludes is an optional list of glob patterns used to exclude certain files from all formatters.
|
||||||
Excludes []string
|
Excludes []string
|
||||||
}
|
}
|
||||||
|
Names []string `toml:"-"`
|
||||||
Formatters map[string]*Formatter `toml:"formatter"`
|
Formatters map[string]*Formatter `toml:"formatter"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadFile reads from path and unmarshals toml into a Config instance.
|
// ReadFile reads from path and unmarshals toml into a Config instance.
|
||||||
func ReadFile(path string) (cfg *Config, err error) {
|
func ReadFile(path string, names []string) (cfg *Config, err error) {
|
||||||
_, err = toml.DecodeFile(path, &cfg)
|
if _, err = toml.DecodeFile(path, &cfg); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to decode config file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// filter formatters based on provided names
|
||||||
|
if len(names) > 0 {
|
||||||
|
filtered := make(map[string]*Formatter)
|
||||||
|
|
||||||
|
// check if the provided names exist in the config
|
||||||
|
for _, name := range names {
|
||||||
|
formatterCfg, ok := cfg.Formatters[name]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("formatter %v not found in config", name)
|
||||||
|
}
|
||||||
|
filtered[name] = formatterCfg
|
||||||
|
}
|
||||||
|
|
||||||
|
// updated formatters
|
||||||
|
cfg.Formatters = filtered
|
||||||
|
}
|
||||||
|
|
||||||
|
// sort the formatter names so that, as we construct pipelines, we add formatters in a determinstic fashion. This
|
||||||
|
// ensures a deterministic order even when all priority values are the same e.g. 0
|
||||||
|
for name := range cfg.Formatters {
|
||||||
|
cfg.Names = append(cfg.Names, name)
|
||||||
|
}
|
||||||
|
sort.Strings(cfg.Names)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import (
|
||||||
func TestReadConfigFile(t *testing.T) {
|
func TestReadConfigFile(t *testing.T) {
|
||||||
as := require.New(t)
|
as := require.New(t)
|
||||||
|
|
||||||
cfg, err := ReadFile("../test/examples/treefmt.toml")
|
cfg, err := ReadFile("../test/examples/treefmt.toml", nil)
|
||||||
as.NoError(err, "failed to read config file")
|
as.NoError(err, "failed to read config file")
|
||||||
|
|
||||||
as.NotNil(cfg)
|
as.NotNil(cfg)
|
||||||
|
|
17
default.nix
Normal file
17
default.nix
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
# This file provides backward compatibility to nix < 2.4 clients
|
||||||
|
{system ? builtins.currentSystem}: let
|
||||||
|
lock = builtins.fromJSON (builtins.readFile ./flake.lock);
|
||||||
|
|
||||||
|
inherit (lock.nodes.flake-compat.locked) owner repo rev narHash;
|
||||||
|
|
||||||
|
flake-compat = fetchTarball {
|
||||||
|
url = "https://github.com/${owner}/${repo}/archive/${rev}.tar.gz";
|
||||||
|
sha256 = narHash;
|
||||||
|
};
|
||||||
|
|
||||||
|
flake = import flake-compat {
|
||||||
|
inherit system;
|
||||||
|
src = ./.;
|
||||||
|
};
|
||||||
|
in
|
||||||
|
flake.defaultNix
|
36
docs/.vitepress/config.mts
Normal file
36
docs/.vitepress/config.mts
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import { defineConfig } from 'vitepress'
|
||||||
|
|
||||||
|
// https://vitepress.dev/reference/site-config
|
||||||
|
export default defineConfig({
|
||||||
|
title: "Treefmt",
|
||||||
|
description: "one CLI to format your repo",
|
||||||
|
themeConfig: {
|
||||||
|
|
||||||
|
logo: '/logo.svg',
|
||||||
|
|
||||||
|
// https://vitepress.dev/reference/default-theme-config
|
||||||
|
nav: [
|
||||||
|
{ text: 'Home', link: '/' },
|
||||||
|
{ text: 'Quick Start', link: '/quick-start' }
|
||||||
|
],
|
||||||
|
|
||||||
|
sidebar: [
|
||||||
|
{ text: 'Quick Start', link: '/quick-start' },
|
||||||
|
{ text: 'Installation', link: '/installation' },
|
||||||
|
{ text: 'Overview', link: '/overview' },
|
||||||
|
{ text: 'Usage', link: '/usage' },
|
||||||
|
{ text: 'Formatter Specification', link: '/formatter-spec' },
|
||||||
|
{ text: 'Contributing', link: '/contributing' },
|
||||||
|
{ text: 'FAQ', link: '/faq' },
|
||||||
|
],
|
||||||
|
|
||||||
|
socialLinks: [
|
||||||
|
{ icon: 'github', link: 'https://git.numtide.com/numtide/treefmt' }
|
||||||
|
],
|
||||||
|
|
||||||
|
footer: {
|
||||||
|
message: 'Released under the <a href="https://git.numtide.com/numtide/treefmt/src/branch/main/LICENSE.md">MIT License</a>.',
|
||||||
|
copyright: "Copyright © 2024-present Treefmt Contributors"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
17
docs/.vitepress/theme/index.ts
Normal file
17
docs/.vitepress/theme/index.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
// https://vitepress.dev/guide/custom-theme
|
||||||
|
import { h } from "vue";
|
||||||
|
import type { Theme } from "vitepress";
|
||||||
|
import DefaultTheme from "vitepress/theme";
|
||||||
|
import "./style.css";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
extends: DefaultTheme,
|
||||||
|
Layout: () => {
|
||||||
|
return h(DefaultTheme.Layout, null, {
|
||||||
|
// https://vitepress.dev/guide/extending-default-theme#layout-slots
|
||||||
|
});
|
||||||
|
},
|
||||||
|
enhanceApp({ app, router, siteData }) {
|
||||||
|
// ...
|
||||||
|
},
|
||||||
|
} satisfies Theme;
|
146
docs/.vitepress/theme/style.css
Normal file
146
docs/.vitepress/theme/style.css
Normal file
|
@ -0,0 +1,146 @@
|
||||||
|
/**
|
||||||
|
* Customize default theme styling by overriding CSS variables:
|
||||||
|
* https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Colors
|
||||||
|
*
|
||||||
|
* Each colors have exact same color scale system with 3 levels of solid
|
||||||
|
* colors with different brightness, and 1 soft color.
|
||||||
|
*
|
||||||
|
* - `XXX-1`: The most solid color used mainly for colored text. It must
|
||||||
|
* satisfy the contrast ratio against when used on top of `XXX-soft`.
|
||||||
|
*
|
||||||
|
* - `XXX-2`: The color used mainly for hover state of the button.
|
||||||
|
*
|
||||||
|
* - `XXX-3`: The color for solid background, such as bg color of the button.
|
||||||
|
* It must satisfy the contrast ratio with pure white (#ffffff) text on
|
||||||
|
* top of it.
|
||||||
|
*
|
||||||
|
* - `XXX-soft`: The color used for subtle background such as custom container
|
||||||
|
* or badges. It must satisfy the contrast ratio when putting `XXX-1` colors
|
||||||
|
* on top of it.
|
||||||
|
*
|
||||||
|
* The soft color must be semi transparent alpha channel. This is crucial
|
||||||
|
* because it allows adding multiple "soft" colors on top of each other
|
||||||
|
* to create a accent, such as when having inline code block inside
|
||||||
|
* custom containers.
|
||||||
|
*
|
||||||
|
* - `default`: The color used purely for subtle indication without any
|
||||||
|
* special meanings attched to it such as bg color for menu hover state.
|
||||||
|
*
|
||||||
|
* - `brand`: Used for primary brand colors, such as link text, button with
|
||||||
|
* brand theme, etc.
|
||||||
|
*
|
||||||
|
* - `tip`: Used to indicate useful information. The default theme uses the
|
||||||
|
* brand color for this by default.
|
||||||
|
*
|
||||||
|
* - `warning`: Used to indicate warning to the users. Used in custom
|
||||||
|
* container, badges, etc.
|
||||||
|
*
|
||||||
|
* - `danger`: Used to show error, or dangerous message to the users. Used
|
||||||
|
* in custom container, badges, etc.
|
||||||
|
* -------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--vp-c-treefmt-orange-1: #f79433;
|
||||||
|
--vp-c-treefmt-orange-2: #f6861a;
|
||||||
|
--vp-c-treefmt-orange-3: #dd6d00;
|
||||||
|
--vp-c-treefmt-orange-soft: rgba(245, 121, 0, 0.14);
|
||||||
|
|
||||||
|
--vp-c-treefmt-green-1: #9de05c;
|
||||||
|
--vp-c-treefmt-green-2: #8fdb45;
|
||||||
|
--vp-c-treefmt-green-3: #73d216;
|
||||||
|
--vp-c-treefmt-green-soft: rgba(115, 210, 22, 0.14);
|
||||||
|
|
||||||
|
--vp-c-treefmt-gray-1: #6d6e6a;
|
||||||
|
--vp-c-treefmt-gray-2: #888a85;
|
||||||
|
--vp-c-treefmt-gray-3: #a0a19d;
|
||||||
|
--vp-c-treefmt-gray-soft: rgba(136, 138, 133, 0.14);
|
||||||
|
|
||||||
|
--vp-c-default-1: var(--vp-c-treefmt-gray-1);
|
||||||
|
--vp-c-default-2: var(--vp-c-treefmt-gray-2);
|
||||||
|
--vp-c-default-3: var(--vp-c-treefmt-gray-3);
|
||||||
|
--vp-c-default-soft: var(--vp-c-treefmt-gray-soft);
|
||||||
|
|
||||||
|
--vp-c-brand-1: var(--vp-c-treefmt-orange-1);
|
||||||
|
--vp-c-brand-2: var(--vp-c-treefmt-orange-2);
|
||||||
|
--vp-c-brand-3: var(--vp-c-treefmt-orange-3);
|
||||||
|
--vp-c-brand-soft: var(--vp-c-treefmt-orange-soft);
|
||||||
|
|
||||||
|
--vp-c-tip-1: var(--vp-c-brand-1);
|
||||||
|
--vp-c-tip-2: var(--vp-c-brand-2);
|
||||||
|
--vp-c-tip-3: var(--vp-c-brand-3);
|
||||||
|
--vp-c-tip-soft: var(--vp-c-brand-soft);
|
||||||
|
|
||||||
|
--vp-c-warning-1: var(--vp-c-yellow-1);
|
||||||
|
--vp-c-warning-2: var(--vp-c-yellow-2);
|
||||||
|
--vp-c-warning-3: var(--vp-c-yellow-3);
|
||||||
|
--vp-c-warning-soft: var(--vp-c-yellow-soft);
|
||||||
|
|
||||||
|
--vp-c-danger-1: var(--vp-c-red-1);
|
||||||
|
--vp-c-danger-2: var(--vp-c-red-2);
|
||||||
|
--vp-c-danger-3: var(--vp-c-red-3);
|
||||||
|
--vp-c-danger-soft: var(--vp-c-red-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component: Button
|
||||||
|
* -------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--vp-button-brand-border: transparent;
|
||||||
|
--vp-button-brand-text: var(--vp-c-white);
|
||||||
|
--vp-button-brand-bg: var(--vp-c-brand-3);
|
||||||
|
--vp-button-brand-hover-border: transparent;
|
||||||
|
--vp-button-brand-hover-text: var(--vp-c-white);
|
||||||
|
--vp-button-brand-hover-bg: var(--vp-c-brand-2);
|
||||||
|
--vp-button-brand-active-border: transparent;
|
||||||
|
--vp-button-brand-active-text: var(--vp-c-white);
|
||||||
|
--vp-button-brand-active-bg: var(--vp-c-brand-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component: Home
|
||||||
|
* -------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--vp-home-hero-image-background-image: linear-gradient(
|
||||||
|
-45deg,
|
||||||
|
var(--vp-c-treefmt-orange-3) 50%,
|
||||||
|
var(--vp-c-treefmt-green-1) 50%
|
||||||
|
);
|
||||||
|
--vp-home-hero-image-filter: blur(44px);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
:root {
|
||||||
|
--vp-home-hero-image-filter: blur(56px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 960px) {
|
||||||
|
:root {
|
||||||
|
--vp-home-hero-image-filter: blur(68px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component: Custom Block
|
||||||
|
* -------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--vp-custom-block-tip-border: transparent;
|
||||||
|
--vp-custom-block-tip-text: var(--vp-c-text-1);
|
||||||
|
--vp-custom-block-tip-bg: var(--vp-c-brand-soft);
|
||||||
|
--vp-custom-block-tip-code-bg: var(--vp-c-brand-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component: Algolia
|
||||||
|
* -------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
.DocSearch {
|
||||||
|
--docsearch-primary-color: var(--vp-c-brand-1) !important;
|
||||||
|
}
|
142
docs/contributing.md
Normal file
142
docs/contributing.md
Normal file
|
@ -0,0 +1,142 @@
|
||||||
|
---
|
||||||
|
outline: deep
|
||||||
|
---
|
||||||
|
|
||||||
|
# Contribution guidelines
|
||||||
|
|
||||||
|
This file contains instructions that will help you make a contribution.
|
||||||
|
|
||||||
|
## Licensing
|
||||||
|
|
||||||
|
The `treefmt` binaries and this user guide are licensed under the [MIT license](https://numtide.github.io/treefmt/LICENSE.html).
|
||||||
|
|
||||||
|
## Before you contribute
|
||||||
|
|
||||||
|
Here you can take a look at the [existing issues](https://github.com/numtide/treefmt/issues). Feel free to contribute, but make sure you have a
|
||||||
|
[GitHub account](https://github.com/join) first :slightly_smiling_face:.
|
||||||
|
|
||||||
|
If you're new to open source, please read GitHub's guide on [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/). It's a quick read,
|
||||||
|
and it's a great way to introduce yourself to how things work behind the scenes in open-source projects.
|
||||||
|
|
||||||
|
Before sending a pull request, make sure that you've read all the guidelines. If you don't understand something, please
|
||||||
|
[state your question clearly in an issue](https://github.com/numtide/treefmt/issues/new) or ask the community on the [treefmt matrix server](https://matrix.to/#/#treefmt:numtide.com).
|
||||||
|
|
||||||
|
## Creating an issue
|
||||||
|
|
||||||
|
If you need to create an issue, make sure to clearly describe it, including:
|
||||||
|
|
||||||
|
- The steps to reproduce it (if it's a bug)
|
||||||
|
- The version of `treefmt` used
|
||||||
|
|
||||||
|
The cache database is stored in a `.db` file in the `~/.cache/treefmt/eval-cache` directory.
|
||||||
|
|
||||||
|
## Making changes
|
||||||
|
|
||||||
|
If you want to introduce changes to the project, please follow these steps:
|
||||||
|
|
||||||
|
- Fork the repository on GitHub
|
||||||
|
- Create a branch on your fork. Don't commit directly to main
|
||||||
|
- Add the necessary tests for your changes
|
||||||
|
- Run `treefmt` in the source directory before you commit your changes
|
||||||
|
- Push your changes to the branch in your repository fork
|
||||||
|
- Submit a pull request to the original repository
|
||||||
|
|
||||||
|
Make sure you based your commits on logical and atomic units!
|
||||||
|
|
||||||
|
## Examples of git history
|
||||||
|
|
||||||
|
<details>
|
||||||
|
|
||||||
|
<summary>Git history that we want to have</summary>
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
* e3ed88b (HEAD -> contribution-guide, upstream/main, origin/main, origin/HEAD, main) Merge pull request #470 from zimbatm/fix_lru_cache
|
||||||
|
|
||||||
|
|\
|
||||||
|
|
||||||
|
| * 1ab7d9f Use rayon for multithreading command
|
||||||
|
|
||||||
|
|/
|
||||||
|
|
||||||
|
* e9c5bb4 Merge pull request #468 from zimbatm/multithread
|
||||||
|
|
||||||
|
|\
|
||||||
|
|
||||||
|
| * de2d6cf Add lint property for Formatter struct
|
||||||
|
|
||||||
|
| * cd2ed17 Fix impl on Formatter get_command() function
|
||||||
|
|
||||||
|
|/
|
||||||
|
|
||||||
|
* 028c344 Merge pull request #465 from rayon/0.15.0-release
|
||||||
|
|
||||||
|
|\
|
||||||
|
|
||||||
|
| * 7b619d6 0.15.0 release
|
||||||
|
|
||||||
|
|/
|
||||||
|
|
||||||
|
* acdf7df Merge pull request #463 from zimbatm/support-multi-part-namespaces
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
|
||||||
|
<summary>Git history that we are <b>trying</b> to avoid</summary>
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
* 4c8aca8 Merge pull request #120 from zimbatm/add-rayon
|
||||||
|
|
||||||
|
|\
|
||||||
|
|
||||||
|
| * fc2b449 use rayon for engine now
|
||||||
|
|
||||||
|
| * 2304683 add rayon config
|
||||||
|
|
||||||
|
| * 5285bd3 bump base image to F30
|
||||||
|
|
||||||
|
* | 4d0fbe2 Merge pull request #114 from rizary/create_method_create_release
|
||||||
|
|
||||||
|
|\ \
|
||||||
|
|
||||||
|
| * | 36a9396 test changed
|
||||||
|
|
||||||
|
| * | 22f681d method create release for github created
|
||||||
|
|
||||||
|
* | | 2ef4ea1 Merge pull request #119 from rizary/config.rs
|
||||||
|
|
||||||
|
|\ \ \
|
||||||
|
|
||||||
|
| |/ /
|
||||||
|
|
||||||
|
|/| |
|
||||||
|
|
||||||
|
| * | 5f1b8f0 unused functions removed
|
||||||
|
|
||||||
|
* | | a93c361 Merge pull request #117 from zimbatm/add-getreleases-to-abstract
|
||||||
|
|
||||||
|
|\ \ \
|
||||||
|
|
||||||
|
| |/ /
|
||||||
|
|
||||||
|
|/| |
|
||||||
|
|
||||||
|
| * | 0a97236 add get_releses for Cargo
|
||||||
|
|
||||||
|
| * | 55e4c57 add get_releases/get_release into engine.rs
|
||||||
|
|
||||||
|
|/ /
|
||||||
|
|
||||||
|
* | badeddd Merge pull request #101 from zimbatm/extreme-cachin
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
Additionally, it's always good to work on improving documentation and adding examples.
|
||||||
|
|
||||||
|
Thank you for considering contributing to `treefmt`.
|
28
docs/faq.md
Normal file
28
docs/faq.md
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
# FAQ
|
||||||
|
|
||||||
|
## How does treefmt function?
|
||||||
|
|
||||||
|
`Treefmt` traverses all your project's folders, maps files to specific code formatters, and formats the code
|
||||||
|
accordingly. Other tools also traverse the filesystem, but not necessarily starting from the root of the project.
|
||||||
|
|
||||||
|
Contrary to other formatters, `treefmt` doesn't preview the changes before writing them to a file. If you want to view
|
||||||
|
the changes, you can always check the diff in your version control (we assume that your project is checked into a
|
||||||
|
version control system).
|
||||||
|
|
||||||
|
You can also rely on version control if errors were introduced into your code as a result of disruptions in the
|
||||||
|
formatter's work.
|
||||||
|
|
||||||
|
## How is the cache organized?
|
||||||
|
|
||||||
|
At the moment, the cache is a [BoltDB] database file in which file paths are mapped to `mtimes`.
|
||||||
|
|
||||||
|
The file is located in:
|
||||||
|
|
||||||
|
```
|
||||||
|
~/.cache/treefmt/eval-cache/<hash-of-the-treefmt.toml-path>.db
|
||||||
|
```
|
||||||
|
|
||||||
|
At the end of each run, the database is updated with the last formatting time entries. In this way, we can
|
||||||
|
compare the last change time of the file to the last formatting time, and figure out which files need re-formatting.
|
||||||
|
|
||||||
|
[BoltDB]: https://github.com/etcd-io/bbolt
|
57
docs/formatter-spec.md
Normal file
57
docs/formatter-spec.md
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
---
|
||||||
|
outline: deep
|
||||||
|
---
|
||||||
|
|
||||||
|
# Formatter Specification
|
||||||
|
|
||||||
|
In order to keep the design of `treefmt` simple, we only supports formatters that adhere to a certain standard. This
|
||||||
|
document outlines that standard.
|
||||||
|
|
||||||
|
If the formatter you would like to use doesn't comply with the rules, it's often possible to create a wrapper script
|
||||||
|
that transforms the usage to match the specification.
|
||||||
|
|
||||||
|
In this design, we rely on `treefmt` to do the tree traversal, and only invoke
|
||||||
|
the code formatter on the selected files.
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
In order for the formatter to comply to this spec, it **MUST** comply with the following:
|
||||||
|
|
||||||
|
### 1. Files passed as arguments
|
||||||
|
|
||||||
|
In order to be integrated with `treefmt`'s workflow, the formatter's CLI must be of the form:
|
||||||
|
|
||||||
|
```
|
||||||
|
<command> [options] [...<files>]
|
||||||
|
```
|
||||||
|
|
||||||
|
Where:
|
||||||
|
|
||||||
|
- `<command>` is the name of the formatting tool.
|
||||||
|
- `[options]` is any number of flags and options that the formatter accepts.
|
||||||
|
- `[...<files>]` is one or more files given to the formatter for processing.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ rustfmt --edition 2018 src/main.rs src/lib.rs
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> It _SHOULD_ processes only the specified files. Files that are not passed _SHOULD_ never be formatted.
|
||||||
|
|
||||||
|
### 2. Write to changed files
|
||||||
|
|
||||||
|
Whenever there is a change to the code formatting, the code formatter **MUST** write to the changes back to the
|
||||||
|
original location.
|
||||||
|
|
||||||
|
If there is no changes to the original file, the formatter **MUST** NOT write to the original location.
|
||||||
|
|
||||||
|
### 3. Idempotent
|
||||||
|
|
||||||
|
The code formatter _SHOULD_ be indempotent. Meaning that it produces stable
|
||||||
|
outputs.
|
||||||
|
|
||||||
|
### 4. Reliable
|
||||||
|
|
||||||
|
We expect the formatter to be reliable and not break the semantics of the formatted files.
|
143
docs/formatters.md
Normal file
143
docs/formatters.md
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
# A list of known formatters
|
||||||
|
|
||||||
|
Here is a list of all the formatters we tested. Feel free to send a PR to add
|
||||||
|
other ones!
|
||||||
|
|
||||||
|
## [prettier](https://prettier.io/)
|
||||||
|
|
||||||
|
An opinionated code formatter that supports many languages.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
command = "prettier"
|
||||||
|
options = ["--write"]
|
||||||
|
includes = [
|
||||||
|
"*.css",
|
||||||
|
"*.html",
|
||||||
|
"*.js",
|
||||||
|
"*.json",
|
||||||
|
"*.jsx",
|
||||||
|
"*.md",
|
||||||
|
"*.mdx",
|
||||||
|
"*.scss",
|
||||||
|
"*.ts",
|
||||||
|
"*.yaml",
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
## [Black](https://github.com/psf/black)
|
||||||
|
|
||||||
|
A python formatter.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
command = "black"
|
||||||
|
includes = ["*.py"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## [clang-format](https://clang.llvm.org/docs/ClangFormat.html)
|
||||||
|
|
||||||
|
A tool to format C/C++/Java/JavaScript/Objective-C/Protobuf/C# code.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
command = "clang-format"
|
||||||
|
options = [ "-i" ]
|
||||||
|
includes = [ "*.c", "*.cpp", "*.cc", "*.h", "*.hpp" ]
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: This example focuses on C/C++ but can be modified to use with other languages.
|
||||||
|
|
||||||
|
## Elm
|
||||||
|
|
||||||
|
```toml
|
||||||
|
command = "elm-format"
|
||||||
|
options = ["--yes"]
|
||||||
|
includes = ["*.elm"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Go
|
||||||
|
|
||||||
|
```toml
|
||||||
|
command = "gofmt"
|
||||||
|
options = ["-w"]
|
||||||
|
includes = ["*.go"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## [Ormolu](https://github.com/tweag/ormolu)
|
||||||
|
|
||||||
|
Haskell formatter. Make sure to use ormolu 0.1.4.0+ as older versions don't
|
||||||
|
adhere to the spec.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
command = "ormolu"
|
||||||
|
options = [
|
||||||
|
"--ghc-opt", "-XBangPatterns",
|
||||||
|
"--ghc-opt", "-XPatternSynonyms",
|
||||||
|
"--ghc-opt", "-XTypeApplications",
|
||||||
|
"--mode", "inplace",
|
||||||
|
"--check-idempotence",
|
||||||
|
]
|
||||||
|
includes = ["*.hs"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## [stylish-haskell](https://github.com/jaspervdj/stylish-haskell)
|
||||||
|
|
||||||
|
Another Haskell formatter.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
command = "stylish-haskell"
|
||||||
|
options = [ "--inplace" ]
|
||||||
|
includes = [ "*.hs" ]
|
||||||
|
```
|
||||||
|
|
||||||
|
## [nixpkgs-fmt](https://github.com/nix-community/nixpkgs-fmt)
|
||||||
|
|
||||||
|
Nix code formatter.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
command = "nixpkgs-fmt"
|
||||||
|
includes = ["*.nix"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## rustfmt
|
||||||
|
|
||||||
|
```toml
|
||||||
|
command = "rustfmt"
|
||||||
|
options = ["--edition", "2018"]
|
||||||
|
includes = ["*.rs"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## [rufo](https://github.com/ruby-formatter/rufo)
|
||||||
|
|
||||||
|
Rufo is an opinionated ruby formatter. By default it exits with status 3 on
|
||||||
|
file change so we have to pass the `-x` option.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
command = "rufo"
|
||||||
|
options = ["-x"]
|
||||||
|
includes = ["*.rb"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## cargo fmt
|
||||||
|
|
||||||
|
`cargo fmt` is not supported as it doesn't follow the spec. It doesn't allow
|
||||||
|
to pass arbitrary files to be formatted, which treefmt relies on. Use `rustfmt`
|
||||||
|
instead (which is what `cargo fmt` uses under the hood).
|
||||||
|
|
||||||
|
## [shfmt](https://github.com/mvdan/sh)
|
||||||
|
|
||||||
|
A shell code formatter.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
command = "shfmt"
|
||||||
|
options = [
|
||||||
|
"-i",
|
||||||
|
"2", # indent 2
|
||||||
|
"-s", # simplify the code
|
||||||
|
"-w", # write back to the file
|
||||||
|
]
|
||||||
|
includes = ["*.sh"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## terraform
|
||||||
|
|
||||||
|
terraform fmt only supports formatting one file at the time. See
|
||||||
|
https://github.com/hashicorp/terraform/pull/28191
|
18
docs/index.md
Normal file
18
docs/index.md
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
---
|
||||||
|
# https://vitepress.dev/reference/default-theme-home-page
|
||||||
|
layout: home
|
||||||
|
|
||||||
|
hero:
|
||||||
|
name: "Treefmt"
|
||||||
|
text: "One CLI to format your repo"
|
||||||
|
image:
|
||||||
|
src: /fmt.gif
|
||||||
|
alt: Treefmt
|
||||||
|
actions:
|
||||||
|
- theme: brand
|
||||||
|
text: Quick Start
|
||||||
|
link: /quick-start
|
||||||
|
- theme: alt
|
||||||
|
text: More Info
|
||||||
|
link: /overview
|
||||||
|
---
|
71
docs/installation.md
Normal file
71
docs/installation.md
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
---
|
||||||
|
outline: deep
|
||||||
|
---
|
||||||
|
|
||||||
|
# Installation
|
||||||
|
|
||||||
|
There are two options to install `treefmt`:
|
||||||
|
|
||||||
|
1. Downloading the latest binary
|
||||||
|
2. Compiling and building it from source.
|
||||||
|
|
||||||
|
## Installing with a binary file
|
||||||
|
|
||||||
|
You can find the list of the latest binaries [here](https://github.com/numtide/treefmt/releases).
|
||||||
|
|
||||||
|
## Building from source
|
||||||
|
|
||||||
|
There are several ways to build `treefmt` from source. Your choice will depend on whether you're a [nix](https://github.com/NixOS/nix) user or
|
||||||
|
not.
|
||||||
|
|
||||||
|
### Non-Nix User
|
||||||
|
|
||||||
|
To try the project without building it, run:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ go run main.go --help
|
||||||
|
```
|
||||||
|
|
||||||
|
The command will output the manual. You can run the tool in this manner with any other flag or option to format your
|
||||||
|
project.
|
||||||
|
|
||||||
|
To build a binary, you need to have `go 1.22` installed. You can find instructions [here](https://go.dev/doc/install).
|
||||||
|
|
||||||
|
Now, if you want to build the project, switch to the project root folder and run:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ go build
|
||||||
|
```
|
||||||
|
|
||||||
|
After the successful execution of the build command, you will find the `treefmt` binary in the project root folder.
|
||||||
|
|
||||||
|
### Nix User
|
||||||
|
|
||||||
|
If you're using both `treefmt` and `nix`, you can go for [`treefmt-nix`](https://github.com/numtide/treefmt-nix), a wrapper that makes installation and
|
||||||
|
configuration of `treefmt` with `nix` easier.
|
||||||
|
|
||||||
|
**Non-flake user**
|
||||||
|
|
||||||
|
Here you also have two options: you can install `treefmt` with plain nix-build , or with nix-shell.
|
||||||
|
|
||||||
|
To build the package with nix-build, just run:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ nix-build -A treefmt
|
||||||
|
```
|
||||||
|
|
||||||
|
**Nix-flake user**
|
||||||
|
|
||||||
|
If you want to use this repository with flakes, please enable the flakes feature first. To run the project with flakes without building it, you can execute the following command in the root folder:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ nix run . -- --help
|
||||||
|
```
|
||||||
|
|
||||||
|
To build the project, run the following command in the root folder:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ nix build
|
||||||
|
```
|
||||||
|
|
||||||
|
The `treefmt` binary will be available in the result folder.
|
75
docs/overview.md
Normal file
75
docs/overview.md
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
# About the project
|
||||||
|
|
||||||
|
`treefmt` is a formatting tool that saves you time: it provides developers with a universal way to trigger all
|
||||||
|
formatters needed for the project in one place.
|
||||||
|
|
||||||
|
## Background
|
||||||
|
|
||||||
|
Typically, each project has its own code standards enforced by the project's owner. Any code contributions must match
|
||||||
|
that given standard, i.e. be formatted in a specific manner.
|
||||||
|
|
||||||
|
At first glance, the task of code formatting may seem trivial: the formatter can be automatically triggered when you
|
||||||
|
save a file in your IDE. Indeed, formatting doesn't take much effort if you're working on a single project long term:
|
||||||
|
setting up the formatters in your IDE won't take much of your time, and then you're ready to go.
|
||||||
|
|
||||||
|
Contrary to that, if you're working on multiple projects at the same time, you may have to update your formatter
|
||||||
|
configs in the IDE each time you switch between the projects. This is because formatter settings aren't
|
||||||
|
project-specific --- they are set up globally for all projects.
|
||||||
|
|
||||||
|
Alternatively, you can trigger formatters manually, one-by-one or in a script. Actually, for bigger projects, it's
|
||||||
|
common to have a script that runs over your project's directories and calls formatters consequently. But it takes time
|
||||||
|
to iterate through all the files.
|
||||||
|
|
||||||
|
All the solutions take up a significant amount of time which a developer could spend doing the actual work. They also
|
||||||
|
require you to remember which formatters and options are used by each project you are working on.
|
||||||
|
|
||||||
|
`treefmt` solves these issues.
|
||||||
|
|
||||||
|
## Why treefmt?
|
||||||
|
|
||||||
|
`treefmt`'s configuration is project-specific, so you don't need to re-configure formatters each time you switch
|
||||||
|
between projects, like you have to when working with formatters in the IDE.
|
||||||
|
|
||||||
|
Contrary to calling formatters from the command line, there's no need to remember all the specific formatters required
|
||||||
|
for each project. Once you set up the config, you can run the tool in any of your project's folders without any
|
||||||
|
additional flags or options.
|
||||||
|
|
||||||
|
Typically, formatters have different ways to say there was a specific error. With `treefmt`, you get a standardized
|
||||||
|
output which is easier to understand than the variegated outputs of different formatters, so it takes less time to
|
||||||
|
grasp what's wrong.
|
||||||
|
|
||||||
|
In addition, `treefmt` works faster than the custom script solution because the changed files are cached and the
|
||||||
|
formatters run only against them. Moreover, formatters are run in parallel, which makes the tool even faster.
|
||||||
|
|
||||||
|
The difference may not be significant for smaller projects, but it gets quite visible as the project grows. For
|
||||||
|
instance, take the caching optimization.
|
||||||
|
|
||||||
|
It takes 22 seconds to traverse a project of 40,559 files and no changes without caching:
|
||||||
|
|
||||||
|
```
|
||||||
|
traversed 40559 files
|
||||||
|
emitted 40559 files for processing
|
||||||
|
matched 33712 files to formatters
|
||||||
|
formatted 33712 files in 22.270884528s
|
||||||
|
```
|
||||||
|
|
||||||
|
...while it takes **270 milliseconds** to traverse the same project with caching:
|
||||||
|
|
||||||
|
```
|
||||||
|
traversed 40559 files
|
||||||
|
emitted 6847 files for processing
|
||||||
|
matched 0 files to formatters
|
||||||
|
formatted 0 files in 270.53513ms
|
||||||
|
```
|
||||||
|
|
||||||
|
The tool can be invoked manually or integrated into your CI. There's currently no integration with IDEs, but the feature is coming soon.
|
||||||
|
|
||||||
|
## What we still need help with
|
||||||
|
|
||||||
|
- **IDE integration:** Most of developers are used to formatting a file upon save in the IDE. So far, you can't use `treefmt` for this purpose, but we're working on it 😀
|
||||||
|
- **Pre-commit hook:** It's good to have your code checked for adherence to the project's standards before commit. `treefmt` pre-commit hook won't let you commit if you have formatting issues.
|
||||||
|
|
||||||
|
As a next step, learn how to [install] and [use] `treefmt`.
|
||||||
|
|
||||||
|
[install]: installation.md
|
||||||
|
[use]: usage.md
|
1629
docs/package-lock.json
generated
Normal file
1629
docs/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
13
docs/package.json
Normal file
13
docs/package.json
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"name": "treefmt-docs",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"devDependencies": {
|
||||||
|
"vitepress": "^1.1.4"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"vitepress": "vitepress",
|
||||||
|
"dev": "vitepress dev",
|
||||||
|
"build": "vitepress build",
|
||||||
|
"preview": "vitepress preview"
|
||||||
|
}
|
||||||
|
}
|
BIN
docs/public/fmt.gif
Normal file
BIN
docs/public/fmt.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 33 KiB |
194
docs/public/logo.svg
Normal file
194
docs/public/logo.svg
Normal file
|
@ -0,0 +1,194 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
inkscape:export-ydpi="512"
|
||||||
|
inkscape:export-xdpi="512"
|
||||||
|
inkscape:export-filename="/home/basile/dev/treefmt.png"
|
||||||
|
sodipodi:docname="treefmt.svg"
|
||||||
|
inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
|
||||||
|
id="svg16"
|
||||||
|
version="1.1"
|
||||||
|
viewBox="0 0 12.7 12.7"
|
||||||
|
height="48"
|
||||||
|
width="48">
|
||||||
|
<defs
|
||||||
|
id="defs10">
|
||||||
|
<inkscape:path-effect
|
||||||
|
hide_knots="false"
|
||||||
|
only_selected="false"
|
||||||
|
apply_with_radius="true"
|
||||||
|
apply_no_radius="true"
|
||||||
|
use_knot_distance="true"
|
||||||
|
flexible="false"
|
||||||
|
chamfer_steps="1"
|
||||||
|
radius="0.5"
|
||||||
|
mode="F"
|
||||||
|
method="auto"
|
||||||
|
unit="px"
|
||||||
|
satellites_param="F,0,0,1,0,0.13229167,0,1 @ F,0,0,1,0,0.13229167,0,1 @ F,0,0,1,0,0.13229167,0,1 @ F,0,0,1,0,0.13229167,0,1"
|
||||||
|
lpeversion="1"
|
||||||
|
is_visible="true"
|
||||||
|
id="path-effect1012"
|
||||||
|
effect="fillet_chamfer" />
|
||||||
|
<inkscape:path-effect
|
||||||
|
hide_knots="false"
|
||||||
|
only_selected="false"
|
||||||
|
apply_with_radius="true"
|
||||||
|
apply_no_radius="true"
|
||||||
|
use_knot_distance="true"
|
||||||
|
flexible="false"
|
||||||
|
chamfer_steps="1"
|
||||||
|
radius="0.5"
|
||||||
|
mode="F"
|
||||||
|
method="auto"
|
||||||
|
unit="px"
|
||||||
|
satellites_param="F,0,0,1,0,0.13229167,0,1 @ F,0,0,1,0,0.13229167,0,1 @ F,0,0,1,0,0.13229167,0,1 @ F,0,0,1,0,0.13229167,0,1"
|
||||||
|
lpeversion="1"
|
||||||
|
is_visible="true"
|
||||||
|
id="path-effect1010"
|
||||||
|
effect="fillet_chamfer" />
|
||||||
|
<inkscape:path-effect
|
||||||
|
hide_knots="false"
|
||||||
|
only_selected="false"
|
||||||
|
apply_with_radius="true"
|
||||||
|
apply_no_radius="true"
|
||||||
|
use_knot_distance="true"
|
||||||
|
flexible="false"
|
||||||
|
chamfer_steps="1"
|
||||||
|
radius="0.5"
|
||||||
|
mode="F"
|
||||||
|
method="auto"
|
||||||
|
unit="px"
|
||||||
|
satellites_param="F,0,0,1,0,0.13229167,0,1 @ F,0,0,1,0,0.13229167,0,1 @ F,0,0,1,0,0.13229167,0,1 @ F,0,0,1,0,0.13229167,0,1"
|
||||||
|
lpeversion="1"
|
||||||
|
is_visible="true"
|
||||||
|
id="path-effect989"
|
||||||
|
effect="fillet_chamfer" />
|
||||||
|
</defs>
|
||||||
|
<sodipodi:namedview
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:window-y="340"
|
||||||
|
inkscape:window-x="1440"
|
||||||
|
inkscape:window-height="1373"
|
||||||
|
inkscape:window-width="2560"
|
||||||
|
units="px"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:document-rotation="0"
|
||||||
|
inkscape:current-layer="layer1"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
inkscape:cy="29.374509"
|
||||||
|
inkscape:cx="-10.431476"
|
||||||
|
inkscape:zoom="11.2"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
borderopacity="1.0"
|
||||||
|
bordercolor="#666666"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
id="base">
|
||||||
|
<inkscape:grid
|
||||||
|
dotted="false"
|
||||||
|
id="grid902"
|
||||||
|
type="xygrid" />
|
||||||
|
</sodipodi:namedview>
|
||||||
|
<metadata
|
||||||
|
id="metadata13">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title></dc:title>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
id="layer1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
inkscape:label="Layer 1">
|
||||||
|
<path
|
||||||
|
d="m 6.3370673,12.637056 c -0.5007028,0 -5.1699157,-2.6957712 -5.42026711,-3.1293926 C 0.66644877,9.074042 0.66644862,3.6824995 0.91680002,3.2488781 1.1671514,2.8152568 5.8363641,0.11948539 6.337067,0.11948538 c 0.5007028,-2e-8 5.169916,2.69577112 5.420267,3.12939252 0.250351,0.4336213 0.250352,5.8251638 0,6.2587852 -0.250351,0.4336214 -4.9195639,3.1293929 -5.4202667,3.1293929 z"
|
||||||
|
inkscape:randomized="0"
|
||||||
|
inkscape:rounded="0.08"
|
||||||
|
inkscape:flatsided="true"
|
||||||
|
sodipodi:arg2="2.0943951"
|
||||||
|
sodipodi:arg1="1.5707963"
|
||||||
|
sodipodi:r2="5.4202676"
|
||||||
|
sodipodi:r1="6.2587852"
|
||||||
|
sodipodi:cy="6.3782705"
|
||||||
|
sodipodi:cx="6.3370669"
|
||||||
|
sodipodi:sides="6"
|
||||||
|
id="path20"
|
||||||
|
style="fill:#729fcf;stroke-width:0.0142755"
|
||||||
|
sodipodi:type="star" />
|
||||||
|
<path
|
||||||
|
sodipodi:type="star"
|
||||||
|
style="fill:#888a85;stroke-width:0.0128537"
|
||||||
|
id="path20-3"
|
||||||
|
sodipodi:sides="6"
|
||||||
|
sodipodi:cx="6.3652418"
|
||||||
|
sodipodi:cy="6.3777179"
|
||||||
|
sodipodi:r1="5.6354737"
|
||||||
|
sodipodi:r2="4.8804641"
|
||||||
|
sodipodi:arg1="1.5707963"
|
||||||
|
sodipodi:arg2="2.0943951"
|
||||||
|
inkscape:flatsided="true"
|
||||||
|
inkscape:rounded="0.08"
|
||||||
|
inkscape:randomized="0"
|
||||||
|
d="m 6.3652422,12.013192 c -0.4508379,0 -4.6550446,-2.4273 -4.8804635,-2.817737 -0.225419,-0.3904371 -0.2254191,-5.2450367 -2e-7,-5.6354738 0.225419,-0.390437 4.4296255,-2.81773694 4.8804634,-2.81773696 0.4508379,-1e-8 4.6550441,2.42729966 4.8804631,2.81773676 0.225419,0.390437 0.225419,5.2450366 0,5.6354737 -0.225418,0.3904371 -4.4296249,2.8177373 -4.8804628,2.8177373 z" />
|
||||||
|
<path
|
||||||
|
sodipodi:type="rect"
|
||||||
|
d="m 4.6302084,3.8364582 5.0270831,0 A 0.13229167,0.13229167 45 0 1 9.7895832,3.9687499 V 4.4979165 A 0.13229167,0.13229167 135 0 1 9.6572915,4.6302082 l -5.0270831,0 A 0.13229167,0.13229167 45 0 1 4.4979167,4.4979165 V 3.9687499 A 0.13229167,0.13229167 135 0 1 4.6302084,3.8364582 Z"
|
||||||
|
inkscape:path-effect="#path-effect989"
|
||||||
|
y="3.8364582"
|
||||||
|
x="4.4979167"
|
||||||
|
height="0.79374999"
|
||||||
|
width="5.2916665"
|
||||||
|
id="rect898"
|
||||||
|
style="fill:#f57900;stroke-width:0.0121433" />
|
||||||
|
<circle
|
||||||
|
r="0.66145831"
|
||||||
|
style="fill:#73d216;stroke-width:0.0221209"
|
||||||
|
id="path860-3"
|
||||||
|
cx="2.778125"
|
||||||
|
cy="4.2333331" />
|
||||||
|
<circle
|
||||||
|
cy="6.4822917"
|
||||||
|
cx="4.6302085"
|
||||||
|
id="path860-3-5"
|
||||||
|
style="fill:#73d216;stroke-width:0.0221209"
|
||||||
|
r="0.66145831" />
|
||||||
|
<circle
|
||||||
|
r="0.66145831"
|
||||||
|
style="fill:#73d216;stroke-width:0.0221209"
|
||||||
|
id="path860-3-5-6"
|
||||||
|
cx="4.6302085"
|
||||||
|
cy="8.9958334" />
|
||||||
|
<path
|
||||||
|
sodipodi:type="rect"
|
||||||
|
d="M 6.4822916,6.0854168 H 9.6572915 A 0.13229167,0.13229167 45 0 1 9.7895832,6.2177085 V 6.7468751 A 0.13229167,0.13229167 135 0 1 9.6572915,6.8791668 H 6.4822916 A 0.13229167,0.13229167 45 0 1 6.3499999,6.7468751 V 6.2177085 A 0.13229167,0.13229167 135 0 1 6.4822916,6.0854168 Z"
|
||||||
|
inkscape:path-effect="#path-effect1010"
|
||||||
|
style="fill:#f57900;stroke-width:0.00979024"
|
||||||
|
id="rect898-2"
|
||||||
|
width="3.4395833"
|
||||||
|
height="0.79374999"
|
||||||
|
x="6.3499999"
|
||||||
|
y="6.0854168" />
|
||||||
|
<path
|
||||||
|
sodipodi:type="rect"
|
||||||
|
d="m 6.4822916,8.598959 3.1749999,0 A 0.13229167,0.13229167 45 0 1 9.7895832,8.7312506 V 9.2604173 A 0.13229167,0.13229167 135 0 1 9.6572915,9.392709 l -3.1749999,0 A 0.13229167,0.13229167 45 0 1 6.3499999,9.2604173 V 8.7312506 A 0.13229167,0.13229167 135 0 1 6.4822916,8.598959 Z"
|
||||||
|
inkscape:path-effect="#path-effect1012"
|
||||||
|
y="8.598959"
|
||||||
|
x="6.3499999"
|
||||||
|
height="0.79374999"
|
||||||
|
width="3.4395833"
|
||||||
|
id="rect898-2-9"
|
||||||
|
style="fill:#f57900;stroke-width:0.00979024" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 7.0 KiB |
18
docs/quick-start.md
Normal file
18
docs/quick-start.md
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
---
|
||||||
|
outline: none
|
||||||
|
---
|
||||||
|
|
||||||
|
# Quick start
|
||||||
|
|
||||||
|
To run `treefmt` in your project, please follow these steps:
|
||||||
|
|
||||||
|
1. [Install] `treefmt`.
|
||||||
|
2. Install any formatters you wish to use with `treefmt`.
|
||||||
|
3. Run `treefmt --init` to generate a basic configuration file `treefmt.toml`.
|
||||||
|
4. Edit the configuration, adding formatters as desired. See [here] for more info on how to configure a formatter.
|
||||||
|
5. Run `treefmt` with the needed flags and options. You can check the supported options by executing `treefmt --help`.
|
||||||
|
|
||||||
|
In the following sections we will guide you through installing and configuring `treefmt` in detail.
|
||||||
|
|
||||||
|
[install]: installation.md
|
||||||
|
[here]: formatter-spec.md
|
107
docs/usage.md
Normal file
107
docs/usage.md
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
# Usage
|
||||||
|
|
||||||
|
You can run treefmt by executing:
|
||||||
|
|
||||||
|
`$ treefmt`
|
||||||
|
|
||||||
|
or, if it's not in your `$PATH`:
|
||||||
|
|
||||||
|
`$ ./treefmt`
|
||||||
|
|
||||||
|
Treefmt has the following specification:
|
||||||
|
|
||||||
|
```
|
||||||
|
treefmt [FLAGS] [OPTIONS] [--] [paths]...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Flags
|
||||||
|
|
||||||
|
`--allow-missing-formatter`
|
||||||
|
|
||||||
|
> Do not exit with an error if some of the configured formatters are missing.
|
||||||
|
|
||||||
|
`--clear-cache`
|
||||||
|
|
||||||
|
> Reset the evaluation cache. Invalidation should happen automatically if the formatting tool has been updated, or if the files to format have changed. If cache wasn't properly invalidated, you can use this flag to clear the cache.
|
||||||
|
|
||||||
|
`--fail-on-change`
|
||||||
|
|
||||||
|
> Exit with error if some files require re-formatting. This is useful for your CI if you want to detect if the contributed code was forgotten to be formatted.
|
||||||
|
|
||||||
|
`-h, --help`
|
||||||
|
|
||||||
|
> Prints available flags and options
|
||||||
|
|
||||||
|
`--init`
|
||||||
|
|
||||||
|
> Creates a new config file `treefmt.toml`.
|
||||||
|
|
||||||
|
`--no-cache`
|
||||||
|
|
||||||
|
> Tells `treefmt` to ignore the evaluation cache entirely. With this flag, you can avoid cache invalidation issues, if any. Typically, the machine that is running treefmt in the CI is starting with a fresh environment each time, so any calculated cache is lost. The `--no-cache` flag eliminates unnecessary work in the CI.
|
||||||
|
|
||||||
|
`-q, --quiet`
|
||||||
|
|
||||||
|
> Don't print output to stderr.
|
||||||
|
|
||||||
|
`--stdin`
|
||||||
|
|
||||||
|
> Format the content passed in stdin.
|
||||||
|
|
||||||
|
`-V, --version`
|
||||||
|
|
||||||
|
> Print version information.
|
||||||
|
|
||||||
|
`-v, --verbose`
|
||||||
|
|
||||||
|
> Change the log verbosity. Log verbosity is based off the number of 'v' used. With one `-v`, your logs will display `[INFO]` and `[ERROR]` messages, while `-vv` will also show `[DEBUG]` messages.
|
||||||
|
|
||||||
|
`--config-file <config-file>`
|
||||||
|
|
||||||
|
> Run with the specified config file which is not in the project tree.
|
||||||
|
|
||||||
|
`-f, --formatters <formatters>...`
|
||||||
|
|
||||||
|
> Only apply selected formatters. Defaults to all formatters.
|
||||||
|
|
||||||
|
`--tree-root <tree-root>`
|
||||||
|
|
||||||
|
> Set the path to the tree root directory where treefmt will look for the files to format. Defaults to the folder holding the `treefmt.toml` file. It’s mostly useful in combination with `--config-file` to specify the project root which won’t coincide with the directory holding `treefmt.toml`.
|
||||||
|
|
||||||
|
`-C <work-dir>`
|
||||||
|
|
||||||
|
> Run as if `treefmt` was started in `<work-dir>` instead of the current working directory (default: `.`). Equivalent to `cd <work dir>; treefmt`.
|
||||||
|
|
||||||
|
## Arguments
|
||||||
|
|
||||||
|
`<paths>...`
|
||||||
|
|
||||||
|
> Paths to format. Defaults to formatting the whole tree
|
||||||
|
|
||||||
|
## CI integration
|
||||||
|
|
||||||
|
Typically, you would use treefmt in the CI with the `--fail-on-change` and `--no-cache flags`. Find the explanations above.
|
||||||
|
|
||||||
|
You can you set a `treefmt` job in the GitHub pipeline for Ubuntu with nix-shell like this:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: treefmt
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches: main
|
||||||
|
jobs:
|
||||||
|
formatter:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v1
|
||||||
|
- uses: cachix/install-nix-action@v12
|
||||||
|
with:
|
||||||
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
|
- uses: cachix/cachix-action@v10
|
||||||
|
with:
|
||||||
|
name: nix-community
|
||||||
|
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||||
|
- name: treefmt
|
||||||
|
run: nix-shell --run "treefmt --fail-on-change --no-cache"
|
||||||
|
```
|
24
docs/vhs/fmt.tape
Normal file
24
docs/vhs/fmt.tape
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
Require treefmt
|
||||||
|
|
||||||
|
Set Shell zsh
|
||||||
|
Set FontSize 11
|
||||||
|
|
||||||
|
Set Theme "Catppuccin Mocha"
|
||||||
|
|
||||||
|
Set Width 320
|
||||||
|
Set Height 220
|
||||||
|
Set Padding 20
|
||||||
|
|
||||||
|
Type "treefmt -c"
|
||||||
|
Sleep 1s
|
||||||
|
Enter
|
||||||
|
|
||||||
|
Sleep 3s
|
||||||
|
Enter
|
||||||
|
Enter
|
||||||
|
Sleep 1s
|
||||||
|
|
||||||
|
Type "treefmt"
|
||||||
|
Enter
|
||||||
|
|
||||||
|
Sleep 5s
|
16
flake.lock
16
flake.lock
|
@ -21,6 +21,21 @@
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"flake-compat": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1688025799,
|
||||||
|
"narHash": "sha256-ktpB4dRtnksm9F5WawoIkEneh1nrEvuxb5lJFt1iOyw=",
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "flake-compat",
|
||||||
|
"rev": "8bf105319d44f6b9f0d764efa4fdef9f1cc9ba1c",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "flake-compat",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
"flake-parts": {
|
"flake-parts": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"nixpkgs-lib": "nixpkgs-lib"
|
"nixpkgs-lib": "nixpkgs-lib"
|
||||||
|
@ -163,6 +178,7 @@
|
||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"devshell": "devshell",
|
"devshell": "devshell",
|
||||||
|
"flake-compat": "flake-compat",
|
||||||
"flake-parts": "flake-parts",
|
"flake-parts": "flake-parts",
|
||||||
"flake-root": "flake-root",
|
"flake-root": "flake-root",
|
||||||
"gomod2nix": "gomod2nix",
|
"gomod2nix": "gomod2nix",
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||||
|
|
||||||
flake-parts.url = "github:hercules-ci/flake-parts";
|
flake-parts.url = "github:hercules-ci/flake-parts";
|
||||||
flake-root.url = "github:srid/flake-root";
|
flake-root.url = "github:srid/flake-root";
|
||||||
treefmt-nix = {
|
treefmt-nix = {
|
||||||
|
@ -19,8 +18,8 @@
|
||||||
url = "github:nix-community/gomod2nix";
|
url = "github:nix-community/gomod2nix";
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
};
|
};
|
||||||
|
|
||||||
nix-filter.url = "github:numtide/nix-filter";
|
nix-filter.url = "github:numtide/nix-filter";
|
||||||
|
flake-compat.url = "github:nix-community/flake-compat";
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs = inputs @ {flake-parts, ...}:
|
outputs = inputs @ {flake-parts, ...}:
|
||||||
|
|
|
@ -8,6 +8,8 @@ import (
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.numtide.com/numtide/treefmt/walk"
|
||||||
|
|
||||||
"git.numtide.com/numtide/treefmt/config"
|
"git.numtide.com/numtide/treefmt/config"
|
||||||
|
|
||||||
"github.com/charmbracelet/log"
|
"github.com/charmbracelet/log"
|
||||||
|
@ -38,7 +40,7 @@ func (f *Formatter) Executable() string {
|
||||||
return f.executable
|
return f.executable
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Formatter) Apply(ctx context.Context, paths []string, filter bool) error {
|
func (f *Formatter) Apply(ctx context.Context, files []*walk.File, filter bool) error {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|
||||||
// construct args, starting with config
|
// construct args, starting with config
|
||||||
|
@ -52,9 +54,9 @@ func (f *Formatter) Apply(ctx context.Context, paths []string, filter bool) erro
|
||||||
f.batch = f.batch[:0]
|
f.batch = f.batch[:0]
|
||||||
|
|
||||||
// filter paths
|
// filter paths
|
||||||
for _, path := range paths {
|
for _, file := range files {
|
||||||
if f.Wants(path) {
|
if f.Wants(file) {
|
||||||
f.batch = append(f.batch, path)
|
f.batch = append(f.batch, file.RelPath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,12 +69,14 @@ func (f *Formatter) Apply(ctx context.Context, paths []string, filter bool) erro
|
||||||
args = append(args, f.batch...)
|
args = append(args, f.batch...)
|
||||||
} else {
|
} else {
|
||||||
// exit early if nothing to process
|
// exit early if nothing to process
|
||||||
if len(paths) == 0 {
|
if len(files) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// append paths to the args
|
// append paths to the args
|
||||||
args = append(args, paths...)
|
for _, file := range files {
|
||||||
|
args = append(args, file.RelPath)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// execute the command
|
// execute the command
|
||||||
|
@ -83,22 +87,22 @@ func (f *Formatter) Apply(ctx context.Context, paths []string, filter bool) erro
|
||||||
if len(out) > 0 {
|
if len(out) > 0 {
|
||||||
_, _ = fmt.Fprintf(os.Stderr, "%s error:\n%s\n", f.name, out)
|
_, _ = fmt.Fprintf(os.Stderr, "%s error:\n%s\n", f.name, out)
|
||||||
}
|
}
|
||||||
return fmt.Errorf("%w: formatter %s failed to apply", err, f.name)
|
return fmt.Errorf("formatter %s failed to apply: %w", f.name, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
|
|
||||||
f.log.Infof("%v files processed in %v", len(paths), time.Now().Sub(start))
|
f.log.Infof("%v files processed in %v", len(files), time.Now().Sub(start))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wants is used to test if a Formatter wants a path based on it's configured Includes and Excludes patterns.
|
// Wants is used to test if a Formatter wants a path based on it's configured Includes and Excludes patterns.
|
||||||
// Returns true if the Formatter should be applied to path, false otherwise.
|
// Returns true if the Formatter should be applied to path, false otherwise.
|
||||||
func (f *Formatter) Wants(path string) bool {
|
func (f *Formatter) Wants(file *walk.File) bool {
|
||||||
match := !PathMatches(path, f.excludes) && PathMatches(path, f.includes)
|
match := !PathMatches(file.RelPath, f.excludes) && PathMatches(file.RelPath, f.includes)
|
||||||
if match {
|
if match {
|
||||||
f.log.Debugf("match: %v", path)
|
f.log.Debugf("match: %v", file)
|
||||||
}
|
}
|
||||||
return match
|
return match
|
||||||
}
|
}
|
||||||
|
@ -107,7 +111,7 @@ func (f *Formatter) Wants(path string) bool {
|
||||||
func NewFormatter(
|
func NewFormatter(
|
||||||
name string,
|
name string,
|
||||||
treeRoot string,
|
treeRoot string,
|
||||||
config *config.Formatter,
|
cfg *config.Formatter,
|
||||||
globalExcludes []glob.Glob,
|
globalExcludes []glob.Glob,
|
||||||
) (*Formatter, error) {
|
) (*Formatter, error) {
|
||||||
var err error
|
var err error
|
||||||
|
@ -116,11 +120,11 @@ func NewFormatter(
|
||||||
|
|
||||||
// capture config and the formatter's name
|
// capture config and the formatter's name
|
||||||
f.name = name
|
f.name = name
|
||||||
f.config = config
|
f.config = cfg
|
||||||
f.workingDir = treeRoot
|
f.workingDir = treeRoot
|
||||||
|
|
||||||
// test if the formatter is available
|
// test if the formatter is available
|
||||||
executable, err := exec.LookPath(config.Command)
|
executable, err := exec.LookPath(cfg.Command)
|
||||||
if errors.Is(err, exec.ErrNotFound) {
|
if errors.Is(err, exec.ErrNotFound) {
|
||||||
return nil, ErrCommandNotFound
|
return nil, ErrCommandNotFound
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
|
@ -129,20 +133,20 @@ func NewFormatter(
|
||||||
f.executable = executable
|
f.executable = executable
|
||||||
|
|
||||||
// initialise internal state
|
// initialise internal state
|
||||||
if config.Pipeline == "" {
|
if cfg.Pipeline == "" {
|
||||||
f.log = log.WithPrefix(fmt.Sprintf("format | %s", name))
|
f.log = log.WithPrefix(fmt.Sprintf("format | %s", name))
|
||||||
} else {
|
} else {
|
||||||
f.log = log.WithPrefix(fmt.Sprintf("format | %s[%s]", config.Pipeline, name))
|
f.log = log.WithPrefix(fmt.Sprintf("format | %s[%s]", cfg.Pipeline, name))
|
||||||
}
|
}
|
||||||
|
|
||||||
f.includes, err = CompileGlobs(config.Includes)
|
f.includes, err = CompileGlobs(cfg.Includes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%w: formatter '%v' includes", err, f.name)
|
return nil, fmt.Errorf("failed to compile formatter '%v' includes: %w", f.name, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
f.excludes, err = CompileGlobs(config.Excludes)
|
f.excludes, err = CompileGlobs(cfg.Excludes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%w: formatter '%v' excludes", err, f.name)
|
return nil, fmt.Errorf("failed to compile formatter '%v' excludes: %w", f.name, err)
|
||||||
}
|
}
|
||||||
f.excludes = append(f.excludes, globalExcludes...)
|
f.excludes = append(f.excludes, globalExcludes...)
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@ func CompileGlobs(patterns []string) ([]glob.Glob, error) {
|
||||||
for i, pattern := range patterns {
|
for i, pattern := range patterns {
|
||||||
g, err := glob.Compile(pattern)
|
g, err := glob.Compile(pattern)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%w: failed to compile include pattern '%v'", err, pattern)
|
return nil, fmt.Errorf("failed to compile include pattern '%v': %w", pattern, err)
|
||||||
}
|
}
|
||||||
globs[i] = g
|
globs[i] = g
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,8 @@ package format
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
|
"git.numtide.com/numtide/treefmt/walk"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Pipeline struct {
|
type Pipeline struct {
|
||||||
|
@ -17,7 +19,7 @@ func (p *Pipeline) Add(f *Formatter) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Pipeline) Wants(path string) bool {
|
func (p *Pipeline) Wants(path *walk.File) bool {
|
||||||
var match bool
|
var match bool
|
||||||
for _, f := range p.sequence {
|
for _, f := range p.sequence {
|
||||||
match = f.Wants(path)
|
match = f.Wants(path)
|
||||||
|
@ -28,7 +30,7 @@ func (p *Pipeline) Wants(path string) bool {
|
||||||
return match
|
return match
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Pipeline) Apply(ctx context.Context, paths []string) error {
|
func (p *Pipeline) Apply(ctx context.Context, paths []*walk.File) error {
|
||||||
for _, f := range p.sequence {
|
for _, f := range p.sequence {
|
||||||
if err := f.Apply(ctx, paths, len(p.sequence) > 1); err != nil {
|
if err := f.Apply(ctx, paths, len(p.sequence) > 1); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
1
main.go
1
main.go
|
@ -36,5 +36,6 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := kong.Parse(&cli.Cli)
|
ctx := kong.Parse(&cli.Cli)
|
||||||
|
cli.ConfigureLogging()
|
||||||
ctx.FatalIfErrorf(ctx.Run())
|
ctx.FatalIfErrorf(ctx.Run())
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
inputs.flake-root.flakeModule
|
inputs.flake-root.flakeModule
|
||||||
./checks.nix
|
./checks.nix
|
||||||
./devshell.nix
|
./devshell.nix
|
||||||
|
./docs.nix
|
||||||
./nixpkgs.nix
|
./nixpkgs.nix
|
||||||
./packages.nix
|
./packages.nix
|
||||||
./treefmt.nix
|
./treefmt.nix
|
||||||
|
|
82
nix/docs.nix
Normal file
82
nix/docs.nix
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
_: {
|
||||||
|
perSystem = {
|
||||||
|
pkgs,
|
||||||
|
self',
|
||||||
|
...
|
||||||
|
}: {
|
||||||
|
packages.docs = pkgs.buildNpmPackage {
|
||||||
|
pname = "treefmt-docs";
|
||||||
|
inherit (self'.packages.default) version;
|
||||||
|
|
||||||
|
src = ../docs;
|
||||||
|
npmDepsHash = "sha256-J9qTWueOcSBq7qRec6YdTuWI2VlVQ0q6AynDLovf6s0=";
|
||||||
|
|
||||||
|
# we have to use a custom build phase because vitepress is doing something funky with the ttty
|
||||||
|
buildPhase = ''
|
||||||
|
cat | npm run build 2>&1 | cat
|
||||||
|
'';
|
||||||
|
|
||||||
|
installPhase = ''
|
||||||
|
runHook preInstall
|
||||||
|
cp -rv .vitepress/dist/ $out
|
||||||
|
runHook postInstall
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
devshells.default = {
|
||||||
|
packages = [
|
||||||
|
pkgs.nodejs
|
||||||
|
];
|
||||||
|
|
||||||
|
commands = let
|
||||||
|
category = "docs";
|
||||||
|
in [
|
||||||
|
{
|
||||||
|
inherit category;
|
||||||
|
name = "docs:dev";
|
||||||
|
help = "serve docs for local development";
|
||||||
|
command = "cd $PRJ_ROOT/docs && npm run dev";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
inherit category;
|
||||||
|
name = "docs:build";
|
||||||
|
help = "create a production build of docs";
|
||||||
|
command = "cd $PRJ_ROOT/docs && npm run build";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
inherit category;
|
||||||
|
name = "docs:preview";
|
||||||
|
help = "preview a production build of docs";
|
||||||
|
command = "cd $PRJ_ROOT/docs && npm run preview";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
inherit category;
|
||||||
|
package = pkgs.vhs;
|
||||||
|
help = "generate terminal gifs";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
category = "docs";
|
||||||
|
help = "regenerate gifs for docs";
|
||||||
|
package = let
|
||||||
|
treefmt = pkgs.writeShellApplication {
|
||||||
|
name = "treefmt";
|
||||||
|
runtimeInputs = [self'.packages.treefmt] ++ (import ./formatters.nix pkgs);
|
||||||
|
text = ''
|
||||||
|
treefmt -C "$PRJ_ROOT/test/examples" --allow-missing-formatter "$@"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
in
|
||||||
|
pkgs.writeShellApplication {
|
||||||
|
name = "gifs";
|
||||||
|
runtimeInputs = [treefmt];
|
||||||
|
text = ''
|
||||||
|
for tape in "$PRJ_ROOT"/docs/vhs/*; do
|
||||||
|
vhs "$tape" -o "$PRJ_ROOT/docs/public/$(basename "$tape" .tape).gif"
|
||||||
|
done
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
|
@ -25,6 +25,7 @@
|
||||||
root = ../.;
|
root = ../.;
|
||||||
exclude = [
|
exclude = [
|
||||||
"nix"
|
"nix"
|
||||||
|
"docs"
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
17
shell.nix
Normal file
17
shell.nix
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
# This file provides backward compatibility to nix < 2.4 clients
|
||||||
|
{system ? builtins.currentSystem}: let
|
||||||
|
lock = builtins.fromJSON (builtins.readFile ./flake.lock);
|
||||||
|
|
||||||
|
inherit (lock.nodes.flake-compat.locked) owner repo rev narHash;
|
||||||
|
|
||||||
|
flake-compat = fetchTarball {
|
||||||
|
url = "https://github.com/${owner}/${repo}/archive/${rev}.tar.gz";
|
||||||
|
sha256 = narHash;
|
||||||
|
};
|
||||||
|
|
||||||
|
flake = import flake-compat {
|
||||||
|
inherit system;
|
||||||
|
src = ./.;
|
||||||
|
};
|
||||||
|
in
|
||||||
|
flake.shellNix
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
[formatter.deadnix]
|
[formatter.deadnix]
|
||||||
command = "deadnix"
|
command = "deadnix"
|
||||||
|
options = ["--edit"]
|
||||||
includes = ["*.nix"]
|
includes = ["*.nix"]
|
||||||
pipeline = "nix"
|
pipeline = "nix"
|
||||||
priority = 1
|
priority = 1
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
[formatter.echo]
|
[formatter.echo]
|
||||||
command = "echo"
|
command = "touch"
|
||||||
includes = [ "*.*" ]
|
includes = [ "*.*" ]
|
|
@ -2,6 +2,7 @@ package walk
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
@ -15,18 +16,42 @@ func (f filesystemWalker) Root() string {
|
||||||
return f.root
|
return f.root
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f filesystemWalker) Walk(_ context.Context, fn filepath.WalkFunc) error {
|
func (f filesystemWalker) Walk(_ context.Context, fn WalkFunc) error {
|
||||||
|
relPathOffset := len(f.root) + 1
|
||||||
|
|
||||||
|
relPathFn := func(path string) (relPath string) {
|
||||||
|
if len(path) >= relPathOffset {
|
||||||
|
relPath = path[relPathOffset:]
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
walkFn := func(path string, info fs.FileInfo, err error) error {
|
||||||
|
file := File{
|
||||||
|
Path: path,
|
||||||
|
RelPath: relPathFn(path),
|
||||||
|
Info: info,
|
||||||
|
}
|
||||||
|
return fn(&file, err)
|
||||||
|
}
|
||||||
|
|
||||||
if len(f.paths) == 0 {
|
if len(f.paths) == 0 {
|
||||||
return filepath.Walk(f.root, fn)
|
return filepath.Walk(f.root, walkFn)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, path := range f.paths {
|
for _, path := range f.paths {
|
||||||
info, err := os.Stat(path)
|
info, err := os.Stat(path)
|
||||||
if err = filepath.Walk(path, fn); err != nil {
|
if err = filepath.Walk(path, walkFn); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = fn(path, info, err); err != nil {
|
file := File{
|
||||||
|
Path: path,
|
||||||
|
RelPath: relPathFn(path),
|
||||||
|
Info: info,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = fn(&file, err); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
33
walk/git.go
33
walk/git.go
|
@ -24,10 +24,20 @@ func (g *gitWalker) Root() string {
|
||||||
return g.root
|
return g.root
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *gitWalker) Walk(ctx context.Context, fn filepath.WalkFunc) error {
|
func (g *gitWalker) Walk(ctx context.Context, fn WalkFunc) error {
|
||||||
|
// for quick relative paths
|
||||||
|
relPathOffset := len(g.root) + 1
|
||||||
|
|
||||||
|
relPathFn := func(path string) (relPath string) {
|
||||||
|
if len(path) >= relPathOffset {
|
||||||
|
relPath = path[relPathOffset:]
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
idx, err := g.repo.Storer.Index()
|
idx, err := g.repo.Storer.Index()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%w: failed to open index", err)
|
return fmt.Errorf("failed to open git index: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(g.paths) > 0 {
|
if len(g.paths) > 0 {
|
||||||
|
@ -49,7 +59,13 @@ func (g *gitWalker) Walk(ctx context.Context, fn filepath.WalkFunc) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return fn(path, info, err)
|
file := File{
|
||||||
|
Path: path,
|
||||||
|
RelPath: relPathFn(path),
|
||||||
|
Info: info,
|
||||||
|
}
|
||||||
|
|
||||||
|
return fn(&file, err)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -66,7 +82,14 @@ func (g *gitWalker) Walk(ctx context.Context, fn filepath.WalkFunc) error {
|
||||||
|
|
||||||
// stat the file
|
// stat the file
|
||||||
info, err := os.Lstat(path)
|
info, err := os.Lstat(path)
|
||||||
if err = fn(path, info, err); err != nil {
|
|
||||||
|
file := File{
|
||||||
|
Path: path,
|
||||||
|
RelPath: relPathFn(path),
|
||||||
|
Info: info,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = fn(&file, err); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -79,7 +102,7 @@ func (g *gitWalker) Walk(ctx context.Context, fn filepath.WalkFunc) error {
|
||||||
func NewGit(root string, paths []string) (Walker, error) {
|
func NewGit(root string, paths []string) (Walker, error) {
|
||||||
repo, err := git.PlainOpen(root)
|
repo, err := git.PlainOpen(root)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%w: failed to open git repo", err)
|
return nil, fmt.Errorf("failed to open git repo: %w", err)
|
||||||
}
|
}
|
||||||
return &gitWalker{root, paths, repo}, nil
|
return &gitWalker{root, paths, repo}, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ package walk
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
"io/fs"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Type string
|
type Type string
|
||||||
|
@ -14,9 +14,21 @@ const (
|
||||||
Filesystem Type = "filesystem"
|
Filesystem Type = "filesystem"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type File struct {
|
||||||
|
Path string
|
||||||
|
RelPath string
|
||||||
|
Info fs.FileInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f File) String() string {
|
||||||
|
return f.Path
|
||||||
|
}
|
||||||
|
|
||||||
|
type WalkFunc func(file *File, err error) error
|
||||||
|
|
||||||
type Walker interface {
|
type Walker interface {
|
||||||
Root() string
|
Root() string
|
||||||
Walk(ctx context.Context, fn filepath.WalkFunc) error
|
Walk(ctx context.Context, fn WalkFunc) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(walkerType Type, root string, paths []string) (Walker, error) {
|
func New(walkerType Type, root string, paths []string) (Walker, error) {
|
||||||
|
|
Reference in New Issue
Block a user