From f07fad5e8034b9f42d2474a4582b2913b552c300 Mon Sep 17 00:00:00 2001 From: Alexandru Naiman Date: Thu, 20 Aug 2020 06:22:44 +0300 Subject: [PATCH] badge(gitlab): adding some badges for GitLab (#422) * wip: added some badges * - some final touches * small fix * - corrected wrong error message * fixes after code review --- api/gitlab.ts | 265 +++++++++++++++++++++++++++++++++++++++++++++ libs/badge-list.ts | 1 + libs/gitlab.ts | 33 ++++++ now.json | 3 +- 4 files changed, 301 insertions(+), 1 deletion(-) create mode 100644 api/gitlab.ts create mode 100644 libs/gitlab.ts diff --git a/api/gitlab.ts b/api/gitlab.ts new file mode 100644 index 0000000..1c2c04d --- /dev/null +++ b/api/gitlab.ts @@ -0,0 +1,265 @@ +import millify from 'millify' +import distanceToNow from 'date-fns/formatDistanceToNow' +import { createBadgenHandler, PathArgs } from '../libs/create-badgen-handler' +import { queryGitlab, restGitlab } from '../libs/gitlab' +import { version } from '../libs/utils' + +const removeNoSignFromHexColor = (hexColor: string) => hexColor.replace('#', '') + +export default createBadgenHandler({ + title: 'Gitlab', + examples: { + '/gitlab/stars/fdroid/fdroidclient': 'stars', + '/gitlab/forks/inkscape/inkscape': 'forks', + '/gitlab/issues/gitlab-org/gitlab-runner': 'issues', + '/gitlab/open-issues/gitlab-org/gitlab-runner': 'issues', + '/gitlab/closed-issues/gitlab-org/gitlab-runner': 'issues', + '/gitlab/label-issues/NickBusey/HomelabOS/Bug': 'issues by label', + '/gitlab/label-issues/NickBusey/HomelabOS/Enhancement/opened': 'open issues by label', + '/gitlab/label-issues/NickBusey/HomelabOS/Help%20wanted/closed': 'closed issues by label', + '/gitlab/mrs/edouardklein/falsisign': 'MRs', + '/gitlab/open-mrs/edouardklein/falsisign': 'open MRs', + '/gitlab/closed-mrs/edouardklein/falsisign': 'closed MRs', + '/gitlab/merged-mrs/edouardklein/falsisign': 'merged MRs', + '/gitlab/branches/gitlab-org%2fgitter/webapp': 'branches', + '/gitlab/releases/AuroraOSS/AuroraStore': 'release', + '/gitlab/release/veloren/veloren': 'latest release', + '/gitlab/tags/commento/commento': 'tags', + '/gitlab/contributors/graphviz/graphviz': 'contributors', + '/gitlab/license/gitlab-org/omnibus-gitlab': 'license', + '/gitlab/commits/cryptsetup/cryptsetup': 'commits count', + '/gitlab/commits/cryptsetup/cryptsetup/coverity_scan': 'commits count (branch ref)', + '/gitlab/commits/cryptsetup/cryptsetup/v2.2.2': 'commits count (tag ref)', + '/gitlab/last-commit/gitlab-org/gitlab-development-kit': 'last commit', + '/gitlab/last-commit/gitlab-org/gitlab-development-kit/updating-chromedriver-install-v2': 'last commit (branch ref)', + '/gitlab/last-commit/gitlab-org/gitlab-development-kit/v0.2.5': 'last commit (tag ref)', + }, + handlers: { + '/gitlab/:topic/:owner/:repo': queryHandler, + '/gitlab/:topic/:owner/:repo': restHandler, + '/gitlab/:topic/:owner/:repo/:label/:state?': queryHandler, + '/gitlab/:topic/:owner/:repo/:ref?': restHandler, + }, +}) + + +async function restHandler({ topic, owner, repo, ...restArgs }: PathArgs) { + const result = await makeRestCall({ topic, owner, repo, ...restArgs }) + + switch (topic) { + case 'mrs': + return { + subject: 'MRs', + status: millify(parseInt(result.headers['x-total'])), + color: 'blue' + } + case 'closed-mrs': + return { + subject: 'closed MRs', + status: millify(parseInt(result.headers['x-total'])), + color: 'blue' + } + case 'open-mrs': + return { + subject: 'open MRs', + status: millify(parseInt(result.headers['x-total'])), + color: 'blue' + } + case 'merged-mrs': + return { + subject: 'merged MRs', + status: millify(parseInt(result.headers['x-total'])), + color: 'blue' + } + case 'commits': + return { + subject: 'commits', + status: millify(parseInt(result.headers['x-total'])), + color: 'blue' + } + case 'last-commit': + const lastDate = result.length && new Date(result[0].committed_date) + const fromNow = lastDate && distanceToNow(lastDate, { addSuffix: true }) + return { + subject: 'last commit', + status: fromNow || 'none', + color: 'green' + } + case 'branches': + return { + subject: 'branches', + status: millify(parseInt(result.headers['x-total'])), + color: 'blue' + } + case 'releases': + return { + subject: 'releases', + status: millify(parseInt(result.headers['x-total'])), + color: 'blue' + } + case 'release': + const [latest] = result + if (!latest) { + return { + subject: 'release', + status: 'none', + color: 'yellow' + } + } + return { + subject: 'release', + status: version(latest.name || latest.tag_name), + color: 'blue' + } + case 'tags': + return { + subject: 'tags', + status: millify(parseInt(result.headers['x-total'])), + color: 'blue' + } + case 'contributors': + return { + subject: 'contributors', + status: millify(parseInt(result.headers['x-total'])), + color: 'blue' + } + case 'license': + return { + subject: 'license', + status: result.license?.name || "no license", + color: result.license ? 'blue' : 'grey' + } + default: + return { + subject: 'gitlab', + status: 'unknown topic', + color: 'grey' + } + } +} + + +async function queryHandler({ topic, owner, repo, ...restArgs }: PathArgs) { + const result = await makeQueryCall({ topic, owner, repo, ...restArgs }) + + if (!result) { + return { + subject: 'gitlab', + status: 'not found', + color: 'grey' + } + } + + switch (topic) { + case 'stars': + return { + subject: topic, + status: millify(result.starCount), + color: 'blue' + } + case 'forks': + return { + subject: topic, + status: millify(result.forksCount), + color: 'blue' + } + case 'issues': + return { + subject: topic, + status: millify(result.issues.count), + color: 'blue' + } + case 'open-issues': + return { + subject: 'open issues', + status: millify(result.openIssuesCount), + color: result.openIssuesCount === 0 ? 'green' : 'orange' + } + case 'closed-issues': + return { + subject: 'closed issues', + status: millify(result.issues.count), + color: 'blue' + } + case 'label-issues': + return { + subject: `${restArgs.label}`, + status: result.label ? millify(result.issues.count) : '0', + color: result.label ? removeNoSignFromHexColor(result.label.color) : 'gray' + } + default: + return { + subject: 'gitlab', + status: 'unknown topic', + color: 'grey' + } + } +} + +const makeQueryCall = async ({ topic, owner, repo, ...restArgs }) => { + const repoQueryBodies = { + 'stars': 'starCount', + 'forks': 'forksCount', + 'open-issues': 'openIssuesCount', + 'closed-issues': 'issues(state:closed){ count }', + 'issues': 'issues{ count }', + } + + let queryBody + switch (topic) { + case 'label-issues': + const { label, state } = restArgs + const stateFilter = state ? `state:${state.toLowerCase()}` : '' + queryBody = ` + issues(labelName:"${label}", ${stateFilter}) { count } + label(title: "${label}"){ color } + ` + break + default: + queryBody = repoQueryBodies[topic] + } + + const query = ` + query { + project(fullPath:"${owner}/${repo}") { + ${queryBody} + } + } +` + return queryGitlab(query).then(res => res.data!.project) +} + +const makeRestCall = async ({ topic, owner, repo, ...restArgs }) => { + const restPaths = { + 'mrs': `/projects/${encodeURIComponent(`${owner}/${repo}`)}/merge_requests`, + 'open-mrs': `/projects/${encodeURIComponent(`${owner}/${repo}`)}/merge_requests?state=opened`, + 'closed-mrs': `/projects/${encodeURIComponent(`${owner}/${repo}`)}/merge_requests?state=closed`, + 'merged-mrs': `/projects/${encodeURIComponent(`${owner}/${repo}`)}/merge_requests?state=merged`, + 'commits': `/projects/${encodeURIComponent(`${owner}/${repo}`)}/repository/commits?${restArgs.ref ? "ref_name=" + restArgs.ref : ''}`, + 'last-commit': `/projects/${encodeURIComponent(`${owner}/${repo}`)}/repository/commits?${restArgs.ref ? "ref_name=" + restArgs.ref : ''}`, + 'branches': `/projects/${encodeURIComponent(`${owner}/${repo}`)}/repository/branches`, + 'tags': `/projects/${encodeURIComponent(`${owner}/${repo}`)}/repository/tags`, + 'contributors': `/projects/${encodeURIComponent(`${owner}/${repo}`)}/repository/contributors`, + 'releases': `/projects/${encodeURIComponent(`${owner}/${repo}`)}/releases`, + 'release': `/projects/${encodeURIComponent(`${owner}/${repo}`)}/releases`, + 'license': `/projects/${encodeURIComponent(`${owner}/${repo}`)}?license=true`, + } + + let restPath = restPaths[topic] + + return restGitlab(restPath, fullResponsePaths.includes(topic)) +} + +const fullResponsePaths = + [ + 'mrs', + 'open-mrs', + 'closed-mrs', + 'merged-mrs', + 'commits', + 'branches', + 'releases', + 'tags', + 'contributors' + ] + diff --git a/libs/badge-list.ts b/libs/badge-list.ts index c1feecb..10eae00 100644 --- a/libs/badge-list.ts +++ b/libs/badge-list.ts @@ -7,6 +7,7 @@ const rel = (...args) => path.resolve(__dirname, ...args) export const liveBadgeList = [ // source control 'github', + 'gitlab', // release registries 'npm', 'david', diff --git a/libs/gitlab.ts b/libs/gitlab.ts new file mode 100644 index 0000000..af39884 --- /dev/null +++ b/libs/gitlab.ts @@ -0,0 +1,33 @@ +import got from './got' +import { BadgenError } from './create-badgen-handler' + +const rand = (arr: T[]): T => arr[Math.floor(Math.random() * arr.length)] + + +// request gitlab api v4 (graphql) +export function queryGitlab(query) { + const headers = { + authorization: `Bearer ${pickGitlabToken()}`, + } + const json = { query } + const endpoint = + process.env.GITLAB_API_GRAPHQL || 'https://gitlab.com/api/graphql' + return got.post(endpoint, { json, headers }).json() +} + +export function restGitlab(path: string, fullResponse = false) { + const headers = { + authorization: `Bearer ${pickGitlabToken()}`, + } + const prefixUrl = process.env.GITLAB_API || 'https://gitlab.com/api/v4' + return fullResponse ? got.get(path, { prefixUrl, headers }) : got.get(path, { prefixUrl, headers }).json() +} + +function pickGitlabToken() { + const { GITLAB_TOKENS } = process.env + if (!GITLAB_TOKENS) { + throw new BadgenError({ status: 'token required' }) + } + const tokens = GITLAB_TOKENS.split(',').map(segment => segment.trim()) + return rand(tokens) +} diff --git a/now.json b/now.json index 9425c6e..e28cf62 100644 --- a/now.json +++ b/now.json @@ -13,6 +13,7 @@ "env": { "GH_TOKENS": "@badgen-gh-tokens", "SENTRY_DSN": "@badgen-sentry-dsn", - "TRACKING_GA": "@badgen-tracking-ga" + "TRACKING_GA": "@badgen-tracking-ga", + "GITLAB_TOKENS": "@badgen-gl-tokens" } }