move core logic to internal/core package, bump to 0.1.3
This commit is contained in:
@@ -0,0 +1,260 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"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) {
|
||||
if override := os.Getenv("SIGIL_REPO"); override != "" {
|
||||
return filepath.Abs(ExpandHome(override))
|
||||
}
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
// 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 {
|
||||
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
|
||||
}
|
||||
|
||||
// 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) {
|
||||
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
|
||||
}
|
||||
|
||||
// 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()
|
||||
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)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
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)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
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
|
||||
}
|
||||
|
||||
// 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) {
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user