aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBernhard Guillon <Bernhard.Guillon@begu.org>2026-07-07 23:17:20 +0200
committerBernhard Guillon <Bernhard.Guillon@begu.org>2026-07-07 23:17:20 +0200
commita62e42b9be8402301631c9043f640eb7f43eff29 (patch)
treed85b47ba88a15d9aa7a109c1cd6426ba9faee4f5
parent2850cd0c3156cdeb42594664cc087641fe740236 (diff)
downloadsourcewatch-a62e42b9be8402301631c9043f640eb7f43eff29.tar.gz
sourcewatch-a62e42b9be8402301631c9043f640eb7f43eff29.zip
Add FNV-1a content hash to detect same-size changesHEADmain
-rw-r--r--cmd_linux.go12
-rw-r--r--cmd_windows.go6
-rw-r--r--types.go25
-rw-r--r--watcher.go4
4 files changed, 44 insertions, 3 deletions
diff --git a/cmd_linux.go b/cmd_linux.go
index 305b7d1..70ee2fb 100644
--- a/cmd_linux.go
+++ b/cmd_linux.go
@@ -194,13 +194,15 @@ func syncFromManifest(remote *Manifest, stack *GitignoreStack) {
if localMeta, exists := localManifest.Files[relPath]; exists {
if localMeta.Size == remoteMeta.Size &&
- localMeta.Symlink == remoteMeta.Symlink {
+ localMeta.Symlink == remoteMeta.Symlink &&
+ (remoteMeta.Hash == 0 || localMeta.Hash == remoteMeta.Hash) {
continue
}
if debugShown < 5 {
- logDebug("mismatch %s: size=%d/%d symlink=%q/%q",
+ logDebug("mismatch %s: size=%d/%d hash=%d/%d symlink=%q/%q",
relPath,
localMeta.Size, remoteMeta.Size,
+ localMeta.Hash, remoteMeta.Hash,
localMeta.Symlink, remoteMeta.Symlink)
debugShown++
}
@@ -310,11 +312,17 @@ func scanLocalDirectory(root string) *Manifest {
linkTarget, _ = os.Readlink(path)
}
+ var hash uint64
+ if !info.IsDir() && linkTarget == "" {
+ hash = hashFile(path)
+ }
+
manifest.Files[relPath] = FileMeta{
Size: info.Size(),
Mode: info.Mode(),
ModTime: info.ModTime(),
Symlink: linkTarget,
+ Hash: hash,
}
return nil
diff --git a/cmd_windows.go b/cmd_windows.go
index f76d95d..2557803 100644
--- a/cmd_windows.go
+++ b/cmd_windows.go
@@ -317,11 +317,17 @@ func scanDirectory(root string, stack *GitignoreStack) *Manifest {
linkTarget, _ = os.Readlink(path)
}
+ var hash uint64
+ if !info.IsDir() && linkTarget == "" {
+ hash = hashFile(path)
+ }
+
manifest.Files[relPath] = FileMeta{
Size: info.Size(),
Mode: info.Mode(),
ModTime: info.ModTime(),
Symlink: linkTarget,
+ Hash: hash,
}
return nil
diff --git a/types.go b/types.go
index bf5dd00..60e1ea9 100644
--- a/types.go
+++ b/types.go
@@ -2,6 +2,7 @@ package main
import (
"encoding/json"
+ "hash/fnv"
"os"
"time"
)
@@ -17,6 +18,7 @@ type FileMeta struct {
Mode os.FileMode
ModTime time.Time
Symlink string
+ Hash uint64
}
type FileMetaJSON struct {
@@ -24,6 +26,7 @@ type FileMetaJSON struct {
Mode uint32 `json:"mode"`
ModTime string `json:"modtime"`
Symlink string `json:"symlink,omitempty"`
+ Hash uint64 `json:"hash,omitempty"`
}
type Manifest struct {
@@ -45,6 +48,7 @@ func (m *Manifest) ToJSON() map[string]FileMetaJSON {
Mode: uint32(v.Mode),
ModTime: v.ModTime.UTC().Format(time.RFC3339Nano),
Symlink: v.Symlink,
+ Hash: v.Hash,
}
}
return files
@@ -59,6 +63,7 @@ func ManifestFromJSON(files map[string]FileMetaJSON) *Manifest {
Mode: os.FileMode(v.Mode),
ModTime: t,
Symlink: v.Symlink,
+ Hash: v.Hash,
}
}
return m
@@ -75,3 +80,23 @@ func DecodeMessage(data []byte) (*ProtocolMessage, error) {
func EncodeMessage(msg *ProtocolMessage) ([]byte, error) {
return json.Marshal(msg)
}
+
+func hashFile(path string) uint64 {
+ f, err := os.Open(path)
+ if err != nil {
+ return 0
+ }
+ defer f.Close()
+ h := fnv.New64a()
+ buf := make([]byte, 32*1024)
+ for {
+ n, err := f.Read(buf)
+ if n > 0 {
+ h.Write(buf[:n])
+ }
+ if err != nil {
+ break
+ }
+ }
+ return h.Sum64()
+}
diff --git a/watcher.go b/watcher.go
index 137bb36..284c3fa 100644
--- a/watcher.go
+++ b/watcher.go
@@ -99,7 +99,8 @@ func syncWrite(srcPath, destPath, relPath string, manifest *Manifest, dryRun boo
}
meta, exists := manifest.Files[relPath]
- if exists && meta.Size == info.Size() && meta.Symlink == linkTarget {
+ srcHash := hashFile(srcPath)
+ if exists && meta.Size == info.Size() && meta.Symlink == linkTarget && (srcHash == 0 || meta.Hash == srcHash) {
return
}
@@ -108,6 +109,7 @@ func syncWrite(srcPath, destPath, relPath string, manifest *Manifest, dryRun boo
Mode: info.Mode(),
ModTime: info.ModTime(),
Symlink: linkTarget,
+ Hash: srcHash,
}
if dryRun {