From Repository State to an Actionable View
A selection in lazygit is a question before it is an action. This chapter studies how lazygit turns one Git repository into several connected views, and how the application keeps a selection, a Git command, and refreshed feedback tied to the same underlying state. By the end, the reader should be able to tell whether they are merely inspecting a projection of the repository or asking Git to mutate it, and why that distinction is essential to safe keyboard-driven work.
The visible interface can make a repository look simpler than it is. A file may have changes in the working tree, changes already staged in the index, or both. A branch name is also a reference with an upstream relationship, divergence counts, and possibly a worktree elsewhere. A commit is an object in a graph, but during an interactive rebase the commit view can also include instructions from a TODO file. A yellow operation indicator can mean that Git is waiting for conflict resolution rather than waiting for another ordinary command. Lazygit is useful because it brings these related states into one navigable surface without pretending they are the same state.
One repository, several projections
The easiest way to understand lazygit is to begin with what a user can select. The Files view is a projection of the working tree and index. Its entries can describe tracked, untracked, added, deleted, renamed, staged, unstaged, or conflicted files. Those labels are not just visual decoration: they indicate which part of Git's state a later operation may affect. A file with both staged and unstaged changes answers two different questions at once—what is already prepared for a commit, and what remains only in the working copy—so lazygit can show the two differences separately in its main area.
The Commits view is a projection of the commit graph. Selecting a commit can reveal its changed files and patch; selecting a branch can show a branch-specific log and its position relative to other references. During a rebase, the visible list can include TODO actions such as edit, squash, or fixup. That is a different kind of item from an ordinary commit: it represents an instruction for a history operation, not a new object that has already been recorded in the graph. The interface has to preserve this distinction because “select this commit” and “change what Git will do with this commit” are separate steps.
Branches add another layer of meaning. The branch model includes whether a branch is currently checked out, whether the head is detached, which remote and branch it tracks, and how many commits it is ahead of or behind that upstream. The same branch may also be checked out in another worktree. A branch list therefore cannot be understood as a simple folder of names. It is a compact view of references and relationships that determine what checkout, merge, rebase, push, or worktree action will mean.
Stashes, remotes, tags, reflog entries, submodules, and the status view complete the picture. They are not all alternative ways of displaying the same object. A stash is a saved working-tree state; a remote is a place and a set of references used for synchronization; the reflog records movements of references over time; and the status view reports the repository and current operation in a form that can guide the next action. Lazygit makes these projections available through side panels and tabs so that a user can move from an entity to the evidence associated with it instead of remembering a separate command for every relationship.
That organization matters most when the selected item has consequences beyond its own list. Selecting a file does not stage it, discard it, or alter its contents. It changes focus and asks the main view to render information about that file. Selecting a commit does not rewrite history. It changes which commit is the subject of the diff and which commit-specific actions are available. Selecting a branch does not merge or push it. It changes the branch whose topology and history are being examined. The same key can therefore be safe in one context and consequential in another because the context determines the action's referent.
Lazygit's context system gives this relationship a runtime form. A context is associated with a view and the state and behavior needed for that view. Side contexts represent files, branches, commits, stash, and related lists; main contexts render selected content; persistent popups represent things such as a commit message; temporary popups provide menus, confirmations, and prompts. When a popup opens, a context manager pushes it over the current context. When the user dismisses it, the manager can restore the underlying view. This is more than a navigation convenience: it preserves the location from which an action was initiated while another interaction temporarily takes focus.
The distinction between a view and a context is useful here. A view is the rendered surface, holding the text that appears on screen. A context owns more of the meaning around that surface: which model it projects, which item is selected, how focus is handled, whether the view is searchable or filterable, and which controllers can respond to input. Controllers bind keys to actions, while helpers share operations across controllers. The repository's codebase guide notes that these responsibilities have evolved over time and that some historical overlap remains. The important user-facing consequence is not a perfect taxonomy; it is that selection, focus, action, and rendering are deliberately connected.
This diagram should not be read as a claim that lazygit has built a second repository database. The Git command boundary is the route through which the application reads and changes Git state. Models make that state usable to the UI; contexts and controllers make it selectable and actionable; views render it. The separation is practical rather than absolute, and the project documentation openly describes legacy code and a remaining large GUI structure. What matters for reasoning is the direction of responsibility: the screen is a projection, not the authority that defines the repository.
The path from a keypress to trustworthy feedback
A typical action crosses several boundaries. The user presses a key while a context has focus. A controller resolves that key in the context's keybinding set and invokes a handler. The handler may ask a helper to prepare a command, show a confirmation or prompt, and then call a Git command object. The command object ultimately runs Git with arguments and environment appropriate to the repository. After Git returns, lazygit refreshes the affected models and asks the relevant contexts and views to render them again.
This path explains why the project groups Git communication separately from UI code. The GitCommand façade collects command families and loaders for branches, commits, files, remotes, stashes, tags, worktrees, and other repository concerns. A common command builder composes arguments, diff settings, repository targeting, and lock behavior. The UI does not have to reconstruct the correct working-tree or Git-directory arguments every time a controller wants to inspect a branch or apply a patch. It asks a command service for a repository operation and receives a result that can be placed in the model.
Repository targeting is particularly important when the application is dealing with submodules or multiple worktrees. Commands are normally pinned to the repository that was open when the command object was built. A refresh worker can therefore keep addressing its original repository even if the process's current directory later changes. That does not make switching repositories invisible; it prevents an old operation from accidentally resolving its arguments against the new one. The distinction is between the application process's current location and the repository identity captured for a specific operation.
The refresh that follows an action is not a single redraw. Lazygit refreshes different scopes—files, submodules, branches, commits, reflog, remotes, worktrees, stash, status, and related views—according to what may have changed. Before Git work runs in a worker, the refresh helper captures the model and context inputs it needs on the UI thread. The worker computes from that snapshot, then sends model and view updates back to the UI thread. This avoids having a worker read mutable selection or context state while the user interface is changing it.
Selection continuity is part of this contract. A refresh can change the contents of a list or the operation state behind it, so blindly keeping the cursor or viewport can mislead. The refresh interface distinguishes ordinary refreshes from those that preserve scroll position, update only content, block input briefly, or originate from a background routine. The visible result is a small detail with a large effect: the next keypress should apply to the state the user thinks they are looking at.
Concurrency creates another identity problem. Suppose the user starts a refresh in one repository and switches to another before the Git commands finish. The old refresh may still return perfectly valid data—for the old repository. It would nevertheless be invalid to write that data into the new repository's model. Lazygit increments a repository-generation counter when switching repositories and carries the captured generation through refresh work. When the worker's update returns, the UI-side callback drops it if the generation no longer matches. The result is discarded not because the Git command failed, but because its answer is no longer relevant to the current screen.
Branch loading has a similar ordering hazard. Two loads can overlap, and the later-started load may finish first. A sequence number lets the UI accept only the newest applicable result rather than allowing an older completion to overwrite it. These mechanisms are easy to miss when looking only at a panel, but they explain why a fast interface must be judged by more than latency. A stale answer displayed confidently is worse than a slow answer that remains correctly associated with its repository.
The event loop and its worker model also define what “busy” means. The UI goroutine processes input and redraws the terminal. Worker goroutines perform user-triggered work such as Git commands and return results to the UI. Background goroutines periodically fetch or refresh; they are intentionally not counted as user-visible busy work, because a periodic routine would otherwise make the application appear busy continuously. Task tracking ensures that a user action is not declared finished while the processing it caused is still in flight. It also handles cases such as a Git command pausing while it waits for credentials.
That distinction gives the command log and inline status indicators a technical role. A long-running operation can be shown as in progress rather than leaving the user to guess whether the keypress was received. The point is not to hide work. It is to expose enough status that the user can tell whether they are waiting for Git, entering input, or looking at a completed state.
There is still a limit to what the architecture can prove. A model can accurately report that a file has no conflict markers; it cannot decide whether a human resolved the conflict correctly. A commit view can show that a rebase produced a new graph; it cannot decide whether the new order expresses the intended design. A refresh can establish that Git accepted an operation and can render the resulting state; it cannot turn that result into a semantic review. Lazygit represents and coordinates state. The user's interpretation remains outside the model.
A useful check after any action is therefore to ask three questions. What changed in Git—the working tree, index, references, or operation state? What changed only in the interface—focus, filtering, layout, or the main diff? And what evidence does the refreshed view provide, as opposed to what still requires reading the code, reviewing the graph, or checking the remote? If the answer to the first question is unclear, the action has crossed the most important boundary without enough understanding.
Lazygit's state-to-view design is valuable precisely because it makes that boundary easier to revisit. The selected entity gives an action a referent; the command layer gives it a repository-aware implementation; the refresh gives the user a new projection; and concurrency safeguards keep that projection attached to the right repository and point in time. Once this model is clear, the next problem becomes more precise: how to select part of a diff and turn that selection into a Git-compatible patch rather than merely highlighting text on a screen.
References
- README.md
- Config.md
- context.go
- types/context.go
- context/setup.go
- Codebase_Guide.md
- commands/git.go
- git_command_builder.go
- models/commit.go
- models/branch.go
- models/file.go
- VISION.md
- Undoing.md
- undo_controller.go
- models/working_tree_state.go
- status.go
- file_loader.go
- refresh_helper.go
- mergeconflicts
- Busy.md
- types/common.go
- gui.go
- gocui/gui.go
- Integration Tests README
- pkg/integration