From 3b9af4f0a1f8f6d64b0fab61294010aba8e8ffc3 Mon Sep 17 00:00:00 2001 From: "Thomas G. Lopes" Date: Thu, 19 Feb 2026 16:42:47 +0000 Subject: [PATCH] Add prune option to apply --- main.go | 111 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 108 insertions(+), 3 deletions(-) diff --git a/main.go b/main.go index 5034df8..b2bc0a7 100644 --- a/main.go +++ b/main.go @@ -33,7 +33,7 @@ func main() { var err error switch args[0] { case "apply": - err = applyCmd() + err = applyCmd(args[1:]) case "add": err = addCmd(args[1:]) case "help", "-h", "--help": @@ -52,11 +52,20 @@ func main() { func usage() { fmt.Println("sigil: minimal dotfile symlink manager") fmt.Println("usage:") - fmt.Println(" sigil apply") + fmt.Println(" sigil apply [--prune]") fmt.Println(" sigil add ") } -func applyCmd() error { +func applyCmd(args []string) error { + prune := false + for _, arg := range args { + if arg == "--prune" { + prune = true + continue + } + return fmt.Errorf("unknown flag %q", arg) + } + repo, err := repoPath() if err != nil { return err @@ -67,6 +76,8 @@ func applyCmd() error { return err } + var stales []string + for _, entry := range entries { if !entry.IsDir() || strings.HasPrefix(entry.Name(), ".") { continue @@ -102,6 +113,28 @@ func applyCmd() error { if err := applyPackage(filesDir, targetRoot); err != nil { return fmt.Errorf("%s: %w", entry.Name(), err) } + + stale, err := findStaleLinks(filesDir, targetRoot) + if err != nil { + return fmt.Errorf("%s: %w", entry.Name(), err) + } + stales = append(stales, stale...) + } + + if len(stales) == 0 { + return nil + } + + if prune { + return removeLinks(stales) + } + + ok, err := promptYesNo("Found stale links. Prune them?", false) + if err != nil { + return err + } + if ok { + return removeLinks(stales) } return nil @@ -395,6 +428,24 @@ func promptWithDefault(reader *bufio.Reader, label, def string) string { return text } +func promptYesNo(message string, def bool) (bool, error) { + reader := bufio.NewReader(os.Stdin) + defLabel := "y/N" + if def { + defLabel = "Y/n" + } + fmt.Printf("%s [%s]: ", message, defLabel) + text, err := reader.ReadString('\n') + if err != nil { + return false, err + } + text = strings.TrimSpace(strings.ToLower(text)) + if text == "" { + return def, nil + } + return text == "y" || text == "yes", nil +} + func moveDirContents(srcDir, destDir string) error { entries, err := os.ReadDir(srcDir) if err != nil { @@ -418,3 +469,57 @@ func moveDirContents(srcDir, destDir string) error { return nil } + +func findStaleLinks(filesDir, targetRoot string) ([]string, error) { + known := make(map[string]struct{}) + + err := filepath.WalkDir(filesDir, func(path string, entry fs.DirEntry, err error) error { + if err != nil { + return err + } + if path == filesDir || entry.IsDir() { + return nil + } + rel, err := filepath.Rel(filesDir, path) + if err != nil { + return err + } + known[filepath.Join(targetRoot, rel)] = struct{}{} + return nil + }) + if err != nil { + return nil, err + } + + var stale []string + for targetPath := range known { + info, err := os.Lstat(targetPath) + if errors.Is(err, os.ErrNotExist) { + continue + } + if err != nil { + return nil, err + } + if info.Mode()&os.ModeSymlink == 0 { + continue + } + src, err := os.Readlink(targetPath) + if err != nil { + return nil, err + } + if _, err := os.Stat(src); errors.Is(err, os.ErrNotExist) { + stale = append(stale, targetPath) + } + } + + return stale, nil +} + +func removeLinks(paths []string) error { + for _, path := range paths { + if err := os.Remove(path); err != nil { + return err + } + } + return nil +}