Skip to content

Commit d531a2f

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

File tree

2 files changed

+152
-0
lines changed

2 files changed

+152
-0
lines changed

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

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

0 commit comments

Comments
 (0)