From 0215a53fcf8a810ccbef9f95048b1a458df1beb1 Mon Sep 17 00:00:00 2001 From: "Thomas G. Lopes" Date: Thu, 5 Mar 2026 14:56:28 +0000 Subject: [PATCH] move core logic to internal/core package, bump to 0.1.3 --- config.go => internal/core/config.go | 38 +++--- .../core/config_test.go | 22 +-- ops.go => internal/core/ops.go | 128 +++++++++++++----- ops_test.go => internal/core/ops_test.go | 42 +++--- path.go => internal/core/path.go | 74 +++++----- path_test.go => internal/core/path_test.go | 22 +-- util.go => internal/core/util.go | 8 +- main.go | 128 +++++------------- 8 files changed, 232 insertions(+), 230 deletions(-) rename config.go => internal/core/config.go (82%) rename config_test.go => internal/core/config_test.go (81%) rename ops.go => internal/core/ops.go (70%) rename ops_test.go => internal/core/ops_test.go (88%) rename path.go => internal/core/path.go (71%) rename path_test.go => internal/core/path_test.go (84%) rename util.go => internal/core/util.go (86%) diff --git a/config.go b/internal/core/config.go similarity index 82% rename from config.go rename to internal/core/config.go index 8a2ae7e..49eff69 100644 --- a/config.go +++ b/internal/core/config.go @@ -1,4 +1,4 @@ -package main +package core import ( "errors" @@ -14,8 +14,8 @@ import ( ) const ( - configFileName = "config.lua" - filesDirName = "files" + ConfigFileName = "config.lua" + FilesDirName = "files" ) type packageConfig struct { @@ -25,9 +25,9 @@ type packageConfig struct { compiledIgnores []*regexp.Regexp } -var errTargetDisabled = errors.New("target disabled for this platform") +var ErrTargetDisabled = errors.New("target disabled for this platform") -func loadConfig(path string) (*packageConfig, error) { +func LoadConfig(path string) (*packageConfig, error) { L := lua.NewState() defer L.Close() L.OpenLibs() @@ -69,18 +69,18 @@ func loadConfig(path string) (*packageConfig, error) { return } - targets[string(ks)] = expandHome(string(vs)) + targets[string(ks)] = ExpandHome(string(vs)) }) if len(targets) == 0 && len(disabled) == 0 { return nil, errors.New("config.target is empty") } - ignore, err := parseIgnore(tbl) + ignore, err := ParseIgnore(tbl) if err != nil { return nil, err } - compiledIgnores, err := compileIgnorePatterns(ignore) + compiledIgnores, err := CompileIgnorePatterns(ignore) if err != nil { return nil, err } @@ -88,24 +88,24 @@ func loadConfig(path string) (*packageConfig, error) { return &packageConfig{targets: targets, disabled: disabled, ignore: ignore, compiledIgnores: compiledIgnores}, nil } -func selectTarget(cfg *packageConfig) (string, error) { +func SelectTarget(cfg *packageConfig) (string, error) { osKey := runtime.GOOS if osKey == "darwin" { osKey = "macos" } if cfg.disabled[osKey] { - return "", errTargetDisabled + return "", ErrTargetDisabled } if target, ok := cfg.targets[osKey]; ok { - return expandHome(target), nil + return ExpandHome(target), nil } if target, ok := cfg.targets["default"]; ok { - return expandHome(target), nil + return ExpandHome(target), nil } return "", fmt.Errorf("missing target for %s and default", osKey) } -func parseIgnore(cfgTbl *lua.LTable) ([]string, error) { +func ParseIgnore(cfgTbl *lua.LTable) ([]string, error) { ignoreVal := cfgTbl.RawGetString("ignore") if ignoreVal == lua.LNil { return nil, nil @@ -148,10 +148,10 @@ func parseIgnore(cfgTbl *lua.LTable) ([]string, error) { return ignore, nil } -func compileIgnorePatterns(patterns []string) ([]*regexp.Regexp, error) { +func CompileIgnorePatterns(patterns []string) ([]*regexp.Regexp, error) { compiled := make([]*regexp.Regexp, 0, len(patterns)) for _, pattern := range patterns { - re, err := globToRegexp(pattern) + re, err := GlobToRegexp(pattern) if err != nil { return nil, fmt.Errorf("invalid ignore pattern %q: %w", pattern, err) } @@ -160,7 +160,7 @@ func compileIgnorePatterns(patterns []string) ([]*regexp.Regexp, error) { return compiled, nil } -func globToRegexp(pattern string) (*regexp.Regexp, error) { +func GlobToRegexp(pattern string) (*regexp.Regexp, error) { pattern = strings.ReplaceAll(pattern, "\\", "/") pattern = strings.TrimPrefix(pattern, "./") if strings.HasPrefix(pattern, "/") { @@ -192,7 +192,7 @@ func globToRegexp(pattern string) (*regexp.Regexp, error) { return regexp.Compile(b.String()) } -func shouldIgnorePath(rel string, cfg *packageConfig) bool { +func ShouldIgnorePath(rel string, cfg *packageConfig) bool { if cfg == nil || len(cfg.compiledIgnores) == 0 { return false } @@ -207,13 +207,13 @@ func shouldIgnorePath(rel string, cfg *packageConfig) bool { return false } -func writeConfig(path, targetRoot string) error { +func WriteConfig(path, targetRoot string) error { osKey := "linux" if runtime.GOOS == "darwin" { osKey = "macos" } - prettyTarget := compressHome(targetRoot) + prettyTarget := CompressHome(targetRoot) content := fmt.Sprintf("---@class SigilConfig\n---@field target table\n---@field ignore? string[]\n\n---@type SigilConfig\nlocal config = {\n\ttarget = {\n\t\t%s = %q,\n\t\tdefault = %q,\n\t},\n\tignore = {\n\t\t-- \"**/.DS_Store\",\n\t\t-- \"**/*.tmp\",\n\t\t-- \"cache/**\",\n\t},\n}\n\nreturn config\n", osKey, prettyTarget, prettyTarget) return os.WriteFile(path, []byte(content), 0o644) } diff --git a/config_test.go b/internal/core/config_test.go similarity index 81% rename from config_test.go rename to internal/core/config_test.go index c965c8c..2d5b4ee 100644 --- a/config_test.go +++ b/internal/core/config_test.go @@ -1,4 +1,4 @@ -package main +package core import ( "regexp" @@ -50,9 +50,9 @@ func TestGlobToRegexp(t *testing.T) { for _, tt := range tests { t.Run(tt.pattern, func(t *testing.T) { - re, err := globToRegexp(tt.pattern) + re, err := GlobToRegexp(tt.pattern) if err != nil { - t.Fatalf("globToRegexp(%q) error: %v", tt.pattern, err) + t.Fatalf("GlobToRegexp(%q) error: %v", tt.pattern, err) } for _, m := range tt.matches { @@ -76,20 +76,20 @@ func TestShouldIgnorePath(t *testing.T) { } // Test with nil config - if shouldIgnorePath("foo.txt", nil) { - t.Error("shouldIgnorePath with nil config should return false") + if ShouldIgnorePath("foo.txt", nil) { + t.Error("ShouldIgnorePath with nil config should return false") } // Test with empty ignores - if shouldIgnorePath("foo.txt", cfg) { - t.Error("shouldIgnorePath with empty ignores should return false") + if ShouldIgnorePath("foo.txt", cfg) { + t.Error("ShouldIgnorePath with empty ignores should return false") } // Add some patterns and re-test patterns := []string{"*.log", "cache/**", "**/.DS_Store"} - compiled, err := compileIgnorePatterns(patterns) + compiled, err := CompileIgnorePatterns(patterns) if err != nil { - t.Fatalf("compileIgnorePatterns error: %v", err) + t.Fatalf("CompileIgnorePatterns error: %v", err) } cfg.compiledIgnores = compiled @@ -110,9 +110,9 @@ func TestShouldIgnorePath(t *testing.T) { for _, tt := range tests { t.Run(tt.path, func(t *testing.T) { - got := shouldIgnorePath(tt.path, cfg) + got := ShouldIgnorePath(tt.path, cfg) if got != tt.ignored { - t.Errorf("shouldIgnorePath(%q) = %v, want %v", tt.path, got, tt.ignored) + t.Errorf("ShouldIgnorePath(%q) = %v, want %v", tt.path, got, tt.ignored) } }) } diff --git a/ops.go b/internal/core/ops.go similarity index 70% rename from ops.go rename to internal/core/ops.go index e2d7606..a3ba013 100644 --- a/ops.go +++ b/internal/core/ops.go @@ -1,4 +1,4 @@ -package main +package core import ( "errors" @@ -10,8 +10,8 @@ import ( "strings" ) -func applyPackage(filesDir, targetRoot string, cfg *packageConfig) error { - if err := ensureDir(targetRoot); err != nil { +func ApplyPackage(filesDir, targetRoot string, cfg *packageConfig) error { + if err := EnsureDir(targetRoot); err != nil { return err } @@ -28,7 +28,7 @@ func applyPackage(filesDir, targetRoot string, cfg *packageConfig) error { return err } - if shouldIgnorePath(rel, cfg) { + if ShouldIgnorePath(rel, cfg) { if entry.IsDir() { return filepath.SkipDir } @@ -38,18 +38,18 @@ func applyPackage(filesDir, targetRoot string, cfg *packageConfig) error { targetPath := filepath.Join(targetRoot, rel) if entry.IsDir() { - return ensureDir(targetPath) + return EnsureDir(targetPath) } srcAbs, err := filepath.Abs(path) if err != nil { return err } - return linkFile(srcAbs, targetPath) + return LinkFile(srcAbs, targetPath) }) } -func linkFile(src, dst string) error { +func LinkFile(src, dst string) error { if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil { return err } @@ -73,7 +73,7 @@ func linkFile(src, dst string) error { return os.Symlink(src, dst) } -func ensureDir(path string) error { +func EnsureDir(path string) error { info, err := os.Lstat(path) if err == nil { if info.IsDir() { @@ -87,7 +87,7 @@ func ensureDir(path string) error { return os.MkdirAll(path, 0o755) } -func findStaleLinks(filesDir, targetRoot string) ([]string, error) { +func FindStaleLinks(filesDir, targetRoot string) ([]string, error) { filesAbs, err := filepath.Abs(filesDir) if err != nil { return nil, err @@ -155,7 +155,7 @@ func findStaleLinks(filesDir, targetRoot string) ([]string, error) { return stale, nil } -func removeLinks(paths []string, dryRun bool) error { +func RemoveLinks(paths []string, dryRun bool) error { for _, path := range paths { if dryRun { fmt.Printf("dry-run: remove %s\n", path) @@ -169,12 +169,12 @@ func removeLinks(paths []string, dryRun bool) error { return nil } -func handleStaleLinks(stales []string) error { +func HandleStaleLinks(stales []string) error { if len(stales) == 0 { return nil } - repo, err := repoPath() + repo, err := RepoPath() if err != nil { return err } @@ -183,7 +183,7 @@ func handleStaleLinks(stales []string) error { for _, path := range stales { fmt.Printf("stale: %s\n", path) - canUnlink, err := staleHasRepoFile(path, repo) + canUnlink, err := StaleHasRepoFile(path, repo) if err != nil { return err } @@ -210,7 +210,7 @@ func handleStaleLinks(stales []string) error { continue } if choice == "u" && canUnlink { - if err := unlinkStale(path, repo); err != nil { + if err := UnlinkStale(path, repo); err != nil { return err } continue @@ -221,15 +221,15 @@ func handleStaleLinks(stales []string) error { return nil } -func staleHasRepoFile(targetPath, repo string) (bool, error) { - repoPath, err := repoPathForTarget(targetPath, repo) +func StaleHasRepoFile(targetPath, repo string) (bool, error) { + RepoPath, err := RepoPathForTarget(targetPath, repo) if err != nil { return false, err } - if repoPath == "" { + if RepoPath == "" { return false, nil } - if _, err := os.Stat(repoPath); errors.Is(err, os.ErrNotExist) { + if _, err := os.Stat(RepoPath); errors.Is(err, os.ErrNotExist) { return false, nil } else if err != nil { return false, err @@ -237,12 +237,12 @@ func staleHasRepoFile(targetPath, repo string) (bool, error) { return true, nil } -func unlinkStale(targetPath, repo string) error { - repoPath, err := repoPathForTarget(targetPath, repo) +func UnlinkStale(targetPath, repo string) error { + RepoPath, err := RepoPathForTarget(targetPath, repo) if err != nil { return err } - if repoPath == "" { + if RepoPath == "" { return nil } @@ -250,19 +250,19 @@ func unlinkStale(targetPath, repo string) error { return err } - if err := copyFile(repoPath, targetPath); err != nil { + if err := CopyFile(RepoPath, targetPath); err != nil { return err } - if err := os.Remove(repoPath); err != nil { + if err := os.Remove(RepoPath); err != nil { return err } - fmt.Printf("unlinked %s (removed %s)\n", targetPath, repoPath) + fmt.Printf("unlinked %s (removed %s)\n", targetPath, RepoPath) return nil } -func restorePackage(filesDir, targetRoot string, dryRun bool) error { +func RestorePackage(filesDir, targetRoot string, dryRun bool) error { filesAbs, err := filepath.Abs(filesDir) if err != nil { return err @@ -284,11 +284,11 @@ func restorePackage(filesDir, targetRoot string, dryRun bool) error { return err } targetPath := filepath.Join(targetRoot, rel) - return restoreOne(path, targetPath, filesAbs, dryRun) + return RestoreOne(path, targetPath, filesAbs, dryRun) }) } -func restorePath(filesDir, targetRoot, relPath string, dryRun bool) error { +func RestorePath(filesDir, targetRoot, relPath string, dryRun bool) error { filesAbs, err := filepath.Abs(filesDir) if err != nil { return err @@ -318,7 +318,7 @@ func restorePath(filesDir, targetRoot, relPath string, dryRun bool) error { return err } targetPath := filepath.Join(targetRoot, rel) - return restoreOne(path, targetPath, filesAbs, dryRun) + return RestoreOne(path, targetPath, filesAbs, dryRun) }) } @@ -326,10 +326,10 @@ func restorePath(filesDir, targetRoot, relPath string, dryRun bool) error { if err != nil { return err } - return restoreOne(sourcePath, filepath.Join(targetRoot, rel), filesAbs, dryRun) + return RestoreOne(sourcePath, filepath.Join(targetRoot, rel), filesAbs, dryRun) } -func restoreOne(sourcePath, targetPath, filesAbs string, dryRun bool) error { +func RestoreOne(sourcePath, targetPath, filesAbs string, dryRun bool) error { info, err := os.Lstat(targetPath) if errors.Is(err, os.ErrNotExist) { return nil @@ -364,10 +364,10 @@ func restoreOne(sourcePath, targetPath, filesAbs string, dryRun bool) error { } fmt.Printf("restored %s\n", targetPath) - return copyFile(sourcePath, targetPath) + return CopyFile(sourcePath, targetPath) } -func copyFile(src, dst string) error { +func CopyFile(src, dst string) error { if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil { return err } @@ -396,7 +396,7 @@ func copyFile(src, dst string) error { return nil } -func moveDirContents(srcDir, destDir string) error { +func MoveDirContents(srcDir, destDir string) error { entries, err := os.ReadDir(srcDir) if err != nil { return err @@ -419,3 +419,65 @@ func moveDirContents(srcDir, destDir string) error { return nil } + +func RemoveOrUnlink(args []string, isRemove bool) error { + _ = isRemove // commands are identical, flag kept for clarity + + flags, pkgSpec, err := ParsePackageFlags(args) + if err != nil { + return err + } + + pkgName, relPath, err := ResolvePackageSpec(pkgSpec) + if err != nil { + return err + } + + repo, err := RepoPath() + if err != nil { + return err + } + + pkgDir := filepath.Join(repo, pkgName) + configPath := filepath.Join(pkgDir, ConfigFileName) + cfg, err := LoadConfig(configPath) + if err != nil { + return err + } + + targetRoot, err := SelectTarget(cfg) + if err != nil { + return err + } + + filesDir := filepath.Join(pkgDir, FilesDirName) + if _, err := os.Stat(filesDir); err != nil { + return err + } + + if relPath == "" { + if err := RestorePackage(filesDir, targetRoot, flags.dryRun); err != nil { + return err + } + if flags.dryRun { + return nil + } + if err := os.RemoveAll(pkgDir); err != nil { + return err + } + fmt.Printf("removed package %s\n", pkgName) + return nil + } + + if err := RestorePath(filesDir, targetRoot, relPath, flags.dryRun); err != nil { + return err + } + if flags.dryRun { + return nil + } + if err := os.RemoveAll(filepath.Join(filesDir, relPath)); err != nil { + return err + } + fmt.Printf("removed %s:%s\n", pkgName, relPath) + return nil +} diff --git a/ops_test.go b/internal/core/ops_test.go similarity index 88% rename from ops_test.go rename to internal/core/ops_test.go index 0112dd7..b40502e 100644 --- a/ops_test.go +++ b/internal/core/ops_test.go @@ -1,4 +1,4 @@ -package main +package core import ( "os" @@ -19,8 +19,8 @@ func TestLinkFile(t *testing.T) { } // Test: create symlink - if err := linkFile(src, dst); err != nil { - t.Errorf("linkFile failed: %v", err) + if err := LinkFile(src, dst); err != nil { + t.Errorf("LinkFile failed: %v", err) } // Verify symlink @@ -33,8 +33,8 @@ func TestLinkFile(t *testing.T) { } // Test: idempotent - same symlink again should succeed - if err := linkFile(src, dst); err != nil { - t.Errorf("linkFile second time failed: %v", err) + if err := LinkFile(src, dst); err != nil { + t.Errorf("LinkFile second time failed: %v", err) } // Test: conflict - different source should error @@ -43,8 +43,8 @@ func TestLinkFile(t *testing.T) { t.Fatalf("failed to create source2: %v", err) } - if err := linkFile(src2, dst); err == nil { - t.Error("linkFile should fail with conflicting symlink") + if err := LinkFile(src2, dst); err == nil { + t.Error("LinkFile should fail with conflicting symlink") } // Test: conflict - non-symlink file should error @@ -53,8 +53,8 @@ func TestLinkFile(t *testing.T) { t.Fatalf("failed to create regular file: %v", err) } - if err := linkFile(src, dst2); err == nil { - t.Error("linkFile should fail when target exists and is not a symlink") + if err := LinkFile(src, dst2); err == nil { + t.Error("LinkFile should fail when target exists and is not a symlink") } } @@ -102,9 +102,9 @@ func TestFindStaleLinks(t *testing.T) { } // Test: no stale links when sources exist - stale, err := findStaleLinks(filesDir, targetDir) + stale, err := FindStaleLinks(filesDir, targetDir) if err != nil { - t.Errorf("findStaleLinks failed: %v", err) + t.Errorf("FindStaleLinks failed: %v", err) } if len(stale) != 0 { t.Errorf("expected 0 stale links, got %d", len(stale)) @@ -135,16 +135,16 @@ func TestFindStaleLinks_IgnoresNonRepoSymlinks(t *testing.T) { t.Fatalf("failed to create external file: %v", err) } - // Create a symlink to external file (should be ignored by findStaleLinks) + // Create a symlink to external file (should be ignored by FindStaleLinks) dst := filepath.Join(targetDir, "link.txt") if err := os.Symlink(external, dst); err != nil { t.Fatalf("failed to create symlink: %v", err) } // Test: should not report external symlinks as stale - stale, err := findStaleLinks(filesDir, targetDir) + stale, err := FindStaleLinks(filesDir, targetDir) if err != nil { - t.Errorf("findStaleLinks failed: %v", err) + t.Errorf("FindStaleLinks failed: %v", err) } if len(stale) != 0 { t.Errorf("expected 0 stale links (external symlinks ignored), got %d", len(stale)) @@ -175,8 +175,8 @@ func TestApplyPackage(t *testing.T) { cfg := &packageConfig{} // empty config, no ignores // Apply package - if err := applyPackage(filesDir, targetDir, cfg); err != nil { - t.Errorf("applyPackage failed: %v", err) + if err := ApplyPackage(filesDir, targetDir, cfg); err != nil { + t.Errorf("ApplyPackage failed: %v", err) } // Verify symlinks exist @@ -234,12 +234,12 @@ func TestApplyPackage_WithIgnores(t *testing.T) { cfg := &packageConfig{ compiledIgnores: []*regexp.Regexp{}, } - re, _ := globToRegexp("cache/**") + re, _ := GlobToRegexp("cache/**") cfg.compiledIgnores = append(cfg.compiledIgnores, re) // Apply package - if err := applyPackage(filesDir, targetDir, cfg); err != nil { - t.Errorf("applyPackage failed: %v", err) + if err := ApplyPackage(filesDir, targetDir, cfg); err != nil { + t.Errorf("ApplyPackage failed: %v", err) } // Verify main file is linked @@ -282,8 +282,8 @@ func TestRestoreOne(t *testing.T) { // Restore (removes symlink, copies file) filesAbs, _ := filepath.Abs(filesDir) - if err := restoreOne(src, dst, filesAbs, false); err != nil { - t.Errorf("restoreOne failed: %v", err) + if err := RestoreOne(src, dst, filesAbs, false); err != nil { + t.Errorf("RestoreOne failed: %v", err) } // Verify symlink is gone diff --git a/path.go b/internal/core/path.go similarity index 71% rename from path.go rename to internal/core/path.go index 634e24e..f4cf8af 100644 --- a/path.go +++ b/internal/core/path.go @@ -1,4 +1,4 @@ -package main +package core import ( "errors" @@ -8,18 +8,18 @@ import ( "strings" ) -// repoPath returns the absolute path to the sigil repository. +// RepoPath returns the absolute path to the sigil repository. // It uses SIGIL_REPO environment variable if set, otherwise defaults to ~/.dotfiles. -func repoPath() (string, error) { +func RepoPath() (string, error) { if override := os.Getenv("SIGIL_REPO"); override != "" { - return filepath.Abs(expandHome(override)) + return filepath.Abs(ExpandHome(override)) } - return filepath.Abs(expandHome("~/.dotfiles")) + return filepath.Abs(ExpandHome("~/.dotfiles")) } -// expandHome expands a leading ~ to the user's home directory. +// ExpandHome expands a leading ~ to the user's home directory. // Returns the original path unchanged if expansion fails or path doesn't start with ~. -func expandHome(path string) string { +func ExpandHome(path string) string { if path == "~" { home, err := os.UserHomeDir() if err != nil { @@ -37,9 +37,9 @@ func expandHome(path string) string { return path } -// compressHome compresses the user's home directory path to ~. +// CompressHome compresses the user's home directory path to ~. // Returns the original path if it doesn't start with the home directory. -func compressHome(path string) string { +func CompressHome(path string) string { home, err := os.UserHomeDir() if err != nil { return path @@ -56,9 +56,9 @@ func compressHome(path string) string { return path } -// splitPackageSpec splits a package spec like "pkg:path/to/file" into (pkg, path). +// SplitPackageSpec splits a package spec like "pkg:path/to/file" into (pkg, path). // If no colon is present, returns (spec, "", nil). -func splitPackageSpec(spec string) (string, string, error) { +func SplitPackageSpec(spec string) (string, string, error) { if spec == "" { return "", "", errors.New("missing package") } @@ -82,8 +82,8 @@ func splitPackageSpec(spec string) (string, string, error) { // resolvePackageSpec resolves a package spec (name, path, or repo path) to (package, relative path). // It handles specs like "pkg:path", absolute paths within the repo, and absolute paths in the target. -func resolvePackageSpec(spec string) (string, string, error) { - repo, err := repoPath() +func ResolvePackageSpec(spec string) (string, string, error) { + repo, err := RepoPath() if err != nil { return "", "", err } @@ -93,26 +93,26 @@ func resolvePackageSpec(spec string) (string, string, error) { return "", "", err } - spec = expandHome(spec) + spec = ExpandHome(spec) if filepath.IsAbs(spec) { - return resolvePathSpec(spec, repoAbs) + return ResolvePathSpec(spec, repoAbs) } if strings.Contains(spec, string(os.PathSeparator)) { - return resolvePathSpec(spec, repoAbs) + return ResolvePathSpec(spec, repoAbs) } clean := filepath.Clean(spec) if strings.HasPrefix(clean, ".") || strings.HasPrefix(clean, string(os.PathSeparator)) { - return resolvePathSpec(clean, repoAbs) + return ResolvePathSpec(clean, repoAbs) } - return splitPackageSpec(spec) + return SplitPackageSpec(spec) } -// resolvePathSpec resolves an absolute path spec to a package and relative path. +// ResolvePathSpec resolves an absolute path spec to a package and relative path. // It checks if the path is within the repo or matches a package's target directory. -func resolvePathSpec(pathSpec, repoAbs string) (string, string, error) { +func ResolvePathSpec(pathSpec, repoAbs string) (string, string, error) { absPath, err := filepath.Abs(pathSpec) if err != nil { return "", "", err @@ -126,7 +126,7 @@ func resolvePathSpec(pathSpec, repoAbs string) (string, string, error) { parts := strings.Split(rel, string(os.PathSeparator)) if len(parts) >= 1 { pkg := parts[0] - if len(parts) >= 2 && parts[1] == filesDirName { + if len(parts) >= 2 && parts[1] == FilesDirName { relPath := filepath.Join(parts[2:]...) return pkg, relPath, nil } @@ -144,19 +144,19 @@ func resolvePathSpec(pathSpec, repoAbs string) (string, string, error) { continue } pkgDir := filepath.Join(repoAbs, entry.Name()) - configPath := filepath.Join(pkgDir, configFileName) - cfg, err := loadConfig(configPath) + configPath := filepath.Join(pkgDir, ConfigFileName) + cfg, err := LoadConfig(configPath) if err != nil { continue } - targetRoot, err := selectTarget(cfg) + targetRoot, err := SelectTarget(cfg) if err != nil { - if errors.Is(err, errTargetDisabled) { + if errors.Is(err, ErrTargetDisabled) { continue } continue } - absTarget, err := filepath.Abs(expandHome(targetRoot)) + absTarget, err := filepath.Abs(ExpandHome(targetRoot)) if err != nil { continue } @@ -172,15 +172,15 @@ func resolvePathSpec(pathSpec, repoAbs string) (string, string, error) { return "", "", fmt.Errorf("could not resolve %s to a package", pathSpec) } -// findPackageByTarget finds a package name that has the given target root. +// FindPackageByTarget finds a package name that has the given target root. // Returns empty string if no package matches. -func findPackageByTarget(repo, targetRoot string) (string, error) { +func FindPackageByTarget(repo, targetRoot string) (string, error) { repoEntries, err := os.ReadDir(repo) if err != nil { return "", err } - absTarget, err := filepath.Abs(expandHome(targetRoot)) + absTarget, err := filepath.Abs(ExpandHome(targetRoot)) if err != nil { return "", err } @@ -191,19 +191,19 @@ func findPackageByTarget(repo, targetRoot string) (string, error) { } pkgDir := filepath.Join(repo, entry.Name()) - configPath := filepath.Join(pkgDir, configFileName) - cfg, err := loadConfig(configPath) + configPath := filepath.Join(pkgDir, ConfigFileName) + cfg, err := LoadConfig(configPath) if err != nil { continue } - target, err := selectTarget(cfg) + target, err := SelectTarget(cfg) if err != nil { - if errors.Is(err, errTargetDisabled) { + if errors.Is(err, ErrTargetDisabled) { continue } continue } - absPkgTarget, err := filepath.Abs(expandHome(target)) + absPkgTarget, err := filepath.Abs(ExpandHome(target)) if err != nil { continue } @@ -215,9 +215,9 @@ func findPackageByTarget(repo, targetRoot string) (string, error) { return "", nil } -// repoPathForTarget returns the repo file path that a target symlink points to. +// RepoPathForTarget returns the repo file path that a target symlink points to. // Returns empty string if the target is not a symlink to the repo. -func repoPathForTarget(targetPath, repo string) (string, error) { +func RepoPathForTarget(targetPath, repo string) (string, error) { info, err := os.Lstat(targetPath) if err != nil { return "", err @@ -252,7 +252,7 @@ func repoPathForTarget(targetPath, repo string) (string, error) { if len(parts) < 3 { return "", nil } - if parts[1] != filesDirName { + if parts[1] != FilesDirName { return "", nil } diff --git a/path_test.go b/internal/core/path_test.go similarity index 84% rename from path_test.go rename to internal/core/path_test.go index ade3bbf..23ea225 100644 --- a/path_test.go +++ b/internal/core/path_test.go @@ -1,4 +1,4 @@ -package main +package core import ( "os" @@ -27,9 +27,9 @@ func TestExpandHome(t *testing.T) { for _, tt := range tests { t.Run(tt.input, func(t *testing.T) { - got := expandHome(tt.input) + got := ExpandHome(tt.input) if got != tt.expected { - t.Errorf("expandHome(%q) = %q, want %q", tt.input, got, tt.expected) + t.Errorf("ExpandHome(%q) = %q, want %q", tt.input, got, tt.expected) } }) } @@ -54,9 +54,9 @@ func TestCompressHome(t *testing.T) { for _, tt := range tests { t.Run(tt.input, func(t *testing.T) { - got := compressHome(tt.input) + got := CompressHome(tt.input) if got != tt.expected { - t.Errorf("compressHome(%q) = %q, want %q", tt.input, got, tt.expected) + t.Errorf("CompressHome(%q) = %q, want %q", tt.input, got, tt.expected) } }) } @@ -80,13 +80,13 @@ func TestSplitPackageSpec(t *testing.T) { for _, tt := range tests { t.Run(tt.input, func(t *testing.T) { - pkg, rel, err := splitPackageSpec(tt.input) + pkg, rel, err := SplitPackageSpec(tt.input) if (err != nil) != tt.wantErr { - t.Errorf("splitPackageSpec(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr) + t.Errorf("SplitPackageSpec(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr) return } if pkg != tt.wantPkg || rel != tt.wantRel { - t.Errorf("splitPackageSpec(%q) = (%q, %q), want (%q, %q)", tt.input, pkg, rel, tt.wantPkg, tt.wantRel) + t.Errorf("SplitPackageSpec(%q) = (%q, %q), want (%q, %q)", tt.input, pkg, rel, tt.wantPkg, tt.wantRel) } }) } @@ -150,13 +150,13 @@ func TestSelectTarget(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := selectTarget(tt.cfg) + got, err := SelectTarget(tt.cfg) if (err != nil) != tt.wantErr { - t.Errorf("selectTarget() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("SelectTarget() error = %v, wantErr %v", err, tt.wantErr) return } if got != tt.want { - t.Errorf("selectTarget() = %q, want %q", got, tt.want) + t.Errorf("SelectTarget() = %q, want %q", got, tt.want) } }) } diff --git a/util.go b/internal/core/util.go similarity index 86% rename from util.go rename to internal/core/util.go index 07500cb..b608be4 100644 --- a/util.go +++ b/internal/core/util.go @@ -1,4 +1,4 @@ -package main +package core import ( "bufio" @@ -12,7 +12,7 @@ type packageFlags struct { dryRun bool } -func parsePackageFlags(args []string) (packageFlags, string, error) { +func ParsePackageFlags(args []string) (packageFlags, string, error) { flags := packageFlags{} var pkg string for _, arg := range args { @@ -43,7 +43,7 @@ func newReader() *bufio.Reader { return stdinReader } -func promptWithDefault(label, def string) (string, error) { +func PromptWithDefault(label, def string) (string, error) { reader := newReader() if def != "" { fmt.Printf("%s [%s]: ", label, def) @@ -62,7 +62,7 @@ func promptWithDefault(label, def string) (string, error) { return text, nil } -func promptYesNo(message string, def bool) (bool, error) { +func PromptYesNo(message string, def bool) (bool, error) { reader := newReader() defLabel := "y/N" if def { diff --git a/main.go b/main.go index c439d31..34c68df 100644 --- a/main.go +++ b/main.go @@ -6,9 +6,11 @@ import ( "os" "path/filepath" "strings" + + "sigil/internal/core" ) -const version = "0.1.2" +const version = "0.1.3" func main() { if len(os.Args) < 2 { @@ -69,7 +71,7 @@ func applyCmd(args []string) error { return fmt.Errorf("unknown flag %q", arg) } - repo, err := repoPath() + repo, err := core.RepoPath() if err != nil { return err } @@ -87,7 +89,7 @@ func applyCmd(args []string) error { } pkgDir := filepath.Join(repo, entry.Name()) - configPath := filepath.Join(pkgDir, configFileName) + configPath := filepath.Join(pkgDir, core.ConfigFileName) if _, err := os.Stat(configPath); err != nil { if errors.Is(err, os.ErrNotExist) { continue @@ -95,20 +97,20 @@ func applyCmd(args []string) error { return err } - cfg, err := loadConfig(configPath) + cfg, err := core.LoadConfig(configPath) if err != nil { return fmt.Errorf("%s: %w", entry.Name(), err) } - targetRoot, err := selectTarget(cfg) + targetRoot, err := core.SelectTarget(cfg) if err != nil { - if errors.Is(err, errTargetDisabled) { + if errors.Is(err, core.ErrTargetDisabled) { continue } return fmt.Errorf("%s: %w", entry.Name(), err) } - filesDir := filepath.Join(pkgDir, filesDirName) + filesDir := filepath.Join(pkgDir, core.FilesDirName) if _, err := os.Stat(filesDir); err != nil { if errors.Is(err, os.ErrNotExist) { continue @@ -116,11 +118,11 @@ func applyCmd(args []string) error { return err } - if err := applyPackage(filesDir, targetRoot, cfg); err != nil { + if err := core.ApplyPackage(filesDir, targetRoot, cfg); err != nil { return fmt.Errorf("%s: %w", entry.Name(), err) } - stale, err := findStaleLinks(filesDir, targetRoot) + stale, err := core.FindStaleLinks(filesDir, targetRoot) if err != nil { return fmt.Errorf("%s: %w", entry.Name(), err) } @@ -132,11 +134,11 @@ func applyCmd(args []string) error { } if prune { - return removeLinks(stales, false) + return core.RemoveLinks(stales, false) } fmt.Printf("Stale links found: %d\n", len(stales)) - return handleStaleLinks(stales) + return core.HandleStaleLinks(stales) } func addCmd(args []string) error { @@ -164,7 +166,7 @@ func addCmd(args []string) error { return fmt.Errorf("%s is a symlink; refusing to add", absPath) } - repo, err := repoPath() + repo, err := core.RepoPath() if err != nil { return err } @@ -184,12 +186,12 @@ func addCmd(args []string) error { var pkgName string - matchedPkg, err := findPackageByTarget(repo, defaultTarget) + matchedPkg, err := core.FindPackageByTarget(repo, defaultTarget) if err != nil { return err } if matchedPkg != "" { - ok, err := promptYesNo(fmt.Sprintf("Merge into existing package %q?", matchedPkg), true) + ok, err := core.PromptYesNo(fmt.Sprintf("Merge into existing package %q?", matchedPkg), true) if err != nil { return err } @@ -200,7 +202,7 @@ func addCmd(args []string) error { if pkgName == "" { var err error - pkgName, err = promptWithDefault("Package name", defaultPkg) + pkgName, err = core.PromptWithDefault("Package name", defaultPkg) if err != nil { return err } @@ -212,12 +214,12 @@ func addCmd(args []string) error { targetRootInput := defaultTarget if pkgName != matchedPkg || matchedPkg == "" { var err error - targetRootInput, err = promptWithDefault("Target path", defaultTarget) + targetRootInput, err = core.PromptWithDefault("Target path", defaultTarget) if err != nil { return err } } - targetRoot, err := filepath.Abs(expandHome(targetRootInput)) + targetRoot, err := filepath.Abs(core.ExpandHome(targetRootInput)) if err != nil { return err } @@ -230,14 +232,14 @@ func addCmd(args []string) error { return err } - filesDir := filepath.Join(pkgDir, filesDirName) + filesDir := filepath.Join(pkgDir, core.FilesDirName) if err := os.MkdirAll(filesDir, 0o755); err != nil { return err } - configPath := filepath.Join(pkgDir, configFileName) + configPath := filepath.Join(pkgDir, core.ConfigFileName) if !pkgExists { - if err := writeConfig(configPath, targetRoot); err != nil { + if err := core.WriteConfig(configPath, targetRoot); err != nil { return err } } @@ -245,7 +247,7 @@ func addCmd(args []string) error { if pkgExists { return errors.New("cannot merge a directory into an existing package yet") } - if err := moveDirContents(absPath, filesDir); err != nil { + if err := core.MoveDirContents(absPath, filesDir); err != nil { return err } } else { @@ -271,86 +273,24 @@ func addCmd(args []string) error { } } - cfg, err := loadConfig(configPath) + cfg, err := core.LoadConfig(configPath) if err != nil { return err } - return applyPackage(filesDir, targetRoot, cfg) + return core.ApplyPackage(filesDir, targetRoot, cfg) } func unlinkCmd(args []string) error { - return removeOrUnlink(args, false) + return core.RemoveOrUnlink(args, false) } func removeCmd(args []string) error { - return removeOrUnlink(args, true) -} - -func removeOrUnlink(args []string, isRemove bool) error { - _ = isRemove // commands are identical, flag kept for clarity - - flags, pkgSpec, err := parsePackageFlags(args) - if err != nil { - return err - } - - pkgName, relPath, err := resolvePackageSpec(pkgSpec) - if err != nil { - return err - } - - repo, err := repoPath() - if err != nil { - return err - } - - pkgDir := filepath.Join(repo, pkgName) - configPath := filepath.Join(pkgDir, configFileName) - cfg, err := loadConfig(configPath) - if err != nil { - return err - } - - targetRoot, err := selectTarget(cfg) - if err != nil { - return err - } - - filesDir := filepath.Join(pkgDir, filesDirName) - if _, err := os.Stat(filesDir); err != nil { - return err - } - - if relPath == "" { - if err := restorePackage(filesDir, targetRoot, flags.dryRun); err != nil { - return err - } - if flags.dryRun { - return nil - } - if err := os.RemoveAll(pkgDir); err != nil { - return err - } - fmt.Printf("removed package %s\n", pkgName) - return nil - } - - if err := restorePath(filesDir, targetRoot, relPath, flags.dryRun); err != nil { - return err - } - if flags.dryRun { - return nil - } - if err := os.RemoveAll(filepath.Join(filesDir, relPath)); err != nil { - return err - } - fmt.Printf("removed %s:%s\n", pkgName, relPath) - return nil + return core.RemoveOrUnlink(args, true) } func statusCmd() error { - repo, err := repoPath() + repo, err := core.RepoPath() if err != nil { return err } @@ -369,23 +309,23 @@ func statusCmd() error { totalPackages++ pkgDir := filepath.Join(repo, entry.Name()) - configPath := filepath.Join(pkgDir, configFileName) - cfg, err := loadConfig(configPath) + configPath := filepath.Join(pkgDir, core.ConfigFileName) + cfg, err := core.LoadConfig(configPath) if err != nil { return fmt.Errorf("%s: %w", entry.Name(), err) } - targetRoot, err := selectTarget(cfg) + targetRoot, err := core.SelectTarget(cfg) if err != nil { - if errors.Is(err, errTargetDisabled) { + if errors.Is(err, core.ErrTargetDisabled) { totalPackages-- // don't count disabled packages continue } return fmt.Errorf("%s: %w", entry.Name(), err) } - filesDir := filepath.Join(pkgDir, filesDirName) - stale, err := findStaleLinks(filesDir, targetRoot) + filesDir := filepath.Join(pkgDir, core.FilesDirName) + stale, err := core.FindStaleLinks(filesDir, targetRoot) if err != nil { return fmt.Errorf("%s: %w", entry.Name(), err) }