From b4afe60bf94ff013d86b11c1d93fc8ee7204829c Mon Sep 17 00:00:00 2001
From: Jesse Duffield <jessedduffield@gmail.com>
Date: Sat, 3 Apr 2021 13:43:43 +1100
Subject: [PATCH] switching repos without restarting the gui

---
 pkg/app/app.go                       |   2 +-
 pkg/gui/arrangement.go               |  14 +++-
 pkg/gui/context.go                   | 107 ++++++++++++++++++---------
 pkg/gui/errors.go                    |   8 +-
 pkg/gui/filetree/commit_file_node.go |   4 +
 pkg/gui/filetree/file_node.go        |   4 +
 pkg/gui/gui.go                       | 104 +++++++++++++-------------
 pkg/gui/keybindings.go               |   4 +-
 pkg/gui/layout.go                    |  20 +++--
 pkg/gui/quitting.go                  |   4 +-
 pkg/gui/recent_repos_panel.go        |  14 +++-
 pkg/gui/submodules_panel.go          |   2 +-
 pkg/utils/utils.go                   |   8 +-
 13 files changed, 182 insertions(+), 113 deletions(-)

diff --git a/pkg/app/app.go b/pkg/app/app.go
index 7a53b2be5..a624a0d30 100644
--- a/pkg/app/app.go
+++ b/pkg/app/app.go
@@ -239,7 +239,7 @@ func (app *App) Run() error {
 		os.Exit(0)
 	}
 
-	err := app.Gui.RunWithRestarts()
+	err := app.Gui.RunAndHandleError()
 	return err
 }
 
diff --git a/pkg/gui/arrangement.go b/pkg/gui/arrangement.go
index 3c6cbc5b9..422941243 100644
--- a/pkg/gui/arrangement.go
+++ b/pkg/gui/arrangement.go
@@ -181,9 +181,12 @@ func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map
 // the default behaviour when accordian mode is NOT in effect. If it is in effect
 // then when it's accessed it will have weight 2, not 1.
 func (gui *Gui) getDefaultStashWindowBox() *boxlayout.Box {
+	gui.State.ContextManager.Lock()
+	defer gui.State.ContextManager.Unlock()
+
 	box := &boxlayout.Box{Window: "stash"}
 	stashWindowAccessed := false
-	for _, context := range gui.State.ContextStack {
+	for _, context := range gui.State.ContextManager.ContextStack {
 		if context.GetWindowName() == "stash" {
 			stashWindowAccessed = true
 		}
@@ -278,9 +281,12 @@ func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box {
 
 func (gui *Gui) currentSideWindowName() string {
 	// there is always one and only one cyclable context in the context stack. We'll look from top to bottom
-	for idx := range gui.State.ContextStack {
-		reversedIdx := len(gui.State.ContextStack) - 1 - idx
-		context := gui.State.ContextStack[reversedIdx]
+	gui.State.ContextManager.Lock()
+	defer gui.State.ContextManager.Unlock()
+
+	for idx := range gui.State.ContextManager.ContextStack {
+		reversedIdx := len(gui.State.ContextManager.ContextStack) - 1 - idx
+		context := gui.State.ContextManager.ContextStack[reversedIdx]
 
 		if context.GetKind() == SIDE_CONTEXT {
 			return context.GetWindowName()
diff --git a/pkg/gui/context.go b/pkg/gui/context.go
index 8b2015711..6d8492411 100644
--- a/pkg/gui/context.go
+++ b/pkg/gui/context.go
@@ -296,7 +296,18 @@ func (gui *Gui) initialViewContextMap() map[string]Context {
 	}
 }
 
-func (gui *Gui) viewTabContextMap() map[string][]tabContext {
+func (gui *Gui) popupViewNames() []string {
+	result := []string{}
+	for _, context := range gui.allContexts() {
+		if context.GetKind() == PERSISTENT_POPUP || context.GetKind() == TEMPORARY_POPUP {
+			result = append(result, context.GetViewName())
+		}
+	}
+
+	return result
+}
+
+func (gui *Gui) initialViewTabContextMap() map[string][]tabContext {
 	return map[string][]tabContext{
 		"branches": {
 			{
@@ -343,7 +354,10 @@ func (gui *Gui) viewTabContextMap() map[string][]tabContext {
 }
 
 func (gui *Gui) currentContextKeyIgnoringPopups() string {
-	stack := gui.State.ContextStack
+	gui.State.ContextManager.Lock()
+	defer gui.State.ContextManager.Unlock()
+
+	stack := gui.State.ContextManager.ContextStack
 
 	for i := range stack {
 		reversedIndex := len(stack) - 1 - i
@@ -361,11 +375,14 @@ func (gui *Gui) currentContextKeyIgnoringPopups() string {
 // hitting escape: you want to go that context's parent instead.
 func (gui *Gui) replaceContext(c Context) error {
 	gui.g.Update(func(*gocui.Gui) error {
-		if len(gui.State.ContextStack) == 0 {
-			gui.State.ContextStack = []Context{c}
+		gui.State.ContextManager.Lock()
+		defer gui.State.ContextManager.Unlock()
+
+		if len(gui.State.ContextManager.ContextStack) == 0 {
+			gui.State.ContextManager.ContextStack = []Context{c}
 		} else {
 			// replace the last item with the given item
-			gui.State.ContextStack = append(gui.State.ContextStack[0:len(gui.State.ContextStack)-1], c)
+			gui.State.ContextManager.ContextStack = append(gui.State.ContextManager.ContextStack[0:len(gui.State.ContextManager.ContextStack)-1], c)
 		}
 
 		return gui.activateContext(c)
@@ -376,28 +393,37 @@ func (gui *Gui) replaceContext(c Context) error {
 
 func (gui *Gui) pushContext(c Context) error {
 	gui.g.Update(func(*gocui.Gui) error {
-		// push onto stack
-		// if we are switching to a side context, remove all other contexts in the stack
-		if c.GetKind() == SIDE_CONTEXT {
-			for _, stackContext := range gui.State.ContextStack {
-				if stackContext.GetKey() != c.GetKey() {
-					if err := gui.deactivateContext(stackContext); err != nil {
-						return err
-					}
-				}
-			}
-			gui.State.ContextStack = []Context{c}
-		} else {
-			// TODO: think about other exceptional cases
-			gui.State.ContextStack = append(gui.State.ContextStack, c)
-		}
+		gui.State.ContextManager.Lock()
+		defer gui.State.ContextManager.Unlock()
 
-		return gui.activateContext(c)
+		return gui.pushContextDirect(c)
 	})
 
 	return nil
 }
 
+func (gui *Gui) pushContextDirect(c Context) error {
+	// push onto stack
+	// if we are switching to a side context, remove all other contexts in the stack
+	if c.GetKind() == SIDE_CONTEXT {
+		for _, stackContext := range gui.State.ContextManager.ContextStack {
+			if stackContext.GetKey() != c.GetKey() {
+				if err := gui.deactivateContext(stackContext); err != nil {
+					return err
+				}
+			}
+		}
+		gui.State.ContextManager.ContextStack = []Context{c}
+	} else {
+		// TODO: think about other exceptional cases
+		gui.State.ContextManager.ContextStack = append(gui.State.ContextManager.ContextStack, c)
+	}
+
+	return gui.activateContext(c)
+}
+
+// asynchronous code idea: functions return an error via a channel, when done
+
 // pushContextWithView is to be used when you don't know which context you
 // want to switch to: you only know the view that you want to switch to. It will
 // look up the context currently active for that view and switch to that context
@@ -407,19 +433,20 @@ func (gui *Gui) pushContextWithView(viewName string) error {
 
 func (gui *Gui) returnFromContext() error {
 	gui.g.Update(func(*gocui.Gui) error {
-		// TODO: add mutexes
+		gui.State.ContextManager.Lock()
+		defer gui.State.ContextManager.Unlock()
 
-		if len(gui.State.ContextStack) == 1 {
+		if len(gui.State.ContextManager.ContextStack) == 1 {
 			// cannot escape from bottommost context
 			return nil
 		}
 
-		n := len(gui.State.ContextStack) - 1
+		n := len(gui.State.ContextManager.ContextStack) - 1
 
-		currentContext := gui.State.ContextStack[n]
-		newContext := gui.State.ContextStack[n-1]
+		currentContext := gui.State.ContextManager.ContextStack[n]
+		newContext := gui.State.ContextManager.ContextStack[n-1]
 
-		gui.State.ContextStack = gui.State.ContextStack[:n]
+		gui.State.ContextManager.ContextStack = gui.State.ContextManager.ContextStack[:n]
 
 		if err := gui.deactivateContext(currentContext); err != nil {
 			return err
@@ -529,24 +556,30 @@ func (gui *Gui) activateContext(c Context) error {
 }
 
 // currently unused
-// func (gui *Gui) renderContextStack() string {
-// 	result := ""
-// 	for _, context := range gui.State.ContextStack {
-// 		result += context.GetKey() + "\n"
-// 	}
-// 	return result
-// }
+func (gui *Gui) renderContextStack() string {
+	result := ""
+	for _, context := range gui.State.ContextManager.ContextStack {
+		result += context.GetKey() + "\n"
+	}
+	return result
+}
 
 func (gui *Gui) currentContext() Context {
-	if len(gui.State.ContextStack) == 0 {
+	gui.State.ContextManager.Lock()
+	defer gui.State.ContextManager.Unlock()
+
+	if len(gui.State.ContextManager.ContextStack) == 0 {
 		return gui.defaultSideContext()
 	}
 
-	return gui.State.ContextStack[len(gui.State.ContextStack)-1]
+	return gui.State.ContextManager.ContextStack[len(gui.State.ContextManager.ContextStack)-1]
 }
 
 func (gui *Gui) currentSideContext() *ListContext {
-	stack := gui.State.ContextStack
+	gui.State.ContextManager.Lock()
+	defer gui.State.ContextManager.Unlock()
+
+	stack := gui.State.ContextManager.ContextStack
 
 	// on startup the stack can be empty so we'll return an empty string in that case
 	if len(stack) == 0 {
diff --git a/pkg/gui/errors.go b/pkg/gui/errors.go
index 651b964c2..c275fcdcc 100644
--- a/pkg/gui/errors.go
+++ b/pkg/gui/errors.go
@@ -5,9 +5,7 @@ import "github.com/go-errors/errors"
 // SentinelErrors are the errors that have special meaning and need to be checked
 // by calling functions. The less of these, the better
 type SentinelErrors struct {
-	ErrNoFiles    error
-	ErrSwitchRepo error
-	ErrRestart    error
+	ErrNoFiles error
 }
 
 const UNKNOWN_VIEW_ERROR_MSG = "unknown view"
@@ -24,14 +22,12 @@ const UNKNOWN_VIEW_ERROR_MSG = "unknown view"
 // localising things in the code.
 func (gui *Gui) GenerateSentinelErrors() {
 	gui.Errors = SentinelErrors{
-		ErrNoFiles:    errors.New(gui.Tr.NoChangedFiles),
-		ErrSwitchRepo: errors.New("switching repo"),
+		ErrNoFiles: errors.New(gui.Tr.NoChangedFiles),
 	}
 }
 
 func (gui *Gui) sentinelErrorsArr() []error {
 	return []error{
 		gui.Errors.ErrNoFiles,
-		gui.Errors.ErrSwitchRepo,
 	}
 }
diff --git a/pkg/gui/filetree/commit_file_node.go b/pkg/gui/filetree/commit_file_node.go
index b6fd5ea55..91aba14c0 100644
--- a/pkg/gui/filetree/commit_file_node.go
+++ b/pkg/gui/filetree/commit_file_node.go
@@ -107,6 +107,10 @@ func (n *CommitFileNode) Flatten(collapsedPaths map[string]bool) []*CommitFileNo
 }
 
 func (node *CommitFileNode) GetNodeAtIndex(index int, collapsedPaths map[string]bool) *CommitFileNode {
+	if node == nil {
+		return nil
+	}
+
 	return getNodeAtIndex(node, index, collapsedPaths).(*CommitFileNode)
 }
 
diff --git a/pkg/gui/filetree/file_node.go b/pkg/gui/filetree/file_node.go
index dfda68f29..649578fb0 100644
--- a/pkg/gui/filetree/file_node.go
+++ b/pkg/gui/filetree/file_node.go
@@ -93,6 +93,10 @@ func (n *FileNode) Flatten(collapsedPaths map[string]bool) []*FileNode {
 }
 
 func (node *FileNode) GetNodeAtIndex(index int, collapsedPaths map[string]bool) *FileNode {
+	if node == nil {
+		return nil
+	}
+
 	return getNodeAtIndex(node, index, collapsedPaths).(*FileNode)
 }
 
diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go
index a9343d115..0a7770a01 100644
--- a/pkg/gui/gui.go
+++ b/pkg/gui/gui.go
@@ -48,6 +48,18 @@ const StartupPopupVersion = 3
 // OverlappingEdges determines if panel edges overlap
 var OverlappingEdges = false
 
+type ContextManager struct {
+	ContextStack []Context
+	sync.Mutex
+}
+
+func NewContextManager(contexts ContextTree) ContextManager {
+	return ContextManager{
+		ContextStack: []Context{contexts.Files},
+		Mutex:        sync.Mutex{},
+	}
+}
+
 // Gui wraps the gocui Gui object which handles rendering and events
 type Gui struct {
 	g                    *gocui.Gui
@@ -83,6 +95,10 @@ type Gui struct {
 	// findSuggestions will take a string that the user has typed into a prompt
 	// and return a slice of suggestions which match that string.
 	findSuggestions func(string) []*types.Suggestion
+
+	// when you enter into a submodule we'll append the superproject's path to this array
+	// so that you can return to the superproject
+	RepoPathStack []string
 }
 
 type RecordedEvent struct {
@@ -298,7 +314,7 @@ type guiState struct {
 
 	Modes Modes
 
-	ContextStack   []Context
+	ContextManager ContextManager
 	ViewContextMap map[string]Context
 
 	// WindowViewNameMap is a mapping of windows to the current view of that window.
@@ -306,35 +322,18 @@ type guiState struct {
 	// side windows we need to know which view to give focus to for a given window
 	WindowViewNameMap map[string]string
 
-	// when you enter into a submodule we'll append the superproject's path to this array
-	// so that you can return to the superproject
-	RepoPathStack []string
+	// tells us whether we've set up our views. We only do this once per repo
+	ViewsSetup bool
 }
 
-func (gui *Gui) resetState() {
-	// we carry over the filter path and diff state
-	prevFiltering := filtering.NewFiltering()
-	prevDiff := Diffing{}
-	prevCherryPicking := CherryPicking{
-		CherryPickedCommits: make([]*models.Commit, 0),
-		ContextKey:          "",
-	}
-	prevRepoPathStack := []string{}
-	if gui.State != nil {
-		prevFiltering = gui.State.Modes.Filtering
-		prevDiff = gui.State.Modes.Diffing
-		prevCherryPicking = gui.State.Modes.CherryPicking
-		prevRepoPathStack = gui.State.RepoPathStack
-	}
-
-	modes := Modes{
-		Filtering:     prevFiltering,
-		CherryPicking: prevCherryPicking,
-		Diffing:       prevDiff,
-	}
-
+func (gui *Gui) resetState(filterPath string) {
 	showTree := gui.Config.GetUserConfig().Gui.ShowFileTree
 
+	screenMode := SCREEN_NORMAL
+	if filterPath != "" {
+		screenMode = SCREEN_HALF
+	}
+
 	gui.State = &guiState{
 		FileManager:           filetree.NewFileManager(make([]*models.File, 0), gui.Log, showTree),
 		CommitFileManager:     filetree.NewCommitFileManager(make([]*models.CommitFile, 0), gui.Log, showTree),
@@ -365,18 +364,22 @@ func (gui *Gui) resetState() {
 				ConflictsMutex: sync.Mutex{},
 			},
 		},
-		SideView:       nil,
-		Ptmx:           nil,
-		Modes:          modes,
+		SideView: nil,
+		Ptmx:     nil,
+		Modes: Modes{
+			Filtering: filtering.NewFiltering(),
+			CherryPicking: CherryPicking{
+				CherryPickedCommits: make([]*models.Commit, 0),
+				ContextKey:          "",
+			},
+			Diffing: Diffing{},
+		},
 		ViewContextMap: gui.initialViewContextMap(),
-		RepoPathStack:  prevRepoPathStack,
+		ScreenMode:     screenMode,
+		ContextManager: NewContextManager(gui.Contexts),
 	}
 
-	if gui.State.Modes.Filtering.Active() {
-		gui.State.ScreenMode = SCREEN_HALF
-	} else {
-		gui.State.ScreenMode = SCREEN_NORMAL
-	}
+	gui.ViewTabContextMap = gui.initialViewTabContextMap()
 }
 
 // for now the split view will always be on
@@ -393,12 +396,11 @@ func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *oscom
 		viewBufferManagerMap: map[string]*tasks.ViewBufferManager{},
 		showRecentRepos:      showRecentRepos,
 		RecordedEvents:       []RecordedEvent{},
+		RepoPathStack:        []string{},
 	}
 
-	gui.resetState()
-	gui.State.Modes.Filtering.SetPath(filterPath)
 	gui.Contexts = gui.contextTree()
-	gui.ViewTabContextMap = gui.viewTabContextMap()
+	gui.resetState(filterPath)
 
 	gui.watchFilesForChanges()
 
@@ -409,8 +411,6 @@ func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *oscom
 
 // Run setup the gui with keybindings and start the mainloop
 func (gui *Gui) Run() error {
-	gui.resetState()
-
 	recordEvents := recordingEvents()
 
 	g, err := gocui.NewGui(gocui.OutputTrue, OverlappingEdges, recordEvents)
@@ -457,6 +457,11 @@ func (gui *Gui) Run() error {
 		go utils.Safe(gui.startBackgroundFetch)
 	}
 
+	go func() {
+		gui.Updater.CheckForNewUpdate(gui.onBackgroundUpdateCheckFinish, false)
+		gui.waitForIntro.Done()
+	}()
+
 	gui.goEvery(time.Second*time.Duration(userConfig.Refresher.RefreshInterval), gui.stopChan, gui.refreshFilesAndSubmodules)
 
 	g.SetManager(gocui.ManagerFunc(gui.layout), gocui.ManagerFunc(gui.getFocusLayout()))
@@ -467,19 +472,17 @@ func (gui *Gui) Run() error {
 	return err
 }
 
-// RunWithRestarts loops, instantiating a new gocui.Gui with each iteration
-// (i.e. when switching repos or restarting). If it's a random error, we quit
-func (gui *Gui) RunWithRestarts() error {
+// RunAndHandleError
+func (gui *Gui) RunAndHandleError() error {
 	gui.StartTime = time.Now()
 	go utils.Safe(gui.replayRecordedEvents)
 
-	for {
-		gui.stopChan = make(chan struct{})
+	gui.stopChan = make(chan struct{})
+	return utils.SafeWithError(func() error {
 		if err := gui.Run(); err != nil {
 			for _, manager := range gui.viewBufferManagerMap {
 				manager.Close()
 			}
-			gui.viewBufferManagerMap = map[string]*tasks.ViewBufferManager{}
 
 			if !gui.fileWatcher.Disabled {
 				gui.fileWatcher.Watcher.Close()
@@ -500,13 +503,14 @@ func (gui *Gui) RunWithRestarts() error {
 				}
 
 				return nil
-			case gui.Errors.ErrSwitchRepo:
-				continue
+
 			default:
 				return err
 			}
 		}
-	}
+
+		return nil
+	})
 }
 
 func (gui *Gui) runSubprocessWithSuspense(subprocess *exec.Cmd) error {
@@ -551,11 +555,9 @@ func (gui *Gui) runSubprocess(subprocess *exec.Cmd) error {
 }
 
 func (gui *Gui) loadNewRepo() error {
-	gui.Updater.CheckForNewUpdate(gui.onBackgroundUpdateCheckFinish, false)
 	if err := gui.updateRecentRepoList(); err != nil {
 		return err
 	}
-	gui.waitForIntro.Done()
 
 	if err := gui.refreshSidePanels(refreshOptions{mode: ASYNC}); err != nil {
 		return err
diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go
index a2f762da6..3ebceaa9e 100644
--- a/pkg/gui/keybindings.go
+++ b/pkg/gui/keybindings.go
@@ -1706,7 +1706,7 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
 		bindings = append(bindings, &Binding{ViewName: "", Key: rune(i+1) + '0', Modifier: gocui.ModNone, Handler: gui.goToSideWindow(window)})
 	}
 
-	for viewName := range gui.viewTabContextMap() {
+	for viewName := range gui.initialViewTabContextMap() {
 		bindings = append(bindings, []*Binding{
 			{
 				ViewName:    viewName,
@@ -1741,7 +1741,7 @@ func (gui *Gui) keybindings() error {
 		}
 	}
 
-	for viewName := range gui.viewTabContextMap() {
+	for viewName := range gui.initialViewTabContextMap() {
 		viewName := viewName
 		tabClickCallback := func(tabIndex int) error { return gui.onViewTabClick(viewName, tabIndex) }
 
diff --git a/pkg/gui/layout.go b/pkg/gui/layout.go
index 32ad1de85..a60a137af 100644
--- a/pkg/gui/layout.go
+++ b/pkg/gui/layout.go
@@ -216,11 +216,6 @@ func (gui *Gui) layout(g *gocui.Gui) error {
 		}
 		v.Frame = false
 		v.FgColor = theme.OptionsColor
-
-		// doing this here because it'll only happen once
-		if err := gui.onInitialViewsCreation(); err != nil {
-			return err
-		}
 	}
 
 	// this view takes up one character. Its only purpose is to show the slash when searching
@@ -271,6 +266,14 @@ func (gui *Gui) layout(g *gocui.Gui) error {
 		gui.State.OldInformation = informationStr
 	}
 
+	if !gui.State.ViewsSetup {
+		if err := gui.onInitialViewsCreation(); err != nil {
+			return err
+		}
+
+		gui.State.ViewsSetup = true
+	}
+
 	if gui.g.CurrentView() == nil {
 		initialContext := gui.Contexts.Files
 		if gui.State.Modes.Filtering.Active() {
@@ -323,8 +326,13 @@ func (gui *Gui) layout(g *gocui.Gui) error {
 func (gui *Gui) onInitialViewsCreation() error {
 	gui.setInitialViewContexts()
 
-	// add tabs to views
+	// hide any popup views. This only applies when we've just switched repos
+	for _, viewName := range gui.popupViewNames() {
+		_, _ = gui.g.SetViewOnBottom(viewName)
+	}
+
 	gui.g.Mutexes.ViewsMutex.Lock()
+	// add tabs to views
 	for _, view := range gui.g.Views() {
 		tabs := gui.viewTabNames(view.Name())
 		if len(tabs) == 0 {
diff --git a/pkg/gui/quitting.go b/pkg/gui/quitting.go
index bd2f09f66..778db1a61 100644
--- a/pkg/gui/quitting.go
+++ b/pkg/gui/quitting.go
@@ -49,13 +49,13 @@ func (gui *Gui) handleTopLevelReturn() error {
 		}
 	}
 
-	repoPathStack := gui.State.RepoPathStack
+	repoPathStack := gui.RepoPathStack
 	if len(repoPathStack) > 0 {
 		n := len(repoPathStack) - 1
 
 		path := repoPathStack[n]
 
-		gui.State.RepoPathStack = repoPathStack[:n]
+		gui.RepoPathStack = repoPathStack[:n]
 
 		return gui.dispatchSwitchToRepo(path)
 	}
diff --git a/pkg/gui/recent_repos_panel.go b/pkg/gui/recent_repos_panel.go
index 691e63d68..c219e1001 100644
--- a/pkg/gui/recent_repos_panel.go
+++ b/pkg/gui/recent_repos_panel.go
@@ -5,6 +5,7 @@ import (
 	"path/filepath"
 
 	"github.com/fatih/color"
+	"github.com/jesseduffield/gocui"
 	"github.com/jesseduffield/lazygit/pkg/commands"
 	"github.com/jesseduffield/lazygit/pkg/env"
 	"github.com/jesseduffield/lazygit/pkg/utils"
@@ -24,6 +25,9 @@ func (gui *Gui) handleCreateRecentReposMenu() error {
 				yellow.Sprint(path),
 			},
 			onPress: func() error {
+				// if we were in a submodule, we want to forget about that stack of repos
+				// so that hitting escape in the new repo does nothing
+				gui.RepoPathStack = []string{}
 				return gui.dispatchSwitchToRepo(path)
 			},
 		}
@@ -73,8 +77,14 @@ func (gui *Gui) dispatchSwitchToRepo(path string) error {
 		return err
 	}
 	gui.GitCommand = newGitCommand
-	gui.State.Modes.Filtering.Reset()
-	return gui.Errors.ErrSwitchRepo
+
+	gui.g.Update(func(*gocui.Gui) error {
+		gui.resetState("")
+
+		return nil
+	})
+
+	return nil
 }
 
 // updateRecentRepoList registers the fact that we opened lazygit in this repo,
diff --git a/pkg/gui/submodules_panel.go b/pkg/gui/submodules_panel.go
index 8c1bcf76a..65ce2c2d5 100644
--- a/pkg/gui/submodules_panel.go
+++ b/pkg/gui/submodules_panel.go
@@ -71,7 +71,7 @@ func (gui *Gui) enterSubmodule(submodule *models.SubmoduleConfig) error {
 	if err != nil {
 		return err
 	}
-	gui.State.RepoPathStack = append(gui.State.RepoPathStack, wd)
+	gui.RepoPathStack = append(gui.RepoPathStack, wd)
 
 	return gui.dispatchSwitchToRepo(submodule.Path)
 }
diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go
index 339bcb0a9..945586001 100644
--- a/pkg/utils/utils.go
+++ b/pkg/utils/utils.go
@@ -365,6 +365,10 @@ func ResolveTemplate(templateStr string, object interface{}) (string, error) {
 // Safe will close tcell if a panic occurs so that we don't end up in a malformed
 // terminal state
 func Safe(f func()) {
+	_ = SafeWithError(func() error { f(); return nil })
+}
+
+func SafeWithError(f func() error) error {
 	panicking := true
 	defer func() {
 		if panicking && gocui.Screen != nil {
@@ -372,7 +376,9 @@ func Safe(f func()) {
 		}
 	}()
 
-	f()
+	err := f()
 
 	panicking = false
+
+	return err
 }