The Patch Is the Unit of Precision

Lazygit's most important staging idea is that a diff is not a picture to admire; it is a set of possible changes that can be selected and transformed. This chapter focuses on how lazygit turns a file-level status into a line, range, or hunk decision, and then turns that decision into a Git-compatible patch. The practical goal is to stage only the intended part of a real change while knowing where the terminal display, the patch data, and Git's acceptance of that patch can differ.

The distinction matters because one file can contain several intentions. Imagine adding an import needed by a new function, correcting a spelling mistake in documentation, and changing a test in the same working copy. A file-level “stage” action would combine them even if they belong in separate commits. The useful question is not “is this file changed?” but “which part of this file is ready to cross from the working tree into the index?” Lazygit makes that question explicit through its staging view.

Three questions hidden in one diff

A file entry answers the broadest question: what does Git think is different? The status has separate staged and unstaged positions, so a file can have changes prepared in the index and other changes that remain only in the working copy. It can also be untracked, added, deleted, renamed, or conflicted. These states determine which operation is meaningful. Unstaging a tracked file is not the same as deleting an untracked file; a rename carries a previous path; a conflict is not an ordinary content edit. The status view is therefore the first boundary around the patch: it says which repository state the diff came from.

The next question is structural: where are the changes grouped? A diff contains headers, hunks, and lines. A hunk is a region where Git shows changed lines together with enough unchanged context to locate them in the old and new file. Within a hunk, added lines represent content in the new file, deleted lines represent content removed from the old file, and context lines are present on both sides. The context is not part of the user's new intent, but it is often necessary for Git to know where the selected change belongs.

The final question is about the unit of intent. Lazygit lets the user select an individual line, a contiguous range of lines, or an entire hunk. Those units overlap, but they solve different problems. A line is appropriate when one addition or deletion is independent. A range is useful when several adjacent lines form one logical edit. A hunk is safer when the surrounding changes are inseparable or when preserving the whole local transformation matters. Choosing a larger unit is not merely faster; it asserts that the selected changes belong together.

This is why the staging interface can show separate unstaged and staged differences for the same file. The unstaged view asks what remains in the working copy relative to the index. The staged view asks what the index contains relative to the last commit. The two views are not duplicates with different colors; they are comparisons against different baselines. The working tree is where edits are being made, the index is the proposed next snapshot, and the last commit is the committed baseline. A partial staging action changes the proposed snapshot without necessarily removing the other edits from the working tree.

Return to the example with a new function, a documentation correction, and a test adjustment. If the new function and its import belong together, selecting only that change moves those lines into the index while the correction and test remain in the unstaged comparison. If the function's change spans more than one displayed block, selecting one small line may create an incomplete intent; a range or whole hunk may be the more accurate unit. Conversely, if a hunk contains two unrelated edits separated only by the diff's context, taking the whole hunk would be convenient but imprecise. The display makes the choice available, but the user still has to decide what belongs in the next snapshot.

After the action, the refreshed views should make that split visible. The staged comparison should contain the selected change, and the unstaged comparison should retain what was deliberately left behind. A successful keypress is not enough: the important evidence is which intention crossed the boundary and which one did not. This is also why a user should inspect the staged diff before committing. The index is an intermediate state, so it gives the user a place to correct grouping without first rewriting a commit.

Lazygit's range selection supports two interaction styles. In sticky mode, the user turns range selection on and moves the cursor to expand the selected interval. In the non-sticky form, holding the range-expansion keys extends the selection temporarily, while ordinary movement resets it. The difference is about control behavior, not patch semantics. Both modes describe a contiguous range; neither creates a non-contiguous collection of arbitrary lines. The same general idea can apply to files or commits for actions that support ranges, but the documentation explicitly limits range selection to supported actions.

A useful check before applying a selection is to state its boundary in ordinary language: “this added block, but not the unrelated deletion below it,” or “the whole hunk because its context and additions express one change.” If that sentence cannot be said clearly, the selection is probably still a visual gesture rather than a well-defined patch.

From a cursor to a Git-compatible patch

The terminal complicates the boundary because the line the user sees is not always one line in the patch. Long lines can wrap to the view width. A cursor position therefore has to be mapped from a wrapped display line back to the original patch line before lazygit can decide what to include. The patch explorer keeps both directions of that mapping: which view lines represent a patch line, and which original patch line corresponds to a view position. Without it, selecting the continuation of a wrapped line could stage the wrong data or make the cursor appear to move unpredictably after a refresh.

The patch explorer also maintains selection state independently of the rendered text. It knows the selected view line, the start of a range, whether the range is sticky, and whether the user is selecting lines or hunks. Hunk mode moves between change blocks rather than asking the user to land on every individual line. A default hunk mode can be enabled for a patch that is not one indivisible hunk, while the user can still switch to a narrower selection when the situation demands it. The visible cursor is the control surface; the underlying state is what gives the cursor precise meaning.

The patch builder is the next boundary. It records whether a file contributes nothing, its whole diff, or only selected lines. That distinction matters because selecting part of a file should not accidentally turn into selecting the file's entire change. When the user has chosen several files or line ranges, the builder can render their selections as one aggregated patch, with each file's contribution retained separately until the operation consumes it.

The transformation step is where the implementation proves that staging is more than highlighting. Suppose a hunk contains a deleted line the user wants to stage and an added line they do not. The unselected added line cannot simply remain as an addition in the generated patch, because that would ask Git to stage it too. Conversely, an unselected deleted line often has to remain as context so that the selected deletion is positioned correctly. Lazygit's transformation converts unselected old-file lines into context, removes unselected new-file lines, and recalculates the hunk header to describe the resulting old and new lengths. The patch still carries the structural information Git needs, but only the selected changes remain changes.

That ordering is subtle around adjacent additions and deletions. The transformer buffers context produced from unselected old-file lines so it can place those lines after selected changes in the correct order. It also accounts for the special newline marker that Git emits when a file lacks a final newline. These details are not trivia if the aim is a valid patch: a patch that looks approximately right to a reader can still be malformed or apply to the wrong file state if its hunk structure is wrong.

The full sequence looks like this:

sequenceDiagram participant U as User participant P as Patch explorer participant B as Patch builder participant G as Git apply participant V as Refreshed views U->>P: Select line, range, or hunk P->>P: Map view lines to patch lines P->>B: Record included changes B->>B: Transform context and hunk headers B->>G: Apply the generated patch G-->>V: Return updated repository state V-->>U: Show staged and unstaged differences

The arrows describe ownership as much as chronology. The user owns intent. The patch explorer owns the relationship between cursor and diff structure. The patch builder owns inclusion and transformation. Git owns applying the resulting patch. The refreshed views report the new state back to the user. If an action fails at the Git boundary, the interface has not proved that the intended change was staged; if it succeeds, the user should still inspect the staged diff rather than infer correctness from the absence of an error.

The same patch machinery supports operations beyond ordinary staging. A reverse transformation swaps the role of old-file and new-file lines so a selected patch can be applied with Git's reverse option. This makes it possible to remove selected content from an existing commit or move it out of one location before applying it elsewhere. When the application uses index and three-way options, Git can use the index and available blob information to find a clean way to apply the change, rather than relying only on a textual match. Three-way application improves the available recovery path; it does not guarantee that a conflicting or semantically inappropriate change will be resolved automatically.

Renames add another important boundary. A whole-file selection can preserve rename metadata, because the operation concerns moving the file together with its content. A partial selection of a renamed file is different: the selected operation should change content while leaving the rename itself in the original commit. Lazygit strips the rename metadata for that partial patch and points the resulting diff at the new path. The result is not a general rule that “renames are just edits”; it is a deliberate distinction between selecting the complete file transition and selecting only its content change.

Precision has limits

A precise interface does not mean every action accepts every selection. Range selection is implemented incrementally; when an action works only on one item, applying it to a range raises an error. That is a useful failure rather than a missing explanation: it prevents the interface from silently choosing an interpretation for an action whose semantics are not defined over multiple items.

Repository state can also make a valid selection unusable. A patch may no longer apply cleanly because the working tree changed, a conflict may require resolution before staging, or a rename and its paths may not have the form the operation expects. The correct response is to read the error and the refreshed status, not to widen the selection until something happens. A larger patch may apply while still expressing the wrong intent.

There is a second limit that no patch algorithm can remove: syntactic precision is not semantic precision. Lazygit can build a patch that contains exactly the selected added and deleted lines, and Git can accept it into the index. Neither fact proves that the selected lines form a correct commit. The user still needs to read the staged diff, consider nearby context, and decide whether the change belongs with the other staged work.

That final inspection is the habit that makes partial staging reliable. Ask what was selected, what was converted into context, what was removed from the generated patch, and what the staged view now contains. Once a user can answer those questions, the patch is no longer an opaque by-product of a shortcut. It is an explicit representation of intent. The next challenge is to carry that representation into commit history, where a selected change may need to become a fixup, an amend, or a controlled rewrite of an older commit.

References