Contributors plugin: Display contributors per contribution category (#443)
This commit is contained in:
1
package-lock.json
generated
1
package-lock.json
generated
@@ -28,6 +28,7 @@
|
|||||||
"linguist-js": "^1.4.3",
|
"linguist-js": "^1.4.3",
|
||||||
"marked": "^2.1.3",
|
"marked": "^2.1.3",
|
||||||
"memory-cache": "^0.2.0",
|
"memory-cache": "^0.2.0",
|
||||||
|
"minimatch": "^3.0.4",
|
||||||
"node-chartist": "^1.0.5",
|
"node-chartist": "^1.0.5",
|
||||||
"node-fetch": "^2.6.1",
|
"node-fetch": "^2.6.1",
|
||||||
"open-graph-scraper": "^4.9.0",
|
"open-graph-scraper": "^4.9.0",
|
||||||
|
|||||||
@@ -44,6 +44,7 @@
|
|||||||
"linguist-js": "^1.4.3",
|
"linguist-js": "^1.4.3",
|
||||||
"marked": "^2.1.3",
|
"marked": "^2.1.3",
|
||||||
"memory-cache": "^0.2.0",
|
"memory-cache": "^0.2.0",
|
||||||
|
"minimatch": "^3.0.4",
|
||||||
"node-chartist": "^1.0.5",
|
"node-chartist": "^1.0.5",
|
||||||
"node-fetch": "^2.6.1",
|
"node-fetch": "^2.6.1",
|
||||||
"open-graph-scraper": "^4.9.0",
|
"open-graph-scraper": "^4.9.0",
|
||||||
|
|||||||
@@ -23,10 +23,11 @@ import util from "util"
|
|||||||
import fetch from "node-fetch"
|
import fetch from "node-fetch"
|
||||||
import readline from "readline"
|
import readline from "readline"
|
||||||
import emoji from "emoji-name-map"
|
import emoji from "emoji-name-map"
|
||||||
|
import minimatch from "minimatch"
|
||||||
prism_lang()
|
prism_lang()
|
||||||
|
|
||||||
//Exports
|
//Exports
|
||||||
export {axios, fs, git, jimp, opengraph, os, paths, processes, rss, url, fetch, util, emoji}
|
export {axios, fs, git, jimp, opengraph, os, paths, processes, rss, url, fetch, util, emoji, minimatch}
|
||||||
|
|
||||||
/**Returns module __dirname */
|
/**Returns module __dirname */
|
||||||
export function __module(module) {
|
export function __module(module) {
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ It's especially useful to acknowledge contributors on release notes.
|
|||||||
|
|
||||||
<table>
|
<table>
|
||||||
<td align="center">
|
<td align="center">
|
||||||
|
<img src="https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.contributors.categories.svg">
|
||||||
|
<details><summary>Raw list with names</summary>
|
||||||
<img src="https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.contributors.svg">
|
<img src="https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.contributors.svg">
|
||||||
|
</details>
|
||||||
<details><summary>With number of contributions</summary>
|
<details><summary>With number of contributions</summary>
|
||||||
<img src="https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.contributors.contributions.svg">
|
<img src="https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.contributors.contributions.svg">
|
||||||
</details>
|
</details>
|
||||||
@@ -14,6 +17,24 @@ It's especially useful to acknowledge contributors on release notes.
|
|||||||
</td>
|
</td>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
**Displaying contributors per categories**
|
||||||
|
|
||||||
|
> 🔣 On web instances, sorting contributors per categories is an extra feature and must be enabled globally in `settings.json`
|
||||||
|
|
||||||
|
To configure contributions categories, pass a JSON object to `plugin_contributors_categories` (use `|` multiline operator for better readability) with categories names as keys and an array of file glob as values:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
plugin_contributors_categories: |
|
||||||
|
{
|
||||||
|
"📚 Documentation": ["README.md", "docs/**"],
|
||||||
|
"💻 Code": ["source/**", "src/**"],
|
||||||
|
"#️⃣ Others": ["*"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Each time a file modified by a contributor match a fileglob, they will be added in said category.
|
||||||
|
Matching is performed in keys order.
|
||||||
|
|
||||||
#### ℹ️ Examples workflows
|
#### ℹ️ Examples workflows
|
||||||
|
|
||||||
[➡️ Available options for this plugin](metadata.yml)
|
[➡️ Available options for this plugin](metadata.yml)
|
||||||
@@ -24,7 +45,8 @@ It's especially useful to acknowledge contributors on release notes.
|
|||||||
# ... other options
|
# ... other options
|
||||||
plugin_contributors: yes
|
plugin_contributors: yes
|
||||||
plugin_contributors_base: "" # Base reference (commit, tag, branch, etc.)
|
plugin_contributors_base: "" # Base reference (commit, tag, branch, etc.)
|
||||||
plugin_contributors_head: master # Head reference (commit, tag, branch, etc.)
|
plugin_contributors_head: main # Head reference (commit, tag, branch, etc.)
|
||||||
plugin_contributors_ignored: bot # Ignore "bot" user
|
plugin_contributors_ignored: bot # Ignore "bot" user
|
||||||
plugin_contributors_contributions: yes # Display number of contributions for each contributor
|
plugin_contributors_contributions: yes # Display number of contributions for each contributor
|
||||||
|
plugin_contributors_sections: contributors # Display contributors sections
|
||||||
```
|
```
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
//Setup
|
//Setup
|
||||||
export default async function({login, q, imports, data, rest, graphql, queries, account}, {enabled = false} = {}) {
|
export default async function({login, q, imports, data, rest, graphql, queries, account}, {enabled = false, extras = false} = {}) {
|
||||||
//Plugin execution
|
//Plugin execution
|
||||||
try {
|
try {
|
||||||
//Check if plugin is enabled and requirements are met
|
//Check if plugin is enabled and requirements are met
|
||||||
@@ -7,7 +7,7 @@ export default async function({login, q, imports, data, rest, graphql, queries,
|
|||||||
return null
|
return null
|
||||||
|
|
||||||
//Load inputs
|
//Load inputs
|
||||||
let {head, base, ignored, contributions} = imports.metadata.plugins.contributors.inputs({data, account, q})
|
let {head, base, ignored, contributions, sections, categories} = imports.metadata.plugins.contributors.inputs({data, account, q})
|
||||||
const repo = {owner:data.repo.owner.login, repo:data.repo.name}
|
const repo = {owner:data.repo.owner.login, repo:data.repo.name}
|
||||||
|
|
||||||
//Retrieve head and base commits
|
//Retrieve head and base commits
|
||||||
@@ -67,8 +67,57 @@ export default async function({login, q, imports, data, rest, graphql, queries,
|
|||||||
for (const contributor of Object.values(contributors))
|
for (const contributor of Object.values(contributors))
|
||||||
contributor.pr = [...new Set(contributor.pr)]
|
contributor.pr = [...new Set(contributor.pr)]
|
||||||
|
|
||||||
|
//Contributions categories
|
||||||
|
const types = Object.fromEntries([...new Set(Object.keys(categories))].map(type => [type, new Set()]))
|
||||||
|
if ((sections.includes("categories"))&&(extras)) {
|
||||||
|
//Temporary directory
|
||||||
|
const repository = `${repo.owner}/${repo.repo}`
|
||||||
|
const path = imports.paths.join(imports.os.tmpdir(), `${repository.replace(/[^\w]/g, "_")}`)
|
||||||
|
console.debug(`metrics/compute/${login}/plugins > contributors > cloning ${repository} to temp dir ${path}`)
|
||||||
|
|
||||||
|
try {
|
||||||
|
//Git clone into temporary directory
|
||||||
|
await imports.fs.rm(path, {recursive:true, force:true})
|
||||||
|
await imports.fs.mkdir(path, {recursive:true})
|
||||||
|
const git = await imports.git(path)
|
||||||
|
await git.clone(`https://github.com/${repository}`, ".").status()
|
||||||
|
|
||||||
|
//Analyze contributors' contributions
|
||||||
|
for (const contributor in contributors) {
|
||||||
|
//Load edited files by contributor
|
||||||
|
const files = []
|
||||||
|
await imports.spawn("git", ["--no-pager", "log", `--author="${contributor}"`, "--regexp-ignore-case", "--no-merges", "--name-only", '--pretty=format:""'], {cwd:path}, {
|
||||||
|
stdout(line) {
|
||||||
|
if (line.trim().length)
|
||||||
|
files.push(line)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
//Search for contributions type in specified categories
|
||||||
|
filesloop: for (const file of files) {
|
||||||
|
for (const [category, globs] of Object.entries(categories)) {
|
||||||
|
for (const glob of [globs].flat(Infinity)) {
|
||||||
|
if (imports.minimatch(file, glob, {nocase:true})) {
|
||||||
|
types[category].add(contributor)
|
||||||
|
continue filesloop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.debug(error)
|
||||||
|
console.debug(`metrics/compute/${login}/plugins > contributors > an error occured while processing ${repository}`)
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
//Cleaning
|
||||||
|
console.debug(`metrics/compute/${login}/plugins > contributors > cleaning temp dir ${path}`)
|
||||||
|
await imports.fs.rm(path, {recursive:true, force:true})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//Results
|
//Results
|
||||||
return {head, base, ref, list:contributors, contributions}
|
return {head, base, ref, list:contributors, categories:types, contributions, sections}
|
||||||
}
|
}
|
||||||
//Handle errors
|
//Handle errors
|
||||||
catch (error) {
|
catch (error) {
|
||||||
|
|||||||
@@ -37,3 +37,30 @@ inputs:
|
|||||||
description: Display contributions
|
description: Display contributions
|
||||||
type: boolean
|
type: boolean
|
||||||
default: no
|
default: no
|
||||||
|
|
||||||
|
# Sections to display
|
||||||
|
plugin_contributors_sections:
|
||||||
|
description: Sections to display
|
||||||
|
type: array
|
||||||
|
format: comma-separated
|
||||||
|
default: contributors
|
||||||
|
example: contributors
|
||||||
|
values:
|
||||||
|
- contributors # Display all contributors
|
||||||
|
- categories # Display contributors per contributions categories
|
||||||
|
|
||||||
|
# Contributions categories
|
||||||
|
# This requires "plugin_contributors_sections" to have "categories" in it to be effective
|
||||||
|
#
|
||||||
|
# Pass a JSON object which contains a mapping of category with fileglobs.
|
||||||
|
# Contributors will be sorted into each category to reflect their contributions.
|
||||||
|
# Note that order a file will only match the first category matching
|
||||||
|
plugin_contributors_categories:
|
||||||
|
description: Contributions categories
|
||||||
|
type: json
|
||||||
|
default: |
|
||||||
|
{
|
||||||
|
"📚 Documentation": ["README.md", "docs/**"],
|
||||||
|
"💻 Code": ["source/**", "src/**"],
|
||||||
|
"#️⃣ Others": ["*"]
|
||||||
|
}
|
||||||
|
|||||||
@@ -920,6 +920,12 @@
|
|||||||
width: .8rem;
|
width: .8rem;
|
||||||
height: .8rem;
|
height: .8rem;
|
||||||
}
|
}
|
||||||
|
.field.contributors-category {
|
||||||
|
margin-left: 12px;
|
||||||
|
}
|
||||||
|
.contributors-categories img {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* Introduction */
|
/* Introduction */
|
||||||
.introduction {
|
.introduction {
|
||||||
|
|||||||
@@ -9,14 +9,19 @@
|
|||||||
of <%= plugins.contributors.head || plugins.contributors.ref?.head?.abbreviatedOid %>
|
of <%= plugins.contributors.head || plugins.contributors.ref?.head?.abbreviatedOid %>
|
||||||
<% } %>
|
<% } %>
|
||||||
</h2>
|
</h2>
|
||||||
|
<% if (plugins.contributors.error) { %>
|
||||||
<section>
|
<section>
|
||||||
<div class="contributors fill-width">
|
<div class="contributors fill-width">
|
||||||
<% if (plugins.contributors.error) { %>
|
|
||||||
<div class="field 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>
|
<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 %>
|
<%= plugins.contributors.error.message %>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
|
<% if (plugins.contributors.sections?.includes("contributors")) { %>
|
||||||
|
<section>
|
||||||
|
<div class="contributors fill-width">
|
||||||
<% for (const [login, {avatar, contributions}] of Object.entries(plugins.contributors.list)) { %>
|
<% for (const [login, {avatar, contributions}] of Object.entries(plugins.contributors.list)) { %>
|
||||||
<div class="label">
|
<div class="label">
|
||||||
<img class="avatar" src="<%= avatar %>" width="22" height="22" alt="" />
|
<img class="avatar" src="<%= avatar %>" width="22" height="22" alt="" />
|
||||||
@@ -26,8 +31,22 @@
|
|||||||
<% } %>
|
<% } %>
|
||||||
</div>
|
</div>
|
||||||
<% } %>
|
<% } %>
|
||||||
<% } %>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
<% } %>
|
||||||
|
<% if (plugins.contributors.sections?.includes("categories")) { %>
|
||||||
|
<section>
|
||||||
|
<% for (const [category, contributors] of Object.entries(plugins.contributors.categories)) { %>
|
||||||
|
<% if (!contributors.size) continue %>
|
||||||
|
<h3 class="field contributors-category"><%= category %></h3>
|
||||||
|
<div class="contributors contributors-categories fill-width">
|
||||||
|
<% for (const contributor of contributors) { %>
|
||||||
|
<img class="avatar" src="<%= plugins.contributors.list[contributor].avatar %>" width="22" height="22" alt="" />
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
</section>
|
||||||
|
<% } %>
|
||||||
|
<% } %>
|
||||||
</section>
|
</section>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|||||||
Reference in New Issue
Block a user