gosick #1

Merged
DelphicOkami merged 162 commits from gosick into main 2026-03-21 23:08:00 +00:00
3 changed files with 114 additions and 17 deletions
Showing only changes of commit a381746cef - Show all commits

View File

@@ -171,7 +171,9 @@ type execCmd struct{}
type execAllCmd struct{}
type rcCmd struct{}
type rcCmd struct {
Castle string `arg:"" optional:"" name:"CASTLE" help:"Castle name."`
}
type generateCmd struct{}
@@ -183,7 +185,7 @@ 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 *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 {

View File

@@ -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/")

View File

@@ -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))