Skip to content

Commit

Permalink
fix: Defer lookup of git in $PATH for git-repo externals
Browse files Browse the repository at this point in the history
  • Loading branch information
twpayne committed Jan 25, 2024
1 parent c7539b0 commit 15f4eb1
Show file tree
Hide file tree
Showing 5 changed files with 76 additions and 23 deletions.
32 changes: 31 additions & 1 deletion internal/chezmoi/lazy.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,23 @@
package chezmoi

import "os/exec"

// A commandFunc is a function that returns an *os/exec.Cmd.
type commandFunc func() *exec.Cmd

// A contentsFunc is a function that returns the contents of a file or an error.
// It is typically used for lazy evaluation of a file's contents.
type contentsFunc func() ([]byte, error)

// A lazyCommand returns an *os/exec.Cmd lazily. It is needed to defer the call
// to os/exec.Command because os/exec.Command calls os/exec.LookupPath and
// therefore depends on the state of $PATH when os/exec.Command is called, not
// the state of $PATH when os/exec.Cmd.{Run,Start} is called.
type lazyCommand struct {
commandFunc commandFunc
command *exec.Cmd
}

// A lazyContents evaluates its contents lazily.
type lazyContents struct {
contentsFunc contentsFunc
Expand All @@ -24,6 +38,22 @@ type lazyLinkname struct {
linknameSHA256 []byte
}

// newLazyCommandFunc returns a new lazyCommand with commandFunc.
func newLazyCommandFunc(commandFunc func() *exec.Cmd) *lazyCommand {
return &lazyCommand{
commandFunc: commandFunc,
}
}

// Command returns lc's command.
func (lc *lazyCommand) Command() *exec.Cmd {
if lc.commandFunc != nil {
lc.command = lc.commandFunc()
lc.commandFunc = nil
}
return lc.command
}

// newLazyContents returns a new lazyContents with contents.
func newLazyContents(contents []byte) *lazyContents {
return &lazyContents{
Expand All @@ -32,7 +62,7 @@ func newLazyContents(contents []byte) *lazyContents {
}

// newLazyContentsFunc returns a new lazyContents with contentsFunc.
func newLazyContentsFunc(contentsFunc func() ([]byte, error)) *lazyContents {
func newLazyContentsFunc(contentsFunc contentsFunc) *lazyContents {
return &lazyContents{
contentsFunc: contentsFunc,
}
Expand Down
36 changes: 20 additions & 16 deletions internal/chezmoi/sourcestate.go
Original file line number Diff line number Diff line change
Expand Up @@ -1132,15 +1132,17 @@ func (s *SourceState) Read(ctx context.Context, options *ReadOptions) error {
switch _, err := s.system.Lstat(destAbsPath); {
case errors.Is(err, fs.ErrNotExist):
// FIXME add support for using builtin git
args := []string{"clone"}
args = append(args, external.Clone.Args...)
args = append(args, external.URL, destAbsPath.String())
cmd := exec.Command("git", args...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
sourceStateCommand := &SourceStateCommand{
cmd: cmd,
cmd: newLazyCommandFunc(func() *exec.Cmd {
args := []string{"clone"}
args = append(args, external.Clone.Args...)
args = append(args, external.URL, destAbsPath.String())
cmd := exec.Command("git", args...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd
}),
origin: external,
forceRefresh: options.RefreshExternals == RefreshExternalsAlways,
refreshPeriod: external.RefreshPeriod,
Expand All @@ -1153,15 +1155,17 @@ func (s *SourceState) Read(ctx context.Context, options *ReadOptions) error {
return err
default:
// FIXME add support for using builtin git
args := []string{"pull"}
args = append(args, external.Pull.Args...)
cmd := exec.Command("git", args...)
cmd.Dir = destAbsPath.String()
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
sourceStateCommand := &SourceStateCommand{
cmd: cmd,
cmd: newLazyCommandFunc(func() *exec.Cmd {
args := []string{"pull"}
args = append(args, external.Pull.Args...)
cmd := exec.Command("git", args...)
cmd.Dir = destAbsPath.String()
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd
}),
origin: external,
forceRefresh: options.RefreshExternals == RefreshExternalsAlways,
refreshPeriod: external.RefreshPeriod,
Expand Down
5 changes: 2 additions & 3 deletions internal/chezmoi/sourcestateentry.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package chezmoi

import (
"encoding/hex"
"os/exec"

"github.com/rs/zerolog"

Expand Down Expand Up @@ -38,7 +37,7 @@ type SourceStateEntry interface {

// A SourceStateCommand represents a command that should be run.
type SourceStateCommand struct {
cmd *exec.Cmd
cmd *lazyCommand
origin SourceStateOrigin
forceRefresh bool
refreshPeriod Duration
Expand Down Expand Up @@ -96,7 +95,7 @@ func (s *SourceStateCommand) Evaluate() error {
// MarshalZerologObject implements
// github.com/rs/zerolog.LogObjectMarshaler.MarshalZerologObject.
func (s *SourceStateCommand) MarshalZerologObject(e *zerolog.Event) {
e.EmbedObject(chezmoilog.OSExecCmdLogObject{Cmd: s.cmd})
e.EmbedObject(chezmoilog.OSExecCmdLogObject{Cmd: s.cmd.Command()})
e.Str("origin", s.origin.OriginString())
}

Expand Down
5 changes: 2 additions & 3 deletions internal/chezmoi/targetstateentry.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"encoding/hex"
"fmt"
"io/fs"
"os/exec"
"runtime"
"time"
)
Expand All @@ -26,7 +25,7 @@ type TargetStateEntry interface {
// A TargetStateModifyDirWithCmd represents running a command that modifies
// a directory.
type TargetStateModifyDirWithCmd struct {
cmd *exec.Cmd
cmd *lazyCommand
forceRefresh bool
refreshPeriod Duration
sourceAttr SourceAttr
Expand Down Expand Up @@ -91,7 +90,7 @@ func (t *TargetStateModifyDirWithCmd) Apply(
}

runAt := time.Now().UTC()
if err := system.RunCmd(t.cmd); err != nil {
if err := system.RunCmd(t.cmd.Command()); err != nil {
return false, fmt.Errorf("%s: %w", actualStateEntry.Path(), err)
}

Expand Down
21 changes: 21 additions & 0 deletions internal/cmd/testdata/scripts/issue3510.txtar
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[windows] skip 'UNIX only'

expandenv $CHEZMOISOURCEDIR/.chezmoiexternal.toml

# test that chezmoi apply does not cache the absence of git in $PATH at startup
exec chezmoi apply
stdout 'using newly-installed git'

-- golden/git --
#!/bin/sh

echo "using newly-installed git"
-- home/user/.local/share/chezmoi/.chezmoiexternal.toml --
[".dir"]
type = "git-repo"
url = "file://$WORK/repo"
-- home/user/.local/share/chezmoi/run_once_before_install-git.sh --
#!/bin/sh

mkdir -p $WORK/bin
install -m 755 $WORK/golden/git $WORK/bin

0 comments on commit 15f4eb1

Please sign in to comment.