Skip to content

Commit 85ce0a2

Browse files
committed
fix: use second precision when comparing file mod times
We were using whatever precision the underlying file system was giving us, and in some cases that can be very detailed. Some formatters mess with the mod time, but not to the same precision (e.g. dos2unix). POSIX also specifies that mod time should be EPOCH (second) precision. This change brings us back in line with how 1.x worked, and should resolve issues with false fail on change errors.
1 parent 23e563b commit 85ce0a2

File tree

8 files changed

+110
-52
lines changed

8 files changed

+110
-52
lines changed

cli/format.go

+14-7
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"runtime"
1212
"runtime/pprof"
1313
"syscall"
14+
"time"
1415

1516
"git.numtide.com/numtide/treefmt/format"
1617
"git.numtide.com/numtide/treefmt/stats"
@@ -389,20 +390,26 @@ func (f *Format) detectFormatted(ctx context.Context) func() error {
389390
return nil
390391
}
391392

392-
// look up current file info
393-
currentInfo, err := os.Stat(file.Path)
393+
// check if the file has changed
394+
changed, newInfo, err := file.HasChanged()
394395
if err != nil {
395-
return fmt.Errorf("failed to stat processed file: %w", err)
396+
return err
396397
}
397398

398-
// check if the file has changed
399-
if !(file.Info.ModTime() == currentInfo.ModTime() && file.Info.Size() == currentInfo.Size()) {
399+
if changed {
400400
// record the change
401401
stats.Add(stats.Formatted, 1)
402402
// log the change for diagnostics
403-
log.Debugf("file has been changed: %s", file.Path)
403+
log.Debug(
404+
"file has changed",
405+
"path", file.Path,
406+
"prev_size", file.Info.Size(),
407+
"current_size", newInfo.Size(),
408+
"prev_mod_time", file.Info.ModTime().Truncate(time.Second),
409+
"current_mod_time", newInfo.ModTime().Truncate(time.Second),
410+
)
404411
// update the file info
405-
file.Info = currentInfo
412+
file.Info = newInfo
406413
}
407414

408415
// mark as processed

cli/format_test.go

+34-30
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"path/filepath"
1010
"regexp"
1111
"testing"
12+
"time"
1213

1314
"git.numtide.com/numtide/treefmt/config"
1415
"git.numtide.com/numtide/treefmt/format"
@@ -159,22 +160,22 @@ func TestSpecifyingFormatters(t *testing.T) {
159160
setup()
160161
_, err := cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir)
161162
as.NoError(err)
162-
assertStats(t, as, 31, 31, 3, 3)
163+
assertStats(t, as, 32, 32, 3, 3)
163164

164165
setup()
165166
_, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir, "--formatters", "elm,nix")
166167
as.NoError(err)
167-
assertStats(t, as, 31, 31, 2, 2)
168+
assertStats(t, as, 32, 32, 2, 2)
168169

169170
setup()
170171
_, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir, "-f", "ruby,nix")
171172
as.NoError(err)
172-
assertStats(t, as, 31, 31, 2, 2)
173+
assertStats(t, as, 32, 32, 2, 2)
173174

174175
setup()
175176
_, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir, "--formatters", "nix")
176177
as.NoError(err)
177-
assertStats(t, as, 31, 31, 1, 1)
178+
assertStats(t, as, 32, 32, 1, 1)
178179

179180
// test bad names
180181
setup()
@@ -204,23 +205,23 @@ func TestIncludesAndExcludes(t *testing.T) {
204205
test.WriteConfig(t, configPath, cfg)
205206
_, err := cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir)
206207
as.NoError(err)
207-
assertStats(t, as, 31, 31, 31, 0)
208+
assertStats(t, as, 32, 32, 32, 0)
208209

209210
// globally exclude nix files
210211
cfg.Global.Excludes = []string{"*.nix"}
211212

212213
test.WriteConfig(t, configPath, cfg)
213214
_, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir)
214215
as.NoError(err)
215-
assertStats(t, as, 31, 31, 30, 0)
216+
assertStats(t, as, 32, 32, 31, 0)
216217

217218
// add haskell files to the global exclude
218219
cfg.Global.Excludes = []string{"*.nix", "*.hs"}
219220

220221
test.WriteConfig(t, configPath, cfg)
221222
_, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir)
222223
as.NoError(err)
223-
assertStats(t, as, 31, 31, 24, 0)
224+
assertStats(t, as, 32, 32, 25, 0)
224225

225226
echo := cfg.Formatters["echo"]
226227

@@ -230,31 +231,31 @@ func TestIncludesAndExcludes(t *testing.T) {
230231
test.WriteConfig(t, configPath, cfg)
231232
_, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir)
232233
as.NoError(err)
233-
assertStats(t, as, 31, 31, 22, 0)
234+
assertStats(t, as, 32, 32, 23, 0)
234235

235236
// remove go files from the echo formatter
236237
echo.Excludes = []string{"*.py", "*.go"}
237238

238239
test.WriteConfig(t, configPath, cfg)
239240
_, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir)
240241
as.NoError(err)
241-
assertStats(t, as, 31, 31, 21, 0)
242+
assertStats(t, as, 32, 32, 22, 0)
242243

243244
// adjust the includes for echo to only include elm files
244245
echo.Includes = []string{"*.elm"}
245246

246247
test.WriteConfig(t, configPath, cfg)
247248
_, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir)
248249
as.NoError(err)
249-
assertStats(t, as, 31, 31, 1, 0)
250+
assertStats(t, as, 32, 32, 1, 0)
250251

251252
// add js files to echo formatter
252253
echo.Includes = []string{"*.elm", "*.js"}
253254

254255
test.WriteConfig(t, configPath, cfg)
255256
_, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir)
256257
as.NoError(err)
257-
assertStats(t, as, 31, 31, 2, 0)
258+
assertStats(t, as, 32, 32, 2, 0)
258259
}
259260

260261
func TestCache(t *testing.T) {
@@ -281,7 +282,7 @@ func TestCache(t *testing.T) {
281282
test.WriteConfig(t, configPath, cfg)
282283
_, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir)
283284
as.NoError(err)
284-
assertStats(t, as, 31, 31, 31, 0)
285+
assertStats(t, as, 32, 32, 32, 0)
285286

286287
out, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir)
287288
as.NoError(err)
@@ -290,7 +291,7 @@ func TestCache(t *testing.T) {
290291
// clear cache
291292
_, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir, "-c")
292293
as.NoError(err)
293-
assertStats(t, as, 31, 31, 31, 0)
294+
assertStats(t, as, 32, 32, 32, 0)
294295

295296
out, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir)
296297
as.NoError(err)
@@ -299,7 +300,7 @@ func TestCache(t *testing.T) {
299300
// clear cache
300301
_, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir, "-c")
301302
as.NoError(err)
302-
assertStats(t, as, 31, 31, 31, 0)
303+
assertStats(t, as, 32, 32, 32, 0)
303304

304305
out, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir)
305306
as.NoError(err)
@@ -308,7 +309,7 @@ func TestCache(t *testing.T) {
308309
// no cache
309310
_, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir, "--no-cache")
310311
as.NoError(err)
311-
assertStats(t, as, 31, 31, 31, 0)
312+
assertStats(t, as, 32, 32, 32, 0)
312313
}
313314

314315
func TestChangeWorkingDirectory(t *testing.T) {
@@ -342,7 +343,7 @@ func TestChangeWorkingDirectory(t *testing.T) {
342343
// this should fail if the working directory hasn't been changed first
343344
_, err = cmd(t, "-C", tempDir)
344345
as.NoError(err)
345-
assertStats(t, as, 31, 31, 31, 0)
346+
assertStats(t, as, 32, 32, 32, 0)
346347
}
347348

348349
func TestFailOnChange(t *testing.T) {
@@ -365,6 +366,9 @@ func TestFailOnChange(t *testing.T) {
365366
_, err := cmd(t, "--fail-on-change", "--config-file", configPath, "--tree-root", tempDir)
366367
as.ErrorIs(err, ErrFailOnChange)
367368

369+
// we have second precision mod time tracking
370+
time.Sleep(time.Second)
371+
368372
// test with no cache
369373
test.WriteConfig(t, configPath, cfg)
370374
_, err = cmd(t, "--fail-on-change", "--config-file", configPath, "--tree-root", tempDir, "--no-cache")
@@ -411,31 +415,31 @@ func TestBustCacheOnFormatterChange(t *testing.T) {
411415
args := []string{"--config-file", configPath, "--tree-root", tempDir}
412416
_, err := cmd(t, args...)
413417
as.NoError(err)
414-
assertStats(t, as, 31, 31, 3, 0)
418+
assertStats(t, as, 32, 32, 3, 0)
415419

416420
// tweak mod time of elm formatter
417421
as.NoError(test.RecreateSymlink(t, binPath+"/"+"elm-format"))
418422

419423
_, err = cmd(t, args...)
420424
as.NoError(err)
421-
assertStats(t, as, 31, 31, 3, 0)
425+
assertStats(t, as, 32, 32, 3, 0)
422426

423427
// check cache is working
424428
_, err = cmd(t, args...)
425429
as.NoError(err)
426-
assertStats(t, as, 31, 0, 0, 0)
430+
assertStats(t, as, 32, 0, 0, 0)
427431

428432
// tweak mod time of python formatter
429433
as.NoError(test.RecreateSymlink(t, binPath+"/"+"black"))
430434

431435
_, err = cmd(t, args...)
432436
as.NoError(err)
433-
assertStats(t, as, 31, 31, 3, 0)
437+
assertStats(t, as, 32, 32, 3, 0)
434438

435439
// check cache is working
436440
_, err = cmd(t, args...)
437441
as.NoError(err)
438-
assertStats(t, as, 31, 0, 0, 0)
442+
assertStats(t, as, 32, 0, 0, 0)
439443

440444
// add go formatter
441445
cfg.Formatters["go"] = &config.Formatter{
@@ -447,38 +451,38 @@ func TestBustCacheOnFormatterChange(t *testing.T) {
447451

448452
_, err = cmd(t, args...)
449453
as.NoError(err)
450-
assertStats(t, as, 31, 31, 4, 0)
454+
assertStats(t, as, 32, 32, 4, 0)
451455

452456
// check cache is working
453457
_, err = cmd(t, args...)
454458
as.NoError(err)
455-
assertStats(t, as, 31, 0, 0, 0)
459+
assertStats(t, as, 32, 0, 0, 0)
456460

457461
// remove python formatter
458462
delete(cfg.Formatters, "python")
459463
test.WriteConfig(t, configPath, cfg)
460464

461465
_, err = cmd(t, args...)
462466
as.NoError(err)
463-
assertStats(t, as, 31, 31, 2, 0)
467+
assertStats(t, as, 32, 32, 2, 0)
464468

465469
// check cache is working
466470
_, err = cmd(t, args...)
467471
as.NoError(err)
468-
assertStats(t, as, 31, 0, 0, 0)
472+
assertStats(t, as, 32, 0, 0, 0)
469473

470474
// remove elm formatter
471475
delete(cfg.Formatters, "elm")
472476
test.WriteConfig(t, configPath, cfg)
473477

474478
_, err = cmd(t, args...)
475479
as.NoError(err)
476-
assertStats(t, as, 31, 31, 1, 0)
480+
assertStats(t, as, 32, 32, 1, 0)
477481

478482
// check cache is working
479483
_, err = cmd(t, args...)
480484
as.NoError(err)
481-
assertStats(t, as, 31, 0, 0, 0)
485+
assertStats(t, as, 32, 0, 0, 0)
482486
}
483487

484488
func TestGitWorktree(t *testing.T) {
@@ -524,7 +528,7 @@ func TestGitWorktree(t *testing.T) {
524528
// add everything to the worktree
525529
as.NoError(wt.AddGlob("."))
526530
as.NoError(err)
527-
run(31, 31, 31, 0)
531+
run(32, 32, 32, 0)
528532

529533
// remove python directory
530534
as.NoError(wt.RemoveGlob("python/*"))
@@ -533,7 +537,7 @@ func TestGitWorktree(t *testing.T) {
533537
// walk with filesystem instead of git
534538
_, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir, "--walk", "filesystem")
535539
as.NoError(err)
536-
assertStats(t, as, 59, 59, 59, 0)
540+
assertStats(t, as, 61, 61, 61, 0)
537541
}
538542

539543
func TestPathsArg(t *testing.T) {
@@ -568,7 +572,7 @@ func TestPathsArg(t *testing.T) {
568572
// without any path args
569573
_, err = cmd(t, "-C", tempDir)
570574
as.NoError(err)
571-
assertStats(t, as, 31, 31, 31, 0)
575+
assertStats(t, as, 32, 32, 32, 0)
572576

573577
// specify some explicit paths
574578
_, err = cmd(t, "-C", tempDir, "-c", "elm/elm.json", "haskell/Nested/Foo.hs")

flake.lock

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

nix/packages/treefmt/formatters.nix

+2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ with pkgs; [
1616
statix
1717
deadnix
1818
terraform
19+
dos2unix
20+
yamlfmt
1921
# util for unit testing
2022
(pkgs.writeShellApplication {
2123
name = "test-fmt";

0 commit comments

Comments
 (0)