mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-10 07:36:27 -04:00
When a user switches into a repo whose .envrc hasn't been approved with `direnv allow`, the previous behavior was to drop a "blocked" error popup and leave the user to fix it externally. That meant opening a terminal, running `direnv allow`, and then either restarting lazygit or switching repos and back to refresh the env — easy to get wrong, easy to forget. When `direnv export json` exits non-zero, follow up with `direnv status --json` to ask direnv whether the current directory has a not-yet- allowed .envrc, and if so, get its path. Then show a confirmation popup with the .envrc contents inline so the user can read what they're approving. Confirming runs `direnv allow <path>` and re-runs the load so the new env reaches subprocesses immediately; cancelling leaves the env unloaded (the same state as before this commit when direnv refused to load the .envrc). Using `direnv status --json` instead of parsing the "is blocked" stderr line means we rely on direnv's structured output rather than its human-readable error format, which is more stable across versions and avoids assumptions about output formatting. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
131 lines
3.9 KiB
Go
131 lines
3.9 KiB
Go
package direnv
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
|
|
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
|
)
|
|
|
|
// LoadResult bundles everything callers might want to know about a direnv
|
|
// invocation. The env-var delta has already been applied to the process by
|
|
// the time Load returns.
|
|
type LoadResult struct {
|
|
// Message is whatever direnv printed to stderr — useful to log
|
|
// (success: "direnv: loading .envrc"; error: the error text).
|
|
Message string
|
|
|
|
// Err is non-nil when direnv exited non-zero or its stdout could
|
|
// not be parsed.
|
|
Err error
|
|
|
|
// Blocked is true when the target .envrc exists but hasn't been
|
|
// approved with `direnv allow` yet. EnvrcPath then holds the path
|
|
// direnv said was blocked, suitable for passing to Allow.
|
|
Blocked bool
|
|
EnvrcPath string
|
|
}
|
|
|
|
// Load runs `direnv export json` for the current working directory and applies
|
|
// the resulting env-var delta to the current process. If direnv isn't on PATH,
|
|
// it's a no-op — users who don't use direnv pay nothing, and users who do need
|
|
// no config to opt in.
|
|
func Load(cmd oscommands.ICmdObjBuilder) LoadResult {
|
|
if _, lookupErr := exec.LookPath("direnv"); lookupErr != nil {
|
|
return LoadResult{}
|
|
}
|
|
|
|
stdout, stderr, runErr := cmd.New([]string{
|
|
"direnv", "export", "json",
|
|
}).DontLog().RunWithOutputs()
|
|
|
|
result := LoadResult{Message: strings.TrimRight(stderr, "\n")}
|
|
|
|
// Apply whatever delta direnv produced even if it exited non-zero.
|
|
// When the new dir's .envrc is blocked, direnv still emits a valid
|
|
// JSON delta on stdout that unloads vars from the previous dir;
|
|
// without applying it the old env would leak into the new repo.
|
|
delta, parseErr := parseDirenvExport([]byte(stdout))
|
|
for k, v := range delta {
|
|
if v == nil {
|
|
_ = os.Unsetenv(k)
|
|
} else {
|
|
_ = os.Setenv(k, *v)
|
|
}
|
|
}
|
|
|
|
// Prefer the runtime error (whose Error() text is direnv's stderr)
|
|
// over a parse error, since it's the more actionable signal.
|
|
if runErr != nil {
|
|
result.Err = runErr
|
|
if envrcPath := queryBlockedEnvrc(cmd); envrcPath != "" {
|
|
result.Blocked = true
|
|
result.EnvrcPath = envrcPath
|
|
}
|
|
} else {
|
|
result.Err = parseErr
|
|
}
|
|
return result
|
|
}
|
|
|
|
// Allow runs `direnv allow <envrcPath>` to approve a .envrc file so the next
|
|
// Load can read it.
|
|
func Allow(cmd oscommands.ICmdObjBuilder, envrcPath string) error {
|
|
return cmd.New([]string{"direnv", "allow", envrcPath}).DontLog().Run()
|
|
}
|
|
|
|
func parseDirenvExport(stdout []byte) (map[string]*string, error) {
|
|
trimmed := bytes.TrimSpace(stdout)
|
|
if len(trimmed) == 0 || bytes.Equal(trimmed, []byte("null")) {
|
|
return nil, nil
|
|
}
|
|
var delta map[string]*string
|
|
if err := json.Unmarshal(trimmed, &delta); err != nil {
|
|
return nil, err
|
|
}
|
|
return delta, nil
|
|
}
|
|
|
|
// queryBlockedEnvrc asks direnv (via `status --json`) whether the current
|
|
// directory has a found-but-not-yet-allowed .envrc, and returns its path
|
|
// if so. We use direnv's structured output rather than parsing the
|
|
// human-readable "is blocked" line because the status output is more
|
|
// stable across versions and locales.
|
|
func queryBlockedEnvrc(cmd oscommands.ICmdObjBuilder) string {
|
|
stdout, _, err := cmd.New([]string{
|
|
"direnv", "status", "--json",
|
|
}).DontLog().RunWithOutputs()
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return parseDirenvStatus([]byte(stdout))
|
|
}
|
|
|
|
func parseDirenvStatus(stdout []byte) string {
|
|
var status struct {
|
|
State struct {
|
|
FoundRC *struct {
|
|
Allowed int `json:"allowed"`
|
|
Path string `json:"path"`
|
|
} `json:"foundRC"`
|
|
} `json:"state"`
|
|
}
|
|
if err := json.Unmarshal(stdout, &status); err != nil {
|
|
return ""
|
|
}
|
|
if status.State.FoundRC == nil {
|
|
return ""
|
|
}
|
|
// direnv's AllowStatus enum (`internal/cmd/rc.go`): 0=Allowed,
|
|
// 1=NotAllowed, 2=Denied. Only NotAllowed is something the user
|
|
// can approve; Denied means they already said no.
|
|
const notAllowed = 1
|
|
if status.State.FoundRC.Allowed != notAllowed {
|
|
return ""
|
|
}
|
|
return status.State.FoundRC.Path
|
|
}
|