UNPKG

reposier

Version:

Tasty CLI on the outside, simple integration with GitHub's API on the inside.

653 lines (613 loc) 21.1 kB
const Configstore = require('configstore') const conf = new Configstore('reposier') const ora = require('ora') const fs = require('fs') const chalk = require('chalk') const json2md = require('json2md') const moment = require('moment') const difference = require('lodash.difference') const _progress = require('cli-progress') /** * Deletes the password in the config file, and saves the token & token information. * @param {String} token The user's token. * @param {Object} tokenInfo An object of info generated from the original request to GitHub's API. */ function replacePassWithToken(token, tokenInfo) { const removePasswordSpinner = ora( 'Removing password and replacing with token ...' ).start() conf.set('token', token) conf.set('tokenInformation', tokenInfo) conf.delete('password') if (!conf.has('password')) { removePasswordSpinner.succeed( 'Successfully removed password and replaced with token.' ) console.log( `\nTo view or revoke authorization for reposier, go to https://github.com/settings/connections/applications/${ process.env.CLIENT_ID }` ) } else { removePasswordSpinner.fail( 'An error occurred while removing your password.' ) } } /** * Generates a token for the app to use in later API calls if needed. */ function generateToken() { const octokit = require('@octokit/rest')({ debug: false }) const generateTokenSpinner = ora('\nGenerating token ...').start() if (conf.has('username') === false || conf.has('password') === false) { generateTokenSpinner.fail( 'reposier could not find a username or password in your configuration.' ) } else { octokit.authenticate({ type: 'basic', username: conf.get('username'), password: conf.get('password') }) octokit.authorization .getOrCreateAuthorizationForApp({ client_id: process.env.CLIENT_ID, client_secret: process.env.CLIENT_SECRET, scopes: ['repo', 'notifications', 'user'], note: 'reposier', note_url: 'https://www.reposier.com/' }) .then(result => { // Successfully created a new token if (result.status === 201) { const token = result.data.token generateTokenSpinner.succeed('Successfully created new token.') // Change out password with token, and save info about request replacePassWithToken(token, result.data) } else if (result.status === 200) { // User already had a token generateTokenSpinner.succeed('Token already exists.') replacePassWithToken('UNDEFINED', result.data) } else { // Failed request generateTokenSpinner.fail('Failed to create a token.') } }) .catch(error => { generateTokenSpinner.fail( "An error occured while contacting GitHub's API." ) console.log(error) }) } } /** * Adds custom converter for json2md to add a link as markdown. * @param {*} input - Input to be converted. * @param {*} json2md Used to call json2md. */ json2md.converters.link = function(input, json2md) { const splitUp = input.split('%%%') const text = splitUp[0] const url = splitUp[1] return `[${text}](${url})` } /** * Add custom converter for json2md to add italics as markdown. * @param {*} input Input to be converted. * @param {*} json2md Used to call json2md. */ json2md.converters.italics = function(input, json2md) { return `_${input}_` } /** * Use momentJS to format a date into a more readable format. * @param {String} date A string in an unfriendly date format. Ex: '2018-05-22T05:58:14Z'. */ function prettyDate(date) { return moment(date).format('MMMM Do YYYY, h:mm:ss A') } /** * Parses data from API request to prepare json2md. * @param {Array} jsonArray Array of JSON data to be parsed by json2md. * @param {Array} repoArray Array of all starred repositories. * @param {Object} data Object of specific repository to be parsed. */ function parseRepoData(jsonArray, repoArray, data) { // Number of Starred Repo const index = repoArray.indexOf(data) + 1 // Name const repoName = data.repo.name // URL const url = data.repo['html_url'] const repoNameObj = { h2: { link: `#${index}. ${repoName}%%%${url}` } } jsonArray.push(repoNameObj) // Description const description = data.repo.description if (description) { const descriptionObj = { blockquote: `${description}` } jsonArray.push(descriptionObj) } else { const descriptionObj = { blockquote: 'No description, website, or topics provided.' } jsonArray.push(descriptionObj) } // Table of Information var tableInfo = { author: '', language: '', stargazers: '', forks: '', starredAt: '', createdAt: '', lastUpdated: '' } // Author const author = data.repo.owner.login if (author) { const url = data.repo.owner['html_url'] const imageUrl = data.repo.owner['avatar_url'] if (imageUrl) { // Asks server to send back an image that is 50px const shrunkImgUrl = imageUrl.concat('&s=50') tableInfo.author = `![${author}](${shrunkImgUrl})[${author}](${url})` } else { tableInfo.author = `[${author}](${url})` } } // Language const language = data.repo.language if (language) { tableInfo.language = language } else { tableInfo.language = '(Not Specified)' } // Stargazers const stargazers = data.repo['stargazers_count'] if (stargazers) { tableInfo.stargazers = stargazers.toLocaleString() } // Forks const forks = data.repo['forks_count'] if (forks) { tableInfo.forks = forks.toLocaleString() } // Starred At const starredAt = data['starred_at'] if (starredAt) { tableInfo.starredAt = prettyDate(starredAt) } // Created At const createdAt = data.repo['created_at'] if (createdAt) { tableInfo.createdAt = prettyDate(createdAt) } // Last Updated const lastUpdated = data.repo['updated_at'] if (lastUpdated) { tableInfo.lastUpdated = prettyDate(lastUpdated) } const tableObj = { table: { headers: [ 'Author', 'Language', 'Stargazers', 'Forks', 'Starred At', 'Created At', 'Last Updated' ], rows: [ [ tableInfo.author, tableInfo.language, tableInfo.stargazers, tableInfo.forks, tableInfo.starredAt, tableInfo.createdAt, tableInfo.lastUpdated ] ] } } jsonArray.push(tableObj) } /** * Function to get a list of the user's starred repositories. * @param {String} user - Defaults to "current". Defines who is being searched for. * @returns {Promise} - Returns a promise that resolves to an array of starred repositories, and rejects to an error. */ function getListOfStarredRepos(user = 'current') { const octokit = require('@octokit/rest')({ debug: false }) if (user !== 'current') { // Getting List of Starred Repos for Temp User return new Promise((resolve, reject) => { // Get Username const username = conf.get('tempUsername') const password = conf.get('tempPassword') const getStarredReposSpinner = ora( `Getting list of starred repos for ${chalk.green(username)} ...` ).start() // Authenticate request to GitHub API octokit.authenticate({ type: 'basic', username: username, password: password }) // Make request to GitHub API octokit.activity // Max Repos Per Page: 100 (Default is 30). // * If user has over 100 starred repos, need to use pagination // * Future patch .getStarredRepos({ per_page: 100 }) .then(result => { // Result is a JSON object with data and headers of the request getStarredReposSpinner.succeed( `Successfully generated list of starred repos for ${chalk.green( username )}.` ) const arrayOfAllStarredRepos = result.data resolve(arrayOfAllStarredRepos) }) .catch(error => { getStarredReposSpinner.fail( "An error occured while contacting GitHub's API." ) console.log(error) reject(error) }) }) } else { return new Promise((resolve, reject) => { // Get Username const username = conf.get('username') const getStarredReposSpinner = ora( `Getting list of starred repos for ${chalk.green(username)} ...` ).start() // Authenticate request to GitHub API octokit.authenticate({ type: 'oauth', token: conf.get('token') }) // Make request to GitHub API octokit.activity // Max Repos Per Page: 100 (Default is 30). // * If user has over 100 starred repos, need to use pagination // * Future patch .getStarredRepos({ per_page: 100 }) .then(result => { // Result is a JSON object with data and headers of the request getStarredReposSpinner.succeed( `Successfully generated list of starred repos for ${chalk.green( username )}.` ) const arrayOfAllStarredRepos = result.data resolve(arrayOfAllStarredRepos) }) .catch(error => { getStarredReposSpinner.fail( "An error occured while contacting GitHub's API." ) console.log(error) reject(error) }) }) } } /** * Goes through messy array of repository information, and pulls out just the repo name and owner for each repo. * @param {Array} arrayOfRepos Array of repositories. Straight from GitHub's API response. * @returns {Array} Returns an array of repoNames and owners. */ function extractReposAndOwners(arrayOfRepos) { let array = [] arrayOfRepos.forEach(repo => { const repoName = repo.repo.name const owner = repo.repo.owner.login const repoObj = { repo: repoName, owner: owner } array.push(repoObj) }) return array } /** * "Transfers" starred repos from one account to another. * @param {String} accountFrom Which account to transfer stars FROM. Either "current" by default, or "temp". */ function transferStars(accountFrom = 'current') { // TODO: Add ability to generate report of what repos were "transfered" // TODO: Progress bar might not be working. Or requests are just going very quickly. // 1. First, get list of starred repos for both account // ** need to compare which repos you don't need to re-star // ** Get list of starred repos for current user // ** Then get list of starred repos for the temp user // 2. Compare the 2 lists // ** If there are any repos that are not on the original list, star them. // Set content-length to 0 for this type of request. const octokit = require('@octokit/rest')({ headers: { 'Content-Length': 0 } }) // 1. if (accountFrom === 'current') { // Get list of starred repos for current user. getListOfStarredRepos() .then(result => { // Go through each repo, and pull out just the name and the author const currentUserRepos = extractReposAndOwners(result) // Get List of Starred Repos for Temporary User getListOfStarredRepos(conf.get('tempUsername')) .then(result => { // Go through each repo, and pull out just the name and the author const tempUserRepos = extractReposAndOwners(result) // 2. Compare the 2 lists of repos // Create a new list of the difference using lodash const reposToStar = difference(currentUserRepos, tempUserRepos) const numberOfReposToStar = reposToStar.length // Set up progress bar const bar = new _progress.Bar() bar.start(numberOfReposToStar, 0) // Star each of those repos for the temporaryUser reposToStar.forEach(repository => { const index = reposToStar.indexOf(repository) + 1 const username = conf.get('tempUsername') const password = conf.get('tempPassword') octokit.authenticate({ type: 'basic', username: username, password: password }) octokit.activity .starRepo({ owner: repository.owner, repo: repository.repo }) .then(result => { // At the last repo, log info about the ratelimit if (index === reposToStar.length) { logRateLimit( octokit, { username: username, password: password }, 'basic' ) } }) .catch(error => console.log(error)) // Increment bar by 1 each time. bar.increment() }) // Stop progress bar bar.stop() // Remember to delete temporary user info from config deleteTemporaryUserInfo() }) .catch(error => { console.log(error) deleteTemporaryUserInfo() }) }) .catch(error => { console.log(error) deleteTemporaryUserInfo() }) } else { // Get list of starred repos for current user. getListOfStarredRepos() .then(result => { // Go through each repo, and pull out just the name and the author const currentUserRepos = extractReposAndOwners(result) // Get List of Starred Repos for Temporary User getListOfStarredRepos(conf.get('tempUsername')) .then(result => { // Go through each repo, and pull out just the name and the author const tempUserRepos = extractReposAndOwners(result) // Compare the 2 lists of repos // Create a new list of the difference using lodash const reposToStar = difference(tempUserRepos, currentUserRepos) const numberOfReposToStar = reposToStar.length // Set up progress bar const bar = new _progress.Bar() bar.start(numberOfReposToStar, 0) // Star each of those repos for the temporaryUser reposToStar.forEach(repository => { const index = reposToStar.indexOf(repository) + 1 const token = conf.get('token') octokit.authenticate({ type: 'oauth', token: token }) octokit.activity .starRepo({ owner: repository.owner, repo: repository.repo }) .then(result => { // At the last repo, log info about the ratelimit if (index === reposToStar.length) { logRateLimit(octokit, { token: token }) } }) .catch(error => console.log(error)) // Increment bar by 1 each time. bar.increment() }) // Stop progress bar bar.stop() // Remember to delete temporary user info from config deleteTemporaryUserInfo() }) .catch(error => { console.log(error) deleteTemporaryUserInfo() }) }) .catch(error => { console.log(error) deleteTemporaryUserInfo() }) } } /** * Logs to the console the rate limit for the account. * @param {Object} authorization An object containing either a username & password, or a token (all as strings). * @param {String} [authType='token'] Describes to function the auth type to use in the request. Either 'token' by default or 'basic'. */ function logRateLimit(octokitInstance, authorization, authType = 'token') { // Octokit Instance that was previously created const octokit = octokitInstance if (authType !== 'token') { // Authenticate with Basic Authentication // Authenticate request octokit.authenticate({ type: 'basic', username: authorization.username, password: authorization.password }) // Use Octokit to get rate limit for account octokit.misc .getRateLimit({}) .then(result => { parseRateLimitResponse(result) }) .catch(error => console.log(error)) } else { // Authenticate with Token octokit.authenticate({ type: 'oauth', token: authorization.token }) // Use Octokit to get rate limit for account octokit.misc .getRateLimit({}) .then(result => { parseRateLimitResponse(result) }) .catch(error => console.log(error)) } } /** * Parses the respnose from requesting a user's rate limit from the GitHub API. Logs info to console. * @param {Object} response Response from GitHub API concerning user's rate limit. */ function parseRateLimitResponse(response) { console.log('') const rateLimitRemaining = response.data.resources.core.remaining // Convert from UTC Epoch Seconds | http://en.wikipedia.org/wiki/Unix_time const rateLimitReset = response.data.resources.core.reset * 1000 const timeToReset = moment(rateLimitReset).format('h:mm A') console.log( `You have ${chalk.green( rateLimitRemaining )} requests remaining. Your limit will reset at ${chalk.green( timeToReset )}.` ) } function deleteTemporaryUserInfo() { conf.delete('tempUsername') conf.delete('tempPassword') } function exportStarredRepoList(jsonArray) { const writingDataSpinner = ora('Writing data ...').start() // Output variable transformed using json2md const mdOutput = json2md(jsonArray) // Create Directory const createDirectorySpinner = ora('Creating export directory ...').start() // Test if the Exports Directory exists fs.access('./exports', error => { if (error) { // Exports directory does not exist // * Need to create directory fs.mkdir('./exports/', error => { if (error) { createDirectorySpinner.fail( 'An error occured while creating the directory, "./exports/"' ) console.log(error) } else { fs.mkdir('./exports/md/', error => { if (error) { createDirectorySpinner.fail( 'An error occured while creating the directory, "./exports/md".' ) } else { createDirectorySpinner.succeed( `Created directory at ${chalk.green('./exports/md')}.` ) fs.writeFile('./exports/md/starredRepos.md', mdOutput, error => { if (error) { writingDataSpinner.fail('Error occured when saving data.') console.log(error) } else { writingDataSpinner.succeed( `File saved at ${chalk.green( './exports/md/starredRepos.md' )}.` ) } }) } }) } }) } else { // Exports directory DOES exist // * Check if md directory exists fs.access('./exports/md', error => { if (error) { console.log(error) // MD directory does not exist // * Need to create directory fs.mkdir('./exports/md/', error => { if (error) { createDirectorySpinner.fail( 'An error occured while creating the directory, "./exports/md".' ) } else { createDirectorySpinner.succeed( `Created directory at ${chalk.green('./exports/md')}.` ) fs.writeFile('./exports/md/starredRepos.md', mdOutput, error => { if (error) { writingDataSpinner.fail('Error occured when saving data.') console.log(error) } else { writingDataSpinner.succeed( `File saved at ${chalk.green( './exports/md/starredRepos.md' )}.` ) } }) } }) } else { createDirectorySpinner.succeed('Directory exists.') // MD directory DOES exist // Write or Overwrite file fs.writeFile('./exports/md/starredRepos.md', mdOutput, error => { if (error) { writingDataSpinner.fail('Error occured when saving data.') console.log(error) } else { writingDataSpinner.succeed( `File saved at ${chalk.green('./exports/md/starredRepos.md')}.` ) } }) } }) } }) } module.exports = { generateToken, getListOfStarredRepos, transferStars, parseRepoData, exportStarredRepoList, prettyDate, extractReposAndOwners }