gosick #1
@@ -171,20 +171,22 @@ type execCmd struct{}
|
||||
|
||||
type execAllCmd struct{}
|
||||
|
||||
type rcCmd struct{}
|
||||
type rcCmd struct {
|
||||
Castle string `arg:"" optional:"" name:"CASTLE" help:"Castle name."`
|
||||
}
|
||||
|
||||
type generateCmd struct{}
|
||||
|
||||
func (c *pullCmd) Run() error { return notImplemented("pull") }
|
||||
func (c *pushCmd) Run() error { return notImplemented("push") }
|
||||
func (c *commitCmd) Run() error { return notImplemented("commit") }
|
||||
func (c *destroyCmd) Run() error { return notImplemented("destroy") }
|
||||
func (c *cdCmd) Run() error { return notImplemented("cd") }
|
||||
func (c *openCmd) Run() error { return notImplemented("open") }
|
||||
func (c *execCmd) Run() error { return notImplemented("exec") }
|
||||
func (c *execAllCmd) Run() error { return notImplemented("exec_all") }
|
||||
func (c *rcCmd) Run() error { return notImplemented("rc") }
|
||||
func (c *generateCmd) Run() error { return notImplemented("generate") }
|
||||
func (c *pullCmd) Run() error { return notImplemented("pull") }
|
||||
func (c *pushCmd) Run() error { return notImplemented("push") }
|
||||
func (c *commitCmd) Run() error { return notImplemented("commit") }
|
||||
func (c *destroyCmd) Run() error { return notImplemented("destroy") }
|
||||
func (c *cdCmd) Run() error { return notImplemented("cd") }
|
||||
func (c *openCmd) Run() error { return notImplemented("open") }
|
||||
func (c *execCmd) Run() error { return notImplemented("exec") }
|
||||
func (c *execAllCmd) Run() error { return notImplemented("exec_all") }
|
||||
func (c *rcCmd) Run(app *core.App) error { return app.Rc(defaultCastle(c.Castle)) }
|
||||
func (c *generateCmd) Run() error { return notImplemented("generate") }
|
||||
|
||||
func defaultCastle(castle string) string {
|
||||
if strings.TrimSpace(castle) == "" {
|
||||
|
||||
@@ -531,6 +531,82 @@ func gitOutput(dir string, args ...string) (string, error) {
|
||||
return string(out), nil
|
||||
}
|
||||
|
||||
// Rc runs the rc hooks for the given castle. It looks for executable files
|
||||
// inside <castle>/.homesick.d and runs them in sorted (lexicographic) order
|
||||
// with the castle root as the working directory, forwarding stdout and stderr
|
||||
// to the App writers.
|
||||
//
|
||||
// If a .homesickrc file exists in the castle root and no parity.rb wrapper
|
||||
// already exists in .homesick.d, a Ruby wrapper script named parity.rb is
|
||||
// written there before execution so that it sorts first.
|
||||
func (a *App) Rc(castle string) error {
|
||||
castleRoot := filepath.Join(a.ReposDir, castle)
|
||||
if _, err := os.Stat(castleRoot); err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return fmt.Errorf("castle %q not found", castle)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
homesickD := filepath.Join(castleRoot, ".homesick.d")
|
||||
homesickRc := filepath.Join(castleRoot, ".homesickrc")
|
||||
|
||||
// If .homesickrc exists, ensure .homesick.d/parity.rb wrapper is created
|
||||
// (but do not overwrite an existing parity.rb).
|
||||
if _, err := os.Stat(homesickRc); err == nil {
|
||||
wrapperPath := filepath.Join(homesickD, "parity.rb")
|
||||
if _, err := os.Stat(wrapperPath); errors.Is(err, os.ErrNotExist) {
|
||||
if mkErr := os.MkdirAll(homesickD, 0o755); mkErr != nil {
|
||||
return fmt.Errorf("create .homesick.d: %w", mkErr)
|
||||
}
|
||||
wrapperContent := "#!/usr/bin/env ruby\n" +
|
||||
"# parity.rb — generated wrapper for legacy .homesickrc\n" +
|
||||
"# Evaluates .homesickrc in the context of the castle root.\n" +
|
||||
"rc_file = File.join(__dir__, '..', '.homesickrc')\n" +
|
||||
"eval(File.read(rc_file), binding, rc_file) if File.exist?(rc_file)\n"
|
||||
if writeErr := os.WriteFile(wrapperPath, []byte(wrapperContent), 0o755); writeErr != nil {
|
||||
return fmt.Errorf("write parity.rb: %w", writeErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := os.Stat(homesickD); err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(homesickD)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// ReadDir returns entries in sorted order already.
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
info, infoErr := entry.Info()
|
||||
if infoErr != nil {
|
||||
return infoErr
|
||||
}
|
||||
if info.Mode()&0o111 == 0 {
|
||||
// Not executable — skip.
|
||||
continue
|
||||
}
|
||||
scriptPath := filepath.Join(homesickD, entry.Name())
|
||||
cmd := exec.Command(scriptPath)
|
||||
cmd.Dir = castleRoot
|
||||
cmd.Stdout = a.Stdout
|
||||
cmd.Stderr = a.Stderr
|
||||
if runErr := cmd.Run(); runErr != nil {
|
||||
return fmt.Errorf("rc script %q failed: %w", entry.Name(), runErr)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func deriveDestination(uri string) string {
|
||||
candidate := strings.TrimSpace(uri)
|
||||
candidate = strings.TrimPrefix(candidate, "https://github.com/")
|
||||
|
||||
@@ -100,7 +100,7 @@ func (s *RcSuite) TestRc_SkipsNonExecutableFiles() {
|
||||
}
|
||||
|
||||
// TestRc_HomesickrcCreatesRubyWrapper verifies that a .homesickrc file causes
|
||||
// a Ruby wrapper to be written into .homesick.d before execution.
|
||||
// a Ruby wrapper called parity.rb to be written into .homesick.d before execution.
|
||||
func (s *RcSuite) TestRc_HomesickrcCreatesRubyWrapper() {
|
||||
castleRoot := s.createCastle("dotfiles")
|
||||
homesickRc := filepath.Join(castleRoot, ".homesickrc")
|
||||
@@ -108,7 +108,7 @@ func (s *RcSuite) TestRc_HomesickrcCreatesRubyWrapper() {
|
||||
|
||||
require.NoError(s.T(), s.app.Rc("dotfiles"))
|
||||
|
||||
wrapperPath := filepath.Join(castleRoot, ".homesick.d", "00_homesickrc.rb")
|
||||
wrapperPath := filepath.Join(castleRoot, ".homesick.d", "parity.rb")
|
||||
require.FileExists(s.T(), wrapperPath)
|
||||
|
||||
info, err := os.Stat(wrapperPath)
|
||||
@@ -120,9 +120,28 @@ func (s *RcSuite) TestRc_HomesickrcCreatesRubyWrapper() {
|
||||
require.Contains(s.T(), string(content), ".homesickrc")
|
||||
}
|
||||
|
||||
// TestRc_HomesickrcWrapperRunsBeforeOtherScripts ensures the wrapper file
|
||||
// (00_homesickrc.rb) sorts before typical user scripts and is present in
|
||||
// .homesick.d after Rc returns.
|
||||
// TestRc_HomesickrcWrapperNotOverwrittenIfExists verifies that an existing
|
||||
// parity.rb is not overwritten when Rc is called again.
|
||||
func (s *RcSuite) TestRc_HomesickrcWrapperNotOverwrittenIfExists() {
|
||||
castleRoot := s.createCastle("dotfiles")
|
||||
homesickRc := filepath.Join(castleRoot, ".homesickrc")
|
||||
require.NoError(s.T(), os.WriteFile(homesickRc, []byte("# ruby setup code\n"), 0o644))
|
||||
|
||||
homesickD := filepath.Join(castleRoot, ".homesick.d")
|
||||
require.NoError(s.T(), os.MkdirAll(homesickD, 0o755))
|
||||
wrapperPath := filepath.Join(homesickD, "parity.rb")
|
||||
originalContent := []byte("#!/bin/sh\n# pre-existing wrapper\n")
|
||||
require.NoError(s.T(), os.WriteFile(wrapperPath, originalContent, 0o755))
|
||||
|
||||
require.NoError(s.T(), s.app.Rc("dotfiles"))
|
||||
|
||||
content, err := os.ReadFile(wrapperPath)
|
||||
require.NoError(s.T(), err)
|
||||
require.Equal(s.T(), originalContent, content, "existing parity.rb must not be overwritten")
|
||||
}
|
||||
|
||||
// TestRc_HomesickrcWrapperCreatedBeforeExecution ensures parity.rb is present
|
||||
// in .homesick.d before any scripts in that directory are executed.
|
||||
func (s *RcSuite) TestRc_HomesickrcWrapperCreatedBeforeExecution() {
|
||||
castleRoot := s.createCastle("dotfiles")
|
||||
homesickRc := filepath.Join(castleRoot, ".homesickrc")
|
||||
@@ -134,7 +153,7 @@ func (s *RcSuite) TestRc_HomesickrcWrapperCreatedBeforeExecution() {
|
||||
// A sentinel script that records whether the wrapper already exists.
|
||||
orderFile := filepath.Join(s.tmpDir, "check.txt")
|
||||
sentinel := filepath.Join(homesickD, "50_check.sh")
|
||||
wrapperPath := filepath.Join(homesickD, "00_homesickrc.rb")
|
||||
wrapperPath := filepath.Join(homesickD, "parity.rb")
|
||||
require.NoError(s.T(), os.WriteFile(sentinel, []byte(
|
||||
"#!/bin/sh\n[ -f "+wrapperPath+" ] && echo present >> "+orderFile+"\n",
|
||||
), 0o755))
|
||||
|
||||
Reference in New Issue
Block a user