← Back to blog

Per-line staging: stage exactly what you want

feature tutorial

Why Granular Staging Matters

A well-crafted Git history tells a story. Each commit represents a single logical change: a bug fix, a refactor, a new feature. But real-world development is rarely that tidy. You fix a bug, then notice a typo nearby, then adjust some formatting, then start a small refactor -- all in the same file. If you stage and commit the entire file at once, you end up with a commit that mixes unrelated changes. That makes code review harder, makes bisecting bugs harder, and makes the commit history less useful as documentation.

Granular staging lets you break your work into meaningful commits, even when the changes are interleaved within the same file. Instead of committing everything at once, you pick exactly which changes belong together.

Three Levels of Staging

File-level staging

The most basic level. You stage entire files with git add:

git add src/auth.ts src/utils.ts

This is fine when each file contains only one logical change. But as soon as a file contains multiple unrelated modifications, file-level staging is too coarse.

Hunk-level staging

Git can break a file's changes into "hunks" -- contiguous blocks of modified lines separated by unchanged code. Using git add -p (patch mode), Git presents each hunk one at a time and asks whether to stage it:

git add -p src/auth.ts

For each hunk, you choose y (stage), n (skip), s (split into smaller hunks), or e (edit manually). This is a major improvement over file-level staging, but it has limitations. Hunks are defined by proximity: if two unrelated changes are close together (separated by fewer than three lines of context), Git groups them into a single hunk. Splitting them requires manual patch editing, which is tedious and error-prone.

Line-level staging

Line-level staging is the most precise option. Instead of working with files or hunks, you select individual lines to stage. Want to stage lines 12 and 15 but not line 14? You can. This gives you complete control over what goes into each commit, regardless of how close together the changes are in the file.

On the command line, line-level staging means manually editing patches with git add -p and the e option. You need to understand the unified diff format, carefully modify the patch, and hope you do not introduce errors. It works, but it is not something most developers want to do regularly.

Line-Level Staging in GitSquid

GitSquid makes line-level staging visual and straightforward. When you select a modified file, the diff viewer shows every changed line. To stage specific lines:

  • Click a single line to select it.
  • Cmd+Click (Ctrl+Click on Windows/Linux) to select multiple individual lines.
  • Shift+Click to select a range of lines.

Once you have selected the lines you want, stage them. Only those lines are added to the index. The rest remain as unstaged modifications in your working directory, ready to be included in a different commit.

The same workflow works in reverse. If you have already staged lines you want to remove from the staging area, you can select them in the staged diff and unstage just those lines.

How it works under the hood

When you stage individual lines, GitSquid generates a patch on the server side that includes only the selected changes. This patch is then applied to the index using Git's patch machinery. By handling the patch generation server-side rather than manipulating the diff client-side, the result is reliable and matches exactly what Git expects. There are no formatting issues or off-by-one errors that you might encounter when editing patches by hand.

Practical Examples

Separating a bug fix from a refactor

You are working on a refactor and notice a bug. You fix it immediately because the fix is obvious. Now your file has two types of changes: the refactor and the bug fix. With line-level staging, you select only the bug fix lines, commit them with a clear message like "fix null check in auth validation," and then commit the refactor separately. The result is two clean, reviewable commits instead of one muddled one.

Splitting debug code from production code

You added some console.log statements while debugging, right next to the actual code changes. Rather than trying to remember which lines are debug and which are real, you visually select only the production changes, stage them, and commit. The debug lines stay in your working directory where you can keep using them or discard them later.

Preparing a partial commit for review

You have a large set of changes, but only part of the work is ready for review. Line-level staging lets you commit the finished portions while keeping work-in-progress code uncommitted. Your pull request stays focused, and the reviewer does not have to wade through incomplete code.

Keeping Commits Atomic

An atomic commit is one that contains a single, complete, logical change. It passes tests, does not break the build, and can be understood on its own. Atomic commits are not just a matter of tidiness. They have practical benefits:

  • Code review. Reviewers can understand each commit independently. A commit titled "fix input validation" that only touches validation logic is easy to review. A commit titled "various changes" that touches validation, styling, and configuration is not.
  • Bug hunting. When something breaks, git bisect can pinpoint the exact commit that introduced the problem. Atomic commits make the result meaningful: you find the specific change that caused the bug, not a grab bag of unrelated modifications.
  • Reverting. If a commit needs to be reverted, an atomic commit can be reverted cleanly. A mixed commit might force you to revert changes you actually wanted to keep.
  • History as documentation. Months later, when someone reads the commit history to understand why a piece of code exists, atomic commits with clear messages tell a useful story.

Line-level staging is the tool that makes atomic commits practical. Without it, creating clean commits from messy real-world development requires discipline and manual effort. With it, the process is as simple as selecting the lines that belong together.

The best commits are the ones that do exactly one thing. Granular staging gives you the control to make that happen, even when your working directory tells a different story.