feat(plugins/languages): count verified commits using user's gpg keys (#911)
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import linguist from "linguist-js"
|
import linguist from "linguist-js"
|
||||||
|
|
||||||
/**Indepth analyzer */
|
/**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) => {
|
return new Promise(async (solve, reject) => {
|
||||||
//Timeout
|
//Timeout
|
||||||
if (Number.isFinite(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)
|
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
|
//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) {
|
for (const repository of repositories) {
|
||||||
//Skip repository if asked
|
//Skip repository if asked
|
||||||
if ((skipped.includes(repository.name.toLocaleLowerCase())) || (skipped.includes(`${repository.owner.login}/${repository.name}`.toLocaleLowerCase()))) {
|
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`)
|
console.debug(`metrics/compute/${login}/plugins > languages > indepth > repo seems empty or impossible to git log, skipping`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
const pending = []
|
||||||
for (let page = 0; ; page++) {
|
for (let page = 0; ; page++) {
|
||||||
try {
|
try {
|
||||||
console.debug(`metrics/compute/${login}/plugins > languages > indepth > processing commits ${page * per_page} from ${(page + 1) * per_page}`)
|
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
|
empty = false
|
||||||
//Commits counter
|
//Commits counter
|
||||||
if (/^commit [0-9a-f]{40}$/.test(line)) {
|
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++
|
results.commits++
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -223,6 +255,7 @@ async function analyze({login, imports, data}, {results, path, categories = ["pr
|
|||||||
results.missed += per_page
|
results.missed += per_page
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
await Promise.allSettled(pending)
|
||||||
results.files += edited.size
|
results.files += edited.size
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -78,10 +78,29 @@ export default async function({login, data, imports, q, rest, account}, {enabled
|
|||||||
|
|
||||||
//Indepth mode
|
//Indepth mode
|
||||||
if (indepth) {
|
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 {
|
try {
|
||||||
console.debug(`metrics/compute/${login}/plugins > languages > switching to indepth mode (this may take some time)`)
|
console.debug(`metrics/compute/${login}/plugins > languages > switching to indepth mode (this may take some time)`)
|
||||||
const existingColors = languages.colors
|
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)
|
Object.assign(languages.colors, existingColors)
|
||||||
console.debug(`metrics/compute/${login}/plugins > languages > indepth analysis missed ${languages.missed} commits`)
|
console.debug(`metrics/compute/${login}/plugins > languages > indepth analysis missed ${languages.missed} commits`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,6 +66,14 @@
|
|||||||
<% } %>
|
<% } %>
|
||||||
</div>
|
</div>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
<% if (plugins.languages.verified?.signature) { %>
|
||||||
|
<div class="row footnote">
|
||||||
|
<div class="field">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M9.585.52a2.678 2.678 0 00-3.17 0l-.928.68a1.178 1.178 0 01-.518.215L3.83 1.59a2.678 2.678 0 00-2.24 2.24l-.175 1.14a1.178 1.178 0 01-.215.518l-.68.928a2.678 2.678 0 000 3.17l.68.928c.113.153.186.33.215.518l.175 1.138a2.678 2.678 0 002.24 2.24l1.138.175c.187.029.365.102.518.215l.928.68a2.678 2.678 0 003.17 0l.928-.68a1.17 1.17 0 01.518-.215l1.138-.175a2.678 2.678 0 002.241-2.241l.175-1.138c.029-.187.102-.365.215-.518l.68-.928a2.678 2.678 0 000-3.17l-.68-.928a1.179 1.179 0 01-.215-.518L14.41 3.83a2.678 2.678 0 00-2.24-2.24l-1.138-.175a1.179 1.179 0 01-.518-.215L9.585.52zM7.303 1.728c.415-.305.98-.305 1.394 0l.928.68c.348.256.752.423 1.18.489l1.136.174c.51.078.909.478.987.987l.174 1.137c.066.427.233.831.489 1.18l.68.927c.305.415.305.98 0 1.394l-.68.928a2.678 2.678 0 00-.489 1.18l-.174 1.136a1.178 1.178 0 01-.987.987l-1.137.174a2.678 2.678 0 00-1.18.489l-.927.68c-.415.305-.98.305-1.394 0l-.928-.68a2.678 2.678 0 00-1.18-.489l-1.136-.174a1.178 1.178 0 01-.987-.987l-.174-1.137a2.678 2.678 0 00-.489-1.18l-.68-.927a1.178 1.178 0 010-1.394l.68-.928c.256-.348.423-.752.489-1.18l.174-1.136c.078-.51.478-.909.987-.987l1.137-.174a2.678 2.678 0 001.18-.489l.927-.68zM11.28 6.78a.75.75 0 00-1.06-1.06L7 8.94 5.78 7.72a.75.75 0 00-1.06 1.06l1.75 1.75a.75.75 0 001.06 0l3.75-3.75z"></path></svg>
|
||||||
|
<%= plugins.languages.verified.signature %> commit<%= s(plugins.languages.verified.signature) %> verified by GPG
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
<% } %>
|
<% } %>
|
||||||
</section>
|
</section>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|||||||
@@ -271,6 +271,12 @@
|
|||||||
margin-right: 6px;
|
margin-right: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.footnote {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-end;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Follow-up */
|
/* Follow-up */
|
||||||
.followup.legend {
|
.followup.legend {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|||||||
25
tests/mocks/api/github/rest/users/listGpgKeysForUser.mjs
Normal file
25
tests/mocks/api/github/rest/users/listGpgKeysForUser.mjs
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user