Git, CI/CD & GitOpsVI · Index / Staging AreaIndex
Staging versus skipping — git add, .gitignore, and the untracked bucket
What you'll learn
- Distinguish "not staged" from "ignored" in git status output
- Read .gitignore patterns and explain glob, anchored, and negation forms
- Decide when a file should be staged once, ignored permanently, or tracked but gitignored
- Use git rm --cached to stop tracking a file without deleting it from the working tree
- Recognise the security implication of accidentally committing secrets
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
Two distinct ideas hide under the word “skipped” in a Git
workflow. A file that the engineer has not yet added is
untracked and git status lists it under “Untracked files”.
A file that .gitignore matches is ignored and git status
does not mention it at all, unless explicit flags force it to.
The difference matters: an untracked file is a candidate for
git add; an ignored file is not in the repository’s domain
unless the engineer forces it. Misreading the two is how
secrets end up in commits and how build artifacts end up
inflating the repository by gigabytes.
The three buckets of git status
git status collapses everything it sees into named buckets,
and the difference between them is what the engineer has
decided:
git status --short
# M terraform/main.tf (modified, staged)
# M ansible/hosts.yml (modified, not staged)
# A playbooks/rotate-creds.yml (added/staged)
# ?? secrets/dev-keys.pem (untracked)
# !! secrets/prod-keys.pem (ignored, would be tracked with --ignored)
The X/Y column in --short output is the staging state on the
left and the working-tree state on the right. The conceptual
buckets:
- Staged — the index differs from HEAD. The file is in the next commit.
- Not staged — the working tree differs from the index. The
file has been edited since the last
git add. - Untracked — the working tree contains a path that is not
in the index, and
.gitignoredoes not match it. The path exists on disk; Git does not have an opinion about it yet. - Ignored —
.gitignorematches the path; Git does not list it under untracked files unless--ignoredis passed. The path is outside the repository’s domain.
flowchart LR
A["Working tree path"] --> B{"in index?"}
B -->|yes| C["tracked file\nstaged or not staged"]
B -->|no| D{".gitignore\nmatches?"}
D -->|yes| E["ignored\nhidden from git status"]
D -->|no| F["untracked\nlisted by git status"]
How .gitignore patterns work
A .gitignore file contains one pattern per line. Patterns are
matched against the path relative to the .gitignore file’s
location (or against any path if the pattern has no slash).
Three forms cover most needs:
# Plain name — matches the file or directory anywhere
*.pem
# Anchored — matches only at the same level as the .gitignore
/secrets/
# Negation — re-include a path that an earlier pattern excluded
!/scripts/deploy.sh
The rules in plain language:
- A pattern without a slash matches the basename at any depth.
*.pemmatchessecrets/dev.pemandtls/root.pem. - A pattern with a leading slash is anchored to the directory
containing the
.gitignorefile./secrets/matches thesecrets/directory at the same level but notterraform/secrets/. - A pattern with a slash anywhere but at the start matches the
pattern relative to the
.gitignorefile.build/**matches anything under abuild/directory at the same level. - A negation pattern (
!) re-includes a path that an earlier pattern excluded. It does not re-include a path that an earlier pattern excluded because the file is not present; Git cannot track a file that does not exist.
Patterns apply cumulatively. A repository can have multiple
.gitignore files: one at the root, plus per-directory files
for finer-grained control. git status reports “nothing to
commit, working tree clean” if every path is either tracked
or ignored.
Staged once, ignored forever
The most common production scenario is a file that was
previously committed but should no longer be in the
repository. Examples: a .env file with development
credentials committed by accident, a node_modules/ directory
checked in before the team adopted a .gitignore, or a
terraform.tfstate file checked in before the team moved
state to S3.
The fix is two-step: stop tracking the file, and add it to
.gitignore. Stopping tracking is the operation that removes
the file from the index without deleting it from the working
tree:
git rm --cached secrets/dev-keys.pem
git commit -m 'stop tracking dev keys (rotate first!)'
The --cached flag tells git rm to remove the path from the
index but leave the working tree file intact. The file remains
on disk; it is simply no longer part of the repository. Adding
the path to .gitignore in the same commit prevents the next
git add -A from re-tracking it.
sequenceDiagram
participant WT as Working tree
participant IDX as Index
participant REPO as Repository
Note over IDX: file currently tracked
WT->>IDX: git rm --cached PATH
Note over IDX: path removed from index
Note over WT: file remains on disk
WT->>REPO: git commit
Note over REPO: path no longer in any commit's tree
The untracked bucket and the build artifact trap
A common source of accidental commits is the untracked bucket.
A Terraform run produces a .terraform/ directory of provider
plugins; an Ansible run produces .retry files; an editor
produces .swp and *~ backups. None belong in the
repository, but they appear in git status as untracked and
lurk in the working tree until a broad git add -A sweeps
them up.
The fix is a .gitignore at the repository root:
# Terraform
.terraform/
*.tfstate
*.tfstate.backup
.terraformrc
terraform.tfvars
# Ansible
*.retry
.ansible/
# Editor and OS
*.swp
*~
.DS_Store
An infrastructure repository without a .gitignore is an
accident waiting to commit. The first commit in any new
infrastructure repository should add a .gitignore; the
discipline is to commit it before any other file.
The git add —force escape hatch
git add -f <path> (or git add --force <path>) stages an
ignored file. The flag exists for legitimate use: a debug
build configuration that should be temporarily committed, a
machine-specific override that needs to ship once, an audit
artifact that must be attached to a specific commit. It is not
a normal workflow.
In an infrastructure repository, the right policy is to treat
git add -f as a deliberate event that must be justified in
the commit message. A pre-commit hook that refuses commits
containing -f justifications in git log is overkill; a
post-commit CI check that scans every commit for ignored
files that were force-added is the right place for the
guard rail.
Production discipline
- Commit a
.gitignorebefore any other file. A repository without a.gitignoreis onegit add -Aaway from a gigabyte ofnode_modules/. - Treat
git rm --cachedas a remediation, not a cleanup. Removing a file from tracking is the right move when the file should never have been tracked. For secrets, the remediation is incomplete until the secret is rotated. - Treat
git add -fas an audit event. Force-adding an ignored file should appear in the commit message and in the post-commit review. The flag exists; the discipline is to use it rarely and visibly.
Cross-course references
- Linux for Production Sysadmins - Part XII (RepoSecurity) covers the package-manager analogue: ignored paths in Git are the same idea as files excluded from a Debian package or a Red Hat RPM, except the Git version is editable per clone.
- Ansible for Production Sysadmins - Part XXXVII (RepoArch)
describes the
.gitignorepatterns that an Ansible repository must include to keep vault files, retry files, and dynamic inventory out of the commit graph. - Terraform for Production Sysadmins - Part IX (State)
treats
.gitignoreas the boundary that keeps the state file out of the repository even when the configuration directory is shared.
Quiz
Knowledge check · 4 questions
Q1. An engineer notices that `git status` shows no output for a file they know is on disk. They check `.gitignore` and confirm the path is matched. What is the right next action if the file holds a development credential that was committed accidentally a year ago?
Q2. Adding a path to `.gitignore` removes the file from every prior commit in the repository's history.
Q3. Explain the difference between a path that `git status` lists under 'Untracked files' and a path that `git status` does not mention at all but exists on disk.
Q4. Stop tracking a sensitive file that was committed by accident, harden the repository to prevent recurrence, and address the leaked secret.
A team discovers that `secrets/dev-keys.pem` was committed to the repository nine months ago. The file holds a long-lived AWS access key for a development environment. The file is on disk in every clone. The team wants to stop tracking the file, prevent future accidental commits of secrets, and address the leaked credential.
Passing score: 75%. Answers are checked in this browser.