diff --git a/_common/git-tag.js b/_common/git-tag.js new file mode 100644 index 0000000..7c63b0e --- /dev/null +++ b/_common/git-tag.js @@ -0,0 +1,145 @@ +'use strict'; + +require('dotenv').config({ path: '.env' }); +if (!process.env.REPO_BASE_DIR) { + // for stderr + console.error('[Warn] REPO_BASE_DIR= not set, using ./repos/'); +} +// ../ because this script is one directory deep +let repoBaseDir = process.env.REPO_BASE_DIR || '../repos'; + +var Crypto = require('crypto'); +var util = require('util'); +var exec = util.promisify(require('child_process').exec); +var Fs = require('node:fs/promises'); + +var Repos = {}; + +Repos.clone = async function (gitUrl, repoPath) { + let uuid = Crypto.randomUUID(); + let tmpPath = `${repoPath}.${uuid}.tmp`; + await exec(`git clone --bare --filter=tree:0 ${gitUrl} ${tmpPath}`); + await Fs.rename(tmpPath, repoPath); +}; + +Repos.checkExists = async function (repoPath) { + let err = await Fs.access(repoPath).catch(Object); + if (!err) { + return true; + } + + if (err.code !== 'ENOENT') { + throw err; + } + return false; +}; + +Repos.getTags = async function (repoPath) { + var { stdout } = await exec(`git --git-dir=${repoPath} tag`); + var rawTags = stdout.trim().split('\n'); + + let tags = []; + for (let tag of rawTags) { + // ex: v1, v2, v1.1, 1.1.0-rc + let maybeVersionRe = /^(v\d+|v?\d+\.\d+)/; + let maybeVersion = maybeVersionRe.test(tag); + if (maybeVersion) { + tags.push(tag); + } + + // TODO date versions + // let maybeVersionDate = /^v?\d{4}[._-]\d{2}[._-]\d{2}[._-]\D/.test(tag); + } + + tags = tags.reverse(); + return tags; +}; + +Repos.getCommitInfo = async function (repoPath, commitish) { + var { stdout } = await exec( + `git --git-dir=${repoPath} log -1 --format="%h %H %ad %cd" --date=iso-strict ${commitish}`, + ); + stdout = stdout.trim(); + var commitParts = stdout.split(/\s+/g); + return { + commit_id: `${commitParts[0]}`, + commit: `${commitParts[1]}`, + date: commitParts[2], + date_authored: commitParts[3], + }; +}; + +/** + * Lists GitHub Releases (w/ uploaded assets) + * + * @param request + * @param {string} owner + * @param {string} gitUrl + * @returns {PromiseLike | Promise} + */ +async function getAllReleases(gitUrl) { + let all = { + releases: [], + download: '', + }; + + let repoName = gitUrl.split('/').pop(); + repoName = repoName.replace(/\.git$/, ''); + + let repoPath = `${repoBaseDir}/${repoName}.git`; + + let isCloned = await Repos.checkExists(repoPath); + if (!isCloned) { + await Repos.clone(gitUrl, repoPath); + } + + let releases = []; + let tags = await Repos.getTags(repoPath); + for (let tag of tags) { + let commitInfo = await Repos.getCommitInfo(repoPath, tag); + let date = new Date(commitInfo.date); + let version = tag.replace(/^v/, ''); + + let rel = { + name: `${repoName}-v${version}`, + version: tag, + git_tag: tag, + git_commit_hash: commitInfo.commit_id, + //git_long_commit_hash: commitInfo.commit, + lts: false, + channel: '', + date: date.toISOString(), + os: '*', + arch: '*', + ext: 'git', + //command: `git clone --depth=1 --single-branch --branch ${tag} ${gitUrl}`, + download: gitUrl, + }; + + releases.push(rel); + } + all.releases = releases; + + return all; +} + +module.exports = getAllReleases; + +if (module === require.main) { + (async function main() { + let all = await getAllReleases('https://github.com/dense-analysis/ale.git'); + // for stderr + let sample = JSON.stringify(all.releases[0], null, 2); + console.error('Sample:'); + console.error(sample); + + all = require('../_webi/normalize.js')(all); + console.info(JSON.stringify(all, null, 2)); + })() + .then(function () { + process.exit(0); + }) + .catch(function (err) { + console.error(err); + }); +} diff --git a/_common/git.js b/_common/git.js deleted file mode 100644 index ba47464..0000000 --- a/_common/git.js +++ /dev/null @@ -1,97 +0,0 @@ -'use strict'; - -require('dotenv').config({ path: '.env' }); -if (!process.env.REPO_BASE_DIR) { - console.warn('[Warn] REPO_BASE_DIR= not set, using ./repos/'); -} -// ../ because this script is one directory deep -let repoBaseDir = process.env.REPO_BASE_DIR || '../repos'; - -const util = require('util'); -const exec = util.promisify(require('child_process').exec); -const fs = require('fs').promises; - -/** - * Lists GitHub Releases (w/ uploaded assets) - * - * @param request - * @param {string} owner - * @param {string} gitUrl - * @returns {PromiseLike | Promise} - */ -async function getAllReleases(gitUrl) { - const all = { - releases: [], - download: '' - }; - - const repoName = gitUrl.split('/').pop(); - const repoPath = `${repoBaseDir}/${repoName}`; - - // Function to clone the repository if it doesn't exist - async function cloneRepository() { - if (!(await repoExists())) { - await exec(`git clone --bare ${gitUrl} ${repoPath}`); - } - } - - // Function to check if the repository already exists - async function repoExists() { - try { - await fs.access(repoPath); - return true; - } catch (error) { - return false; - } - } - - // Function to list all version tags - async function listVersionTags() { - await cloneRepository(); - const { stdout } = await exec(`git --git-dir=${repoPath} tag`); - const tags = stdout.trim().split('\n'); - return tags; - } - - // Function to create an array of release info objects - async function createReleaseInfo() { - const tags = await listVersionTags(); - const releaseInfo = []; - - for (const tag of tags) { - const { stdout: tagDate } = await exec( - `git --git-dir=${repoPath} log -1 --format=%ad --date=iso ${tag}` - ); - const tagObject = { - name: `${repoName}-v${tag}`, - version: tag, - date: tagDate.trim(), - ext: 'git', - command: `git clone --depth=1 --single-branch --branch ${tag} ${gitUrl}` - }; - releaseInfo.push(tagObject); - } - - return releaseInfo; - } - - all.releases = await createReleaseInfo(); - - return all; -} -module.exports = getAllReleases; - -if (module === require.main) { - (async function main() { - let all = await getAllReleases('https://github.com/jshint/jshint'); - console.log(all.releases[0]); - all = require('../_webi/normalize.js')(all); - console.info(JSON.stringify(all, null, 2)); - })() - .then(function () { - process.exit(0); - }) - .catch(function (err) { - console.error(err); - }); -} diff --git a/_webi/normalize.js b/_webi/normalize.js index a81d296..1f7e912 100644 --- a/_webi/normalize.js +++ b/_webi/normalize.js @@ -20,7 +20,7 @@ Object.keys(osMap).forEach(function (name) { maps.oses[name] = true; }); -var formats = ['zip', 'xz', 'tar', 'pkg', 'msi', 'git', 'exe', 'dmg']; +var formats = ['zip', 'xz', 'tar', 'pkg', 'msi', 'git', 'exe', 'dmg', 'git']; formats.forEach(function (name) { maps.formats[name] = true; }); diff --git a/_webi/releases.js b/_webi/releases.js index 5468b9f..684b765 100644 --- a/_webi/releases.js +++ b/_webi/releases.js @@ -114,6 +114,10 @@ Releases.renderBash = async function ( .replace(/^\s*#?WEBI_MINOR=.*/m, 'WEBI_MINOR=' + v.minor) .replace(/^\s*#?WEBI_PATCH=.*/m, 'WEBI_PATCH=' + v.patch) .replace(/^\s*#?WEBI_BUILD=.*/m, 'WEBI_BUILD=' + v.build) + .replace( + /^\s*#?WEBI_GIT_TAG=.*/m, + "WEBI_GIT_TAG='" + rel.git_tag + "'", + ) .replace(/^\s*#?WEBI_LTS=.*/m, 'WEBI_LTS=' + rel.lts) .replace(/^\s*#?WEBI_CHANNEL=.*/m, 'WEBI_CHANNEL=' + rel.channel) .replace( @@ -187,32 +191,44 @@ Releases.renderPowerShell = async function ( .readFile(path.join(__dirname, 'template.ps1'), 'utf8') .then(function (tplTxt) { var pkgver = pkg + '@' + ver; - return tplTxt - .replace( - /^(#)?\$Env:WEBI_HOST\s*=.*/im, - "$Env:WEBI_HOST = '" + baseurl + "'", - ) - .replace( - /^(#)?\$Env:WEBI_PKG\s*=.*/im, - "$Env:WEBI_PKG = '" + pkgver + "'", - ) - .replace( - /^(#)?\$Env:PKG_NAME\s*=.*/im, - "$Env:PKG_NAME = '" + pkg + "'", - ) - .replace( - /^(#)?\$Env:WEBI_VERSION\s*=.*/im, - "$Env:WEBI_VERSION = '" + rel.version + "'", - ) - .replace( - /^(#)?\$Env:WEBI_PKG_URL\s*=.*/im, - "$Env:WEBI_PKG_URL = '" + rel.download + "'", - ) - .replace( - /^(#)?\$Env:WEBI_PKG_FILE\s*=.*/im, - "$Env:WEBI_PKG_FILE = '" + rel.name + "'", - ) - .replace(reInstallTpl, '\n' + installTxt); + return ( + tplTxt + .replace( + /^(#)?\$Env:WEBI_HOST\s*=.*/im, + "$Env:WEBI_HOST = '" + baseurl + "'", + ) + .replace( + /^(#)?\$Env:WEBI_PKG\s*=.*/im, + "$Env:WEBI_PKG = '" + pkgver + "'", + ) + .replace( + /^(#)?\$Env:PKG_NAME\s*=.*/im, + "$Env:PKG_NAME = '" + pkg + "'", + ) + .replace( + /^(#)?\$Env:WEBI_VERSION\s*=.*/im, + "$Env:WEBI_VERSION = '" + rel.version + "'", + ) + .replace( + /^(#)?\$Env:WEBI_GIT_TAG\s*=.*/im, + "$Env:WEBI_GIT_TAG = '" + rel.git_tag + "'", + ) + .replace( + /^(#)?\$Env:WEBI_PKG_URL\s*=.*/im, + "$Env:WEBI_PKG_URL = '" + rel.download + "'", + ) + // TODO replace WEBI_PKG_FILE (which is sometimes a dir) + .replace( + /^(#)?\$Env:WEBI_PKG_PATHNAME\s*=.*/im, + "$Env:WEBI_PKG_PATHNAME = '" + rel.name + "'", + ) + // TODO deprecate + .replace( + /^(#)?\$Env:WEBI_PKG_FILE\s*=.*/im, + "$Env:WEBI_PKG_FILE = '" + rel.name + "'", + ) + .replace(reInstallTpl, '\n' + installTxt) + ); }); }); }; diff --git a/_webi/template.sh b/_webi/template.sh index 0b4f713..1cc7327 100644 --- a/_webi/template.sh +++ b/_webi/template.sh @@ -25,6 +25,7 @@ __bootstrap_webi() { #WEBI_PATCH= # TODO not sure if BUILD is the best name for this #WEBI_BUILD= + #WEBI_GIT_TAG= #WEBI_LTS= #WEBI_CHANNEL= #WEBI_EXT= @@ -248,6 +249,32 @@ __bootstrap_webi() { echo "Saved as ${my_dl_rel}" } + webi_git_clone() { ( + my_url="${1}" + my_dl="${2}" + + my_dl_rel="$( + webi_sub_home "${my_dl}" + )" + if [ -e "${my_dl}" ]; then + echo "Found ${my_dl_rel}" + + cp -RPp "${my_dl}" "${WEBI_TMP}/${WEBI_PKG_FILE}/" + return 0 + fi + + echo "Cloning ${my_url}" + cmd_git="git clone --config advice.detachedHead=false --quiet --depth=1 --single-branch" + rm -rf "${my_dl}.part" + if ! $cmd_git "${my_url}" --branch "${WEBI_GIT_TAG}" "${my_dl}.part"; then + echo >&2 "failed to git clone ${WEBI_PKG_URL}" + exit 1 + fi + mv "${my_dl}.part" "${my_dl}" + + cp -RPp "${my_dl}" "${WEBI_TMP}/${WEBI_PKG_FILE}/" + ); } + # detect which archives can be used webi_extract() { ( @@ -265,6 +292,9 @@ __bootstrap_webi() { elif [ "exe" = "$WEBI_EXT" ]; then echo "Moving ${my_dl_rel}" mv "${WEBI_PKG_PATH}/$WEBI_PKG_FILE" . + elif [ "git" = "$WEBI_EXT" ]; then + echo "Moving ${my_dl_rel}" + mv "${WEBI_PKG_PATH}/$WEBI_PKG_FILE" . elif [ "xz" = "$WEBI_EXT" ]; then echo "Inflating ${my_dl_rel}" unxz -c "${WEBI_PKG_PATH}/$WEBI_PKG_FILE" > "$(basename "$WEBI_PKG_FILE")" @@ -456,6 +486,12 @@ __bootstrap_webi() { webi_pre_install() { webi_check_installed webi_check_available + if test "git" = "${WEBI_EXT}"; then + webi_git_clone \ + "${WEBI_PKG_URL}" \ + "${WEBI_PKG_PATH}/${WEBI_PKG_FILE}" + return 0 + fi webi_download \ "${WEBI_PKG_URL}" \ "${WEBI_PKG_PATH}/${WEBI_PKG_FILE}" diff --git a/_webi/transform-releases.js b/_webi/transform-releases.js index 590ef16..2870a15 100644 --- a/_webi/transform-releases.js +++ b/_webi/transform-releases.js @@ -182,14 +182,18 @@ async function filterReleases( function selectMatches(rel) { if (os) { - if (rel.os !== os) { - return false; + if (rel.os !== '*') { + if (rel.os !== os) { + return false; + } } } if (arch) { - if (rel.arch !== arch) { - return false; + if (rel.arch !== '*') { + if (rel.arch !== arch) { + return false; + } } } diff --git a/_webi/webi-pwsh.ps1 b/_webi/webi-pwsh.ps1 index e0c965d..088699c 100644 --- a/_webi/webi-pwsh.ps1 +++ b/_webi/webi-pwsh.ps1 @@ -100,7 +100,7 @@ if ($exename -eq "-V" -or $exename -eq "--version" -or $exename -eq "version" -o # Fetch .ps1 # TODO detect formats -$PKG_URL = "$Env:WEBI_HOST/api/installers/$exename.ps1?formats=zip,exe,tar" +$PKG_URL = "$Env:WEBI_HOST/api/installers/$exename.ps1?formats=zip,exe,tar,git" Write-Output "Downloading $PKG_URL" # Invoke-WebRequest -UserAgent "Windows amd64" "$PKG_URL" -OutFile ".\.local\tmp\$exename.install.ps1" & curl.exe -fsSL -A "$Env:WEBI_UA" "$PKG_URL" -o .\.local\tmp\$exename.install.ps1