gitly
Version:
An API to download and/or extract git repositories
341 lines (326 loc) • 10.3 kB
JavaScript
// src/utils/clone.ts
import spawn from "cross-spawn";
import { rm } from "fs/promises";
import path from "path";
import * as tar2 from "tar";
// src/utils/archive.ts
import os from "os";
import { join } from "path";
import * as tar from "tar";
function getArchiveUrl(info, options = {}) {
var _a6;
const { path: repo, type } = info;
if ((_a6 = options.url) == null ? void 0 : _a6.filter) {
return options.url.filter(info);
}
switch (info.hostname) {
case "bitbucket":
return `https://bitbucket.org${repo}/get/${type}.tar.gz`;
case "gitlab":
return `https://gitlab.com${repo}/-/archive/${type}/${repo.split("/")[2]}-${type}.tar.gz`;
default:
return `https://github.com${repo}/archive/${type}.tar.gz`;
}
}
function getArchivePath(info, options = {}) {
const { path: path2, type, hostname: site } = info;
return join(
options.temp || join(os.homedir(), ".gitly"),
site,
path2,
`${type}.tar.gz`
);
}
var extract2 = tar.extract;
// src/utils/error.ts
var GitlyAbstractError = class extends Error {
constructor(message, code = -1) {
super(message);
this.message = message;
this.code = code;
this.rawMessage = message;
const type = this.type = this.ctor.type;
this.message = `[${type ? `gitly:${type}` : "gitly"}]: ${message}`;
Object.setPrototypeOf(this, new.target.prototype);
}
get ctor() {
return this.constructor;
}
};
var _a;
var GitlyUknownError = (_a = class extends GitlyAbstractError {
}, _a.type = "unknown" /* Unknown */, _a);
var _a2;
var GitlyFetchError = (_a2 = class extends GitlyAbstractError {
}, _a2.type = "fetch" /* Fetch */, _a2);
var _a3;
var GitlyExtractError = (_a3 = class extends GitlyAbstractError {
}, _a3.type = "extract" /* Extract */, _a3);
var _a4;
var GitlyDownloadError = (_a4 = class extends GitlyAbstractError {
}, _a4.type = "download" /* Download */, _a4);
var _a5;
var GitlyCloneError = (_a5 = class extends GitlyAbstractError {
}, _a5.type = "clone" /* Clone */, _a5);
// src/utils/execute.ts
async function execute(tasks) {
return new Promise((resolve2, reject) => {
const next = () => execute(tasks.slice(1)).then(resolve2);
return tasks[0]().then((t) => t ? resolve2(t) : next()).catch(reject);
});
}
// src/utils/exists.ts
import { constants, promises as fs } from "fs";
import { isAbsolute } from "path";
// src/utils/parse.ts
import { URL } from "url";
function parse(url, options = {}) {
const { url: normalized, host } = normalizeURL(url, options);
const result = new URL(normalized);
const paths = (result.pathname || "").split("/").filter(Boolean);
const owner = paths.shift() || "";
const repository = paths.shift() || "";
return {
protocol: (result.protocol || "https").replace(/:/g, ""),
host: result.host || host || "github.com",
hostname: (result.hostname || host || "github").replace(/\.(\S+)/, ""),
hash: result.hash || "",
href: result.href || "",
path: result.pathname || "",
repository,
owner,
type: (result.hash || "#master").substring(1)
};
}
function normalizeURL(url, options) {
const { host } = options;
if (url.includes("0") && Array.from(url.matchAll(/0/g)).length > 25) {
throw new Error("Invalid argument");
}
if ((host == null ? void 0 : host.includes("0")) && Array.from(host.matchAll(/0/g)).length > 25) {
throw new Error("Invalid argument");
}
const isNotProtocol = !/http(s)?:\/\//.test(url);
const hasHost = /([\S]+):.+/.test(url);
const hasTLD = /[\S]+\.([\D]+)/.test(url);
let normalizedURL = url.replace("www.", "").replace(".git", "");
let updatedHost = host || "";
if (isNotProtocol && hasHost) {
const hostMatch = url.match(/([\S]+):.+/);
updatedHost = hostMatch ? hostMatch[1] : "";
normalizedURL = `https://${updatedHost}.com/${normalizedURL.replace(`${updatedHost}:`, "")}`;
} else if (isNotProtocol && hasTLD) {
normalizedURL = `https://${normalizedURL}`;
} else if (isNotProtocol) {
const tldMatch = (host || "").match(/[\S]+\.([\D]+)/);
const domain = (host || "github").replace(
`.${tldMatch ? tldMatch[1] : "com"}`,
""
);
const tld = tldMatch ? tldMatch[1] : "com";
normalizedURL = `https://${domain}.${tld}/${normalizedURL}`;
}
return { url: normalizedURL, host: updatedHost };
}
// src/utils/exists.ts
async function exists(path2, options = {}) {
let _path = path2;
if (!isAbsolute(path2)) {
_path = getArchivePath(parse(path2), options);
}
try {
await fs.access(_path, constants.F_OK);
return true;
} catch (_) {
}
return false;
}
// src/utils/offline.ts
import { promises as dns } from "dns";
var { lookup } = dns;
async function isOffline() {
try {
await lookup("google.com");
return false;
} catch (_) {
}
return true;
}
// src/utils/clone.ts
async function clone(repository, options = {}) {
const info = parse(repository, options);
const archivePath = getArchivePath(info, options);
const directory = archivePath.replace(/\.tar\.gz$/, "");
let order = [];
const local = async () => exists(`${archivePath}.tar.gz`);
const remote = async () => {
var _a6;
if (await exists(archivePath)) {
await rm(archivePath);
}
const depth = ((_a6 = options == null ? void 0 : options.git) == null ? void 0 : _a6.depth) || 1;
if (repository.includes("--upload-pack") || directory.includes("--upload-pack")) {
throw new GitlyCloneError("Invalid argument");
}
if (typeof depth !== "number") {
throw new GitlyCloneError("Invalid depth option");
}
if (info.href.includes("--upload-pack")) {
throw new GitlyCloneError("Invalid argument");
}
const child = spawn("git", [
"clone",
"--depth",
depth.toString(),
info.href,
directory
]);
await new Promise((resolve2, reject) => {
child.on("error", (reason) => reject(new GitlyCloneError(reason.message)));
child.on("close", (code) => {
if (code === 0) {
rm(path.resolve(directory, ".git"), { recursive: true }).then(
() => (
// Create the archive after cloning
tar2.create(
{
gzip: true,
file: archivePath,
// Go one level up to include the repository name in the archive
cwd: path.resolve(archivePath, ".."),
portable: true
},
[info.type]
)
)
).then(
() => rm(path.resolve(directory), {
recursive: true
})
).then(resolve2).catch((error) => reject(new GitlyCloneError(error.message)));
} else {
reject(new GitlyCloneError("Failed to clone the repository"));
}
});
});
return archivePath;
};
if (await isOffline() || options.cache) {
order = [local];
} else if (options.force || ["master", "main"].includes(info.type)) {
order = [remote, local];
}
try {
const result = await execute(order);
if (typeof result === "boolean") {
return archivePath;
}
return result;
} catch (error) {
if (options.throw) {
throw error;
}
}
return "";
}
// src/utils/download.ts
import { rm as rm2 } from "shelljs";
// src/utils/fetch.ts
import axios from "axios";
import * as stream from "stream";
import { promisify } from "util";
// src/utils/write.ts
import { createWriteStream, promises as fs2 } from "fs";
import { dirname, normalize } from "path";
var { mkdir } = fs2;
async function write(path2) {
const _path = normalize(path2);
await mkdir(dirname(_path), { recursive: true });
return createWriteStream(_path);
}
// src/utils/fetch.ts
var pipeline2 = promisify(stream.pipeline);
async function fetch(url, file, options = {}) {
const response = await axios.get(url, {
headers: options.headers,
responseType: "stream",
validateStatus: (status) => status >= 200 && status < 500
});
const { statusText: message, status: code } = response;
if (code >= 400) throw new GitlyDownloadError(message, code);
if (code >= 300 && code < 400 && response.headers.location) {
return fetch(response.headers.location, file);
}
await pipeline2(response.data, await write(file));
return file;
}
// src/utils/download.ts
async function download(repository, options = {}) {
const info = parse(repository, options);
const archivePath = getArchivePath(info, options);
const url = getArchiveUrl(info, options);
const local = async () => exists(archivePath);
const remote = async () => {
if (await exists(archivePath)) {
rm2(archivePath);
}
return fetch(url, archivePath, options);
};
let order = [local, remote];
if (await isOffline() || options.cache) {
order = [local];
} else if (options.force || ["master", "main"].includes(info.type)) {
order = [remote, local];
}
try {
const result = await execute(order);
if (typeof result === "boolean") {
return archivePath;
}
return result;
} catch (error) {
if (options.throw) {
throw error;
}
}
return "";
}
// src/utils/extract.ts
import { promises as fs3 } from "fs";
import { resolve } from "path";
var { mkdir: mkdir2 } = fs3;
var extract_default = async (source, destination, options = {}) => {
var _a6;
const _destination = resolve(destination);
if (await exists(source, options)) {
try {
const filter = ((_a6 = options.extract) == null ? void 0 : _a6.filter) ? options.extract.filter : () => true;
await mkdir2(destination, { recursive: true });
await extract2({ strip: 1, filter, file: source, cwd: _destination });
return _destination;
} catch (_) {
}
}
return "";
};
// src/utils/gitly.ts
async function gitly(repository, destination, options) {
let source = "";
switch (options == null ? void 0 : options.backend) {
case "git":
source = await clone(repository, options);
break;
default:
source = await download(repository, options);
break;
}
return [source, await extract_default(source, destination, options)];
}
export {
clone,
gitly as default,
download,
extract_default as extract,
parse
};
//# sourceMappingURL=main.js.map