diff --git a/package-lock.json b/package-lock.json
index 71ebae22..58c64e6e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -28,6 +28,7 @@
"linguist-js": "^1.4.3",
"marked": "^2.1.3",
"memory-cache": "^0.2.0",
+ "minimatch": "^3.0.4",
"node-chartist": "^1.0.5",
"node-fetch": "^2.6.1",
"open-graph-scraper": "^4.9.0",
diff --git a/package.json b/package.json
index b711c692..cf281a01 100644
--- a/package.json
+++ b/package.json
@@ -44,6 +44,7 @@
"linguist-js": "^1.4.3",
"marked": "^2.1.3",
"memory-cache": "^0.2.0",
+ "minimatch": "^3.0.4",
"node-chartist": "^1.0.5",
"node-fetch": "^2.6.1",
"open-graph-scraper": "^4.9.0",
diff --git a/source/app/metrics/utils.mjs b/source/app/metrics/utils.mjs
index b9d44562..6872cc7a 100644
--- a/source/app/metrics/utils.mjs
+++ b/source/app/metrics/utils.mjs
@@ -23,10 +23,11 @@ import util from "util"
import fetch from "node-fetch"
import readline from "readline"
import emoji from "emoji-name-map"
+import minimatch from "minimatch"
prism_lang()
//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 */
export function __module(module) {
diff --git a/source/plugins/contributors/README.md b/source/plugins/contributors/README.md
index b38c5bbf..bcc5f86a 100644
--- a/source/plugins/contributors/README.md
+++ b/source/plugins/contributors/README.md
@@ -6,7 +6,10 @@ It's especially useful to acknowledge contributors on release notes.
-
+
+ Raw list with names
+
+
With number of contributions
@@ -14,6 +17,24 @@ It's especially useful to acknowledge contributors on release notes.
|
+**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
[➡️ Available options for this plugin](metadata.yml)
@@ -23,8 +44,9 @@ It's especially useful to acknowledge contributors on release notes.
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.)
- plugin_contributors_ignored: bot # Ignore "bot" user
- plugin_contributors_contributions: yes # Display number of contributions for each contributor
+ plugin_contributors_base: "" # Base reference (commit, tag, branch, etc.)
+ plugin_contributors_head: main # Head reference (commit, tag, branch, etc.)
+ plugin_contributors_ignored: bot # Ignore "bot" user
+ plugin_contributors_contributions: yes # Display number of contributions for each contributor
+ plugin_contributors_sections: contributors # Display contributors sections
```
\ No newline at end of file
diff --git a/source/plugins/contributors/index.mjs b/source/plugins/contributors/index.mjs
index 390dd932..c7ad5082 100644
--- a/source/plugins/contributors/index.mjs
+++ b/source/plugins/contributors/index.mjs
@@ -1,5 +1,5 @@
//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
try {
//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
//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}
//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))
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
- return {head, base, ref, list:contributors, contributions}
+ return {head, base, ref, list:contributors, categories:types, contributions, sections}
}
//Handle errors
catch (error) {
diff --git a/source/plugins/contributors/metadata.yml b/source/plugins/contributors/metadata.yml
index 92f68b93..0be209bc 100644
--- a/source/plugins/contributors/metadata.yml
+++ b/source/plugins/contributors/metadata.yml
@@ -37,3 +37,30 @@ inputs:
description: Display contributions
type: boolean
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": ["*"]
+ }
diff --git a/source/templates/classic/style.css b/source/templates/classic/style.css
index d4351292..c9006f08 100644
--- a/source/templates/classic/style.css
+++ b/source/templates/classic/style.css
@@ -920,6 +920,12 @@
width: .8rem;
height: .8rem;
}
+ .field.contributors-category {
+ margin-left: 12px;
+ }
+ .contributors-categories img {
+ margin-right: 0;
+ }
/* Introduction */
.introduction {
diff --git a/source/templates/repository/partials/contributors.ejs b/source/templates/repository/partials/contributors.ejs
index c6b60b5a..3e92a180 100644
--- a/source/templates/repository/partials/contributors.ejs
+++ b/source/templates/repository/partials/contributors.ejs
@@ -9,25 +9,44 @@
of <%= plugins.contributors.head || plugins.contributors.ref?.head?.abbreviatedOid %>
<% } %>
-
-
- <% if (plugins.contributors.error) { %>
+ <% if (plugins.contributors.error) { %>
+
+
<%= plugins.contributors.error.message %>
- <% } else { %>
- <% for (const [login, {avatar, contributions}] of Object.entries(plugins.contributors.list)) { %>
-
-

- <%= login %>
- <% if (plugins.contributors.contributions) { %>
-
+
+
+ <% } else { %>
+ <% if (plugins.contributors.sections?.includes("contributors")) { %>
+
+
+ <% for (const [login, {avatar, contributions}] of Object.entries(plugins.contributors.list)) { %>
+
+

+ <%= login %>
+ <% if (plugins.contributors.contributions) { %>
+
+ <% } %>
+
+ <% } %>
+
+
+ <% } %>
+ <% if (plugins.contributors.sections?.includes("categories")) { %>
+
+ <% for (const [category, contributors] of Object.entries(plugins.contributors.categories)) { %>
+ <% if (!contributors.size) continue %>
+ <%= category %>
+
+ <% for (const contributor of contributors) { %>
+

<% } %>
<% } %>
- <% } %>
-
-
+
+ <% } %>
+ <% } %>
<% } %>