本页内容
git status
先说结论: git status 是一个只读命令,用来检查当前工作目录、暂存区(索引)和提交历史(HEAD)的状态。它快速比较这三种状态,报告哪些文件未跟踪、已修改,或已为下一次提交暂存。在将变更写入磁盘前,它是监控仓库状态的主要工具。
git status 到底做了什么?
要真正掌握 Git,需要抛开“它只是跟踪文件差异的工具”这一简单认识。可以把 Git 想象成管理三棵不同内容树的系统。这三棵树代表代码生命周期中的不同阶段:
- 工作目录(或工作树): 操作系统文件系统中的实际沙盒。当你在 VS Code 或 Vim 中编辑文件时,实际上是在直接修改工作树。
- 索引(或暂存区): 存储在
.git/index中的二进制缓存文件。它相当于提交前的清单——一个扁平的目录列表,包含下一次提交所需文件的准确路径、权限和 SHA-1 哈希值。 - HEAD(最近一次提交): 指向当前分支顶端提交对象的指针。HEAD 表示仓库最近的检查点。
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
执行 git status 时,Git 不会完整扫描提交历史,而是运行高度优化的三方比较:
- 比较 A:索引与 HEAD。 Git 将暂存区(
.git/index)中的路径—哈希映射与HEAD指向的树对象进行比较。如果索引中的文件哈希与 HEAD 不同,或文件存在于索引但不存在于 HEAD,就会归类为**“待提交的变更”**(已暂存)。 - 比较 B:工作目录与索引。 Git 将硬盘上的当前文件与索引中保存的路径—哈希映射进行比较。如果工作树中的文件状态不同于暂存记录,就会归类为**“未暂存的变更”**(已修改)。
- 比较 C:工作目录与索引(检查未跟踪文件)。 工作树中凡是没有对应索引条目、且未被
.gitignore规则忽略的文件,都会归类为**“未跟踪文件”**。
统计缓存:为什么 git status 这么快
在拥有数十万个文件的大型项目中,如果每次都给工作树中的每个文件计算哈希,再与索引比较,git status 会慢得难以使用。
为避免这种情况,Git 使用统计信息缓存技术。.git/index 文件保存每个已跟踪文件的元数据,包括:
- mtime: 上次修改的时间戳。
- ctime: 上次元数据变更的时间戳。
- dev / ino: 文件系统设备号和 inode 号。
- uid / gid: 用户和用户组所有者。
- 文件大小: 以字节为单位的文件大小。
运行 git status 时,Git 会向操作系统查询工作树中文件的文件系统统计信息(这是非常快速的 lstat() 系统调用)。如果文件大小、inode 或修改时间与 .git/index 中缓存的值一致,Git 就能确定文件内容没有变化。此时它会跳过读取文件内容,也完全跳过重新计算 SHA-1 哈希。只有统计元数据不同,Git 才会打开文件、计算哈希并检查差异。
.git/ 内部发生了什么(动手探索)
下面通过创建一个全新仓库,观察文件生命周期每一步的内部文件比较变化,拆解 git status 的工作方式。
第 1 步:初始化仓库
首先使用 git init 初始化一个干净的仓库。
$ mkdir git-status-demo && cd git-status-demo
$ git init
Initialized empty Git repository in /git-status-demo/.git/
检查 .git 目录的状态时,可以看到索引文件还不存在,HEAD 指向一个尚未包含任何提交的默认分支。
$ cat .git/HEAD
ref: refs/heads/main
$ ls -l .git/index
ls: cannot access '.git/index': No such file or directory
现在运行 git status:
$ git status
On branch main
No commits yet
nothing to commit (create/copy files and use "git add" to track)
底层过程: Git 检查 .git/index。由于找不到该文件,它判断当前没有已跟踪文件;随后读取 .git/HEAD,找到当前分支(main),解析分支引用。由于尚未产生提交,该引用还不存在,因此 Git 得出“目前没有提交”的结论。
第 2 步:引入未跟踪文件
创建名为 README.md 的文件。
$ echo "# Git Status Demo" > README.md
现在检查 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)
底层过程:
- Git 扫描工作目录,找到
README.md。 - Git 尝试读取
.git/index,但索引文件仍不存在。 - 文件存在于工作目录而不在索引中,因此 Git 将其标记为未跟踪。
第 3 步:暂存文件
使用 git add 暂存该文件。
$ git add README.md
此时 .git/ 内会立即发生两件事:
- Git 压缩
README.md的内容,并将新的 blob 对象写入.git/objects/。 - Git 创建
.git/index文件,写入README.md的索引条目,包括文件统计信息和 blob 的 SHA-1 哈希。
使用底层命令 git ls-files --stage 验证索引条目:
$ git ls-files --stage
100644 c5d3bc8d139371ff4cf9e97623b2255fb722b2d9 0 README.md
验证 blob 是否存在于 .git/objects/ 中:
$ git cat-file -t c5d3bc8d139371ff4cf9e97623b2255fb722b2d9
blob
$ git cat-file -p c5d3bc8d139371ff4cf9e97623b2255fb722b2d9
# Git Status Demo
现在运行 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
底层过程:
- Git 读取
.git/HEAD->refs/heads/main。由于尚未有提交,refs/heads/main仍不存在。 - Git 读取
.git/index,找到一个条目:README.md指向 blobc5d3bc8d1393...。 - HEAD 为空,而索引包含
README.md,这表示索引中有一个 HEAD 没有的文件。因此,该文件被报告为已暂存、准备提交(新文件)。 - Git 将
README.md的工作目录统计信息与.git/index中缓存的统计信息比较。两者一致,因此没有未暂存的修改。
第 4 步:创建提交
使用 git commit 提交变更。
$ git commit -m "Initial commit"
[main (root-commit) 55a420b] Initial commit
1 file changed, 1 insertion(+)
create mode 100644 README.md
现在检查 git status:
$ git status
On branch main
nothing to commit, working tree clean
底层过程:
- 这次提交创建了一个提交对象(哈希:
55a420b...)和一个表示仓库状态的树对象。 - Git 更新
.git/refs/heads/main,使其包含这个新提交对象的哈希。 - 运行
git status时,Git 解析HEAD->refs/heads/main-> 提交55a420b。 - Git 将提交
55a420b中的树(其中列出README.md及其 blobc5d3bc8d1393...)与暂存索引.git/index(其中也列出同一个文件及同一个 blob)比较。哈希完全一致,表示没有已暂存的变更。 - Git 将索引统计信息与文件系统统计信息比较。两者一致,表示没有已修改的变更。
第 5 步:修改文件
在工作树中修改 README.md:
$ echo "# Git Status Demo" > README.md
$ echo "Added a new line of text." >> README.md
运行 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")
底层过程:
- Git 对
README.md调用lstat()并读取元数据。与.git/index中的条目相比,修改时间(mtime)和文件大小发生了变化。 - Git 打开
README.md,计算修改后内容的 SHA-1 哈希,得到新哈希:c7cb148f2e21f06939fe9ac90d42a302294f12dd。 - 由于计算出的哈希不同于
.git/index中保存的哈希(c5d3bc8d1393...),Git 报告该文件已修改但未暂存。 - Git 将索引条目与 HEAD 的树条目比较。两者都有
c5d3bc8d1393...,因此没有已暂存的变更。
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
命令选项与进阶用法
虽然 git status 很简单,但多个进阶选项能让它更适合编写脚本、调试以及查看复杂状态。
1. 简短格式(--short 或 -s)
处理大量已修改文件时,默认的详细输出可能让人难以快速查看。简短格式以紧凑的两列布局显示变更。
$ git status -s
M README.md
?? new_script.js
两列分别表示暂存区(第 1 列)和工作目录(第 2 列):
M(第 1 列为空格、第 2 列为M):文件在工作树中已修改,但修改尚未暂存。M(第 1 列为M、第 2 列为空格):文件已修改,变更已在索引中暂存。MM(两列都有内容):文件先被修改并暂存,随后又在工作树中再次修改。A:文件新加入暂存区。??:文件未跟踪。
2. 包含被忽略的文件(--ignored)
默认情况下,与 .gitignore 中模式匹配的文件会完全隐藏,不出现在状态信息中。如果你在排查某个文件为何没有被跟踪,可以使用 --ignored 标志。
$ 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. 使用 --porcelain 编写脚本
如果 shell 脚本、Git 别名或 CI/CD 脚本需要检查仓库是否有未提交的变更,不要解析 git status 的标准输出,因为它可能在不同 Git 版本之间变化。应改用 --porcelain。
$ git status --porcelain
M README.md
?? new_script.js
--porcelain 标志保证输出格式向后兼容,并在未来版本中保持稳定。它以可预测、可供机器读取的格式输出内容,省略颜色和辅助说明。
在 Bash 脚本中快速检查仓库是否有未提交变更:
if [ -z "$(git status --porcelain)" ]; then
echo "Working directory is clean. Proceeding..."
else
echo "Error: You have uncommitted changes!" >&2
exit 1
fi
实际场景:文件权限与换行符
有时 git status 会显示文件已修改,但运行 git diff 时却看不到代码有任何变化。通常有两个原因:换行符变化或文件模式变化。
场景 A:文件权限(chmod)
Git 会跟踪文件的可执行位(可执行文件为 100755,普通文件为 100644)。如果你或某个脚本改变了文件权限(例如运行 chmod +x script.sh),Git 就会报告文件已修改。
如果项目不关心文件权限(例如由 Windows、macOS 和 Linux 用户组成的跨平台团队),可以配置 Git 忽略权限变化:
$ git config core.filemode false
场景 B:换行符不一致(LF 与 CRLF)
Windows 系统一直使用回车加换行(CRLF)作为行尾,而 Linux 和 macOS 使用换行(LF)。如果文本编辑器自动改变行尾,Git 会因为二进制哈希不同而将整个文件标记为已修改。
要全局修复换行符不一致问题,请配置 Git 的 autocrlf 设置:
# On Windows
$ git config --global core.autocrlf true
# On macOS/Linux
$ git config --global core.autocrlf input
常见陷阱与错误
1. 在子目录中运行 git status
如果你在嵌套子目录中运行 git status,Git 默认会显示相对于该子目录的路径,有时会让路径看起来难以理解。无论你从哪里运行命令,如果希望路径都相对于根目录显示,请运行:
$ git status --relative=false
2. 已暂存文件仍出现在“未暂存的变更”下
这是初学者经常困惑的一点。如果你使用 git add file.txt 暂存文件,然后在编辑器中又向 file.txt 添加一行,git status 会将该文件列出两次:
Changes to be committed:
modified: file.txt
Changes not staged for commit:
modified: file.txt
这是因为 Git 暂存的是运行 git add 时文件的精确快照。后续编辑只存在于工作树中,必须再次暂存。
3. .gitignore 中的文件仍然出现
如果文件在加入 .gitignore 之前就已被 Git 跟踪,那么它发生变化时仍会出现在 git status 中。.gitignore 只会阻止未跟踪文件被跟踪。若要停止跟踪该文件、但不从电脑中删除它,必须将其从索引中清除:
$ git rm --cached path/to/file.txt
命令速查
| 命令 | 说明 |
|---|---|
git status | 以完整详细格式显示状态(默认)。 |
git status -s / --short | 以紧凑的两列格式显示状态。 |
git status -b / --branch | 显示分支和跟踪分支详情(为简短格式补充上下文)。 |
git status --ignored | 显示被 .gitignore 忽略的文件。 |
git status --porcelain | 输出针对机器解析和脚本编写进行了优化。 |
git status -uall | 在未跟踪目录中显示每个文件,而不只是显示目录名。 |
常见问题(大家还会问)
为什么 git status 会显示“当前分支 main,没有需要提交的内容,工作树干净”?
这条消息表示 Git 的三棵树完全一致:工作树中所有文件的 SHA-1 哈希,与暂存索引(.git/index)中列出的哈希一致;索引中的哈希又与 HEAD 提交树中登记的哈希一致。因此没有需要修改或记录的内容。
git status 如何知道文件被重命名?
Git 并不会显式跟踪重命名,而是通过检测得出。当一个路径中的文件被删除、另一个路径中又添加了文件时,git status 会读取索引并比较内容哈希。如果被删除文件与新添加文件的内容完全相同(或高度相似),Git 会计算相似度并将其报告为重命名(例如,renamed: old_file.txt -> new_file.txt)。
运行 git status 会修改我的文件吗?
不会。对于工作目录和提交历史而言,git status 完全是只读的。它只检查文件并比较元数据。最多,它可能会更新 .git/index 中的统计缓存,使其匹配当前文件系统时间,以提升后续命令的性能;但它绝不会修改源代码或撤销变更。
如何使用 git status 检查自己落后于或领先于远程分支?
如果本地分支正在跟踪远程分支(例如 origin/main),git status 会将本地分支的 HEAD 引用与缓存的远程跟踪引用(.git/refs/remotes/origin/main)进行比较。它会显示类似“你的分支领先于 ‘origin/main’ 2 个提交”或“你的分支落后于……”的消息。请注意,这些信息基于你上一次 fetch;git status 不会在运行时查询远程服务器,因此应先运行 git fetch,再获取准确、最新的跟踪信息。
检验你的掌握情况
正在加载测验…