feat : add GiteaApiService with branch/commit/PR methods

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Matthieu
2026-03-13 13:57:01 +01:00
parent 0b8e2bfc63
commit 136d0eaaa4

View File

@@ -0,0 +1,238 @@
<?php
declare(strict_types=1);
namespace App\Service;
use App\Entity\GiteaConfiguration;
use App\Entity\Project;
use App\Entity\Task;
use App\Exception\GiteaApiException;
use App\Repository\GiteaConfigurationRepository;
use Symfony\Component\String\Slugger\AsciiSlugger;
use Symfony\Component\String\Slugger\SluggerInterface;
use Symfony\Contracts\HttpClient\Exception\ExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
final readonly class GiteaApiService
{
private SluggerInterface $slugger;
public function __construct(
private HttpClientInterface $httpClient,
private GiteaConfigurationRepository $configRepository,
private TokenEncryptor $tokenEncryptor,
) {
$this->slugger = new AsciiSlugger('fr');
}
public function testConnection(): bool
{
try {
$this->request('GET', '/api/v1/version');
return true;
} catch (GiteaApiException) {
return false;
}
}
/**
* @return array<array{full_name: string, name: string, owner: array{login: string}}>
*/
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<array{name: string, commit: 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 function (array $branch) use ($regex): bool {
return 1 === preg_match($regex, $branch['name']);
}));
}
/**
* @return array<array{sha: string, commit: array{message: string, author: array}, created: string}>
*/
public function listCommits(Project $project, string $branch): array
{
$this->assertProjectHasRepo($project);
return $this->request('GET', sprintf(
'/api/v1/repos/%s/%s/commits',
$project->getGiteaOwner(),
$project->getGiteaRepo(),
), [
'query' => ['sha' => $branch, 'limit' => 30],
]);
}
/**
* @return array<array{number: int, title: string, state: string, head: array, user: array, merged: bool}>
*/
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.');
}
return $this->tokenEncryptor->decrypt($encrypted);
}
private function assertProjectHasRepo(Project $project): void
{
if (!$project->hasGiteaRepo()) {
throw new GiteaApiException('Project has no Gitea repository configured.');
}
}
/**
* @param array<string, mixed> $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 (ExceptionInterface $e) {
throw new GiteaApiException('Gitea API error: '.$e->getMessage(), 0, $e);
}
}
}