☰
isomorphic-git status 全解析:用纯 JavaScript 精准判断任意文件的 Git 状态
2026/9/26 10:28:52 网站建设 项目流程
  • 开发工具

【免费下载链接】isomorphic-git

A pure JavaScript implementation of git for node and browsers!

项目地址:https://gitcode.com/gh_mirrors/is/isomorphic-git
点击查看免费下载

git.status是 isomorphic-git 提供的单文件状态查询 API:只要给定工作区目录与文件路径,它就会返回一个描述该文件当前 Git 状态的字符串(如"unmodified"、"*modified"、"ignored")。本指南以 0.70.7 版本文档为核心,结合仓库源码与测试用例,带你完整掌握status的参数语义、11 种返回值的判定逻辑、.gitignore边界行为以及缓存与性能调优方案。读完本文,你将能够在自己基于 isomorphic-git 构建的工具(如 CLI、CI 检查、编辑器插件)中精确复刻原生git status的文件级判断能力。

快速开始:一行代码读取文件状态

git.status的用法非常直接,只需要提供文件系统、工作区目录和目标文件路径:

let status = await git.status({ dir: '/', filepath: 'README.md' }) console.log(status)

在上面的示例中,dir指向仓库的工作区根目录,filepath是相对于该目录的路径(示例中为README.md)。调用后会返回一个Promise<string>,解析为该文件当前的 Git 状态字符串。

在 Node.js 环境中,你需要同时传入fs参数(或通过插件系统注册文件系统):

import git from 'isomorphic-git' import fs from 'fs' const status = await git.status({ fs, dir: '/path/to/repo', filepath: 'README.md' }) console.log(status) // 例如 "unmodified"

关于文件系统的注册与"Bring Your Own FS"(BYOFS)机制,可参考 plugin_fs.md 与 guide-fs.md;dir与gitdir两个概念的区别见 dir-vs-gitdir.md。

参数详解

status的核心参数如下表所示:

参数类型 [= 默认值]描述
corestring = 'default'插件注入时使用的插件核心标识符
fs [deprecated]FileSystem包含 git 仓库的文件系统。会覆盖插件系统提供的fs
dirstring工作树目录路径
gitdirstring = join(dir, '.git')git 目录路径
filepathstring要查询状态的文件路径
returnPromise<string>解析成功后返回文件的 git 状态

从源码看参数的默认值与校验

在 src/api/status.js 的实现中,参数默认值与校验逻辑清晰可见:

export async function status({ fs: _fs, dir, gitdir = join(dir, '.git'), filepath, cache = {}, refresh = true, }) { try { assertParameter('fs', _fs) assertParameter('gitdir', gitdir) assertParameter('filepath', filepath) const fs = new FileSystem(_fs) const updatedGitdir = await discoverGitdir({ fsp: fs, dotgit: gitdir }) // ...

几个值得注意的细节:

  • gitdir默认值为join(dir, '.git'):绝大多数场景只需传dir。只有当使用裸仓库(bare repository)或gitdir与dir分离时才需要显式传入,与原生 git 的--work-tree/--git-dir参数对应。
  • cache默认是空对象{}:用于跨调用共享中间解析结果(packfile 等),详见下文"性能优化"一节。
  • refresh默认是true:这是源码中比文档更进一步的参数(0.70.7 版本文档未列出,但源码已实现)。当工作区文件内容与暂存 blob 一致时,status会顺手刷新.git/index的 stat 缓存;设为false后,调用对 index 变为只读,代价是后续对 stat 信息已漂移的文件会重新计算 SHA1。
  • discoverGitdir:即使传入的是工作区路径而非.git目录,也会向上探测真正的 git 目录位置,兼容 linked worktree 等布局。

filepath 的路径约定

filepath必须是相对dir的路径,源码中通过join(dir, filepath)组装实际文件位置,并通过getOidAtPath按/分段在 HEAD 树中逐层定位(见 src/api/status.js 中的getOidAtPath函数)。这意味着嵌套目录(如src/utils/index.js)也只需直接给出相对路径字符串。

返回值详解:11 种状态的完整语义

status可能返回的字符串值如下表:

status描述
"ignored"文件被某个 .gitignore 规则忽略
"unmodified"文件与 HEAD 提交一致,未做修改
"*modified"文件有修改,但尚未暂存
"*deleted"文件已被删除,但删除操作尚未暂存
"*added"文件未被跟踪(untracked),尚未暂存
"absent"文件在 HEAD 提交、暂存区和工作区中均不存在
"modified"文件有修改,且已暂存
"deleted"文件已被删除,且已暂存
"added"此前未被跟踪的文件,现已暂存
"*unmodified"工作区与 HEAD 提交一致,但 index(暂存区)内容不同
"*absent"文件不在工作区和 HEAD 提交中,但存在于暂存区

命名规律值得单独说明:带*前缀的状态表示"存在未被暂存(staged)的差异",不带前缀则表示差异已经进入暂存区或完全没有差异。例如"*modified"是"改了但没git add","modified"是"改了且已git add"。

从源码看判定逻辑:H/I/W 三位布尔模型

11 种状态并非凭空枚举,而是由三个布尔量组合而成。在 src/api/status.js 中可以看到核心判定模型:

const H = treeOid !== null // head:文件是否存在于 HEAD 提交 const I = indexEntry !== null // index:文件是否存在于暂存区 const W = stats !== null // working dir:文件是否存在于工作区
  • treeOid来自getHeadTree+getOidAtPath:解析HEAD引用得到 commit,再读取其 tree 对象,按路径逐层查找该文件的 blob OID;
  • indexEntry来自GitIndexManager.acquire:在 index 中线性查找路径匹配的条目;
  • stats来自fs.lstat(join(dir, filepath)):工作区中是否存在该文件。

随后源码按照 H/W/I 的 8 种组合逐一分支(源码中以---、-A-、--A等注释直观标注了每种组合):

if (!H && !W && !I) return 'absent' // --- if (!H && !W && I) return '*absent' // -A- if (!H && W && !I) return '*added' // --A if (!H && W && I) { // 新增文件:比较工作区 oid 与暂存 oid return workdirOid === indexEntry.oid ? 'added' : '*added' } if (H && !W && !I) return 'deleted' // A-- if (H && !W && I) return '*deleted' // AA- / AB- if (H && W && !I) return '*undeleted' // A-A / A-B(从 index 删除但工作区还在) if (H && W && I) { // 全部存在:进一步比较三个 oid if (workdirOid === treeOid) { return workdirOid === indexEntry.oid ? 'unmodified' : '*unmodified' } else { return workdirOid === indexEntry.oid ? 'modified' : '*modified' } }

其中H && W && I(文件在三个位置都存在)是最常见的情况,此时需要调用getWorkdirOid计算工作区文件的 blob OID,再与 HEAD OID、index OID 两两比较:

  • 三者一致 →"unmodified";
  • 工作区 == HEAD、但 index 不同 →"*unmodified"(即"暂存区与 HEAD 不一致",典型场景是git add后又把文件恢复成原样);
  • 工作区 != HEAD、但工作区 == index →"modified"(修改已暂存);
  • 工作区 != HEAD 且 != index →"*modified"(修改未暂存)。

值得注意的是源码中还包含了文档未列出的两种额外状态:"*undeleted"(文件已从 index 删除但工作区仍存在且内容与 HEAD 一致)与"*undeletemodified"(已从 index 删除但工作区存在且有修改),它们对应H && W && !I的分支。0.70.7 版本文档的状态表没有收录这两项,但源码的 JSDoc 与测试均已覆盖,属文档滞后于实现的情况。

状态流转的完整测试验证

仓库的tests/test-status.js 完整演示了"修改 → 暂存"过程中的状态迁移:

// 初始:a.txt 未修改,b.txt 改了没 add,c.txt 删了没 add,d.txt 未跟踪,e.txt 不存在 const a = await status({ fs, dir, gitdir, filepath: 'a.txt' }) // 'unmodified' const b = await status({ fs, dir, gitdir, filepath: 'b.txt' }) // '*modified' const c = await status({ fs, dir, gitdir, filepath: 'c.txt' }) // '*deleted' const d = await status({ fs, dir, gitdir, filepath: 'd.txt' }) // '*added' const e = await status({ fs, dir, gitdir, filepath: 'e.txt' }) // 'absent' // 逐个 add / remove 之后 await add({ fs, dir, gitdir, filepath: 'b.txt' }) // 现在 b 是 'modified' await remove({ fs, dir, gitdir, filepath: 'c.txt' }) // 现在 c 是 'deleted' await add({ fs, dir, gitdir, filepath: 'd.txt' }) // 现在 d 是 'added'

测试还覆盖了两个"怪癖"场景:

  • "*unmodified":把a.txt改成'Hi'后add,再写回原内容。此时工作区与 HEAD 一致但 index 记录的是'Hi'的 OID,返回"*unmodified";
  • "*undeleted":remove后工作区文件仍在,返回"*undeleted";
  • "*absent":新增e.txt并add,然后删除工作区文件,此时文件"仅存在于 index",返回"*absent"。

ignored 状态:.gitignore 的精确边界

当文件在 HEAD 与 index 中都不存在(即未被跟踪)时,status才会检查.gitignore:

if (treeOid === null && indexEntry === null) { const ignored = await GitIgnoreManager.isIgnored({ fs, gitdir: updatedGitdir, dir, filepath, }) if (ignored) return 'ignored' }

这一点与原生 git 的行为保持一致:.gitignore只管辖未跟踪文件。测试用例专门验证了"已跟踪文件命中 .gitignore 规则"的边界情况:

// 先把 i.txt 加入跟踪,再写入 '.gitignore' 忽略它,并修改 i.txt 内容 expect(await status({ fs, dir, gitdir, filepath: 'i.txt' })).toEqual('*added')

即使.gitignore写了i.txt,已跟踪文件的真实状态(此时它是"有修改的未跟踪文件")仍然会如实上报,而不是错误地返回"ignored"。源码注释也明确说明了这一点:statusMatrix同样遵循该语义。

测试还覆盖了嵌套忽略规则:f.txt、g/g.txt、h/h.txt均因各级.gitignore返回"ignored",而i/i.txt未被忽略则返回"*added"。

性能优化:cache 参数与 statusMatrix

不要逐文件裸调用 status

如果对仓库每个文件都单独调用一次status而不共享任何中间结果,性能会非常糟糕。原因在于每次调用都可能需要重新读取并解析.git/objects/pack中的 packfile——对大型仓库而言,单个文件的状态查询可能耗 10ms~100ms,累积到数千个文件就是数分钟级别的开销,并行调用甚至会同时挤爆内存(详见 cache.md 中的演示与实测数据)。

共享 cache 对象

解决方法是传入同一个cache对象,让 packfile 的解析结果在多次调用间复用:

let cache = {} for (const filepath of await git.listFiles({ fs, dir, cache })) { console.log(`${filepath}: ${await git.status({ fs, dir, filepath, cache })}`) } // 用完后丢弃引用即可,cache 只是一个普通对象,会被垃圾回收 cache = {}

cache的具体机制是:isomorphic-git 会把中间数据以 Symbol 属性挂到传入对象上,因此不要直接操作cache内部;清空缓存只需删除所有引用等待 GC。src/api/status.js 中cache = {}的默认值也意味着:不传 cache,每次调用都是"零缓存"的全量计算。

批量场景优先选 statusMatrix

当需要一次性了解整个仓库的状态时,官方推荐使用statusMatrix(src/api/statusMatrix.js,文档见 statusMatrix.md)。它基于walk机制一次性遍历 HEAD / WORKDIR / STAGE 三棵树,以[filepath, head, workdir, stage]四元组构成的二维数组返回全部文件状态,比逐文件status快几个数量级(cache.md 中给出的实测对比是 843ms vs 2 分钟以上)。

选择建议:

  • 只关心单个或少数文件→status(语义直白,返回人类可读字符串);
  • 需要全量状态报告、变更清单、CI 检查→statusMatrix(返回紧凑矩阵,且支持filter、filepaths、ref等参数,还能通过patternglob 过滤,见 statusMatrix.md 的示例);
  • 在status循环场景中,务必共享cache对象。

边界场景与实现细节

空仓库(无任何提交)

getHeadTree在解析HEAD时若抛出NotFoundError(分支尚无提交),会返回空树:

try { oid = await GitRefManager.resolve({ fs, gitdir: updatedGitdir, ref: 'HEAD' }) } catch (e) { if (e instanceof NotFoundError) return [] // 处理没有提交的新分支 }

因此新init的仓库也能正常工作。测试验证:在空仓库中写入a.txt返回"*added",add过的b.txt返回"added"。

core.autocrlf 与 CRLF/LF 归一化

getWorkdirOid计算工作区文件哈希时,会读取core.autocrlf配置并按其归一化规则处理,避免"CRLF 检出导致 LF blob 被误判为已修改":

const config = await GitConfigManager.get({ fs, gitdir: updatedGitdir }) const autocrlf = await config.get('core.autocrlf') const object = await fs.read(join(dir, filepath), { autocrlf })

对应测试(honours core.autocrlf when hashing the working copy)验证:在core.autocrlf = true的仓库中,blob 以 LF 存储、工作区为 CRLF 时,status仍返回"unmodified"。

refresh 参数与 index stat 缓存

默认refresh = true时,若工作区文件内容哈希与 index OID 一致但 stat 信息不同(例如 mtime 被触碰),status会顺手把新 stat 写回 index(源码中通过GitIndexManager.acquire调用index.insert),让后续调用能直接复用缓存 OID 而免于重新哈希。当fs.stat无法提供可靠大小(如 BrowserFS 的 HTTP 后端返回 size = -1)时会跳过刷新。测试does not modify .git/index when refresh is false验证了refresh: false下 index 文件字节级不变(只读语义)。

小结

git.status是 isomorphic-git 文件状态判断的基石 API:三个参数(dir、gitdir、filepath)即可获得语义精确的状态字符串;其内部通过 HEAD / index / workdir 的三位布尔模型加上 OID 两两比较,完整复刻了原生 git 的状态机;.gitignore只影响未跟踪文件、core.autocrlf参与工作区哈希计算等细节,也与原生 git 保持了一致。在批量场景中,请务必使用共享cache或直接切换到statusMatrix,避免陷入逐文件解析 packfile 的性能陷阱。

进一步阅读:源码实现、测试用例、statusMatrix 文档、cache 参数详解、dir 与 gitdir 的区别。

  • 开发工具

【免费下载链接】isomorphic-git

A pure JavaScript implementation of git for node and browsers!

项目地址:https://gitcode.com/gh_mirrors/is/isomorphic-git
点击查看免费下载
上一篇:HsMod 炉石传说插件:60+ 功能解决换皮肤、MMR 显示与挂机开包
下一篇:WechatHook:新手也能跑通的微信自动化实验田(附完整避坑清单)

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询