Commit a029e66

Anton Medvedev <anton@medv.io>
2023-10-12 00:02:02
Add badge tiers
1 parent 19937a2
Changed files (18)
src/all-badges/abc-commit/abc-commit.ts
@@ -1,17 +1,17 @@
 import { Commit, Repo } from '../../collect/collect.js'
 import { BadgePresenter, ID, Present } from '../../badges.js'
 
-export default new (class implements BadgePresenter {
+export default new (class extends BadgePresenter {
   url = new URL(import.meta.url)
+  tiers = true
   badges = [
-    'abcdef-commit',
-    'abcde-commit',
-    'abcd-commit',
-    'abc-commit',
-    'ab-commit',
     'a-commit',
+    'ab-commit',
+    'abc-commit',
+    'abcd-commit',
+    'abcde-commit',
+    'abcdef-commit',
   ] as const
-  tiers = true as const
   present: Present = (data, grant) => {
     const types: [string, ID][] = [
       ['abcdef', 'abcdef-commit'],
@@ -29,10 +29,9 @@ export default new (class implements BadgePresenter {
           const re = new RegExp(`^(${prefix})`)
           if (re.test(commit.sha)) {
             order[prefix.length] = () =>
-              grant(
-                badge,
-                `One of my commit sha starts with "${prefix}".`,
-              ).evidence(link(re, repo, commit))
+              grant(badge, `One of my commit sha starts with "${prefix}".`)
+                .evidence(link(re, repo, commit))
+                .tier(prefix.length)
             break
           }
         }
src/all-badges/bad-words/bad-words.ts
@@ -2,7 +2,7 @@ import { Commit, Repo } from '../../collect/collect.js'
 import { Present, BadgePresenter } from '../../badges.js'
 import { linkCommit } from '../../utils.js'
 
-export default new (class implements BadgePresenter {
+export default new (class extends BadgePresenter {
   url = new URL(import.meta.url)
   badges = ['bad-words'] as const
   present: Present = (data, grant) => {
src/all-badges/chore-commit/chore-commit.ts
@@ -1,6 +1,6 @@
 import { BadgePresenter, Present } from '../../badges.js'
 
-export default new (class implements BadgePresenter {
+export default new (class extends BadgePresenter {
   url = new URL(import.meta.url)
   badges = ['chore-commit'] as const
   tiers = false
src/all-badges/covid-19/covid-19.ts
@@ -1,6 +1,6 @@
 import { BadgePresenter, Present } from '../../badges.js'
 
-export default new (class implements BadgePresenter {
+export default new (class extends BadgePresenter {
   url = new URL(import.meta.url)
   badges = ['covid-19'] as const
   present: Present = (data, grant) => {
src/all-badges/dead-commit/dead-commit.ts
@@ -1,7 +1,7 @@
 import { Commit, Repo } from '../../collect/collect.js'
 import { BadgePresenter, Present } from '../../badges.js'
 
-export default new (class implements BadgePresenter {
+export default new (class extends BadgePresenter {
   url = new URL(import.meta.url)
   badges = ['dead-commit'] as const
   present: Present = (data, grant) => {
src/all-badges/delorean/delorean.ts
@@ -1,6 +1,6 @@
 import { BadgePresenter, Present } from '../../badges.js'
 
-export default new (class implements BadgePresenter {
+export default new (class extends BadgePresenter {
   url = new URL(import.meta.url)
   badges = ['delorean'] as const
   present: Present = (data, grant) => {
src/all-badges/fix-commit/fix-commit.ts
@@ -1,8 +1,9 @@
 import { BadgePresenter, Grant, Present } from '../../badges.js'
 import { Commit } from '../../collect/collect.js'
 
-export default new (class implements BadgePresenter {
+export default new (class extends BadgePresenter {
   url = new URL(import.meta.url)
+  tiers = true
   badges = [
     'fix-2',
     'fix-3',
@@ -11,7 +12,6 @@ export default new (class implements BadgePresenter {
     'fix-6',
     'fix-6+', // For more than 6
   ] as const
-  tiers = true
   present: Present = (data, grant) => {
     for (const repo of data.repos) {
       const sequences: Commit[][] = []
@@ -48,17 +48,29 @@ export default new (class implements BadgePresenter {
         const description = `I did ${count} sequential fixes.`
 
         if (count === 2)
-          grant('fix-2', description).evidenceCommitsWithMessage(...sec)
+          grant('fix-2', description)
+            .evidenceCommitsWithMessage(...sec)
+            .tier(1)
         else if (count === 3)
-          grant('fix-3', description).evidenceCommitsWithMessage(...sec)
+          grant('fix-3', description)
+            .evidenceCommitsWithMessage(...sec)
+            .tier(2)
         else if (count === 4)
-          grant('fix-4', description).evidenceCommitsWithMessage(...sec)
+          grant('fix-4', description)
+            .evidenceCommitsWithMessage(...sec)
+            .tier(3)
         else if (count === 5)
-          grant('fix-5', description).evidenceCommitsWithMessage(...sec)
+          grant('fix-5', description)
+            .evidenceCommitsWithMessage(...sec)
+            .tier(4)
         else if (count === 6)
-          grant('fix-6', description).evidenceCommitsWithMessage(...sec)
+          grant('fix-6', description)
+            .evidenceCommitsWithMessage(...sec)
+            .tier(5)
         else if (count > 6)
-          grant('fix-6+', description).evidenceCommitsWithMessage(...sec)
+          grant('fix-6+', description)
+            .evidenceCommitsWithMessage(...sec)
+            .tier(6)
       }
     }
   }
src/all-badges/github-anniversary/github-anniversary.ts
@@ -1,6 +1,6 @@
 import { BadgePresenter, Present } from '../../badges.js'
 
-export default new (class implements BadgePresenter {
+export default new (class extends BadgePresenter {
   url = new URL(import.meta.url)
   badges = [
     'github-anniversary-5',
src/all-badges/mass-delete-commit/mass-delete-commit.ts
@@ -1,12 +1,9 @@
 import { BadgePresenter, Present } from '../../badges.js'
 
-export default new (class implements BadgePresenter {
+export default new (class extends BadgePresenter {
   url = new URL(import.meta.url)
-  badges = [
-    'mass-delete-commit-10k',
-    'mass-delete-commit'
-  ] as const
   tiers = true
+  badges = ['mass-delete-commit-10k', 'mass-delete-commit'] as const
   present: Present = (data, grant) => {
     for (const repo of data.repos) {
       for (const commit of repo.commits) {
@@ -14,20 +11,18 @@ export default new (class implements BadgePresenter {
           (commit.deletions ?? 0) > 1000 &&
           (commit.deletions ?? 0) / (commit.additions ?? 0) > 100
         ) {
-          grant(
-            'mass-delete-commit',
-            'When I delete code, I delete a lot.',
-          ).evidenceCommits(commit)
+          grant('mass-delete-commit', 'When I delete code, I delete a lot.')
+            .evidenceCommits(commit)
+            .tier(1)
         }
 
         if (
           (commit.deletions ?? 0) > 10_000 &&
           (commit.deletions ?? 0) / (commit.additions ?? 0) > 100
         ) {
-          grant(
-            'mass-delete-commit-10k',
-            'When I delete code, I delete a lot.',
-          ).evidenceCommits(commit)
+          grant('mass-delete-commit-10k', 'When I delete code, I delete a lot.')
+            .evidenceCommits(commit)
+            .tier(2)
         }
       }
     }
src/all-badges/my-badges-contributor/my-badges-contributor.ts
@@ -1,7 +1,7 @@
 import { Pull } from '../../collect/collect.js'
 import { BadgePresenter, Present } from '../../badges.js'
 
-export default new (class implements BadgePresenter {
+export default new (class extends BadgePresenter {
   url = new URL(import.meta.url)
   badges = ['my-badges-contributor'] as const
   present: Present = (data, grant) => {
src/all-badges/revert-revert-commit/revert-revert-commit.ts
@@ -1,7 +1,7 @@
 import { Commit } from '../../collect/collect.js'
 import { BadgePresenter, Present } from '../../badges.js'
 
-export default new (class implements BadgePresenter {
+export default new (class extends BadgePresenter {
   url = new URL(import.meta.url)
   badges = ['revert-revert-commit'] as const
   present: Present = (data, grant) => {
src/all-badges/star-gazer/star-gazer.ts
@@ -1,6 +1,6 @@
 import { BadgePresenter, Present } from '../../badges.js'
 
-export default new (class implements BadgePresenter {
+export default new (class extends BadgePresenter {
   url = new URL(import.meta.url)
   badges = ['star-gazer'] as const
   present: Present = (data, grant) => {
src/all-badges/stars/stars.ts
@@ -1,17 +1,17 @@
 import { BadgePresenter, Present } from '../../badges.js'
 
-export default new (class implements BadgePresenter {
+export default new (class extends BadgePresenter {
   url = new URL(import.meta.url)
+  tiers = true
   badges = [
-    'stars-20000',
-    'stars-10000',
-    'stars-5000',
-    'stars-2000',
-    'stars-1000',
-    'stars-500',
     'stars-100',
+    'stars-500',
+    'stars-1000',
+    'stars-2000',
+    'stars-5000',
+    'stars-10000',
+    'stars-20000',
   ] as const
-  tiers = true
   present: Present = (data, grant) => {
     let totalStars = 0
 
@@ -34,12 +34,26 @@ ${reasonable.join('\n')}
 
 <sup>I have push, maintainer or admin permissions, so I'm definitely an author.<sup>
 `
-
-    this.badges.forEach((badge) => {
-      const limit = +badge.slice(6)
-      if (totalStars >= limit) {
-        grant(badge, `I collected ${limit} stars.`).evidence(text)
-      }
-    })
+    if (totalStars >= 100) {
+      grant('stars-100', `I collected 100 stars.`).evidence(text).tier(1)
+    }
+    if (totalStars >= 500) {
+      grant('stars-500', 'I collected 500 stars.').evidence(text).tier(2)
+    }
+    if (totalStars >= 1000) {
+      grant('stars-1000', 'I collected 1000 stars.').evidence(text).tier(3)
+    }
+    if (totalStars >= 2000) {
+      grant('stars-2000', 'I collected 2000 stars.').evidence(text).tier(4)
+    }
+    if (totalStars >= 5000) {
+      grant('stars-5000', 'I collected 5000 stars.').evidence(text).tier(5)
+    }
+    if (totalStars >= 10000) {
+      grant('stars-10000', 'I collected 10000 stars.').evidence(text).tier(6)
+    }
+    if (totalStars >= 20000) {
+      grant('stars-20000', 'I collected 20000 stars.').evidence(text).tier(7)
+    }
   }
 })()
src/all-badges/time-of-commit/time-of-commit.ts
@@ -1,7 +1,7 @@
 import { BadgePresenter, Present } from '../../badges.js'
 import { Commit, User } from '../../collect/collect.js'
 
-export default new (class implements BadgePresenter {
+export default new (class extends BadgePresenter {
   url = new URL(import.meta.url)
   badges = ['midnight-commits', 'morning-commits', 'evening-commits'] as const
   present: Present = (data, grant) => {
src/all-badges/yeti/yeti.ts
@@ -1,6 +1,6 @@
 import { BadgePresenter, Present } from '../../badges.js'
 
-export default new (class implements BadgePresenter {
+export default new (class extends BadgePresenter {
   url = new URL(import.meta.url)
   badges = ['yeti'] as const
   present: Present = (data, grant) => {
src/badges.ts
@@ -12,11 +12,11 @@ for (const {
 
 export type ID = (typeof allBadges)[number]['default']['badges'][number]
 
-export interface BadgePresenter {
-  url: URL
-  badges: unknown
-  tiers?: boolean
-  present: Present
+export abstract class BadgePresenter {
+  abstract url: URL
+  tiers = false
+  abstract badges: unknown
+  abstract present: Present
 }
 
 export type Grant = ReturnType<typeof badgeCollection>
@@ -25,73 +25,102 @@ export type Present = (data: Data, grant: Grant) => void
 
 export type Badge = {
   id: ID
+  tier: number
   desc: string
   body: string
   image: string
 }
 
-const voidGrant = {
-  evidence() {},
-  evidenceCommits() {},
-  evidenceCommitsWithMessage() {},
-  evidencePRs() {},
-}
-
 export function badgeCollection(
-  badges: Badge[],
-  baseUrl: URL,
+  userBadges: Badge[],
+  presenter: (typeof allBadges)[number]['default'],
   pickBadges: string[],
   omitBadges: string[],
+  compact: boolean,
 ) {
-  const indexes = new Map(badges.map((x, i) => [x.id, i]))
-  const baseDir = path.basename(path.dirname(fileURLToPath(baseUrl)))
+  const indexes = new Map(userBadges.map((x, i) => [x.id, i]))
+  const baseDir = path.basename(path.dirname(fileURLToPath(presenter.url)))
 
   return function grant(id: ID, desc: string) {
-    if (!pickBadges.includes(id) || omitBadges.includes(id)) {
-      if (indexes.has(id)) {
-        badges.splice(indexes.get(id)!, 1)
-      }
-      return voidGrant
-    }
+    // if (!pickBadges.includes(id) || omitBadges.includes(id)) {
+    //   if (indexes.has(id)) {
+    //     badges.splice(indexes.get(id)!, 1)
+    //   }
+    //   return voidGrant
+    // }
 
     const badge: Badge = {
       id,
+      tier: 0,
       desc,
       body: '',
       image: `https://github.com/my-badges/my-badges/blob/master/src/all-badges/${baseDir}/${id}.png?raw=true`,
     }
 
-    if (!indexes.has(id)) {
-      badges.push(badge)
-      indexes.set(id, badges.length - 1)
+    if (compact) {
+      let found = false
+      for (const badgeId of presenter.badges) {
+        if (indexes.has(badgeId)) {
+          const index = indexes.get(badgeId)!
+          const alreadyExistingBadge = userBadges[index]
+          if (alreadyExistingBadge.tier < badge.tier) {
+            userBadges[index] = badge
+          }
+        }
+      }
+      if (!found) {
+        userBadges.push(badge)
+        indexes.set(id, userBadges.length - 1)
+      }
     } else {
-      badges[indexes.get(id)!] = badge
+      if (indexes.has(id)) {
+        userBadges[indexes.get(id)!] = badge
+      } else {
+        userBadges.push(badge)
+        indexes.set(id, userBadges.length - 1)
+      }
     }
 
-    return {
-      evidence(text: string) {
-        badge.body = text
-      },
-      evidenceCommits(...commits: Commit[]) {
-        this.evidence(
-          'Commits:\n\n' + commits.map((x) => `- ${linkCommit(x)}`).join('\n'),
-        )
-      },
-      evidenceCommitsWithMessage(...commits: Commit[]) {
-        this.evidence(
-          'Commits:\n\n' +
-            commits.map((x) => `- ${linkCommit(x)}: ${x.message}`).join('\n'),
-        )
-      },
-      evidencePRs(...pulls: Pull[]) {
-        this.evidence(
-          'Pull requests:\n\n' +
-            pulls
-              .map(linkPull)
-              .map((x) => '- ' + x)
-              .join('\n'),
-        )
-      },
-    }
+    return new Evidence(badge)
+  }
+}
+
+class Evidence {
+  constructor(private badge: Badge) {}
+
+  tier(tier: number) {
+    this.badge.tier = tier
+    return this
+  }
+
+  evidence(text: string) {
+    this.badge.body = text
+    return this
+  }
+
+  evidenceCommits(...commits: Commit[]) {
+    this.evidence(
+      'Commits:\n\n' + commits.map((x) => `- ${linkCommit(x)}`).join('\n'),
+    )
+    return this
+  }
+
+  evidenceCommitsWithMessage(...commits: Commit[]) {
+    this.evidence(
+      'Commits:\n\n' +
+        commits.map((x) => `- ${linkCommit(x)}: ${x.message}`).join('\n'),
+    )
+    return this
+  }
+
+  evidencePRs(...pulls: Pull[]) {
+    this.evidence(
+      'Pull requests:\n\n' +
+        pulls
+          .map(linkPull)
+          .map((x) => '- ' + x)
+          .join('\n'),
+    )
+    return this
   }
 }
src/main.ts
@@ -15,7 +15,7 @@ void (async function main() {
   const { env } = process
   const argv = minimist(process.argv.slice(2), {
     string: ['data', 'repo', 'token', 'size', 'user', 'pick', 'omit'],
-    boolean: ['dryrun', 'tiers'],
+    boolean: ['dryrun', 'compact'],
   })
   const {
     token = env.GITHUB_TOKEN,
@@ -26,7 +26,7 @@ void (async function main() {
     dryrun,
     pick,
     omit,
-    tiers,
+    compact,
   } = argv
   const [owner, repo] = repository?.split('/', 2) || [username, username]
   const pickBadges = pick ? pick.split(',') : names
@@ -74,7 +74,7 @@ void (async function main() {
     fs.writeFileSync(`data/${username}.json`, JSON.stringify(data, null, 2))
   }
 
-  let badges: Badge[] = []
+  let userBadges: Badge[] = []
   let oldJson: string | undefined
   let jsonSha: string | undefined
   if (owner && repo) {
@@ -92,7 +92,7 @@ void (async function main() {
         'utf8',
       )
       jsonSha = myBadgesResp.data.sha
-      badges = JSON.parse(oldJson)
+      userBadges = JSON.parse(oldJson)
     } catch (err) {
       if (err instanceof RequestError && err.response?.status != 404) {
         throw err
@@ -101,13 +101,27 @@ void (async function main() {
   }
 
   for (const { default: presenter } of allBadges) {
-    const grant = badgeCollection(badges, presenter.url, pickBadges, omitBadges)
+    const grant = badgeCollection(
+      userBadges,
+      presenter,
+      pickBadges,
+      omitBadges,
+      compact,
+    )
     presenter.present(data, grant)
   }
-  console.log('Badges', badges)
+  console.log('Badges', userBadges)
 
   if (owner && repo) {
-    await updateBadges(octokit, owner, repo, badges, oldJson, jsonSha, dryrun)
-    await updateReadme(octokit, owner, repo, badges, size, dryrun, tiers)
+    await updateBadges(
+      octokit,
+      owner,
+      repo,
+      userBadges,
+      oldJson,
+      jsonSha,
+      dryrun,
+    )
+    await updateReadme(octokit, owner, repo, userBadges, size, dryrun)
   }
 })()
src/update-readme.ts
@@ -7,7 +7,6 @@ export function generateReadme(
   readme: string,
   badges: Badge[],
   size: number | string = 64,
-  tiers: boolean,
 ) {
   const startString = '<!-- my-badges start -->'
   const endString = '<!-- my-badges end -->'
@@ -17,20 +16,11 @@ export function generateReadme(
   const start = content.indexOf(startString)
   const end = content.indexOf(endString)
   const needToAddNewLine = content[end + endString.length + 1] !== '\n'
-  const highestBadges = allBadges.flatMap(({ default: { badges: _badges , tiers: _tiers} }) =>
-    _tiers
-      ? _badges.find((badge) => badges.some(({ id }) => id === badge))
-      : _badges,
-  )
-  const filter = tiers
-    ? ({ id }: Badge) => highestBadges.includes(id)
-    : () => true
 
   if (start !== -1 && end !== -1) {
     content = content.slice(0, start) + content.slice(end + endString.length)
 
     const badgesHtml = badges
-      .filter(filter)
       .map((badge) => {
         const desc = quoteAttr(badge.desc)
         // prettier-ignore
@@ -58,7 +48,6 @@ export async function updateReadme(
   badges: Badge[],
   size: number | string,
   dryrun: boolean,
-  tiers: boolean,
 ) {
   const readme = await octokit.request<'readme'>(
     'GET /repos/{owner}/{repo}/readme',
@@ -67,11 +56,11 @@ export async function updateReadme(
       repo,
     },
   )
-  const content = await generateReadme(
+
+  const content = generateReadme(
     Buffer.from(readme.data.content, 'base64').toString('utf8'),
     badges,
     size,
-    tiers,
   )
 
   await upload(