diff --git a/pkg/app/errors.go b/pkg/app/errors.go index ee24cff49..8c6eb4bbf 100644 --- a/pkg/app/errors.go +++ b/pkg/app/errors.go @@ -3,6 +3,8 @@ package app import ( "strings" + "github.com/go-errors/errors" + "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/i18n" "github.com/samber/lo" ) @@ -14,6 +16,11 @@ type errorMapping struct { // knownError takes an error and tells us whether it's an error that we know about where we can print a nicely formatted version of it rather than panicking with a stack trace func knownError(tr *i18n.TranslationSet, err error) (string, bool) { + var quitWithMessage types.ErrQuitWithMessage + if errors.As(err, &quitWithMessage) { + return quitWithMessage.Message, true + } + errorMessage := err.Error() knownErrorMessages := []string{minGitVersionErrorMessage(tr), tr.BareRepoNotSupported} diff --git a/pkg/commands/git_commands/repo_paths.go b/pkg/commands/git_commands/repo_paths.go index 088dd86b6..9850b5a4f 100644 --- a/pkg/commands/git_commands/repo_paths.go +++ b/pkg/commands/git_commands/repo_paths.go @@ -81,6 +81,19 @@ func (self *RepoPaths) GitLocationEnvVars() []string { return self.gitLocationEnvVars } +// Returns the path of the worktree or of the worktree's git dir if it no longer +// exists. This happens if the repo or the worktree is deleted while lazygit is +// running. +func (self *RepoPaths) MissingDir() (string, bool) { + for _, path := range []string{self.worktreePath, self.worktreeGitDirPath} { + if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) { + return path, true + } + } + + return "", false +} + // Returns the repo paths for a typical repo func MockRepoPaths(currentPath string) *RepoPaths { return &RepoPaths{ diff --git a/pkg/commands/git_commands/repo_paths_test.go b/pkg/commands/git_commands/repo_paths_test.go index c7b7705b4..683c41b81 100644 --- a/pkg/commands/git_commands/repo_paths_test.go +++ b/pkg/commands/git_commands/repo_paths_test.go @@ -2,6 +2,8 @@ package git_commands import ( "fmt" + "os" + "path/filepath" "runtime" "strings" "testing" @@ -314,3 +316,45 @@ func TestGetRepoPaths(t *testing.T) { }) } } + +func TestMissingDir(t *testing.T) { + scenarios := []struct { + name string + deletedDir func(repoPaths *RepoPaths) string + }{ + { + name: "nothing deleted", + deletedDir: nil, + }, + { + name: "worktree deleted", + deletedDir: (*RepoPaths).WorktreePath, + }, + { + name: "git dir deleted", + deletedDir: (*RepoPaths).WorktreeGitDirPath, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + root := t.TempDir() + repoPaths := &RepoPaths{ + worktreePath: filepath.Join(root, "worktree"), + worktreeGitDirPath: filepath.Join(root, "repo", ".git", "worktrees", "worktree"), + } + assert.NoError(t, os.MkdirAll(repoPaths.WorktreePath(), 0o755)) + assert.NoError(t, os.MkdirAll(repoPaths.WorktreeGitDirPath(), 0o755)) + + expectedPath := "" + if s.deletedDir != nil { + expectedPath = s.deletedDir(repoPaths) + assert.NoError(t, os.RemoveAll(expectedPath)) + } + + path, missing := repoPaths.MissingDir() + assert.Equal(t, expectedPath, path) + assert.Equal(t, expectedPath != "", missing) + }) + } +} diff --git a/pkg/gui/controllers/helpers/refresh_helper.go b/pkg/gui/controllers/helpers/refresh_helper.go index 1f27221d8..826d12400 100644 --- a/pkg/gui/controllers/helpers/refresh_helper.go +++ b/pkg/gui/controllers/helpers/refresh_helper.go @@ -226,6 +226,24 @@ func (self *RefreshHelper) performRefresh(options types.RefreshOptions, calledFr return } + // If the repo or worktree was deleted, there's nothing we can show any + // more, and every git command would fail. + if path, missing := env.git.RepoPaths.MissingDir(); missing { + self.c.OnUIThread(func() error { + if self.c.State().GetRepoGeneration() != env.generation { + // We have switched away from the deleted worktree + return nil + } + + return types.ErrQuitWithMessage{ + Message: utils.ResolvePlaceholderString( + self.c.Tr.RepoDirNoLongerExists, map[string]string{"path": path}, + ), + } + }) + return + } + // A RefreshBlockingInput caller wants keyboard input withheld until the // refreshed state is in place (see IGuiCommon.RefreshBlockingInput). Begin // the block synchronously here in the calling handler, so that no keypress diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 6f1a04875..803980c50 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -1046,7 +1046,7 @@ func (gui *Gui) RunAndHandleError(startArgs appTypes.StartArgs) error { close(gui.stopChan) - if errors.Is(err, gocui.ErrQuit) { + if errors.Is(err, gocui.ErrQuit) && !errors.As(err, &types.ErrQuitWithMessage{}) { if gui.c.State().GetRetainOriginalDir() { if err := gui.helpers.RecordDirectory.RecordDirectory(gui.InitialDir); err != nil { return err diff --git a/pkg/gui/types/common.go b/pkg/gui/types/common.go index 6f77c7856..7665e96e6 100644 --- a/pkg/gui/types/common.go +++ b/pkg/gui/types/common.go @@ -490,3 +490,17 @@ const ( SCREEN_HALF SCREEN_FULL ) + +// Returning this error from a handler quits lazygit like gocui.ErrQuit does, +// and then prints the message in the terminal. +type ErrQuitWithMessage struct { + Message string +} + +func (e ErrQuitWithMessage) Error() string { + return e.Message +} + +func (e ErrQuitWithMessage) Unwrap() error { + return gocui.ErrQuit +} diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 1cb516631..0fb388216 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -561,6 +561,7 @@ type TranslationSet struct { StashOptions string NotARepository string WorkingDirectoryDoesNotExist string + RepoDirNoLongerExists string ScrollLeft string ScrollRight string DiscardPatch string @@ -1759,6 +1760,7 @@ func EnglishTranslationSet() *TranslationSet { StashOptions: "Stash options", NotARepository: "Error: must be run inside a git repository", WorkingDirectoryDoesNotExist: "Error: the current working directory does not exist", + RepoDirNoLongerExists: "Error: {{.path}} no longer exists", ScrollLeft: "Scroll left", ScrollRight: "Scroll right", DiscardPatch: "Discard patch",