2019-07-10 11:18:51 +00:00
|
|
|
import URL, { URLSearchParams } from 'url';
|
2019-05-29 12:00:56 +00:00
|
|
|
import is from '@sindresorhus/is';
|
|
|
|
|
2019-07-10 08:41:12 +00:00
|
|
|
import { api } from './gl-got-wrapper';
|
2019-05-29 12:00:56 +00:00
|
|
|
import * as hostRules from '../../util/host-rules';
|
2019-11-26 15:13:07 +00:00
|
|
|
import GitStorage, { StatusResult } from '../git/storage';
|
2019-10-05 08:00:32 +00:00
|
|
|
import {
|
|
|
|
PlatformConfig,
|
|
|
|
RepoParams,
|
|
|
|
RepoConfig,
|
|
|
|
PlatformPrOptions,
|
2019-11-26 15:13:07 +00:00
|
|
|
GotResponse,
|
|
|
|
Pr,
|
|
|
|
Issue,
|
|
|
|
VulnerabilityAlert,
|
2019-10-05 08:00:32 +00:00
|
|
|
} from '../common';
|
2019-07-14 05:45:39 +00:00
|
|
|
import { configFileNames } from '../../config/app-strings';
|
2019-07-15 09:04:05 +00:00
|
|
|
import { logger } from '../../logger';
|
2019-09-12 10:48:31 +00:00
|
|
|
import { sanitize } from '../../util/sanitize';
|
2019-09-25 10:42:11 +00:00
|
|
|
import { smartTruncate } from '../utils/pr-body';
|
2019-11-26 15:13:07 +00:00
|
|
|
import { RenovateConfig } from '../../config';
|
2019-05-29 12:00:56 +00:00
|
|
|
|
2019-07-14 05:45:39 +00:00
|
|
|
const defaultConfigFile = configFileNames[0];
|
2019-05-29 12:00:56 +00:00
|
|
|
let config: {
|
|
|
|
storage: GitStorage;
|
2019-11-29 07:56:10 +00:00
|
|
|
gitPrivateKey?: string;
|
2019-05-29 12:00:56 +00:00
|
|
|
repository: string;
|
|
|
|
localDir: string;
|
|
|
|
defaultBranch: string;
|
|
|
|
baseBranch: string;
|
|
|
|
email: string;
|
|
|
|
prList: any[];
|
|
|
|
issueList: any[];
|
2019-07-14 05:45:39 +00:00
|
|
|
optimizeForDisabled: boolean;
|
2019-05-29 12:00:56 +00:00
|
|
|
} = {} as any;
|
2018-07-27 17:49:56 +00:00
|
|
|
|
2019-05-20 08:59:30 +00:00
|
|
|
const defaults = {
|
2019-05-21 11:20:09 +00:00
|
|
|
hostType: 'gitlab',
|
2019-05-20 08:59:30 +00:00
|
|
|
endpoint: 'https://gitlab.com/api/v4/',
|
|
|
|
};
|
2017-02-11 07:14:19 +00:00
|
|
|
|
2019-07-10 11:18:51 +00:00
|
|
|
let authorId: number;
|
|
|
|
|
2019-06-13 05:25:39 +00:00
|
|
|
export async function initPlatform({
|
2019-05-29 12:00:56 +00:00
|
|
|
endpoint,
|
|
|
|
token,
|
|
|
|
}: {
|
|
|
|
token: string;
|
2019-10-05 08:00:32 +00:00
|
|
|
endpoint: string;
|
2019-11-26 15:13:07 +00:00
|
|
|
}): Promise<PlatformConfig> {
|
2019-05-20 08:59:30 +00:00
|
|
|
if (!token) {
|
|
|
|
throw new Error('Init: You must configure a GitLab personal access token');
|
|
|
|
}
|
|
|
|
if (endpoint) {
|
2019-08-14 04:04:09 +00:00
|
|
|
defaults.endpoint = endpoint.replace(/\/?$/, '/'); // always add a trailing slash
|
|
|
|
api.setBaseUrl(defaults.endpoint);
|
2019-05-20 08:59:30 +00:00
|
|
|
} else {
|
2019-08-14 04:04:09 +00:00
|
|
|
logger.info('Using default GitLab endpoint: ' + defaults.endpoint);
|
2019-05-20 08:59:30 +00:00
|
|
|
}
|
2019-08-14 04:04:09 +00:00
|
|
|
let gitAuthor: string;
|
2019-06-13 05:25:39 +00:00
|
|
|
try {
|
2019-07-10 11:18:51 +00:00
|
|
|
const user = (await api.get(`user`, { token })).body;
|
2019-08-14 04:04:09 +00:00
|
|
|
gitAuthor = user.email;
|
2019-07-10 11:18:51 +00:00
|
|
|
authorId = user.id;
|
2019-06-13 05:25:39 +00:00
|
|
|
} catch (err) {
|
|
|
|
logger.info(
|
|
|
|
{ err },
|
|
|
|
'Error authenticating with GitLab. Check that your token includes "user" permissions'
|
|
|
|
);
|
|
|
|
throw new Error('Init: Authentication failure');
|
|
|
|
}
|
2019-08-14 04:04:09 +00:00
|
|
|
const platformConfig: PlatformConfig = {
|
|
|
|
endpoint: defaults.endpoint,
|
|
|
|
gitAuthor,
|
|
|
|
};
|
|
|
|
return platformConfig;
|
2019-05-20 08:59:30 +00:00
|
|
|
}
|
|
|
|
|
2017-04-21 05:00:26 +00:00
|
|
|
// Get all repositories that the user has access to
|
2019-11-26 15:13:07 +00:00
|
|
|
export async function getRepos(): Promise<string[]> {
|
2018-04-04 11:38:06 +00:00
|
|
|
logger.info('Autodiscovering GitLab repositories');
|
2017-04-21 05:00:26 +00:00
|
|
|
try {
|
2019-12-02 19:40:13 +00:00
|
|
|
const url = `projects?membership=true&per_page=100&with_merge_requests_enabled=true`;
|
2019-05-29 12:00:56 +00:00
|
|
|
const res = await api.get(url, { paginate: true });
|
2017-10-18 09:40:48 +00:00
|
|
|
logger.info(`Discovered ${res.body.length} project(s)`);
|
2019-05-29 12:00:56 +00:00
|
|
|
return res.body.map(
|
|
|
|
(repo: { path_with_namespace: string }) => repo.path_with_namespace
|
|
|
|
);
|
2017-04-21 05:00:26 +00:00
|
|
|
} catch (err) {
|
2017-07-19 06:05:26 +00:00
|
|
|
logger.error({ err }, `GitLab getRepos error`);
|
2017-04-21 05:00:26 +00:00
|
|
|
throw err;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-11-25 16:13:39 +00:00
|
|
|
function urlEscape(str: string): string {
|
2018-02-03 11:06:25 +00:00
|
|
|
return str ? str.replace(/\//g, '%2F') : str;
|
2017-12-14 05:19:24 +00:00
|
|
|
}
|
|
|
|
|
2019-11-26 15:13:07 +00:00
|
|
|
export function cleanRepo(): void {
|
2018-10-29 17:25:11 +00:00
|
|
|
// istanbul ignore if
|
|
|
|
if (config.storage) {
|
|
|
|
config.storage.cleanRepo();
|
|
|
|
}
|
2018-08-29 11:38:06 +00:00
|
|
|
// In theory most of this isn't necessary. In practice..
|
2019-05-29 12:00:56 +00:00
|
|
|
config = {} as any;
|
2018-08-29 11:38:06 +00:00
|
|
|
}
|
|
|
|
|
2017-02-11 07:14:19 +00:00
|
|
|
// Initialize GitLab by getting base branch
|
2019-05-29 12:00:56 +00:00
|
|
|
export async function initRepo({
|
|
|
|
repository,
|
2019-11-29 07:56:10 +00:00
|
|
|
gitPrivateKey,
|
2019-05-29 12:00:56 +00:00
|
|
|
localDir,
|
2019-07-14 05:45:39 +00:00
|
|
|
optimizeForDisabled,
|
2019-11-26 15:13:07 +00:00
|
|
|
}: RepoParams): Promise<RepoConfig> {
|
2019-05-29 12:00:56 +00:00
|
|
|
config = {} as any;
|
2018-01-25 11:24:13 +00:00
|
|
|
config.repository = urlEscape(repository);
|
2019-11-29 07:56:10 +00:00
|
|
|
config.gitPrivateKey = gitPrivateKey;
|
2018-10-29 17:25:11 +00:00
|
|
|
config.localDir = localDir;
|
2019-11-26 15:13:07 +00:00
|
|
|
let res: GotResponse<{
|
|
|
|
archived: boolean;
|
|
|
|
mirror: boolean;
|
|
|
|
default_branch: string;
|
|
|
|
http_url_to_repo: string;
|
|
|
|
forked_from_project: boolean;
|
|
|
|
}>;
|
2017-02-11 07:14:19 +00:00
|
|
|
try {
|
2019-05-29 12:00:56 +00:00
|
|
|
res = await api.get(`projects/${config.repository}`);
|
2018-09-02 19:13:23 +00:00
|
|
|
if (res.body.archived) {
|
|
|
|
logger.info(
|
|
|
|
'Repository is archived - throwing error to abort renovation'
|
|
|
|
);
|
|
|
|
throw new Error('archived');
|
|
|
|
}
|
2019-06-13 04:05:58 +00:00
|
|
|
if (res.body.mirror) {
|
|
|
|
logger.info(
|
|
|
|
'Repository is a mirror - throwing error to abort renovation'
|
|
|
|
);
|
|
|
|
throw new Error('mirror');
|
|
|
|
}
|
2018-11-10 22:19:20 +00:00
|
|
|
if (res.body.default_branch === null) {
|
|
|
|
throw new Error('empty');
|
|
|
|
}
|
2019-07-14 05:45:39 +00:00
|
|
|
if (optimizeForDisabled) {
|
2019-11-26 15:13:07 +00:00
|
|
|
let renovateConfig: RenovateConfig;
|
2019-07-14 05:45:39 +00:00
|
|
|
try {
|
|
|
|
renovateConfig = JSON.parse(
|
|
|
|
Buffer.from(
|
|
|
|
(await api.get(
|
|
|
|
`projects/${config.repository}/repository/files/${defaultConfigFile}?ref=${res.body.default_branch}`
|
|
|
|
)).body.content,
|
|
|
|
'base64'
|
|
|
|
).toString()
|
|
|
|
);
|
|
|
|
} catch (err) {
|
|
|
|
// Do nothing
|
|
|
|
}
|
|
|
|
if (renovateConfig && renovateConfig.enabled === false) {
|
|
|
|
throw new Error('disabled');
|
|
|
|
}
|
|
|
|
}
|
2017-07-06 12:12:52 +00:00
|
|
|
config.defaultBranch = res.body.default_branch;
|
|
|
|
config.baseBranch = config.defaultBranch;
|
2018-01-25 11:24:13 +00:00
|
|
|
logger.debug(`${repository} default branch = ${config.baseBranch}`);
|
2017-06-22 09:56:23 +00:00
|
|
|
// Discover our user email
|
2019-05-29 12:00:56 +00:00
|
|
|
config.email = (await api.get(`user`)).body.email;
|
2019-05-02 21:34:52 +00:00
|
|
|
logger.debug('Bot email=' + config.email);
|
2017-12-23 07:03:16 +00:00
|
|
|
delete config.prList;
|
2019-05-04 05:41:07 +00:00
|
|
|
logger.debug('Enabling Git FS');
|
2019-05-20 08:59:30 +00:00
|
|
|
const opts = hostRules.find({
|
2019-05-24 15:40:39 +00:00
|
|
|
hostType: defaults.hostType,
|
|
|
|
url: defaults.endpoint,
|
2019-05-20 08:59:30 +00:00
|
|
|
});
|
2019-11-26 15:13:07 +00:00
|
|
|
let url: string;
|
2019-06-06 15:06:04 +00:00
|
|
|
if (res.body.http_url_to_repo === null) {
|
|
|
|
logger.debug('no http_url_to_repo found. Falling back to old behaviour.');
|
|
|
|
const { host, protocol } = URL.parse(defaults.endpoint);
|
|
|
|
url = GitStorage.getUrl({
|
|
|
|
protocol: protocol!.slice(0, -1) as any,
|
|
|
|
auth: 'oauth2:' + opts.token,
|
|
|
|
host,
|
|
|
|
repository,
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
logger.debug(`${repository} http URL = ${res.body.http_url_to_repo}`);
|
|
|
|
const repoUrl = URL.parse(`${res.body.http_url_to_repo}`);
|
|
|
|
repoUrl.auth = 'oauth2:' + opts.token;
|
|
|
|
url = URL.format(repoUrl);
|
|
|
|
}
|
2019-05-04 05:41:07 +00:00
|
|
|
config.storage = new GitStorage();
|
|
|
|
await config.storage.initRepo({
|
|
|
|
...config,
|
|
|
|
url,
|
|
|
|
});
|
2018-11-07 09:52:33 +00:00
|
|
|
} catch (err) /* istanbul ignore next */ {
|
2019-06-05 16:45:40 +00:00
|
|
|
logger.debug({ err }, 'Caught initRepo error');
|
2018-11-09 12:19:45 +00:00
|
|
|
if (err.message.includes('HEAD is not a symbolic ref')) {
|
|
|
|
throw new Error('empty');
|
|
|
|
}
|
2018-11-10 22:19:20 +00:00
|
|
|
if (['archived', 'empty'].includes(err.message)) {
|
2018-09-02 19:13:23 +00:00
|
|
|
throw err;
|
|
|
|
}
|
2018-11-07 09:52:33 +00:00
|
|
|
if (err.statusCode === 403) {
|
|
|
|
throw new Error('forbidden');
|
|
|
|
}
|
2018-12-01 17:43:00 +00:00
|
|
|
if (err.statusCode === 404) {
|
|
|
|
throw new Error('not-found');
|
|
|
|
}
|
2019-07-14 05:45:39 +00:00
|
|
|
if (err.message === 'disabled') {
|
|
|
|
throw err;
|
|
|
|
}
|
2018-09-05 08:31:21 +00:00
|
|
|
logger.info({ err }, 'Unknown GitLab initRepo error');
|
2017-02-11 07:14:19 +00:00
|
|
|
throw err;
|
|
|
|
}
|
2019-08-14 08:51:12 +00:00
|
|
|
const repoConfig: RepoConfig = {
|
2019-08-14 09:38:13 +00:00
|
|
|
baseBranch: config.baseBranch,
|
2019-08-14 08:51:12 +00:00
|
|
|
isFork: !!res.body.forked_from_project,
|
|
|
|
};
|
2019-08-14 04:04:09 +00:00
|
|
|
return repoConfig;
|
2017-02-11 07:14:19 +00:00
|
|
|
}
|
|
|
|
|
2019-11-26 15:13:07 +00:00
|
|
|
export function getRepoForceRebase(): boolean {
|
2017-11-15 14:31:20 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2019-11-25 16:13:39 +00:00
|
|
|
export async function setBaseBranch(
|
|
|
|
branchName = config.baseBranch
|
2019-11-26 15:13:07 +00:00
|
|
|
): Promise<void> {
|
2019-05-15 11:04:16 +00:00
|
|
|
logger.debug(`Setting baseBranch to ${branchName}`);
|
|
|
|
config.baseBranch = branchName;
|
|
|
|
await config.storage.setBaseBranch(branchName);
|
2017-07-06 08:26:18 +00:00
|
|
|
}
|
|
|
|
|
2019-05-29 12:00:56 +00:00
|
|
|
export /* istanbul ignore next */ function setBranchPrefix(
|
|
|
|
branchPrefix: string
|
2019-11-25 16:13:39 +00:00
|
|
|
): Promise<void> {
|
2019-05-17 06:10:40 +00:00
|
|
|
return config.storage.setBranchPrefix(branchPrefix);
|
|
|
|
}
|
|
|
|
|
2017-02-11 07:14:19 +00:00
|
|
|
// Search
|
|
|
|
|
2017-10-16 09:59:59 +00:00
|
|
|
// Get full file list
|
2019-11-25 16:13:39 +00:00
|
|
|
export function getFileList(branchName = config.baseBranch): Promise<string[]> {
|
2018-10-29 17:25:11 +00:00
|
|
|
return config.storage.getFileList(branchName);
|
2017-10-16 09:59:59 +00:00
|
|
|
}
|
|
|
|
|
2017-02-11 07:14:19 +00:00
|
|
|
// Returns true if branch exists, otherwise false
|
2019-11-25 16:13:39 +00:00
|
|
|
export function branchExists(branchName: string): Promise<boolean> {
|
2018-10-29 17:25:11 +00:00
|
|
|
return config.storage.branchExists(branchName);
|
2017-06-22 09:56:23 +00:00
|
|
|
}
|
|
|
|
|
2019-11-24 04:09:13 +00:00
|
|
|
// Returns the combined status for a branch.
|
|
|
|
export async function getBranchStatus(
|
|
|
|
branchName: string,
|
|
|
|
requiredStatusChecks?: string[] | null
|
2019-11-26 15:13:07 +00:00
|
|
|
): Promise<string> {
|
2019-11-24 04:09:13 +00:00
|
|
|
logger.debug(`getBranchStatus(${branchName})`);
|
|
|
|
if (!requiredStatusChecks) {
|
|
|
|
// null means disable status checks, so it always succeeds
|
|
|
|
return 'success';
|
|
|
|
}
|
|
|
|
if (Array.isArray(requiredStatusChecks) && requiredStatusChecks.length) {
|
|
|
|
// This is Unsupported
|
|
|
|
logger.warn({ requiredStatusChecks }, `Unsupported requiredStatusChecks`);
|
|
|
|
return 'failed';
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!(await branchExists(branchName))) {
|
|
|
|
throw new Error('repository-changed');
|
|
|
|
}
|
|
|
|
|
|
|
|
// First, get the branch commit SHA
|
|
|
|
const branchSha = await config.storage.getBranchCommit(branchName);
|
|
|
|
// Now, check the statuses for that commit
|
|
|
|
const url = `projects/${config.repository}/repository/commits/${branchSha}/statuses`;
|
|
|
|
const res = await api.get(url, { paginate: true });
|
|
|
|
logger.debug(`Got res with ${res.body.length} results`);
|
|
|
|
if (res.body.length === 0) {
|
|
|
|
// Return 'pending' if we have no status checks
|
|
|
|
return 'pending';
|
|
|
|
}
|
|
|
|
let status = 'success';
|
|
|
|
// Return 'success' if all are success
|
|
|
|
res.body.forEach((check: { status: string; allow_failure?: boolean }) => {
|
|
|
|
// If one is failed then don't overwrite that
|
|
|
|
if (status !== 'failure') {
|
|
|
|
if (!check.allow_failure) {
|
|
|
|
if (check.status === 'failed') {
|
|
|
|
status = 'failure';
|
|
|
|
} else if (check.status !== 'success') {
|
|
|
|
({ status } = check);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
return status;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Pull Request
|
|
|
|
|
|
|
|
export async function createPr(
|
|
|
|
branchName: string,
|
|
|
|
title: string,
|
|
|
|
rawDescription: string,
|
|
|
|
labels?: string[] | null,
|
|
|
|
useDefaultBranch?: boolean,
|
|
|
|
platformOptions?: PlatformPrOptions
|
2019-11-26 15:13:07 +00:00
|
|
|
): Promise<Pr> {
|
2019-11-24 04:09:13 +00:00
|
|
|
const description = sanitize(rawDescription);
|
|
|
|
const targetBranch = useDefaultBranch
|
|
|
|
? config.defaultBranch
|
|
|
|
: config.baseBranch;
|
|
|
|
logger.debug(`Creating Merge Request: ${title}`);
|
|
|
|
const res = await api.post(`projects/${config.repository}/merge_requests`, {
|
|
|
|
body: {
|
|
|
|
source_branch: branchName,
|
|
|
|
target_branch: targetBranch,
|
|
|
|
remove_source_branch: true,
|
|
|
|
title,
|
|
|
|
description,
|
|
|
|
labels: is.array(labels) ? labels.join(',') : null,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
const pr = res.body;
|
|
|
|
pr.number = pr.iid;
|
|
|
|
pr.branchName = branchName;
|
|
|
|
pr.displayNumber = `Merge Request #${pr.iid}`;
|
|
|
|
pr.isModified = false;
|
|
|
|
// istanbul ignore if
|
|
|
|
if (config.prList) {
|
|
|
|
config.prList.push(pr);
|
|
|
|
}
|
|
|
|
if (platformOptions && platformOptions.gitLabAutomerge) {
|
|
|
|
try {
|
|
|
|
await api.put(
|
|
|
|
`projects/${config.repository}/merge_requests/${pr.iid}/merge`,
|
|
|
|
{
|
|
|
|
body: {
|
|
|
|
should_remove_source_branch: true,
|
|
|
|
merge_when_pipeline_succeeds: true,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
);
|
|
|
|
} catch (err) /* istanbul ignore next */ {
|
|
|
|
logger.debug({ err }, 'Automerge on PR creation failed');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return pr;
|
|
|
|
}
|
|
|
|
|
2019-11-26 15:13:07 +00:00
|
|
|
export async function getPr(iid: number): Promise<Pr> {
|
2019-11-24 04:09:13 +00:00
|
|
|
logger.debug(`getPr(${iid})`);
|
|
|
|
const url = `projects/${config.repository}/merge_requests/${iid}?include_diverged_commits_count=1`;
|
|
|
|
const pr = (await api.get(url)).body;
|
|
|
|
// Harmonize fields with GitHub
|
|
|
|
pr.branchName = pr.source_branch;
|
|
|
|
pr.targetBranch = pr.target_branch;
|
|
|
|
pr.number = pr.iid;
|
|
|
|
pr.displayNumber = `Merge Request #${pr.iid}`;
|
|
|
|
pr.body = pr.description;
|
|
|
|
pr.isStale = pr.diverged_commits_count > 0;
|
|
|
|
pr.state = pr.state === 'opened' ? 'open' : pr.state;
|
|
|
|
pr.isModified = true;
|
|
|
|
if (pr.merge_status === 'cannot_be_merged') {
|
|
|
|
logger.debug('pr cannot be merged');
|
|
|
|
pr.canMerge = false;
|
|
|
|
pr.isConflicted = true;
|
|
|
|
} else if (pr.state === 'open') {
|
|
|
|
const branchStatus = await getBranchStatus(pr.branchName, []);
|
|
|
|
if (branchStatus === 'success') {
|
|
|
|
pr.canMerge = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Check if the most recent branch commit is by us
|
|
|
|
// If not then we don't allow it to be rebased, in case someone's changes would be lost
|
|
|
|
const branchUrl = `projects/${
|
|
|
|
config.repository
|
|
|
|
}/repository/branches/${urlEscape(pr.source_branch)}`;
|
|
|
|
try {
|
|
|
|
const branch = (await api.get(branchUrl)).body;
|
|
|
|
const branchCommitEmail =
|
|
|
|
branch && branch.commit ? branch.commit.author_email : null;
|
|
|
|
// istanbul ignore if
|
|
|
|
if (branchCommitEmail === config.email) {
|
|
|
|
pr.isModified = false;
|
|
|
|
} else {
|
|
|
|
logger.debug(
|
|
|
|
{ branchCommitEmail, configEmail: config.email, iid: pr.iid },
|
|
|
|
'Last committer to branch does not match bot email, so PR cannot be rebased.'
|
|
|
|
);
|
|
|
|
pr.isModified = true;
|
|
|
|
}
|
|
|
|
} catch (err) {
|
|
|
|
logger.debug({ err }, 'Error getting PR branch');
|
|
|
|
if (pr.state === 'open' || err.statusCode !== 404) {
|
|
|
|
logger.warn({ err }, 'Error getting PR branch');
|
|
|
|
pr.isConflicted = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return pr;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Return a list of all modified files in a PR
|
2019-11-26 15:13:07 +00:00
|
|
|
export async function getPrFiles(mrNo: number): Promise<string[]> {
|
2019-11-24 04:09:13 +00:00
|
|
|
logger.debug({ mrNo }, 'getPrFiles');
|
|
|
|
if (!mrNo) {
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
const files = (await api.get(
|
|
|
|
`projects/${config.repository}/merge_requests/${mrNo}/changes`
|
|
|
|
)).body.changes;
|
|
|
|
return files.map((f: { new_path: string }) => f.new_path);
|
|
|
|
}
|
|
|
|
|
|
|
|
// istanbul ignore next
|
2019-11-26 15:13:07 +00:00
|
|
|
async function closePr(iid: number): Promise<void> {
|
2019-11-24 04:09:13 +00:00
|
|
|
await api.put(`projects/${config.repository}/merge_requests/${iid}`, {
|
|
|
|
body: {
|
|
|
|
state_event: 'close',
|
|
|
|
},
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function updatePr(
|
|
|
|
iid: number,
|
|
|
|
title: string,
|
|
|
|
description: string
|
2019-11-26 15:13:07 +00:00
|
|
|
): Promise<void> {
|
2019-11-24 04:09:13 +00:00
|
|
|
await api.put(`projects/${config.repository}/merge_requests/${iid}`, {
|
|
|
|
body: {
|
|
|
|
title,
|
|
|
|
description: sanitize(description),
|
|
|
|
},
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2019-11-26 15:13:07 +00:00
|
|
|
export async function mergePr(iid: number): Promise<boolean> {
|
2019-11-24 04:09:13 +00:00
|
|
|
try {
|
|
|
|
await api.put(`projects/${config.repository}/merge_requests/${iid}/merge`, {
|
|
|
|
body: {
|
|
|
|
should_remove_source_branch: true,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
return true;
|
|
|
|
} catch (err) /* istanbul ignore next */ {
|
|
|
|
if (err.statusCode === 401) {
|
|
|
|
logger.info('No permissions to merge PR');
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
if (err.statusCode === 406) {
|
|
|
|
logger.info('PR not acceptable for merging');
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
logger.debug({ err }, 'merge PR error');
|
|
|
|
logger.info('PR merge failed');
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-11-26 15:13:07 +00:00
|
|
|
export function getPrBody(input: string): string {
|
2019-11-24 04:09:13 +00:00
|
|
|
return smartTruncate(
|
|
|
|
input
|
|
|
|
.replace(/Pull Request/g, 'Merge Request')
|
|
|
|
.replace(/PR/g, 'MR')
|
|
|
|
.replace(/\]\(\.\.\/pull\//g, '](../merge_requests/'),
|
|
|
|
1000000
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Branch
|
|
|
|
|
2017-02-11 07:14:19 +00:00
|
|
|
// Returns the Pull Request for a branch. Null if not exists.
|
2019-11-26 15:13:07 +00:00
|
|
|
export async function getBranchPr(branchName: string): Promise<Pr> {
|
2017-02-11 07:14:19 +00:00
|
|
|
logger.debug(`getBranchPr(${branchName})`);
|
2019-05-04 05:41:07 +00:00
|
|
|
// istanbul ignore if
|
2018-04-17 06:29:55 +00:00
|
|
|
if (!(await branchExists(branchName))) {
|
2018-02-28 03:43:30 +00:00
|
|
|
return null;
|
|
|
|
}
|
2019-07-10 11:18:51 +00:00
|
|
|
const query = new URLSearchParams({
|
|
|
|
per_page: '100',
|
|
|
|
state: 'opened',
|
|
|
|
source_branch: branchName,
|
|
|
|
}).toString();
|
|
|
|
const urlString = `projects/${config.repository}/merge_requests?${query}`;
|
2019-07-09 05:54:42 +00:00
|
|
|
const res = await api.get(urlString, { paginate: true });
|
2017-02-11 07:14:19 +00:00
|
|
|
logger.debug(`Got res with ${res.body.length} results`);
|
2019-05-29 12:00:56 +00:00
|
|
|
let pr: any = null;
|
|
|
|
res.body.forEach((result: { source_branch: string }) => {
|
2017-02-11 07:14:19 +00:00
|
|
|
if (result.source_branch === branchName) {
|
|
|
|
pr = result;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
if (!pr) {
|
|
|
|
return null;
|
|
|
|
}
|
2017-11-01 09:36:58 +00:00
|
|
|
return getPr(pr.iid);
|
2017-02-11 07:14:19 +00:00
|
|
|
}
|
|
|
|
|
2019-11-25 16:13:39 +00:00
|
|
|
export function getAllRenovateBranches(
|
|
|
|
branchPrefix: string
|
|
|
|
): Promise<string[]> {
|
2018-10-29 17:25:11 +00:00
|
|
|
return config.storage.getAllRenovateBranches(branchPrefix);
|
|
|
|
}
|
|
|
|
|
2019-11-25 16:13:39 +00:00
|
|
|
export function isBranchStale(branchName: string): Promise<boolean> {
|
2018-10-29 17:25:11 +00:00
|
|
|
return config.storage.isBranchStale(branchName);
|
|
|
|
}
|
|
|
|
|
2019-05-29 12:00:56 +00:00
|
|
|
export function commitFilesToBranch(
|
|
|
|
branchName: string,
|
|
|
|
files: any[],
|
|
|
|
message: string,
|
2018-10-29 17:25:11 +00:00
|
|
|
parentBranch = config.baseBranch
|
2019-11-25 16:13:39 +00:00
|
|
|
): Promise<void> {
|
2019-05-04 05:41:07 +00:00
|
|
|
return config.storage.commitFilesToBranch(
|
2018-10-29 17:25:11 +00:00
|
|
|
branchName,
|
|
|
|
files,
|
|
|
|
message,
|
|
|
|
parentBranch
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2019-11-25 16:13:39 +00:00
|
|
|
export function getFile(
|
|
|
|
filePath: string,
|
|
|
|
branchName?: string
|
|
|
|
): Promise<string> {
|
2018-10-29 17:25:11 +00:00
|
|
|
return config.storage.getFile(filePath, branchName);
|
|
|
|
}
|
|
|
|
|
2019-11-25 16:13:39 +00:00
|
|
|
export async function deleteBranch(
|
|
|
|
branchName: string,
|
|
|
|
shouldClosePr = false
|
2019-11-26 15:13:07 +00:00
|
|
|
): Promise<void> {
|
2018-12-25 04:54:15 +00:00
|
|
|
if (shouldClosePr) {
|
2018-10-29 17:25:11 +00:00
|
|
|
logger.debug('Closing PR');
|
|
|
|
const pr = await getBranchPr(branchName);
|
|
|
|
// istanbul ignore if
|
|
|
|
if (pr) {
|
2018-12-25 04:54:15 +00:00
|
|
|
await closePr(pr.number);
|
2018-10-29 17:25:11 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return config.storage.deleteBranch(branchName);
|
|
|
|
}
|
|
|
|
|
2019-11-25 16:13:39 +00:00
|
|
|
export function mergeBranch(branchName: string): Promise<void> {
|
2018-10-29 17:25:11 +00:00
|
|
|
return config.storage.mergeBranch(branchName);
|
|
|
|
}
|
|
|
|
|
2019-11-25 16:13:39 +00:00
|
|
|
export function getBranchLastCommitTime(branchName: string): Promise<Date> {
|
2018-10-29 17:25:11 +00:00
|
|
|
return config.storage.getBranchLastCommitTime(branchName);
|
|
|
|
}
|
|
|
|
|
|
|
|
// istanbul ignore next
|
2019-11-26 15:13:07 +00:00
|
|
|
export function getRepoStatus(): Promise<StatusResult> {
|
2018-10-29 17:25:11 +00:00
|
|
|
return config.storage.getRepoStatus();
|
|
|
|
}
|
|
|
|
|
2019-05-29 12:00:56 +00:00
|
|
|
export async function getBranchStatusCheck(
|
|
|
|
branchName: string,
|
|
|
|
context: string
|
2019-11-26 15:13:07 +00:00
|
|
|
): Promise<string | null> {
|
2018-10-29 17:25:11 +00:00
|
|
|
// First, get the branch commit SHA
|
|
|
|
const branchSha = await config.storage.getBranchCommit(branchName);
|
2017-08-08 21:03:52 +00:00
|
|
|
// Now, check the statuses for that commit
|
2019-06-07 04:34:57 +00:00
|
|
|
const url = `projects/${config.repository}/repository/commits/${branchSha}/statuses`;
|
2018-10-29 19:03:35 +00:00
|
|
|
// cache-bust in case we have rebased
|
2019-05-29 12:00:56 +00:00
|
|
|
const res = await api.get(url, { useCache: false });
|
2017-08-08 21:03:52 +00:00
|
|
|
logger.debug(`Got res with ${res.body.length} results`);
|
|
|
|
for (const check of res.body) {
|
|
|
|
if (check.name === context) {
|
|
|
|
return check.state;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2019-05-29 12:00:56 +00:00
|
|
|
export async function setBranchStatus(
|
|
|
|
branchName: string,
|
|
|
|
context: string,
|
|
|
|
description: string,
|
|
|
|
state: string,
|
|
|
|
targetUrl?: string
|
2019-11-25 16:13:39 +00:00
|
|
|
): Promise<void> {
|
2018-10-29 17:25:11 +00:00
|
|
|
// First, get the branch commit SHA
|
|
|
|
const branchSha = await config.storage.getBranchCommit(branchName);
|
2017-08-06 13:38:10 +00:00
|
|
|
// Now, check the statuses for that commit
|
2018-10-29 17:25:11 +00:00
|
|
|
const url = `projects/${config.repository}/statuses/${branchSha}`;
|
2019-05-29 12:00:56 +00:00
|
|
|
const options: any = {
|
2019-03-01 09:39:09 +00:00
|
|
|
state: state.replace('failure', 'failed'), // GitLab uses 'failed', not 'failure'
|
2017-08-06 13:38:10 +00:00
|
|
|
description,
|
|
|
|
context,
|
|
|
|
};
|
|
|
|
if (targetUrl) {
|
|
|
|
options.target_url = targetUrl;
|
|
|
|
}
|
2018-10-29 05:53:03 +00:00
|
|
|
try {
|
2019-05-29 12:00:56 +00:00
|
|
|
await api.post(url, { body: options });
|
2018-10-29 05:53:03 +00:00
|
|
|
} catch (err) /* istanbul ignore next */ {
|
2019-02-13 16:47:07 +00:00
|
|
|
if (
|
2019-10-13 05:46:12 +00:00
|
|
|
err.body &&
|
|
|
|
err.body.message &&
|
|
|
|
err.body.message.startsWith(
|
2019-02-13 16:47:07 +00:00
|
|
|
'Cannot transition status via :enqueue from :pending'
|
|
|
|
)
|
|
|
|
) {
|
2019-10-13 05:46:12 +00:00
|
|
|
// https://gitlab.com/gitlab-org/gitlab-foss/issues/25807
|
2019-02-13 16:47:07 +00:00
|
|
|
logger.info('Ignoring status transition error');
|
|
|
|
} else {
|
|
|
|
logger.debug({ err });
|
|
|
|
logger.warn('Failed to set branch status');
|
|
|
|
}
|
2018-10-29 05:53:03 +00:00
|
|
|
}
|
2017-08-06 13:38:10 +00:00
|
|
|
}
|
|
|
|
|
2017-02-11 07:14:19 +00:00
|
|
|
// Issue
|
|
|
|
|
2019-11-25 16:13:39 +00:00
|
|
|
export async function getIssueList(): Promise<any[]> {
|
2018-06-21 06:39:24 +00:00
|
|
|
if (!config.issueList) {
|
2019-05-29 12:00:56 +00:00
|
|
|
const res = await api.get(
|
|
|
|
`projects/${config.repository}/issues?state=opened`,
|
|
|
|
{
|
|
|
|
useCache: false,
|
|
|
|
}
|
|
|
|
);
|
2018-06-21 06:39:24 +00:00
|
|
|
// istanbul ignore if
|
|
|
|
if (!is.array(res.body)) {
|
|
|
|
logger.warn({ responseBody: res.body }, 'Could not retrieve issue list');
|
|
|
|
return [];
|
|
|
|
}
|
2019-05-29 12:00:56 +00:00
|
|
|
config.issueList = res.body.map((i: { iid: number; title: string }) => ({
|
2018-06-21 06:39:24 +00:00
|
|
|
iid: i.iid,
|
|
|
|
title: i.title,
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
return config.issueList;
|
2017-12-18 08:39:52 +00:00
|
|
|
}
|
|
|
|
|
2019-11-26 15:13:07 +00:00
|
|
|
export async function findIssue(title: string): Promise<Issue | null> {
|
2018-10-03 13:47:03 +00:00
|
|
|
logger.debug(`findIssue(${title})`);
|
|
|
|
try {
|
|
|
|
const issueList = await getIssueList();
|
2019-05-29 12:00:56 +00:00
|
|
|
const issue = issueList.find((i: { title: string }) => i.title === title);
|
2018-10-03 13:47:03 +00:00
|
|
|
if (!issue) {
|
|
|
|
return null;
|
|
|
|
}
|
2019-05-29 12:00:56 +00:00
|
|
|
const issueBody = (await api.get(
|
2018-10-03 13:47:03 +00:00
|
|
|
`projects/${config.repository}/issues/${issue.iid}`
|
2018-10-09 03:05:20 +00:00
|
|
|
)).body.description;
|
2018-10-03 13:47:03 +00:00
|
|
|
return {
|
|
|
|
number: issue.iid,
|
|
|
|
body: issueBody,
|
|
|
|
};
|
|
|
|
} catch (err) /* istanbul ignore next */ {
|
|
|
|
logger.warn('Error finding issue');
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-11-25 16:13:39 +00:00
|
|
|
export async function ensureIssue(
|
|
|
|
title: string,
|
|
|
|
body: string
|
2019-11-26 15:13:07 +00:00
|
|
|
): Promise<'updated' | 'created' | null> {
|
2018-06-21 06:39:24 +00:00
|
|
|
logger.debug(`ensureIssue()`);
|
2019-09-12 10:48:31 +00:00
|
|
|
const description = getPrBody(sanitize(body));
|
2018-06-21 06:39:24 +00:00
|
|
|
try {
|
|
|
|
const issueList = await getIssueList();
|
2019-05-29 12:00:56 +00:00
|
|
|
const issue = issueList.find((i: { title: string }) => i.title === title);
|
2018-06-21 06:39:24 +00:00
|
|
|
if (issue) {
|
2019-05-29 12:00:56 +00:00
|
|
|
const existingDescription = (await api.get(
|
2018-06-21 06:39:24 +00:00
|
|
|
`projects/${config.repository}/issues/${issue.iid}`
|
2018-10-09 03:05:20 +00:00
|
|
|
)).body.description;
|
2018-10-12 08:36:09 +00:00
|
|
|
if (existingDescription !== description) {
|
2018-06-21 06:39:24 +00:00
|
|
|
logger.debug('Updating issue body');
|
2019-05-29 12:00:56 +00:00
|
|
|
await api.put(`projects/${config.repository}/issues/${issue.iid}`, {
|
2018-10-12 08:36:09 +00:00
|
|
|
body: { description },
|
2018-06-21 06:39:24 +00:00
|
|
|
});
|
|
|
|
return 'updated';
|
|
|
|
}
|
|
|
|
} else {
|
2019-05-29 12:00:56 +00:00
|
|
|
await api.post(`projects/${config.repository}/issues`, {
|
2018-06-21 06:39:24 +00:00
|
|
|
body: {
|
|
|
|
title,
|
2018-10-12 08:36:09 +00:00
|
|
|
description,
|
2018-06-21 06:39:24 +00:00
|
|
|
},
|
|
|
|
});
|
2018-07-05 07:52:31 +00:00
|
|
|
// delete issueList so that it will be refetched as necessary
|
|
|
|
delete config.issueList;
|
2018-06-21 06:39:24 +00:00
|
|
|
return 'created';
|
|
|
|
}
|
|
|
|
} catch (err) /* istanbul ignore next */ {
|
|
|
|
if (err.message.startsWith('Issues are disabled for this repo')) {
|
|
|
|
logger.info(`Could not create issue: ${err.message}`);
|
|
|
|
} else {
|
2018-09-05 08:31:21 +00:00
|
|
|
logger.warn({ err }, 'Could not ensure issue');
|
2018-06-21 06:39:24 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2019-11-25 16:13:39 +00:00
|
|
|
export async function ensureIssueClosing(title: string): Promise<void> {
|
2018-06-21 06:39:24 +00:00
|
|
|
logger.debug(`ensureIssueClosing()`);
|
|
|
|
const issueList = await getIssueList();
|
|
|
|
for (const issue of issueList) {
|
|
|
|
if (issue.title === title) {
|
|
|
|
logger.info({ issue }, 'Closing issue');
|
2019-05-29 12:00:56 +00:00
|
|
|
await api.put(`projects/${config.repository}/issues/${issue.iid}`, {
|
2018-07-30 09:32:06 +00:00
|
|
|
body: { state_event: 'close' },
|
2018-06-21 06:39:24 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2017-12-18 08:39:52 +00:00
|
|
|
|
2019-11-25 16:13:39 +00:00
|
|
|
export async function addAssignees(
|
|
|
|
iid: number,
|
|
|
|
assignees: string[]
|
2019-11-26 15:13:07 +00:00
|
|
|
): Promise<void> {
|
2017-11-01 12:55:36 +00:00
|
|
|
logger.debug(`Adding assignees ${assignees} to #${iid}`);
|
2017-11-10 08:59:12 +00:00
|
|
|
try {
|
2019-11-25 16:13:39 +00:00
|
|
|
let assigneeId = (await api.get(`users?username=${assignees[0]}`)).body[0]
|
2019-05-29 12:00:56 +00:00
|
|
|
.id;
|
2019-11-25 16:13:39 +00:00
|
|
|
let url = `projects/${config.repository}/merge_requests/${iid}?assignee_id=${assigneeId}`;
|
2019-05-29 12:00:56 +00:00
|
|
|
await api.put(url);
|
2019-11-25 16:13:39 +00:00
|
|
|
try {
|
|
|
|
if (assignees.length > 1) {
|
|
|
|
url = `projects/${config.repository}/merge_requests/${iid}?assignee_ids[]=${assigneeId}`;
|
|
|
|
for (let i = 1; i < assignees.length; i += 1) {
|
|
|
|
assigneeId = (await api.get(`users?username=${assignees[i]}`)).body[0]
|
|
|
|
.id;
|
|
|
|
url += `&assignee_ids[]=${assigneeId}`;
|
|
|
|
}
|
|
|
|
await api.put(url);
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
logger.error({ iid, assignees }, 'Failed to add multiple assignees');
|
|
|
|
}
|
2017-11-10 08:59:12 +00:00
|
|
|
} catch (err) {
|
|
|
|
logger.error({ iid, assignees }, 'Failed to add assignees');
|
2017-02-11 07:14:19 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-11-26 15:13:07 +00:00
|
|
|
export function addReviewers(iid: number, reviewers: string[]): void {
|
2017-11-10 08:29:24 +00:00
|
|
|
logger.debug(`addReviewers('${iid}, '${reviewers})`);
|
2018-04-06 09:31:34 +00:00
|
|
|
logger.warn('Unimplemented in GitLab: approvals');
|
2017-02-11 07:14:19 +00:00
|
|
|
}
|
|
|
|
|
2019-11-25 16:13:39 +00:00
|
|
|
export async function deleteLabel(
|
|
|
|
issueNo: number,
|
|
|
|
label: string
|
2019-11-26 15:13:07 +00:00
|
|
|
): Promise<void> {
|
2018-11-08 19:01:10 +00:00
|
|
|
logger.debug(`Deleting label ${label} from #${issueNo}`);
|
|
|
|
try {
|
|
|
|
const pr = await getPr(issueNo);
|
2019-05-29 12:00:56 +00:00
|
|
|
const labels = (pr.labels || []).filter((l: string) => l !== label).join();
|
|
|
|
await api.put(`projects/${config.repository}/merge_requests/${issueNo}`, {
|
2018-11-08 19:01:10 +00:00
|
|
|
body: { labels },
|
|
|
|
});
|
|
|
|
} catch (err) /* istanbul ignore next */ {
|
|
|
|
logger.warn({ err, issueNo, label }, 'Failed to delete label');
|
|
|
|
}
|
2018-09-14 10:51:33 +00:00
|
|
|
}
|
|
|
|
|
2019-11-26 15:13:07 +00:00
|
|
|
async function getComments(issueNo: number): Promise<any[]> {
|
2018-06-27 08:15:15 +00:00
|
|
|
// GET projects/:owner/:repo/merge_requests/:number/notes
|
2018-06-12 05:18:28 +00:00
|
|
|
logger.debug(`Getting comments for #${issueNo}`);
|
2018-06-27 08:35:33 +00:00
|
|
|
const url = `projects/${config.repository}/merge_requests/${issueNo}/notes`;
|
2019-05-29 12:00:56 +00:00
|
|
|
const comments = (await api.get(url, { paginate: true })).body;
|
2018-06-12 05:18:28 +00:00
|
|
|
logger.debug(`Found ${comments.length} comments`);
|
|
|
|
return comments;
|
2017-10-18 13:28:51 +00:00
|
|
|
}
|
|
|
|
|
2019-11-26 15:13:07 +00:00
|
|
|
async function addComment(issueNo: number, body: string): Promise<void> {
|
2018-06-27 08:15:15 +00:00
|
|
|
// POST projects/:owner/:repo/merge_requests/:number/notes
|
2019-05-29 12:00:56 +00:00
|
|
|
await api.post(
|
2018-06-27 08:15:15 +00:00
|
|
|
`projects/${config.repository}/merge_requests/${issueNo}/notes`,
|
2018-06-12 05:18:28 +00:00
|
|
|
{
|
|
|
|
body: { body },
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2019-11-25 16:13:39 +00:00
|
|
|
async function editComment(
|
|
|
|
issueNo: number,
|
|
|
|
commentId: number,
|
|
|
|
body: string
|
2019-11-26 15:13:07 +00:00
|
|
|
): Promise<void> {
|
2018-10-25 17:01:39 +00:00
|
|
|
// PUT projects/:owner/:repo/merge_requests/:number/notes/:id
|
2019-05-29 12:00:56 +00:00
|
|
|
await api.put(
|
2019-06-07 04:34:57 +00:00
|
|
|
`projects/${config.repository}/merge_requests/${issueNo}/notes/${commentId}`,
|
2018-06-12 05:18:28 +00:00
|
|
|
{
|
|
|
|
body: { body },
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2019-11-26 15:13:07 +00:00
|
|
|
async function deleteComment(
|
|
|
|
issueNo: number,
|
|
|
|
commentId: number
|
|
|
|
): Promise<void> {
|
2018-06-27 08:15:15 +00:00
|
|
|
// DELETE projects/:owner/:repo/merge_requests/:number/notes/:id
|
2019-05-29 12:00:56 +00:00
|
|
|
await api.delete(
|
2018-06-27 08:35:33 +00:00
|
|
|
`projects/${config.repository}/merge_requests/${issueNo}/notes/${commentId}`
|
2018-06-12 05:18:28 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2019-05-29 12:00:56 +00:00
|
|
|
export async function ensureComment(
|
|
|
|
issueNo: number,
|
|
|
|
topic: string | null | undefined,
|
2019-09-07 12:51:00 +00:00
|
|
|
rawContent: string
|
2019-11-26 15:13:07 +00:00
|
|
|
): Promise<void> {
|
2019-09-12 10:48:31 +00:00
|
|
|
const content = sanitize(rawContent);
|
2018-10-23 16:46:45 +00:00
|
|
|
const massagedTopic = topic
|
|
|
|
? topic.replace(/Pull Request/g, 'Merge Request').replace(/PR/g, 'MR')
|
|
|
|
: topic;
|
2018-06-12 05:18:28 +00:00
|
|
|
const comments = await getComments(issueNo);
|
2019-05-29 12:00:56 +00:00
|
|
|
let body: string;
|
2018-06-12 05:18:28 +00:00
|
|
|
let commentId;
|
|
|
|
let commentNeedsUpdating;
|
|
|
|
if (topic) {
|
2018-10-23 16:46:45 +00:00
|
|
|
logger.debug(`Ensuring comment "${massagedTopic}" in #${issueNo}`);
|
2018-06-12 05:18:28 +00:00
|
|
|
body = `### ${topic}\n\n${content}`;
|
2018-10-23 16:46:45 +00:00
|
|
|
body = body.replace(/Pull Request/g, 'Merge Request').replace(/PR/g, 'MR');
|
2019-05-29 12:00:56 +00:00
|
|
|
comments.forEach((comment: { body: string; id: number }) => {
|
2018-10-23 16:46:45 +00:00
|
|
|
if (comment.body.startsWith(`### ${massagedTopic}\n\n`)) {
|
2018-06-12 05:18:28 +00:00
|
|
|
commentId = comment.id;
|
|
|
|
commentNeedsUpdating = comment.body !== body;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
logger.debug(`Ensuring content-only comment in #${issueNo}`);
|
|
|
|
body = `${content}`;
|
2019-05-29 12:00:56 +00:00
|
|
|
comments.forEach((comment: { body: string; id: number }) => {
|
2018-06-12 05:18:28 +00:00
|
|
|
if (comment.body === body) {
|
|
|
|
commentId = comment.id;
|
|
|
|
commentNeedsUpdating = false;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
if (!commentId) {
|
|
|
|
await addComment(issueNo, body);
|
|
|
|
logger.info({ repository: config.repository, issueNo }, 'Added comment');
|
|
|
|
} else if (commentNeedsUpdating) {
|
|
|
|
await editComment(issueNo, commentId, body);
|
|
|
|
logger.info({ repository: config.repository, issueNo }, 'Updated comment');
|
|
|
|
} else {
|
|
|
|
logger.debug('Comment is already update-to-date');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-11-25 16:13:39 +00:00
|
|
|
export async function ensureCommentRemoval(
|
|
|
|
issueNo: number,
|
|
|
|
topic: string
|
2019-11-26 15:13:07 +00:00
|
|
|
): Promise<void> {
|
2018-06-12 05:18:28 +00:00
|
|
|
logger.debug(`Ensuring comment "${topic}" in #${issueNo} is removed`);
|
|
|
|
const comments = await getComments(issueNo);
|
|
|
|
let commentId;
|
2019-05-29 12:00:56 +00:00
|
|
|
comments.forEach((comment: { body: string; id: number }) => {
|
2018-06-12 05:18:28 +00:00
|
|
|
if (comment.body.startsWith(`### ${topic}\n\n`)) {
|
|
|
|
commentId = comment.id;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
if (commentId) {
|
|
|
|
await deleteComment(issueNo, commentId);
|
|
|
|
}
|
2017-10-19 11:30:26 +00:00
|
|
|
}
|
|
|
|
|
2019-07-10 11:18:51 +00:00
|
|
|
const mapPullRequests = (pr: {
|
|
|
|
iid: number;
|
|
|
|
source_branch: string;
|
|
|
|
title: string;
|
|
|
|
state: string;
|
|
|
|
created_at: string;
|
2019-11-26 15:13:07 +00:00
|
|
|
}): Pr => ({
|
2019-07-10 11:18:51 +00:00
|
|
|
number: pr.iid,
|
|
|
|
branchName: pr.source_branch,
|
|
|
|
title: pr.title,
|
|
|
|
state: pr.state === 'opened' ? 'open' : pr.state,
|
|
|
|
createdAt: pr.created_at,
|
|
|
|
});
|
|
|
|
|
2019-11-26 15:13:07 +00:00
|
|
|
async function fetchPrList(): Promise<Pr[]> {
|
2019-07-10 11:18:51 +00:00
|
|
|
const query = new URLSearchParams({
|
|
|
|
per_page: '100',
|
|
|
|
author_id: `${authorId}`,
|
|
|
|
}).toString();
|
|
|
|
const urlString = `projects/${config.repository}/merge_requests?${query}`;
|
2019-11-24 20:43:46 +00:00
|
|
|
try {
|
|
|
|
const res = await api.get(urlString, { paginate: true });
|
|
|
|
return res.body.map(mapPullRequests);
|
|
|
|
} catch (err) /* istanbul ignore next */ {
|
|
|
|
logger.debug({ err }, 'Error fetching PR list');
|
|
|
|
if (err.statusCode === 403) {
|
|
|
|
throw new Error('authentication-error');
|
|
|
|
}
|
|
|
|
throw err;
|
|
|
|
}
|
2019-07-10 11:18:51 +00:00
|
|
|
}
|
|
|
|
|
2019-11-26 15:13:07 +00:00
|
|
|
export async function getPrList(): Promise<Pr[]> {
|
2017-11-14 08:55:05 +00:00
|
|
|
if (!config.prList) {
|
2019-07-10 11:18:51 +00:00
|
|
|
config.prList = await fetchPrList();
|
2017-11-14 08:55:05 +00:00
|
|
|
}
|
|
|
|
return config.prList;
|
|
|
|
}
|
|
|
|
|
2019-11-26 15:13:07 +00:00
|
|
|
function matchesState(state: string, desiredState: string): boolean {
|
2017-11-24 06:31:20 +00:00
|
|
|
if (desiredState === 'all') {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
if (desiredState[0] === '!') {
|
|
|
|
return state !== desiredState.substring(1);
|
|
|
|
}
|
|
|
|
return state === desiredState;
|
|
|
|
}
|
|
|
|
|
2019-05-29 12:00:56 +00:00
|
|
|
export async function findPr(
|
|
|
|
branchName: string,
|
|
|
|
prTitle?: string | null,
|
|
|
|
state = 'all'
|
2019-11-26 15:13:07 +00:00
|
|
|
): Promise<Pr> {
|
2017-02-11 07:14:19 +00:00
|
|
|
logger.debug(`findPr(${branchName}, ${prTitle}, ${state})`);
|
2017-11-14 08:55:05 +00:00
|
|
|
const prList = await getPrList();
|
|
|
|
return prList.find(
|
2019-05-29 12:00:56 +00:00
|
|
|
(p: { branchName: string; title: string; state: string }) =>
|
2017-11-14 08:55:05 +00:00
|
|
|
p.branchName === branchName &&
|
|
|
|
(!prTitle || p.title === prTitle) &&
|
2017-11-24 06:31:20 +00:00
|
|
|
matchesState(p.state, state)
|
2017-11-14 08:55:05 +00:00
|
|
|
);
|
2017-02-11 07:14:19 +00:00
|
|
|
}
|
|
|
|
|
2019-11-25 16:13:39 +00:00
|
|
|
export function getCommitMessages(): Promise<string[]> {
|
2018-10-29 17:25:11 +00:00
|
|
|
return config.storage.getCommitMessages();
|
2018-04-14 19:47:22 +00:00
|
|
|
}
|
2018-06-21 06:39:24 +00:00
|
|
|
|
2019-11-26 15:13:07 +00:00
|
|
|
export function getVulnerabilityAlerts(): VulnerabilityAlert[] {
|
2018-07-28 06:47:49 +00:00
|
|
|
return [];
|
|
|
|
}
|