Skip to content

Commit 5522507

Browse files
committed
feat: add notify based formatter
Close: numtide#509
1 parent 45881a4 commit 5522507

File tree

4 files changed

+176
-4
lines changed

4 files changed

+176
-4
lines changed

config/config.go

+11
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ type Config struct {
2929
Walk string `mapstructure:"walk" toml:"walk,omitempty"`
3030
WorkingDirectory string `mapstructure:"working-dir" toml:"-"`
3131
Stdin bool `mapstructure:"stdin" toml:"-"` // not allowed in config
32+
Watch bool `mapstructure:"watch" toml:"-"` // not allowed in config
3233

3334
FormatterConfigs map[string]*Formatter `mapstructure:"formatter" toml:"formatter,omitempty"`
3435

@@ -98,6 +99,10 @@ func SetFlags(fs *pflag.FlagSet) {
9899
"stdin", false,
99100
"Format the context passed in via stdin.",
100101
)
102+
fs.Bool(
103+
"watch", false,
104+
"Watch the filesystem for changes and apply formatters when changes are detected. (env $TREEFMT_WATCH)",
105+
)
101106
fs.String(
102107
"tree-root", "",
103108
"The root directory from which treefmt will start walking the filesystem (defaults to the directory "+
@@ -157,6 +162,7 @@ func FromViper(v *viper.Viper) (*Config, error) {
157162
"clear-cache": false,
158163
"no-cache": false,
159164
"stdin": false,
165+
"watch": false,
160166
"working-dir": ".",
161167
}
162168

@@ -185,6 +191,11 @@ func FromViper(v *viper.Viper) (*Config, error) {
185191
cfg.Walk = walk.Stdin.String()
186192
}
187193

194+
// if the watch flag was passed, we force the watch walk type
195+
if cfg.Watch {
196+
cfg.Walk = walk.Watch.String()
197+
}
198+
188199
// determine the tree root
189200
if cfg.TreeRoot == "" {
190201
// if none was specified, we first try with tree-root-file

walk/type_enum.go

+8-4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

walk/walk.go

+3
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const (
2222
Stdin
2323
Filesystem
2424
Git
25+
Watch
2526

2627
BatchSize = 1024
2728
)
@@ -215,6 +216,8 @@ func NewReader(
215216
reader = NewFilesystemReader(root, path, statz, BatchSize)
216217
case Git:
217218
reader, err = NewGitReader(root, path, statz)
219+
case Watch:
220+
reader, err = NewWatchReader(root, path, statz)
218221

219222
default:
220223
return nil, fmt.Errorf("unknown walk type: %v", walkType)

walk/watch.go

+154
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
package walk
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"io"
8+
"log"
9+
"os"
10+
"path/filepath"
11+
"strings"
12+
13+
"github.com/fsnotify/fsnotify"
14+
"github.com/numtide/treefmt/v2/stats"
15+
"golang.org/x/sync/errgroup"
16+
)
17+
18+
type WatchReader struct {
19+
root string
20+
path string
21+
22+
log *log.Logger
23+
stats *stats.Stats
24+
25+
eg *errgroup.Group
26+
watcher *fsnotify.Watcher
27+
}
28+
29+
func (f *WatchReader) Read(ctx context.Context, files []*File) (n int, err error) {
30+
// ensure we record how many files we traversed
31+
defer func() {
32+
f.stats.Add(stats.Traversed, n)
33+
}()
34+
35+
LOOP:
36+
// keep filling files up to it's length
37+
for n < len(files) {
38+
select {
39+
// exit early if the context was cancelled
40+
case <-ctx.Done():
41+
err = ctx.Err()
42+
if err == nil {
43+
return n, fmt.Errorf("context cancelled: %w", ctx.Err())
44+
}
45+
46+
return n, nil
47+
48+
// read the next event from the channel
49+
case event, ok := <-f.watcher.Events:
50+
if !ok {
51+
// channel was closed, exit the loop
52+
err = io.EOF
53+
54+
break LOOP
55+
}
56+
57+
// skip if the event is a chmod or rename event since it doesn't
58+
// change the file contents
59+
if !event.Has(fsnotify.Create) && !event.Has(fsnotify.Write) {
60+
continue
61+
}
62+
63+
file, err := os.Open(event.Name)
64+
if errors.Is(err, os.ErrNotExist) {
65+
// file was deleted, skip it
66+
continue
67+
} else if err != nil {
68+
return n, fmt.Errorf("failed to stat file %s: %w", event.Name, err)
69+
}
70+
defer file.Close()
71+
info, err := file.Stat()
72+
if err != nil {
73+
return n, fmt.Errorf("failed to stat file %s: %w", event.Name, err)
74+
}
75+
76+
fmt.Print("event:", event.Op, event.Name)
77+
// determine a path relative to the root
78+
relPath, err := filepath.Rel(f.root, event.Name)
79+
if err != nil {
80+
return n, fmt.Errorf("failed to determine a relative path for %s: %w", event.Name, err)
81+
}
82+
83+
// add to the file array and increment n
84+
files[n] = &File{
85+
Path: event.Name,
86+
RelPath: relPath,
87+
Info: info,
88+
}
89+
n++
90+
91+
case err, ok := <-f.watcher.Errors:
92+
if !ok {
93+
return n, fmt.Errorf("failed to read from watcher: %w", err)
94+
}
95+
f.log.Printf("error: %s", err)
96+
}
97+
}
98+
99+
return n, err
100+
}
101+
102+
// Close waits for all watcher processing to complete.
103+
func (f *WatchReader) Close() error {
104+
err := f.watcher.Close()
105+
if err != nil {
106+
return fmt.Errorf("failed to close watcher: %w", err)
107+
}
108+
109+
err = f.eg.Wait()
110+
if err != nil {
111+
return fmt.Errorf("failed to wait for processing to complete: %w", err)
112+
}
113+
114+
return nil
115+
}
116+
117+
func NewWatchReader(
118+
root string,
119+
path string,
120+
statz *stats.Stats,
121+
) (*WatchReader, error) {
122+
// create an error group for managing the processing loop
123+
eg := errgroup.Group{}
124+
125+
watcher, err := fsnotify.NewWatcher()
126+
if err != nil {
127+
log.Fatalf("failed to create watcher: %v", err)
128+
}
129+
130+
r := WatchReader{
131+
root: root,
132+
path: path,
133+
log: log.Default(),
134+
stats: statz,
135+
eg: &eg,
136+
watcher: watcher,
137+
}
138+
139+
// path is relative to the root, so we create a fully qualified version
140+
// we also clean the path up in case there are any ../../ components etc.
141+
fqPath := filepath.Clean(filepath.Join(root, path))
142+
143+
// ensure the path is within the root
144+
if !strings.HasPrefix(fqPath, root) {
145+
return nil, fmt.Errorf("path '%s' is outside of the root '%s'", fqPath, root)
146+
}
147+
148+
// start watching the path
149+
if err := watcher.Add(fqPath); err != nil {
150+
return nil, fmt.Errorf("failed to watch path %s: %w", fqPath, err)
151+
}
152+
153+
return &r, nil
154+
}

0 commit comments

Comments
 (0)