//go:build !windows package main import ( "context" "os" "path/filepath" "sync" "time" "github.com/fsnotify/fsnotify" ) // watchForTests uses inotify directly for integration testing. // This is only used in tests - production uses socket-based events from Windows watcher. func watchForTests(ctx context.Context, hostDir, dataDir string, manifest *Manifest, stack *GitignoreStack, dryRun bool) error { watcher, err := fsnotify.NewWatcher() if err != nil { return err } defer watcher.Close() var mu sync.Mutex debounceTimers := make(map[string]*time.Timer) debounceOps := make(map[string]fsnotify.Op) if err := addWatchersForTests(watcher, hostDir, stack); err != nil { return err } for { select { case <-ctx.Done(): return nil case event, ok := <-watcher.Events: if !ok { return nil } relPath, err := filepath.Rel(hostDir, event.Name) if err != nil { continue } if isGitDir(relPath) { continue } if stack.IsIgnored(relPath) { continue } if event.Op&fsnotify.Create != 0 { srcPath := filepath.Join(hostDir, relPath) info, err := os.Lstat(srcPath) if err == nil && info.IsDir() { if hasGitignore(srcPath) { stack.Push(srcPath) } watcher.Add(srcPath) filepath.Walk(srcPath, func(path string, info os.FileInfo, err error) error { if err != nil || !info.IsDir() || path == srcPath { return nil } watcher.Add(path) return nil }) } } mu.Lock() if existing, exists := debounceOps[relPath]; exists { debounceTimers[relPath].Stop() debounceOps[relPath] = existing | event.Op } else { debounceOps[relPath] = event.Op } path := relPath debounceTimers[relPath] = time.AfterFunc(200*time.Millisecond, func() { mu.Lock() op := debounceOps[path] delete(debounceOps, path) delete(debounceTimers, path) mu.Unlock() destPath := filepath.Join(dataDir, path) srcPath := filepath.Join(hostDir, path) switch { case op&fsnotify.Create != 0: syncCreate(srcPath, destPath, path, manifest, stack, dryRun, watcher) case op&fsnotify.Write != 0: syncWrite(srcPath, destPath, path, manifest, dryRun) case op&fsnotify.Remove != 0: syncRemove(destPath, path, manifest, dryRun) case op&fsnotify.Rename != 0: syncRemove(destPath, path, manifest, dryRun) } }) mu.Unlock() case err, ok := <-watcher.Errors: if !ok { return nil } _ = err } } } func addWatchersForTests(watcher *fsnotify.Watcher, root string, stack *GitignoreStack) error { return filepath.Walk(root, func(path string, info os.FileInfo, err error) error { if err != nil { return err } relPath, err := filepath.Rel(root, path) if err != nil { return err } if isGitDir(relPath) { if info.IsDir() { return filepath.SkipDir } return nil } if info.IsDir() { if hasGitignore(path) { stack.Push(path) } if !stack.IsIgnored(relPath) { return watcher.Add(path) } } return nil }) }