本页内容

git status

先说结论: git status 是一个只读命令,用来检查当前工作目录、暂存区(索引)和提交历史(HEAD)的状态。它快速比较这三种状态,报告哪些文件未跟踪、已修改,或已为下一次提交暂存。在将变更写入磁盘前,它是监控仓库状态的主要工具。


git status 到底做了什么?

要真正掌握 Git,需要抛开“它只是跟踪文件差异的工具”这一简单认识。可以把 Git 想象成管理三棵不同内容树的系统。这三棵树代表代码生命周期中的不同阶段:

  1. 工作目录(或工作树): 操作系统文件系统中的实际沙盒。当你在 VS Code 或 Vim 中编辑文件时,实际上是在直接修改工作树。
  2. 索引(或暂存区): 存储在 .git/index 中的二进制缓存文件。它相当于提交前的清单——一个扁平的目录列表,包含下一次提交所需文件的准确路径、权限和 SHA-1 哈希值。
  3. 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)

底层过程:

  1. Git 扫描工作目录,找到 README.md。
  2. Git 尝试读取 .git/index,但索引文件仍不存在。
  3. 文件存在于工作目录而不在索引中,因此 Git 将其标记为未跟踪。

第 3 步:暂存文件

使用 git add 暂存该文件。

$ git add README.md

此时 .git/ 内会立即发生两件事:

  1. Git 压缩 README.md 的内容,并将新的 blob 对象写入 .git/objects/。
  2. 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

底层过程:

  1. Git 读取 .git/HEAD -> refs/heads/main。由于尚未有提交,refs/heads/main 仍不存在。
  2. Git 读取 .git/index,找到一个条目:README.md 指向 blob c5d3bc8d1393...。
  3. HEAD 为空,而索引包含 README.md,这表示索引中有一个 HEAD 没有的文件。因此,该文件被报告为已暂存、准备提交(新文件)。
  4. 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

底层过程:

  1. 这次提交创建了一个提交对象(哈希:55a420b...)和一个表示仓库状态的树对象。
  2. Git 更新 .git/refs/heads/main,使其包含这个新提交对象的哈希。
  3. 运行 git status 时,Git 解析 HEAD -> refs/heads/main -> 提交 55a420b。
  4. Git 将提交 55a420b 中的树(其中列出 README.md 及其 blob c5d3bc8d1393...)与暂存索引 .git/index(其中也列出同一个文件及同一个 blob)比较。哈希完全一致,表示没有已暂存的变更。
  5. 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")

底层过程:

  1. Git 对 README.md 调用 lstat() 并读取元数据。与 .git/index 中的条目相比,修改时间(mtime)和文件大小发生了变化。
  2. Git 打开 README.md,计算修改后内容的 SHA-1 哈希,得到新哈希:c7cb148f2e21f06939fe9ac90d42a302294f12dd。
  3. 由于计算出的哈希不同于 .git/index 中保存的哈希(c5d3bc8d1393...),Git 报告该文件已修改但未暂存。
  4. 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,再获取准确、最新的跟踪信息。


检验你的掌握情况

正在加载测验…