From 2c744122cab1cfe036fb78cd19f23e41b8c0b9c9 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 19 Apr 2026 15:44:52 +0300 Subject: [PATCH] chore: update contributors list --- contributors.json | 79 +----------- scripts/list-contributors.ts | 235 ++++++++++++++++++++++++----------- 2 files changed, 169 insertions(+), 145 deletions(-) diff --git a/contributors.json b/contributors.json index 86f480074c..5134dff233 100644 --- a/contributors.json +++ b/contributors.json @@ -1,5 +1,5 @@ { - "ℹ️": "Use `npm run list-contributors`. as a reference to update this list.", + "ℹ️": "Auto-generated file. Run `pnpm run list-contributors` to regenerate.", "contributors": [ { "name": "eliandoran", @@ -15,14 +15,17 @@ }, { "name": "adoriandoran", + "fullName": "Adorian Doran", "url": "https://github.com/adoriandoran" }, { "name": "perfectra1n", + "fullName": "Jon Fuller", "url": "https://github.com/perfectra1n" }, { "name": "pano9000", + "fullName": "Panagiotis Papadopoulos", "url": "https://github.com/pano9000" }, { @@ -33,81 +36,9 @@ "name": "JYC333", "url": "https://github.com/JYC333" }, - { - "name": "francistw", - "url": "https://github.com/francistw" - }, { "name": "Nriver", "url": "https://github.com/Nriver" - }, - { - "name": "thfrei", - "url": "https://github.com/thfrei" - }, - { - "name": "nathancahill", - "url": "https://github.com/nathancahill" - }, - { - "name": "contributor", - "url": "https://github.com/contributor" - }, - { - "name": "FliegendeWurst", - "url": "https://github.com/FliegendeWurst" - }, - { - "name": "hasecilu", - "url": "https://github.com/hasecilu" - }, - { - "name": "Meinzzzz", - "url": "https://github.com/Meinzzzz" - }, - { - "name": "Sarah-Hussein", - "url": "https://github.com/Sarah-Hussein" - }, - { - "name": "zerebos", - "url": "https://github.com/zerebos" - }, - { - "name": "meichthys", - "url": "https://github.com/meichthys" - }, - { - "name": "questamor", - "url": "https://github.com/questamor" - }, - { - "name": "SukantGujar", - "url": "https://github.com/SukantGujar" - }, - { - "name": "soulsands", - "url": "https://github.com/soulsands" - }, - { - "name": "noobhjy", - "url": "https://github.com/noobhjy" - }, - { - "name": "laurent22", - "url": "https://github.com/laurent22" - }, - { - "name": "mlewand", - "url": "https://github.com/mlewand" - }, - { - "name": "lzinga", - "url": "https://github.com/lzinga" - }, - { - "name": "ytrkptl", - "url": "https://github.com/ytrkptl" } ] -} \ No newline at end of file +} diff --git a/scripts/list-contributors.ts b/scripts/list-contributors.ts index 719898d679..c58766d5f6 100644 --- a/scripts/list-contributors.ts +++ b/scripts/list-contributors.ts @@ -1,30 +1,152 @@ -import { execSync} from "node:child_process"; +import { execSync } from "node:child_process"; +import { readFileSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; + +interface ContributorEntry { + name: string; + fullName?: string; + url: string; + role?: string; +} + +interface ContributorFile { + contributors: ContributorEntry[]; +} interface ContributorInfo { name: string; - fullName?: string + fullName?: string; email?: string; commitCount: number; - translationCommitCount?: number; - role?: string; url?: string; } -interface showTableParams { +interface ShowTableParams { title: string; comment?: string; contributors: ContributorInfo[]; columns: (keyof ContributorInfo)[]; } +const TRANSLATION_PATHS = [ + "apps/client/src/translations/", + "apps/server/src/assets/translations/" +]; + +/** Authors that are bots or automated tools, not real contributors. */ +const EXCLUDED_AUTHORS = new Set([ + "Languages add-on", + "Hosted Weblate", + "renovate[bot]" +]); + +const NOREPLY_PATTERN = /^(?:\d+\+)?(.+)@users\.noreply\.github\.com$/; + +/** + * Manual mapping for contributors whose git email doesn't reveal their + * GitHub username (i.e. no noreply email in .mailmap). + */ +const EMAIL_TO_GITHUB: Record = { + "contact@eliandoran.me": "eliandoran", + "zadam.apps@gmail.com": "zadam", + "adorian@esevo.ro": "adoriandoran", + "jonfuller2012@gmail.com": "perfectra1n", +}; + +const CONTRIBUTORS_PATH = join(__dirname, "..", "contributors.json"); + +/** + * Resolves a GitHub username from an email address. + * + * 1. Checks the manual mapping. + * 2. Extracts from GitHub noreply emails (e.g. "12345+user@…"). + * 3. Scans .mailmap for alternate emails that match the noreply pattern. + */ +function resolveGitHub(email: string, name: string): string | undefined { + if (EMAIL_TO_GITHUB[email]) return EMAIL_TO_GITHUB[email]; + + const noreply = email.match(NOREPLY_PATTERN); + if (noreply) return noreply[1]; + + // Grep .mailmap for alternate emails that match the noreply pattern + try { + const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const mailmapContent = execSync(`grep -i "${escapedName}" .mailmap 2>/dev/null`).toString(); + for (const line of mailmapContent.split("\n")) { + // Extract all emails from the line (inside angle brackets) + for (const [, email] of line.matchAll(/<([^>]+)>/g)) { + const match = email.match(NOREPLY_PATTERN); + if (match) return match[1]; + } + } + } catch { /* no matches */ } + + return undefined; +} + +function parseShortlog(rawOutput: string): Map { + const result = new Map(); + for (const line of rawOutput.split("\n")) { + const match = line.match(/^\s*(\d+)\s+(.+?)\s+<(.+)>$/); + if (match) { + result.set(match[2], { email: match[3], commitCount: parseInt(match[1]) }); + } + } + return result; +} + async function main() { - listLocalGitContributors(); + const { developers } = listLocalGitContributors(); await listGitHubContributors(); + updateContributorsJson(developers); +} + +function listLocalGitContributors() { + const allOutput = execSync("git shortlog -sne --no-merges HEAD -- src/ apps/").toString(); + const translationOutput = execSync(`git shortlog -sne --no-merges HEAD -- ${TRANSLATION_PATHS.join(" ")}`).toString(); + + const allContribs = parseShortlog(allOutput); + const translationContribs = parseShortlog(translationOutput); + + const developers: ContributorInfo[] = []; + const translators: ContributorInfo[] = []; + const MIN_COMMITS = 100; + for (const [name, { email, commitCount }] of allContribs) { + if (EXCLUDED_AUTHORS.has(name)) continue; + + const translationCommitCount = translationContribs.get(name)?.commitCount ?? 0; + const isTranslator = translationCommitCount > commitCount * 0.5; + + const githubUsername = resolveGitHub(email, name); + const url = githubUsername ? `https://github.com/${githubUsername}` : undefined; + const entry: ContributorInfo = { name, email, commitCount, url }; + + if (isTranslator) { + if (commitCount >= 20) translators.push(entry); + } else if (commitCount >= MIN_COMMITS) { + developers.push(entry); + } + } + + // showTable({ + // title: "Local Git Contributors (Developers)", + // columns: ["name", "url", "commitCount"], + // contributors: developers + // }); + + // showTable({ + // title: "Local Git Contributors (Translators)", + // comment: "Contributors where >50% of commits are to translation files.", + // columns: ["name", "url", "commitCount"], + // contributors: translators + // }); + + return { developers, translators }; } async function listGitHubContributors() { let list: any[] | null = null; - + const response = await fetch("https://api.github.com/repos/TriliumNext/Trilium/contributors"); if (response.ok) { list = await response.json(); @@ -47,79 +169,50 @@ async function listGitHubContributors() { } as ContributorInfo; }); - showTable({ - title: "GitHub Contributor List", - comment: "Note: the GitHub list also include contributors that did not directly contribute to Trilium, but to submodules used in the Trilium's repo.", - contributors: contributors, - columns: ["name", "url", "commitCount"] - }); + // showTable({ + // title: "GitHub Contributor List", + // comment: "Note: the GitHub list also include contributors that did not directly contribute to Trilium, but to submodules used in the Trilium's repo.", + // contributors: contributors, + // columns: ["name", "url", "commitCount"] + // }); } -const TRANSLATION_PATHS = [ - "apps/client/src/translations/", - "apps/server/src/assets/translations/" -]; +/** + * Updates contributors.json, preserving pinned entries (those with special + * roles like lead-dev, original-dev) and regenerating the rest from git data. + */ +function updateContributorsJson(developers: ContributorInfo[]) { + // Read existing file to preserve pinned entries + const existing: ContributorFile = JSON.parse(readFileSync(CONTRIBUTORS_PATH, "utf-8")); + const pinnedRoles = new Set(["lead-dev", "original-dev"]); + const pinned = existing.contributors.filter((c) => c.role && pinnedRoles.has(c.role)); -/** Authors that are bots or automated tools, not real contributors. */ -const EXCLUDED_AUTHORS = new Set([ - "Languages add-on", - "Hosted Weblate", - "renovate[bot]" -]); + // Build a set of pinned GitHub usernames to avoid duplicates + const pinnedNames = new Set(pinned.map((c) => c.name)); -function parseShortlog(rawOutput: string): Map { - const result = new Map(); - for (const line of rawOutput.split("\n")) { - const match = line.match(/^\s*(\d+)\s+(.+?)\s+<(.+)>$/); - if (match) { - result.set(match[2], { email: match[3], commitCount: parseInt(match[1]) }); - } - } - return result; -} + const contributors: ContributorEntry[] = [...pinned]; -function listLocalGitContributors() { - const allOutput = execSync("git shortlog -sne --no-merges HEAD -- src/ apps/").toString(); - const translationOutput = execSync(`git shortlog -sne --no-merges HEAD -- ${TRANSLATION_PATHS.join(" ")}`).toString(); + // Add developers (skip those already pinned) + for (const dev of developers) { + const githubName = dev.url?.replace("https://github.com/", ""); + if (!githubName || pinnedNames.has(githubName)) continue; - const allContribs = parseShortlog(allOutput); - const translationContribs = parseShortlog(translationOutput); - - const developers: ContributorInfo[] = []; - const translators: ContributorInfo[] = []; - let rank = 0; - for (const [name, { email, commitCount }] of allContribs) { - if (EXCLUDED_AUTHORS.has(name)) continue; - if (++rank > 20) break; - - const translationCommitCount = translationContribs.get(name)?.commitCount ?? 0; - const isTranslator = translationCommitCount > commitCount * 0.5; - - const entry: ContributorInfo = { name, email, commitCount }; - - if (isTranslator) { - translators.push(entry); - } else { - developers.push(entry); - } + contributors.push({ + name: githubName, + fullName: dev.name !== githubName ? dev.name : undefined, + url: dev.url! + }); } - showTable({ - title: "Local Git Contributors (Developers)", - comment: "", - columns: ["name", "email", "commitCount"], - contributors: developers - }); - - showTable({ - title: "Local Git Contributors (Translators)", - comment: "Contributors where >50% of commits are to translation files.", - columns: ["name", "email", "commitCount"], - contributors: translators - }); + const output = { + "ℹ️": "Auto-generated file. Run `pnpm run list-contributors` to regenerate.", + contributors + }; + writeFileSync(CONTRIBUTORS_PATH, JSON.stringify(output, null, 4) + "\n"); + console.log(`\n✅ Updated ${CONTRIBUTORS_PATH} with ${contributors.length} contributors.`); } -function showTable(params: showTableParams) { +function showTable(params: ShowTableParams) { console.log(`\n──── ${params.title} ────`); if (params.comment) { console.log(`\n${params.comment}\n`); @@ -127,4 +220,4 @@ function showTable(params: showTableParams) { console.table(params.contributors, params.columns); } -main(); \ No newline at end of file +main();