@crguezl/gh-utilities
Version:
Set of functions to be used inside gh-extensions
652 lines (557 loc) • 17.6 kB
JavaScript
const ins = require("util").inspect;
const tmp = require('tmp');
const debug = false;
const deb = (...args) => {
if (debug) console.log(ins(...args, { depth: null }));
};
const fs = require("fs");
const shell = require('shelljs');
//const path = require('path');
const balanced = require('balanced-match');
const concurrently = 'npx concurrently';
//console.log(concurrently);
function showError(error) {
if (error) {
console.error(`Error!: ${error}`);
process.exit(1);
}
}
exports.showError = showError;
function sh(executable, ...args) {
let command = `${executable} ${args.join('')}`;
deb(command);
let result = shell.exec(command, { silent: true });
if (result.code !== 0) {
shell.echo(`Error: Command "${command}" failed\n${result.stderr}`);
shell.exit(result.code);
}
return result.stdout.replace(/\s+$/, '');
}
exports.sh = sh;
function shContinue(executable, ...args) {
let command = `${executable} ${args.join('')}`;
deb(command);
let result = shell.exec(command, { silent: true });
if (result.code !== 0) {
shell.echo(`Error: Command "${command}" failed\n${result.stderr}`);
}
return result.stdout.replace(/\s+$/, '');
}
exports.shContinue = sh.shContinue;
function shStderr(executable, ...args) {
let command = `${executable} ${args.join('')}`;
// console.log(command);
let result = shell.exec(command, { silent: true });
return result.stderr;
}
exports.shStderr = shStderr;
function shSilentlyContinue(executable, arg, cb) {
let command = `${executable} ${arg}`;
//deb(command);
let result = shell.exec(command, { silent: true }, cb);
return result;
}
exports.shSilentlyContinue = shSilentlyContinue;
const gh = (...args) => sh("gh", ...args);
exports.gh = gh;
const ghCont = (...args) => shContinue("gh", ...args);
exports.ghCont = ghCont;
const ghCode = (...args) => shStderr("gh", ...args);
exports.ghCode = ghCode;
const ghSilentlyCont = (arg, cb) => shSilentlyContinue("gh", arg, cb);
exports.ghSilentlyCont = ghSilentlyCont;
function names2urls(names) {
let urls = names.map(repoName => gh(`browse -n --repo ${repoName}`));
return urls.map(u => u.replace(/\s*$/, '.git'));
}
exports.names2urls = names2urls;
function getUserLogin() {
/* See also
gh auth status -t
give us the user and the token
*/
let result = gh(`api 'user' --jq .login`);
return result;
}
exports.getUserLogin = getUserLogin;
function getDefaultOrg() {
let checkCommand = `gh config get 'current-org'`;
let getResult = shell.exec(checkCommand, { silent: true });
let defaultOrg = getResult.stdout.replace(/\s+$/,'');
return defaultOrg;
}
exports.getDefaultOrg = getDefaultOrg;
function getEduDefaultOrg() {
let checkCommand = `gh edu get -o`;
let getResult = shell.exec(checkCommand, { silent: true });
let defaultOrg = getResult.stdout.replace(/\s+$/,'');
return defaultOrg;
}
exports.getEduDefaultOrg = getEduDefaultOrg;
function setDefaultOrg(org) {
let setCommand = `gh config set 'current-org' '${org}'`;
let setResult = shell.exec(setCommand, { silent: true });
//console.log(setResult);
let defaultOrg = getDefaultOrg();
return defaultOrg == org;
}
exports.setDefaultOrg = setDefaultOrg;
// Makes a paginated query and returns an array with the name of the repos
function executeQuery(query) {
//console.log(`executeQuery`)
let command = `gh api graphql --paginate -f query='${query}'`;
//console.log(command);
let queryResult = shell.exec(command, { silent: true });
if (queryResult.code !== 0 || queryResult.length === 0) {
console.error(`No repos found in org "${org}" matching query "${search}"`)
process.exit(1);
}
/**************************/
let rout = queryResult.stdout;
//console.log(rout);
let chunkInfo; //, dummy = { data: { organization: { repositories: { edges: []}}}};
let result = [];
while (chunkInfo = balanced('{', '}', rout)) {
//console.log(chunkInfo);
let currentObj = JSON.parse('{'+chunkInfo.body+'}');
//console.log(ins(currentObj, {depth:null}));
if (currentObj.data && currentObj.data.organization)
result = result.concat(currentObj.data.organization.repositories.edges);
else result = result.concat(currentObj.data.search.edges);
rout = chunkInfo.post;
}
//dummy.data.organization.repositories.edges = result;
//console.log(ins(result, {depth:null}));
//console.log(typeof result);
result = result.map(x => x.node.name);
//console.log(ins(result, {depth:null}));
return result;
}
const allRepos = (org) => `
query($endCursor: String) {
organization(login: "${org}") {
repositories(first: 100, after: $endCursor) {
pageInfo {
endCursor
hasNextPage
}
edges {
node {
name
}
}
}
}
}
`;
function fzfGetRepos(org, regexp) {
/*
let command = `gh repo list -L100 ${org} --json name --jq '.[] | .name' | fzf -m`;
let result = shell.exec(command, { silent: false });
if (result.code !== 0) process.exit(result.code);
return result.stdout.replace(/\s+$/,'')
*/
let result = executeQuery(allRepos(org));
//console.log(`Inside fzfGetRepos(${org}, ${regexp}) called executeQuery. Result = ${ins(result, { depth: null})}`);
if (regexp) {
regexp = new RegExp(regexp,'i');
result = result.filter(rn => {
return regexp.test(rn)
});
}
result = result.join("\n");
const name = tmp.tmpNameSync();
fs.writeFileSync(name, result)
//console.log('Created temporary filename: ', name);
let command = `cat ${name} | fzf -m --bind 'ctrl-a:toggle-all' --prompt='${org}:Use tab to choose repos to download> ' --layout=reverse --border`;
let fzfresult = shell.exec(command, { silent: false });
console.clear();
if (!fzfresult || fzfresult.code !== 0) {
return [];
}
// console.log(`"${fzfresult}"`);
let repoList = fzfresult.stdout.replace(/\s+$/,'').split(/\s+/);
let repoSpec = repoList.join(',');
//console.log(`----\n${repoSpec}\n----`);
return repoSpec;
}
const searchForRepos = (search, org, includeForks) => `
query($endCursor: String) {
search(type: REPOSITORY, query: "org:${org} ${search} in:name fork:${includeForks}", first: 100, after: $endCursor) {
pageInfo {
hasNextPage
endCursor
}
edges {
node {
... on Repository {
name
}
}
}
}
}
`
function getRepoListFromAPISearch(options, org) {
let search = options.search;
let regexp = options.regexp;
//let query;
//console.log('getRepoListFromAPISearch '+search+" "+org)
if (!org) {
console.error("Aborting. Specify a GitHub organization");
process.exit(1);
}
try {
if (search === ".") {
return fzfGetRepos(org, regexp);
} else {
let includeForks = options.fork || "true";
let result = executeQuery(searchForRepos(search, org, includeForks));
//console.log(result)
return result.join(",");
}
} catch (error) {
console.error(`${error}: No repos found in org "${org}" matching query "${search}"`)
}
}
/* REST version
function getRepoListFromAPISearch(search, org) {
let jqQuery;
let query;
//console.log('getRepoListFromAPISearch '+search+" "+org)
if (!org) {
console.error("Aborting. Specify a GitHub organization");
process.exit(1);
}
try {
if (search !== ".") {
query = `search/repositories?q=org%3A${org}`;
query += `%20${encodeURIComponent(search)}%20in%3Aname`;
jqQuery = '.items | .[].full_name';
} else {
// Or get all repos
if (gh(`api "users/${org}" -q '.type'`).match(/Organization/i)) {
query = `orgs/${org}/repos`;
}
else
query = `users/${org}/repos`;
jqQuery = '.[].full_name'
}
let command = `gh api --paginate "${query}" -q "${jqQuery}"`;
let queryResult = shell.exec(command, { silent: true });
if (queryResult.code !== 0 || queryResult.length === 0) {
console.error(`No repos found in org "${org}" matching query "${search}"`)
process.exit(1);
}
let repos = queryResult.stdout.replace(/\s+$/, '').replace(/^\s+/, '');
let result = repos.split(/\s+/);
result = result.join(",");
return result;
} catch (error) {
console.error(`No repos found in org "${org}" matching query "${search}"`)
}
}
*/
exports.getRepoListFromAPISearch = getRepoListFromAPISearch;
function getRepoListFromFile(file) {
return fs.readFileSync(file, "utf-8")
.replace(/\s*$/, "") // trim
.replace(/^\n*/, "")
.replace(/\n+/g, ",")
}
exports.getRepoListFromFile = getRepoListFromFile;
function getNumberOfCommits(ownerSlashRepo) {
let [owner, repo] = ownerSlashRepo.split('/');
let defaultBranch = gh(`api /repos/${ownerSlashRepo} --jq .default_branch`);
// console.log(owner, repo);
const queryNumberOfCommits = `'
query {
repository(owner:"${owner}", name:"${repo}") {
object(expression:"${defaultBranch}") {
... on Commit {
history {
totalCount
}
}
}
}
}'`
if (RepoIsEmpty(ownerSlashRepo)) {
return ['no branch', 0];
} else {
let numCommits = ghCont(`api graphql --paginate -f query=${queryNumberOfCommits} --jq .[].[].[].[].[]`)
return [defaultBranch, numCommits];
}
}
exports.getNumberOfCommits = getNumberOfCommits;
function branches(ownerSlashRepo) {
let [owner, repo] = ownerSlashRepo.split('/');
let query = (orgName, repoName) => `
query {
organization(login: "${orgName}") {
repository(name: "${repoName}") {
id
name
refs(refPrefix: "refs/heads/", first: 2) {
edges {
node {
branchName:name
}
}
pageInfo {
endCursor #use this value to paginate through repos with more than 100 branches
}
}
}
}
}
`
let queryS = `api graphql -f query='${query(owner, repo)}'`;
//console.log(queryS)
let result = ghCont(queryS)
let branchNames = JSON.parse(result).data.organization.repository.refs.edges; // array of branch names
if (branchNames.length) {
branchNames = branchNames.map(b => b.node.branchName)
}
return branchNames;
}
exports.branches = branches;
function numBranches(ownerSlashRepo) {
let splitted = ownerSlashRepo.map(r => r.split('/'));
let alias = splitted.map(r => r[1]).map(r => r.replace(/[.-]/g, '_'));
let query = (alias, orgName, repoName) => {
return `
${alias}: organization(login: "${orgName}") {
repository(name: "${repoName}") {
id
name
refs(refPrefix: "refs/heads/", first: 2) {
edges {
node {
branchName:name
}
}
}
}
}
`
}
let bigQuery = 'query {\n';
splitted.forEach(([org, repo], i) => {
bigQuery += query(alias[i], org, repo)
});
bigQuery += '\n}';
//console.log(bigQuery);
let queryS = `api graphql -f query='${bigQuery}'`;
//console.log(queryS)
try {
result = ghCont(queryS)
result = JSON.parse(result);
let branchesLengths = alias.map((a, i) => result.data[a].repository.refs.edges.length)
return branchesLengths;
} catch(e) {
throw "Error inside function 'numBranches'\n"+e
}
}
exports.numBranches = numBranches;
/*
let testArr = [
"ULL-MFP-AET-2122/aprender-markdown-anabel-coello-perez-alu0100885200",
"ULL-MII-SYTWS-2122/asyncserialize-lorenaolaru",
"ULL-MFP-AET-2122/aprender-markdown-adela-gonzalez-maury-alu0101116204"
]
console.log(numBranches(testArr));
process.exit(0);
*/
// https://stackoverflow.com/questions/49442317/github-graphql-repository-query-commits-totalcount
function RepoIsEmpty(ownerSlashRepo) {
return !branches(ownerSlashRepo).length;
}
exports.RepoIsEmpty = RepoIsEmpty;
function fzfGetOrg() {
// if (process.env["GITHUB_ORG"]) return process.env["GITHUB_ORG"];
let defaultOrg = getDefaultOrg();
if (defaultOrg && defaultOrg.length) return defaultOrg;
let command = `gh api --paginate /user/memberships/orgs --jq '.[].organization | .login' | fzf --prompt='Choose an organization> ' --layout=reverse --border`;
let orgResult = shell.exec(command, { silent: false });
//console.log(orgResult);
//console.log(`'${orgResult.stdout}'`);
if (orgResult.code == 0) return orgResult.stdout.replace(/\s+/, '');
console.error("Please, provide a GitHub Organization to work with!");
process.exit(0);
}
exports.fzfGetOrg = fzfGetOrg;
function getRepoList(options, org) {
let repos;
if (options.csr)
repos = options.csr;
else if (options.file)
repos = getRepoListFromFile(options.file);
else if (options.search) {
repos = getRepoListFromAPISearch(options, org);
}
else {
options.search = '.';
repos = getRepoListFromAPISearch(options, org);
}
repos = (repos && repos.length) ? repos.split(/\s*,\s*/) : [];
repos = addImplicitOrgIfNeeded(repos, org);
return repos;
}
exports.getRepoList = getRepoList;
const LegalGHRepoNames = /^(?:([\p{Letter}\p{Number}._-]+)\/)?([\p{Letter}\p{Number}._-]+)$/ui;
function addImplicitOrgIfNeeded(repos, org) {
//console.log(repos);
if (org) {
return repos.map(r => {
let m = LegalGHRepoNames.exec(String(r));
if (m) {
if (!m[1]) r = org + "/" + r;
} else
showError(`The repofnames2 '${r}' does not matches the pattern 'OrganizationName/repoName'`)
return r;
})
}
return repos;
}
exports.addImplicitOrgIfNeeded = addImplicitOrgIfNeeded;
function addSubmodules({urls, repos, parallel, depth, cloneOnly, submoduleArgs=[], cloneArgs}) {
//console.log(repos);
let nb = numBranches(repos)
parallel = Math.min(parallel, urls.length);
let par = `${concurrently} -m ${parallel} `;
console.log(`cloning with ${parallel} concurrent processes ...`);
urls.forEach(
(url, i) => {
let isEmpty = nb[i] === 0;
if (isEmpty && !cloneOnly) {
console.log(`Skipping to add repo ${url} because is empty!`)
}
else {
let command = ` "git clone ${url} ${cloneArgs.join(' ')}"`;
if (depth) command += ` --depth ${depth}`
par += command;
}
})
//console.log(par);
let result = shell.exec(par, { silent: false });
if (result.code !== 0) {
console.error(`Error: Command "${par}" failed\n${result.stderr}`);
}
// add submodules sequentially and absorbgitdirs
console.log(cloneOnly);
if (!cloneOnly) {
console.log("Inside urls.forEach")
urls.forEach(
(url, i) => {
let isEmpty = nb[i] === 0;
if (isEmpty) {
console.log(`Skipping to add repo ${url} because is empty!`)
}
else {
let repoName = repos[i].split('/')[1];
let command = `git submodule add ${url} ${submoduleArgs.join(" ")}; git submodule absorbgitdirs ${repoName}`;
let result = shell.exec(command, { silent: false });
if ((result.code !== 0) || result.error) {
shell.echo(`Error: Command "${command}" failed\n${result.stderr}`);
console.log(`Skipping to add repo ${url}!\n\n`)
}
}
})
}
}
exports.addSubmodules = addSubmodules;
function getMembers(org) {
// Get the list of members of the organization
let result = ghSilentlyCont(`api graphql --paginate -f query='
query($endCursor: String) {
organization(login: "${org}") {
membersWithRole(first: 10, after:$endCursor) {
pageInfo {
hasNextPage,
endCursor
}
edges {
node {
login
name
url
}
role
}
}
}
}
'
`);
/* example of output
{
"data": {
"organization": {
"membersWithRole": {
"pageInfo": {
"hasNextPage": true,
"endCursor": "Y3Vyc29yOnYyOpHOAVenaQ=="
},
"edges": [
{
"node": {
"login": "crguezl",
"name": "Casiano Rodriguez-Leon"
},
"role": "ADMIN"
},
{
"node": {
"login": "casiano",
"name": "Casiano"
},
"role": "MEMBER"
},
{
"node": {
"login": "amarrerod",
"name": "Alejandro Marrero"
},
"role": "MEMBER"
},
{
"node": {
"login": "ivan-ga",
"name": "Iván González"
},
"role": "MEMBER"
}
]
}
}
}
}
*/
if (result.stderr) {
let messages = JSON.parse(result.stdout).errors.map(x => x.message);
console.log(messages.join("\n"));
process.exit(1);
}
rout = result.stdout;
//console.log(rout);
let chunkInfo, members = [];
while (chunkInfo = balanced('{', '}', rout)) {
//console.log(chunkInfo);
let currentObj = JSON.parse('{' + chunkInfo.body + '}');
let currMembers = currentObj.data.organization.membersWithRole.edges.map(x => {
let y = x.node;
y.role = x.role.toLowerCase();
return y;
});
//console.log(currMembers);
members = members.concat(currMembers);
rout = chunkInfo.post;
}
return members;
} // end getMembers
module.exports.getMembers = getMembers;
//fzfGetOrg();
//getRepoListFromAPISearch('.', 'ULL-ESIT-DMSI-1920')
//console.log(setDefaultOrg('ULL-MFP-AET-2122'));
//getMembers('ULL-ESIT-DMSI-1920')