slugger = new AsciiSlugger('fr'); } public function testConnection(): bool { try { $this->request('GET', '/api/v1/version'); return true; } catch (GiteaApiException) { return false; } } /** * @return array */ public function listRepositories(): array { $result = []; $page = 1; do { $data = $this->request('GET', '/api/v1/repos/search', [ 'query' => ['page' => $page, 'limit' => 50], ]); $result = array_merge($result, $data['data'] ?? []); ++$page; } while (!empty($data['data']) && 50 === count($data['data'])); return $result; } public function getDefaultBranch(Project $project): string { $this->assertProjectHasRepo($project); $data = $this->request('GET', sprintf( '/api/v1/repos/%s/%s', $project->getGiteaOwner(), $project->getGiteaRepo(), )); return $data['default_branch'] ?? 'main'; } public function createBranch(Project $project, Task $task, string $type, string $baseBranch): string { $this->assertProjectHasRepo($project); $branchName = $this->generateBranchName($task, $type); $this->request('POST', sprintf( '/api/v1/repos/%s/%s/branches', $project->getGiteaOwner(), $project->getGiteaRepo(), ), [ 'json' => [ 'new_branch_name' => $branchName, 'old_branch_name' => $baseBranch, ], ]); return $branchName; } public function generateBranchName(Task $task, string $type): string { $project = $task->getProject(); if (null === $project) { throw new GiteaApiException('Task has no project.'); } $slug = $this->slugger->slug($task->getTitle())->lower()->truncate(50)->toString(); $slug = rtrim($slug, '-'); return sprintf('%s/%s-%d-%s', $type, $project->getCode(), $task->getNumber(), $slug); } /** * @return array */ public function listBranches(Project $project, string $taskCode): array { $this->assertProjectHasRepo($project); $allBranches = []; $page = 1; do { $pageBranches = $this->request('GET', sprintf( '/api/v1/repos/%s/%s/branches', $project->getGiteaOwner(), $project->getGiteaRepo(), ), [ 'query' => ['page' => $page, 'limit' => 50], ]); $allBranches = array_merge($allBranches, $pageBranches); ++$page; } while (!empty($pageBranches) && 50 === count($pageBranches)); $regex = sprintf('#^[^/]+/%s($|-.+)#', preg_quote($taskCode, '#')); return array_values(array_filter( $allBranches, static fn (array $branch): bool => 1 === preg_match($regex, $branch['name']), )); } /** * @return array */ public function listBranchCommits(Project $project, string $branch): array { $this->assertProjectHasRepo($project); $defaultBranch = $this->getDefaultBranch($project); $data = $this->request('GET', sprintf( '/api/v1/repos/%s/%s/compare/%s...%s', $project->getGiteaOwner(), $project->getGiteaRepo(), $defaultBranch, urlencode($branch), )); return $data['commits'] ?? []; } /** * @return array */ public function listPullRequests(Project $project, string $taskCode): array { $this->assertProjectHasRepo($project); $branches = $this->listBranches($project, $taskCode); $prs = []; foreach ($branches as $branch) { $branchPrs = $this->request('GET', sprintf( '/api/v1/repos/%s/%s/pulls', $project->getGiteaOwner(), $project->getGiteaRepo(), ), [ 'query' => ['state' => 'all', 'head' => $branch['name']], ]); $prs = array_merge($prs, $branchPrs); } // Fetch CI status for each PR foreach ($prs as &$pr) { $sha = $pr['head']['sha'] ?? null; if (null !== $sha) { try { $pr['ci_statuses'] = $this->request('GET', sprintf( '/api/v1/repos/%s/%s/commits/%s/statuses', $project->getGiteaOwner(), $project->getGiteaRepo(), $sha, )); } catch (GiteaApiException) { $pr['ci_statuses'] = []; } } } return $prs; } private function getConfiguration(): GiteaConfiguration { $config = $this->configRepository->findSingleton(); if (null === $config) { throw new GiteaApiException('Gitea is not configured.'); } return $config; } private function getDecryptedToken(GiteaConfiguration $config): string { $encrypted = $config->getEncryptedToken(); if (null === $encrypted) { throw new GiteaApiException('Gitea token is not set.'); } try { return $this->tokenEncryptor->decrypt($encrypted); } catch (Throwable $e) { throw new GiteaApiException('Failed to decrypt Gitea token: '.$e->getMessage(), 0, $e); } } private function extractGiteaError(HttpExceptionInterface $e): string { try { $body = $e->getResponse()->getContent(false); $data = json_decode($body, true); if (is_array($data)) { return $data['message'] ?? $data['error'] ?? $body; } return $body ?: 'Unknown Gitea error'; } catch (ExceptionInterface) { return 'Gitea API error (HTTP '.$e->getResponse()->getStatusCode().')'; } } private function assertProjectHasRepo(Project $project): void { if (!$project->hasGiteaRepo()) { throw new GiteaApiException('Project has no Gitea repository configured.'); } } /** * @param array $options */ private function request(string $method, string $path, array $options = []): array { $config = $this->getConfiguration(); $token = $this->getDecryptedToken($config); $options['headers'] = array_merge($options['headers'] ?? [], [ 'Authorization' => 'token '.$token, 'Accept' => 'application/json', ]); $options['timeout'] = 10; try { $response = $this->httpClient->request($method, rtrim($config->getUrl(), '/').$path, $options); return $response->toArray(); } catch (HttpExceptionInterface $e) { $message = $this->extractGiteaError($e); throw new GiteaApiException($message, $e->getResponse()->getStatusCode(), $e); } catch (ExceptionInterface $e) { throw new GiteaApiException('Gitea API error: '.$e->getMessage(), 0, $e); } } }