Der Codeberg-Sync speichert den Ersteller (user.login) von Issues und PRs in der neuen Ticket-Spalte author (Migration 010). Die Buero-Visualisierung zaehlt offene Tickets (alles ausser Done), deren Ersteller weder fds2610 noch fds2610-bot ist, und zeigt die Anzahl in einer kleinen roten Fahne auf dem PO-Tisch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
163 lines
5 KiB
PHP
163 lines
5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Monitor\Api\Sync;
|
|
|
|
use RuntimeException;
|
|
|
|
final class CurlCodebergClient implements CodebergClient
|
|
{
|
|
private const PER_PAGE = 50;
|
|
private const MAX_PAGES = 20;
|
|
|
|
public function __construct(
|
|
private readonly string $apiBase,
|
|
private readonly ?string $token = null,
|
|
) {
|
|
}
|
|
|
|
public function listIssues(string $owner, string $repo, string $state): array
|
|
{
|
|
$issues = [];
|
|
|
|
for ($page = 1; $page <= self::MAX_PAGES; $page++) {
|
|
$url = sprintf(
|
|
'%s/repos/%s/%s/issues?state=%s&type=issues&page=%d&limit=%d',
|
|
$this->apiBase,
|
|
rawurlencode($owner),
|
|
rawurlencode($repo),
|
|
rawurlencode($state),
|
|
$page,
|
|
self::PER_PAGE
|
|
);
|
|
|
|
$pageIssues = $this->fetch($url);
|
|
if ($pageIssues === []) {
|
|
break;
|
|
}
|
|
|
|
foreach ($pageIssues as $issue) {
|
|
$issues[] = [
|
|
'number' => (int) $issue['number'],
|
|
'title' => (string) $issue['title'],
|
|
'body' => isset($issue['body']) ? (string) $issue['body'] : null,
|
|
'author' => self::authorLogin($issue),
|
|
'labels' => array_map(
|
|
static fn (array $label): array => [
|
|
'name' => (string) $label['name'],
|
|
'color' => (string) $label['color'],
|
|
],
|
|
$issue['labels'] ?? []
|
|
),
|
|
];
|
|
}
|
|
|
|
if (count($pageIssues) < self::PER_PAGE) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return $issues;
|
|
}
|
|
|
|
public function listOpenPullRequests(string $owner, string $repo): array
|
|
{
|
|
$pulls = [];
|
|
|
|
for ($page = 1; $page <= self::MAX_PAGES; $page++) {
|
|
$url = sprintf(
|
|
'%s/repos/%s/%s/issues?state=open&type=pulls&page=%d&limit=%d',
|
|
$this->apiBase,
|
|
rawurlencode($owner),
|
|
rawurlencode($repo),
|
|
$page,
|
|
self::PER_PAGE
|
|
);
|
|
|
|
$pagePulls = $this->fetch($url);
|
|
|
|
foreach ($pagePulls as $pull) {
|
|
$pulls[] = [
|
|
'number' => (int) $pull['number'],
|
|
'title' => (string) ($pull['title'] ?? ''),
|
|
'body' => isset($pull['body']) ? (string) $pull['body'] : null,
|
|
'author' => self::authorLogin($pull),
|
|
];
|
|
}
|
|
|
|
if (count($pagePulls) < self::PER_PAGE) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return $pulls;
|
|
}
|
|
|
|
/**
|
|
* Issue #113: Login des Kontos, das Issue/PR angelegt hat (Feld "user" der Codeberg-API).
|
|
*
|
|
* @param array<string, mixed> $item
|
|
*/
|
|
private static function authorLogin(array $item): ?string
|
|
{
|
|
$login = is_array($item['user'] ?? null) ? ($item['user']['login'] ?? null) : null;
|
|
|
|
return is_string($login) && $login !== '' ? $login : null;
|
|
}
|
|
|
|
public function repoPermissions(string $owner, string $repo): array
|
|
{
|
|
$url = sprintf('%s/repos/%s/%s', $this->apiBase, rawurlencode($owner), rawurlencode($repo));
|
|
$data = $this->fetch($url);
|
|
$permissions = is_array($data['permissions'] ?? null) ? $data['permissions'] : [];
|
|
|
|
return ['push' => (bool) ($permissions['push'] ?? false)];
|
|
}
|
|
|
|
public function listWebhooks(string $owner, string $repo): array
|
|
{
|
|
$url = sprintf('%s/repos/%s/%s/hooks', $this->apiBase, rawurlencode($owner), rawurlencode($repo));
|
|
$data = $this->fetch($url);
|
|
|
|
return array_map(
|
|
static fn (array $hook): array => [
|
|
'active' => (bool) ($hook['active'] ?? false),
|
|
'url' => (string) ($hook['config']['url'] ?? ''),
|
|
'content_type' => (string) ($hook['config']['content_type'] ?? ''),
|
|
],
|
|
$data
|
|
);
|
|
}
|
|
|
|
/** @return array<int|string, mixed> */
|
|
private function fetch(string $url): array
|
|
{
|
|
$headers = ['Accept: application/json'];
|
|
if ($this->token !== null && $this->token !== '') {
|
|
$headers[] = 'Authorization: token ' . $this->token;
|
|
}
|
|
|
|
$ch = curl_init($url);
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_HTTPHEADER => $headers,
|
|
CURLOPT_TIMEOUT => 15,
|
|
]);
|
|
$body = curl_exec($ch);
|
|
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
$error = curl_error($ch);
|
|
curl_close($ch);
|
|
|
|
if ($body === false) {
|
|
throw new RuntimeException('Codeberg-API nicht erreichbar: ' . $error);
|
|
}
|
|
if ($status >= 400) {
|
|
throw new CodebergApiException($status, 'Codeberg-API-Fehler (HTTP ' . $status . '): ' . $body);
|
|
}
|
|
|
|
$decoded = json_decode((string) $body, true);
|
|
|
|
return is_array($decoded) ? $decoded : [];
|
|
}
|
|
}
|