# Lazygit Custom Patch Building Architecture: How "Rebase Magic" Works Under the Hood

> Explore the lazygit custom patch building architecture. Understand the UI controller, patch engine, and Git integration layers powering its rebase magic for efficient Git workflows.

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

---

**Lazygit's custom patch building architecture separates concerns into three tightly-coupled layers: a UI controller that manages the side-context view, a patch engine that tracks line selections in-memory, and a Git integration layer that executes `git apply` and continues the rebase.**

Lazygit's "rebase magic" feature allows users to edit commits during an interactive rebase by building custom patches from specific diff lines. This functionality relies on a sophisticated architecture that balances UI responsiveness with Git command execution. Understanding this system reveals how the terminal application maintains clean separation between user interactions, in-memory state management, and Git orchestration.

## The Three-Layer Architecture

The custom patch builder is organized into distinct layers that communicate through well-defined interfaces. Each layer has specific responsibilities and source files.

### UI and Controller Layer

The UI layer handles keystrokes, manages the secondary panel view, validates working-tree state, and drives interface refreshes. The `PatchBuildingHelper` in [`pkg/gui/controllers/helpers/patch_building_helper.go`](https://github.com/jesseduffield/lazygit/blob/main/pkg/gui/controllers/helpers/patch_building_helper.go) serves as the glue between user input and the patch engine. This helper validates that the repository is not already in a rebase-conflict state via `ValidateNormalWorkingTreeState` before opening the builder.

The `CustomPatchBuilder` context declared in [`pkg/gui/types/context/custom_patch_builder.go`](https://github.com/jesseduffield/lazygit/blob/main/pkg/gui/types/context/custom_patch_builder.go) sets up the secondary panel (side context) that displays the custom patch preview. When users toggle lines or confirm patches, this layer translates those actions into calls to the underlying engine.

### Patch Engine Layer

The core logic resides in [`pkg/commands/patch/patch_builder.go`](https://github.com/jesseduffield/lazygit/blob/main/pkg/commands/patch/patch_builder.go), where the `PatchBuilder` struct maintains an in-memory representation of the patch. Rather than storing the entire patch text on every interaction, the engine stores the original diff once and tracks which line indices are *included* using a `set.Set[int]`. This makes toggling operations computationally cheap.

Supporting this are:
- [`pkg/commands/patch/patch.go`](https://github.com/jesseduffield/lazygit/blob/main/pkg/commands/patch/patch.go) – Defines the `Patch` struct with headers and hunks, providing helpers like `ContainsChanges()`
- [`pkg/commands/patch/format.go`](https://github.com/jesseduffield/lazygit/blob/main/pkg/commands/patch/format.go) – Contains `formatView` for colored display (green background for included lines) and `formatPlain` for generating text suitable for `git apply`

### Git Integration Layer

This layer executes actual Git commands using the assembled patch. The `Git.Patch` wrapper in [`pkg/git/patch.go`](https://github.com/jesseduffield/lazygit/blob/main/pkg/git/patch.go) forwards operations to the `PatchBuilder` and runs `git apply --cached` when the user confirms. The rebase orchestration in [`pkg/commands/git_commands/rebase.go`](https://github.com/jesseduffield/lazygit/blob/main/pkg/commands/git_commands/rebase.go) manages the interactive rebase flow and calls the patch engine when continuing after custom patch application.

## How the Custom Patch Builder Works (Step-by-Step)

The architecture coordinates through a specific sequence when users engage "Edit commit" during an interactive rebase:

1. **Entry and Validation** – When the user selects "Edit commit", `PatchBuildingHelper.ValidateNormalWorkingTreeState` verifies the repo is not in a conflict state. If validation fails, the operation aborts with an error message.

2. **View Initialization** – The helper opens the side context via `self.c.Context().Push(types.SIDE_CONTEXT)` and calls `RefreshPatchBuildingPanel`. This retrieves the file diff via `Git.WorkingTree.ShowFileDiff` and requests a rendered secondary diff from `PatchBuilder.RenderPatchForFile`.

3. **Line Selection Tracking** – As users toggle lines (via `ToggleLine` in the controller), the engine adds or removes line indices from the inclusion set. The `formatView` function immediately reflects these changes with visual feedback—typically a green background on the first character of included lines.

4. **Patch Application** – When the user presses **Enter** in the secondary panel, `Git.Patch.ApplyCustomPatch` generates plain-text via `formatPlain` and executes `git apply --cached`. For reverse operations, it uses `git apply --reverse`.

5. **State Reset and Continue** – Upon successful application, `PatchBuilder.Reset()` clears the inclusion set and underlying state. The UI returns to the commit-files panel, and `git rebase --continue` proceeds automatically via the rebase controller.

6. **Abort Handling** – Pressing **Esc** triggers `PatchBuildingHelper.Escape`, popping the side context. Pressing **Ctrl-D** or invoking reset calls `PatchBuildingHelper.Reset`, which clears the builder state, refreshes the commit-files view, and hides the secondary panel.

## Core Implementation Details

The interaction between these layers appears in several key code paths. When opening the builder, the helper coordinates between Git operations and the patch engine:

```go
func (h *PatchBuildingHelper) RefreshPatchBuildingPanel(opts types.OnFocusOpts) {
    // Retrieve the diff of the selected file
    diff, _ := h.c.Git().WorkingTree.ShowFileDiff(...)

    // Render the current custom patch (may be empty)
    secondaryDiff := h.c.Git().Patch.PatchBuilder.RenderPatchForFile(patch.RenderPatchForFileOpts{
        Filename: path,
        Plain:    false,
        Reverse:  false,
        TurnAddedFilesIntoDiffAgainstEmptyFile: true,
    })

    // Set up the side-context state
    state := patch_exploring.NewState(diff, selectedLineIdx,
        h.c.Contexts().CustomPatchBuilder.GetView(),
        oldState, h.c.UserConfig().Gui.UseHunkModeInStagingView)
    h.c.Contexts().CustomPatchBuilder.SetState(state)

    // Render both panels
    h.c.RenderToMainViews(types.RefreshMainOpts{
        Pair: h.c.MainViewPairs().PatchBuilding,
        Main: &types.ViewUpdateOpts{
            Task:  types.NewRenderStringWithoutScrollTask(state.GetMainContent()),
            Title: h.c.Tr.Patch,
        },
        Secondary: &types.ViewUpdateOpts{
            Task:  types.NewRenderStringWithoutScrollTask(secondaryDiff),
            Title: h.c.Tr.CustomPatch,
        },
    })
}

```

Toggling lines demonstrates the efficient index-tracking approach. The key-binding handler updates the engine state and refreshes the UI:

```go
func (c *CustomPatchController) ToggleLine() error {
    // Retrieve the current line index from the side view
    lineIdx := c.c.Contexts().CustomPatchBuilder.GetState().CurrentLineIdx()
    
    // Ask the engine to include/exclude the line
    if err := c.c.Git().Patch.PatchBuilder.ToggleLine(lineIdx); err != nil {
        return err
    }
    
    // Refresh UI so the line gets visual feedback
    c.c.Helpers().PatchBuilding.RefreshPatchBuildingPanel(types.OnFocusOpts{})
    return nil
}

```

Applying the patch and continuing the rebase shows the clean separation between the patch engine and Git commands:

```go
func (c *RebaseController) ApplyCustomPatchAndContinue() error {
    // Apply the accumulated custom patch (plain text)
    if err := c.c.Git().Patch.ApplyCustomPatch(); err != nil {
        return err
    }
    
    // Discard the builder so it won't interfere with later steps
    c.c.Git().Patch.PatchBuilder.Reset()
    
    // Continue the interactive rebase
    return c.c.Commands().RebaseContinue()
}

```

The reset mechanism ensures abandoned patches never affect subsequent rebase steps:

```go
func (h *PatchBuildingHelper) Reset() error {
    h.c.Git().Patch.PatchBuilder.Reset()
    if h.c.Context().CurrentStatic().GetKind() != types.SIDE_CONTEXT {
        h.Escape()
    }
    h.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.COMMIT_FILES}})
    h.c.PostRefreshUpdate(h.c.Context().Current())
    return nil
}

```

## Summary

- **Three-layer separation**: The architecture cleanly divides UI control (`PatchBuildingHelper`), in-memory patch modeling (`PatchBuilder`), and Git execution (`Git.Patch` wrapper) to maintain testability and modularity.
- **Efficient line tracking**: Instead of storing patch text repeatedly, the engine in [`pkg/commands/patch/patch_builder.go`](https://github.com/jesseduffield/lazygit/blob/main/pkg/commands/patch/patch_builder.go) tracks only line indices in a set, making toggle operations instantaneous.
- **Side-context UI**: The secondary panel lives in `types.SIDE_CONTEXT`, managed through [`pkg/gui/types/context/custom_patch_builder.go`](https://github.com/jesseduffield/lazygit/blob/main/pkg/gui/types/context/custom_patch_builder.go), providing split-view diff comparison.
- **Stateful reset**: `PatchBuilder.Reset()` guarantees that abandoned or applied patches clear their inclusion sets, preventing state leakage between rebase steps.
- **Integration testing**: The flow is verified under `pkg/integration/tests/patch_building/`, ensuring that building, applying, and resetting patches works correctly during live rebase operations.

## Frequently Asked Questions

### What files control the lazygit custom patch building UI?

The primary UI controller lives in [`pkg/gui/controllers/helpers/patch_building_helper.go`](https://github.com/jesseduffield/lazygit/blob/main/pkg/gui/controllers/helpers/patch_building_helper.go), which manages opening, refreshing, escaping, and resetting the custom-patch view. The context definition resides in [`pkg/gui/types/context/custom_patch_builder.go`](https://github.com/jesseduffield/lazygit/blob/main/pkg/gui/types/context/custom_patch_builder.go), which configures the secondary panel used for the split-view display.

### How does lazygit track which lines are included in a custom patch?

The `PatchBuilder` struct in [`pkg/commands/patch/patch_builder.go`](https://github.com/jesseduffield/lazygit/blob/main/pkg/commands/patch/patch_builder.go) stores the original diff once and maintains a `set.Set[int]` of included line indices. When users toggle lines via `ToggleLine`, the engine adds or removes indices from this set. The [`format.go`](https://github.com/jesseduffield/lazygit/blob/main/format.go) file then renders these selections with visual highlighting using `formatView`.

### What happens when a user applies a custom patch during a rebase?

The `ApplyCustomPatch` method in the Git integration layer generates a plain-text patch via `formatPlain` and executes `git apply --cached`. Upon success, `PatchBuilder.Reset()` clears the builder state, and the rebase controller in [`pkg/commands/git_commands/rebase.go`](https://github.com/jesseduffield/lazygit/blob/main/pkg/commands/git_commands/rebase.go) automatically runs `git rebase --continue` to proceed with the interactive rebase.

### How does the patch builder reset its state?

State reset occurs through `PatchBuilder.Reset()` in the engine layer, which clears the inclusion set and underlying `Patch` data. The UI layer's `PatchBuildingHelper.Reset()` calls this method, pops the side context if necessary, and refreshes the commit-files view to ensure no visual remnants of the abandoned patch remain.