-
Notifications
You must be signed in to change notification settings - Fork 0
feat: enhance drift visualization with content drift and orphan detection (go4dot-ov1.5) #124
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -117,3 +117,78 @@ func findOrphanedSymlinks(configPath, home string) []string { | |||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| return orphans | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // findOrphanFiles finds files in home managed directories that aren't tracked | ||||||||||||||||||||||||||||||
| // by the config source. Only checks directories where the config has files | ||||||||||||||||||||||||||||||
| // (not parent traversal directories). Skips root directory to avoid scanning | ||||||||||||||||||||||||||||||
| // the entire home. | ||||||||||||||||||||||||||||||
| func findOrphanFiles(configPath, home string) []string { | ||||||||||||||||||||||||||||||
| var orphans []string | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // Build set of expected file paths (relative to config root) | ||||||||||||||||||||||||||||||
| expectedFiles := make(map[string]bool) | ||||||||||||||||||||||||||||||
| _ = filepath.Walk(configPath, func(path string, info os.FileInfo, err error) error { | ||||||||||||||||||||||||||||||
| if err != nil || info.IsDir() { | ||||||||||||||||||||||||||||||
| return nil | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| relPath, err := filepath.Rel(configPath, path) | ||||||||||||||||||||||||||||||
| if err == nil { | ||||||||||||||||||||||||||||||
| expectedFiles[relPath] = true | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| return nil | ||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // Get directories that directly contain config files | ||||||||||||||||||||||||||||||
| fileDirs := make(map[string]bool) | ||||||||||||||||||||||||||||||
| for relPath := range expectedFiles { | ||||||||||||||||||||||||||||||
| dir := filepath.Dir(relPath) | ||||||||||||||||||||||||||||||
| fileDirs[dir] = true | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // Walk these directories in home, find unmanaged files | ||||||||||||||||||||||||||||||
| for relDir := range fileDirs { | ||||||||||||||||||||||||||||||
| // Skip root directory to avoid scanning entire home | ||||||||||||||||||||||||||||||
| if relDir == "." { | ||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| targetDir := filepath.Join(home, relDir) | ||||||||||||||||||||||||||||||
| entries, err := os.ReadDir(targetDir) | ||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| for _, entry := range entries { | ||||||||||||||||||||||||||||||
| if entry.IsDir() { | ||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||
|
Comment on lines
+161
to
+163
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Symlinks-to-directories treated as orphan files
This can produce spurious orphan reports for directories that are symlinked into a managed folder by another tool (e.g. a plugin manager placing a symlinked directory beside managed dotfiles).
Suggested change
|
||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| entryRelPath := filepath.Join(relDir, entry.Name()) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // Skip if this file is expected from the config | ||||||||||||||||||||||||||||||
| if expectedFiles[entryRelPath] { | ||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // Skip symlinks pointing into our config dir (handled by MissingFiles) | ||||||||||||||||||||||||||||||
| entryPath := filepath.Join(targetDir, entry.Name()) | ||||||||||||||||||||||||||||||
| if entry.Type()&os.ModeSymlink != 0 { | ||||||||||||||||||||||||||||||
| linkDest, err := os.Readlink(entryPath) | ||||||||||||||||||||||||||||||
| if err == nil { | ||||||||||||||||||||||||||||||
| if !filepath.IsAbs(linkDest) { | ||||||||||||||||||||||||||||||
| linkDest = filepath.Join(targetDir, linkDest) | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| linkDest = filepath.Clean(linkDest) | ||||||||||||||||||||||||||||||
| relToConfig, err := filepath.Rel(configPath, linkDest) | ||||||||||||||||||||||||||||||
| if err == nil && !strings.HasPrefix(relToConfig, "..") && relToConfig != ".." { | ||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| orphans = append(orphans, entryRelPath) | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| return orphans | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Silent false negative for large same-size files
When two files both exceed 10 MB and happen to have identical sizes, this block returns
false(i.e. "no drift"), even if their contents actually differ. A config file that was edited so that the byte count was preserved — characters replaced without adding or removing bytes — would be silently missed. For a dotfiles manager, that scenario is common (e.g. editing a value in-place in a config).Because the size equality is already checked on line 233, this skip only applies to the narrow case of equal-size files above 10 MB. It would be safer to return a distinct "unknown" sentinel or at least add a comment that explicitly warns callers that content drift may go undetected for large same-size files.