mirror of
https://github.com/renovatebot/renovate.git
synced 2025-01-12 06:56:24 +00:00
feat: GitFs for GitLab (#2692)
Since Gitlab does not support using tokens to write to repo, `commitFilesToBranch` will always use the API. This could be changed once GitFS over SSH is implemented. Closes #2549
This commit is contained in:
parent
be65176dc4
commit
975ee2b79b
4 changed files with 468 additions and 253 deletions
|
@ -1,16 +1,21 @@
|
||||||
|
const URL = require('url');
|
||||||
const is = require('@sindresorhus/is');
|
const is = require('@sindresorhus/is');
|
||||||
const addrs = require('email-addresses');
|
const addrs = require('email-addresses');
|
||||||
|
|
||||||
const get = require('./gl-got-wrapper');
|
const get = require('./gl-got-wrapper');
|
||||||
const hostRules = require('../../util/host-rules');
|
const hostRules = require('../../util/host-rules');
|
||||||
|
const GitStorage = require('../git/storage');
|
||||||
|
const Storage = require('./storage');
|
||||||
|
|
||||||
let config = {};
|
let config = {
|
||||||
|
storage: new Storage(),
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getRepos,
|
getRepos,
|
||||||
cleanRepo,
|
cleanRepo,
|
||||||
initRepo,
|
initRepo,
|
||||||
getRepoStatus: () => ({}),
|
getRepoStatus,
|
||||||
getRepoForceRebase,
|
getRepoForceRebase,
|
||||||
setBaseBranch,
|
setBaseBranch,
|
||||||
// Search
|
// Search
|
||||||
|
@ -79,13 +84,25 @@ function urlEscape(str) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function cleanRepo() {
|
function cleanRepo() {
|
||||||
|
// istanbul ignore if
|
||||||
|
if (config.storage) {
|
||||||
|
config.storage.cleanRepo();
|
||||||
|
}
|
||||||
// In theory most of this isn't necessary. In practice..
|
// In theory most of this isn't necessary. In practice..
|
||||||
get.reset();
|
get.reset();
|
||||||
config = {};
|
config = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize GitLab by getting base branch
|
// Initialize GitLab by getting base branch
|
||||||
async function initRepo({ repository, token, oauth, endpoint, gitAuthor }) {
|
async function initRepo({
|
||||||
|
repository,
|
||||||
|
token,
|
||||||
|
oauth,
|
||||||
|
endpoint,
|
||||||
|
gitAuthor,
|
||||||
|
gitFs,
|
||||||
|
localDir,
|
||||||
|
}) {
|
||||||
const opts = hostRules.find(
|
const opts = hostRules.find(
|
||||||
{ platform: 'gitlab' },
|
{ platform: 'gitlab' },
|
||||||
{ token, endpoint, oauth }
|
{ token, endpoint, oauth }
|
||||||
|
@ -97,6 +114,8 @@ async function initRepo({ repository, token, oauth, endpoint, gitAuthor }) {
|
||||||
config = {};
|
config = {};
|
||||||
get.reset();
|
get.reset();
|
||||||
config.repository = urlEscape(repository);
|
config.repository = urlEscape(repository);
|
||||||
|
config.gitFs = gitFs;
|
||||||
|
config.localDir = localDir;
|
||||||
if (gitAuthor) {
|
if (gitAuthor) {
|
||||||
try {
|
try {
|
||||||
config.gitAuthor = addrs.parseOneAddress(gitAuthor);
|
config.gitAuthor = addrs.parseOneAddress(gitAuthor);
|
||||||
|
@ -123,7 +142,27 @@ async function initRepo({ repository, token, oauth, endpoint, gitAuthor }) {
|
||||||
// Discover our user email
|
// Discover our user email
|
||||||
config.email = (await get(`user`)).body.email;
|
config.email = (await get(`user`)).body.email;
|
||||||
delete config.prList;
|
delete config.prList;
|
||||||
delete config.fileList;
|
// istanbul ignore if
|
||||||
|
if (config.gitFs) {
|
||||||
|
logger.debug('Enabling Git FS');
|
||||||
|
let { protocol, host } = URL.parse(opts.endpoint);
|
||||||
|
host = host || 'gitlab.com';
|
||||||
|
protocol = protocol || 'https:';
|
||||||
|
const url = URL.format({
|
||||||
|
protocol,
|
||||||
|
auth: 'oauth2:' + (config.forkToken || opts.token),
|
||||||
|
hostname: host,
|
||||||
|
pathname: repository + '.git',
|
||||||
|
});
|
||||||
|
config.storage = new GitStorage();
|
||||||
|
await config.storage.initRepo({
|
||||||
|
...config,
|
||||||
|
url,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
config.storage = new Storage();
|
||||||
|
await config.storage.initRepo(config);
|
||||||
|
}
|
||||||
await Promise.all([getPrList(), getFileList()]);
|
await Promise.all([getPrList(), getFileList()]);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.debug('Caught initRepo error');
|
logger.debug('Caught initRepo error');
|
||||||
|
@ -144,89 +183,22 @@ async function setBaseBranch(branchName) {
|
||||||
if (branchName) {
|
if (branchName) {
|
||||||
logger.debug(`Setting baseBranch to ${branchName}`);
|
logger.debug(`Setting baseBranch to ${branchName}`);
|
||||||
config.baseBranch = branchName;
|
config.baseBranch = branchName;
|
||||||
delete config.fileList;
|
await config.storage.setBaseBranch(branchName);
|
||||||
await getFileList(branchName);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search
|
// Search
|
||||||
|
|
||||||
// Get full file list
|
// Get full file list
|
||||||
async function getFileList(branchName = config.baseBranch) {
|
function getFileList(branchName = config.baseBranch) {
|
||||||
if (config.fileList) {
|
return config.storage.getFileList(branchName);
|
||||||
return config.fileList;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
let url = `projects/${
|
|
||||||
config.repository
|
|
||||||
}/repository/tree?ref=${branchName}&per_page=100`;
|
|
||||||
if (!(process.env.RENOVATE_DISABLE_FILE_RECURSION === 'true')) {
|
|
||||||
url += '&recursive=true';
|
|
||||||
}
|
|
||||||
const res = await get(url, { paginate: true });
|
|
||||||
config.fileList = res.body
|
|
||||||
.filter(item => item.type === 'blob' && item.mode !== '120000')
|
|
||||||
.map(item => item.path)
|
|
||||||
.sort();
|
|
||||||
logger.debug(`Retrieved fileList with length ${config.fileList.length}`);
|
|
||||||
} catch (err) {
|
|
||||||
logger.info('Error retrieving git tree - no files detected');
|
|
||||||
config.fileList = [];
|
|
||||||
}
|
|
||||||
return config.fileList;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Branch
|
// Branch
|
||||||
|
|
||||||
// Returns true if branch exists, otherwise false
|
// Returns true if branch exists, otherwise false
|
||||||
async function branchExists(branchName) {
|
function branchExists(branchName) {
|
||||||
logger.debug(`Checking if branch exists: ${branchName}`);
|
return config.storage.branchExists(branchName);
|
||||||
try {
|
|
||||||
const url = `projects/${config.repository}/repository/branches/${urlEscape(
|
|
||||||
branchName
|
|
||||||
)}`;
|
|
||||||
const res = await get(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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getAllRenovateBranches(branchPrefix) {
|
|
||||||
logger.debug(`getAllRenovateBranches(${branchPrefix})`);
|
|
||||||
const allBranches = await get(
|
|
||||||
`projects/${config.repository}/repository/branches`
|
|
||||||
);
|
|
||||||
return allBranches.body.reduce((arr, branch) => {
|
|
||||||
if (branch.name.startsWith(branchPrefix)) {
|
|
||||||
arr.push(branch.name);
|
|
||||||
}
|
|
||||||
return arr;
|
|
||||||
}, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function isBranchStale(branchName) {
|
|
||||||
logger.debug(`isBranchStale(${branchName})`);
|
|
||||||
const branchDetails = await getBranchDetails(branchName);
|
|
||||||
logger.trace({ branchDetails }, 'branchDetails');
|
|
||||||
const parentSha = branchDetails.body.commit.parent_ids[0];
|
|
||||||
logger.debug(`parentSha=${parentSha}`);
|
|
||||||
const baseCommitSHA = await getBaseCommitSHA();
|
|
||||||
logger.debug(`baseCommitSHA=${baseCommitSHA}`);
|
|
||||||
// Return true if the SHAs don't match
|
|
||||||
return parentSha !== baseCommitSHA;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the Pull Request for a branch. Null if not exists.
|
// Returns the Pull Request for a branch. Null if not exists.
|
||||||
|
@ -252,6 +224,79 @@ async function getBranchPr(branchName) {
|
||||||
return getPr(pr.iid);
|
return getPr(pr.iid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getAllRenovateBranches(branchPrefix) {
|
||||||
|
return config.storage.getAllRenovateBranches(branchPrefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isBranchStale(branchName) {
|
||||||
|
return config.storage.isBranchStale(branchName);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function commitFilesToBranch(
|
||||||
|
branchName,
|
||||||
|
files,
|
||||||
|
message,
|
||||||
|
parentBranch = config.baseBranch
|
||||||
|
) {
|
||||||
|
// GitLab does not support push with GitFs
|
||||||
|
let storage = config.storage;
|
||||||
|
// istanbul ignore if
|
||||||
|
if (config.gitFs) {
|
||||||
|
storage = new Storage();
|
||||||
|
storage.initRepo(config);
|
||||||
|
}
|
||||||
|
const res = await storage.commitFilesToBranch(
|
||||||
|
branchName,
|
||||||
|
files,
|
||||||
|
message,
|
||||||
|
parentBranch
|
||||||
|
);
|
||||||
|
// Reopen PR if it previousluy existed and was closed by GitLab when we deleted branch
|
||||||
|
const pr = await getBranchPr(branchName);
|
||||||
|
// istanbul ignore if
|
||||||
|
if (pr) {
|
||||||
|
logger.debug('Reopening PR');
|
||||||
|
await reopenPr(pr.number);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFile(filePath, branchName) {
|
||||||
|
return config.storage.getFile(filePath, branchName);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteBranch(branchName, closePr = false) {
|
||||||
|
if (closePr) {
|
||||||
|
logger.debug('Closing PR');
|
||||||
|
const pr = await getBranchPr(branchName);
|
||||||
|
// istanbul ignore if
|
||||||
|
if (pr) {
|
||||||
|
await get.put(
|
||||||
|
`projects/${config.repository}/merge_requests/${pr.number}`,
|
||||||
|
{
|
||||||
|
body: {
|
||||||
|
state_event: 'close',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return config.storage.deleteBranch(branchName);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeBranch(branchName) {
|
||||||
|
return config.storage.mergeBranch(branchName);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBranchLastCommitTime(branchName) {
|
||||||
|
return config.storage.getBranchLastCommitTime(branchName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// istanbul ignore next
|
||||||
|
function getRepoStatus() {
|
||||||
|
return config.storage.getRepoStatus();
|
||||||
|
}
|
||||||
|
|
||||||
// Returns the combined status for a branch.
|
// Returns the combined status for a branch.
|
||||||
async function getBranchStatus(branchName, requiredStatusChecks) {
|
async function getBranchStatus(branchName, requiredStatusChecks) {
|
||||||
logger.debug(`getBranchStatus(${branchName})`);
|
logger.debug(`getBranchStatus(${branchName})`);
|
||||||
|
@ -264,17 +309,13 @@ async function getBranchStatus(branchName, requiredStatusChecks) {
|
||||||
logger.warn({ requiredStatusChecks }, `Unsupported requiredStatusChecks`);
|
logger.warn({ requiredStatusChecks }, `Unsupported requiredStatusChecks`);
|
||||||
return 'failed';
|
return 'failed';
|
||||||
}
|
}
|
||||||
// First, get the branch to find the commit SHA
|
// First, get the branch commit SHA
|
||||||
let url = `projects/${config.repository}/repository/branches/${urlEscape(
|
const branchSha = await config.storage.getBranchCommit(branchName);
|
||||||
branchName
|
|
||||||
)}`;
|
|
||||||
let res = await get(url);
|
|
||||||
const branchSha = res.body.commit.id;
|
|
||||||
// Now, check the statuses for that commit
|
// Now, check the statuses for that commit
|
||||||
url = `projects/${
|
const url = `projects/${
|
||||||
config.repository
|
config.repository
|
||||||
}/repository/commits/${branchSha}/statuses`;
|
}/repository/commits/${branchSha}/statuses`;
|
||||||
res = await get(url);
|
const res = await get(url);
|
||||||
logger.debug(`Got res with ${res.body.length} results`);
|
logger.debug(`Got res with ${res.body.length} results`);
|
||||||
if (res.body.length === 0) {
|
if (res.body.length === 0) {
|
||||||
// Return 'pending' if we have no status checks
|
// Return 'pending' if we have no status checks
|
||||||
|
@ -298,17 +339,13 @@ async function getBranchStatus(branchName, requiredStatusChecks) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getBranchStatusCheck(branchName, context) {
|
async function getBranchStatusCheck(branchName, context) {
|
||||||
// First, get the branch to find the commit SHA
|
// First, get the branch commit SHA
|
||||||
let url = `projects/${config.repository}/repository/branches/${urlEscape(
|
const branchSha = await config.storage.getBranchCommit(branchName);
|
||||||
branchName
|
|
||||||
)}`;
|
|
||||||
let res = await get(url);
|
|
||||||
const branchSha = res.body.commit.id;
|
|
||||||
// Now, check the statuses for that commit
|
// Now, check the statuses for that commit
|
||||||
url = `projects/${
|
const url = `projects/${
|
||||||
config.repository
|
config.repository
|
||||||
}/repository/commits/${branchSha}/statuses`;
|
}/repository/commits/${branchSha}/statuses`;
|
||||||
res = await get(url);
|
const res = await get(url);
|
||||||
logger.debug(`Got res with ${res.body.length} results`);
|
logger.debug(`Got res with ${res.body.length} results`);
|
||||||
for (const check of res.body) {
|
for (const check of res.body) {
|
||||||
if (check.name === context) {
|
if (check.name === context) {
|
||||||
|
@ -325,14 +362,10 @@ async function setBranchStatus(
|
||||||
state,
|
state,
|
||||||
targetUrl
|
targetUrl
|
||||||
) {
|
) {
|
||||||
// First, get the branch to find the commit SHA
|
// First, get the branch commit SHA
|
||||||
let url = `projects/${config.repository}/repository/branches/${urlEscape(
|
const branchSha = await config.storage.getBranchCommit(branchName);
|
||||||
branchName
|
|
||||||
)}`;
|
|
||||||
const res = await get(url);
|
|
||||||
const branchSha = res.body.commit.id;
|
|
||||||
// Now, check the statuses for that commit
|
// Now, check the statuses for that commit
|
||||||
url = `projects/${config.repository}/statuses/${branchSha}`;
|
const url = `projects/${config.repository}/statuses/${branchSha}`;
|
||||||
const options = {
|
const options = {
|
||||||
state,
|
state,
|
||||||
description,
|
description,
|
||||||
|
@ -349,60 +382,6 @@ async function setBranchStatus(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteBranch(branchName, closePr = false) {
|
|
||||||
if (closePr) {
|
|
||||||
logger.debug('Closing PR');
|
|
||||||
const pr = await getBranchPr(branchName);
|
|
||||||
// istanbul ignore if
|
|
||||||
if (pr) {
|
|
||||||
await get.put(
|
|
||||||
`projects/${config.repository}/merge_requests/${pr.number}`,
|
|
||||||
{
|
|
||||||
body: {
|
|
||||||
state_event: 'close',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await get.delete(
|
|
||||||
`projects/${config.repository}/repository/branches/${urlEscape(branchName)}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function mergeBranch(branchName) {
|
|
||||||
logger.debug(`mergeBranch(${branchName}`);
|
|
||||||
const branchURI = encodeURIComponent(branchName);
|
|
||||||
try {
|
|
||||||
await get.post(
|
|
||||||
`projects/${
|
|
||||||
config.repository
|
|
||||||
}/repository/commits/${branchURI}/cherry_pick?branch=${config.baseBranch}`
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
logger.info({ err }, `Error pushing branch merge for ${branchName}`);
|
|
||||||
throw new Error('Branch automerge failed');
|
|
||||||
}
|
|
||||||
// Update base commit
|
|
||||||
config.baseCommitSHA = null;
|
|
||||||
// Delete branch
|
|
||||||
await deleteBranch(branchName);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getBranchLastCommitTime(branchName) {
|
|
||||||
try {
|
|
||||||
const res = await get(
|
|
||||||
`projects/${config.repository}/repository/commits?ref_name=${urlEscape(
|
|
||||||
branchName
|
|
||||||
)}`
|
|
||||||
);
|
|
||||||
return new Date(res.body[0].committed_date);
|
|
||||||
} catch (err) {
|
|
||||||
logger.error({ err }, `getBranchLastCommitTime error`);
|
|
||||||
return new Date();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Issue
|
// Issue
|
||||||
|
|
||||||
async function getIssueList() {
|
async function getIssueList() {
|
||||||
|
@ -780,106 +759,8 @@ function getPrBody(input) {
|
||||||
.replace(/\]\(\.\.\/pull\//g, '](../merge_requests/');
|
.replace(/\]\(\.\.\/pull\//g, '](../merge_requests/');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generic File operations
|
function getCommitMessages() {
|
||||||
|
return config.storage.getCommitMessages();
|
||||||
async function getFile(filePath, branchName) {
|
|
||||||
logger.debug(`getFile(filePath=${filePath}, branchName=${branchName})`);
|
|
||||||
if (!branchName || branchName === config.baseBranch) {
|
|
||||||
if (config.fileList && !config.fileList.includes(filePath)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const url = `projects/${config.repository}/repository/files/${urlEscape(
|
|
||||||
filePath
|
|
||||||
)}?ref=${branchName || config.baseBranch}`;
|
|
||||||
const res = await get(url);
|
|
||||||
return Buffer.from(res.body.content, '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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a new commit, create branch if not existing
|
|
||||||
async function commitFilesToBranch(
|
|
||||||
branchName,
|
|
||||||
files,
|
|
||||||
message,
|
|
||||||
parentBranch = config.baseBranch
|
|
||||||
) {
|
|
||||||
logger.debug(
|
|
||||||
`commitFilesToBranch('${branchName}', files, message, '${parentBranch})'`
|
|
||||||
);
|
|
||||||
const opts = {
|
|
||||||
body: {
|
|
||||||
branch: branchName,
|
|
||||||
commit_message: message,
|
|
||||||
start_branch: parentBranch,
|
|
||||||
actions: [],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
// istanbul ignore if
|
|
||||||
if (config.gitAuthor) {
|
|
||||||
opts.body.author_name = config.gitAuthor.name;
|
|
||||||
opts.body.author_email = config.gitAuthor.address;
|
|
||||||
}
|
|
||||||
for (const file of files) {
|
|
||||||
const action = {
|
|
||||||
file_path: file.name,
|
|
||||||
content: Buffer.from(file.contents).toString('base64'),
|
|
||||||
encoding: 'base64',
|
|
||||||
};
|
|
||||||
action.action = (await getFile(file.name)) ? 'update' : 'create';
|
|
||||||
opts.body.actions.push(action);
|
|
||||||
}
|
|
||||||
let res = 'created';
|
|
||||||
try {
|
|
||||||
if (await branchExists(branchName)) {
|
|
||||||
logger.debug('Deleting existing branch');
|
|
||||||
await deleteBranch(branchName);
|
|
||||||
res = 'updated';
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
// istanbul ignore next
|
|
||||||
logger.info(`Ignoring branch deletion failure`);
|
|
||||||
}
|
|
||||||
logger.debug('Adding commits');
|
|
||||||
await get.post(`projects/${config.repository}/repository/commits`, opts);
|
|
||||||
// Reopen PR if it previousluy existed and was closed by GitLab when we deleted branch
|
|
||||||
const pr = await getBranchPr(branchName);
|
|
||||||
// istanbul ignore if
|
|
||||||
if (pr) {
|
|
||||||
logger.debug('Reopening PR');
|
|
||||||
await reopenPr(pr.number);
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET /projects/:id/repository/commits
|
|
||||||
async function getCommitMessages() {
|
|
||||||
logger.debug('getCommitMessages');
|
|
||||||
const res = await get(`projects/${config.repository}/repository/commits`);
|
|
||||||
return res.body.map(commit => commit.title);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getBranchDetails(branchName) {
|
|
||||||
const url = `/projects/${config.repository}/repository/branches/${urlEscape(
|
|
||||||
branchName
|
|
||||||
)}`;
|
|
||||||
return get(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getBaseCommitSHA() {
|
|
||||||
if (!config.baseCommitSHA) {
|
|
||||||
const branchDetails = await getBranchDetails(config.baseBranch);
|
|
||||||
config.baseCommitSHA = branchDetails.body.commit.id;
|
|
||||||
}
|
|
||||||
return config.baseCommitSHA;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getVulnerabilityAlerts() {
|
function getVulnerabilityAlerts() {
|
||||||
|
|
303
lib/platform/gitlab/storage.js
Normal file
303
lib/platform/gitlab/storage.js
Normal file
|
@ -0,0 +1,303 @@
|
||||||
|
const get = require('./gl-got-wrapper');
|
||||||
|
|
||||||
|
function urlEscape(str) {
|
||||||
|
return str ? str.replace(/\//g, '%2F') : str;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Storage {
|
||||||
|
constructor() {
|
||||||
|
// config
|
||||||
|
let config = {};
|
||||||
|
// cache
|
||||||
|
let baseCommitSHA = null;
|
||||||
|
let branchFiles = {};
|
||||||
|
|
||||||
|
Object.assign(this, {
|
||||||
|
initRepo,
|
||||||
|
cleanRepo,
|
||||||
|
getRepoStatus: () => ({}),
|
||||||
|
branchExists,
|
||||||
|
commitFilesToBranch,
|
||||||
|
createBranch,
|
||||||
|
deleteBranch,
|
||||||
|
getAllRenovateBranches,
|
||||||
|
getBranchCommit,
|
||||||
|
getBranchLastCommitTime,
|
||||||
|
getCommitMessages,
|
||||||
|
getFile,
|
||||||
|
getFileList,
|
||||||
|
isBranchStale,
|
||||||
|
mergeBranch,
|
||||||
|
setBaseBranch,
|
||||||
|
});
|
||||||
|
|
||||||
|
function initRepo(args) {
|
||||||
|
cleanRepo();
|
||||||
|
config = { ...args };
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanRepo() {
|
||||||
|
baseCommitSHA = null;
|
||||||
|
branchFiles = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Branch
|
||||||
|
|
||||||
|
// Returns true if branch exists, otherwise false
|
||||||
|
async function branchExists(branchName) {
|
||||||
|
logger.debug(`Checking if branch exists: ${branchName}`);
|
||||||
|
try {
|
||||||
|
const url = `projects/${
|
||||||
|
config.repository
|
||||||
|
}/repository/branches/${urlEscape(branchName)}`;
|
||||||
|
const res = await get(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAllRenovateBranches(branchPrefix) {
|
||||||
|
logger.debug(`getAllRenovateBranches(${branchPrefix})`);
|
||||||
|
const allBranches = await get(
|
||||||
|
`projects/${config.repository}/repository/branches`
|
||||||
|
);
|
||||||
|
return allBranches.body.reduce((arr, branch) => {
|
||||||
|
if (branch.name.startsWith(branchPrefix)) {
|
||||||
|
arr.push(branch.name);
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function isBranchStale(branchName) {
|
||||||
|
logger.debug(`isBranchStale(${branchName})`);
|
||||||
|
const branchDetails = await getBranchDetails(branchName);
|
||||||
|
logger.trace({ branchDetails }, 'branchDetails');
|
||||||
|
const parentSha = branchDetails.body.commit.parent_ids[0];
|
||||||
|
logger.debug(`parentSha=${parentSha}`);
|
||||||
|
const baseSHA = await getBaseCommitSHA();
|
||||||
|
logger.debug(`baseSHA=${baseSHA}`);
|
||||||
|
// Return true if the SHAs don't match
|
||||||
|
return parentSha !== baseSHA;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a new commit, create branch if not existing
|
||||||
|
async function commitFilesToBranch(
|
||||||
|
branchName,
|
||||||
|
files,
|
||||||
|
message,
|
||||||
|
parentBranch = config.baseBranch
|
||||||
|
) {
|
||||||
|
logger.debug(
|
||||||
|
`commitFilesToBranch('${branchName}', files, message, '${parentBranch})'`
|
||||||
|
);
|
||||||
|
const opts = {
|
||||||
|
body: {
|
||||||
|
branch: branchName,
|
||||||
|
commit_message: message,
|
||||||
|
start_branch: parentBranch,
|
||||||
|
actions: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// istanbul ignore if
|
||||||
|
if (config.gitAuthor) {
|
||||||
|
opts.body.author_name = config.gitAuthor.name;
|
||||||
|
opts.body.author_email = config.gitAuthor.address;
|
||||||
|
}
|
||||||
|
for (const file of files) {
|
||||||
|
const action = {
|
||||||
|
file_path: file.name,
|
||||||
|
content: Buffer.from(file.contents).toString('base64'),
|
||||||
|
encoding: 'base64',
|
||||||
|
};
|
||||||
|
action.action = (await getFile(file.name)) ? 'update' : 'create';
|
||||||
|
opts.body.actions.push(action);
|
||||||
|
}
|
||||||
|
let res = 'created';
|
||||||
|
try {
|
||||||
|
if (await branchExists(branchName)) {
|
||||||
|
logger.debug('Deleting existing branch');
|
||||||
|
await deleteBranch(branchName);
|
||||||
|
res = 'updated';
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// istanbul ignore next
|
||||||
|
logger.info(`Ignoring branch deletion failure`);
|
||||||
|
}
|
||||||
|
logger.debug('Adding commits');
|
||||||
|
await get.post(`projects/${config.repository}/repository/commits`, opts);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createBranch(branchName, sha) {
|
||||||
|
await get.post(
|
||||||
|
`projects/${config.repository}/repository/branches?branch=${urlEscape(
|
||||||
|
branchName
|
||||||
|
)}&ref=${sha}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteBranch(branchName) {
|
||||||
|
await get.delete(
|
||||||
|
`projects/${config.repository}/repository/branches/${urlEscape(
|
||||||
|
branchName
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search
|
||||||
|
|
||||||
|
// Get full file list
|
||||||
|
async function getFileList(branchName = config.baseBranch) {
|
||||||
|
if (branchFiles[branchName]) {
|
||||||
|
return branchFiles[branchName];
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
let url = `projects/${
|
||||||
|
config.repository
|
||||||
|
}/repository/tree?ref=${branchName}&per_page=100`;
|
||||||
|
if (!(process.env.RENOVATE_DISABLE_FILE_RECURSION === 'true')) {
|
||||||
|
url += '&recursive=true';
|
||||||
|
}
|
||||||
|
const res = await get(url, { paginate: true });
|
||||||
|
branchFiles[branchName] = res.body
|
||||||
|
.filter(item => item.type === 'blob' && item.mode !== '120000')
|
||||||
|
.map(item => item.path)
|
||||||
|
.sort();
|
||||||
|
logger.debug(
|
||||||
|
`Retrieved fileList with length ${branchFiles[branchName].length}`
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
logger.info('Error retrieving git tree - no files detected');
|
||||||
|
branchFiles[branchName] = [];
|
||||||
|
}
|
||||||
|
return branchFiles[branchName];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generic File operations
|
||||||
|
|
||||||
|
async function getFile(filePath, branchName) {
|
||||||
|
logger.debug(`getFile(filePath=${filePath}, branchName=${branchName})`);
|
||||||
|
if (!branchName || branchName === config.baseBranch) {
|
||||||
|
if (
|
||||||
|
branchFiles[branchName] &&
|
||||||
|
!branchFiles[branchName].includes(filePath)
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const url = `projects/${config.repository}/repository/files/${urlEscape(
|
||||||
|
filePath
|
||||||
|
)}?ref=${branchName || config.baseBranch}`;
|
||||||
|
const res = await get(url);
|
||||||
|
return Buffer.from(res.body.content, '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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /projects/:id/repository/commits
|
||||||
|
async function getCommitMessages() {
|
||||||
|
logger.debug('getCommitMessages');
|
||||||
|
const res = await get(`projects/${config.repository}/repository/commits`);
|
||||||
|
return res.body.map(commit => commit.title);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBranchDetails(branchName) {
|
||||||
|
const url = `/projects/${
|
||||||
|
config.repository
|
||||||
|
}/repository/branches/${urlEscape(branchName)}`;
|
||||||
|
return get(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getBaseCommitSHA() {
|
||||||
|
if (!baseCommitSHA) {
|
||||||
|
const branchDetails = await getBranchDetails(config.baseBranch);
|
||||||
|
baseCommitSHA = branchDetails.body.commit.id;
|
||||||
|
}
|
||||||
|
return baseCommitSHA;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function mergeBranch(branchName) {
|
||||||
|
logger.debug(`mergeBranch(${branchName}`);
|
||||||
|
const branchURI = encodeURIComponent(branchName);
|
||||||
|
try {
|
||||||
|
await get.post(
|
||||||
|
`projects/${
|
||||||
|
config.repository
|
||||||
|
}/repository/commits/${branchURI}/cherry_pick?branch=${
|
||||||
|
config.baseBranch
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
logger.info({ err }, `Error pushing branch merge for ${branchName}`);
|
||||||
|
throw new Error('Branch automerge failed');
|
||||||
|
}
|
||||||
|
// Update base commit
|
||||||
|
baseCommitSHA = null;
|
||||||
|
// Delete branch
|
||||||
|
await deleteBranch(branchName);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getBranchCommit(branchName) {
|
||||||
|
const branchUrl = `projects/${
|
||||||
|
config.repository
|
||||||
|
}/repository/branches/${urlEscape(branchName)}`;
|
||||||
|
try {
|
||||||
|
const branch = (await get(branchUrl)).body;
|
||||||
|
if (branch && branch.commit) {
|
||||||
|
return branch.commit.id;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// istanbul ignore next
|
||||||
|
logger.error({ err }, `getBranchCommit error`);
|
||||||
|
}
|
||||||
|
// istanbul ignore next
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getBranchLastCommitTime(branchName) {
|
||||||
|
try {
|
||||||
|
const res = await get(
|
||||||
|
`projects/${
|
||||||
|
config.repository
|
||||||
|
}/repository/commits?ref_name=${urlEscape(branchName)}`
|
||||||
|
);
|
||||||
|
return new Date(res.body[0].committed_date);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error({ err }, `getBranchLastCommitTime error`);
|
||||||
|
return new Date();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setBaseBranch(branchName) {
|
||||||
|
if (branchName) {
|
||||||
|
logger.debug(`Setting baseBranch to ${branchName}`);
|
||||||
|
config.baseBranch = branchName;
|
||||||
|
branchFiles = {};
|
||||||
|
await getFileList(branchName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Storage;
|
9
test/platform/gitlab/__snapshots__/storage.spec.js.snap
Normal file
9
test/platform/gitlab/__snapshots__/storage.spec.js.snap
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`platform/gitlab/storage createBranch() creates the branch 1`] = `
|
||||||
|
Array [
|
||||||
|
Array [
|
||||||
|
"projects/undefined/repository/branches?branch=renovate%2Fsome-branch&ref=commit",
|
||||||
|
],
|
||||||
|
]
|
||||||
|
`;
|
22
test/platform/gitlab/storage.spec.js
Normal file
22
test/platform/gitlab/storage.spec.js
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
describe('platform/gitlab/storage', () => {
|
||||||
|
jest.mock('../../../lib/platform/gitlab/gl-got-wrapper');
|
||||||
|
const GitlabStorage = require('../../../lib/platform/gitlab/storage');
|
||||||
|
const GitStorage = require('../../../lib/platform/git/storage');
|
||||||
|
const get = require('../../../lib/platform/gitlab/gl-got-wrapper');
|
||||||
|
it('has same API for git storage', () => {
|
||||||
|
const gitlabMethods = Object.keys(new GitlabStorage()).sort();
|
||||||
|
const gitMethods = Object.keys(new GitStorage()).sort();
|
||||||
|
expect(gitlabMethods).toMatchObject(gitMethods);
|
||||||
|
});
|
||||||
|
it('getRepoStatus exists', async () => {
|
||||||
|
expect((await new GitlabStorage()).getRepoStatus()).toEqual({});
|
||||||
|
});
|
||||||
|
describe('createBranch()', () => {
|
||||||
|
it('creates the branch', async () => {
|
||||||
|
get.post.mockReturnValue({});
|
||||||
|
const storage = new GitlabStorage();
|
||||||
|
await storage.createBranch('renovate/some-branch', 'commit');
|
||||||
|
expect(get.post.mock.calls).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in a new issue