- 开发工具
【免费下载链接】isomorphic-git
A pure JavaScript implementation of git for node and browsers!
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的核心参数如下表所示:
| 参数 | 类型 [= 默认值] | 描述 |
|---|---|---|
| core | string = 'default' | 插件注入时使用的插件核心标识符 |
| fs [deprecated] | FileSystem | 包含 git 仓库的文件系统。会覆盖插件系统提供的fs |
| dir | string | 工作树目录路径 |
| gitdir | string = join(dir, '.git') | git 目录路径 |
| filepath | string | 要查询状态的文件路径 |
| return | Promise<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!
相关推荐
isomorphic-git isDescendent 使用指南:纯 JavaScript 判断 Git 提交的祖先关系
isomorphic git isDescendent 使用指南:纯 JavaScript 判断 Git 提交的祖先关系 导读 isDescendent 是 i
开发工具探索 **Isomorphic Git**:全栈友好的纯JavaScript实现Git
探索 Isomorphic Git :全栈友好的纯JavaScript实现Git 是一个令人惊艳的开源项目,它完全使用JavaScript编写,可以在Node.
开发工具Tiny RDM 在 macOS 上安装后无法打开怎么解决?
Tiny RDM 在 macOS 上安装后无法打开怎么解决? 在 macOS 上安装 Tiny RDM 桌面版后,有的用户双击启动会收到系统提示,报 不受信任
开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考