takin
Version:
Front end engineering base toolchain and scaffold
332 lines • 11.2 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.download = exports.getName = exports.supportProtocol = exports.parseOptions = exports.getGitHash = exports.addSupportGitSite = void 0;
const execa_1 = __importDefault(require("execa"));
const fs_extra_1 = __importDefault(require("fs-extra"));
const lodash_1 = __importDefault(require("lodash"));
const path_1 = __importDefault(require("path"));
const errors_1 = require("../errors");
const logger_1 = require("../logger");
const tar = __importStar(require("./tar"));
const GIT_PROTOCOL_PREFIX = /^git:/;
/**
* 支持的链接类型:
* - http://github.com/user/repo.git#v1.0.27
* - https://github.com/user/repo.git#v1.0.27
* - git@github.com:user/repo.git
* - user/repo.git#v1.0.27
* - github:user/repo.git#v1.0.27
*/
const GIT_URL_REGEXP = /^(?:(?:https?:\/\/)?([^:/]+\.[^:/]+)\/|git@([^:/]+)[:/]|([^/]+):)?([^/.~@/\s]+)\/([^/\s#]+)(?:((?:\/[^/\s#]+)+))?(?:\/)?(?:#(.+))?/;
const DEFAULT_SITE = 'github.com';
/**
* 支持的站点
*/
const SUPPORTED_SITES = new Map([
[
'github.com',
{
type: 'github',
generateGitTarUrl: getTarUrlByRepo
}
],
[
'gitlab.com',
{
type: 'gitlab',
generateGitTarUrl: getTarUrlByRepo
}
],
[
'bitbucket.org',
{
type: 'bitbucket',
generateGitTarUrl: getTarUrlByRepo
}
]
]);
/**
* 添加支持的 git 站点
* @param siteUrl - 站点地址, 如 github.com
* @param siteType - 站点类型, 如 git / gitlab / bitbucket
*/
function addSupportGitSite(siteUrl, siteConfig) {
if (SUPPORTED_SITES.has(siteUrl)) {
return logger_1.logger.warn(`跳过添加 ${siteConfig.type} 类型的 Git 站点: ${siteUrl}, 原因: 已存在.`);
}
SUPPORTED_SITES.set(siteUrl, {
type: siteConfig.type,
generateGitTarUrl: siteConfig.generateGitTarUrl || getTarUrlByRepo
});
}
exports.addSupportGitSite = addSupportGitSite;
function parseGitUrl(src) {
const match = GIT_URL_REGEXP.exec(src);
if (!match) {
throw new errors_1.DownloaderError(`无法解析链接: ${src}`);
}
const site = match[1] || match[2] || match[3] || DEFAULT_SITE;
const user = match[4];
const name = match[5].replace(/\.git$/, '');
const subdir = match[6];
const ref = match[7] || 'HEAD';
const url = `https://${site}/${user}/${name}`;
const ssh = `git@${site}:${user}/${name}`;
const mode = SUPPORTED_SITES.has(site) ? 'tar' : 'git';
return {
site,
user,
name,
ref,
url,
ssh,
subdir,
mode
};
}
/**
*
* @param repo - 仓库信息
* @returns 生成的 tar 地址
*/
async function getTarUrlByRepo(repo) {
var _a;
const siteType = (_a = SUPPORTED_SITES.get(repo.site)) === null || _a === void 0 ? void 0 : _a.type;
const hash = await getGitHash(repo);
if (!hash)
throw new errors_1.DownloaderError(`${repo.url} 未解析出有效的 hash 信息`);
// 这里保存下 hash 信息
repo.hash = hash;
const url = siteType === 'gitlab'
? `${repo.url}/-/archive/${hash}/${repo.name}-${hash}.tar.gz`
: siteType === 'bitbucket'
? `${repo.url}/get/${hash}.tar.gz`
: `${repo.url}/archive/${hash}.tar.gz`;
const repoDir = `${repo.name}-${hash}`;
const strip = (repo.subdir ? path_1.default.posix.join(repoDir, repo.subdir) : repoDir).split('/').length;
return {
...lodash_1.default.omit(repo, [
'site',
'user',
'name',
'ref',
'hash',
'ssh',
'subdir',
'mode'
]),
url,
strip
};
}
/**
* 获取仓库 ref 信息
* @param repo - 仓库信息
* @param type - 获取 ref 信息的方式, 支持 url 或 ssh 两种
* @returns ref 信息数组
*/
async function fetchGitRefs(repo, type) {
try {
const { stdout } = await execa_1.default.command(`git ls-remote ${repo[type]}`);
return stdout
.split('\n')
.filter(Boolean)
.map((row) => {
const [hash, ref] = row.split('\t');
if (ref === 'HEAD') {
return { type: 'HEAD', hash };
}
const match = /refs\/([\w-]+)\/(.+)/.exec(ref);
if (!match) {
throw new errors_1.DownloaderError(`无法解析 ${ref}`);
}
return {
type: match[1] === 'heads'
? 'branch'
: match[1] === 'refs'
? 'ref'
: match[1],
name: match[2],
hash
};
});
}
catch (error) {
throw new errors_1.DownloaderError(`拉取 Git 信息失败 ${repo.url}, 原因: ${error.message}`);
}
}
async function getGitHash(repo) {
var _a;
let refs;
try {
// 优先从 url 拉取, 但可能没权限
refs = await fetchGitRefs(repo, 'url');
}
catch (error) {
logger_1.logger.debug(error === null || error === void 0 ? void 0 : error.message);
logger_1.logger.debug('将使用 ssh 模式重新拉取 Git 信息');
refs = await fetchGitRefs(repo, 'ssh');
}
refs = refs || [];
if (repo.ref === 'HEAD') {
return (_a = refs.find((ref) => ref.type === 'HEAD')) === null || _a === void 0 ? void 0 : _a.hash;
}
for (const ref of refs) {
if (ref.name === repo.ref) {
logger_1.logger.debug('成功匹配到 Git Hash:', repo.ref, '=>', ref.hash);
return ref.hash;
}
}
if (repo.ref.length < 8)
return;
for (const ref of refs) {
if (ref.hash.startsWith(repo.ref))
return ref.hash;
}
}
exports.getGitHash = getGitHash;
async function downloadByGit(repo, dest) {
const gitDir = path_1.default.resolve(dest, '.git');
await execa_1.default.command(`git clone ${repo.ssh} ${dest}`);
// 强制指定 --git-dir 避免仓库套仓库时 git 识别错误
const gitOptions = [`-C ${dest}`, `--git-dir ${gitDir}`].join(' ');
await execa_1.default.command(`git ${gitOptions} checkout ${repo.ref} --force`);
logger_1.logger.debug(`切换仓库 ref 为: ${repo.ref}`);
// 尝试获取 ref 对应的 hash 值并保存
try {
const { stdout } = await execa_1.default.command(`git ${gitOptions} rev-list -n 1 ${repo.ref}`);
const hash = stdout.split('\n').filter(Boolean)[0];
if (hash)
repo.hash = hash;
logger_1.logger.debug(`获取到当前 ref 对应的 hash 值: ${hash}`);
}
catch (error) {
logger_1.logger.debug('获取 hash 值失败', error);
}
// 清理 .git 文件夹
await fs_extra_1.default.emptyDir(gitDir);
}
async function downloadByTar(repo, dest) {
var _a;
const generateGitTarUrl = (_a = SUPPORTED_SITES.get(repo.site)) === null || _a === void 0 ? void 0 : _a.generateGitTarUrl;
if (!generateGitTarUrl) {
throw new errors_1.DownloaderError(`Git 站点: ${repo.site} 缺少 generateGitTarUrl 方法支持`);
}
const tarOptions = tar.parseOptions(await generateGitTarUrl(repo));
await tar.download(tarOptions, dest);
}
/**
* 解析 git 选项或链接
* @param urlOrOptions - git 下载链接或选项
* @returns git 仓库信息
*/
function parseOptions(urlOrOptions) {
let options;
if (typeof urlOrOptions === 'string') {
options = parseGitUrl(urlOrOptions);
}
else {
const ref = urlOrOptions.ref ||
urlOrOptions.commit ||
urlOrOptions.tag ||
urlOrOptions.branch ||
'';
const opts = { ...lodash_1.default.omit(urlOrOptions, ['commit', 'tag', 'branch']), ref };
const url = opts.url;
delete opts.url;
options = {
...parseGitUrl(url),
...opts
};
}
return options;
}
exports.parseOptions = parseOptions;
/**
* 判断是否支持处理当前链接
* @param url - 链接
* @returns 是否支持该链接
*/
function supportProtocol(url) {
if (!url)
return false;
// 去除 git: 的协议头
url = url.replace(GIT_PROTOCOL_PREFIX, '');
const match = GIT_URL_REGEXP.exec(url);
if (!match)
return false;
const siteTypeOrHost = match[1] || match[2] || match[3];
if (siteTypeOrHost) {
// 检查是否为支持的站点或站点类型
for (const [host, site] of SUPPORTED_SITES) {
if (siteTypeOrHost === host || siteTypeOrHost === site.type)
return true;
continue;
}
return false;
}
return true;
}
exports.supportProtocol = supportProtocol;
/**
* 从 git 仓库选项中获取名称
* @param options - git 仓库选项
* @returns 名称
*/
function getName(options) {
return `${options.user}/${options.name}`;
}
exports.getName = getName;
/**
* 下载 git repo 到指定的目录
* @param options - git 选项
* @param dest - 下载目录
* @returns 下载并解压后的目录
*/
async function download(options, dest) {
await fs_extra_1.default.ensureDir(dest);
await fs_extra_1.default.emptyDir(dest);
logger_1.logger.debug('Git 仓库信息', options);
if (options.mode === 'git') {
await downloadByGit(options, dest);
}
else {
try {
await downloadByTar(options, dest);
}
catch (error) {
logger_1.logger.debug('通过 tar 方式下载', `${options.user}/${options.name}`, '失败, 原因为:', error === null || error === void 0 ? void 0 : error.message, '尝试使用 git clone 方式下载');
// fallback 后 改写 mode 为 git
options.mode = 'git';
await downloadByGit(options, dest);
}
}
}
exports.download = download;
//# sourceMappingURL=git.js.map