Skip to content

Commit d4e1bc9

Browse files
authored
Add API to get contributor list (#285)
1 parent d007ded commit d4e1bc9

File tree

4 files changed

+113
-7
lines changed

4 files changed

+113
-7
lines changed

Dockerfile.creatorsgarten

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
FROM node:hydrogen AS base
2-
RUN yarn global add pnpm turbo
2+
RUN yarn global add pnpm turbo@^1.8.3
33
WORKDIR /app
44

55
# Generate a pruned workspace

packages/contentsgarten/src/ContentsgartenRouter.ts

+32-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { AuthState, User } from './ContentsgartenAuth'
66
import type { ContentsgartenRequestContext } from './ContentsgartenContext'
77
import { PageDatabaseSearch } from './ContentsgartenPageDatabase'
88
import { LaxPageRefRegex, PageRefRegex } from './PageRefRegex'
9+
import { cache } from './cache'
910
import {
1011
GetPageResult,
1112
getPage,
@@ -14,7 +15,6 @@ import {
1415
savePageToDatabase,
1516
} from './getPage'
1617
import { t } from './trpc'
17-
import { cache } from './cache'
1818

1919
export { GetPageResult } from './getPage'
2020
export { PageRefRegex }
@@ -70,6 +70,37 @@ export const ContentsgartenRouter = t.router({
7070
return { ...result, perf: ctx.perf.toMessageArray() }
7171
},
7272
),
73+
getContributors: t.procedure
74+
.meta({ summary: 'Returns the contributors of a page' })
75+
.input(
76+
z.object({
77+
pageRef: LaxPageRef,
78+
}),
79+
)
80+
.output(
81+
z.object({
82+
contributors: z.array(
83+
z.object({
84+
login: z.string(),
85+
avatarUrl: z.string(),
86+
contributions: z.number(),
87+
}),
88+
),
89+
perf: z.array(z.string()),
90+
}),
91+
)
92+
.query(async ({ input: { pageRef }, ctx }) => {
93+
const filePath = pageRefToFilePath(ctx, pageRef)
94+
const result = await cache(
95+
ctx,
96+
`contributors:${filePath}`,
97+
async () => {
98+
return ctx.app.storage.listContributors(ctx, filePath)
99+
},
100+
300e3,
101+
)
102+
return { ...result, perf: ctx.perf.toMessageArray() }
103+
}),
73104
getEditPermission: t.procedure
74105
.meta({
75106
summary:

packages/contentsgarten/src/ContentsgartenStorage.ts

+73-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { GitHubApp } from './GitHubApp'
22
import type { RequestContext } from './RequestContext'
33
import { resolveGitHubUsernameFromId } from './resolveGitHubUsernameFromId'
4-
import { resolveOctokit, ContentsgartenOctokit } from './resolveOctokit'
4+
import { ContentsgartenOctokit, resolveOctokit } from './resolveOctokit'
55

66
export interface ContentsgartenStorage {
77
getFile(ctx: RequestContext, path: string): Promise<GetFileResult | undefined>
@@ -10,8 +10,13 @@ export interface ContentsgartenStorage {
1010
path: string,
1111
options: PutFileOptions,
1212
): Promise<PutFileResult>
13+
listContributors(
14+
ctx: RequestContext,
15+
path: string,
16+
): Promise<ListContributorsResult>
1317
listFiles(ctx: RequestContext): Promise<string[]>
1418
}
19+
1520
export interface GetFileResult {
1621
content: Buffer
1722
lastModified?: string
@@ -32,6 +37,16 @@ export interface PutFileResult {
3237
lastModifiedBy: string[]
3338
}
3439

40+
export interface ListContributorsResult {
41+
contributors: Contributor[]
42+
}
43+
44+
export interface Contributor {
45+
login: string
46+
avatarUrl: string
47+
contributions: number
48+
}
49+
3550
export class GitHubStorage implements ContentsgartenStorage {
3651
owner: string
3752
repo: string
@@ -128,6 +143,63 @@ export class GitHubStorage implements ContentsgartenStorage {
128143
.filter((item) => item.type === 'blob')
129144
.map((item) => item.path!)
130145
}
146+
147+
async listContributors(
148+
ctx: RequestContext,
149+
path: string,
150+
): Promise<ListContributorsResult> {
151+
const octokit = await resolveOctokit(ctx, this.config.app, this.config.repo)
152+
const { owner, repo } = this
153+
const { data } = await octokit.request('POST /graphql', {
154+
query: `query($path: String!) {
155+
repository(owner: "${owner}", name: "${repo}") {
156+
object(expression: "${this.config.branch}") {
157+
... on Commit {
158+
history(first: 100, path: $path) {
159+
nodes {
160+
oid
161+
committedDate
162+
authors(first: 2) {
163+
nodes {
164+
user {
165+
avatarUrl(size: 64)
166+
login
167+
}
168+
}
169+
}
170+
}
171+
}
172+
}
173+
}
174+
}
175+
}`,
176+
variables: {
177+
path,
178+
},
179+
})
180+
const commits = data.data.repository.object.history.nodes
181+
const profileMap = new Map<string, Contributor>()
182+
for (const commit of commits) {
183+
for (const { user } of commit.authors.nodes) {
184+
if (user.login.endsWith('[bot]')) continue
185+
const profile = profileMap.get(user.login)
186+
if (profile) {
187+
profile.contributions++
188+
} else {
189+
profileMap.set(user.login, {
190+
login: user.login,
191+
avatarUrl: user.avatarUrl,
192+
contributions: 1,
193+
})
194+
}
195+
}
196+
}
197+
return {
198+
contributors: Array.from(profileMap.values()).sort(
199+
(a, b) => b.contributions - a.contributions,
200+
),
201+
}
202+
}
131203
}
132204

133205
export interface GitHubStorageConfig {

packages/contentsgarten/src/testing.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1+
import { createHash } from 'crypto'
12
import fs from 'fs'
23
import path from 'path'
3-
import { createHash } from 'crypto'
4-
import { ContentsgartenStorage } from './ContentsgartenStorage'
5-
import { ContentsgartenAuth } from './ContentsgartenAuth'
6-
import { ContentsgartenTeamResolver } from './ContentsgartenTeamResolver'
74
import { Contentsgarten } from './Contentsgarten'
5+
import { ContentsgartenAuth } from './ContentsgartenAuth'
86
import { ContentsgartenPageDatabase } from './ContentsgartenPageDatabase'
7+
import { ContentsgartenStorage } from './ContentsgartenStorage'
8+
import { ContentsgartenTeamResolver } from './ContentsgartenTeamResolver'
99

1010
export namespace testing {
1111
export function createFakeStorage(): ContentsgartenStorage {
@@ -37,6 +37,9 @@ export namespace testing {
3737
lastModifiedBy: [],
3838
}
3939
},
40+
async listContributors(ctx, filePath) {
41+
return { contributors: [] }
42+
},
4043
}
4144

4245
function getFsPath(filePath: string) {

0 commit comments

Comments
 (0)