git add

TL;DR: git add is the command that moves changes from your Working Directory into the Staging Area (the Index). Under the hood, Git hashes and compresses your file contents into blob objects inside the .git/objects/ directory, then updates the .git/index binary file to map your file paths to these new blob hashes. Staging is a critical intermediate step that allows you to craft precise, logical commits.


What Does git add Actually Do?

To understand git add, you must look past the interface of Git and examine its database structure. Many version control systems track “deltas”—a list of changes or diffs between file versions. Git does not. Git tracks snapshots.

Every time you stage a file using git add, you are telling Git to capture a snapshot of that file’s content at this exact second and save it as a blob object (binary large object) in its object database.

The Three-Tier Architecture of Git

Git manages your project’s history using a three-tier architecture:

  1. Working Directory: Your local filesystem where files are edited.
  2. Staging Area (The Index): A binary file located at .git/index that serves as a transition area. It holds the blueprint for the next commit.
  3. The Git Repository (HEAD): The database containing all past commit objects, tree structures, and blobs.
graph LR
    subgraph "Git Staging Lifecycle"
        WD["Working Directory<br>(Modify Files)"] -->|git add| SA["Staging Area / Index<br>(Update .git/index)"]
        SA -->|git commit| GR["Git Repository<br>(Record Commit)"]
    end

When you edit a file, the change exists only in the Working Directory. When you execute git add, Git performs two major steps:

Step 1: Write a Blob to the Object Database

Git reads the contents of the target file, prepends a simple header containing the object type and the file’s size in bytes (e.g., blob 12\0), and hashes the entire combination using the SHA-1 algorithm. This produces a unique 40-character hex hash.

Next, Git compresses the file content using zlib and writes the compressed content to a file inside .git/objects/. The path to the file is derived from the SHA-1 hash. For example, if the hash is 3b18e512dba79e4c8300dd08aeb37f8e728b8dad, the file is stored at .git/objects/3b/18e512dba79e4c8300dd08aeb37f8e728b8dad.

Step 2: Update the Index Manifest

Once the blob is safely stored, Git updates the binary .git/index file. It inserts or updates an entry that maps the file’s relative path (e.g., src/main.js) to the new blob hash, alongside filesystem metadata (permissions, timestamps, file size) used for stat caching (see git status).

This means the staging area is not a physical directory. It is simply a list of pathnames and their corresponding blob hashes. When you commit, Git uses this list to write the tree objects and commit object.


What Happens Inside .git/ (Hands-on exploration)

Let’s initialize a brand-new repository and trace exactly how git add alters the internal database.

Step 1: Set Up a Empty Repository

First, let’s create a directory, initialize it using git init, and inspect the .git/objects/ folder.

$ mkdir git-add-demo && cd git-add-demo
$ git init
Initialized empty Git repository in /git-add-demo/.git/

$ find .git/objects/ -type f
# (Output is empty - no objects have been created yet)

If we look for the index file, we’ll see it doesn’t even exist yet:

$ ls -l .git/index
ls: cannot access '.git/index': No such file or directory

Step 2: Create a File and Stage It

Now, let’s create a file and run git add.

$ echo "hello world" > greeting.txt
$ git add greeting.txt

Let’s check the objects directory again:

$ find .git/objects/ -type f
.git/objects/3b/18e512dba79e4c8300dd08aeb37f8e728b8dad

A new file has appeared! Let’s examine its type and content using Git’s plumbing commands:

# Verify the object type
$ git cat-file -t 3b18e512dba79e4c8300dd08aeb37f8e728b8dad
blob

# Read the object content
$ git cat-file -p 3b18e512dba79e4c8300dd08aeb37f8e728b8dad
hello world

Let’s inspect the .git/index file. Since it is a binary file, opening it with a text editor will show garbled characters. However, we can use the plumbing command git ls-files --stage to print its current contents:

$ git ls-files --stage
100644 3b18e512dba79e4c8300dd08aeb37f8e728b8dad 0	greeting.txt

This output tells us:

  • 100644: Standard non-executable file permissions.
  • 3b18e512...: The SHA-1 hash of the staged content.
  • 0: The stage slot number (used for resolving merge conflicts).
  • greeting.txt: The path of the file.

Step 3: Modifying and Staging Again (Dangling Blobs)

What happens if we modify greeting.txt and run git add again? Let’s find out.

$ echo "hello world v2" > greeting.txt
$ git add greeting.txt

Let’s check our objects directory:

$ find .git/objects/ -type f
.git/objects/3b/18e512dba79e4c8300dd08aeb37f8e728b8dad
.git/objects/fa/1c977d2427a1df58514cc5256e07a3f36098d5

We now have two blob files. Let’s inspect the new blob:

$ git cat-file -p fa1c977d2427a1df58514cc5256e07a3f36098d5
hello world v2

And what does the index look like now?

$ git ls-files --stage
100644 fa1c977d2427a1df58514cc5256e07a3f36098d5 0	greeting.txt

Crucial Internal Detail: The index entry for greeting.txt has been updated to point to the new blob (fa1c97...). The old blob (3b18e5...) is still stored in .git/objects/, but nothing points to it. It is now a dangling blob.

Because Git creates blobs during git add, staging changes is incredibly safe. If you accidentally delete your working directory modifications before committing, you can often recover your work by finding the dangling blob in .git/objects/ using git fsck --lost-found.


Detailed Command Options & Advanced Usage

Staging files file-by-file can be tedious. Git provides various options to customize how you stage your modifications.

1. Global Staging vs. Relative Staging

Understanding the difference between git add ., git add -A, and git add -u is crucial for avoiding staging mistakes.

git add . (Staging the current directory)

This stages all modifications, additions, and deletions within the current directory and its subdirectories. If you run it in a subdirectory, it will ignore changes in parent or sibling directories.

git add -A or git add --all

This stages all modifications, additions, and deletions across the entire repository working tree, regardless of which directory you are currently in.

git add -u or git add --update

This stages modifications and deletions of currently tracked files only. It will absolutely ignore any new, untracked files. This is useful when you want to update existing code without adding new scratch files or scripts.

2. Interactive Patch Mode (git add -p or --patch)

This is one of Git’s most powerful features. Instead of staging an entire file, --patch lets you review every single change (hunk) individually and decide whether to stage it.

When you run git add -p, Git will present a diff chunk and ask: Stage this hunk [y, n, q, a, d, j, J, g, /, s, e, ?]?

$ git add -p
diff --git a/greeting.txt b/greeting.txt
index fa1c977..0a29486 100644
--- a/greeting.txt
+++ b/greeting.txt
@@ -1 +1,2 @@
 hello world v2
+Staging only this line!
Stage this hunk [y, n, q, a, d, s, e, ?]?

Here are the most common options:

  • y (yes): Stage this hunk.
  • n (no): Do not stage this hunk.
  • q (quit): Exit patch mode without staging any further hunks.
  • a (all): Stage this hunk and all subsequent hunks in this file.
  • d (don’t): Do not stage this hunk or any subsequent hunks in this file.
  • s (split): Split the current hunk into smaller hunks if possible.
  • e (edit): Manually edit the hunk in your text editor.
  • ? (help): Print descriptions of all options.

3. Forcing Ignored Files (git add -f or --force)

If a file matches a pattern in your .gitignore but you absolutely must track it (for example, a vendor file that must be checked in), you cannot stage it normally. You must force Git to track it:

$ git add -f secrets.config

Real-World Scenario: Preparing Clean Commits

Imagine you are working on a feature, and while doing so, you spot a typo in a completely unrelated utility function and fix it. You now have changes in two files: src/feature.js and src/utils.js.

If you run git commit -a (which stages and commits all modified files at once), you will bundle the feature code and the unrelated bugfix into a single commit. This makes review difficult and makes it impossible to revert the feature later without also reverting the utility fix.

Instead, you use git add to separate the commits:

# Step 1: Stage and commit only the utility fix
$ git add src/utils.js
$ git commit -m "fix: resolve typo in utility function"

# Step 2: Stage and commit the feature code
$ git add src/feature.js
$ git commit -m "feat: implement user registration pipeline"

If the changes were in the same file, you would run git add -p to stage only the lines related to the bugfix first, commit, and then stage the remaining lines for the feature.


Common Pitfalls and Mistakes

1. Staging Large Binary Files or Secrets

A common mistake is running git add . and accidentally staging a .env file containing API keys or a massive database backup (db.sql).

  • The Fix: If you haven’t committed yet, unstage the file immediately using git restore --staged <file>. Next, add the file name to your .gitignore file so it doesn’t get staged again.

2. Modifying a File After Staging

If you run git add file.js, then open your editor and make more changes, running git status will display file.js in both the staged and unstaged sections.

  • Why: Git staged a snapshot of the file at the exact moment you ran git add. The subsequent edits are not staged.
  • The Fix: You must run git add file.js again to capture the latest modifications.

3. Directory Staging Illusion

Sometimes developers run git add empty-folder/ and expect it to be tracked.

  • Why: Git tracks files, not directories. An empty directory cannot have a hash, so it cannot be added to the .git/index.
  • The Fix: To track an empty directory, create a dummy file (commonly named .gitkeep or .placeholder) inside it and stage that file.

Command Quick Reference

CommandAction
git add <file>Stages a specific file.
git add <dir>Stages all changes within a directory.
git add .Stages all changes in the current directory and its subdirectories.
git add -A / --allStages all changes in the entire working directory.
git add -u / --updateStages changes to tracked files only (ignores untracked files).
git add -p / --patchStarts interactive patch mode to stage specific lines (hunks).
git add -f / --forceForce-stages a file that is ignored by .gitignore.

FAQ (People Also Ask)

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

Historically (in Git 1.x), git add . only staged new and modified files, ignoring deleted files, whereas git add -A staged deletions as well. Since Git 2.0, both commands stage deletions. The primary difference now is scope: git add . only stages files inside the current working directory, while git add -A stages files across the entire repository.

Does git add duplicate files and waste disk space?

Yes, git add creates new blob objects in the .git/objects/ folder, which takes up disk space. However, because Git compresses files using zlib, these blobs are typically much smaller than the original files. Additionally, during a git commit or routine garbage collection (git gc), Git packages loose blobs into highly compressed binary packfiles, reducing the storage overhead to a minimum.

How do I undo a git add?

If you want to unstage a file (remove it from the staging area but keep your changes in the working directory), run:

git restore --staged <file>

If you are using an older version of Git, use:

git reset HEAD <file>

Can I stage files that are already ignored in .gitignore?

No, git add will ignore files listed in .gitignore by default. If you try to add an ignored file, Git will display an error message. If you must add it anyway, bypass the safety check by using the force flag: git add -f <file>.


Test Your Knowledge

Loading quiz…