remote-git-tags 源码阅读

285 阅读4分钟

1 学习目标

了解 remote-git-tags 作用和使用场景 了解 promisify 实现原理


2 库介绍

这个库的作用是:从远程仓库获取所有标签。 原理:通过执行 gti ls-remote --tags xxx(仓库路径) 获取 tags 应用:可以用于切换 tags 发布版本等

// 如何使用
import test from 'ava';
import remoteGitTags from './index.js';
// 第一个参数是测试用例的名称
// 第二个参数是一个函数,该函数会注入t对象,所有的断言都是通过这个注入的t进行的
test('main', async () => {
	const tags = await remoteGitTags('https://github.com/sindresorhus/got');
	console.log(tags);
});
/*
  Map(149) {
  'v0.1.0' => '1e7746d8c44a75ee9ec8ed94356dd5e8ba9c3bc2',
  'v0.1.1' => '98d1263b1331573dbc6c3110addb038dc5804bb8',
   .....
  }
*/

3 拉取源码

克隆源码文件 git clone github.com/sindresorhu… 进入文件 cd remote-git-tags

4 package.json文件

Node 12 以下都是 CommonJS 模块机制。 Node 13 增加了对 ES6 模块的支持
默认情况下

  • 对于.js 结尾文件和无拓展名文件是以 CommonJS 方式加载
  • 对于.mjs 文件则当作 es 模块加载
    如果 package.json 文件中存在 type 属性
  • 值为 moduel 则当作 es 模块处理
  • 值为 commonjs 则被当作 commonJs 模块处理
// ava 一个开源的Node.js测试运行器  
// xo eslint包装器,内置了eslint
{
    "type": "module",
    "engines": {
            "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
    },
    "scripts": {
            "test": "xo && ava"
    }
}

5 源码文件

源码只有22行,可以很明显的看出来
获取所有 tags git ls-remote --tags github.com/sindresorhu…
把所有 tags 和对应的 hash值 存在 Map 对象中

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

6 调试源码

进行调试
VSCode 打开当前项目,打开 package.json 文件,执行 script 属性中的 test 命令并执行调试脚本命令

image.png


7 promisify

Node.js 提供了一个util 模块,提供了一些方法

1、util.promisify
  promisify 方法和 callbackify 方法功能正好相反,将 callback 转换成 Promise
2、util.format
  第一个参数为一个字符串,用于指定格式,后面的参数为要格式化的参数,返回指定格式的字符串
3、util.callbackify
  将一个 async 函数,或者返回值为 Promise 的函数,转换 callback
4、util.inspect
  将任意对象转换为字符串。

7.1 简单实现

在以前经常会遇到回调地狱的问题,一层套一层的callback,如何实现一个promise?

// promisify 参数 arg 为函数
function promisify(arg){
  return (...args) => {   //返回第二层函数,用来接收参数
    return new Promise((reslove,reject) => { //返回promise 
      arg(...args,(err,data) => {  
        if (err) {
          reject(err)
        }else {
          reslove(data); //返回数据
        }
      })
    })
  }
}
// 例如
const fs = require('fs');
// 使用 promise 将 fs.readdir 转换成 promise
const readdir = promisify(fs.readdir);
// 使用 promise 再由 async/await 转成同步
(async ()=>{
  const read = await readdir('./');
  console.log(read); //打印数据
})()
 
// 或者也可以直接使用 then
readdir('./').then(res => {
    // do something...
}).catch(err =>{
    // 捕获错误信息
})

7.2 util.promisify源码 ( 源码地址

const kCustomPromisifiedSymbol = SymbolFor('nodejs.util.promisify.custom');
const kCustomPromisifyArgsSymbol = Symbol('customPromisifyArgs');

let validateFunction;

function promisify(original) {
  // Lazy-load to avoid a circular dependency.
  if (validateFunction === undefined)
    ({ validateFunction } = require('internal/validators'));

  validateFunction(original, 'original');

  if (original[kCustomPromisifiedSymbol]) {
    const fn = original[kCustomPromisifiedSymbol];

    validateFunction(fn, 'util.promisify.custom');

    return ObjectDefineProperty(fn, kCustomPromisifiedSymbol, {
      value: fn, enumerable: false, writable: false, configurable: true
    });
  }

  // Names to create an object from in case the callback receives multiple
  // arguments, e.g. ['bytesRead', 'buffer'] for fs.read.
  const argumentNames = original[kCustomPromisifyArgsSymbol];

  function fn(...args) {
    return new Promise((resolve, reject) => {
      ArrayPrototypePush(args, (err, ...values) => {
        if (err) {
          return reject(err);
        }
        if (argumentNames !== undefined && values.length > 1) {
          const obj = {};
          for (let i = 0; i < argumentNames.length; i++)
            obj[argumentNames[i]] = values[i];
          resolve(obj);
        } else {
          resolve(values[0]);
        }
      });
      ReflectApply(original, this, args);
    });
  }

  ObjectSetPrototypeOf(fn, ObjectGetPrototypeOf(original));

  ObjectDefineProperty(fn, kCustomPromisifiedSymbol, {
    value: fn, enumerable: false, writable: false, configurable: true
  });
  return ObjectDefineProperties(
    fn,
    ObjectGetOwnPropertyDescriptors(original)
  );
}

promisify.custom = kCustomPromisifiedSymbol;

8 child_process

Node 的子进程模块

// 子进程 包括以下模块
1、ChildProcess
  ChildProcess 的实例,表示衍生的子进程,通过child_process.spawn() 等方法来创建出来的
2、exec
  衍生 shell 并在该 shell 中运行命令,完成后将 stdout 和 stderr 传给回调函数
3、execFile
  与exec类似,不同之处在于,默认情况下,它直接衍生命令,而不先衍生 shell
4、execFileSync
  exec 的同步版本,其将阻塞 Node.js 事件循环
5、execSync
  execFile 的同步版本,其将阻塞 Node.js 事件循环
6、fork
  衍生新的 Node.js 进程并使用建立的 IPC 通信通道(其允许在父子进程之间发送消息)调用指定的模块
7、spawn
  异步衍生子进程,不会阻塞 Node.js 事件循环
8、spawnSync: [Function: spawnSync]
  同步衍生子进程,会阻塞事件循环,直到衍生的进程退出或终止

9 总结

通过本文,一句话简述 remote-git-tags 原理
通过 Node.js 的子进程 childProcess 中的 execFile 模块,执行 git ls-remote --tags repoUrl 获取所有 tags 和 tags 对应 hash 值存放在 Map 对象中
学习了 Node 模块使用什么加载方式,学习了 promisify 的原理,了解到 node 的 xo 库和 ava 库