Add contributors plugin (#126)
This commit is contained in:
14
source/app/mocks/api/github/graphql/contributors.commit.mjs
Normal file
14
source/app/mocks/api/github/graphql/contributors.commit.mjs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
/**Mocked data */
|
||||||
|
export default function({faker, query, login = faker.internet.userName()}) {
|
||||||
|
console.debug("metrics/compute/mocks > mocking graphql api result > contributors/commit")
|
||||||
|
return ({
|
||||||
|
repository:{
|
||||||
|
object:{
|
||||||
|
oid:"MOCKED_SHA",
|
||||||
|
abbreviatedOid:"MOCKED_SHA",
|
||||||
|
messageHeadline:faker.lorem.sentence(),
|
||||||
|
committedDate:faker.date.recent(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -11,13 +11,20 @@
|
|||||||
},
|
},
|
||||||
data:page < 2 ? new Array(per_page).fill(null).map(() => ({
|
data:page < 2 ? new Array(per_page).fill(null).map(() => ({
|
||||||
sha:"MOCKED_SHA",
|
sha:"MOCKED_SHA",
|
||||||
|
get author() {
|
||||||
|
return this.commit.author
|
||||||
|
},
|
||||||
commit:{
|
commit:{
|
||||||
author:{
|
author:{
|
||||||
name:owner,
|
name:owner,
|
||||||
|
login:faker.internet.userName(),
|
||||||
|
avatar_url:null,
|
||||||
date:`${faker.date.recent(14)}`,
|
date:`${faker.date.recent(14)}`,
|
||||||
},
|
},
|
||||||
committer:{
|
committer:{
|
||||||
name:owner,
|
name:owner,
|
||||||
|
login:faker.internet.userName(),
|
||||||
|
avatar_url:null,
|
||||||
date:`${faker.date.recent(14)}`,
|
date:`${faker.date.recent(14)}`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
25
source/plugins/contributors/README.md
Normal file
25
source/plugins/contributors/README.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
### 🏅 Contributors
|
||||||
|
|
||||||
|
The *contributors* plugin lets you display repositories contributors from a commit range, that can be specified through either sha, tags, branch, etc.
|
||||||
|
|
||||||
|
It's especially useful to acknowledge contributors on release notes.
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<td align="center">
|
||||||
|
<img src="https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.contributors.svg">
|
||||||
|
<img width="900" height="1" alt="">
|
||||||
|
</td>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
#### ℹ️ Examples workflows
|
||||||
|
|
||||||
|
[➡️ Available options for this plugin](metadata.yml)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- uses: lowlighter/metrics@latest
|
||||||
|
with:
|
||||||
|
# ... other options
|
||||||
|
plugin_contributors: yes
|
||||||
|
plugin_contributors_base: "" # Base reference (commit, tag, branch, etc.)
|
||||||
|
plugin_contributors_head: master # Head reference (commit, tag, branch, etc.)
|
||||||
|
```
|
||||||
69
source/plugins/contributors/index.mjs
Normal file
69
source/plugins/contributors/index.mjs
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
//Setup
|
||||||
|
export default async function({login, q, imports, data, rest, graphql, queries, account}, {enabled = false} = {}) {
|
||||||
|
//Plugin execution
|
||||||
|
try {
|
||||||
|
//Check if plugin is enabled and requirements are met
|
||||||
|
if ((!enabled)||(!q.contributors))
|
||||||
|
return null
|
||||||
|
|
||||||
|
//Load inputs
|
||||||
|
let {head, base} = imports.metadata.plugins.contributors.inputs({data, account, q})
|
||||||
|
const repo = {owner:data.repo.owner.login, repo:data.repo.name}
|
||||||
|
|
||||||
|
//Retrieve head and base commits
|
||||||
|
console.debug(`metrics/compute/${login}/plugins > contributors > querying api head and base commits`)
|
||||||
|
const ref = {
|
||||||
|
head:(await graphql(queries.contributors.commit({...repo, expression:head}))).repository.object,
|
||||||
|
base:(await graphql(queries.contributors.commit({...repo, expression:base}))).repository.object,
|
||||||
|
}
|
||||||
|
|
||||||
|
//Get commit activity
|
||||||
|
console.debug(`metrics/compute/${login}/plugins > contributors > querying api for commits between [${ref.base?.abbreviatedOid ?? null}] and [${ref.head?.abbreviatedOid ?? null}]`)
|
||||||
|
const commits = []
|
||||||
|
for (let page = 0; ; page++) {
|
||||||
|
console.debug(`metrics/compute/${login}/plugins > contributors > loading page ${page}`)
|
||||||
|
try {
|
||||||
|
const {data:loaded} = await rest.repos.listCommits({...repo, per_page:100, page})
|
||||||
|
if (loaded.map(({sha}) => sha).includes(ref.base?.oid)) {
|
||||||
|
console.debug(`metrics/compute/${login}/plugins > contributors > reached ${ref.base?.oid}`)
|
||||||
|
commits.push(...loaded.slice(0, loaded.map(({sha}) => sha).indexOf(ref.base.oid)))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (!loaded.length) {
|
||||||
|
console.debug(`metrics/compute/${login}/plugins > contributors > no more page to load`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
commits.push(...loaded)
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
if (/Git Repository is empty/.test(error))
|
||||||
|
break
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Remove commits after head
|
||||||
|
const start = Math.max(0, commits.map(({sha}) => sha).indexOf(ref.head?.oid))
|
||||||
|
commits.splice(0, start)
|
||||||
|
console.debug(`metrics/compute/${login}/plugins > contributors > ${commits.length} commits loaded (${start} removed)`)
|
||||||
|
|
||||||
|
//Compute contributors and contributions
|
||||||
|
let contributors = {}
|
||||||
|
for (const {author:{login, avatar_url:avatar}} of commits) {
|
||||||
|
if (!login)
|
||||||
|
continue
|
||||||
|
if (!(login in contributors))
|
||||||
|
contributors[login] = {avatar:avatar ? await imports.imgb64(avatar) : "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg==", contributions:0}
|
||||||
|
else
|
||||||
|
contributors[login].contributions++
|
||||||
|
}
|
||||||
|
contributors = Object.fromEntries(Object.entries(contributors).sort((a, b) => b.contributions - a.contributions))
|
||||||
|
|
||||||
|
//Results
|
||||||
|
return {head, base, ref, list:contributors}
|
||||||
|
}
|
||||||
|
//Handle errors
|
||||||
|
catch (error) {
|
||||||
|
throw {error:{message:"An error occured", instance:error}}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
source/plugins/contributors/metadata.yml
Normal file
23
source/plugins/contributors/metadata.yml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
name: "🏅 Contributors"
|
||||||
|
cost: N/A
|
||||||
|
supports:
|
||||||
|
- repository
|
||||||
|
inputs:
|
||||||
|
|
||||||
|
# Enable or disable plugin
|
||||||
|
plugin_contributors:
|
||||||
|
description: Display repository contributors
|
||||||
|
type: boolean
|
||||||
|
default: no
|
||||||
|
|
||||||
|
# Base reference (commit, tag, branch, etc.)
|
||||||
|
plugin_contributors_base:
|
||||||
|
description: Base reference
|
||||||
|
type: string
|
||||||
|
default: ""
|
||||||
|
|
||||||
|
# Head reference (commit, tag, branch, etc.)
|
||||||
|
plugin_contributors_head:
|
||||||
|
description: Head reference
|
||||||
|
type: string
|
||||||
|
default: master
|
||||||
12
source/plugins/contributors/queries/commit.graphql
Normal file
12
source/plugins/contributors/queries/commit.graphql
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
query ContributorsCommit {
|
||||||
|
repository(owner: "$owner" name: "$repo") {
|
||||||
|
object(expression: "$expression") {
|
||||||
|
... on Commit {
|
||||||
|
oid
|
||||||
|
abbreviatedOid
|
||||||
|
messageHeadline
|
||||||
|
committedDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
source/plugins/contributors/tests.yml
Normal file
13
source/plugins/contributors/tests.yml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
- name: Contributors plugin (default)
|
||||||
|
uses: lowlighter/metrics@latest
|
||||||
|
with:
|
||||||
|
token: MOCKED_TOKEN
|
||||||
|
plugin_contributors: yes
|
||||||
|
|
||||||
|
- name: Contributors plugin (default)
|
||||||
|
uses: lowlighter/metrics@latest
|
||||||
|
with:
|
||||||
|
token: MOCKED_TOKEN
|
||||||
|
plugin_contributors: yes
|
||||||
|
plugin_contributors_head: MOCKED_SHA
|
||||||
|
plugin_contributors_base: MOCKED_SHA
|
||||||
@@ -686,6 +686,27 @@
|
|||||||
fill: #58a6ff;
|
fill: #58a6ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Contributors */
|
||||||
|
.contributors {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-left: 6px;
|
||||||
|
}
|
||||||
|
.contributors .label {
|
||||||
|
padding-left: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.contributors .label img {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
.contributors .contributions {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 100%;
|
||||||
|
right: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
/* Fade animation */
|
/* Fade animation */
|
||||||
.af {
|
.af {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|||||||
@@ -7,5 +7,6 @@
|
|||||||
"stargazers",
|
"stargazers",
|
||||||
"people",
|
"people",
|
||||||
"activity",
|
"activity",
|
||||||
|
"contributors",
|
||||||
"licenses"
|
"licenses"
|
||||||
]
|
]
|
||||||
30
source/templates/repository/partials/contributors.ejs
Normal file
30
source/templates/repository/partials/contributors.ejs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<% if (plugins.contributors) { %>
|
||||||
|
<section>
|
||||||
|
<h2 class="field">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M4.25 2.5c-1.336 0-2.75 1.164-2.75 3 0 2.15 1.58 4.144 3.365 5.682A20.565 20.565 0 008 13.393a20.561 20.561 0 003.135-2.211C12.92 9.644 14.5 7.65 14.5 5.5c0-1.836-1.414-3-2.75-3-1.373 0-2.609.986-3.029 2.456a.75.75 0 01-1.442 0C6.859 3.486 5.623 2.5 4.25 2.5zM8 14.25l-.345.666-.002-.001-.006-.003-.018-.01a7.643 7.643 0 01-.31-.17 22.075 22.075 0 01-3.434-2.414C2.045 10.731 0 8.35 0 5.5 0 2.836 2.086 1 4.25 1 5.797 1 7.153 1.802 8 3.02 8.847 1.802 10.203 1 11.75 1 13.914 1 16 2.836 16 5.5c0 2.85-2.045 5.231-3.885 6.818a22.08 22.08 0 01-3.744 2.584l-.018.01-.006.003h-.002L8 14.25zm0 0l.345.666a.752.752 0 01-.69 0L8 14.25z"></path></svg>
|
||||||
|
Contributors
|
||||||
|
<% if (plugins.contributors.base || plugins.contributors.ref?.base?.abbreviatedOid) { %>
|
||||||
|
from <%= plugins.contributors.base || plugins.contributors.ref?.base?.abbreviatedOid %> to <%= plugins.contributors.head || plugins.contributors.ref?.head?.abbreviatedOid %>
|
||||||
|
<% } else if (plugins.contributors.head || plugins.contributors.ref?.head?.abbreviatedOid) { %>
|
||||||
|
of <%= plugins.contributors.head || plugins.contributors.ref?.head?.abbreviatedOid %>
|
||||||
|
<% } %>
|
||||||
|
</h2>
|
||||||
|
<section>
|
||||||
|
<div class="contributors fill-width">
|
||||||
|
<% if (plugins.contributors.error) { %>
|
||||||
|
<div class="field error">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M2.343 13.657A8 8 0 1113.657 2.343 8 8 0 012.343 13.657zM6.03 4.97a.75.75 0 00-1.06 1.06L6.94 8 4.97 9.97a.75.75 0 101.06 1.06L8 9.06l1.97 1.97a.75.75 0 101.06-1.06L9.06 8l1.97-1.97a.75.75 0 10-1.06-1.06L8 6.94 6.03 4.97z"></path></svg>
|
||||||
|
<%= plugins.contributors.error.message %>
|
||||||
|
</div>
|
||||||
|
<% } else { %>
|
||||||
|
<% for (const [login, {avatar}] of Object.entries(plugins.contributors.list)) { %>
|
||||||
|
<div class="label">
|
||||||
|
<img class="avatar" src="data:image/png;base64,<%= avatar %>" width="22" height="22" alt="" />
|
||||||
|
<%= login %>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
<% } %>
|
||||||
Reference in New Issue
Block a user