Git, CI/CD & GitOpsVI · Index / Staging AreaIndex
Partial staging with -p — hunk-level staging for clean commits
What you'll learn
- Explain how git add -p divides a file into hunks and presents them interactively
- Drive the interactive prompts: y/n/q/a/d/e/?
- Choose between edit (e) and split (s) for non-trivial hunks
- Recognise when hunk-level staging is the right tool and when it is overkill
- Avoid the common mistake of staging a debug print alongside the real fix
Prerequisites
Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x
Real working tree files accumulate changes. A Terraform module
that started as “fix the S3 bucket tags” might also pick up a
renamed variable, a reordered resource block, and a debug
print() left over from a failed plan. Whole-file staging —
git add terraform/main.tf — commits all of it together. The
commit message says “fix bucket tags” but the diff is four
unrelated changes. The reviewer cannot reason about the change
and the audit trail loses meaning. git add -p solves this by
splitting the file into hunks and letting you stage some while
leaving others in the working tree.
How git add -p works
git add -p (or git add --patch) walks the file hunk by hunk.
A hunk is a contiguous run of added and removed lines. Git’s
default hunk-splitting uses the same algorithm that produces
git diff output: changes separated by at least three unchanged
lines become separate hunks. For each hunk, Git presents a
prompt and waits for a one-key answer:
git add -p terraform/main.tf
diff --git a/terraform/main.tf b/terraform/main.tf
@@ -12,7 +12,7 @@ resource "aws_s3_bucket" "logs" {
bucket = "prod-logs"
- acl = "private"
+ acl = "log-delivery"
tags = {
Owner = "platform"
(1/2) Stage this hunk [y,n,q,a,d,e,?]?
The single-letter answers:
y— stage this hunk.n— do not stage this hunk; leave it in the working tree.q— quit; stage the current and all remaining hunks, then stop.a— stage this hunk and all remaining hunks in this file.d— do not stage this hunk or any remaining hunks in this file.e— edit the hunk manually; Git opens an editor with the hunk and lets you remove lines so only the parts you want are staged.s— split the current hunk into smaller hunks (useful when Git’s default split groups two unrelated changes).?— print the full help text.
flowchart TB
A["git add -p"] --> B["split file into hunks"]
B --> C{"for each hunk"}
C -->|y| D["stage hunk"]
C -->|n| E["leave in working tree"]
C -->|e| F["edit hunk manually"]
C -->|s| G["split into smaller hunks"]
C -->|q| H["quit, stage remainder"]
When hunk-level staging is the right tool
Three patterns make git add -p the obvious choice:
- A file holds a real fix and a drive-by cleanup. The real change is two lines; the cleanup is a renamed variable and a reordered block. The commit should describe the fix only.
- A debug artifact has been left in the file. A
print()statement, aconsole.log, or a temporaryterraform { ignore_changes }block was added during debugging and needs to stay in the working tree but not in the commit. - Two features share a file by accident. A new resource block was added to a Terraform file that already had an unrelated fix. The two changes should ship in two commits.
In each case the alternative — staging the whole file — forces
multiple unrelated changes into one commit. The downstream cost
is a git bisect that lands on an unrelated cleanup, a code
review that has to evaluate four changes at once, and a revert
that pulls in the unrelated changes along with the bad fix.
When hunk-level staging is overkill
For some files, partial staging is more friction than the change warrants:
- A new file that has not been committed yet. There is no
base to diff against.
git add -pwill refuse because there is nothing to split. Use plaingit add <path>. - A file with one logical change. If the entire diff is one
hunk and the whole hunk is the change,
git add -pis justgit add <path>with extra prompts. Skip it. - A binary file. Hunks are a text concept.
git add -pwill refuse or will offer the whole file as a single hunk. - A regenerated file. A
package-lock.jsonor a generated Terraform plan file should be committed whole or not at all; partial staging is meaningless.
Driving the editor
The most useful non-y answer is e. When a hunk holds more
than one logical change, e opens an editor with the hunk
displayed in unified-diff format. The engineer deletes the lines
they do not want to stage and saves the file; Git stages
whatever remains.
@@ -12,7 +12,9 @@ resource "aws_s3_bucket" "logs" {
bucket = "prod-logs"
- acl = "private"
+ acl = "log-delivery"
+ print("DEBUG: applied tags")
tags = {
Owner = "platform"
Removing the + print(...) line from the editor buffer and
saving stages only the acl change. The print line stays in
the working tree and can be removed in a follow-up commit. The
? prompt in the hunk header prints a help line that is also a
reminder: any line deleted from the editor buffer is not
staged; any line left in is staged; lines can be edited in place
if the change needs adjustment.
The other useful answer is s. When Git groups two unrelated
changes into one hunk because they are close together in the
file, s asks Git to re-split with a finer threshold. If the
re-split succeeds, the engineer sees two hunks where there was
one and can stage each independently.
Reviewing the partial stage
After the interactive session ends, the index holds only the staged hunks and the working tree holds the un-staged hunks plus the originally committed bytes. Both views are inspectable:
git diff --cached # what is staged
git diff # what is still in the working tree
git status # both, side by side
The next commit will contain exactly what git diff --cached
shows. The un-staged hunks remain in the working tree and will
need to be staged (or reverted) before a second commit can
describe them.
Production discipline
- Default to
git add -pfor any file edited more than once in a session. The cost of the interactive prompts is small compared to the cost of a commit that ships an unrelated change. - Inspect
git diff --cachedafter every partial stage. The staged hunks are exactly what the next commit will contain. A 30-second review catches a missing hunk before it becomes a missing fix. - Use
efor surgical precision. When a hunk groups two changes,elets you delete the lines you do not want to stage rather than splitting the change across two commits.
Cross-course references
- Linux for Production Sysadmins - Part XV (DiffAndPatch)
covers the unified-diff format that
git add -puses; the hunk header@@ -a,b +c,d @@is the same formatpatch -p1consumes. - Ansible for Production Sysadmins - Part XXXVII (RepoArch) treats partial staging as the way to keep unrelated playbook changes in separate commits, which is what makes per-feature pull requests reviewable.
- Terraform for Production Sysadmins - Part X (Plan) covers
the analogous tool for Terraform plans:
terraform plan -outfollowed byterraform apply, where the plan file is the unit of review rather than the staged hunks.
Quiz
Knowledge check · 4 questions
Q1. An engineer has edited `terraform/main.tf` and added a debug `print()` statement that they want to keep in the working tree for further debugging. The rest of the change is a real fix that should ship in the next commit. Which command is most appropriate?
Q2. `git add -p` can split a binary file into hunks for selective staging.
Q3. Inside the interactive `git add -p` prompt, name two answers that let you control exactly which lines of a hunk are staged, and explain the difference between them.
Q4. Use partial staging to extract a real fix from a working tree that also contains a debug artifact, then verify the next commit is clean.
An engineer is debugging a Terraform plan failure. They have edited `terraform/main.tf` to make a real fix (changing an IAM policy ARN) and have also added `print('DEBUG: tags', local.tags)` to a resource block. The real fix is ready to ship; the `print()` is leftover debugging that must stay in the working tree for further investigation. The engineer wants to commit only the real fix.
Passing score: 75%. Answers are checked in this browser.