Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions internal/scope/scope.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Package scope abstracts the bidirectional path translation between a file's
// "live" location on the local filesystem and its "storage" location within
// the lnk repository.
package scope

import (
"fmt"
"path/filepath"
)

// Resolver translates between absolute live paths and storage-relative paths.
type Resolver interface {
// ToStorage translates an absolute filesystem path into a path relative
// to the resolver's base directory.
ToStorage(absPath string) (string, error)

// ToLive translates a storage-relative path back into an absolute
// filesystem path.
ToLive(storagePath string) (string, error)

// BaseDir returns the directory within the lnk repo where files for
// this scope are stored.
BaseDir() string
}

// HomeRelativeResolver maps paths relative to the user's home directory.
// This is the resolver for common and host scopes.
type HomeRelativeResolver struct {
Home string
StorageDir string
}

// ToStorage returns the path of absPath relative to the home directory.
func (r *HomeRelativeResolver) ToStorage(absPath string) (string, error) {
rel, err := filepath.Rel(r.Home, absPath)
if err != nil {
return "", fmt.Errorf("make home-relative: %w", err)
}
return rel, nil
}

// ToLive returns the absolute path for a storage-relative path.
func (r *HomeRelativeResolver) ToLive(storagePath string) (string, error) {
return filepath.Join(r.Home, storagePath), nil
}

// BaseDir returns the configured storage directory for this scope.
func (r *HomeRelativeResolver) BaseDir() string {
return r.StorageDir
}
2 changes: 1 addition & 1 deletion service/add.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ func (s *Service) Add(ctx context.Context, host string, paths []string) error {
host = NormalizeHost(host)
seen := make(map[string]struct{}, len(paths))
for _, input := range paths {
file, err := homeRelativePath(input)
file, err := s.homeRelativePath(input)
if err != nil {
return err
}
Expand Down
10 changes: 4 additions & 6 deletions service/doctor.go
Original file line number Diff line number Diff line change
Expand Up @@ -258,11 +258,6 @@ func (s *Service) doctorFix(ctx context.Context, host string, all, pruneEmpty bo
// scanCrossScope finds tracked paths that are inside a git repo. These belong
// in project scope, not common/host scope.
func (s *Service) scanCrossScope(ctx context.Context) ([]string, error) {
homeDir, err := os.UserHomeDir()
if err != nil {
return nil, fmt.Errorf("resolve home directory: %w", err)
}

format, err := s.getFormat()
if err != nil {
return nil, err
Expand All @@ -280,7 +275,10 @@ func (s *Service) scanCrossScope(ctx context.Context) ([]string, error) {
return nil, itemErr
}
for _, item := range items {
absPath := filepath.Join(homeDir, item)
absPath, err := s.resolver.ToLive(item)
if err != nil {
return nil, err
}
if _, statErr := os.Stat(absPath); statErr != nil {
if errors.Is(statErr, os.ErrNotExist) {
continue
Expand Down
2 changes: 1 addition & 1 deletion service/move.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ func (s *Service) Move(ctx context.Context, input string, toHost string, toCommo
if err := s.requireGitRepo(); err != nil {
return err
}
file, err := homeRelativePath(input)
file, err := s.homeRelativePath(input)
if err != nil {
return err
}
Expand Down
57 changes: 41 additions & 16 deletions service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
fspkg "github.com/polymorcodeus/lnk/internal/fs"
gitpkg "github.com/polymorcodeus/lnk/internal/git"
"github.com/polymorcodeus/lnk/internal/lnkerror"
"github.com/polymorcodeus/lnk/internal/scope"
"github.com/polymorcodeus/lnk/internal/tracker"
)

Expand All @@ -31,10 +32,20 @@ type Service struct {
git *gitpkg.Git
format tracker.RepoFormat
gitConfigured bool
resolver scope.Resolver
}

type Option func(*Service)

// WithResolver sets the scope resolver used to translate between live and
// storage paths. When unset, New constructs a HomeRelativeResolver from
// os.UserHomeDir().
func WithResolver(r scope.Resolver) Option {
return func(s *Service) {
s.resolver = r
}
}

// WithColor is a convenience wrapper that returns git.WithColor
func WithColor(enabled bool) Option {
if enabled {
Expand Down Expand Up @@ -101,9 +112,29 @@ func New(repoPath string, opts ...Option) *Service {
opt(s)
}

if s.resolver == nil {
homeDir, err := os.UserHomeDir()
if err != nil {
// Preserve existing error behavior by using a resolver that
// surfaces the home-directory lookup error when used.
s.resolver = &failingResolver{err: fmt.Errorf("resolve home directory: %w", err)}
} else {
s.resolver = &scope.HomeRelativeResolver{Home: homeDir}
}
}

return s
}

// failingResolver returns its construction error for every operation.
type failingResolver struct {
err error
}

func (r *failingResolver) ToStorage(string) (string, error) { return "", r.err }
func (r *failingResolver) ToLive(string) (string, error) { return "", r.err }
func (r *failingResolver) BaseDir() string { return "" }

// ResolveRepoPath resolves the repo path from explicit flag or environment.
func ResolveRepoPath(explicit string) string {
if explicit != "" {
Expand Down Expand Up @@ -223,7 +254,7 @@ func (s *Service) findOwnerInScope(relativePath, host string) (*owner, error) {
// resolveRemovalScope determines which scope owns a path for removal/forget operations.
func (s *Service) resolveRemovalScope(input, host string) (string, homePath, error) {
// host is explicitly not normalized to allow for input lookup across all scopes
file, err := homeRelativePath(input)
file, err := s.homeRelativePath(input)
if err != nil {
return "", homePath{}, err
}
Expand Down Expand Up @@ -286,11 +317,10 @@ func (s *Service) scanCollisions() ([]OwnershipCollision, error) {

// repointManagedSymlink recreates the live symlink so it points to the new storage path.
func (s *Service) repointManagedSymlink(relativePath, targetPath string) error {
homeDir, err := os.UserHomeDir()
livePath, err := s.resolver.ToLive(relativePath)
if err != nil {
return fmt.Errorf("resolve home directory: %w", err)
return err
}
livePath := filepath.Join(homeDir, relativePath)
if _, err := os.Lstat(livePath); errors.Is(err, os.ErrNotExist) {
return nil
}
Expand Down Expand Up @@ -324,16 +354,12 @@ func (s *Service) stagePaths(ctx context.Context, paths ...string) error {
}

// homeRelativePath resolves an input path to an absolute path and its relative path from $HOME.
func homeRelativePath(input string) (homePath, error) {
func (s *Service) homeRelativePath(input string) (homePath, error) {
absPath, err := filepath.Abs(input)
if err != nil {
return homePath{}, fmt.Errorf("resolve path %s: %w", input, err)
}
homeDir, err := os.UserHomeDir()
if err != nil {
return homePath{}, fmt.Errorf("resolve home directory: %w", err)
}
relativePath, err := filepath.Rel(homeDir, absPath)
relativePath, err := s.resolver.ToStorage(absPath)
if err != nil {
return homePath{}, fmt.Errorf("resolve relative path %s: %w", input, err)
}
Expand Down Expand Up @@ -377,11 +403,6 @@ func isManagedSymlink(livePath, expectedTarget string) bool {

// profileItems returns the ordered list of items that make up the effective machine profile.
func (s *Service) profileItems(host string) ([]profileItem, error) {
homeDir, err := os.UserHomeDir()
if err != nil {
return nil, fmt.Errorf("resolve home directory: %w", err)
}

format, err := s.getFormat()
if err != nil {
return nil, err
Expand Down Expand Up @@ -410,10 +431,14 @@ func (s *Service) profileItems(host string) ([]profileItem, error) {
if err != nil {
return nil, err
}
livePath, err := s.resolver.ToLive(relativePath)
if err != nil {
return nil, err
}
items = append(items, profileItem{
RelativePath: relativePath,
RepoPath: filepath.Join(hostPath, relativePath),
LivePath: filepath.Join(homeDir, relativePath),
LivePath: livePath,
})
}
}
Expand Down
Loading