Commit bb67855
Changed files (10)
src
all-badges
src/all-badges/time-of-commit/evening-commits.png
Binary file
src/all-badges/time-of-commit/midnight-commits.png
Binary file
src/all-badges/time-of-commit/morning-commits.png
Binary file
src/all-badges/time-of-commit/time-of-commit.ts
@@ -0,0 +1,183 @@
+import {BadgePresenter, Present} from '../../badges.js'
+import {Commit, Data, User} from '../../collect/collect.js'
+
+export default new class implements BadgePresenter {
+ url = new URL(import.meta.url)
+ badges = [
+ 'midnight-commits',
+ 'morning-commits',
+ 'evening-commits',
+ ] as const
+ present: Present = (data, grant) => {
+ const midnightCommits: Commit[] = []
+ const morningCommits: Commit[] = []
+ const eveningCommits: Commit[] = []
+
+ const timezoneOffset = guessTimezone(data.user)
+
+ for (const repo of data.repos) {
+ for (const commit of repo.commits) {
+ const date = new Date(commit.committedDate)
+
+ date.setHours(date.getUTCHours() + timezoneOffset)
+
+ if (date.getUTCHours() == 0 && date.getUTCMinutes() < 5) {
+ midnightCommits.push(commit)
+ }
+ if (date.getUTCHours() >= 4 && date.getUTCHours() < 6) {
+ morningCommits.push(commit)
+ }
+ if (date.getUTCHours() >= 21) {
+ eveningCommits.push(commit)
+ }
+ }
+ }
+
+ if (midnightCommits.length > 0) {
+ grant('midnight-commits', 'I commit at midnight.')
+ .evidenceCommits(...midnightCommits.sort(latest).slice(0, 6))
+ }
+ if (morningCommits.length > 0) {
+ grant('morning-commits', 'I commit in the morning.')
+ .evidenceCommits(...morningCommits.sort(latest).slice(0, 6))
+ }
+ if (eveningCommits.length > 0) {
+ grant('evening-commits', 'I commit in the evening.')
+ .evidenceCommits(...eveningCommits.sort(latest).slice(0, 6))
+ }
+ }
+}
+
+function latest(a: Commit, b: Commit) {
+ return new Date(b.committedDate).getTime() - new Date(a.committedDate).getTime()
+}
+
+function guessTimezone(user: User) {
+ const location = user.location ? user.location.toLowerCase() : ''
+ const regexMapping = [
+ {pattern: /\bamsterdam\b|\bnetherlands\b/, offset: 2},
+ {pattern: /\bantwerp\b|\bghent\b/, offset: 2},
+ {pattern: /\bargentina\b|\bbuenos aires\b/, offset: -3},
+ {pattern: /\bathens\b|\bgreece\b/, offset: 3},
+ {pattern: /\bbangkok\b|\bthailand\b/, offset: 7},
+ {pattern: /\bbarcelona\b|\bvalencia\b|\bseville\b/, offset: 2},
+ {pattern: /\bbeijing\b|\bchina\b/, offset: 8},
+ {pattern: /\bbelgrade\b|\bserbia\b/, offset: 2},
+ {pattern: /\bberlin\b|\bgermany\b/, offset: 2},
+ {pattern: /\bbilbao\b|\bzaragoza\b/, offset: 2},
+ {pattern: /\bbordeaux\b|\bmarseille\b|\blyon\b/, offset: 2},
+ {pattern: /\bbratislava\b|\bslovakia\b/, offset: 2},
+ {pattern: /\bbrazil\b|\bsao paulo\b|\brio de janeiro\b/, offset: -3},
+ {pattern: /\bbrussels\b|\bbelgium\b/, offset: 2},
+ {pattern: /\bbucharest\b|\bromania\b/, offset: 3},
+ {pattern: /\bbudapest\b|\bhungary\b/, offset: 2},
+ {pattern: /\bcairo\b|\begypt\b/, offset: 2},
+ {pattern: /\bcasablanca\b|\bmorocco\b/, offset: 1},
+ {pattern: /\bchicago\b|\billinois\b|\bil\b/, offset: -5},
+ {pattern: /\bchile\b|\bsantiago\b/, offset: -3},
+ {pattern: /\bchisinau\b|\bmoldova\b/, offset: 3},
+ {pattern: /\bcolombia\b|\bbogota\b/, offset: -5},
+ {pattern: /\bcopenhagen\b|\bdenmark\b/, offset: 2},
+ {pattern: /\bcosta rica\b|\bsan jose\b/, offset: -6},
+ {pattern: /\bcrete\b|\bthessaloniki\b/, offset: 3},
+ {pattern: /\bdhaka\b|\bbangladesh\b/, offset: 6},
+ {pattern: /\bdubai\b|\buae\b|\babu dhabi\b/, offset: 4},
+ {pattern: /\bdublin\b|\bireland\b/, offset: 1},
+ {pattern: /\becuador\b|\bquito\b/, offset: -5},
+ {pattern: /\bfaro\b|\bevora\b/, offset: 1},
+ {pattern: /\bfiji\b/, offset: 12},
+ {pattern: /\bflorence\b|\bpalermo\b/, offset: 2},
+ {pattern: /\bflorida\b|\bmiami\b/, offset: -4},
+ {pattern: /\bgdansk\b|\bwroclaw\b/, offset: 2},
+ {pattern: /\bgeneva\b|\bbern\b/, offset: 2},
+ {pattern: /\bgenoa\b|\bturin\b/, offset: 2},
+ {pattern: /\bglasgow\b|\bliverpool\b|\bmanchester\b/, offset: 0},
+ {pattern: /\bgothenburg\b|\bmalmo\b/, offset: 2},
+ {pattern: /\bhamburg\b|\bmunich\b|\bfrankfurt\b/, offset: 2},
+ {pattern: /\bhawaii\b/, offset: -10},
+ {pattern: /\bhelsinki\b|\bfinland\b/, offset: 3},
+ {pattern: /\biraq\b|\bbaghdad\b/, offset: 3},
+ {pattern: /\bisrael\b|\btel aviv\b/, offset: 3},
+ {pattern: /\bjakarta\b|\bindonesia\b/, offset: 7},
+ {pattern: /\bjohannesburg\b|\bsouth africa\b/, offset: 2},
+ {pattern: /\bjordan\b|\bamman\b/, offset: 3},
+ {pattern: /\bkathmandu\b|\bnepal\b/, offset: 5.75},
+ {pattern: /\bkiev\b|\bukraine\b/, offset: 3},
+ {pattern: /\bkuala lumpur\b|\bmalaysia\b/, offset: 8},
+ {pattern: /\blagos\b|\bnigeria\b/, offset: 1},
+ {pattern: /\bled\b|\bslovenia\b/, offset: 2},
+ {pattern: /\bledz\b|\bkrakow\b/, offset: 2},
+ {pattern: /\blisbon\b|\bportugal\b/, offset: 1},
+ {pattern: /\bljubljana\b|\bslovenia\b/, offset: 2},
+ {pattern: /\blondon\b|\buk\b/, offset: 0},
+ {pattern: /\blos angeles\b|\bcalifornia\b|\bca\b/, offset: -7},
+ {pattern: /\bluxembourg\b/, offset: 2},
+ {pattern: /\blyon\b|\bnantes\b/, offset: 2},
+ {pattern: /\bmadrid\b|\bspain\b/, offset: 2},
+ {pattern: /\bmalta\b|\bvalletta\b/, offset: 2},
+ {pattern: /\bmelbourne\b/, offset: 11},
+ {pattern: /\bmexico\b|\bmexico city\b/, offset: -5},
+ {pattern: /\bminsk\b|\bbelarus\b/, offset: 3},
+ {pattern: /\bmonaco\b/, offset: 2},
+ {pattern: /\bmontreal\b|\bquebec\b/, offset: -4},
+ {pattern: /\bmoscow\b|\brussia\b/, offset: 3},
+ {pattern: /\bmumbai\b|\bindia\b/, offset: 5.5},
+ {pattern: /\bnairobi\b|\bkenya\b/, offset: 3},
+ {pattern: /\bnaples\b|\bmilan\b|\bvenice\b/, offset: 2},
+ {pattern: /\bnew delhi\b/, offset: 5.5},
+ {pattern: /\bnew york\b|\bny\b/, offset: -4},
+ {pattern: /\bnew zealand\b|\bauckland\b/, offset: 13},
+ {pattern: /\bnice\b|\btoulouse\b/, offset: 2},
+ {pattern: /\bnicosia\b|\bcyprus\b/, offset: 3},
+ {pattern: /\bodessa\b|\blviv\b/, offset: 3},
+ {pattern: /\boslo\b|\bnorway\b/, offset: 2},
+ {pattern: /\bpakistan\b|\blahore\b|\bkarachi\b/, offset: 5},
+ {pattern: /\bpanama\b|\bpanama city\b/, offset: -5},
+ {pattern: /\bpapua new guinea\b|\bport moresby\b/, offset: 10},
+ {pattern: /\bparis\b|\bfrance\b/, offset: 2},
+ {pattern: /\bperu\b|\blima\b/, offset: -5},
+ {pattern: /\bpodgorica\b|\bmontenegro\b/, offset: 2},
+ {pattern: /\bporto\b|\bbraga\b/, offset: 1},
+ {pattern: /\bprague\b|\bczech republic\b|\bczechia\b/, offset: 2},
+ {pattern: /\breykjavik\b|\biceland\b/, offset: 0},
+ {pattern: /\briga\b|\blatvia\b/, offset: 3},
+ {pattern: /\brome\b|\bitaly\b/, offset: 2},
+ {pattern: /\brotterdam\b|\butrecht\b/, offset: 2},
+ {pattern: /\bsalzburg\b|\bgraz\b/, offset: 2},
+ {pattern: /\bsarajevo\b|\bbosnia and herzegovina\b/, offset: 2},
+ {pattern: /\bsaudi arabia\b|\briyadh\b/, offset: 3},
+ {pattern: /\bseoul\b|\bsouth korea\b/, offset: 9},
+ {pattern: /\bshanghai\b/, offset: 8},
+ {pattern: /\bsheffield\b|\bleeds\b/, offset: 0},
+ {pattern: /\bsingapore\b/, offset: 8},
+ {pattern: /\bskopje\b|\bnorth macedonia\b/, offset: 2},
+ {pattern: /\bsofia\b|\bbulgaria\b/, offset: 3},
+ {pattern: /\bstockholm\b|\bsweden\b/, offset: 2},
+ {pattern: /\bstuttgart\b|\bdusseldorf\b/, offset: 2},
+ {pattern: /\bsydney\b|\baustralia\b/, offset: 11},
+ {pattern: /\btallinn\b|\bestonia\b/, offset: 3},
+ {pattern: /\btehran\b|\biran\b/, offset: 3.5},
+ {pattern: /\btexas\b|\bhouston\b|\bdallas\b/, offset: -5},
+ {pattern: /\btirana\b|\balbania\b/, offset: 2},
+ {pattern: /\btokyo\b|\bjapan\b/, offset: 9},
+ {pattern: /\btoronto\b|\bontario\b/, offset: -4},
+ {pattern: /\btunisia\b|\btunis\b/, offset: 1},
+ {pattern: /\bvalencia\b|\bmalaga\b/, offset: 2},
+ {pattern: /\bvancouver\b|\bbritish columbia\b/, offset: -7},
+ {pattern: /\bvenezuela\b|\bcaracas\b/, offset: -4},
+ {pattern: /\bvienna\b|\baustria\b/, offset: 2},
+ {pattern: /\bvilnius\b|\blithuania\b/, offset: 3},
+ {pattern: /\bwarsaw\b|\bpoland\b/, offset: 2},
+ {pattern: /\bzagreb\b|\bcroatia\b/, offset: 2},
+ {pattern: /\bzurich\b|\bswitzerland\b/, offset: 2},
+ ]
+
+ for (const mapping of regexMapping) {
+ if (mapping.pattern.test(location)) {
+ return mapping.offset
+ }
+ }
+
+ return 0
+}
+
src/all-badges/index.ts
@@ -6,4 +6,5 @@ export const allBadges = [
await import('./mass-delete-commit/mass-delete-commit.js'),
await import('./revert-revert-commit/revert-revert-commit.js'),
await import('./yeti/yeti.js'),
+ await import('./time-of-commit/time-of-commit.js'),
] as const
src/collect/collect.ts
@@ -5,6 +5,7 @@ import {CommitsQuery} from './commits.js'
import fs from 'node:fs'
import {fileURLToPath} from 'url'
import {IssuesQuery} from './issues.js'
+import {UserQuery} from './user.js'
export type Data = {
user: User
@@ -12,7 +13,7 @@ export type Data = {
pulls: Pull[]
issues: Issues[]
}
-export type User = Endpoints['GET /users/{username}']['response']['data']
+export type User = UserQuery['user']
export type Repo = Endpoints['GET /users/{username}/repos']['response']['data'][0] & {
commits: Commit[]
}
@@ -21,7 +22,7 @@ export type Pull = PullsQuery['user']['pullRequests']['nodes'][0]
export type Issues = IssuesQuery['user']['issues']['nodes'][0]
export async function collect(username: string, octokit: Octokit): Promise<Data> {
- const user = (await octokit.request('GET /users/{username}', {username})).data
+ const {user} = await octokit.graphql<UserQuery>(loadGraphql('./user.graphql'), {login: username})
const data: Data = {
user: user,
@@ -54,7 +55,7 @@ export async function collect(username: string, octokit: Octokit): Promise<Data>
const commits = octokit.graphql.paginate.iterator<CommitsQuery>(loadGraphql('./commits.graphql'), {
owner: repo.owner.login,
name: repo.name,
- author: user.node_id,
+ author: user.id,
})
for await (const resp of commits) {
src/collect/commits.graphql
@@ -13,6 +13,7 @@ query CommitsQuery(
totalCount
nodes {
sha: oid
+ committedDate
message
messageBody
additions
src/collect/commits.ts
@@ -6,6 +6,7 @@ export type CommitsQuery = {
totalCount: number
nodes: Array<{
sha: string
+ committedDate: string
message: string
messageBody: string
additions: number
src/collect/user.graphql
@@ -0,0 +1,52 @@
+query UserQuery(
+ $login: String!,
+) {
+ user(login: $login) {
+ id
+ login
+ name
+ avatarUrl
+ bio
+ company
+ location
+ email
+ twitterUsername
+ websiteUrl
+ status {
+ createdAt
+ emoji
+ message
+ }
+ createdAt
+ followers {
+ totalCount
+ }
+ following {
+ totalCount
+ }
+ anyPinnableItems
+ pinnedItems(first: 6) {
+ totalCount
+ nodes {
+ ... on Gist {
+ name
+ }
+ ... on Repository {
+ name
+ }
+ }
+ }
+ sponsoring {
+ totalCount
+ }
+ sponsors {
+ totalCount
+ }
+ }
+ rateLimit {
+ limit
+ cost
+ remaining
+ resetAt
+ }
+}
src/collect/user.ts
@@ -0,0 +1,45 @@
+export type UserQuery = {
+ user: {
+ id: string
+ login: string
+ name: string
+ avatarUrl: string
+ bio: string | null
+ company: string | null
+ location: string | null
+ email: string
+ twitterUsername: string | null
+ websiteUrl: string | null
+ status: {
+ createdAt: string
+ emoji: string
+ message: string
+ } | null
+ createdAt: string
+ followers: {
+ totalCount: number
+ }
+ following: {
+ totalCount: number
+ }
+ anyPinnableItems: boolean
+ pinnedItems: {
+ totalCount: number
+ nodes: Array<{
+ name: string
+ }>
+ }
+ sponsoring: {
+ totalCount: number
+ }
+ sponsors: {
+ totalCount: number
+ }
+ }
+ rateLimit: {
+ limit: number
+ cost: number
+ remaining: number
+ resetAt: string
+ }
+}