Compare commits
8 Commits
980bb1e42d
...
c6695aef29
| Author | SHA1 | Date | |
|---|---|---|---|
| c6695aef29 | |||
| 71ff667963 | |||
| bd5517ea5d | |||
| 6685e7aea2 | |||
| fd9aeece00 | |||
| 5a58be10c8 | |||
| dd8e3035df | |||
| b1d3ec4b5b |
@@ -42,3 +42,4 @@ sigil unlink ~/.config/wezterm/wezterm.lua
|
|||||||
|
|
||||||
## Development
|
## Development
|
||||||
- When modifying this repo, rebuild and install the binary: `go build -o ~/.local/bin/sigil .`
|
- When modifying this repo, rebuild and install the binary: `go build -o ~/.local/bin/sigil .`
|
||||||
|
- Bump the version constant in `main.go` for every change (follow semver: bugfix = patch, feature = minor, breaking = major)
|
||||||
|
|||||||
+119
@@ -0,0 +1,119 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGlobToRegexp(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
pattern string
|
||||||
|
matches []string
|
||||||
|
noMatch []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
pattern: "*.txt",
|
||||||
|
matches: []string{"file.txt", "foo.txt"},
|
||||||
|
noMatch: []string{"file.log", "dir/file.txt", "file.txt.bak"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: "**/*.txt",
|
||||||
|
matches: []string{"dir/file.txt", "a/b/c/file.txt"},
|
||||||
|
noMatch: []string{"file.txt", "file.log", "file.txt.bak"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: "cache/**",
|
||||||
|
matches: []string{"cache/file", "cache/a/b/c"},
|
||||||
|
noMatch: []string{"my-cache/file", "cache"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: "?.log",
|
||||||
|
matches: []string{"a.log", "1.log"},
|
||||||
|
noMatch: []string{"file.log", ".log", "ab.log"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: "**/.DS_Store",
|
||||||
|
matches: []string{"foo/.DS_Store", "a/b/.DS_Store"},
|
||||||
|
noMatch: []string{".DS_Store", ".DS_Store.bak", "foo/.DS_Store.txt"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: "./file.txt",
|
||||||
|
matches: []string{"file.txt"},
|
||||||
|
noMatch: []string{"./file.txt"}, // leading ./ is stripped
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: "/absolute",
|
||||||
|
matches: []string{"absolute"},
|
||||||
|
noMatch: []string{"/absolute"}, // leading / is stripped
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.pattern, func(t *testing.T) {
|
||||||
|
re, err := globToRegexp(tt.pattern)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("globToRegexp(%q) error: %v", tt.pattern, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, m := range tt.matches {
|
||||||
|
if !re.MatchString(m) {
|
||||||
|
t.Errorf("pattern %q should match %q, but didn't", tt.pattern, m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, m := range tt.noMatch {
|
||||||
|
if re.MatchString(m) {
|
||||||
|
t.Errorf("pattern %q should NOT match %q, but did", tt.pattern, m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShouldIgnorePath(t *testing.T) {
|
||||||
|
cfg := &packageConfig{
|
||||||
|
compiledIgnores: []*regexp.Regexp{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test with nil config
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add some patterns and re-test
|
||||||
|
patterns := []string{"*.log", "cache/**", "**/.DS_Store"}
|
||||||
|
compiled, err := compileIgnorePatterns(patterns)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("compileIgnorePatterns error: %v", err)
|
||||||
|
}
|
||||||
|
cfg.compiledIgnores = compiled
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
path string
|
||||||
|
ignored bool
|
||||||
|
}{
|
||||||
|
{"debug.log", true},
|
||||||
|
{"app.log", true},
|
||||||
|
{"file.txt", false},
|
||||||
|
{"cache/foo", true},
|
||||||
|
{"cache/a/b", true},
|
||||||
|
{"my-cache/foo", false},
|
||||||
|
{".DS_Store", false}, // root-level .DS_Store not matched by **/.DS_Store
|
||||||
|
{"foo/.DS_Store", true},
|
||||||
|
{"main.go", false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.path, func(t *testing.T) {
|
||||||
|
got := shouldIgnorePath(tt.path, cfg)
|
||||||
|
if got != tt.ignored {
|
||||||
|
t.Errorf("shouldIgnorePath(%q) = %v, want %v", tt.path, got, tt.ignored)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const version = "0.1.0"
|
const version = "0.1.1"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if len(os.Args) < 2 {
|
if len(os.Args) < 2 {
|
||||||
@@ -56,6 +56,7 @@ func usage() {
|
|||||||
fmt.Println(" sigil unlink <package> [--dry-run]")
|
fmt.Println(" sigil unlink <package> [--dry-run]")
|
||||||
fmt.Println(" sigil remove <package> [--dry-run]")
|
fmt.Println(" sigil remove <package> [--dry-run]")
|
||||||
fmt.Println(" sigil status")
|
fmt.Println(" sigil status")
|
||||||
|
fmt.Println(" sigil -v, --version")
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyCmd(args []string) error {
|
func applyCmd(args []string) error {
|
||||||
@@ -198,7 +199,11 @@ func addCmd(args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if pkgName == "" {
|
if pkgName == "" {
|
||||||
pkgName = promptWithDefault("Package name", defaultPkg)
|
var err error
|
||||||
|
pkgName, err = promptWithDefault("Package name", defaultPkg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if pkgName == "" {
|
if pkgName == "" {
|
||||||
return errors.New("package name cannot be empty")
|
return errors.New("package name cannot be empty")
|
||||||
}
|
}
|
||||||
@@ -206,7 +211,11 @@ func addCmd(args []string) error {
|
|||||||
|
|
||||||
targetRootInput := defaultTarget
|
targetRootInput := defaultTarget
|
||||||
if pkgName != matchedPkg || matchedPkg == "" {
|
if pkgName != matchedPkg || matchedPkg == "" {
|
||||||
targetRootInput = promptWithDefault("Target path", defaultTarget)
|
var err error
|
||||||
|
targetRootInput, err = promptWithDefault("Target path", defaultTarget)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
targetRoot, err := filepath.Abs(expandHome(targetRootInput))
|
targetRoot, err := filepath.Abs(expandHome(targetRootInput))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -271,66 +280,16 @@ func addCmd(args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func unlinkCmd(args []string) error {
|
func unlinkCmd(args []string) error {
|
||||||
flags, pkgSpec, err := parsePackageFlags(args)
|
return removeOrUnlink(args, false)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeCmd(args []string) error {
|
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)
|
flags, pkgSpec, err := parsePackageFlags(args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -380,11 +339,9 @@ func removeCmd(args []string) error {
|
|||||||
if err := restorePath(filesDir, targetRoot, relPath, flags.dryRun); err != nil {
|
if err := restorePath(filesDir, targetRoot, relPath, flags.dryRun); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if flags.dryRun {
|
if flags.dryRun {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.RemoveAll(filepath.Join(filesDir, relPath)); err != nil {
|
if err := os.RemoveAll(filepath.Join(filesDir, relPath)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -403,11 +360,14 @@ func statusCmd() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var totalPackages, stalePackages, totalStale int
|
||||||
|
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
if !entry.IsDir() || strings.HasPrefix(entry.Name(), ".") {
|
if !entry.IsDir() || strings.HasPrefix(entry.Name(), ".") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
totalPackages++
|
||||||
pkgDir := filepath.Join(repo, entry.Name())
|
pkgDir := filepath.Join(repo, entry.Name())
|
||||||
configPath := filepath.Join(pkgDir, configFileName)
|
configPath := filepath.Join(pkgDir, configFileName)
|
||||||
cfg, err := loadConfig(configPath)
|
cfg, err := loadConfig(configPath)
|
||||||
@@ -418,6 +378,7 @@ func statusCmd() error {
|
|||||||
targetRoot, err := selectTarget(cfg)
|
targetRoot, err := selectTarget(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, errTargetDisabled) {
|
if errors.Is(err, errTargetDisabled) {
|
||||||
|
totalPackages-- // don't count disabled packages
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
return fmt.Errorf("%s: %w", entry.Name(), err)
|
return fmt.Errorf("%s: %w", entry.Name(), err)
|
||||||
@@ -433,11 +394,19 @@ func statusCmd() error {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stalePackages++
|
||||||
|
totalStale += len(stale)
|
||||||
fmt.Printf("%s: stale links (%d)\n", entry.Name(), len(stale))
|
fmt.Printf("%s: stale links (%d)\n", entry.Name(), len(stale))
|
||||||
for _, path := range stale {
|
for _, path := range stale {
|
||||||
fmt.Printf(" %s\n", path)
|
fmt.Printf(" %s\n", path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if totalPackages > 0 {
|
||||||
|
fmt.Printf("\n%d packages, %d stale links in %d packages\n", totalPackages, totalStale, stalePackages)
|
||||||
|
} else {
|
||||||
|
fmt.Println("\nno packages found")
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,9 +63,9 @@ func linkFile(src, dst string) error {
|
|||||||
if current == src {
|
if current == src {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return fmt.Errorf("conflict at %s (points to %s)", dst, current)
|
return fmt.Errorf("conflict at %s (symlink points to %s, expected %s). Run 'sigil unlink %s' to restore and remove the conflicting link", dst, current, src, dst)
|
||||||
}
|
}
|
||||||
return fmt.Errorf("conflict at %s (exists and is not a symlink)", dst)
|
return fmt.Errorf("conflict at %s (file exists and is not a symlink). Back up or remove this file first", dst)
|
||||||
} else if !errors.Is(err, os.ErrNotExist) {
|
} else if !errors.Is(err, os.ErrNotExist) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -93,8 +93,13 @@ func findStaleLinks(filesDir, targetRoot string) ([]string, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
targetAbs, err := filepath.Abs(targetRoot)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
var stale []string
|
var stale []string
|
||||||
walkErr := filepath.WalkDir(targetRoot, func(path string, entry fs.DirEntry, err error) error {
|
walkErr := filepath.WalkDir(filesDir, func(path string, entry fs.DirEntry, err error) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -102,29 +107,43 @@ func findStaleLinks(filesDir, targetRoot string) ([]string, error) {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
info, err := os.Lstat(path)
|
rel, err := filepath.Rel(filesAbs, path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
targetPath := filepath.Join(targetAbs, rel)
|
||||||
|
info, err := os.Lstat(targetPath)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
// Symlink doesn't exist - not stale, just not applied
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if info.Mode()&os.ModeSymlink == 0 {
|
if info.Mode()&os.ModeSymlink == 0 {
|
||||||
|
// Not a symlink, skip
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
src, err := os.Readlink(path)
|
src, err := os.Readlink(targetPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if !filepath.IsAbs(src) {
|
if !filepath.IsAbs(src) {
|
||||||
src = filepath.Join(filepath.Dir(path), src)
|
src = filepath.Join(filepath.Dir(targetPath), src)
|
||||||
}
|
}
|
||||||
src = filepath.Clean(src)
|
src = filepath.Clean(src)
|
||||||
|
|
||||||
|
// Only consider symlinks pointing to our repo
|
||||||
if !strings.HasPrefix(src, filesAbs+string(os.PathSeparator)) && src != filesAbs {
|
if !strings.HasPrefix(src, filesAbs+string(os.PathSeparator)) && src != filesAbs {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if the source file in repo still exists
|
||||||
if _, err := os.Stat(src); errors.Is(err, os.ErrNotExist) {
|
if _, err := os.Stat(src); errors.Is(err, os.ErrNotExist) {
|
||||||
stale = append(stale, path)
|
stale = append(stale, targetPath)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 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 != "" {
|
if override := os.Getenv("SIGIL_REPO"); override != "" {
|
||||||
return filepath.Abs(expandHome(override))
|
return filepath.Abs(expandHome(override))
|
||||||
@@ -15,6 +17,8 @@ func repoPath() (string, error) {
|
|||||||
return filepath.Abs(expandHome("~/.dotfiles"))
|
return filepath.Abs(expandHome("~/.dotfiles"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 == "~" {
|
if path == "~" {
|
||||||
home, err := os.UserHomeDir()
|
home, err := os.UserHomeDir()
|
||||||
@@ -33,6 +37,8 @@ func expandHome(path string) string {
|
|||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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()
|
home, err := os.UserHomeDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -50,6 +56,8 @@ func compressHome(path string) string {
|
|||||||
return path
|
return 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 == "" {
|
if spec == "" {
|
||||||
return "", "", errors.New("missing package")
|
return "", "", errors.New("missing package")
|
||||||
@@ -72,6 +80,8 @@ func splitPackageSpec(spec string) (string, string, error) {
|
|||||||
return pkg, rel, nil
|
return pkg, rel, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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) {
|
func resolvePackageSpec(spec string) (string, string, error) {
|
||||||
repo, err := repoPath()
|
repo, err := repoPath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -100,6 +110,8 @@ func resolvePackageSpec(spec string) (string, string, error) {
|
|||||||
return splitPackageSpec(spec)
|
return splitPackageSpec(spec)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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)
|
absPath, err := filepath.Abs(pathSpec)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -160,6 +172,8 @@ func resolvePathSpec(pathSpec, repoAbs string) (string, string, error) {
|
|||||||
return "", "", fmt.Errorf("could not resolve %s to a package", pathSpec)
|
return "", "", fmt.Errorf("could not resolve %s to a package", pathSpec)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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)
|
repoEntries, err := os.ReadDir(repo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -201,6 +215,8 @@ func findPackageByTarget(repo, targetRoot string) (string, error) {
|
|||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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)
|
info, err := os.Lstat(targetPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
+163
@@ -0,0 +1,163 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestExpandHome(t *testing.T) {
|
||||||
|
home, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
t.Skipf("cannot get home dir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"~", home},
|
||||||
|
{"~/foo", filepath.Join(home, "foo")},
|
||||||
|
{"~/foo/bar", filepath.Join(home, "foo", "bar")},
|
||||||
|
{"/absolute/path", "/absolute/path"},
|
||||||
|
{"relative/path", "relative/path"},
|
||||||
|
{"", ""},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.input, func(t *testing.T) {
|
||||||
|
got := expandHome(tt.input)
|
||||||
|
if got != tt.expected {
|
||||||
|
t.Errorf("expandHome(%q) = %q, want %q", tt.input, got, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCompressHome(t *testing.T) {
|
||||||
|
home, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
t.Skipf("cannot get home dir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{home, "~"},
|
||||||
|
{filepath.Join(home, "foo"), "~/foo"},
|
||||||
|
{filepath.Join(home, "foo", "bar"), "~/foo/bar"},
|
||||||
|
{"/other/path", "/other/path"},
|
||||||
|
{"relative/path", "relative/path"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.input, func(t *testing.T) {
|
||||||
|
got := compressHome(tt.input)
|
||||||
|
if got != tt.expected {
|
||||||
|
t.Errorf("compressHome(%q) = %q, want %q", tt.input, got, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSplitPackageSpec(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
wantPkg string
|
||||||
|
wantRel string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{"pkg", "pkg", "", false},
|
||||||
|
{"pkg:path", "pkg", "path", false},
|
||||||
|
{"pkg:path/to/file", "pkg", "path/to/file", false},
|
||||||
|
{"pkg:/leading", "pkg", "leading", false},
|
||||||
|
{"/leading/pkg", "leading/pkg", "", false}, // leading slash trimmed
|
||||||
|
{"", "", "", true},
|
||||||
|
{":nope", "", "", true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.input, func(t *testing.T) {
|
||||||
|
pkg, rel, err := splitPackageSpec(tt.input)
|
||||||
|
if (err != nil) != 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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSelectTarget(t *testing.T) {
|
||||||
|
goos := runtime.GOOS
|
||||||
|
osKey := goos
|
||||||
|
if osKey == "darwin" {
|
||||||
|
osKey = "macos"
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
cfg *packageConfig
|
||||||
|
want string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "matching os",
|
||||||
|
cfg: &packageConfig{
|
||||||
|
targets: map[string]string{osKey: "/foo"},
|
||||||
|
},
|
||||||
|
want: filepath.Join("/foo"),
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fallback to default",
|
||||||
|
cfg: &packageConfig{
|
||||||
|
targets: map[string]string{"default": "/bar"},
|
||||||
|
},
|
||||||
|
want: filepath.Join("/bar"),
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "os overrides default",
|
||||||
|
cfg: &packageConfig{
|
||||||
|
targets: map[string]string{osKey: "/os", "default": "/default"},
|
||||||
|
},
|
||||||
|
want: filepath.Join("/os"),
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "disabled target",
|
||||||
|
cfg: &packageConfig{
|
||||||
|
targets: map[string]string{},
|
||||||
|
disabled: map[string]bool{osKey: true},
|
||||||
|
},
|
||||||
|
want: "",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing target",
|
||||||
|
cfg: &packageConfig{
|
||||||
|
targets: map[string]string{"other": "/other"},
|
||||||
|
},
|
||||||
|
want: "",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := selectTarget(tt.cfg)
|
||||||
|
if (err != nil) != 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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -43,7 +43,7 @@ func newReader() *bufio.Reader {
|
|||||||
return stdinReader
|
return stdinReader
|
||||||
}
|
}
|
||||||
|
|
||||||
func promptWithDefault(label, def string) string {
|
func promptWithDefault(label, def string) (string, error) {
|
||||||
reader := newReader()
|
reader := newReader()
|
||||||
if def != "" {
|
if def != "" {
|
||||||
fmt.Printf("%s [%s]: ", label, def)
|
fmt.Printf("%s [%s]: ", label, def)
|
||||||
@@ -51,12 +51,15 @@ func promptWithDefault(label, def string) string {
|
|||||||
fmt.Printf("%s: ", label)
|
fmt.Printf("%s: ", label)
|
||||||
}
|
}
|
||||||
|
|
||||||
text, _ := reader.ReadString('\n')
|
text, err := reader.ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
text = strings.TrimSpace(text)
|
text = strings.TrimSpace(text)
|
||||||
if text == "" {
|
if text == "" {
|
||||||
return def
|
return def, nil
|
||||||
}
|
}
|
||||||
return text
|
return text, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func promptYesNo(message string, def bool) (bool, error) {
|
func promptYesNo(message string, def bool) (bool, error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user