@@ -4,20 +4,23 @@ import (
4
4
"context"
5
5
"crypto/sha1"
6
6
"encoding/hex"
7
- "errors"
8
7
"fmt"
9
8
"io/fs"
10
9
"os"
11
10
"path/filepath"
12
11
"time"
13
12
13
+ "git.numtide.com/numtide/treefmt/internal/format"
14
+ "github.com/charmbracelet/log"
15
+
14
16
"github.com/adrg/xdg"
15
17
"github.com/vmihailenco/msgpack/v5"
16
18
bolt "go.etcd.io/bbolt"
17
19
)
18
20
19
21
const (
20
- modifiedBucket = "modified"
22
+ pathsBucket = "paths"
23
+ formattersBucket = "formatters"
21
24
)
22
25
23
26
// Entry represents a cache entry, indicating the last size and modified time for a file path.
@@ -33,7 +36,9 @@ var db *bolt.DB
33
36
//
34
37
// The database will be located in `XDG_CACHE_DIR/treefmt/eval-cache/<id>.db`, where <id> is determined by hashing
35
38
// the treeRoot path. This associates a given treeRoot with a given instance of the cache.
36
- func Open (treeRoot string , clean bool ) (err error ) {
39
+ func Open (treeRoot string , clean bool , formatters map [string ]* format.Formatter ) (err error ) {
40
+ l := log .WithPrefix ("cache" )
41
+
37
42
// determine a unique and consistent db name for the tree root
38
43
h := sha1 .New ()
39
44
h .Write ([]byte (treeRoot ))
@@ -45,27 +50,84 @@ func Open(treeRoot string, clean bool) (err error) {
45
50
return fmt .Errorf ("%w: could not resolve local path for the cache" , err )
46
51
}
47
52
48
- // force a clean of the cache if specified
49
- if clean {
50
- err := os .Remove (path )
51
- if errors .Is (err , os .ErrNotExist ) {
52
- err = nil
53
- } else if err != nil {
54
- return fmt .Errorf ("%w: failed to clear cache" , err )
55
- }
56
- }
57
-
58
53
db , err = bolt .Open (path , 0o600 , nil )
59
54
if err != nil {
60
55
return fmt .Errorf ("%w: failed to open cache" , err )
61
56
}
62
57
63
58
err = db .Update (func (tx * bolt.Tx ) error {
64
- _ , err := tx .CreateBucket ([]byte (modifiedBucket ))
65
- if errors .Is (err , bolt .ErrBucketExists ) {
59
+ // create bucket for tracking paths
60
+ pathsBucket , err := tx .CreateBucketIfNotExists ([]byte (pathsBucket ))
61
+ if err != nil {
62
+ return fmt .Errorf ("%w: failed to create paths bucket" , err )
63
+ }
64
+
65
+ // create bucket for tracking formatters
66
+ formattersBucket , err := tx .CreateBucketIfNotExists ([]byte (formattersBucket ))
67
+ if err != nil {
68
+ return fmt .Errorf ("%w: failed to create formatters bucket" , err )
69
+ }
70
+
71
+ // check for any newly configured or modified formatters
72
+ for name , formatter := range formatters {
73
+
74
+ stat , err := os .Lstat (formatter .Executable ())
75
+ if err != nil {
76
+ return fmt .Errorf ("%w: failed to state formatter executable" , err )
77
+ }
78
+
79
+ entry , err := getEntry (formattersBucket , name )
80
+ if err != nil {
81
+ return fmt .Errorf ("%w: failed to retrieve entry for formatter" , err )
82
+ }
83
+
84
+ clean = clean || entry == nil || ! (entry .Size == stat .Size () && entry .Modified == stat .ModTime ())
85
+ l .Debug (
86
+ "checking if formatter has changed" ,
87
+ "name" , name ,
88
+ "clean" , clean ,
89
+ "entry" , entry ,
90
+ "stat" , stat ,
91
+ )
92
+
93
+ // record formatters info
94
+ entry = & Entry {
95
+ Size : stat .Size (),
96
+ Modified : stat .ModTime (),
97
+ }
98
+
99
+ if err = putEntry (formattersBucket , name , entry ); err != nil {
100
+ return fmt .Errorf ("%w: failed to write formatter entry" , err )
101
+ }
102
+ }
103
+
104
+ // check for any removed formatters
105
+ if err = formattersBucket .ForEach (func (key []byte , _ []byte ) error {
106
+ _ , ok := formatters [string (key )]
107
+ if ! ok {
108
+ // remove the formatter entry from the cache
109
+ if err = formattersBucket .Delete (key ); err != nil {
110
+ return fmt .Errorf ("%w: failed to remove formatter entry" , err )
111
+ }
112
+ // indicate a clean is required
113
+ clean = true
114
+ }
66
115
return nil
116
+ }); err != nil {
117
+ return fmt .Errorf ("%w: failed to check for removed formatters" , err )
118
+ }
119
+
120
+ if clean {
121
+ // remove all path entries
122
+ c := pathsBucket .Cursor ()
123
+ for k , v := c .First (); ! (k == nil && v == nil ); k , v = c .Next () {
124
+ if err = c .Delete (); err != nil {
125
+ return fmt .Errorf ("%w: failed to remove path entry" , err )
126
+ }
127
+ }
67
128
}
68
- return err
129
+
130
+ return nil
69
131
})
70
132
71
133
return
@@ -93,11 +155,24 @@ func getEntry(bucket *bolt.Bucket, path string) (*Entry, error) {
93
155
}
94
156
}
95
157
158
+ // putEntry is a helper for writing cache entries into bolt.
159
+ func putEntry (bucket * bolt.Bucket , path string , entry * Entry ) error {
160
+ bytes , err := msgpack .Marshal (entry )
161
+ if err != nil {
162
+ return fmt .Errorf ("%w: failed to marshal cache entry" , err )
163
+ }
164
+
165
+ if err = bucket .Put ([]byte (path ), bytes ); err != nil {
166
+ return fmt .Errorf ("%w: failed to put cache entry" , err )
167
+ }
168
+ return nil
169
+ }
170
+
96
171
// ChangeSet is used to walk a filesystem, starting at root, and outputting any new or changed paths using pathsCh.
97
172
// It determines if a path is new or has changed by comparing against cache entries.
98
173
func ChangeSet (ctx context.Context , root string , pathsCh chan <- string ) error {
99
174
return db .Update (func (tx * bolt.Tx ) error {
100
- bucket := tx .Bucket ([]byte (modifiedBucket ))
175
+ bucket := tx .Bucket ([]byte (pathsBucket ))
101
176
102
177
return filepath .Walk (root , func (path string , info fs.FileInfo , err error ) error {
103
178
if err != nil {
@@ -142,13 +217,9 @@ func Update(paths []string) (int, error) {
142
217
var changes int
143
218
144
219
return changes , db .Update (func (tx * bolt.Tx ) error {
145
- bucket := tx .Bucket ([]byte (modifiedBucket ))
220
+ bucket := tx .Bucket ([]byte (pathsBucket ))
146
221
147
222
for _ , path := range paths {
148
- if path == "" {
149
- continue
150
- }
151
-
152
223
cached , err := getEntry (bucket , path )
153
224
if err != nil {
154
225
return err
@@ -166,18 +237,13 @@ func Update(paths []string) (int, error) {
166
237
continue
167
238
}
168
239
169
- cacheInfo := Entry {
240
+ entry := Entry {
170
241
Size : pathInfo .Size (),
171
242
Modified : pathInfo .ModTime (),
172
243
}
173
244
174
- bytes , err := msgpack .Marshal (cacheInfo )
175
- if err != nil {
176
- return fmt .Errorf ("%w: failed to marshal mod time" , err )
177
- }
178
-
179
- if err = bucket .Put ([]byte (path ), bytes ); err != nil {
180
- return fmt .Errorf ("%w: failed to put mode time" , err )
245
+ if err = putEntry (bucket , path , & entry ); err != nil {
246
+ return err
181
247
}
182
248
}
183
249
0 commit comments