How Lazygit Manages State When Switching Between Repositories

Lazygit maintains a separate GUI state object for each repository in a RepoStateMap and restores the appropriate state instantly when users switch repos, preserving scroll positions, selections, and UI context.

When working across multiple git repositories, developers need their terminal UI to remember exactly where they left off. This article explains how lazygit manages state when switching between repositories using a sophisticated per-repo state caching system implemented in the pkg/gui package.

Per-Repository State Architecture

The GuiRepoState Container

All UI data—including panel selections, scroll positions, search states, and mode objects—is encapsulated in a GuiRepoState instance. Each state object is bound to a specific repository identifier defined as type Repo string within the lazygit source.

The RepoStateMap Registry

The central Gui struct maintains a lookup table that associates repository paths with their corresponding state objects. In pkg/gui/gui.go, the map is defined as:

type Gui struct {
    // …
    RepoStateMap map[Repo]*GuiRepoState
}

When a user opens a repository for the first time, lazygit creates a new GuiRepoState and stores it in this map. Subsequent switches retrieve the existing entry, ensuring no UI context is lost between sessions.

The Repository Switching Workflow

Entry Point: onSwitchToNewRepo

The switching process begins in onSwitchToNewRepo, which delegates to onNewRepo to reinitialize git commands before resetting the UI state. From pkg/gui/gui.go:

func (gui *Gui) onSwitchToNewRepo(startArgs appTypes.StartArgs, contextKey types.ContextKey) error {
    return gui.onNewRepo(startArgs, contextKey)
}

State Restoration Logic in resetState

The resetState method in pkg/gui/gui.go determines whether to reuse cached state or initialize fresh UI components:

func (gui *Gui) resetState(startArgs appTypes.StartArgs) types.Context {
    worktreePath := gui.git.RepoPaths.WorktreePath()
    if state := gui.RepoStateMap[Repo(worktreePath)]; state != nil {
        // Re‑use stored state
        gui.State = state
        // …reset a few transient fields
        return gui.c.Context().Current()
    }
    // …otherwise create a brand‑new GuiRepoState
}

When a stored state exists, the function clears transient UI flags (such as lingering popups) and returns the previously active context, effectively teleporting the user back to their exact previous position in that repository.

Tracking Repository Paths with RepoPathStack

Beyond state caching, lazygit tracks the navigation hierarchy using a thread-safe RepoPathStack. Defined in pkg/gui/gui.go as:

RepoPathStack *utils.StringStack

This stack records movement through submodules and worktrees. Controllers in pkg/gui/controllers/helpers/repos_helper.go push and pop entries when entering or leaving nested repositories, ensuring the correct path is used for state lookups in RepoStateMap.

Practical Implementation Examples

Switching Repositories Programmatically

To switch repos from a custom command or test:

func switchToRepo(gui *gui.Gui, repoPath string) error {
    startArgs := appTypes.StartArgs{}
    gui.git.RepoPaths = git_commands.MockRepoPaths(repoPath)
    return gui.onSwitchToNewRepo(startArgs, context.NO_CONTEXT)
}

Inspecting Cached State

Debug or inspect stored state for a specific repository:

func printRepoState(gui *gui.Gui, repoPath string) {
    key := gui.RepoStateMap[gui.Repo(Repo(repoPath))]
    if key == nil {
        fmt.Println("No state cached for:", repoPath)
        return
    }
    fmt.Printf("State for %s – current view: %s, selected line: %d\n",
        repoPath,
        key.CurrentViewName(),
        key.Contexts.Files.GetSelectedLine(),
    )
}

Forcing State Reinitialization

Clear cached state to force a fresh UI rebuild:

func forceReinit(gui *gui.Gui) error {
    delete(gui.RepoStateMap, Repo(gui.git.RepoPaths.WorktreePath()))
    return gui.resetState(appTypes.StartArgs{})
}

Key Source Files

Summary

  • Lazygit caches a complete GuiRepoState object for every repository in RepoStateMap
  • The resetState function checks RepoStateMap using the worktree path to restore previous UI context
  • Repository switching is instantaneous because the UI reuses existing state objects rather than rebuilding them
  • The RepoPathStack maintains navigation history for complex submodule and worktree hierarchies
  • All state management logic resides in the pkg/gui package with clear separation between state storage and switching workflows

Frequently Asked Questions

Does lazygit preserve my scroll position when switching back to a previous repository?

Yes. Because lazygit stores the complete GuiRepoState object—including the selected line index and view context—in RepoStateMap, returning to a repository restores your exact scroll position and cursor location instantly without requiring a full refresh.

How does lazygit handle state for git worktrees?

Each worktree is treated as a distinct repository entry in RepoStateMap because the map keys are derived from WorktreePath() as implemented in pkg/commands/git_commands/repo_paths.go. This ensures separate UI states for each worktree of the same repository.

What happens to open popups or modal dialogs when I switch repositories?

The resetState function clears transient UI flags and popup states when restoring a cached state, ensuring that lingering modals from a previous session do not persist when you return to a repository.

Can I manually clear the cached state for a specific repository?

Yes. You can delete the entry from RepoStateMap using the worktree path as the key, then call resetState to force lazygit to rebuild the UI state from scratch, as shown in the forceReinit example above.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →