package main import ( "errors" "fmt" "os" "path/filepath" "strings" ) func repoPath() (string, error) { if override := os.Getenv("SIGIL_REPO"); override != "" { return filepath.Abs(expandHome(override)) } return filepath.Abs(expandHome("~/.dotfiles")) } func expandHome(path string) string { if path == "~" { home, err := os.UserHomeDir() if err != nil { return path } return home } if strings.HasPrefix(path, "~/") { home, err := os.UserHomeDir() if err != nil { return path } return filepath.Join(home, path[2:]) } return path } func compressHome(path string) string { home, err := os.UserHomeDir() if err != nil { return path } clean := filepath.Clean(path) homeClean := filepath.Clean(home) if clean == homeClean { return "~" } if strings.HasPrefix(clean, homeClean+string(os.PathSeparator)) { rel := strings.TrimPrefix(clean, homeClean+string(os.PathSeparator)) return filepath.Join("~", rel) } return path } 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 resolvePackageSpec(spec string) (string, string, error) { repo, err := repoPath() if err != nil { return "", "", err } repoAbs, err := filepath.Abs(repo) if err != nil { return "", "", err } spec = expandHome(spec) if filepath.IsAbs(spec) { return resolvePathSpec(spec, repoAbs) } if strings.Contains(spec, string(os.PathSeparator)) { return resolvePathSpec(spec, repoAbs) } clean := filepath.Clean(spec) if strings.HasPrefix(clean, ".") || strings.HasPrefix(clean, string(os.PathSeparator)) { return resolvePathSpec(clean, repoAbs) } return splitPackageSpec(spec) } func resolvePathSpec(pathSpec, repoAbs string) (string, string, error) { absPath, err := filepath.Abs(pathSpec) if err != nil { return "", "", err } if strings.HasPrefix(absPath, repoAbs+string(os.PathSeparator)) || absPath == repoAbs { rel, err := filepath.Rel(repoAbs, absPath) if err != nil { return "", "", err } parts := strings.Split(rel, string(os.PathSeparator)) if len(parts) >= 1 { pkg := parts[0] if len(parts) >= 2 && parts[1] == filesDirName { relPath := filepath.Join(parts[2:]...) return pkg, relPath, nil } return pkg, filepath.Join(parts[1:]...), nil } } entries, err := os.ReadDir(repoAbs) if err != nil { return "", "", err } for _, entry := range entries { if !entry.IsDir() || strings.HasPrefix(entry.Name(), ".") { continue } pkgDir := filepath.Join(repoAbs, entry.Name()) configPath := filepath.Join(pkgDir, configFileName) cfg, err := loadConfig(configPath) if err != nil { continue } targetRoot, err := selectTarget(cfg) if err != nil { if errors.Is(err, errTargetDisabled) { continue } continue } absTarget, err := filepath.Abs(expandHome(targetRoot)) if err != nil { continue } if strings.HasPrefix(absPath, absTarget+string(os.PathSeparator)) || absPath == absTarget { rel, err := filepath.Rel(absTarget, absPath) if err != nil { return "", "", err } return entry.Name(), rel, nil } } return "", "", fmt.Errorf("could not resolve %s to a package", pathSpec) } func findPackageByTarget(repo, targetRoot string) (string, error) { repoEntries, err := os.ReadDir(repo) if err != nil { return "", err } absTarget, err := filepath.Abs(expandHome(targetRoot)) if err != nil { return "", err } for _, entry := range repoEntries { if !entry.IsDir() || strings.HasPrefix(entry.Name(), ".") { continue } pkgDir := filepath.Join(repo, entry.Name()) configPath := filepath.Join(pkgDir, configFileName) cfg, err := loadConfig(configPath) if err != nil { continue } target, err := selectTarget(cfg) if err != nil { if errors.Is(err, errTargetDisabled) { continue } continue } absPkgTarget, err := filepath.Abs(expandHome(target)) if err != nil { continue } if absPkgTarget == absTarget { return entry.Name(), nil } } return "", nil } func repoPathForTarget(targetPath, repo string) (string, error) { info, err := os.Lstat(targetPath) 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) repoAbs, err := filepath.Abs(repo) if err != nil { return "", err } rel, err := filepath.Rel(repoAbs, src) if err != nil { return "", err } if strings.HasPrefix(rel, "..") { return "", nil } parts := strings.Split(rel, string(os.PathSeparator)) if len(parts) < 3 { return "", nil } if parts[1] != filesDirName { return "", nil } return filepath.Join(repoAbs, rel), nil }