mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-08-24 10:13:41 -05: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>
299 lines
7.6 KiB
Go
299 lines
7.6 KiB
Go
package app
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/go-errors/errors"
|
|
"github.com/sirupsen/logrus"
|
|
"github.com/spf13/afero"
|
|
|
|
appTypes "github.com/jesseduffield/lazygit/pkg/app/types"
|
|
"github.com/jesseduffield/lazygit/pkg/commands/direnv"
|
|
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
|
|
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
|
"github.com/jesseduffield/lazygit/pkg/common"
|
|
"github.com/jesseduffield/lazygit/pkg/config"
|
|
"github.com/jesseduffield/lazygit/pkg/constants"
|
|
"github.com/jesseduffield/lazygit/pkg/env"
|
|
"github.com/jesseduffield/lazygit/pkg/gui"
|
|
"github.com/jesseduffield/lazygit/pkg/i18n"
|
|
integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types"
|
|
"github.com/jesseduffield/lazygit/pkg/logs"
|
|
"github.com/jesseduffield/lazygit/pkg/updates"
|
|
)
|
|
|
|
// App is the struct that's instantiated from within main.go and it manages
|
|
// bootstrapping and running the application.
|
|
type App struct {
|
|
*common.Common
|
|
closers []io.Closer
|
|
Config config.AppConfigurer
|
|
OSCommand *oscommands.OSCommand
|
|
Gui *gui.Gui
|
|
}
|
|
|
|
func Run(
|
|
config config.AppConfigurer,
|
|
common *common.Common,
|
|
startArgs appTypes.StartArgs,
|
|
) {
|
|
app, err := NewApp(config, startArgs.IntegrationTest, common)
|
|
|
|
if err == nil {
|
|
err = app.Run(startArgs)
|
|
}
|
|
|
|
if err != nil {
|
|
if errorMessage, known := knownError(common.Tr, err); known {
|
|
log.Fatal(errorMessage)
|
|
}
|
|
newErr := errors.Wrap(err, 0)
|
|
stackTrace := newErr.ErrorStack()
|
|
app.Log.Error(stackTrace)
|
|
|
|
log.Fatalf("%s: %s\n\n%s", common.Tr.ErrorOccurred, constants.Links.Issues, stackTrace)
|
|
}
|
|
}
|
|
|
|
func NewCommon(config config.AppConfigurer) (*common.Common, error) {
|
|
userConfig := config.GetUserConfig()
|
|
appState := config.GetAppState()
|
|
log := newLogger(config)
|
|
// Initialize with English for the time being; the real translation set for
|
|
// the configured language will be read after reading the user config
|
|
tr := i18n.EnglishTranslationSet()
|
|
|
|
cmn := &common.Common{
|
|
Log: log,
|
|
Tr: tr,
|
|
AppState: appState,
|
|
Debug: config.GetDebug(),
|
|
Fs: afero.NewOsFs(),
|
|
}
|
|
cmn.SetUserConfig(userConfig)
|
|
return cmn, nil
|
|
}
|
|
|
|
func newLogger(cfg config.AppConfigurer) *logrus.Entry {
|
|
if cfg.GetDebug() {
|
|
logPath, err := config.LogPath()
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
return logs.NewDevelopmentLogger(logPath)
|
|
}
|
|
|
|
return logs.NewProductionLogger()
|
|
}
|
|
|
|
// NewApp bootstrap a new application
|
|
func NewApp(config config.AppConfigurer, test integrationTypes.IntegrationTest, common *common.Common) (*App, error) {
|
|
app := &App{
|
|
closers: []io.Closer{},
|
|
Config: config,
|
|
Common: common,
|
|
}
|
|
|
|
app.OSCommand = oscommands.NewOSCommand(common, config, oscommands.GetPlatform(), oscommands.NewNullGuiIO(app.Log))
|
|
|
|
updater, err := updates.NewUpdater(common, config, app.OSCommand)
|
|
if err != nil {
|
|
return app, err
|
|
}
|
|
|
|
dirName, err := os.Getwd()
|
|
if err != nil {
|
|
return app, err
|
|
}
|
|
|
|
gitVersion, err := app.validateGitVersion()
|
|
if err != nil {
|
|
return app, err
|
|
}
|
|
|
|
// If we're not in a repo, GetRepoPaths will return an error. The error is moot for us
|
|
// at this stage, since we'll try to init a new repo in setupRepo(), below
|
|
repoPaths, err := git_commands.GetRepoPaths(app.OSCommand.Cmd, gitVersion)
|
|
if err != nil {
|
|
common.Log.Infof("Error getting repo paths: %v", err)
|
|
}
|
|
|
|
showRecentRepos, err := app.setupRepo(repoPaths)
|
|
if err != nil {
|
|
return app, err
|
|
}
|
|
|
|
// used for testing purposes
|
|
if os.Getenv("SHOW_RECENT_REPOS") == "true" {
|
|
showRecentRepos = true
|
|
}
|
|
|
|
app.Gui, err = gui.NewGui(common, config, gitVersion, updater, showRecentRepos, dirName, test)
|
|
if err != nil {
|
|
return app, err
|
|
}
|
|
return app, nil
|
|
}
|
|
|
|
const minGitVersionStr = "2.32.0"
|
|
|
|
func minGitVersionErrorMessage(tr *i18n.TranslationSet) string {
|
|
return fmt.Sprintf(tr.MinGitVersionError, minGitVersionStr)
|
|
}
|
|
|
|
func (app *App) validateGitVersion() (*git_commands.GitVersion, error) {
|
|
version, err := git_commands.GetGitVersion(app.OSCommand)
|
|
// if we get an error anywhere here we'll show the same status
|
|
minVersionError := errors.New(minGitVersionErrorMessage(app.Tr))
|
|
if err != nil {
|
|
return nil, minVersionError
|
|
}
|
|
|
|
minRequiredVersion, _ := git_commands.ParseGitVersion(minGitVersionStr)
|
|
if version.IsOlderThanVersion(minRequiredVersion) {
|
|
return nil, minVersionError
|
|
}
|
|
|
|
return version, nil
|
|
}
|
|
|
|
func isDirectoryAGitRepository(dir string) (bool, error) {
|
|
info, err := os.Stat(filepath.Join(dir, ".git"))
|
|
return info != nil, err
|
|
}
|
|
|
|
func openRecentRepo(app *App) bool {
|
|
for _, repoDir := range app.Config.GetAppState().RecentRepos {
|
|
if isRepo, _ := isDirectoryAGitRepository(repoDir); isRepo {
|
|
if err := os.Chdir(repoDir); err == nil {
|
|
// We're still in setup, before the gui exists, so we can't show the approval popup
|
|
// that DispatchSwitchTo offers for blocked .envrc files; just log and move on.
|
|
// Also, the logs only go to the debug log, not the Command Log, because that's not
|
|
// available yet, either.
|
|
result := direnv.Load(app.OSCommand.Cmd)
|
|
if result.Message != "" {
|
|
app.Log.WithField("message", result.Message).Info("direnv")
|
|
}
|
|
if result.Err != nil {
|
|
app.Log.WithError(result.Err).Warn("direnv load failed")
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (app *App) setupRepo(
|
|
repoPaths *git_commands.RepoPaths,
|
|
) (bool, error) {
|
|
if env.GetGitDirEnv() != "" {
|
|
// we've been given the git dir directly. Skip setup
|
|
return false, nil
|
|
}
|
|
|
|
// if we are not in a git repo, we ask if we want to `git init`
|
|
if repoPaths == nil {
|
|
cwd, err := os.Getwd()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
if isRepo, err := isDirectoryAGitRepository(cwd); isRepo {
|
|
return false, err
|
|
}
|
|
|
|
var shouldInitRepo bool
|
|
initialBranchArg := ""
|
|
switch app.UserConfig().NotARepository {
|
|
case "prompt":
|
|
// Offer to initialize a new repository in current directory.
|
|
fmt.Print(app.Tr.CreateRepo)
|
|
response, _ := bufio.NewReader(os.Stdin).ReadString('\n')
|
|
shouldInitRepo = (strings.Trim(response, " \r\n") == "y")
|
|
if shouldInitRepo {
|
|
// Ask for the initial branch name
|
|
fmt.Print(app.Tr.InitialBranch)
|
|
response, _ := bufio.NewReader(os.Stdin).ReadString('\n')
|
|
if trimmedResponse := strings.Trim(response, " \r\n"); len(trimmedResponse) > 0 {
|
|
initialBranchArg += "--initial-branch=" + trimmedResponse
|
|
}
|
|
}
|
|
case "create":
|
|
shouldInitRepo = true
|
|
case "skip":
|
|
shouldInitRepo = false
|
|
case "quit":
|
|
fmt.Fprintln(os.Stderr, app.Tr.NotARepository)
|
|
os.Exit(1)
|
|
default:
|
|
fmt.Fprintln(os.Stderr, app.Tr.IncorrectNotARepository)
|
|
os.Exit(1)
|
|
}
|
|
|
|
if shouldInitRepo {
|
|
args := []string{"git", "init"}
|
|
if initialBranchArg != "" {
|
|
args = append(args, initialBranchArg)
|
|
}
|
|
if err := app.OSCommand.Cmd.New(args).Run(); err != nil {
|
|
return false, err
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
// check if we have a recent repo we can open
|
|
if openRecentRepo(app) {
|
|
return true, nil
|
|
}
|
|
|
|
fmt.Fprintln(os.Stderr, app.Tr.NoRecentRepositories)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Run this afterward so that the previous repo creation steps can run without this interfering
|
|
if repoPaths.IsBareRepo() {
|
|
|
|
fmt.Print(app.Tr.BareRepo)
|
|
|
|
response, _ := bufio.NewReader(os.Stdin).ReadString('\n')
|
|
|
|
if shouldOpenRecent := strings.Trim(response, " \r\n") == "y"; !shouldOpenRecent {
|
|
os.Exit(0)
|
|
}
|
|
|
|
if openRecentRepo(app) {
|
|
return true, nil
|
|
}
|
|
|
|
fmt.Println(app.Tr.NoRecentRepositories)
|
|
os.Exit(1)
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
func (app *App) Run(startArgs appTypes.StartArgs) error {
|
|
err := app.Gui.RunAndHandleError(startArgs)
|
|
return err
|
|
}
|
|
|
|
// Close closes any resources
|
|
func (app *App) Close() error {
|
|
for _, closer := range app.closers {
|
|
if err := closer.Close(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|