diff --git a/source/plugins/languages/analyzers.mjs b/source/plugins/languages/analyzers.mjs index 6a1c7c7e0a3..f8963ef58a3 100644 --- a/source/plugins/languages/analyzers.mjs +++ b/source/plugins/languages/analyzers.mjs @@ -1,7 +1,7 @@ import linguist from "linguist-js" /**Indepth analyzer */ -export async function indepth({login, data, imports, repositories}, {skipped, categories, timeout}) { +export async function indepth({login, data, imports, repositories, gpg}, {skipped, categories, timeout}) { return new Promise(async (solve, reject) => { //Timeout if (Number.isFinite(timeout)) { @@ -9,8 +9,31 @@ export async function indepth({login, data, imports, repositories}, {skipped, ca setTimeout(() => reject(`Reached maximum execution time of ${timeout}m for analysis`), timeout * 60 * 1000) } + //GPG keys imports + for (const {id, pub} of gpg) { + const path = imports.paths.join(imports.os.tmpdir(), `${data.user.databaseId}.${id}.gpg`) + console.debug(`metrics/compute/${login}/plugins > languages > saving gpg ${id} to ${path}`) + try { + await imports.fs.writeFile(path, pub) + if (process.env.GITHUB_ACTIONS) { + console.debug(`metrics/compute/${login}/plugins > languages > importing gpg ${id}`) + await imports.run(`gpg --import ${path}`) + } + else + console.debug(`metrics/compute/${login}/plugins > languages > skipping import of gpg ${id}`) + } + catch (error) { + console.debug(`metrics/compute/${login}/plugins > languages > indepth > an error occured while importing gpg ${id}, skipping...`) + } + finally { + //Cleaning + console.debug(`metrics/compute/${login}/plugins > languages > indepth > cleaning ${path}`) + await imports.fs.rm(path, {recursive:true, force:true}) + } + } + //Compute repositories stats from fetched repositories - const results = {total:0, lines:{}, stats:{}, colors:{}, commits:0, files:0, missed:0} + const results = {total:0, lines:{}, stats:{}, colors:{}, commits:0, files:0, missed:0, verified:{signature:0}} for (const repository of repositories) { //Skip repository if asked if ((skipped.includes(repository.name.toLocaleLowerCase())) || (skipped.includes(`${repository.owner.login}/${repository.name}`.toLocaleLowerCase()))) { @@ -170,6 +193,7 @@ async function analyze({login, imports, data}, {results, path, categories = ["pr console.debug(`metrics/compute/${login}/plugins > languages > indepth > repo seems empty or impossible to git log, skipping`) return } + const pending = [] for (let page = 0; ; page++) { try { console.debug(`metrics/compute/${login}/plugins > languages > indepth > processing commits ${page * per_page} from ${(page + 1) * per_page}`) @@ -182,6 +206,14 @@ async function analyze({login, imports, data}, {results, path, categories = ["pr empty = false //Commits counter if (/^commit [0-9a-f]{40}$/.test(line)) { + if (results.verified) { + const sha = line.match(/[0-9a-f]{40}/)?.[0] + if (sha) { + pending.push(imports.run(`git verify-commit ${sha}`, {cwd:path, env:{LANG:"en_GB"}}, {log:false, prefixed:false}) + .then(() => results.verified.signature++) + .catch(() => null)) + } + } results.commits++ return } @@ -223,6 +255,7 @@ async function analyze({login, imports, data}, {results, path, categories = ["pr results.missed += per_page } } + await Promise.allSettled(pending) results.files += edited.size } diff --git a/source/plugins/languages/index.mjs b/source/plugins/languages/index.mjs index 5263e10f4e6..6e49ce06705 100644 --- a/source/plugins/languages/index.mjs +++ b/source/plugins/languages/index.mjs @@ -78,10 +78,29 @@ export default async function({login, data, imports, q, rest, account}, {enabled //Indepth mode if (indepth) { + //Fetch gpg keys (web-flow is GitHub's public key when making changes from web ui) + const gpg = [] + try { + for (const username of [login, "web-flow"]) { + const {data:keys} = await rest.users.listGpgKeysForUser({username}) + gpg.push(...keys.map(({key_id:id, raw_key:pub, emails}) => ({id, pub, emails}))) + if (username === login) { + for (const {email} of gpg.flatMap(({emails}) => emails)) { + console.debug(`metrics/compute/${login}/plugins > languages > auto-adding ${email} to commits_authoring (fetched from gpg)`) + data.shared["commits.authoring"].push(email) + } + } + } + } + catch (error) { + console.debug(`metrics/compute/${login}/plugins > languages > ${error}`) + } + + //Analyze languages try { console.debug(`metrics/compute/${login}/plugins > languages > switching to indepth mode (this may take some time)`) const existingColors = languages.colors - Object.assign(languages, await indepth_analyzer({login, data, imports, repositories}, {skipped, categories, timeout})) + Object.assign(languages, await indepth_analyzer({login, data, imports, repositories, gpg}, {skipped, categories, timeout})) Object.assign(languages.colors, existingColors) console.debug(`metrics/compute/${login}/plugins > languages > indepth analysis missed ${languages.missed} commits`) } diff --git a/source/templates/classic/partials/languages.ejs b/source/templates/classic/partials/languages.ejs index b319b1a8647..520a5a8be2b 100644 --- a/source/templates/classic/partials/languages.ejs +++ b/source/templates/classic/partials/languages.ejs @@ -66,6 +66,14 @@ <% } %> <% } %> + <% if (plugins.languages.verified?.signature) { %> +
+
+ + <%= plugins.languages.verified.signature %> commit<%= s(plugins.languages.verified.signature) %> verified by GPG +
+
+ <% } %> <% } %> <% } %> diff --git a/source/templates/classic/style.css b/source/templates/classic/style.css index dde098b88c6..e61c8caf370 100644 --- a/source/templates/classic/style.css +++ b/source/templates/classic/style.css @@ -271,6 +271,12 @@ margin-right: 6px; } + .footnote { + width: 100%; + justify-content: flex-end; + font-size: 12px; + } + /* Follow-up */ .followup.legend { font-size: 12px; diff --git a/tests/mocks/api/github/rest/users/listGpgKeysForUser.mjs b/tests/mocks/api/github/rest/users/listGpgKeysForUser.mjs new file mode 100644 index 00000000000..1103b2f6923 --- /dev/null +++ b/tests/mocks/api/github/rest/users/listGpgKeysForUser.mjs @@ -0,0 +1,25 @@ +/**Mocked data */ +export default function({ faker }, target, that, [{ username }]) { + console.debug("metrics/compute/mocks > mocking rest api result > rest.users.listGpgKeysForUser") + return ({ + status: 200, + url: `https://api.github.com/users/${username}/`, + headers: { + server: "GitHub.com", + status: "200 OK", + "x-oauth-scopes": "repo", + }, + data: [ + { + key_id: faker.datatype.hexaDecimal(16), + raw_key: "-----BEGIN PGP PUBLIC KEY BLOCK-----\n(dummy content)\n-----END PGP PUBLIC KEY BLOCK-----", + emails: [ + { + email: faker.internet.email(), + verified: true + } + ] + } + ], + }) +}