const async = require('async')
const parse = require('parse-link-header')
const Octokit = require('@octokit/rest')
const { verifyRequired } = require('@bowtie/utils')
const Base = require('./Base')
const Jekyll = require('./Jekyll')
/**
* GitHub class
*/
class GitHub extends Base {
/**
* Create new GitHub object
*
* @constructor
* @param {Object} [options] - Options for this GitHub instance
* @param {String} [options.token] - Initialize GitHub with auth token
*/
constructor (options = {}) {
super(options)
this.octokit = new Octokit()
if (options.token) {
this.auth(options.token)
}
}
/**
* Generic exec method for calling octokit methods
*
* @param {String} key - Key for exec call (return object key)
* @param {Function} action - Octokit action to be used
* @param {Object} [params] - Additional params (sent to github)
* @returns {Promise<Object>} - Returns promise with response data
*/
_exec (key, action, params = {}) {
return new Promise(
(resolve, reject) => {
this.logger.info(`Exec github for key: ${key}`)
if (params['per_page'] && params['per_page'].toString() === '0') {
let list = []
let nextPage = '1'
async.whilst(
() => nextPage !== null,
(callback) => {
const actionParams = Object.assign({}, params, { page: nextPage, per_page: '100' })
if (!this.cache) {
Object.assign(actionParams, {
headers: {
'If-None-Match': ''
}
})
}
action(actionParams)
.then(response => {
const pagination = parse(response.headers.link)
nextPage = pagination && pagination['next'] ? pagination['next']['page'] : null
list.push(...response['data'])
callback(null, list)
})
.catch(callback)
},
(err, n) => {
if (err) {
this.emit('err', err)
reject(err)
} else {
resolve({
[key]: list,
pages: null
})
}
}
)
} else {
action(params)
.then(resp => {
resolve({
[key]: resp.data,
pages: parse(resp.headers.link)
})
})
.catch(err => {
this.emit('err', err)
reject(err)
})
}
}
)
}
/**
* Create a new Jekyll instance for this GitHub connection
* @param {Object} params - Parameters
* @param {String} params.owner - Repo owner name
* @param {String} params.repo - Repo name
*/
jekyll (params = {}) {
verifyRequired(params, [ 'owner', 'repo' ])
return new Jekyll(Object.assign({}, params, { github: this }))
}
/**
* Authorize this GitHub instance
* @param {String} token - GitHub access token
*/
auth (token) {
if (token) {
this.octokit = new Octokit({ auth: `token ${token}` })
}
}
/**
* List orgs for authenticated user
* @param {Object} [params] - Additional params (sent to github)
*/
orgs (params = {}) {
return this._exec('orgs', this.octokit.orgs.listForAuthenticatedUser, params)
}
/**
* List repos for authenticated user
* @param {Object} [params] - Additional params (sent to github)
*/
repos (params = {}) {
return this._exec('repos', this.octokit.repos.list, params)
}
/**
* Get a specific repo
* @param {Object} params - Parameters
* @param {String} params.owner - Repo owner name
* @param {String} params.repo - Repo name
*/
repo (params = {}) {
verifyRequired(params, [ 'owner', 'repo' ])
return this._exec('repo', this.octokit.repos.get, params)
}
/**
* Get contributors for a specific repo
* @param {Object} params - Parameters
* @param {String} params.owner - Repo owner name
* @param {String} params.repo - Repo name
*/
contributors (params = {}) {
verifyRequired(params, [ 'owner', 'repo' ])
return this._exec('contributors', this.octokit.repos.listContributors, params)
}
/**
* Get collaborators for a specific repo
* @param {Object} params - Parameters
* @param {String} params.owner - Repo owner name
* @param {String} params.repo - Repo name
*/
collaborators (params = {}) {
verifyRequired(params, [ 'owner', 'repo' ])
return this._exec('collaborators', this.octokit.repos.listCollaborators, params)
}
/**
* Get branches for a specific repo
* @param {Object} params - Parameters
* @param {String} params.owner - Repo owner name
* @param {String} params.repo - Repo name
*/
branches (params = {}) {
verifyRequired(params, [ 'owner', 'repo' ])
return this._exec('branches', this.octokit.repos.listBranches, params)
}
/**
* Get a single branch for a specific repo
* @param {Object} params - Parameters
* @param {String} params.owner - Repo owner name
* @param {String} params.repo - Repo name
* @param {String} params.branch - Branch name
*/
branch (params = {}) {
verifyRequired(params, [ 'owner', 'repo', 'branch' ])
return this._exec('branch', this.octokit.repos.getBranch, params)
}
/**
* Get the currently authenticated user
* @param {Object} [params] - Additional params (sent to github)
*/
user (params = {}) {
return this._exec('user', this.octokit.users.getAuthenticated, params)
}
/**
* Get file(s) list or conent from github
* @param {Object} [params] - Parameters
* @param {String} [params.path='.'] - Path to load (Default is entire repo)
* @param {Boolean} [params.recursive] - Recursively load files (for dir path only)
* @param {Boolean} [params.flatten] - Flatten response (for recursive only)
* @param {Boolean} [params.tree] - Return file tree response (for dir path only)
*/
files (params = {}) {
if (!params.path) {
params.path = '.'
}
[ 'recursive', 'flatten', 'tree' ].forEach(opt => {
params[opt] = params[opt] && params[opt].toString().toLowerCase() === 'true'
})
// this.logger.info('LOADING GH FILES WITH')
// this.logger.info(JSON.stringify(params))
return this._loadPath(params)
}
/**
* Update a file on github
* @param {Object} params - Parameters
* @param {String} params.owner - Repo owner name
* @param {String} params.repo - Repo name
* @param {String} params.path - Path of file being updated
* @param {String} params.message - Message for commit on updating file
* @param {String} params.content - New content for updated file
* @param {String} params.sha - Current sha of file being updated
*/
updateFile (params = {}) {
verifyRequired(params, [ 'owner', 'repo', 'path', 'message', 'content', 'sha' ])
if (params['ref']) {
params['branch'] = params['ref']
}
return this._exec('file', this.octokit.repos.updateFile, params)
}
/**
* Create a file on github
* @param {Object} params - Parameters
* @param {String} params.owner - Repo owner name
* @param {String} params.repo - Repo name
* @param {String} params.path - Path of file being created
* @param {String} params.message - Message for commit on creating file
* @param {String} params.content - New content for created file
*/
createFile (params = {}) {
verifyRequired(params, [ 'owner', 'repo', 'path', 'message', 'content' ])
if (params['ref']) {
params['branch'] = params['ref']
}
return this._exec('file', this.octokit.repos.createFile, params)
}
/**
* Update a file on github
* @param {Object} params - Parameters
* @param {String} params.owner - Repo owner name
* @param {String} params.repo - Repo name
* @param {String} params.path - Path of file being deleted
* @param {String} params.message - Message for commit on deleting file
* @param {String} params.sha - Current sha of file being deleted
*/
deleteFile (params = {}) {
verifyRequired(params, [ 'owner', 'repo', 'path', 'message', 'sha' ])
if (params['ref']) {
params['branch'] = params['ref']
}
return this._exec('file', this.octokit.repos.deleteFile, params)
}
/**
* Create or update 1 or more files on github
* @param {Object} params - Parameters
* @param {String} params.owner - Repo owner name
* @param {String} params.repo - Repo name
* @param {Array} params.files - Array of file objects with base64 encoded content
* @param {String} params.message - Message for commit on creating file(s)
*/
upsertFiles (params = {}) {
verifyRequired(params, [ 'owner', 'repo', 'files', 'message' ])
return new Promise(
(resolve, reject) => {
const options = Object.assign({}, params)
const branchName = params['ref'] || 'master'
const { files, message } = params
const fileDefaults = {
mode: '100644',
encoding: 'utf-8'
}
const fileList = files.map(file => {
return Object.assign({}, fileDefaults, file)
})
Promise.all(fileList.map(file => {
return this.octokit.git.createBlob(Object.assign({}, options, {
content: file.content,
encoding: file.encoding
}))
})).then(blobs => {
return this.octokit.repos.getBranch(Object.assign({}, options, {
branch: branchName
})).then(branch => {
return this.octokit.git.createTree(Object.assign({}, options, {
tree: fileList.map((file, index) => {
return {
path: file.path,
mode: file.mode,
type: 'blob',
sha: blobs[index].data.sha
}
}),
base_tree: branch.data.commit.sha
})).then(tree => {
return this.octokit.git.createCommit(Object.assign({}, options, {
message: message,
tree: tree.data.sha,
parents: [
branch.data.commit.sha
]
}))
}).then(commit => {
return this.octokit.git.updateRef(Object.assign({}, options, {
ref: `heads/${branchName}`,
sha: commit.data.sha
})).then(resp => {
resolve(commit)
})
})
})
})
.catch(err => {
this.emit('err', err)
reject(err)
})
}
)
}
/**
* Load a given path with options
* @param {Object} [options] - Load path options
*/
_loadPath (options = {}) {
return new Promise(
(resolve, reject) => {
const content = {}
if (!this.cache) {
Object.assign(options, {
headers: {
'If-None-Match': ''
}
})
}
this.octokit.repos.getContents(options)
.then(resp => {
if (Array.isArray(resp.data)) {
const files = resp.data
if (!options.flatten && !options.tree) {
content.files = files
}
async.each(files, (file, next) => {
if (options.flatten) {
content[file.path] = options.tree ? file.sha : file
} else if (options.tree) {
content[file.path] = (file.type === 'dir' && options.recursive) ? {} : file.sha
}
if (file.type === 'dir' && options.recursive) {
const subDirOpts = Object.assign({}, options, {
path: file.path
})
this._loadPath(subDirOpts)
.then(subDirContent => {
if (options.flatten) {
Object.assign(content, subDirContent)
} else {
if (options.tree) {
Object.assign(content[file.path], subDirContent)
} else {
Object.assign(file, subDirContent)
}
}
next()
}).catch(next)
} else {
next()
}
}, err => {
if (err) {
this.emit('err', err)
reject(err)
} else {
resolve(content)
}
})
} else {
const file = resp.data
if (options.flatten) {
content[file.path] = options.tree ? file.sha : file
} else {
content.file = options.tree ? file.sha : file
}
resolve(content)
}
})
.catch(err => {
this.emit('err', err)
reject(err)
})
}
)
}
}
module.exports = GitHub