Data Flow from User Input to Git Command Execution in lazygit: A Complete Technical Guide
When you press a key in lazygit, the input flows through a six-stage pipeline from controller keybindings to git command builders, ultimately executing as an OS subprocess while the UI refreshes.
Understanding how lazygit transforms a simple keystroke into a concrete git binary invocation is essential for contributors and power users. This guide traces the complete data flow through the lazygit architecture, from the StagingController capturing input to the CmdObj executing in your shell.
The Six-Stage Data Flow Pipeline
The journey from user input to git execution follows a strict hierarchy: Keybinding → Controller → Helper → Git Command Builder → OS Command → Subprocess Runner.
Stage 1: Keybinding to Controller
When a user presses c in the staging view, the event is captured in the controller's keybinding definition. In pkg/gui/controllers/staging_controller.go, the GetKeybindings method maps the physical key to a handler function:
{
Key: opts.GetKey(opts.Config.Files.CommitChanges), // usually "c"
Handler: self.c.Helpers().WorkingTree.HandleCommitPress,
Description: self.c.Tr.Commit,
}
The Handler field points to WorkingTreeHelper.HandleCommitPress, initiating the transition from the controller layer to the helper layer.
Stage 2: Controller to Helper
The controller delegates business logic to WorkingTreeHelper located in pkg/gui/controllers/helpers/working_tree_helper.go. The HandleCommitPress method validates preconditions, prepares commit messages, and initiates command construction:
func (self *WorkingTreeHelper) HandleCommitPress() error {
return self.WithEnsureCommittableFiles(func() error {
// ... prepare summary/description ...
cmdObj := self.c.Git().Commit.CommitCmdObj(summary, description, false)
self.c.LogAction(self.c.Tr.Actions.Commit)
return self.c.RunSubprocessAndRefresh(cmdObj)
})
}
This helper acts as the bridge between UI state and git operations, ensuring there are committable files before proceeding.
Stage 3: Helper to Git Command Builder
The helper invokes CommitCmdObj from pkg/commands/git_commands/commit.go to construct the actual git command. This method builds the argument list dynamically based on user configuration:
func (self *CommitCommands) CommitCmdObj(summary, description string, forceSkipHooks bool) *oscommands.CmdObj {
messageArgs := self.commitMessageArgs(summary, description)
skipHookPrefix := self.UserConfig().Git.SkipHookPrefix
cmdArgs := NewGitCmd("commit").
ArgIf(forceSkipHooks || (skipHookPrefix != "" && strings.HasPrefix(summary, skipHookPrefix)), "--no-verify").
ArgIf(self.signoffFlag() != "", self.signoffFlag()).
Arg(messageArgs...).ToArgv()
return self.cmd.New(cmdArgs)
}
The builder pattern constructs the final command line, handling conditional flags like --no-verify for skipped hooks or --signoff based on configuration.
Stage 4: Git Command to OS Command
CommitCmdObj returns a *oscommands.CmdObj from pkg/commands/oscommands. This object encapsulates the executable path, arguments, working directory, and environment variables. It represents the abstraction layer between lazygit's domain logic and the operating system's process execution capabilities.
Stage 5: OS Command to Subprocess Runner
The helper passes the CmdObj to RunSubprocessAndRefresh defined in pkg/gui/gui_common.go. This method suspends the terminal UI, executes the command, and handles the refresh cycle:
func (self *guiCommon) RunSubprocessAndRefresh(cmdObj *oscommands.CmdObj) error {
// Suspends UI, runs command, refreshes views after completion
return self.runSubprocess(cmdObj, true)
}
The runner manages the transition from lazygit's interactive terminal UI to the subprocess execution environment, ensuring the screen state is restored and views are refreshed after completion.
Stage 6: Subprocess to Git Execution
Finally, the CmdObj invokes exec.Command through the internal cmdObjRunner. The actual git binary executes with the constructed arguments, and stdout/stderr streams are captured and displayed in lazygit's UI panels. Upon completion, the UI refreshes to reflect the new repository state.
Error Handling and GPG Signing
When GPG signing is required, the flow diverts through GpgHelper in pkg/gui/controllers/helpers/gpg_helper.go. The WithGpgHandling method determines whether the command requires interactive GPG passphrase entry:
func (self *GpgHelper) WithGpgHandling(cmdObj *oscommands.CmdObj, waitingStatus string, onSuccess func()) error {
// Determines if interactive GPG is needed
// Runs in subprocess if interactive, otherwise streams directly
}
If interactive GPG is detected, the helper ensures the command runs in a subprocess with proper terminal attachment, allowing the user to enter passphrases while maintaining the standard refresh cycle.
Practical Code Example
Here is the complete flow from keypress to execution, demonstrating how the components wire together:
// Stage 1: Keybinding captures 'c' key
func (self *StagingController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
return []*types.Binding{
{
Key: opts.GetKey(opts.Config.Files.CommitChanges),
Handler: self.c.Helpers().WorkingTree.HandleCommitPress, // Stage 2
Description: self.c.Tr.Commit,
},
}
}
// Stage 2: Helper validates and prepares
func (self *WorkingTreeHelper) HandleCommitPress() error {
return self.WithEnsureCommittableFiles(func() error {
cmdObj := self.c.Git().Commit.CommitCmdObj(summary, description, false) // Stage 3
return self.c.RunSubprocessAndRefresh(cmdObj) // Stage 5
})
}
// Stage 3: Command builder constructs git arguments
func (self *CommitCommands) CommitCmdObj(summary, description string, forceSkipHooks bool) *oscommands.CmdObj {
cmdArgs := NewGitCmd("commit").
ArgIf(forceSkipHooks, "--no-verify").
Arg("-m", summary).
ToArgv()
return self.cmd.New(cmdArgs) // Returns *oscommands.CmdObj (Stage 4)
}
Executing this pipeline results in the terminal running:
git commit -m "Your message" [--no-verify]
The UI then refreshes automatically to show the new commit in the history panel.
Summary
- Keybindings in controllers (
pkg/gui/controllers/staging_controller.go) map physical keys to handler functions. - Helpers (
pkg/gui/controllers/helpers/working_tree_helper.go) validate state and orchestrate command construction. - Git command builders (
pkg/commands/git_commands/commit.go) construct argument lists using the builder pattern. - CmdObj (
pkg/commands/oscommands) encapsulates the OS command with working directory and environment. - Subprocess runners (
pkg/gui/gui_common.go) suspend the UI, execute the binary, and refresh views. - GPG handling diverts interactive commands through a specialized helper to handle passphrase entry.
Frequently Asked Questions
How does lazygit handle interactive git commands like GPG signing?
When a command requires GPG signing, GpgHelper.WithGpgHandling in pkg/gui/controllers/helpers/gpg_helper.go intercepts the CmdObj. It detects whether interactive passphrase entry is required and runs the command in a subprocess with terminal attachment, allowing the user to enter credentials while maintaining the standard UI refresh cycle after completion.
What is the difference between RunSubprocess and RunSubprocessAndRefresh?
RunSubprocessAndRefresh, defined in pkg/gui/gui_common.go, wraps the lower-level runSubprocess with an automatic UI refresh after the command completes. Use RunSubprocessAndRefresh for git operations that modify repository state and require view updates. Use the internal runSubprocess directly only when you need custom refresh logic or when running non-git utilities.
Where are git command arguments constructed in lazygit?
Git command arguments are built in pkg/commands/git_commands/ using a fluent builder pattern. For example, CommitCmdObj in pkg/commands/git_commands/commit.go constructs the git commit argument list, conditionally adding flags like --no-verify or --signoff based on user configuration and method parameters.
How does lazygit suspend the UI during subprocess execution?
The RunSubprocessAndRefresh method in pkg/gui/gui_common.go coordinates with the cmdObjRunner to suspend lazygit's interactive terminal UI (typically using gocui suspend/resume mechanisms), execute the oscommands.CmdObj via exec.Command, capture stdout/stderr streams, and restore the UI state upon completion.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →