feat(go): implement link with subdir and force handling
This commit is contained in:
@@ -18,6 +18,7 @@ type App struct {
|
||||
Stdout io.Writer
|
||||
Stderr io.Writer
|
||||
Verbose bool
|
||||
Force bool
|
||||
}
|
||||
|
||||
func New(stdout io.Writer, stderr io.Writer) (*App, error) {
|
||||
@@ -126,8 +127,44 @@ func (a *App) Diff(castle string) error {
|
||||
return runGit(filepath.Join(a.ReposDir, castle), "diff")
|
||||
}
|
||||
|
||||
func (a *App) Link(string) error {
|
||||
return errors.New("link is not implemented in Go yet")
|
||||
func (a *App) Link(castle string) error {
|
||||
if strings.TrimSpace(castle) == "" {
|
||||
castle = "dotfiles"
|
||||
}
|
||||
return a.LinkCastle(castle)
|
||||
}
|
||||
|
||||
func (a *App) LinkCastle(castle string) error {
|
||||
castleHome := filepath.Join(a.ReposDir, castle, "home")
|
||||
info, err := os.Stat(castleHome)
|
||||
if err != nil || !info.IsDir() {
|
||||
return fmt.Errorf("could not symlink %s, expected %s to exist and contain dotfiles", castle, castleHome)
|
||||
}
|
||||
|
||||
subdirs, err := readSubdirs(filepath.Join(a.ReposDir, castle, ".homesick_subdir"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := a.linkEach(castleHome, castleHome, subdirs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, subdir := range subdirs {
|
||||
base := filepath.Join(castleHome, subdir)
|
||||
if _, err := os.Stat(base); err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if err := a.linkEach(castleHome, base, subdirs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) Unlink(string) error {
|
||||
@@ -138,6 +175,127 @@ func (a *App) Track(string, string) error {
|
||||
return errors.New("track is not implemented in Go yet")
|
||||
}
|
||||
|
||||
func (a *App) linkEach(castleHome string, baseDir string, subdirs []string) error {
|
||||
entries, err := os.ReadDir(baseDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
name := entry.Name()
|
||||
if name == "." || name == ".." {
|
||||
continue
|
||||
}
|
||||
|
||||
source := filepath.Join(baseDir, name)
|
||||
ignore, err := matchesIgnoredDir(castleHome, source, subdirs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ignore {
|
||||
continue
|
||||
}
|
||||
|
||||
relDir, err := filepath.Rel(castleHome, baseDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
destination := filepath.Join(a.HomeDir, relDir, name)
|
||||
if relDir == "." {
|
||||
destination = filepath.Join(a.HomeDir, name)
|
||||
}
|
||||
|
||||
if err := a.linkPath(source, destination); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) linkPath(source string, destination string) error {
|
||||
absSource, err := filepath.Abs(source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(destination), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
info, err := os.Lstat(destination)
|
||||
if err == nil {
|
||||
if info.Mode()&os.ModeSymlink != 0 {
|
||||
target, readErr := os.Readlink(destination)
|
||||
if readErr == nil && target == absSource {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if !a.Force {
|
||||
return fmt.Errorf("%s exists", destination)
|
||||
}
|
||||
|
||||
if rmErr := os.RemoveAll(destination); rmErr != nil {
|
||||
return rmErr
|
||||
}
|
||||
} else if !errors.Is(err, os.ErrNotExist) {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.Symlink(absSource, destination); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func readSubdirs(path string) ([]string, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return []string{}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
lines := strings.Split(string(data), "\n")
|
||||
result := make([]string, 0, len(lines))
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" {
|
||||
continue
|
||||
}
|
||||
result = append(result, filepath.Clean(trimmed))
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func matchesIgnoredDir(castleHome string, candidate string, subdirs []string) (bool, error) {
|
||||
absCandidate, err := filepath.Abs(candidate)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
ignoreSet := map[string]struct{}{}
|
||||
for _, subdir := range subdirs {
|
||||
clean := filepath.Clean(subdir)
|
||||
for clean != "." && clean != string(filepath.Separator) {
|
||||
ignoreSet[filepath.Join(castleHome, clean)] = struct{}{}
|
||||
next := filepath.Dir(clean)
|
||||
if next == clean {
|
||||
break
|
||||
}
|
||||
clean = next
|
||||
}
|
||||
}
|
||||
|
||||
_, ok := ignoreSet[absCandidate]
|
||||
return ok, nil
|
||||
}
|
||||
|
||||
func runGit(dir string, args ...string) error {
|
||||
cmd := exec.Command("git", args...)
|
||||
cmd.Dir = dir
|
||||
|
||||
Reference in New Issue
Block a user