From 6f2be072722b37e0b37e05adb63413264ea3d52e Mon Sep 17 00:00:00 2001 From: "Thomas G. Lopes" Date: Thu, 19 Feb 2026 17:07:38 +0000 Subject: [PATCH] Support unlinking package subpaths --- main.go | 151 ++++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 113 insertions(+), 38 deletions(-) diff --git a/main.go b/main.go index 5c1ecc2..f919cdb 100644 --- a/main.go +++ b/main.go @@ -87,11 +87,33 @@ func parsePackageFlags(args []string) (packageFlags, string, error) { pkg = arg } if pkg == "" { - return flags, "", errors.New("missing package name") + return flags, "", errors.New("missing package") } return flags, pkg, nil } +func splitPackageSpec(spec string) (string, string, error) { + if spec == "" { + return "", "", errors.New("missing package") + } + + parts := strings.SplitN(spec, ":", 2) + pkg := parts[0] + rel := "" + if len(parts) == 2 { + rel = parts[1] + } + + pkg = strings.Trim(pkg, "/") + rel = strings.TrimPrefix(rel, "/") + + if pkg == "" { + return "", "", errors.New("invalid package") + } + + return pkg, rel, nil +} + func applyCmd(args []string) error { prune := false for _, arg := range args { @@ -275,7 +297,12 @@ func addCmd(args []string) error { } func unlinkCmd(args []string) error { - flags, pkgName, err := parsePackageFlags(args) + flags, pkgSpec, err := parsePackageFlags(args) + if err != nil { + return err + } + + pkgName, relPath, err := splitPackageSpec(pkgSpec) if err != nil { return err } @@ -302,7 +329,11 @@ func unlinkCmd(args []string) error { return err } - return restorePackage(filesDir, targetRoot, flags.dryRun) + if relPath == "" { + return restorePackage(filesDir, targetRoot, flags.dryRun) + } + + return restorePath(filesDir, targetRoot, relPath, flags.dryRun) } func removeCmd(args []string) error { @@ -708,44 +739,88 @@ func restorePackage(filesDir, targetRoot string, dryRun bool) error { return err } targetPath := filepath.Join(targetRoot, rel) - - info, err := os.Lstat(targetPath) - if errors.Is(err, os.ErrNotExist) { - return nil - } - if err != nil { - return err - } - if info.Mode()&os.ModeSymlink == 0 { - return nil - } - - src, err := os.Readlink(targetPath) - if err != nil { - return err - } - if !filepath.IsAbs(src) { - src = filepath.Join(filepath.Dir(targetPath), src) - } - src = filepath.Clean(src) - - if !strings.HasPrefix(src, filesAbs+string(os.PathSeparator)) && src != filesAbs { - return nil - } - - if dryRun { - fmt.Printf("dry-run: restore %s\n", targetPath) - return nil - } - - if err := os.Remove(targetPath); err != nil { - return err - } - - return copyFile(path, targetPath) + return restoreOne(path, targetPath, filesAbs, dryRun) }) } +func restorePath(filesDir, targetRoot, relPath string, dryRun bool) error { + filesAbs, err := filepath.Abs(filesDir) + if err != nil { + return err + } + + relPath = filepath.Clean(relPath) + if strings.HasPrefix(relPath, "..") || filepath.IsAbs(relPath) { + return fmt.Errorf("invalid relative path %q", relPath) + } + + sourcePath := filepath.Join(filesDir, relPath) + info, err := os.Lstat(sourcePath) + if err != nil { + return err + } + + if info.IsDir() { + return filepath.WalkDir(sourcePath, func(path string, entry fs.DirEntry, err error) error { + if err != nil { + return err + } + if entry.IsDir() { + return nil + } + rel, err := filepath.Rel(filesDir, path) + if err != nil { + return err + } + targetPath := filepath.Join(targetRoot, rel) + return restoreOne(path, targetPath, filesAbs, dryRun) + }) + } + + rel, err := filepath.Rel(filesDir, sourcePath) + if err != nil { + return err + } + return restoreOne(sourcePath, filepath.Join(targetRoot, rel), filesAbs, dryRun) +} + +func restoreOne(sourcePath, targetPath, filesAbs string, dryRun bool) error { + info, err := os.Lstat(targetPath) + if errors.Is(err, os.ErrNotExist) { + return nil + } + if err != nil { + return err + } + if info.Mode()&os.ModeSymlink == 0 { + return nil + } + + src, err := os.Readlink(targetPath) + if err != nil { + return err + } + if !filepath.IsAbs(src) { + src = filepath.Join(filepath.Dir(targetPath), src) + } + src = filepath.Clean(src) + + if !strings.HasPrefix(src, filesAbs+string(os.PathSeparator)) && src != filesAbs { + return nil + } + + if dryRun { + fmt.Printf("dry-run: restore %s\n", targetPath) + return nil + } + + if err := os.Remove(targetPath); err != nil { + return err + } + + return copyFile(sourcePath, targetPath) +} + func copyFile(src, dst string) error { if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil { return err