# Data Flow from User Input to Git Command Execution in lazygit: A Complete Technical Guide

> Explore the data flow from user input to git command execution in lazygit. Understand the six-stage pipeline from keybindings to subprocess execution and UI refresh. Learn how lazygit works.

- Repository: [Jesse Duffield/lazygit](https://github.com/jesseduffield/lazygit)
- Tags: internals
- Published: 2026-03-02

---

**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`](https://github.com/jesseduffield/lazygit/blob/main/pkg/gui/controllers/staging_controller.go), the `GetKeybindings` method maps the physical key to a handler function:

```go
{
    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`](https://github.com/jesseduffield/lazygit/blob/main/pkg/gui/controllers/helpers/working_tree_helper.go). The `HandleCommitPress` method validates preconditions, prepares commit messages, and initiates command construction:

```go
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`](https://github.com/jesseduffield/lazygit/blob/main/pkg/commands/git_commands/commit.go) to construct the actual git command. This method builds the argument list dynamically based on user configuration:

```go
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`](https://github.com/jesseduffield/lazygit/blob/main/pkg/gui/gui_common.go). This method suspends the terminal UI, executes the command, and handles the refresh cycle:

```go
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`](https://github.com/jesseduffield/lazygit/blob/main/pkg/gui/controllers/helpers/gpg_helper.go). The `WithGpgHandling` method determines whether the command requires interactive GPG passphrase entry:

```go
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:

```go
// 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:

```bash
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`](https://github.com/jesseduffield/lazygit/blob/main/pkg/gui/controllers/staging_controller.go)) map physical keys to handler functions.
- **Helpers** ([`pkg/gui/controllers/helpers/working_tree_helper.go`](https://github.com/jesseduffield/lazygit/blob/main/pkg/gui/controllers/helpers/working_tree_helper.go)) validate state and orchestrate command construction.
- **Git command builders** ([`pkg/commands/git_commands/commit.go`](https://github.com/jesseduffield/lazygit/blob/main/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`](https://github.com/jesseduffield/lazygit/blob/main/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`](https://github.com/jesseduffield/lazygit/blob/main/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`](https://github.com/jesseduffield/lazygit/blob/main/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`](https://github.com/jesseduffield/lazygit/blob/main/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`](https://github.com/jesseduffield/lazygit/blob/main/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.