# How Lazygit's Staging View Handles Partial File Staging and Hunk Management

> Master lazygit's staging view for partial file staging and hunk management. Learn how it converts UI selections into Git patches for efficient version control.

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

---

**Lazygit's staging view enables partial file staging by converting UI selections into targeted Git patches through a `patch_exploring.State` system that maps view lines to patch indices, then applies them via `git apply --cached`.**

The `jesseduffield/lazygit` terminal UI provides granular control over Git staging through its sophisticated staging view, allowing users to stage individual lines, ranges, or entire hunks without leaving the interface. This partial file staging capability relies on a patch-exploration subsystem that translates visual selections into precise Git operations.

## Architecture of the Staging View

The staging view orchestrates four core components to manage partial staging. The **StagingController** ([`pkg/gui/controllers/staging_controller.go`](https://github.com/jesseduffield/lazygit/blob/main/pkg/gui/controllers/staging_controller.go)) handles keybindings and orchestrates patch application. The **StagingHelper** ([`pkg/gui/controllers/helpers/staging_helper.go`](https://github.com/jesseduffield/lazygit/blob/main/pkg/gui/controllers/helpers/staging_helper.go)) refreshes the dual panels and builds diff strings. The **patch_exploring.State** ([`pkg/gui/patch_exploring/state.go`](https://github.com/jesseduffield/lazygit/blob/main/pkg/gui/patch_exploring/state.go)) tracks cursor position and selection modes while mapping between view lines and patch lines. Finally, the internal **patch.Patch** library (`pkg/commands/patch`) parses unified diffs and generates subset patches.

The workflow follows a linear pipeline: user selection → state mapping → patch transformation → Git application. When you select lines in the UI, the `State` object converts those view indices into patch line numbers. The `StagingController` then generates a temporary patch containing only those lines and applies it to the index or working tree.

## Selection Modes and Hunk Management

Lazygit supports three distinct selection modes controlled via `State.selectMode`: **LINE**, **RANGE**, and **HUNK**. In LINE mode, the cursor targets a single line. RANGE mode activates when you hold Shift or toggle range selection, creating a non-sticky selection across multiple lines. HUNK mode selects the entire change block containing the cursor.

You toggle hunk mode using `ToggleSelectHunk`, implemented in [`pkg/gui/patch_exploring/state.go`](https://github.com/jesseduffield/lazygit/blob/main/pkg/gui/patch_exploring/state.go):

```go
func (s *State) ToggleSelectHunk() {
    if s.selectMode == HUNK {
        s.selectMode = LINE
    } else {
        s.selectMode = HUNK
        s.userEnabledHunkMode = true
        s.selectedLineIdx = s.viewLineIndices[s.patch.GetNextChangeIdx(s.patchLineIndices[s.selectedLineIdx])]
    }
}

```

When hunk mode activates, the cursor automatically jumps to the next change line within the current hunk. Lazygit can also auto-enable hunk mode by default if configured:

```go
if useHunkModeByDefault && !patch.IsSingleHunkForWholeFile() {
    selectMode = HUNK
}

```

## Mapping UI Selections to Patch Lines

The `State` maintains two parallel index slices to bridge the gap between wrapped view lines and actual patch content:

```go
viewLineIndices []int   // view line -> patch line
patchLineIndices []int  // patch line -> view line

```

When you select a range, the controller retrieves the corresponding patch line indices through `SelectedPatchRange`:

```go
func (s *State) SelectedPatchRange() (int, int) {
    start, end := s.SelectedViewRange()
    return s.patchLineIndices[start], s.patchLineIndices[end]
}

```

This bidirectional mapping ensures that wrapped display lines correctly resolve to the underlying diff lines, regardless of terminal width or line wrapping.

## Building and Applying Partial Patches

The core staging logic resides in `StagingController.applySelection`, which constructs a temporary Git patch from your selection. The process uses the `patch` library's transform capabilities:

```go
firstLineIdx, lastLineIdx := state.SelectedPatchRange()
patchToApply := patch.
    Parse(state.GetDiff()).
    Transform(patch.TransformOpts{
        Reverse:             reverse,
        IncludedLineIndices: patch.ExpandRange(firstLineIdx, lastLineIdx),
        FileNameOverride:    path,
    }).
    FormatPlain()

```

The `ExpandRange` function ensures that wrapped view lines mapping to the same patch line are fully included, preventing malformed patches. For hunk selections, `firstLineIdx` and `lastLineIdx` span the complete hunk boundaries.

Once generated, the controller applies the patch through the Git command layer:

```go
err := self.c.Git().Patch.ApplyPatch(
    patchToApply,
    git_commands.ApplyPatchOpts{
        Reverse: reverse,
        Cached:  !reverse || self.staged,
    },
)

```

Setting `Cached: true` stages changes to the index during normal staging operations. When discarding (`reverse: true`), the patch applies in reverse without the `--cached` flag, removing changes from the working tree.

## Refreshing the Staging Panels

After every operation, `StagingHelper.RefreshStagingPanel` reconciles the dual-panel view by fetching fresh diffs for both staged and unstaged content:

```go
mainDiff := self.c.Git().WorkingTree.WorktreeFileDiff(file, true, false)   // unstaged
secondaryDiff := self.c.Git().WorkingTree.WorktreeFileDiff(file, true, true) // staged

```

These diffs feed into `patch_exploring.NewState`, which preserves cursor position when possible while updating the available content and hunk boundaries.

## User Workflow and Keybindings

| Action | Default Key | Implementation |
|--------|-------------|----------------|
| **Stage/Unstage** | `Space` | Calls `applySelectionAndRefresh` with `reverse: false`, building a partial patch and running `git apply --cached` |
| **Toggle Hunk Mode** | `Ctrl-h` | Invokes `ToggleSelectHunk`, switching between LINE and HUNK selection modes |
| **Range Select** | `Shift-↑/↓` or `V` | Activates RANGE mode via `ToggleSelectRange` for multi-line selection |
| **Discard Selection** | `d` | Calls `applySelectionAndRefresh(true)` to apply the patch in reverse |
| **Edit Hunk** | `e` | Opens the hunk in an external editor, then reapplies the modified patch |

## Summary

- **Three selection modes** (LINE, RANGE, HUNK) in `patch_exploring.State` control the granularity of partial staging operations.
- **Bidirectional index mapping** via `viewLineIndices` and `patchLineIndices` ensures accurate translation between wrapped UI lines and actual diff lines.
- **Patch transformation** through `patch.Parse` and `Transform` generates minimal patches containing only selected lines using `ExpandRange` for boundary safety.
- **Git application** via `ApplyPatch` with `Cached: true` stages content, while reverse application handles discards.
- **Dual-panel refresh** through `StagingHelper` maintains synchronized views of staged and unstaged changes using `WorktreeFileDiff`.

## Frequently Asked Questions

### How does lazygit map visual lines to actual patch lines?

Lazygit maintains two parallel slices in `patch_exploring.State`: `viewLineIndices` maps view lines to patch lines, while `patchLineIndices` maps patch lines back to view lines. When you select a range, `SelectedPatchRange()` uses these slices to convert view indices into the patch line numbers required for generating the partial patch.

### What is the difference between LINE, RANGE, and HUNK selection modes?

LINE mode stages the single line under your cursor. RANGE mode allows you to select multiple consecutive lines using Shift or visual mode. HUNK mode selects the entire diff hunk containing your cursor. You toggle hunk mode with `Ctrl-h`, which calls `ToggleSelectHunk()` and automatically repositions the cursor to the next change within the hunk.

### How does lazygit stage only part of a file to the git index?

The `StagingController` calls `applySelection`, which retrieves the selected patch line range via `State.SelectedPatchRange()`. It then parses the full diff using `patch.Parse()`, transforms it with `IncludedLineIndices` set to your selection, and formats a new patch. Finally, it executes `git apply --cached` via `ApplyPatch()` with `Cached: true` to add only those lines to the index.

### Can you discard individual lines or hunks using the staging view?

Yes. When you press `d` to discard, the controller calls `applySelectionAndRefresh` with `reverse: true`. This generates a patch from your selection and applies it in reverse using `git apply` without the `--cached` flag, effectively removing those specific changes from the working tree while leaving the rest of the file intact.