feat : add GiteaApiService with branch/commit/PR methods
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
238
src/Service/GiteaApiService.php
Normal file
238
src/Service/GiteaApiService.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user