源码逆思维,从 remote-git-tags 的用途反向推导实现方案【玩转源码】
【摘要】 前言不知不觉中,源码的文章也写了几期了,每一期都能多少有些收获。原本,我在看 remote-git-tags 源码之前,计划带着 3W 的思维去看。但是,我有了一瞬间的停顿。这个库的源码不是很多,是不是可以改变个思维。当已知库的实际用途的情况下,是不是可以反向推导出实现方案。没准,过程中能时不时来个惊喜。功能测试先来看一下,remote-git-tags 具体是干什么的。1、先生成 pack...
前言
不知不觉中,源码的文章也写了几期了,每一期都能多少有些收获。
原本,我在看 remote-git-tags 源码之前,计划带着 3W 的思维去看。但是,我有了一瞬间的停顿。
这个库的源码不是很多,是不是可以改变个思维。当已知库的实际用途的情况下,是不是可以反向推导出实现方案。
没准,过程中能时不时来个惊喜。
功能测试
先来看一下,remote-git-tags 具体是干什么的。
1、先生成 package.json 文件
npm init -y
2、在 package.json 中添加字段 type 且设置值为 module。
"type": "module",
3、安装 remote-git-tags
npm install remote-git-tags
4、新增一个 test.js 文件,引入 remote-git-tags,请求具体的 github 地址并打印结果
import remoteGitTags from 'remote-git-tags';
console.log(await remoteGitTags('https://github.com/wxmp-project/wxmp-travel'));
5、返回的结果是包含具体 tags 的 Map 对象。
小结
remote-git-tags 的用途是:
返回远程仓库中的所有 tags。
反推实现
功能拆分
从 remote-git-tags 用途上可以总结出两个关键的功能点:
1、获取远程仓库中 tags 信息
2、将得到的 tags 按 Map 格式返回
接下来我们就从这两点出发,逐步实现它的功能。
获取远程仓库中 tags 信息
git ls-remote
我先到 Git命令手册 里寻找可以查看远程仓库的信息的命令,但是手册中命令还挺多的,一个个找效率太低。
其实刚开始看到这个库的时候,我就搜索了一下 remote,然后查看到了一个命令
git remote
详细了解了一下它的具体用法,并没有获取 tag 的相关命令。随后,我又找到了一个命令
git ls-remote
这个命令的功能是:在远程存储库中列出引用。
它有很多的选项
git ls-remote [--heads] [--tags] [--refs] [--upload-pack=<exec>]
[-q | --quiet] [--exit-code] [--get-url]
[--symref] [<repository> [<refs>…]]
其中
--tags:将显示存储在 refs / tags 中的引用。
--get-url :扩展给定远程存储库的 URL,并退出而不与远程进行通话。
我们运行一下这个命令,看看实际的打印结果
git ls-remote --tags https://github.com/wxmp-project/wxmp-travel.git
运行之后,返回了 wxmp-travel 仓库中的全部 tags
接下来就到功能的关键点:如何在本地建立与远程仓库的通信并拿到返回信息?
node:child_process
建立通信需要借助 Node.js 创建一个进程。这方面的知识点,我其实掌握的不是特别熟练,通过进程等关键字检索,找到了一个相关的模块:node:child_process。
它的主要功能是:
提供了生成子流程的能力。
看了一下它提供的方法,其中 exec 和 execFile,可以帮助执行一个进程并把结果返回。而这两个方法的主要区别是,exec 会产生一个 shell,而 execFile 不会。
我们不需要 shell,所以选择使用 execFile 方法。
execFile
这个方法的语法如下:
child_process.execFile(file[, args][, options][, callback])
解释一下上面的参数:
file:要运行的可执行文件的名称或路径。
args:字符串参数列表。
options:配置项列表。可选
callback:进程终止时输出函数。
前面写道获取远程仓库信息的命令,将命令按照上述的参数描述一一对应,最终的方法也就出来了:
import childProcess from 'node:child_process';
childProcess.execFile('git', ['ls-remote', '--tags', 'https://github.com/wxmp-project/wxmp-travel'], (error, stdout, stderr) => {
if (error) return;
console.log('stdout:', stdout);
});
打印一下结果,和在 git 项目中运行的结果是一致的。
但是,没错,还有个但是。
我也本来以为第一个功能点就这样实现了,但是我又看了一下文档,发下它下面还有一行文字,并且有一段实现代码。
这个功能大致意思就是如果 execFile 方法用 util.promisify() 调用,会将返回的函数转成一个 promise 函数。
所以上面的代码可以改下为如下代码:
import util from 'node:util';
import childProcess from 'node:child_process';
const execFile = util.promisify(childProcess.execFile);
async function remoteGitTags() {
const { stdout } = await execFile('git', ['ls-remote', '--tags', 'https://github.com/wxmp-project/wxmp-travel']);
console.log('stdout:', stdout);
}
remoteGitTags();
采用第二种写法的原因也很简单:
Promise 解决回调地狱。
node:module
补充一个小知识点。
node:module 是 Node.js 中的语法。其中 module 对应具体的模块名,允许 import 'node:module' 或require('node:module') 两种引入方式。
小结
以上,经过一系列的模式和尝试,我们顺利的拿到了远程仓库的 tags 信息。
返回 Map 格式的全部 tags
接下来,则是对返回值重置的过程。最终想要的效果是将返回的值重置为 Map 键值对,其中键为 tag 名,值为哈希值。

先打印了一下 stdout 的类型
console.log('stdout:', typeof stdout);
结果发现是字符串。
1、所以我们需要先将字符串转成数组。
const stdList = stdout.trim().split('\n');
2、声明一个 Map 对象
const tagMap = new Map();
3、将得到的数组进行循环,每一个元素需要进一步处理
- 将每一个元素下的字符串进行分割。注意这里的分隔符是 \t 不是 \n, 因为中间是制表符不是空格。(上面打印结果里的截图很明显,可以返回去再看一下)
- 使用正则得到最终的 tag 名,将前面的部分替换成空。
- 将得到的值添加到 tagMap 中。
stdList.forEach(item => {
// 制表符符进行分割
const [hash, tag] = item.split('\t');
// 正则匹配 tag 名
const tagName = tag.replace(/^refs\/tags\//, '');
// map 中加入值
tagMap.set(tagName, hash);
});
打印最终的结果
代码对比
整理一下最终实现的完整的代码:
import util from 'node:util';
import childProcess from 'node:child_process';
const execFile = util.promisify(childProcess.execFile);
async function remoteGitTags(gitUrl) {
const { stdout } = await execFile('git', ['ls-remote', '--tags', gitUrl]);
if (stdout) {
const stdList = stdout.trim().split('\n');
const tagMap = new Map();
stdList.forEach(item => {
// 制表符符进行分割
const [hash, tag] = item.split('\t');
// 正则匹配 tag 名
const tagName = tag.replace(/^refs\/tags\//, '');
// map 中加入值
tagMap.set(tagName, hash);
});
return tagMap;
}
}
console.log(await remoteGitTags('https://github.com/wxmp-project/wxmp-travel'));
我再贴一下源码
import { promisify } from 'node:util';
import childProcess from 'node:child_process';
const execFile = promisify(childProcess.execFile);
export default async function remoteGitTags(repoUrl) {
const { stdout } = await execFile('git', ['ls-remote', '--tags', repoUrl]);
const tags = new Map();
for (const line of stdout.trim().split('\n')) {
const [hash, tagReference] = line.split('\t');
// Strip off the indicator of dereferenced tags so we can override the
// previous entry which points at the tag hash and not the commit hash
// `refs/tags/v9.6.0^{}` → `v9.6.0`
const tagName = tagReference.replace(/^refs\/tags\//, '').replace(/\^{}$/, '');
tags.set(tagName, hash);
}
return tags;
}
总体上看,核心实现思想是一致的,但是部分代码略不同。一部分是写法习惯,还有一个是我没有考虑全。
1、refs/tags/v9.6.0^{} → v9.6.0
tag 名可能会携带字符 ^{},这个确实是我之前没想到的,所以需要加额外的正则去掉。
replace(/\^{}$/, '')
总结
反向思维,去实现已知的功能,是一个难得的锻炼机会。
1、功能实现的过程,是一个将知识点重组的过程。这个过程中,可以找到已知知识点的应用场景,也可以通过查找和阅读学习未知的知识点。
2、省去了自己想命题的时间,还可以将自己代码与源码做对比,找出不足之处。
3、在这个过程里,学习到的新知识点,往往会掌握的更牢固一些。
4、对于 Git 提供的 ls-remote 命令,有了较深刻的了解。
5、熟悉并掌握了 Node.js 提供的 child_process.execFile 和 util.promisify 两个模块方法的功能和应用场景。
以上就是本次分享的内容。如果觉得有帮助,欢迎留言讨论、点赞 、收藏,持续产出技术分享。
我是 叶一一,非职业「传道授业解惑」的技术博主。「趣学前端」、「CSS畅想」系列作者。
华夏美食、国漫、古风重度爱好者,刑侦、无限流小说初级玩家。
欢迎技术或非技术问题的讨论。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
作者其他文章
评论(0)