From b7fd6636d619f01abbc35e585ad031e425b8def9 Mon Sep 17 00:00:00 2001 From: Jonathan Danse Date: Wed, 1 Jul 2026 09:44:46 +0200 Subject: [PATCH 1/3] feat: capture issue author identity in fetch:issues --- src/Command/FetchIssuesCommand.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Command/FetchIssuesCommand.php b/src/Command/FetchIssuesCommand.php index 669a5d7..fe0215d 100644 --- a/src/Command/FetchIssuesCommand.php +++ b/src/Command/FetchIssuesCommand.php @@ -73,6 +73,11 @@ protected function fetchOrgIssues(): void number author { login + avatarUrl + url + ... on User { + name + } } repository { name @@ -91,9 +96,13 @@ protected function fetchOrgIssues(): void $issuesCount += count($nodes); foreach ($nodes as $node) { + $author = $node['author'] ?? null; $issues[] = [ 'number' => $node['number'], - 'login' => $node['author']['login'] ?? null, + 'login' => $author['login'] ?? null, + 'name' => $author['name'] ?? null, + 'avatar_url' => $author['avatarUrl'] ?? null, + 'html_url' => $author['url'] ?? null, 'repository' => $node['repository']['name'], ]; } From 594d350cb178db87612fca9bc7e0f42a83877852 Mon Sep 17 00:00:00 2001 From: Jonathan Danse Date: Wed, 1 Jul 2026 09:46:22 +0200 Subject: [PATCH 2/3] feat: capture PR author + reviewer identity in fetch:pullrequests:all --- src/Command/FetchPullRequestsAllCommand.php | 30 +++++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/src/Command/FetchPullRequestsAllCommand.php b/src/Command/FetchPullRequestsAllCommand.php index e03fa90..896237a 100644 --- a/src/Command/FetchPullRequestsAllCommand.php +++ b/src/Command/FetchPullRequestsAllCommand.php @@ -73,11 +73,21 @@ protected function fetchOrgPullRequests(): void number author { login + avatarUrl + url + ... on User { + name + } } reviews(first: 100) { nodes { author { login + avatarUrl + url + ... on User { + name + } } } } @@ -97,15 +107,25 @@ protected function fetchOrgPullRequests(): void foreach ($nodes as $node) { $reviewers = []; foreach ($node['reviews']['nodes'] as $review) { - $reviewerLogin = $review['author']['login'] ?? null; - if ($reviewerLogin !== null) { - $reviewers[$reviewerLogin] = true; + $reviewer = $review['author'] ?? null; + $reviewerLogin = $reviewer['login'] ?? null; + if ($reviewerLogin !== null && !isset($reviewers[$reviewerLogin])) { + $reviewers[$reviewerLogin] = [ + 'login' => $reviewerLogin, + 'name' => $reviewer['name'] ?? null, + 'avatar_url' => $reviewer['avatarUrl'] ?? null, + 'html_url' => $reviewer['url'] ?? null, + ]; } } + $author = $node['author'] ?? null; $pullRequests[] = [ 'number' => $node['number'], - 'login' => $node['author']['login'] ?? null, - 'reviewers' => array_keys($reviewers), + 'login' => $author['login'] ?? null, + 'name' => $author['name'] ?? null, + 'avatar_url' => $author['avatarUrl'] ?? null, + 'html_url' => $author['url'] ?? null, + 'reviewers' => array_values($reviewers), ]; } From 7925e07ea7f7eb2585b13b2ca220913a6f2a0350 Mon Sep 17 00:00:00 2001 From: Jonathan Danse Date: Wed, 1 Jul 2026 11:18:51 +0200 Subject: [PATCH 3/3] feat: full-population ranking from fetched identity, no getUser --- src/Command/GenerateTopStatsCommand.php | 94 ++++++++++++++++--------- 1 file changed, 62 insertions(+), 32 deletions(-) diff --git a/src/Command/GenerateTopStatsCommand.php b/src/Command/GenerateTopStatsCommand.php index 9548388..62e2118 100644 --- a/src/Command/GenerateTopStatsCommand.php +++ b/src/Command/GenerateTopStatsCommand.php @@ -5,12 +5,9 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; -use Throwable; class GenerateTopStatsCommand extends AbstractCommand { - private const TOP_N = 25; - protected function configure(): void { $this->setName('traces:generate:topstats') @@ -27,6 +24,10 @@ protected function configure(): void protected function execute(InputInterface $input, OutputInterface $output): int { + // This command makes no GitHub API call: identity comes from the fetched + // files. The --ghtoken option is kept only for pipeline compatibility + // (Makefile / gh-pages.yml pass it); parent::execute() still sets up an + // unused Github client. parent::execute($input, $output); // contributors_prs.json is produced by traces:generate:topcompanies, so this @@ -45,21 +46,22 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->fetchConfiguration($input->getOption('config')); - /** @var array}> $pullRequests */ + /** @var array}> $pullRequests */ $pullRequests = json_decode(file_get_contents(self::FILE_PULLREQUESTS_ALL) ?: '', true); - /** @var array $issues */ + /** @var array $issues */ $issues = json_decode(file_get_contents(self::FILE_ISSUES) ?: '', true); /** @var array $contributors */ $contributors = json_decode(file_get_contents(self::FILE_CONTRIBUTORS_PRS) ?: '', true); + $identities = $this->buildIdentities($pullRequests, $issues); $aggregate = $this->aggregate($pullRequests, $issues); $this->enrichContributors($contributors, $aggregate); file_put_contents(self::FILE_CONTRIBUTORS_PRS, json_encode($contributors, JSON_PRETTY_PRINT)); - file_put_contents(self::FILE_TOP_REVIEWERS, json_encode($this->buildRanking($aggregate['reviews'], $contributors), JSON_PRETTY_PRINT)); - file_put_contents(self::FILE_TOP_ISSUES, json_encode($this->buildRanking($aggregate['issuesOpened'], $contributors), JSON_PRETTY_PRINT)); - file_put_contents(self::FILE_TOP_PULLREQUESTS, json_encode($this->buildRanking($aggregate['pullRequestsOpened'], $contributors), JSON_PRETTY_PRINT)); + file_put_contents(self::FILE_TOP_REVIEWERS, json_encode($this->buildRanking($aggregate['reviews'], $contributors, $identities), JSON_PRETTY_PRINT)); + file_put_contents(self::FILE_TOP_ISSUES, json_encode($this->buildRanking($aggregate['issuesOpened'], $contributors, $identities), JSON_PRETTY_PRINT)); + file_put_contents(self::FILE_TOP_PULLREQUESTS, json_encode($this->buildRanking($aggregate['pullRequestsOpened'], $contributors, $identities), JSON_PRETTY_PRINT)); $this->output->writeLn(['', 'Top stats generated.']); @@ -67,8 +69,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int } /** - * @param array}> $pullRequests - * @param array $issues + * @param array}> $pullRequests + * @param array $issues * * @return array{reviews: array, issuesOpened: array, pullRequestsOpened: array} */ @@ -86,10 +88,11 @@ public function aggregate(array $pullRequests, array $issues): array // A review counts for its reviewer regardless of the PR author (even a // deleted/null author) — only self-reviews are excluded. foreach ($pullRequest['reviewers'] as $reviewer) { - if ($reviewer === $author) { + $reviewerLogin = $reviewer['login']; + if ($reviewerLogin === $author) { continue; } - $reviews[$reviewer] = ($reviews[$reviewer] ?? 0) + 1; + $reviews[$reviewerLogin] = ($reviews[$reviewerLogin] ?? 0) + 1; } } @@ -107,6 +110,39 @@ public function aggregate(array $pullRequests, array $issues): array ]; } + /** + * @param array}> $pullRequests + * @param array $issues + * + * @return array + */ + public function buildIdentities(array $pullRequests, array $issues): array + { + $identities = []; + $add = function (?string $login, ?string $name, ?string $avatarUrl, ?string $url) use (&$identities): void { + if ($login === null || isset($identities[$login])) { + return; + } + $identities[$login] = [ + 'name' => $name ?? $login, + 'avatar_url' => $avatarUrl ?? '', + 'html_url' => $url ?? 'https://github.com/' . $login, + ]; + }; + + foreach ($pullRequests as $pullRequest) { + $add($pullRequest['login'] ?? null, $pullRequest['name'] ?? null, $pullRequest['avatar_url'] ?? null, $pullRequest['html_url'] ?? null); + foreach ($pullRequest['reviewers'] as $reviewer) { + $add($reviewer['login'], $reviewer['name'] ?? null, $reviewer['avatar_url'] ?? null, $reviewer['html_url'] ?? null); + } + } + foreach ($issues as $issue) { + $add($issue['login'] ?? null, $issue['name'] ?? null, $issue['avatar_url'] ?? null, $issue['html_url'] ?? null); + } + + return $identities; + } + /** * @param array $contributors * @param array{reviews: array, issuesOpened: array, pullRequestsOpened: array} $aggregate @@ -127,10 +163,11 @@ private function enrichContributors(array &$contributors, array $aggregate): voi /** * @param array $counts * @param array $contributors + * @param array $identities * * @return array{updatedAt: string, items: array} */ - private function buildRanking(array $counts, array $contributors): array + private function buildRanking(array $counts, array $contributors, array $identities): array { $logins = array_keys($counts); usort($logins, function (string $a, string $b) use ($counts): int { @@ -146,7 +183,7 @@ private function buildRanking(array $counts, array $contributors): array if (!$this->configKeepExcludedUsers && in_array($login, $this->configExclusions, true)) { continue; } - $identity = $this->resolveIdentity($login, $contributors); + $identity = $this->resolveIdentity($login, $contributors, $identities); $items[] = [ 'rank' => $rank, 'login' => $login, @@ -155,9 +192,7 @@ private function buildRanking(array $counts, array $contributors): array 'html_url' => $identity['html_url'], 'count' => $counts[$login], ]; - if (++$rank > self::TOP_N) { - break; - } + ++$rank; } return ['updatedAt' => date('Y-m-d'), 'items' => $items]; @@ -165,10 +200,11 @@ private function buildRanking(array $counts, array $contributors): array /** * @param array $contributors + * @param array $identities * * @return array{name:string, avatar_url:string, html_url:string} */ - private function resolveIdentity(string $login, array $contributors): array + private function resolveIdentity(string $login, array $contributors, array $identities): array { if (isset($contributors[$login]) && is_array($contributors[$login])) { $record = $contributors[$login]; @@ -180,20 +216,14 @@ private function resolveIdentity(string $login, array $contributors): array ]; } - try { - $user = $this->github->getUser($login); - - return [ - 'name' => (string) ($user['name'] ?? $login), - 'avatar_url' => (string) ($user['avatar_url'] ?? ''), - 'html_url' => (string) ($user['html_url'] ?? 'https://github.com/' . $login), - ]; - } catch (Throwable) { - return [ - 'name' => $login, - 'avatar_url' => '', - 'html_url' => 'https://github.com/' . $login, - ]; + if (isset($identities[$login])) { + return $identities[$login]; } + + return [ + 'name' => $login, + 'avatar_url' => '', + 'html_url' => 'https://github.com/' . $login, + ]; } }