1. 背景
在开发一款js静态分析工具时,我遇到了一些git相关的需求:
- 通过git比较出工程中两个分支的改动文件
- 跨分支读取
针对这个两个问题,我们先看看用 git 命令如何操作:
1.1 分支比较
通过如下的 git 命令即可进行分支比较:
css
git diff [分支1] [分支2] --name-status
打印结果可能如下:
css
M a.js
D b.vue
A c.vue
R092 .whistle.js .whistle111.js
...
这里每一行就是一个改动文件,空格前是改动类型,空格后是改动的具体文件。
M: 修改 D: 删除 A: 新增 R: 重命名,这里的 092 代表是改动前后两个文件的相似度。[了解更多]
1.2 跨分支读取
这里先说一下跨分支读取的意思:假如我现在处于a分支中开发,我想要实现在不切换分支的情况下读取b分支某个文件的内容和某个文件夹下面的文件列表。
通过调研,发现可以通过 git cat-file 和 git ls-tree 来实现:
- 读取文件内容
bash
#读取文件
git cat-file -p [分支]:[文件相对路径]
- 列出目录下的文件及文件夹列表
bash
git ls-tree [分支]:[目录相对路径]
所以,现在的问题是如何在nodejs中集成这功能,且支持打包到 Cli工具和 Vscode 拓展。
2. 实现方案
2.1 exec 调用 Git 命令
首先想到最简单的方案就是通过Node.js 的 child_process 模块中的 exec调用Git命令:
typescript
import { execSync } from "child_process";
function getChangeFileList(baseBranch: string, targetBranch: string) {
return execSync(`git diff ${baseBranch} ${targetBranch} --name-status`, {
cwd: projectDir,
}).toString().trim();
}
这种方法使用简单,无需依赖外部的包,且能够被打包到node cli工具和vscode插件中。
目前有许多js库都基于这个原理,例如 simple-git,可以进一步简化使用的成本。
但经过一段时间的使用,发现这种方式存在两个问题:
- 依赖本机安装的Git,且需要正确配置环境变量(一般开发都会安装git,不是很严重的问题)。
- 通过创建子进程调用外部命令,而创建子进程是有性能开销的,当调用太多次后有性能问题,具体有多差可以看后面的对比。
由于有了这些问题,所以调研实践了市面的其他几种方案。
2.2 execFile 调用 Git 可执行文件
上面我们介绍了通过 child_process模块中的 exec(execSync是exec的同步形式)方法,它是通过调用shell来执行命令的,而在这个模块中还有一个execFile 方法,可以通过git程序的路径来执行:
ini
const res = execFileSync("/usr/bin/git", ["cat-file", "-p", `master:packages.json`]);
下面是nodejs文档的描述
child_process.exec() 和 child_process.execFile() 之间区别的重要性可能因平台而异。 在 Unix 类型的操作系统(Unix、Linux、macOS)上,child_process.execFile() 可以更高效,因为它默认不衍生 shell。 但是,在 Windows 上,.bat 和 .cmd 文件在没有终端的情况下无法自行执行,因此无法使用 child_process.execFile() 启动。
可以看到这种方法性能有所提升,但是必须指定git的位置。
2.3 dugite
dugite 是一个execFile原理实现git绑定的js库,但于上面不同的是,它在安装后会调用postinstall
钩子去下载一个精简版版的git,所以不需要手动指定git路径。

postinstall 钩子

node_modules
中的可执行文件
再从dugite的github可以看成,dugite是github-desktop
的一部分,而github-desktop
是基于electron开发的,那么可以推测出这个库对于开发跨端的桌面程序有很好的支持。

2.3 NodeGit
NodeGit 是一个基于 C++ 实现的Node.js 原生模块,实现了对 libgit2 的封装。
ini
const repo = await NodeGit.Repository.open(gitBasePath);
经过使用发现这个库优点如下:
- 功能强大,API复杂
- 原生模块性能好
- API基于Promise来实现。
但是使用了这个库也发现了一些问题:
- 原生模块导致安装复杂,比如在我的mac m1 电脑安装会失败,需要指定安装
^0.28.0-alpha.21
这个版本。 - 在一些低版本Linux 上面安装会失败,需要手动升级GCC版本。
- vscode插件不支持原生模块。
2.4 isomorphic-git
isomorphic-git 是一个纯 JavaScript 的库,提供了跨浏览器和 Node.js 环境使用的 Git 功能。它不依赖于外部的 Git 客户端或二进制文件,而是通过 JavaScript 实现了 Git 的核心功能。
使用 isomorphic-git 实现跨分支读取:
javascript
import * as git from "isomorphic-git";
import fs from 'fs';
const oid = await git1.resolveRef({
fs: fs,
dir: projectDir,
ref: 'master',
});
let cache = {};
const res = await git1.readBlob({
fs: fs,
dir: projectDir,
oid,
cache,
filepath: 'packages.json'
});
console.log(res.blob);
这个库支持通过 cache 参数缓存结果,提高批量读取文件的速度。
3. 总结
下面我会从性能、浏览器支持、vscode插件机制几个角度进行对比:
exec + git | execFile + git | dugite | NodeGit | isomorphic-git | |
读取 620个不同文件 | 11888 ms | 9357 ms | 4002 ms | 92ms | 开启cache: 810 ms 不开cache: 12828 ms |
star | - | - | 443 | 5.5k | 7.1k |
浏览器支持 | 不支持 | 不支持 | 不支持 | 不支持 | 支持 |
vscode插件 | 支持 | 支持 | 不支持,需要 npm install下载依赖 | 不支持 | 支持 |
node cli 工具 | 支持 | 支持 | 支持 | 支持,但可能需要升级系统的库 | 支持 |
API支持 | 需要手写git命令 | 需要手写git命令 | 需要手写git命令 | API封装度高,使用复杂 | API封装度高,使用较为简单 |
根据上述表格,可以得到如下结论:
- isomorphic-git综合能力最强,可以兼容大部分场景。
- 需要浏览器支持使用isomorphic-git
- 需要性能使用 nodegit
- electron 集成可以考虑使用 dugite,github 官方背书
- 不依赖外部git的方案:dugite、nodegit、isomorphic-git
但接入哪一个,还需要在项目中对比。