|
4 | 4 |
|
5 | 5 | import { getGitHubHeaders } from "./github.js"; |
6 | 6 |
|
7 | | -const GITHUB_GRAPHQL_URL = "https://api.github.com/graphql"; |
8 | | -const COHORT_PROJECT_PATTERN = /๋ฆฌํธ์ฝ๋ ์คํฐ๋\s*\d+๊ธฐ/; |
9 | | - |
10 | | -/** |
11 | | - * GitHub GraphQL API ํธ์ถ ํฌํผ |
12 | | - * |
13 | | - * @param {string} query |
14 | | - * @param {string} appToken |
15 | | - * @returns {Promise<object>} |
16 | | - */ |
17 | | -async function graphql(query, appToken) { |
18 | | - const response = await fetch(GITHUB_GRAPHQL_URL, { |
19 | | - method: "POST", |
20 | | - headers: { |
21 | | - ...getGitHubHeaders(appToken), |
22 | | - "Content-Type": "application/json", |
23 | | - }, |
24 | | - body: JSON.stringify({ query }), |
25 | | - }); |
26 | | - |
27 | | - if (!response.ok) { |
28 | | - throw new Error(`GraphQL request failed: ${response.status} ${response.statusText}`); |
29 | | - } |
30 | | - |
31 | | - const result = await response.json(); |
32 | | - if (result.errors) { |
33 | | - throw new Error(`GraphQL error: ${JSON.stringify(result.errors)}`); |
34 | | - } |
35 | | - |
36 | | - return result.data; |
37 | | -} |
38 | | - |
39 | | -/** |
40 | | - * ํ์ฌ ์งํ ์ค์ธ ๊ธฐ์ ํ๋ก์ ํธ ID๋ฅผ ์กฐํํ๋ค. |
41 | | - * "๋ฆฌํธ์ฝ๋ ์คํฐ๋X๊ธฐ" ํจํด์ ์ด๋ฆฐ ํ๋ก์ ํธ๋ฅผ ์ฐพ๋๋ค. |
42 | | - * |
43 | | - * @param {string} repoOwner |
44 | | - * @param {string} repoName |
45 | | - * @param {string} appToken |
46 | | - * @returns {Promise<string|null>} ํ๋ก์ ํธ node ID, ์์ผ๋ฉด null |
47 | | - */ |
48 | | -async function fetchActiveCohortProjectId(repoOwner, repoName, appToken) { |
49 | | - const data = await graphql( |
50 | | - `{ |
51 | | - repository(owner: "${repoOwner}", name: "${repoName}") { |
52 | | - projectsV2(first: 20) { |
53 | | - nodes { |
54 | | - id |
55 | | - title |
56 | | - closed |
57 | | - } |
58 | | - } |
59 | | - } |
60 | | - }`, |
61 | | - appToken |
62 | | - ); |
63 | | - |
64 | | - const projects = data.repository.projectsV2.nodes; |
65 | | - const active = projects.find( |
66 | | - (p) => !p.closed && COHORT_PROJECT_PATTERN.test(p.title) |
67 | | - ); |
68 | | - |
69 | | - if (!active) { |
70 | | - console.warn( |
71 | | - `[fetchActiveCohortProjectId] No open cohort project found for ${repoOwner}/${repoName}` |
72 | | - ); |
73 | | - return null; |
74 | | - } |
75 | | - |
76 | | - console.log( |
77 | | - `[fetchActiveCohortProjectId] Active cohort project: "${active.title}" (${active.id})` |
78 | | - ); |
79 | | - return active.id; |
80 | | -} |
81 | | - |
82 | | -/** |
83 | | - * ๊ธฐ์ ํ๋ก์ ํธ์์ ํด๋น ์ ์ ๊ฐ ๋จธ์งํ PR ๋ฒํธ ๋ชฉ๋ก์ ๋ฐํํ๋ค. |
84 | | - * ํ๋ก์ ํธ ์์ดํ
์ ํ์ด์ง๋ค์ด์
ํ๋ฉฐ author.login์ผ๋ก ํํฐ๋งํ๋ค. |
85 | | - * |
86 | | - * @param {string} projectId |
87 | | - * @param {string} username |
88 | | - * @param {string} appToken |
89 | | - * @returns {Promise<number[]>} |
90 | | - */ |
91 | | -async function fetchUserMergedPRsInProject(projectId, username, appToken) { |
92 | | - const prNumbers = []; |
93 | | - let cursor = null; |
94 | | - |
95 | | - while (true) { |
96 | | - const afterClause = cursor ? `, after: "${cursor}"` : ""; |
97 | | - const data = await graphql( |
98 | | - `{ |
99 | | - node(id: "${projectId}") { |
100 | | - ... on ProjectV2 { |
101 | | - items(first: 100${afterClause}) { |
102 | | - pageInfo { hasNextPage endCursor } |
103 | | - nodes { |
104 | | - content { |
105 | | - ... on PullRequest { |
106 | | - number |
107 | | - state |
108 | | - author { login } |
109 | | - } |
110 | | - } |
111 | | - } |
112 | | - } |
113 | | - } |
114 | | - } |
115 | | - }`, |
116 | | - appToken |
117 | | - ); |
118 | | - |
119 | | - const { nodes, pageInfo } = data.node.items; |
120 | | - |
121 | | - for (const item of nodes) { |
122 | | - const pr = item.content; |
123 | | - if ( |
124 | | - pr?.state === "MERGED" && |
125 | | - pr?.author?.login?.toLowerCase() === username.toLowerCase() |
126 | | - ) { |
127 | | - prNumbers.push(pr.number); |
128 | | - } |
129 | | - } |
130 | | - |
131 | | - if (!pageInfo.hasNextPage) break; |
132 | | - cursor = pageInfo.endCursor; |
133 | | - } |
134 | | - |
135 | | - return prNumbers; |
136 | | -} |
137 | | - |
138 | | -/** |
139 | | - * ํ์ฌ ๊ธฐ์ ํ๋ก์ ํธ์์ ํด๋น ์ ์ ๊ฐ ์ ์ถํ ๋ฌธ์ ๋ชฉ๋ก์ ๋ฐํํ๋ค. |
140 | | - * |
141 | | - * ๊ธฐ์ ํ๋ก์ ํธ๋ฅผ ์ฐพ์ง ๋ชปํ๋ฉด ์ ์ฒด ๋ ํฌ ํธ๋ฆฌ ์ค์บ(fetchUserSolutions)์ผ๋ก ํด๋ฐฑํ๋ค. |
142 | | - * |
143 | | - * @param {string} repoOwner |
144 | | - * @param {string} repoName |
145 | | - * @param {string} username |
146 | | - * @param {string} appToken |
147 | | - * @returns {Promise<string[]>} |
148 | | - */ |
149 | | -export async function fetchCohortUserSolutions( |
150 | | - repoOwner, |
151 | | - repoName, |
152 | | - username, |
153 | | - appToken |
154 | | -) { |
155 | | - const projectId = await fetchActiveCohortProjectId( |
156 | | - repoOwner, |
157 | | - repoName, |
158 | | - appToken |
159 | | - ); |
160 | | - |
161 | | - if (!projectId) { |
162 | | - console.warn( |
163 | | - `[fetchCohortUserSolutions] Falling back to full tree scan for ${username}` |
164 | | - ); |
165 | | - return fetchUserSolutions(repoOwner, repoName, username, appToken); |
166 | | - } |
167 | | - |
168 | | - const prNumbers = await fetchUserMergedPRsInProject( |
169 | | - projectId, |
170 | | - username, |
171 | | - appToken |
172 | | - ); |
173 | | - |
174 | | - console.log( |
175 | | - `[fetchCohortUserSolutions] ${username} has ${prNumbers.length} merged PRs in current cohort` |
176 | | - ); |
177 | | - |
178 | | - const problemNames = new Set(); |
179 | | - for (const prNumber of prNumbers) { |
180 | | - const submissions = await fetchPRSubmissions( |
181 | | - repoOwner, |
182 | | - repoName, |
183 | | - prNumber, |
184 | | - username, |
185 | | - appToken |
186 | | - ); |
187 | | - for (const { problemName } of submissions) { |
188 | | - problemNames.add(problemName); |
189 | | - } |
190 | | - } |
191 | | - |
192 | | - return Array.from(problemNames); |
193 | | -} |
194 | | - |
195 | 7 | /** |
196 | 8 | * Fetches problem-categories.json from the repo root via GitHub API. |
197 | 9 | * Returns parsed JSON object, or null if the file is not found (404). |
|
0 commit comments