feat(go): implement link with subdir and force handling
This commit is contained in:
@@ -74,7 +74,14 @@ func Run(args []string, stdout io.Writer, stderr io.Writer) int {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
case "link", "unlink", "track", "pull", "push", "commit", "destroy", "cd", "open", "exec", "exec_all", "rc", "generate":
|
case "link":
|
||||||
|
castle := defaultCastle(args)
|
||||||
|
if err := app.LinkCastle(castle); err != nil {
|
||||||
|
_, _ = fmt.Fprintf(stderr, "error: %v\n", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
case "unlink", "track", "pull", "push", "commit", "destroy", "cd", "open", "exec", "exec_all", "rc", "generate":
|
||||||
_, _ = fmt.Fprintf(stderr, "error: %s is not implemented in Go yet\n", command)
|
_, _ = fmt.Fprintf(stderr, "error: %s is not implemented in Go yet\n", command)
|
||||||
return 2
|
return 2
|
||||||
default:
|
default:
|
||||||
@@ -100,10 +107,11 @@ func printHelp(w io.Writer) {
|
|||||||
_, _ = fmt.Fprintln(w, " show_path [CASTLE]")
|
_, _ = fmt.Fprintln(w, " show_path [CASTLE]")
|
||||||
_, _ = fmt.Fprintln(w, " status [CASTLE]")
|
_, _ = fmt.Fprintln(w, " status [CASTLE]")
|
||||||
_, _ = fmt.Fprintln(w, " diff [CASTLE]")
|
_, _ = fmt.Fprintln(w, " diff [CASTLE]")
|
||||||
|
_, _ = fmt.Fprintln(w, " link [CASTLE]")
|
||||||
_, _ = fmt.Fprintln(w, " version | -v | --version")
|
_, _ = fmt.Fprintln(w, " version | -v | --version")
|
||||||
_, _ = fmt.Fprintln(w, "")
|
_, _ = fmt.Fprintln(w, "")
|
||||||
_, _ = fmt.Fprintln(w, "Not implemented yet:")
|
_, _ = fmt.Fprintln(w, "Not implemented yet:")
|
||||||
_, _ = fmt.Fprintln(w, " link, unlink, track, pull, push, commit, destroy, cd, open, exec, exec_all, rc, generate")
|
_, _ = fmt.Fprintln(w, " unlink, track, pull, push, commit, destroy, cd, open, exec, exec_all, rc, generate")
|
||||||
_, _ = fmt.Fprintln(w, "")
|
_, _ = fmt.Fprintln(w, "")
|
||||||
_, _ = fmt.Fprintln(w, "Build: go build -o dist/homesick-go ./cmd/homesick")
|
_, _ = fmt.Fprintln(w, "Build: go build -o dist/homesick-go ./cmd/homesick")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ type App struct {
|
|||||||
Stdout io.Writer
|
Stdout io.Writer
|
||||||
Stderr io.Writer
|
Stderr io.Writer
|
||||||
Verbose bool
|
Verbose bool
|
||||||
|
Force bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(stdout io.Writer, stderr io.Writer) (*App, error) {
|
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")
|
return runGit(filepath.Join(a.ReposDir, castle), "diff")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) Link(string) error {
|
func (a *App) Link(castle string) error {
|
||||||
return errors.New("link is not implemented in Go yet")
|
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 {
|
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")
|
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 {
|
func runGit(dir string, args ...string) error {
|
||||||
cmd := exec.Command("git", args...)
|
cmd := exec.Command("git", args...)
|
||||||
cmd.Dir = dir
|
cmd.Dir = dir
|
||||||
|
|||||||
Reference in New Issue
Block a user