diff --git a/src/Service/GiteaApiService.php b/src/Service/GiteaApiService.php new file mode 100644 index 0000000..d20a71e --- /dev/null +++ b/src/Service/GiteaApiService.php @@ -0,0 +1,238 @@ +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 function (array $branch) use ($regex): bool { + return 1 === preg_match($regex, $branch['name']); + })); + } + + /** + * @return array + */ + 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 + */ + 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 $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); + } + } +}