commit f1b312c573720555f8aaa74f82cb84215837a66b
parent a4c9505fc628315db5c7c543bf1d9e51f31c1fbc
Author: Erik Loualiche <eloualic@umn.edu>
Date: Sun, 22 Mar 2026 10:30:59 -0500
feat: esync check reports broken symlinks with their dangling targets
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Diffstat:
1 file changed, 20 insertions(+), 0 deletions(-)
diff --git a/cmd/check.go b/cmd/check.go
@@ -19,6 +19,7 @@ import (
var (
greenHeader = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("10"))
yellowHeader = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("11"))
+ redHeader = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("9"))
dimText = lipgloss.NewStyle().Foreground(lipgloss.Color("8"))
)
@@ -87,6 +88,7 @@ func printPreview(cfg *config.Config) error {
var included []fileEntry
var excluded []fileEntry
+ var brokenLinks []fileEntry
var includedSize int64
err := filepath.Walk(localDir, func(path string, info os.FileInfo, err error) error {
@@ -104,6 +106,15 @@ func printPreview(cfg *config.Config) error {
return nil
}
+ // Detect broken symlinks (Walk uses Lstat, so symlinks show up with err==nil)
+ if info.Mode()&os.ModeSymlink != 0 {
+ if _, statErr := os.Stat(path); statErr != nil {
+ target, _ := os.Readlink(path)
+ brokenLinks = append(brokenLinks, fileEntry{path: rel, rule: target})
+ }
+ return nil // skip all symlinks from included/excluded lists
+ }
+
// Check against ignore patterns
for _, pattern := range patterns {
if matchesIgnorePattern(rel, info, pattern) {
@@ -168,6 +179,15 @@ func printPreview(cfg *config.Config) error {
}
fmt.Println()
+ // --- Broken symlinks ---
+ if len(brokenLinks) > 0 {
+ fmt.Println(redHeader.Render(" Broken symlinks:"))
+ for _, f := range brokenLinks {
+ fmt.Printf(" %-40s %s\n", f.path, dimText.Render("-> "+f.rule))
+ }
+ fmt.Println()
+ }
+
// --- Totals ---
totals := fmt.Sprintf(" %d files included (%s) | %d excluded",
len(included), formatSize(includedSize), len(excluded))