git status
TL;DR: git status is a read-only command that inspects the current state of your working directory, staging area (index), and commit history (HEAD). By performing a rapid three-way comparison between these three states, it reports which files are untracked, modified, or staged for the next commit. It is the primary tool for monitoring repository state before writing changes to disk.
What Does git status Actually Do?
To truly master Git, you must discard the mental model that it is simply a tool tracking file diffs. Instead, visualize Git as a manager of three distinct trees of content. These trees represent different stages of your code’s lifecycle:
- The Working Directory (or Working Tree): The actual sandbox on your operating system’s filesystem. When you edit a file in VS Code or Vim, you are directly modifying the Working Directory.
- The Index (or Staging Area): A binary cache file stored at
.git/index. It acts as a pre-commit manifest—a single flat directory listing containing the exact file paths, permissions, and SHA-1 hashes of the files that will make up the next commit. - HEAD (The Last Commit): A pointer to the commit object at the tip of your current branch. HEAD represents the most recent checkpoint of your repository.
graph LR
subgraph "The Three Trees of Git"
A[Working Directory<br>Local Files] -->|git add| B[Index / Staging Area<br>.git/index]
B -->|git commit| C[HEAD<br>Last Commit]
end
style A fill:#f9f,stroke:#333,stroke-width:2px
style B fill:#bbf,stroke:#333,stroke-width:2px
style C fill:#bfb,stroke:#333,stroke-width:2px
When you invoke git status, Git does not execute a full scan of your commit history. Instead, it runs a highly optimized three-way diff:
- Comparison A: Index vs. HEAD. Git compares the path-and-hash mappings in the staging area (
.git/index) to the tree object pointed to byHEAD. If a file has a different hash in the Index compared to HEAD, or if it exists in the Index but not in HEAD, it is categorized as “Changes to be committed” (staged). - Comparison B: Working Directory vs. Index. Git compares the current files on your hard drive against the path-and-hash mappings stored in the Index. If a file’s state in the working directory differs from its staging record, it is categorized as “Changes not staged for commit” (modified).
- Comparison C: Working Directory vs. Index (Untracked Check). Any file found in the working directory that has no corresponding entry in the Index (and is not ignored by
.gitignorerules) is categorized as “Untracked files”.
The Stat Cache: Why git status is Instantaneous
In a massive project with hundreds of thousands of files, hashing every single file in the working directory to compare it against the Index would make git status painfully slow.
To prevent this, Git utilizes a technique called Stat Caching. The .git/index file contains metadata for every tracked file, including:
- mtime: The timestamp of the last modification.
- ctime: The timestamp of the last metadata change.
- dev / ino: The filesystem device and inode numbers.
- uid / gid: The user and group owners.
- file size: The file size in bytes.
When you run git status, Git queries the operating system for the filesystem stats of your working directory files (a very fast lstat() system call). If the file size, inode, or modification time matches the cached values in .git/index, Git guarantees that the file content has not changed. It skips reading the file content and skips recalculating the SHA-1 hash entirely. Only if the stat metadata differs will Git open the file, hash it, and check for differences.
What Happens Inside .git/ (Hands-on exploration)
Let’s dissect how git status behaves by creating a fresh repository and observing how its internal file comparisons change at every step of a file lifecycle.
Step 1: Initialize the Repository
Let’s start by initializing a clean repository using git init.
$ mkdir git-status-demo && cd git-status-demo
$ git init
Initialized empty Git repository in /git-status-demo/.git/
If we check the state of the .git directory, we notice there is no index file yet, and HEAD points to a default branch that doesn’t have any commits.
$ cat .git/HEAD
ref: refs/heads/main
$ ls -l .git/index
ls: cannot access '.git/index': No such file or directory
If we run git status now:
$ git status
On branch main
No commits yet
nothing to commit (create/copy files and use "git add" to track)
Under the hood: Git checked for .git/index. Since it didn’t find one, it determined there are zero tracked files. It read .git/HEAD to find the current branch (main), resolved the branch reference (which doesn’t exist yet because there are no commits), and concluded that there are no commits.
Step 2: Introduce an Untracked File
Let’s create a file named README.md.
$ echo "# Git Status Demo" > README.md
Now let’s check git status:
$ git status
On branch main
No commits yet
Untracked files:
(use "git add <file>..." to include in what will be committed)
README.md
nothing added to commit but untracked files present (use "git add" to track)
Under the hood:
- Git scans the working directory and finds
README.md. - It attempts to read
.git/index. The index file still does not exist. - Because the file exists in the working directory but not in the index, Git marks it as untracked.
Step 3: Staging the File
Let’s stage the file using git add.
$ git add README.md
Instantly, two things happen inside .git/:
- Git compresses the contents of
README.mdand writes a new blob object into.git/objects/. - Git creates the
.git/indexfile, writing the entry forREADME.mdincluding its file stats and the blob’s SHA-1 hash.
Let’s verify the index entry using the plumbing command git ls-files --stage:
$ git ls-files --stage
100644 c2ba50567fbf9d424b423d242dc541f5e8d91a99 0 README.md
Let’s verify the blob exists in .git/objects/:
$ git cat-file -t c2ba50567fbf9d424b423d242dc541f5e8d91a99
blob
$ git cat-file -p c2ba50567fbf9d424b423d242dc541f5e8d91a99
# Git Status Demo
Now let’s run git status:
$ git status
On branch main
No commits yet
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: README.md
Under the hood:
- Git reads
.git/HEAD->refs/heads/main. The referencerefs/heads/mainstill does not exist (no commits yet). - Git reads
.git/indexand finds one entry:README.mdpointing to blobc2ba50567fbf9.... - Since
HEADis empty and the index containsREADME.md, the index has a file that is not inHEAD. Thus, the file is reported as staged for commit (new file). - Git checks the working directory stat of
README.mdagainst the stat cached in.git/index. They match, so there are no unstaged modifications.
Step 4: Making a Commit
Let’s commit the change using git commit.
$ git commit -m "Initial commit"
[main (root-commit) 55a420b] Initial commit
1 file changed, 1 insertion(+)
create mode 100644 README.md
Now let’s check git status:
$ git status
On branch main
nothing to commit, working tree clean
Under the hood:
- The commit created a commit object (hash:
55a420b...) and a tree object representing the repository state. - Git updated
.git/refs/heads/mainto contain the hash of this new commit object. - When
git statusruns, it resolvesHEAD->refs/heads/main-> commit55a420b. - It compares the tree inside commit
55a420b(which listsREADME.mdwith blobc2ba50567fbf9...) with the staging index.git/index(which also listsREADME.mdwith blobc2ba50567fbf9...). The hashes are identical, meaning no staged changes. - It compares the index stats with the filesystem stats. They match, meaning no modified changes.
Step 5: Modifying the File
Let’s modify README.md in the working directory:
$ echo "# Git Status Demo" > README.md
$ echo "Added a new line of text." >> README.md
Let’s run git status:
$ git status
On branch main
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: README.md
no changes added to commit (use "git add" and/or "git commit -a")
Under the hood:
- Git calls
lstat()onREADME.mdand reads its metadata. The modified time (mtime) and file size have changed compared to the entry in.git/index. - Git opens
README.md, calculates the SHA-1 hash of the modified content (which yields a new hash:d78be558c49e75551bead5b2f2c8d5be5a21074e). - Since this computed hash differs from the one stored in
.git/index(c2ba50567fbf9...), Git reports the file as modified but unstaged. - Git compares the index entry to
HEAD’s tree entry. Both havec2ba50567fbf9.... Since they match, there are no changes staged.
graph TD
subgraph "Three-Way Hash Comparison"
HEAD["HEAD Commit Tree<br>Hash: c2ba505..."]
Index["Staging Index<br>Hash: c2ba505..."]
Work["Working Directory<br>Hash: d78be55..."]
end
HEAD ===|Staged: Match| Index
Index---|Modified: Mismatch| Work
Detailed Command Options & Advanced Usage
While git status is simple, several advanced options make it more powerful for scripting, debugging, and viewing complex states.
1. The Short Format (--short or -s)
When working with many modified files, the default verbose output can be overwhelming. The short format displays changes in a compact two-column layout.
$ git status -s
M README.md
?? new_script.js
The columns represent the Staging Area (column 1) and the Working Directory (column 2):
M(space in col 1,Min col 2): The file has been modified in the working directory, but the modification is not staged.M(Min col 1, space in col 2): The file was modified and the change is staged in the index.MM(both columns): The file was modified, those changes were staged, and the file was modified again in the working directory.A: The file is newly added to the staging area.??: The file is untracked.
2. Including Ignored Files (--ignored)
By default, files matching patterns in .gitignore are completely hidden from status. If you are troubleshooting why a specific file isn’t being tracked, use the --ignored flag.
$ echo "logs/" > .gitignore
$ mkdir logs && touch logs/debug.log
$ git status --ignored
On branch main
...
Ignored files:
(use "git add -f <file>..." to include in what will be committed)
logs/
3. Scripting with --porcelain
If you are writing a shell script, git alias, or CI/CD script that needs to check if the repository is dirty, never parse the standard output of git status (it can change between Git versions). Use --porcelain instead.
$ git status --porcelain
M README.md
?? new_script.js
The --porcelain flag guarantees that the output format remains backwards-compatible and stable across all future versions of Git. It prints output in a predictable machine-readable format, omitting colors and helper text.
To quickly check if a repository has uncommitted changes in a bash script:
if [ -z "$(git status --porcelain)" ]; then
echo "Working directory is clean. Proceeding..."
else
echo "Error: You have uncommitted changes!" >&2
exit 1
fi
Real-World Scenario: File Permissions & Line Endings
Sometimes git status will show a file as modified, but when you run git diff, you see absolutely no changes in the code. This is usually caused by two common issues: line endings or file mode changes.
Scenario A: File Permissions (chmod)
Git tracks file executable bits (100755 for executables, 100644 for regular files). If you or a script changes the permissions of a file (e.g., chmod +x script.sh), Git will report the file as modified.
If this happens on a project where file permissions don’t matter (like a cross-platform Windows/macOS/Linux team), you can configure Git to ignore permission changes:
$ git config core.filemode false
Scenario B: Line Ending Inconsistencies (LF vs CRLF)
Windows systems historically use Carriage Return + Line Feed (CRLF) to end lines, while Linux and macOS use Line Feed (LF). If a text editor automatically changes your line endings, Git will flag the entire file as modified because the binary hashes are different.
To fix line ending mismatches globally, configure Git’s autocrlf setting:
# On Windows
$ git config --global core.autocrlf true
# On macOS/Linux
$ git config --global core.autocrlf input
Common Pitfalls and Mistakes
1. Running git status in a Subdirectory
If you run git status from a nested subdirectory, Git will by default show paths relative to that subdirectory, which can sometimes make paths look confusing. To see paths relative to the root directory regardless of where you are running the command, run:
$ git status --relative=false
2. A Staged File is Still Shown under “Changes not staged for commit”
This is a classic point of confusion for beginners. If you stage a file using git add file.txt, and then open your editor and add another line to file.txt, git status will list the file twice:
Changes to be committed:
modified: file.txt
Changes not staged for commit:
modified: file.txt
This happens because Git staged the exact snapshot of the file at the moment you ran git add. The subsequent edits exist only in your working directory and must be staged again.
3. File in .gitignore Still Showing Up
If a file was already tracked in Git before you added it to .gitignore, it will continue to show up in git status when changed. .gitignore only prevents untracked files from being tracked. To stop tracking the file without deleting it from your computer, you must clear it from the index:
$ git rm --cached path/to/file.txt
Command Quick Reference
| Command | Description |
|---|---|
git status | Shows status in full verbose format (default). |
git status -s / --short | Shows status in a condensed, two-column format. |
git status -b / --branch | Shows branch and tracking branch details (adds context to short format). |
git status --ignored | Shows files that are ignored by .gitignore. |
git status --porcelain | Output optimized for machine parsing and scripting. |
git status -uall | Shows individual files within untracked directories rather than just the directory name. |
FAQ (People Also Ask)
Why does git status say “On branch main, nothing to commit, working tree clean”?
This message indicates that all three of Git’s trees are in perfect alignment. The SHA-1 hashes of all files in your Working Directory match the hashes listed in your Staging Index (.git/index), which in turn match the hashes registered in the HEAD commit’s tree. There are no modifications to make or record.
How does git status know a file has been renamed?
Git does not actually track renames explicitly. It detects them. When a file is deleted in one path and added in another, git status reads the index and compares the content hashes. If the content of the deleted file and the added file is identical (or highly similar), Git calculates a similarity index and reports the file as renamed (e.g., renamed: old_file.txt -> new_file.txt).
Can running git status change my files?
No. git status is entirely read-only with respect to your working directory and commit history. It inspects files and compares metadata. At most, it may update the stat cache in .git/index to match the current filesystem times (improving performance for subsequent commands), but it will never modify your source code or revert changes.
How do I use git status to check if I am behind/ahead of a remote branch?
If your local branch is tracking a remote branch (e.g., origin/main), git status will compare your local branch’s HEAD reference to the cached remote tracking reference (.git/refs/remotes/origin/main). It will display messages like “Your branch is ahead of ‘origin/main’ by 2 commits” or “Your branch is behind…”. Note that this is based on your last fetch; Git does not query the remote server during git status, so run git fetch first to get accurate up-to-date tracking information.
Test Your Knowledge
Loading quiz…