From 5498cdc8c1fd27efeae4fd98e63ea1c1d9ba9c23 Mon Sep 17 00:00:00 2001 From: Simon Lecoq <22963968+lowlighter@users.noreply.github.com> Date: Thu, 4 Nov 2021 00:24:17 -0400 Subject: [PATCH] feat(plugins/notable): add `indepth` mode (#635) [skip ci] --- source/plugins/notable/index.mjs | 84 +++++++++++++++++-- source/plugins/notable/metadata.yml | 6 ++ .../plugins/notable/queries/commits.graphql | 20 +++++ source/templates/classic/partials/notable.ejs | 12 ++- source/templates/classic/style.css | 55 ++++++++++++ 5 files changed, 170 insertions(+), 7 deletions(-) create mode 100644 source/plugins/notable/queries/commits.graphql diff --git a/source/plugins/notable/index.mjs b/source/plugins/notable/index.mjs index 19119246..bddc0fc5 100644 --- a/source/plugins/notable/index.mjs +++ b/source/plugins/notable/index.mjs @@ -1,5 +1,5 @@ //Setup -export default async function({login, q, imports, graphql, data, account, queries}, {enabled = false} = {}) { +export default async function({login, q, imports, rest, graphql, data, account, queries}, {enabled = false, extras = false} = {}) { //Plugin execution try { //Check if plugin is enabled and requirements are met @@ -7,10 +7,10 @@ export default async function({login, q, imports, graphql, data, account, querie return null //Load inputs - let {filter, repositories, from} = imports.metadata.plugins.notable.inputs({data, account, q}) + let {filter, repositories, from, indepth} = imports.metadata.plugins.notable.inputs({data, account, q}) //Iterate through contributed repositories - const notable = new Map() + const commits = [] { let cursor = null let pushed = 0 @@ -21,15 +21,89 @@ export default async function({login, q, imports, graphql, data, account, querie edges .filter(({node}) => ({all:true, organization:node.isInOrganization, user:!node.isInOrganization}[from])) .filter(({node}) => imports.ghfilter(filter, {name:node.nameWithOwner, user:node.owner.login, stars:node.stargazers.totalCount, watchers:node.watchers.totalCount, forks:node.forks.totalCount})) - .map(({node}) => notable.set((repositories || !node.isInOrganization) ? node.nameWithOwner : node.owner.login, {organization:node.isInOrganization, avatarUrl:node.owner.avatarUrl})) + .map(({node}) => commits.push({handle:node.nameWithOwner, stars:node.stargazers.totalCount, organization:node.isInOrganization, avatarUrl:node.owner.avatarUrl})) pushed = edges.length } while ((pushed) && (cursor)) } //Set contributions - const contributions = (await Promise.all([...notable.entries()].map(async ([name, {avatarUrl, organization}]) => ({name, avatar:await imports.imgb64(avatarUrl), organization})))).sort((a, b) => a.name.localeCompare(b.name)) + let contributions = (await Promise.all(commits.map(async ({handle, stars, avatarUrl, organization}) => ({name:handle.split("/").shift(), handle, stars, avatar:await imports.imgb64(avatarUrl), organization})))).sort((a, b) => a.name.localeCompare(b.name)) console.debug(`metrics/compute/${login}/plugins > notable > found ${contributions.length} notable contributions`) + //Extras features + if (extras) { + //Indepth + if (indepth) { + console.debug(`metrics/compute/${login}/plugins > notable > indepth`) + for (const contribution of contributions) { + //Prepare data + const {handle, stars} = contribution + const [owner, repo] = handle.split("/") + try { + //Count total commits on repository + const {repository:{defaultBranchRef:{target:{history}}}} = await graphql(queries.notable.commits({owner, repo})) + contribution.history = history.totalCount + + //Load maintainers (errors probably means that token is not allowed to list contributors hence not a maintainer of said repo) + const {data:collaborators} = await rest.repos.listCollaborators({owner, repo}).catch(() => ({data:[]})) + const maintainers = collaborators.filter(({role_name:role}) => ["admin", "maintain", "write"].includes(role)).map(({login}) => login) + + //Count total commits of user + const {data:contributions = []} = await rest.repos.getContributorsStats({owner, repo}) + const commits = contributions.filter(({author}) => author.login.toLocaleLowerCase() === login.toLocaleLowerCase()).reduce((a, {total:b}) => a + b, 0) + + //Save user data + contribution.user = { + commits, + percentage:commits/contribution.history, + maintainer:maintainers.includes(login), + get stars() { + return this.maintainer ? stars : this.percentage*stars + } + } + console.debug(`metrics/compute/${login}/plugins > notable > indepth > successfully processed ${owner}/${repo}`) + } + catch (error) { + console.debug(error) + console.debug(`metrics/compute/${login}/plugins > notable > indepth > failed to compute for ${owner}/${repo}`) + } + } + } + } + + //Aggregate contributions + if (from !== "all") { + console.debug(`metrics/compute/${login}/plugins > notable > aggregating results`) + contributions = contributions.filter(({organization}) => (from === "organization")&&(organization)) + const aggregated = new Map() + for (const {name, handle, avatar, organization, stars, ..._extras} of contributions) { + const key = repositories ? handle : name + if (aggregated.has(key)) { + const aggregate = aggregated.get(key) + aggregate.aggregated++ + if (extras) { + const {history = 0, user:{commits = 0, percentage = 0, maintainer = false} = {}} = _extras + aggregate.history = aggregate.history ?? 0 + aggregate.history += history + aggregate.user = aggregate.user ?? {} + aggregate.user.commits += commits + aggregate.user.percentage += percentage + aggregate.user.maintainer = aggregate.user.maintainer || maintainer + } + } + else + aggregated.set(key, {name:key, handle, avatar, organization, stars, aggregated:1, ..._extras}) + } + contributions = [...aggregated.values()] + if (extras) { + //Normalize contribution percentage + contributions.map(aggregate => aggregate.user ? aggregate.user.percentage /= aggregate.aggregated : null) + //Sort contribution by maintainer first and then by contribution percentage + contributions = contributions.sort((a, b) => ((b.user?.percentage + b.user?.maintainer) || 0) - ((a.user?.percentage + a.user?.maintainer) || 0)) + } + } + + //Results return {contributions} } diff --git a/source/plugins/notable/metadata.yml b/source/plugins/notable/metadata.yml index 0139c924..b3ca1477 100644 --- a/source/plugins/notable/metadata.yml +++ b/source/plugins/notable/metadata.yml @@ -36,4 +36,10 @@ inputs: plugin_notable_repositories: description: Also display repository name type: boolean + default: no + + # Compute notable contributions with measured impact + plugin_notable_indepth: + description: Indepth notable contributions processing + type: boolean default: no \ No newline at end of file diff --git a/source/plugins/notable/queries/commits.graphql b/source/plugins/notable/queries/commits.graphql new file mode 100644 index 00000000..c30d4b5c --- /dev/null +++ b/source/plugins/notable/queries/commits.graphql @@ -0,0 +1,20 @@ +{ + repository(owner: "$owner", name: "$repo") { + ...RepoFragment + } +} + +fragment RepoFragment on Repository { + name + defaultBranchRef { + name + target { + ... on Commit { + id + history(first: 0) { + totalCount + } + } + } + } +} \ No newline at end of file diff --git a/source/templates/classic/partials/notable.ejs b/source/templates/classic/partials/notable.ejs index 0d9b5743..403ed8d2 100644 --- a/source/templates/classic/partials/notable.ejs +++ b/source/templates/classic/partials/notable.ejs @@ -16,10 +16,18 @@ <% } else { %> <% if (plugins.notable.contributions.length) { %>
- <% for (const {name, avatar, organization} of plugins.notable.contributions) { %> -
+ <% for (const {name, avatar, organization, user:{commits = 0, maintainer = false, percentage = 0} = {}} of plugins.notable.contributions) { %> +
.2 ? "a" : percentage > .1 ? "b" : percentage > .05 ? "c" : "" %> "> avatar" src="<%= avatar %>" width="16" height="16" /> @<%= name %> + <% if (commits) { %> + + + + <%= commits %> + + + <% } %>
<% } %>
diff --git a/source/templates/classic/style.css b/source/templates/classic/style.css index 1385d50b..d0c2f36e 100644 --- a/source/templates/classic/style.css +++ b/source/templates/classic/style.css @@ -171,6 +171,61 @@ font-size: 12px; background-color: #959da520; } + .contribution.organization.s { + color: #EB355E; + background-color: #EB355E26; + border-color: #EB355E; + } + .contribution.organization.a { + color: #D79533; + background-color: #E7BD6926; + border-color: #E7BD69; + } + .contribution.organization.b { + color: #9D8FFF; + background-color: #9E91FF26; + border-color: #9E91FF; + } + .contribution.organization.c { + color: #58A6FF; + background-color: #58A6FF26; + border-color: #58A6FF; + } + .contribution .gauge-base, .contribution .gauge-arc { + stroke: currentColor; + stroke-width: 4; + } + .contribution .gauge text { + fill: currentColor; + font-size: 12px; + font-size: 15px; + letter-spacing: -2px; + } + .contribution .gauge { + margin: 0 4px; + color: inherit; + } + .contribution .commits-icon { + fill: currentColor; + width: 10px; + height: 10px; + margin-left: -9px; + margin-top: 8px; + color: inherit; + filter: brightness(.5); + } + .contribution.s .side { + background-color: #EB355E26; + } + .contribution.a .side { + background-color: #E7BD6926; + } + .contribution.b .side { + background-color: #9E91FF26; + } + .contribution.c .side { + background-color: #58A6FF26; + } .contribution.organization .avatar { margin: 0 4px;