mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-11-06 13:00:59 -04:00
Quit with an error message when the repo or worktree is deleted
If the repo or linked worktree that lazygit has open is deleted while lazygit is running, every git command fails. lazygit no longer panics since the previous commit, but it shows empty lists, and there's nothing useful left to do in it. Check at the start of every refresh whether the worktree and its git dir still exist. If one of them is gone, quit and print its path in the terminal. The background files refresh does this check too, so lazygit quits within ten seconds even if nobody is using it. To quit from a refresh, return an error that wraps gocui.ErrQuit; this keeps it out of the error popup. Switching to another worktree or to a recent repo would be an alternative, but it would be more surprising than quitting. Also, there isn't always a main worktree to switch to. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
787a309b3a
commit
4b61c4b650
@@ -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}
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user