获取远程仓库中的 tags,从 remote-git-tags 的用途反向推导实现方案

前言

不知不觉中,源码的文章也写了几期了,每一期都能多少有些收获。

原本,我在看 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、返回的结果是包含具体 tagsMap 对象

获取远程仓库中的 tags,从 remote-git-tags 的用途反向推导实现方案

小结

remote-git-tags 的用途是:

获取远程仓库中的所有 tags

反推实现

功能拆分

remote-git-tags 用途上可以总结出两个关键的功能点:

1、获取远程仓库中 tags 信息

2、将得到的 tagsMap 格式返回

接下来我们就从这两点出发,逐步实现它的功能。

获取远程仓库中 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

获取远程仓库中的 tags,从 remote-git-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 项目中运行的结果是一致的。

获取远程仓库中的 tags,从 remote-git-tags 的用途反向推导实现方案

但是,没错,还有个但是。

我也本来以为第一个功能点就这样实现了,但是我又看了一下文档,发下它下面还有一行文字,并且有一段实现代码。

这个功能大致意思就是如果 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 名,值为哈希值。

获取远程仓库中的 tags,从 remote-git-tags 的用途反向推导实现方案

先打印了一下 stdout 的类型

console.log('stdout:', typeof stdout);

结果发现是字符串。

获取远程仓库中的 tags,从 remote-git-tags 的用途反向推导实现方案

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);
});

打印最终的结果

获取远程仓库中的 tags,从 remote-git-tags 的用途反向推导实现方案

代码对比

整理一下最终实现的完整的代码:

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畅想」系列作者。

华夏美食、国漫、古风重度爱好者,刑侦、无限流小说初级玩家。

欢迎技术或非技术问题的讨论。

本文正在参加「金石计划」

原文链接:https://juejin.cn/post/7215640327633715259 作者:叶一一

(0)
上一篇 2023年3月30日 下午4:16
下一篇 2023年3月30日 下午4:26

相关推荐

发表评论

登录后才能评论