Skip to content

Instantly share code, notes, and snippets.

@cvan
Created September 28, 2022 07:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cvan/7474708d475630201d3aeb266998867c to your computer and use it in GitHub Desktop.
Save cvan/7474708d475630201d3aeb266998867c to your computer and use it in GitHub Desktop.
extract user/repo pair from a GitHub URL
#!/usr/bin/env node
const fs = require("fs");
const path = require("path");
// Copy this file into a file into $HOME/.local/bin/ (or a similar directory in `$PATH`)
//
// 1. pbpaste > ~/.local/bin/git_pair.js
// x-sel --clipboard --output > ~/.local/bin/git_pair.js
// 2. Run commands above to generate an executable file called `git_pair`
// 3. chmod +x ~/.local/bin/git_pair
// 4. git_pair
/*
Run these shell commands to generate an executable file called `git_pair`
cd $HOME/.local/bin
ls -al git_pair
rm git_pair
tee git_pair <<'EO_TEST_RUN'
#!/bin/bash
node ./git_pair.js ${@}
EO_TEST_RUN
chmod u+x git_pair
ls -al git_pair
git_pair
*/
const SCRIPT_NAME = "git_pair";
const SCRIPT_VERSION = "v0.1.0";
const DEFAULT_FALLBACK_ORIGIN = "https://github.com";
const PROVIDER_HOST_ORIGINS = {
github: "https://github.com",
gitlab: "https://gitlab.com"
};
const safeTrim = (text) => `${text || ""}`.replace(/\s+/g, " ").trim();
const isMaybeUrl = (text) => {
if (typeof text || !text?.length) {
return false;
}
const urlMaybe = safeTrim(text);
return (
urlMaybe.startsWith("//") ||
text.startsWith("https:") ||
text.startsWith("http:")
);
};
function parseGhUrl(url, customHost = DEFAULT_FALLBACK_ORIGIN) {
const inputChunks = safeTrim(url).split("/");
if (inputChunks.length === 2) {
let [user, repo] = inputChunks.filter((x) => !isMaybeUrl(x));
return [user, repo].join("/");
}
if (inputChunks.length < 2) {
return url;
}
// Ensure there's a protocol.
let hostOrigin = "";
if (
typeof customHost === "undefined" ||
(typeof customHost === "string" &&
customHost?.length &&
!customHost.includes("/") &&
!customHost.includes("."))
) {
// matches:
// - 'gitlab'
// - 'github'
const matchFound = PROVIDER_HOST_ORIGINS[customHost.toLowerCase()];
hostOrigin = matchFound || PROVIDER_HOST_ORIGINS.github;
if (url.toLowerCase().includes("gitlab.")) {
hostOrigin = PROVIDER_HOST_ORIGINS.gitlab;
}
if (url.toLowerCase().includes("github.")) {
hostOrigin = PROVIDER_HOST_ORIGINS.github;
}
} else {
hostOrigin = customHost;
if (
!hostOrigin.startsWith("https://") &&
!hostOrigin.startsWith("http://")
) {
if (hostOrigin.startsWith("//")) {
hostOrigin = `https:${hostOrigin}`;
} else {
hostOrigin = `https://${hostOrigin}`;
}
}
}
let rootHostname = new URL(hostOrigin)?.hostname;
const { origin, hostname, pathname } = new URL(
url.includes(`${rootHostname}/`) ? url.split(`${rootHostname}/`)[1] : url,
fallbackOrigin || DEFAULT_FALLBACK_ORIGIN
);
const [user, repo] = [...pathname.split("/")].splice(1, 2);
const userRepo = [user.trim(), repo.trim().replace(/.git$/i, "")].join("/");
return `${userRepo}`;
}
const getSafeArgs = (args) =>
(args || []).join(" ").replace(/\s+/g, " ").trim().split(" ");
function processInput(inputArgs = []) {
const safeArgs = getSafeArgs(inputArgs);
const getFlag = (flag = "--output") =>
safeArgs.find((x) => flag === x.replace(/^-+/g, ""));
const getFlagValue = (flag = "--output") =>
safeArgs[safeArgs.findIndex((x) => flag === x.replace(/^-+/g, "")) + 1];
const hasFlag = (flag = "help") => Boolean(getFlag(flag));
if (hasFlag("help")) {
console.log(help());
process.exit(0);
return [undefined, undefined];
}
if (hasFlag("version")) {
process.stdout.write(`${SCRIPT_VERSION}\n`);
process.exit(0);
return [undefined, undefined];
}
const possibleInputUrl =
safeArgs.find((x) => `${x || ""}`.trim().split("/").length >= 2) ||
safeArgs[0];
const possibleInputFallbackOrigin =
safeArgs.find(
(x) =>
`${x || ""}`.trim().includes(".") &&
x.length >= 2 &&
x !== possibleInputUrl
) || safeArgs[1];
return [
possibleInputUrl || undefined,
possibleInputFallbackOrigin || undefined
];
}
function readStdinPiped() {
const rlp = require("readline");
const { stdin: input, stdout: output } = process;
const rl = rlp.createInterface({ input, output });
function ask() {
return new Promise((resolve) => {
return rl.question(
`${help()}
Enter a GitHub/GitLab link:
${"\x1b[2m"}${"\x1b[3m"}[example]${"\x1b[22m"}${"\x1b[0m"}\t${"\x1b[4m"}${"\x1b[2m"}${"https://github.com/greensock/GSAP/issues/322#issuecomment-553763556"}${"\x1b[24m"}
${"\x1b[0m"}
\n ___________________________\n \n `,
(value) => {
rl.output.write("\n");
resolve(value);
}
);
});
}
return ask();
}
function help() {
return (
"\n" +
`
${SCRIPT_NAME}
${SCRIPT_VERSION}
Usage: ${SCRIPT_NAME} [URL] [HOST]
${"\x1b[2m"}${"\x1b[3m"}[URL]${"\x1b[22m"}${"\x1b[0m"}\t${"\x1b[4m"}${"\x1b[2m"}${"https://github.com/greensock/GSAP/issues/322#issuecomment-553763556"}${"\x1b[24m"}${"\x1b[0m"}
${"\x1b[2m"}${"\x1b[3m"}[HOST]${"\x1b[22m"}${"\x1b[0m"}\t${"\x1b[4m"}${"\x1b[2m"}${"https://github.com"}${"\x1b[24m"}
${"\x1b[0m"}
${"\x1b[2m"}${"\x1b[3m"}[URL]${"\x1b[22m"}${"\x1b[0m"}\t${"\x1b[4m"}${"\x1b[2m"}${"https://gitlab.com/stavros/IPFessay/-/tree/master/ipfessay"}${"\x1b[24m"}${"\x1b[0m"}
${"\x1b[2m"}${"\x1b[3m"}[HOST]${"\x1b[22m"}${"\x1b[0m"}\t${"\x1b[4m"}${"\x1b[2m"}${"gitlab"}${"\x1b[24m"}
${"\x1b[0m"}
`.trim() +
"\n"
);
}
function parseAndExit(args) {
const [url, origin] = processInput(args) || [];
if (
typeof url === "string" &&
url.length &&
typeof origin === "string" &&
origin?.length
) {
if (process.stdout.isTTY) {
// Clear screen.
process.stdout.write("\033c");
}
process.stdout.write(parseGhUrl(url, origin));
process.stdout.write("\n");
process.exit(0);
return;
}
if (process.stdout.isTTY) {
process.stdout.write("\n");
process.stdout.moveCursor(1, -3);
process.stdout.write(
`${"\x1b[31m"}` + " [ ERROR ]" + `${"\x1b[0m"}\t Invalid URL\n\n`
);
process.stdout.write("\n");
}
process.exit(1);
}
function main() {
const argv = [...(process?.argv || [])].slice(2);
if (argv.length) {
parseAndExit(argv);
} else {
readStdinPiped().then((input) => {
const delimeter = " ";
const fauxArgs = (input || "").trim().split(delimeter);
parseAndExit(fauxArgs);
});
}
}
// @see https://nodejs.org/docs/latest/api/packages.html#determining-module-system
if (typeof require !== "undefined" && require.main === module) {
main();
}
const TEST = {};
TEST.cases = [
"greensock/GSAP",
"greensock/GSAP",
"github.com/greensock/GSAP",
"https://github.com/greensock/GSAP",
"https://github.com/greensock/GSAP.git",
"https://github.com/greensock/GSAP/blob/master/src/Flip.js",
"https://github.com/greensock/GSAP.git/blob/master/src/Flip.js"
];
TEST.expected = "greensock/GSAP";
const testResults = {};
TEST.cases.forEach((input) => {
const output = parseGhUrl(input);
testResults[input] = output;
});
const testResultsFormatted = Object.keys(testResults).map((input) => {
const output = testResults[input];
const outcome = output === TEST.expected ? "PASS" : "FAIL";
return { input, output, outcome };
});
console.table(testResultsFormatted);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment