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/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-7rO9STC2.js"></script>
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
- let url = `/user/repos?sort=pushed&per_page=100&affiliation=owner${visibility}`;
202
- while (url) {
203
- const { json, next } = await rest(token, url);
204
- const page = json;
205
- repos.push(...page);
206
- if (page.length === 0 || new Date(page[page.length - 1].pushed_at) < since) break;
207
- url = next;
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
- async function listCommits(token, repo, since) {
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 = `/repos/${repo.full_name}/commits?since=${since.toISOString()}&per_page=100`;
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
- async function listOpenIssues(token, repo) {
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
- let url = `/repos/${repo.full_name}/issues?state=open&per_page=100`;
263
- let pages = 0;
264
- while (url && pages < 5) {
265
- let result;
266
- try {
267
- result = await rest(token, url);
268
- } catch (err) {
269
- if (err instanceof Error && err.message.includes("HTTP 409")) return [];
270
- throw err;
271
- }
272
- for (const it of result.json) {
273
- items.push({
274
- repo: repo.full_name,
275
- type: it.pull_request ? "pr" : "issue",
276
- number: it.number,
277
- title: it.title,
278
- state: it.state,
279
- labels: it.labels.map((l) => l.name),
280
- updatedAt: it.updated_at,
281
- url: it.html_url,
282
- comments: it.comments
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 { login, repos, activity, issues } = await fetchActivity(token, from);
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") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "github-reporadar",
3
- "version": "0.1.1",
3
+ "version": "0.1.4",
4
4
  "description": "自分のGitHub活動を一晩ぶん眺める計器盤。npx github-reporadarでローカルに開く",
5
5
  "license": "MIT",
6
6
  "bin": {