const logger = require('winston'); const glGot = require('gl-got'); const config = {}; module.exports = { initRepo, // Search findFilePaths, // Branch branchExists, getBranchPr, // issue addAssignees, addReviewers, addLabels, // PR findPr, checkForClosedPr, createPr, getPr, updatePr, // file commitFilesToBranch, getFile, getFileContent, getFileJson, }; // Initialize GitLab by getting base branch async function initRepo(repoName, token, endpoint) { logger.debug(`initRepo(${repoName})`); if (token) { process.env.GITLAB_TOKEN = token; } else if (!process.env.GITLAB_TOKEN) { throw new Error(`No token found for GitLab repository ${repoName}`); } if (token) { process.env.GITLAB_TOKEN = token; } if (endpoint) { process.env.GITLAB_ENDPOINT = endpoint; } config.repoName = repoName.replace('/', '%2F'); try { const res = await glGot(`projects/${config.repoName}`); config.defaultBranch = res.body.default_branch; logger.debug(`${repoName} default branch = ${config.defaultBranch}`); } catch (err) { logger.error(`GitLab init error: ${JSON.stringify(err)}`); throw err; } return config; } // Search // Returns an array of file paths in current repo matching the fileName async function findFilePaths(fileName) { logger.verbose('Can\'t find multiple package.json files in GitLab'); return [fileName]; } // Branch // Returns true if branch exists, otherwise false async function branchExists(branchName) { logger.debug(`Checking if branch exists: ${branchName}`); try { const url = `projects/${config.repoName}/repository/branches/${branchName}`; const res = await glGot(url); if (res.statusCode === 200) { logger.debug('Branch exists'); return true; } // This probably shouldn't happen logger.debug('Branch doesn\'t exist'); return false; } catch (error) { if (error.statusCode === 404) { // If file not found, then return false logger.debug('Branch doesn\'t exist'); return false; } // Propagate if it's any other error throw error; } } // Returns the Pull Request for a branch. Null if not exists. async function getBranchPr(branchName) { logger.debug(`getBranchPr(${branchName})`); const urlString = `projects/${config.repoName}/merge_requests?state=opened`; const res = await glGot(urlString); logger.debug(`Got res with ${res.body.length} results`); let pr = null; res.body.forEach((result) => { if (result.source_branch === branchName) { pr = result; } }); if (!pr) { return null; } return getPr(pr.id); } // Issue async function addAssignees(prNo, assignees) { logger.debug(`Adding assignees ${assignees} to #${prNo}`); if (assignees.length > 1) { logger.error('Cannot assign more than one assignee to Merge Requests'); } let url = `projects/${config.repoName}/merge_requests/${prNo}`; url = `${url}?assignee_id=${assignees[0]}`; await glGot.put(url); } async function addReviewers(prNo, reviewers) { logger.debug(`addReviewers('${prNo}, '${reviewers})`); logger.error('No reviewer functionality in GitLab'); } async function addLabels(prNo, labels) { logger.debug(`Adding labels ${labels} to #${prNo}`); let url = `projects/${config.repoName}/merge_requests/${prNo}`; url = `${url}?labels=${labels.join(',')}`; await glGot.put(url); } async function findPr(branchName, prTitle, state = 'all') { logger.debug(`findPr(${branchName}, ${prTitle}, ${state})`); const urlString = `projects/${config.repoName}/merge_requests?state=${state}`; const res = await glGot(urlString); let pr = null; res.body.forEach((result) => { if ((!prTitle || result.title === prTitle) && result.source_branch === branchName) { pr = result; // GitHub uses number, GitLab uses iid pr.number = pr.id; pr.body = pr.description; pr.displayNumber = `Merge Request #${pr.iid}`; if (pr.state !== 'opened') { pr.isClosed = true; } } }); return pr; } // Pull Request async function checkForClosedPr(branchName, prTitle) { const pr = await findPr(branchName, prTitle, 'closed'); if (pr) { return true; } return false; } async function createPr(branchName, title, body) { logger.debug(`Creating Merge Request: ${title}`); const res = await glGot.post(`projects/${config.repoName}/merge_requests`, { body: { source_branch: branchName, target_branch: config.defaultBranch, title, description: body, }, }); const pr = res.body; pr.number = pr.id; pr.displayNumber = `Merge Request #${pr.iid}`; return pr; } async function getPr(prNo) { logger.debug(`getPr(${prNo})`); const url = `projects/${config.repoName}/merge_requests/${prNo}`; const pr = (await glGot(url)).body; // Harmonize fields with GitHub pr.number = pr.id; pr.displayNumber = `Merge Request #${pr.iid}`; pr.body = pr.description; if (pr.state === 'closed' || pr.state === 'merged') { logger.debug('pr is closed'); pr.isClosed = true; } if (pr.merge_status === 'cannot_be_merged') { logger.debug('pr cannot be merged'); pr.isUnmergeable = true; } // We can't rebase through GitLab API pr.canRebase = false; return pr; } async function updatePr(prNo, title, body) { await glGot.put(`projects/${config.repoName}/merge_requests/${prNo}`, { body: { title, description: body, }, }); } // Generic File operations async function getFile(filePath, branchName = config.defaultBranch) { const res = await glGot(`projects/${config.repoName}/repository/files?file_path=${filePath}&ref=${branchName}`); return res.body.content; } async function getFileContent(filePath, branchName) { try { const file = await getFile(filePath, branchName); return new Buffer(file, 'base64').toString(); } catch (error) { if (error.statusCode === 404) { // If file not found, then return null JSON return null; } // Propagate if it's any other error throw error; } } async function getFileJson(filePath, branchName) { try { const fileContent = await getFileContent(filePath, branchName); return JSON.parse(fileContent); } catch (error) { if (error.statusCode === 404) { // If file not found, then return null JSON return null; } // Propagate if it's any other error throw error; } } async function createFile(branchName, filePath, fileContents, message) { await glGot.post(`projects/${config.repoName}/repository/files`, { body: { file_path: filePath, branch_name: branchName, commit_message: message, encoding: 'base64', content: new Buffer(fileContents).toString('base64'), }, }); } async function updateFile(branchName, filePath, fileContents, message) { await glGot.put(`projects/${config.repoName}/repository/files`, { body: { file_path: filePath, branch_name: branchName, commit_message: message, encoding: 'base64', content: new Buffer(fileContents).toString('base64'), }, }); } // Add a new commit, create branch if not existing async function commitFilesToBranch( branchName, files, message, parentBranch = config.defaultBranch) { logger.debug(`commitFilesToBranch('${branchName}', files, message, '${parentBranch})'`); if (branchName !== parentBranch) { const isBranchExisting = await branchExists(branchName); if (isBranchExisting) { logger.debug(`Branch ${branchName} already exists`); } else { logger.debug(`Creating branch ${branchName}`); await createBranch(branchName); } } for (const file of files) { const existingFile = await getFileContent(file.name, branchName); if (existingFile) { logger.debug(`${file.name} exists - updating it`); await updateFile(branchName, file.name, file.contents, message); } else { logger.debug(`Creating file ${file.name}`); await createFile(branchName, file.name, file.contents, message); } } } // Internal branch operations // Creates a new branch with provided commit async function createBranch(branchName, ref = config.defaultBranch) { await glGot.post(`projects/${config.repoName}/repository/branches`, { body: { branch_name: branchName, ref, }, }); }