diff --git a/.gitignore b/.gitignore index e7343051..456ad124 100644 --- a/.gitignore +++ b/.gitignore @@ -22,5 +22,6 @@ interviews # кеш для @11ty/eleventy-cache-assets .cache +.issues.json .env diff --git a/src/libs/github-contribution-service/github-contribution-service.js b/src/libs/github-contribution-service/github-contribution-service.js deleted file mode 100644 index 5e6c732f..00000000 --- a/src/libs/github-contribution-service/github-contribution-service.js +++ /dev/null @@ -1,294 +0,0 @@ -const os = require('os') -const { Octokit } = require('@octokit/core') -const Cache = require('@11ty/eleventy-fetch') -const fetch = require('node-fetch') - -Cache.concurrency = os.cpus().length - -const { GITHUB_TOKEN } = process.env - -const octokit = new Octokit({ - auth: GITHUB_TOKEN, - request: { fetch }, -}) - -const CACHE_KEY_STAT = 'GITHUB_AUTHORS_CONTRIBUTION' -const CACHE_KEY_EXISTS = 'GITHUB_AUTHORS_EXISTS' -const CACHE_KEY_ID = 'GITHUB_AUTHORS_ID' -const CACHE_KEY_ACTIONS = 'GITHUB_AUTHORS_ACTIONS' -const CACHE_DURATION = '48d' - -const assetCache = {} -assetCache['stat'] = new Cache.AssetCache(CACHE_KEY_STAT) -assetCache['exists'] = new Cache.AssetCache(CACHE_KEY_EXISTS) -assetCache['id'] = new Cache.AssetCache(CACHE_KEY_ID) -assetCache['actions'] = new Cache.AssetCache(CACHE_KEY_ACTIONS) - -const responsePromise = {} -responsePromise['stat'] = undefined -responsePromise['exists'] = undefined -responsePromise['id'] = undefined -responsePromise['actions'] = undefined - -// graphql не поддерживает '-' в именах, поэтому применяем хак с заменой -const escapedOriginalSymbol = '-' -const escapedSymbol = '____' -function escape(data) { - return data.replaceAll(escapedOriginalSymbol, escapedSymbol) -} - -function getData(query) { - return octokit.graphql(` - query { - ${query} - } - `) -} - -// Запросы GraphQL к единичным сущностям - -function searchContributions({ author, repo, type }) { - const query = `repo:${repo} type:${type} author:${author}` - return ` - search(query: "${query}", type: ISSUE) { - count: issueCount - } - ` -} - -function userExists({ author }) { - return ` - search( - query: "user:${author}", - type: USER - ) { - userCount - } - ` -} - -function userID({ author }) { - return ` - user(login: "${author}") { - id - } - ` -} - -function repoActions({ authorID, repo }) { - const repoParts = repo.split('/') - if (!authorID) { - return '' - } - return ` - repository( - owner: "${repoParts[0]}", - name: "${repoParts[1]}" - ) { - people: defaultBranchRef { - target { - ... on Commit { - history( - author: { id: "${authorID}" }, - path: "people" - ) { - nodes { - committedDate, - associatedPullRequests(first: 1) { - nodes { - mergedAt - } - } - } - } - } - } - } - } - ` -} - -// Построение единичных запросов к GraphQL по всем авторам - -function buildQueryForAuthorContribution({ author, repo, type }) { - return ` - ${escape(author)}: ${searchContributions({ author, repo, type })} - ` -} - -function buildQueryForAuthorExists({ author }) { - return ` - ${escape(author)}: ${userExists({ author })} - ` -} - -function buildQueryForAuthorID({ author }) { - const query = ` - ${escape(author)}: ${userID({ author })} - ` - return query -} - -function buildQueryForAuthorAction({ author, authorID, repo }) { - return ` - ${escape(author)}: ${repoActions({ authorID, repo })} - ` -} - -// Построение групповых запросов к GraphQL по всем авторам - -function buildQueryForAuthorContributions({ authors, repo, type }) { - return authors.map((author) => buildQueryForAuthorContribution({ author, repo, type })).join('') -} - -function buildQueryForAuthorsExists({ authors }) { - return authors.map((author) => buildQueryForAuthorExists({ author })).join('') -} - -function buildQueryForAuthorIDs({ authors }) { - return authors.map((author) => buildQueryForAuthorID({ author })).join('') -} - -function buildQueryForAuthorActions({ authors, authorIDs, repo }) { - return authors - .map((author) => { - if (authorIDs[author]) { - return buildQueryForAuthorAction({ author, authorID: authorIDs[author].id, repo }) - } else { - return '' - } - }) - .join('') -} - -// Отправка запросов к GraphQL - -async function getAuthorsContribution({ authors, repo }) { - if (GITHUB_TOKEN === '') { - return [] - } - const [issueResponse, prResponse] = await Promise.all([ - getData(buildQueryForAuthorContributions({ authors, repo, type: 'issue' })), - getData(buildQueryForAuthorContributions({ authors, repo, type: 'pr' })), - ]) - - return authors.reduce((usersData, author) => { - const escapedName = escape(author) - usersData[author] = { - issues: issueResponse[escapedName]?.count ?? 0, - pr: prResponse[escapedName]?.count ?? 0, - } - return usersData - }, {}) -} - -async function getAuthorsExists({ authors }) { - if (GITHUB_TOKEN === '') { - return [] - } - - const [authorExists] = await Promise.all([getData(buildQueryForAuthorsExists({ authors }))]) - - return authors.reduce((usersData, author) => { - const escapedName = escape(author) - usersData[author] = authorExists[escapedName] - return usersData - }, {}) -} - -async function getAuthorsIDs({ authors }) { - if (GITHUB_TOKEN === '') { - return [] - } - - const [authorIDs] = await Promise.all([getData(buildQueryForAuthorIDs({ authors }))]) - - return authors.reduce((usersData, author) => { - const escapedName = escape(author) - usersData[author] = authorIDs[escapedName] - return usersData - }, {}) -} - -async function getActionsInRepo({ authors, authorIDs, repo }) { - if (GITHUB_TOKEN === '') { - return [] - } - - let authorActions = {} - - const chunkSize = 10 - for (let i = 0; i < authors.length; i += chunkSize) { - const chunk = authors.slice(i, i + chunkSize) - const [chunkAuthorActions] = await Promise.all([ - getData(buildQueryForAuthorActions({ authors: chunk, authorIDs, repo })), - ]) - authorActions = { - ...authorActions, - ...chunkAuthorActions, - } - } - - return authors.reduce((usersData, author) => { - const escapedName = escape(author) - usersData[author] = authorActions[escapedName] - return usersData - }, {}) -} - -// Использование локального кеша - -async function getAuthorsContributionWithCache({ authors, repo }) { - if (assetCache['stat'].isCacheValid(CACHE_DURATION)) { - return assetCache['stat'].getCachedValue() - } - - responsePromise['stat'] = responsePromise['stat'] || getAuthorsContribution({ authors, repo }) - const response = await responsePromise['stat'] - await assetCache['stat'].save(response, 'json') - - return response -} - -async function getAuthorsIDsWithCache({ authors, repo }) { - if (assetCache['id'].isCacheValid(CACHE_DURATION)) { - return assetCache['id'].getCachedValue() - } - - responsePromise['id'] = responsePromise['id'] || getAuthorsIDs({ authors, repo }) - const response = await responsePromise['id'] - await assetCache['id'].save(response, 'json') - - return response -} - -async function getAuthorsExistsWithCache({ authors }) { - if (assetCache['exists'].isCacheValid(CACHE_DURATION)) { - return assetCache['exists'].getCachedValue() - } - - responsePromise['exists'] = responsePromise['exists'] || getAuthorsExists({ authors }) - const response = await responsePromise['exists'] - await assetCache['exists'].save(response, 'json') - - return response -} - -async function getActionsInRepoWithCache({ authors, authorIDs, repo }) { - if (assetCache['actions'].isCacheValid(CACHE_DURATION)) { - return assetCache['actions'].getCachedValue() - } - - responsePromise['actions'] = responsePromise['actions'] || getActionsInRepo({ authors, authorIDs, repo }) - const response = await responsePromise['actions'] - await assetCache['actions'].save(response, 'json') - - return response -} - -module.exports = { - getAuthorsContributionWithCache, - getAuthorsExistsWithCache, - getAuthorsIDsWithCache, - getActionsInRepoWithCache, -} diff --git a/src/libs/github-contribution-stats/github-contribution-stats.js b/src/libs/github-contribution-stats/github-contribution-stats.js new file mode 100644 index 00000000..3dc2d862 --- /dev/null +++ b/src/libs/github-contribution-stats/github-contribution-stats.js @@ -0,0 +1,35 @@ +const issues = require('../../../.issues.json') + +const stats = issues.reduce((result, issue) => { + const user = issue['user']['login'].toLowerCase() + const isPullRequest = 'pull_request' in issue + const pullRequestIncrement = isPullRequest ? 1 : 0 + const issueIncrement = !isPullRequest ? 1 : 0 + const pullRequestDate = isPullRequest ? new Date(issue['closed_at']) : new Date() + + if (result && user in result) { + result[user]['issues'] += issueIncrement + result[user]['pr'] += pullRequestIncrement + if (result[user]['first'] > pullRequestDate) { + result[user]['first'] = pullRequestDate + } + return result + } else { + return { + ...result, + [user]: { + issues: issueIncrement, + pr: pullRequestIncrement, + first: pullRequestDate, + }, + } + } +}, {}) + +function getAuthorContributionStats() { + return stats +} + +module.exports = { + getAuthorContributionStats, +} diff --git a/src/views/person.11tydata.js b/src/views/person.11tydata.js index 95225a98..6971698b 100644 --- a/src/views/person.11tydata.js +++ b/src/views/person.11tydata.js @@ -168,21 +168,14 @@ module.exports = { }, badgesFields: function (data) { - const { authorData } = data - - if (!authorData) { - return null + const { contributionStat } = data + const releaseDate = new Date('2021-10-12T00:00:00Z') + const theFirstDate = new Date('1970-01-01T00:00:00Z') + let pullRequestDate = releaseDate + if (contributionStat && !contributionStat['first'] === theFirstDate) { + pullRequestDate = contributionStat } - - const nodes = authorData?.contributionActions?.people?.target?.history?.nodes - - const prNodes = nodes && nodes.length > 0 ? nodes[nodes.length - 1]?.associatedPullRequests?.nodes : null - - const pullRequestDate = - prNodes && prNodes.length > 0 ? prNodes[prNodes.length - 1]?.mergedAt : '2021-10-12T00:00:00Z' - - // TODO: Решить вопрос с датой для участников: Игорь Коровченко, Ольга Алексашенко - const githubFirstContribution = new Date(pullRequestDate) + const githubFirstContribution = pullRequestDate .toLocaleString('ru', { year: 'numeric', month: 'long', diff --git a/src/views/views.11tydata.js b/src/views/views.11tydata.js index 7fcf7c01..94a8151e 100644 --- a/src/views/views.11tydata.js +++ b/src/views/views.11tydata.js @@ -1,17 +1,10 @@ const path = require('path') const fsp = require('fs/promises') -const { URL } = require('url') const frontMatter = require('gray-matter') const { baseUrl, mainSections } = require('../../config/constants') const categoryColors = require('../../config/category-colors') const { titleFormatter } = require('../libs/title-formatter/title-formatter') -const { - getAuthorsContributionWithCache, - getAuthorsExistsWithCache, - getAuthorsIDsWithCache, - getActionsInRepoWithCache, -} = require('../libs/github-contribution-service/github-contribution-service') -const { contentRepLink } = require('../../config/constants') +const { getAuthorContributionStats } = require('../libs/github-contribution-stats/github-contribution-stats') const { setPath } = require('../libs/collection-helpers/set-path') const { isProdEnv } = require('../../config/env.js') @@ -315,33 +308,10 @@ module.exports = { return !!docsByPerson[personId] || !!practicesByPerson[personId] || !!answersByPerson[personId] }) - const authorsNames = filteredAuthors.map((author) => author.fileSlug) - let contributionStat = undefined - let contributorExists = undefined - let contributorIDs = undefined - let contributionActions = undefined if (isProdEnv) { - contributionStat = await getAuthorsContributionWithCache({ - authors: authorsNames, - // 'https://github.com/doka-guide/content' -> 'doka-guide/content' - repo: new URL(contentRepLink).pathname.replace(/^\//, ''), - }) - contributorExists = await getAuthorsExistsWithCache({ - authors: authorsNames, - }) - contributorIDs = await getAuthorsIDsWithCache({ - authors: authorsNames.filter((a) => (contributorExists[a] ? contributorExists[a].userCount > 0 : false)), - // 'https://github.com/doka-guide/content' -> 'doka-guide/content' - repo: new URL(contentRepLink).pathname.replace(/^\//, ''), - }) - contributionActions = await getActionsInRepoWithCache({ - authors: authorsNames, - authorIDs: contributorIDs, - // 'https://github.com/doka-guide/content' -> 'doka-guide/content' - repo: new URL(contentRepLink).pathname.replace(/^\//, ''), - }) + contributionStat = getAuthorContributionStats() } return filteredAuthors @@ -435,8 +405,7 @@ module.exports = { totalArticles, totalPractices, totalAnswers, - contributionStat: contributionStat ? contributionStat[personId] : null, - contributionActions: contributionActions ? contributionActions[personId] : null, + contributionStat: contributionStat ? contributionStat[personId.toLowerCase()] : null, } }) .sort((person1, person2) => person2.totalArticles - person1.totalArticles)