From 26769541942aff957ee97dc3d719d90c0574c20a Mon Sep 17 00:00:00 2001 From: Simon Lecoq <22963968+lowlighter@users.noreply.github.com> Date: Mon, 2 Aug 2021 13:41:03 +0200 Subject: [PATCH] Contributors plugin: Display contributors per contribution category (#443) --- package-lock.json | 1 + package.json | 1 + source/app/metrics/utils.mjs | 3 +- source/plugins/contributors/README.md | 32 +++++++++-- source/plugins/contributors/index.mjs | 55 ++++++++++++++++++- source/plugins/contributors/metadata.yml | 27 +++++++++ source/templates/classic/style.css | 6 ++ .../repository/partials/contributors.ejs | 45 ++++++++++----- 8 files changed, 148 insertions(+), 22 deletions(-) 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) { %> -
<%= contributions %>
+
+
+ <% } else { %> + <% if (plugins.contributors.sections?.includes("contributors")) { %> +
+
+ <% for (const [login, {avatar, contributions}] of Object.entries(plugins.contributors.list)) { %> +
+ + <%= login %> + <% if (plugins.contributors.contributions) { %> +
<%= 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) { %> + <% } %>
<% } %> - <% } %> -
-
+ + <% } %> + <% } %> <% } %>