github-reporadar 0.1.1 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/assets/index-LfkmcOcl.js +73 -0
- package/dist/index.html +1 -1
- package/dist-cli/cli.js +107 -47
- package/package.json +1 -1
- package/dist/assets/index-7rO9STC2.js +0 -73
package/dist/index.html
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@400;500;600;700&display=swap"
|
|
17
17
|
rel="stylesheet"
|
|
18
18
|
/>
|
|
19
|
-
<script type="module" crossorigin src="/assets/index-
|
|
19
|
+
<script type="module" crossorigin src="/assets/index-LfkmcOcl.js"></script>
|
|
20
20
|
<link rel="stylesheet" crossorigin href="/assets/index-B5UAXuco.css">
|
|
21
21
|
</head>
|
|
22
22
|
<body>
|
package/dist-cli/cli.js
CHANGED
|
@@ -195,24 +195,41 @@ async function fetchLogin(token) {
|
|
|
195
195
|
const { json } = await rest(token, "/user");
|
|
196
196
|
return json.login;
|
|
197
197
|
}
|
|
198
|
-
async function listRecentRepos(token, since, includePrivate) {
|
|
199
|
-
const repos = [];
|
|
198
|
+
async function listRecentRepos(token, since, includePrivate, orgs = []) {
|
|
200
199
|
const visibility = "";
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
200
|
+
const urls = [
|
|
201
|
+
`/user/repos?sort=pushed&per_page=100&affiliation=owner${visibility}`,
|
|
202
|
+
// --orgで足した組織。トークンが見える範囲(type=all)をpushed降順で
|
|
203
|
+
...orgs.map((o) => `/orgs/${encodeURIComponent(o)}/repos?sort=pushed&per_page=100&type=all`)
|
|
204
|
+
];
|
|
205
|
+
const seen = /* @__PURE__ */ new Set();
|
|
206
|
+
const repos = [];
|
|
207
|
+
for (const start of urls) {
|
|
208
|
+
let url = start;
|
|
209
|
+
while (url) {
|
|
210
|
+
const { json, next } = await rest(token, url);
|
|
211
|
+
const page = json;
|
|
212
|
+
for (const r of page) {
|
|
213
|
+
if (!seen.has(r.full_name)) {
|
|
214
|
+
seen.add(r.full_name);
|
|
215
|
+
repos.push(r);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (page.length === 0 || new Date(page[page.length - 1].pushed_at) < since) break;
|
|
219
|
+
url = next;
|
|
220
|
+
}
|
|
208
221
|
}
|
|
209
222
|
return repos.filter((r) => {
|
|
210
223
|
return new Date(r.pushed_at) >= since;
|
|
211
224
|
});
|
|
212
225
|
}
|
|
213
|
-
|
|
226
|
+
function commitsUrl(fullName, since, author) {
|
|
227
|
+
const base = `/repos/${fullName}/commits?since=${since.toISOString()}&per_page=100`;
|
|
228
|
+
return author ? `${base}&author=${encodeURIComponent(author)}` : base;
|
|
229
|
+
}
|
|
230
|
+
async function listCommits(token, repo, since, author) {
|
|
214
231
|
const commits = [];
|
|
215
|
-
let url =
|
|
232
|
+
let url = commitsUrl(repo.full_name, since, author);
|
|
216
233
|
let pages = 0;
|
|
217
234
|
while (url && pages < MAX_COMMIT_PAGES) {
|
|
218
235
|
let result;
|
|
@@ -228,6 +245,19 @@ async function listCommits(token, repo, since) {
|
|
|
228
245
|
}
|
|
229
246
|
return commits.filter((c) => c.author?.type !== "Bot");
|
|
230
247
|
}
|
|
248
|
+
function parsePrMeta(prs) {
|
|
249
|
+
const out = /* @__PURE__ */ new Map();
|
|
250
|
+
for (const n of prs?.nodes ?? []) {
|
|
251
|
+
out.set(n.number, {
|
|
252
|
+
author: n.author?.login ?? null,
|
|
253
|
+
reviewers: n.reviewRequests.nodes.map((r) => r.requestedReviewer?.login).filter((l) => typeof l === "string"),
|
|
254
|
+
reviewDecision: n.reviewDecision ?? null,
|
|
255
|
+
isDraft: n.isDraft,
|
|
256
|
+
linkedIssues: (n.closingIssuesReferences?.nodes ?? []).map((x) => x.number)
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
return out;
|
|
260
|
+
}
|
|
231
261
|
async function fetchOpenCounts(token, fullNames, limit) {
|
|
232
262
|
const result = /* @__PURE__ */ new Map();
|
|
233
263
|
const CHUNK = 25;
|
|
@@ -236,7 +266,7 @@ async function fetchOpenCounts(token, fullNames, limit) {
|
|
|
236
266
|
await Promise.all(chunks.map((chunk) => limit(async () => {
|
|
237
267
|
const fields = chunk.map((name, j) => {
|
|
238
268
|
const [owner, repo] = name.split("/");
|
|
239
|
-
return `r${j}: repository(owner: ${JSON.stringify(owner)}, name: ${JSON.stringify(repo)}) { issues(states: OPEN) { totalCount } pullRequests(states: OPEN) { totalCount } }`;
|
|
269
|
+
return `r${j}: repository(owner: ${JSON.stringify(owner)}, name: ${JSON.stringify(repo)}) { issues(states: OPEN) { totalCount } pullRequests(states: OPEN, first: 50) { totalCount nodes { number isDraft reviewDecision author { login } reviewRequests(first: 10) { nodes { requestedReviewer { ... on User { login } ... on Team { name } } } } closingIssuesReferences(first: 10) { nodes { number } } } } }`;
|
|
240
270
|
}).join("\n");
|
|
241
271
|
const res = await fetch(`${API}/graphql`, {
|
|
242
272
|
method: "POST",
|
|
@@ -251,39 +281,52 @@ ${fields}
|
|
|
251
281
|
const node = json.data?.[`r${j}`];
|
|
252
282
|
result.set(name, {
|
|
253
283
|
pr: node?.pullRequests.totalCount ?? 0,
|
|
254
|
-
issue: node?.issues.totalCount ?? 0
|
|
284
|
+
issue: node?.issues.totalCount ?? 0,
|
|
285
|
+
prMeta: parsePrMeta(node?.pullRequests)
|
|
255
286
|
});
|
|
256
287
|
});
|
|
257
288
|
})));
|
|
258
289
|
return result;
|
|
259
290
|
}
|
|
260
|
-
|
|
291
|
+
function issuesUrls(fullName, login) {
|
|
292
|
+
const base = `/repos/${fullName}/issues?state=open&per_page=100`;
|
|
293
|
+
if (!login) return [base];
|
|
294
|
+
const me = encodeURIComponent(login);
|
|
295
|
+
return [`${base}&creator=${me}`, `${base}&assignee=${me}`];
|
|
296
|
+
}
|
|
297
|
+
async function listOpenIssues(token, repo, login) {
|
|
261
298
|
const items = [];
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
let
|
|
266
|
-
|
|
267
|
-
result
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
299
|
+
const seen = /* @__PURE__ */ new Set();
|
|
300
|
+
for (const start of issuesUrls(repo.full_name, login)) {
|
|
301
|
+
let url = start;
|
|
302
|
+
let pages = 0;
|
|
303
|
+
while (url && pages < 5) {
|
|
304
|
+
let result;
|
|
305
|
+
try {
|
|
306
|
+
result = await rest(token, url);
|
|
307
|
+
} catch (err) {
|
|
308
|
+
if (err instanceof Error && err.message.includes("HTTP 409")) return [];
|
|
309
|
+
throw err;
|
|
310
|
+
}
|
|
311
|
+
for (const it of result.json) {
|
|
312
|
+
if (seen.has(it.number)) continue;
|
|
313
|
+
seen.add(it.number);
|
|
314
|
+
items.push({
|
|
315
|
+
repo: repo.full_name,
|
|
316
|
+
type: it.pull_request ? "pr" : "issue",
|
|
317
|
+
number: it.number,
|
|
318
|
+
title: it.title,
|
|
319
|
+
state: it.state,
|
|
320
|
+
labels: it.labels.map((l) => l.name),
|
|
321
|
+
updatedAt: it.updated_at,
|
|
322
|
+
url: it.html_url,
|
|
323
|
+
comments: it.comments,
|
|
324
|
+
author: it.user?.login ?? null
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
url = result.next;
|
|
328
|
+
pages++;
|
|
284
329
|
}
|
|
285
|
-
url = result.next;
|
|
286
|
-
pages++;
|
|
287
330
|
}
|
|
288
331
|
return items;
|
|
289
332
|
}
|
|
@@ -312,8 +355,9 @@ async function fetchActivity(token, from, opts) {
|
|
|
312
355
|
const limit = createLimiter(CONCURRENCY);
|
|
313
356
|
const [login, recentRepos] = await Promise.all([
|
|
314
357
|
fetchLogin(token),
|
|
315
|
-
listRecentRepos(token, from)
|
|
358
|
+
listRecentRepos(token, from, opts.includePrivate, opts.orgs ?? [])
|
|
316
359
|
]);
|
|
360
|
+
const mineOnly = (r) => r.owner.login.toLowerCase() === login.toLowerCase() ? null : login;
|
|
317
361
|
const openCounts = await fetchOpenCounts(token, recentRepos.map((r) => r.full_name), limit);
|
|
318
362
|
const issueRepos = recentRepos.filter((r) => {
|
|
319
363
|
const c = openCounts.get(r.full_name);
|
|
@@ -322,7 +366,7 @@ async function fetchActivity(token, from, opts) {
|
|
|
322
366
|
const progress = createProgress(recentRepos.length, issueRepos.length);
|
|
323
367
|
const commitsPromise = Promise.all(
|
|
324
368
|
recentRepos.map(
|
|
325
|
-
(r) => limit(() => listCommits(token, r, from)).then((commits) => {
|
|
369
|
+
(r) => limit(() => listCommits(token, r, from, mineOnly(r))).then((commits) => {
|
|
326
370
|
progress.repoDone();
|
|
327
371
|
return commits;
|
|
328
372
|
})
|
|
@@ -330,7 +374,7 @@ async function fetchActivity(token, from, opts) {
|
|
|
330
374
|
);
|
|
331
375
|
const issuesPromise = Promise.all(
|
|
332
376
|
issueRepos.map(
|
|
333
|
-
(r) => limit(() => listOpenIssues(token, r)).then((items) => {
|
|
377
|
+
(r) => limit(() => listOpenIssues(token, r, mineOnly(r))).then((items) => {
|
|
334
378
|
progress.issueDone();
|
|
335
379
|
return items;
|
|
336
380
|
})
|
|
@@ -367,7 +411,11 @@ async function fetchActivity(token, from, opts) {
|
|
|
367
411
|
}))
|
|
368
412
|
});
|
|
369
413
|
});
|
|
370
|
-
const issues = issuesByRepo.flat()
|
|
414
|
+
const issues = issuesByRepo.flat().map((it) => {
|
|
415
|
+
if (it.type !== "pr") return it;
|
|
416
|
+
const meta = openCounts.get(it.repo)?.prMeta.get(it.number);
|
|
417
|
+
return meta ? { ...it, ...meta } : it;
|
|
418
|
+
});
|
|
371
419
|
return { login, repos, activity, issues };
|
|
372
420
|
}
|
|
373
421
|
const REPO_CACHE_DIR = join(import.meta.dirname, "..", "public", "data");
|
|
@@ -382,9 +430,11 @@ async function runSync(opts) {
|
|
|
382
430
|
const from = new Date(to.getTime() - windowDays * 24 * 60 * 60 * 1e3);
|
|
383
431
|
log(`GitHub RepoRadar sync: ${from.toISOString().slice(0, 10)} 〜 ${to.toISOString().slice(0, 10)} (${windowDays}日)`);
|
|
384
432
|
const token = getToken();
|
|
385
|
-
const
|
|
433
|
+
const orgs = opts.orgs ?? [];
|
|
434
|
+
if (orgs.length > 0) log(`組織も取得: ${orgs.join(", ")}(自分がauthorのコミットだけ数える)`);
|
|
435
|
+
const { login, repos, activity, issues } = await fetchActivity(token, from, { includePrivate: true, orgs });
|
|
386
436
|
mkdirSync(opts.dataDir, { recursive: true });
|
|
387
|
-
const meta = { lastSyncAt: to.toISOString(), login, windowDays, includesPrivate: true };
|
|
437
|
+
const meta = { lastSyncAt: to.toISOString(), login, windowDays, includesPrivate: true, ...orgs.length > 0 ? { orgs } : {} };
|
|
388
438
|
writeFileSync(join(opts.dataDir, "repos.json"), JSON.stringify(repos, null, 2));
|
|
389
439
|
writeFileSync(join(opts.dataDir, "activity.json"), JSON.stringify(activity, null, 2));
|
|
390
440
|
writeFileSync(join(opts.dataDir, "issues.json"), JSON.stringify(issues, null, 2));
|
|
@@ -414,7 +464,8 @@ const DEFAULTS = {
|
|
|
414
464
|
dataDir: null,
|
|
415
465
|
includePrivate: false,
|
|
416
466
|
sort: "active",
|
|
417
|
-
limit: 15
|
|
467
|
+
limit: 15,
|
|
468
|
+
orgs: []
|
|
418
469
|
};
|
|
419
470
|
function intIn(flag, raw, min, max) {
|
|
420
471
|
if (raw === void 0 || raw.startsWith("--")) throw new RangeError(`${flag} には値が要ります`);
|
|
@@ -425,7 +476,7 @@ function intIn(flag, raw, min, max) {
|
|
|
425
476
|
return n;
|
|
426
477
|
}
|
|
427
478
|
function parseArgs(argv) {
|
|
428
|
-
const out = { ...DEFAULTS };
|
|
479
|
+
const out = { ...DEFAULTS, orgs: [] };
|
|
429
480
|
let command = null;
|
|
430
481
|
for (let i = 0; i < argv.length; i++) {
|
|
431
482
|
const a = argv[i];
|
|
@@ -458,6 +509,14 @@ function parseArgs(argv) {
|
|
|
458
509
|
out.dataDir = v;
|
|
459
510
|
break;
|
|
460
511
|
}
|
|
512
|
+
case "--org": {
|
|
513
|
+
const v = argv[++i];
|
|
514
|
+
if (v === void 0 || v.startsWith("--")) throw new RangeError(`${a}には値が要ります`);
|
|
515
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9-]*$/.test(v)) throw new RangeError(`--orgはGitHubの組織名(英数字とハイフン)で指定してください(受け取った値: ${v})`);
|
|
516
|
+
if (out.orgs.includes(v)) throw new RangeError(`--org ${v}が重複しています`);
|
|
517
|
+
out.orgs.push(v);
|
|
518
|
+
break;
|
|
519
|
+
}
|
|
461
520
|
case "--sort": {
|
|
462
521
|
const v = argv[++i];
|
|
463
522
|
if (!SORTS.includes(v)) throw new RangeError(`--sortは${SORTS.join(" | ")} のどれか(受け取った値: ${v})`);
|
|
@@ -491,6 +550,7 @@ const USAGE = [
|
|
|
491
550
|
row("--port N", "待ち受けポート(既定5177。使用中なら次を探す)"),
|
|
492
551
|
row("--no-open", "ブラウザを開かない"),
|
|
493
552
|
row("--no-sync", "同期せずに手元のデータで開く"),
|
|
553
|
+
row("--org NAME", "組織のrepoも取得する(繰り返し可)。組織では自分がauthorのコミットと、自分が作ったか担当のissue/PRだけ数える"),
|
|
494
554
|
row("--data-dir DIR", "データの置き場(既定: $XDG_CACHE_HOME か ~/.cache の github-reporadar/data)"),
|
|
495
555
|
row("--sort KEY", "brief の並び: active | recent | stale"),
|
|
496
556
|
row("--limit N", "brief の件数(既定15)"),
|
|
@@ -625,7 +685,7 @@ async function main() {
|
|
|
625
685
|
}
|
|
626
686
|
const dataDir = args.dataDir ?? defaultDataDir(process.env, homedir());
|
|
627
687
|
if (args.command === "sync" || args.command === "serve" && args.sync) {
|
|
628
|
-
await runSync({ dataDir, days: args.days });
|
|
688
|
+
await runSync({ dataDir, days: args.days, orgs: args.orgs });
|
|
629
689
|
}
|
|
630
690
|
if (args.command === "sync") return;
|
|
631
691
|
if (args.command === "brief") {
|