- 开发工具
【免费下载链接】isomorphic-git
A pure JavaScript implementation of git for node and browsers!
本篇指南以 isomorphic-git 项目官方 Quick Start 文档(website/versioned_docs/version-1.x/guide-quickstart.md)为骨架,带你在浏览器环境中完成一次完整的 Git 操作之旅:从搭建 LightningFS 虚拟文件系统与 isomorphic-git 运行环境,到克隆真实仓库、查看提交历史、追踪文件状态、暂存修改、删除文件并提交新版本。读完本文,你将掌握 isomorphic-git 的clone、log、status、add、remove、commit六大核心 API 的调用方式、参数含义与底层实现原理,并能在自己的浏览器项目中直接复刻这套工作流。
环境搭建:让 Git 跑在浏览器里
isomorphic-git 的卖点在于它是"同构"的:同一套 API 既可以在 Node.js 环境运行,也可以在浏览器中运行(项目描述即 "A pure JavaScript implementation of git for node and browsers!")。浏览器没有文件系统,也没有 git 二进制,因此官方 Quick Start 使用了两样东西配合:
- LightningFS(
@isomorphic-git/lightning-fs):一个基于 IndexedDB 的浏览器文件系统实现,为 isomorphic-git 提供fs接口; - isomorphic-git 浏览器版 HTTP 客户端:负责执行网络请求,替代 Node.js 的
http模块。
官方文档给出的初始化代码如下:
<script src="https://unpkg.com/@isomorphic-git/lightning-fs"></script> <script src="https://unpkg.com/isomorphic-git"></script> <script type="module"> import http from 'https://unpkg.com/isomorphic-git/http/web/index.js' // Initialize isomorphic-git with a file system window.fs = new LightningFS('fs') // I prefer using the Promisified version honestly window.pfs = window.fs.promises </script>要点说明:
- 两个
<script>标签分别把LightningFS构造器和git全局对象挂到window上; new LightningFS('fs')中的'fs'是虚拟文件系统在 IndexedDB 中的数据库名称;- 浏览器端 HTTP 客户端需要从
isomorphic-git/http/web/index.js以 ES Module 方式导入(对应源码为 src/http/web);Node.js 环境则改用 src/http/node; window.fs.promises是 Promise 化的文件系统 API(pfs.mkdir、pfs.readdir、pfs.writeFile、pfs.unlink等),方便配合await使用。
从源码结构看,所有公开 API 都从 src/index.js 统一导出,同时支持具名导出(import { clone } from 'isomorphic-git')和默认导出对象(import git from 'isomorphic-git',即教程中git.clone的调用方式)。关于fs参数的具体契约,可进一步参考 docs/fs.md。
选择工作目录:在虚拟文件系统里建文件夹
环境就绪后,第一步是选定一个工作目录。Git 操作需要一个"工作树"(working tree),在浏览器场景中它同样存在于 LightningFS 里:
window.dir = '/tutorial' console.log(dir); await pfs.mkdir(dir); // Behold - it is empty! await pfs.readdir(dir);这里把虚拟路径/tutorial作为本次实验的目录,pfs.mkdir创建它,pfs.readdir确认它是空的。教程中的window.dir = '/tutorial'是为了让每个代码块都能访问到共享状态——这种写法与 Docusaurus 交互式文档(js live代码块可在线运行)是配套的;你在自己的项目里完全可以按局部变量或模块作用域组织dir。
关于工作树与 Git 目录的关系,isomorphic-git 有专门的说明文档 docs/dir-vs-gitdir.md:dir是工作树目录,gitdir默认是join(dir, '.git'),即仓库元数据所在的目录。后续所有 API 都接受这个可选的gitdir参数。
克隆仓库:clone 的参数与 CORS 代理
目录就绪后,官方教程克隆了 isomorphic-git 自己的仓库("how meta!")。为了节省时间、带宽和浏览器存储空间,只克隆单个分支、且只取最近 10 个提交:
await git.clone({ fs, http, dir, corsProxy: 'https://cors.isomorphic-git.org', url: 'https://github.com/isomorphic-git/isomorphic-git', ref: 'main', singleBranch: true, depth: 10 }); // Now it should not be empty... await pfs.readdir(dir);为什么需要 corsProxy
浏览器环境受同源策略(CORS)限制,而当时的 GitHub git clone 端点没有返回 CORS 响应头。isomorphic-git 的解决方案是引入一个 CORS 代理服务器(corsProxy),把浏览器发出的 git 协议请求转发到真实远端。官方文档的注释点明了这一历史背景:"They never suspected that abrowserwould want to run 'git clone'!"。如果你有自建代理,也可以参考 @isomorphic-git/cors-proxy 的部署方式(该链接为 npm 包页面,本项目文档 docs/guide-webworker.md 与 docs/authentication.md 中亦有相关使用说明)。
clone 的完整参数清单
对照 src/api/clone.js 的 JSDoc,clone除教程用到的参数外还支持:
| 参数 | 类型/默认值 | 说明 |
|---|---|---|
fs | FsClient | 必填,文件系统实现 |
http | HttpClient | 必填,HTTP 客户端 |
dir | string | 工作树目录(noCheckout为 true 时可不传) |
gitdir | string =join(dir,'.git') | Git 目录路径 |
url | string | 必填,远端仓库 URL |
corsProxy | string | CORS 代理,会被写入该仓库的 git config |
ref | string | 要检出的分支,默认是远端"主分支" |
singleBranch | boolean = false | 只拉取单个分支而不是全部分支 |
noCheckout | boolean = false | 只 fetch 不检出,省去写盘时间 |
noTags | boolean = false | 默认会拉取全部标签,设为 true 可禁用 |
remote | string = 'origin' | 新建远端的名 |
depth | number | 获取多少层历史(浅克隆) |
since | Date | 只获取该日期之后的提交,与depth互斥 |
exclude | string[] = [] | 让服务端不要发送这些 ref 可达的提交 |
relative | boolean = false | depth相对当前浅深度而非分支尖端计算 |
headers | object = {} | 附加 HTTP 请求头,类似 git 的extraHeader |
cache | object | 缓存对象,见 docs/cache.md |
onProgress/onMessage/onAuth等 | 回调 | 进度、消息、认证等回调,见 docs/onProgress.md 等 |
从实现看,clone内部由 src/commands/clone.js 完成,实际流程是"fetch + checkout"的组合,并通过discoverGitdir定位真正的.git目录。assertParameter(src/utils/assertParameter.js)会在参数缺失时立即抛出MissingParameterError。
查看提交历史:log 的用法与返回结构
克隆完成后,用git.log查看该分支的最近提交历史:
await git.log({fs, dir})教程提示"expand the objects so you can see all the properties"。log返回一个ReadCommitResult数组,每个元素包含oid(提交 SHA-1)与commit对象(message、tree、parent、author、committer等字段)。
对照 src/api/log.js,log还支持这些参数:
ref = 'HEAD':从哪个提交开始往回走;depth:限制返回的提交数量(教程后续提交时会用到depth: 1);since:只返回晚于该日期的历史;filepath:只返回该文件的提交历史;follow/force:配合单文件历史使用(支持跟踪重命名);includeChanges:让每条记录附带变更的文件对象 ID。
// 只看最近 1 条提交(教程提交后的用法) let commits = await git.log({fs, dir, depth: 1}) console.log(commits[0])追踪文件状态:status 的 13 种取值
Git 的核心职责是追踪文件。isomorphic-git 的git.status负责把工作目录中的单个文件与当前分支(HEAD)进行对比:
await git.status({fs, dir, filepath: 'README.md'})刚克隆完,一切未改动,返回"unmodified"。教程随后依次演示了修改、新增、删除三种场景下状态的变化,这正是理解暂存区(index/staging area)的关键。
修改文件:从*modified到modified
await pfs.writeFile(`${dir}/README.md`, 'Very short README', 'utf8') await git.status({fs, dir, filepath: 'README.md'})带星号的"*modified"表示"文件在工作目录里有改动,但还没有进入暂存区"。教程用一个很形象的比喻:文本编辑器会在标题栏显示*表示"有未保存的更改"。执行git.add之后:
await git.add({fs, dir, filepath: 'README.md'}) await git.status({fs, dir, filepath: 'README.md'})星号消失,状态变成"modified"——改动已暂存。
新增文件:从*added到added
await pfs.writeFile(`${dir}/newfile.txt`, 'Hello World', 'utf8') await git.status({fs, dir, filepath: 'newfile.txt'})未跟踪的新文件返回"*added"(已出现在工作目录但未暂存),git.add之后变为"added"。
删除文件:先 unlink,再 remove
await pfs.unlink(`${dir}/package.json`) await git.status({fs, dir, filepath: 'package.json'})有意思的是,仅仅删掉文件还不够——你还需要告诉 git 你删除了它:
await git.remove({fs, dir, filepath: 'package.json'}) await git.status({fs, dir, filepath: 'package.json'})一个"反直觉"的边界情况
教程还演示了一个容易困惑的场景:如果对实际上并没有删除的文件执行git.remove会怎样?
await git.remove({fs, dir, filepath: 'package-lock.json'}) await git.status({fs, dir, filepath: 'package-lock.json'})结果是文件同时报告为"untracked"和"deleted"。这是因为git.remove只把文件从索引(index)中移除,并不会删除工作目录里的文件——它的文档注释明确写着 "Note that this does NOT delete the file in the working directory"(见 src/api/remove.js)。于是工作目录里有文件、索引里没有、HEAD 里有,状态机便同时呈现"未跟踪"与"已删除"两个特征。教程随后用git.add把这个文件重新加回索引,恢复"added"状态。
status 的完整取值表
src/api/status.js 的 JSDoc 给出了status的全部 13 种可能返回值,是理解该 API 的第一手权威资料:
| status | 说明 |
|---|---|
"ignored" | 文件被 .gitignore 规则忽略 |
"unmodified" | 文件与 HEAD 提交一致 |
"*modified" | 有改动,尚未暂存 |
"*deleted" | 已删除,删除尚未暂存 |
"*added" | 未跟踪,尚未暂存 |
"absent" | HEAD、暂存区、工作目录中都不存在 |
"modified" | 有改动,已暂存 |
"deleted" | 已删除,已暂存 |
"added" | 先前未跟踪,现已暂存 |
"*unmodified" | 工作目录与 HEAD 一致,但索引不同 |
"*absent" | 工作目录与 HEAD 都没有,但索引中存在 |
"*undeleted" | 已从索引删除,但工作目录仍有该文件 |
"*undeletemodified" | 已从索引删除,但工作目录中的文件有改动 |
带*前缀的状态意味着"尚未暂存"。从 src/api/status.js 的实现可以推断,status的判定基于三元组(HEAD 中是否存在 / 索引中是否存在 / 工作目录中是否存在),并通过对工作目录文件重新计算 blob 的 SHA-1(hashObject)与 HEAD 树、索引条目比对得出最终结论;.gitignore只对未跟踪文件生效,代码注释中对此有明确说明。若某条路径同时存在于 HEAD 与索引中,则工作目录的哈希基于core.autocrlf规范化后计算,避免 CRLF 换行导致误报 modified。
暂存与移除的底层实现
git.add(见 src/api/add.js)把文件加入 git 索引(暂存区):
await git.add({fs, dir, filepath: 'README.md'})其参数与实现要点:
filepath支持字符串或字符串数组,也支持目录(会递归处理子文件);force = false:即使匹配 .gitignore 也强制加入,等价于git add --force;默认情况下被忽略的文件会被跳过(内部通过GitIgnoreManager.isIgnored判断,并遵守"已在索引中的路径不受后续 ignore 规则影响"的语义);parallel = true:并行处理多个文件,更快但更耗内存;- 实现上通过
GitIndexManager.acquire独占式修改索引(src/managers/GitIndexManager.js),为每个文件调用_writeObject写入 blob 对象,再把{filepath, stats, oid}插入索引;目录参数会递归展开为子文件逐个处理。
git.remove(见 src/api/remove.js)则只做一件事:在索引中删除该条目(index.delete({ filepath })),工作目录文件原样保留。这解释了前面"删除但未删除"的怪现象。
提交:commit 的调用与返回值
教程把一天的"工作成果"(删掉 package.json、把 README 改成 "Very short README")正式提交:
let sha = await git.commit({ fs, dir, message: 'Delete package.json and overwrite README.', author: { name: 'Mr. Test', email: 'mrtest@example.com' } }) console.log(sha)git.commit返回新提交的 SHA-1 对象 ID(shasum)。对照 src/api/commit.js,还有这些实用参数:
author/committer:都支持name、email、timestamp(Unix 秒)、timezoneOffset(与 UTC 的分钟差);不传committer时默认复用author;name/email未指定时回退到仓库配置的user.name/user.email;amend = false:为 true 时替换ref指向的最近一次提交;dryRun = false:只模拟提交以验证能否成功;noUpdateBranch = false:创建提交但不更新分支指针;disallowEmpty = false:没有暂存改动时是否抛EmptyCommitError;parent/tree:显式指定父提交与树对象,默认分别取ref指向的提交和由当前索引生成的新树;signingKey/onSign:PGP 私钥签名支持。
提交后再次用git.log({fs, dir, depth: 1})查看最新提交,即可确认自己"工作成果"已经进入历史。
清理现场:重置 LightningFS
交互式教程的末尾提供了一个"一键清空"的代码块,用于在重复练习时重置虚拟文件系统:
window.fs = new LightningFS('fs', { wipe: true }) window.pfs = window.fs.promises console.log('done')wipe: true会清空该名字对应的 IndexedDB 存储,让你从零开始重跑整个流程。
总结与延伸阅读
至此,你已经在浏览器里走完了 Git 的核心生命周期:clone(获取)→ status(对比)→ add/remove(暂存/移除)→ commit(提交)→ log(查看历史)。教程最后提到:"This just scratches the surface"——isomorphic-git 的能力远不止于此。
本项目 1.x 版本的全部公开 API 可查阅 website/versioned_docs/version-1.x/alphabetic.md(字母序索引),或直接浏览 src/api 目录下的源码与 JSDoc。继续深入学习还可以参考:
- docs/fs.md:文件系统实现契约,Node 与浏览器各自如何接入;
- docs/dir-vs-gitdir.md:
dir与gitdir的区别与默认约定; - docs/cache.md:
cache参数如何加速多次操作; - docs/http.md:HTTP 客户端接口说明;
- docs/guide-quickstart-with-bundlers.md 与 website/versioned_docs/version-1.x/guide-browser.md:面向打包器(webpack/rollup)与浏览器的更深入配置指南;
- 各 API 的测试用例集中在tests(如tests/test-clone.js、tests/test-status.js、tests/test-add.js、tests/test-commit.js),可对照验证本文涉及的行为。
- 开发工具
【免费下载链接】isomorphic-git
A pure JavaScript implementation of git for node and browsers!
相关推荐
isomorphic-git 浏览器端快速上手:在浏览器中运行 git clone、status、add、commit 全流程
isomorphic git 浏览器端快速上手:在浏览器中运行 git clone、status、add、commit 全流程 导读 isomorphic gi
开发工具探索isomorphic-git:浏览器中的纯JavaScript Git实现
探索isomorphic git:浏览器中的纯JavaScript Git实现 isomorphic git是一个强大的纯JavaScript Git实现,它能
开发工具isomorphic-git 快速上手:Node 与浏览器中克隆仓库、status/add/commit 工作流完整实践
isomorphic git 快速上手:Node 与浏览器中克隆仓库、status/add/commit 工作流完整实践 本文基于仓库内的 Quick Star
开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考