Git add: choose which files go into the next commit

Most repositories are full of commits that contain three unrelated changes, and the cause is almost always the same command typed on reflex. git add . is fast, it works, and it quietly decides on your behalf what the next commit says. The point of git add is the opposite of speed: it is the one place where you get to choose what the commit means before anyone reads it.

The index is a snapshot, not a list of filenames

git add updates the index using the content found in the working tree right now. That word content matters more than it looks. The index holds a snapshot of file contents, and it is that snapshot, not the files on disk, that becomes the next commit.

The consequence catches everyone once. git add only records the content of the specified files at the moment it runs. Edit the same file afterwards and the new edit is not in the commit unless git add runs again. The file is not staged. A particular version of the file is staged.

That design is what makes partial commits possible at all. Because the index is a separate layer, the state of a file on disk and the state that is about to be committed can differ deliberately, and three commands read the three gaps between the layers.

git status --short
git diff
git diff --cached

git diff compares the working tree against the index, so it shows what is not staged. git diff --cached compares the index against HEAD, so it shows what is. In the short status, each path carries two letters: outside a merge the first is the state of the index and the second the state of the working tree, and ?? marks a path Git has never heard of.

Ignored files are skipped by default. Naming one explicitly on the command line makes the command fail with a list of them, while ignored files reached by directory recursion or by globbing are passed over silently. -f overrides this, and the fact that it takes a flag is a hint about how often it is the right answer. The complete option list is in the git add documentation.

The four ways to say everything

There are four spellings of "stage what has changed", they are not interchangeable, and the differences are entirely about deletions and about scope.

Command New files Modified files Deleted files Scope
git add . Staged Staged Staged The current directory downwards
git add -A Staged Staged Staged The whole working tree when no path is given
git add -u Ignored Staged Staged Paths already in the index, whole tree when no path is given
git add --no-all <path> Staged Staged Ignored The given path only

git add . and git add -A differ only in where they start. Giving a directory name records not just a file modified in that directory and a file added to it, but also a file removed from it. Older versions of Git ignored removals here, which is the reason --no-all still exists for anyone whose muscle memory predates that change.

-u is the precise one. It updates the index only where an entry already exists for a matching path, so it stages modifications and deletions and adds nothing new. That is the command for a session where new files are present that are not ready to be tracked yet.

One more option belongs in this group even though it stages nothing. -N, or --intent-to-add, records the fact that a path will be added later by placing an entry with no content in the index. The immediate benefit is that git diff then shows the contents of an otherwise untracked new file, and git commit -a will include it.

Before any of them, a dry run costs nothing and prints exactly what would happen, including which paths would be ignored.

git add -n .
git add -n --ignore-missing .

Staging part of a file

When one file contains two unrelated changes, the fix is not to commit both and apologise in the message. git add -p walks the difference between the index and the working tree hunk by hunk and asks about each one. It is the same machinery as interactive mode, jumping straight to the patch step.

The prompt accepts single letters, and four of them carry the work.

y   stage this hunk
n   do not stage this hunk
s   split the current hunk into smaller hunks
e   manually edit the current hunk

The rest are navigation. a stages this hunk and all later ones in the file, d skips this one and all later ones, g jumps to a chosen hunk, / searches for a hunk matching a regular expression, j and k leave a hunk undecided and move to the next or previous undecided one, J and K do the same without skipping decided hunks, q quits without staging the remainder, and p reprints the current hunk. Setting interactive.singleKey to true removes the need to press return after each answer.

s is the one that turns a vague intention into a clean commit. Git splits hunks at context boundaries, so a hunk covering two adjacent changes will often break into two that can be answered separately. When it will not split far enough, e opens the hunk in an editor and applies the result to the index.

Editing a patch has rules, and they are mechanical. Deleting a + line prevents that addition from being staged. Turning a - into a space prevents that removal from being staged. Modifying only half of a - and + pair produces a confusing index. Deleting every line in the patch aborts the operation and stages nothing. Adding context or removal lines, deleting them, or editing their contents makes the patch impossible to apply, so those are the operations to avoid entirely. The same editor appears from git add -e, which opens the whole diff against the index rather than one hunk.

Interactive mode for a directory full of changes

git add -i is the older sibling and is better when the question is which files rather than which lines. It shows the status output and then a menu.

*** Commands ***
  1: status     2: update     3: revert     4: add untracked
  5: patch      6: diff       7: quit       8: help
What now>

A prompt ending in a single > takes one choice. A prompt ending in >> takes several, separated by spaces or commas, and it accepts ranges: 2-5 7,9 selects those six entries, 7- selects everything from the seventh onwards, and * selects all. Prefixing with - removes a selection, so -2 deselects the second entry. An empty line confirms.

The four working subcommands map onto the four questions. update stages the selected paths. revert returns the staged state of selected paths to the HEAD version, which makes newly added paths untracked again. add untracked brings in files Git does not know about, one selection at a time rather than all at once. diff shows what is about to be committed.

The status display inside interactive mode is more informative than it first appears, because it prints staged and unstaged line counts per path side by side, so a file with staged changes and further unstaged edits is obvious rather than something to deduce.

Undoing a git add

Unstaging is not a special command, it is git add in reverse.

git reset src/config.ts
git restore --staged src/config.ts
git reset -p

git reset <pathspec> resets index entries for matching paths to their state at HEAD and does not touch the working tree or the branch, which makes it the exact opposite of git add <pathspec>. git restore --staged <pathspec> is the newer spelling of the same operation. For part of a file, git reset -p applies the chosen hunks in reverse to the index, mirroring git add -p.

None of these three discards an edit. They move a change from staged to unstaged, and the file on disk stays as it is. Discarding the edit itself is git restore <path> without --staged, which is a different and irreversible operation.

Options that change the index without changing a file

Four options exist for cases where the problem is metadata rather than content, and each one solves a mess that is otherwise solved badly by hand.

--chmod=+x and --chmod=-x override the executable bit of the files being added. The bit changes only in the index; the files on disk are left alone. That is the correct way to make a script executable for everyone who clones the repository without depending on whatever permissions the local checkout happens to have.

git add --chmod=+x scripts/deploy.sh

--renormalize runs a fresh clean pass over all tracked files and adds them again, which is what is needed after changing core.autocrlf or the text attribute to repair files committed with the wrong line endings. It implies -u, so it touches tracked paths only. The conversion is not blind: a CRLF sequence cleans to LF, a lone CR is left as it is, and a CRCRLF sequence is only partially cleaned to CRLF.

--refresh adds nothing at all. It updates the cached stat information in the index, which is the cure for the situation where git status insists a file is modified although its content is identical.

--ignore-errors continues past files that cannot be indexed instead of aborting the whole command, while still exiting with a non-zero status so a script notices. add.ignoreErrors makes that the default for a repository.

The parts that behave differently on a Mac

Four local details turn git add . into a bad habit specifically here.

Finder writes .DS_Store into a folder the first time it is opened in a window, so these files appear without anyone creating them. Once one is tracked, every colleague who browses that folder produces a conflicting binary change. The global ignore file is the right place for it, at $XDG_CONFIG_HOME/git/ignore or $HOME/.config/git/ignore when that variable is unset.

mkdir -p ~/.config/git
printf '.DS_Store\n._*\n.AppleDouble\n' >> ~/.config/git/ignore

macOS decomposes Unicode in filenames, so a name containing a diacritic is stored as a base character plus a combining mark. core.precomposeUnicode reverts that decomposition, and without it a file added here can appear as a second, near-identical path to anyone on Linux or Windows.

APFS is case-insensitive in its usual configuration, which git clone and git init detect by probing, setting core.ignoreCase accordingly. Renaming a file by changing only its capitalisation therefore needs care, because a listing that finds makefile where Git expected Makefile is treated as the same file.

And adding a directory that contains its own .git directory stages an embedded repository rather than its files. Git warns about this unless --no-warn-embedded-repo is passed, and the warning is the useful kind: the fix is git submodule add, not force.

Most of these are noticed sooner when the folder listing and the command line are the same view of the same directory. That is the practical case for a file manager with a built-in terminal, and the comparison with other file managers sets out which tools combine the two.

What to change first

Put .DS_Store in the global ignore file today, then replace the reflex: run git status --short and git add -p instead of git add . for one week. The commits get smaller, the messages get truthful, and reviewing a diff stops being an act of faith. If checking a folder means leaving the terminal, Atriens keeps both in one window.

Frequently asked questions

What is the difference between git add . and git add -A?

Only the starting point. git add . stages additions, modifications and removals from the current directory downwards, while git add -A does the same for the whole working tree when no path is given. Both include deletions, which is what surprises people who remember older versions of Git ignoring them.

How do you stage only some of the changes in one file?

Use git add -p, which walks the difference between the index and the working tree one hunk at a time. Answer y or n per hunk, press s to split a hunk that covers two unrelated changes, and e to edit the hunk by hand. git add -e opens the entire diff in an editor instead.

How do you unstage a file without losing the changes?

Run git reset <path> or git restore --staged <path>. Both reset the index entry for that path to its state at HEAD and leave the file on disk untouched, so the edit becomes unstaged rather than lost. For part of a file, git reset -p is the reverse of git add -p.

Why does git add ignore some files?

Ignored files are excluded by default, so anything matching .gitignore, .git/info/exclude or the global ignore file is skipped. Naming an ignored file explicitly makes the command fail and list it, while ignored files found by recursion or globbing are skipped silently. git add -f overrides this, and git add -n shows in advance what would be ignored.

Does git add stage the file or the current version of the file?

The current version. The index holds a snapshot of content, so editing a file after staging it leaves the earlier version in the next commit unless git add runs again. Checking git diff before committing shows exactly this gap, since it lists changes present on disk but not in the index.

Back to all posts