diff --git a/src/VCS/Adapter/Git/GitLab.php b/src/VCS/Adapter/Git/GitLab.php index 15eeb98c..460a0509 100644 --- a/src/VCS/Adapter/Git/GitLab.php +++ b/src/VCS/Adapter/Git/GitLab.php @@ -168,12 +168,53 @@ public function getInstallationRepository(string $repositoryName): array public function searchRepositories(string $owner, int $page, int $per_page, string $search = ''): array { - throw new Exception("Not implemented"); + $ownerPath = $this->getOwnerPath($owner); + $url = "/groups/{$ownerPath}/projects?page={$page}&per_page={$per_page}"; + + if (!empty($search)) { + $url .= "&search=" . urlencode($search); + } + + $response = $this->call(self::METHOD_GET, $url, ['PRIVATE-TOKEN' => $this->accessToken]); + + $responseHeaders = $response['headers'] ?? []; + $responseHeadersStatusCode = $responseHeaders['status-code'] ?? 0; + if ($responseHeadersStatusCode >= 400) { + return []; + } + + $responseBody = $response['body'] ?? []; + if (!is_array($responseBody)) { + return []; + } + + $repositories = []; + foreach ($responseBody as $repo) { + $repositories[] = [ + 'id' => $repo['id'] ?? 0, + 'name' => $repo['name'] ?? '', + 'description' => $repo['description'] ?? '', + 'private' => ($repo['visibility'] ?? '') === 'private', + ]; + } + + return $repositories; } public function getRepositoryName(string $repositoryId): string { - throw new Exception("Not implemented"); + $url = "/projects/{$repositoryId}"; + + $response = $this->call(self::METHOD_GET, $url, ['PRIVATE-TOKEN' => $this->accessToken]); + + $responseHeaders = $response['headers'] ?? []; + $responseHeadersStatusCode = $responseHeaders['status-code'] ?? 0; + if ($responseHeadersStatusCode >= 400) { + throw new Exception("Repository {$repositoryId} not found"); + } + + $responseBody = $response['body'] ?? []; + return $responseBody['path'] ?? ''; } public function getRepositoryTree(string $owner, string $repositoryName, string $branch, bool $recursive = false): array @@ -260,7 +301,18 @@ public function getUser(string $username): array public function getOwnerName(string $installationId, ?int $repositoryId = null): string { - throw new Exception("Not implemented"); + if ($repositoryId !== null) { + $url = "/projects/{$repositoryId}"; + $response = $this->call(self::METHOD_GET, $url, ['PRIVATE-TOKEN' => $this->accessToken]); + $responseBody = $response['body'] ?? []; + $namespace = $responseBody['namespace'] ?? []; + return $namespace['path'] ?? ''; + } + + $url = "/user"; + $response = $this->call(self::METHOD_GET, $url, ['PRIVATE-TOKEN' => $this->accessToken]); + $responseBody = $response['body'] ?? []; + return $responseBody['username'] ?? ''; } public function getPullRequest(string $owner, string $repositoryName, int $pullRequestNumber): array @@ -280,7 +332,31 @@ public function getPullRequestFromBranch(string $owner, string $repositoryName, public function listBranches(string $owner, string $repositoryName): array { - throw new Exception("Not implemented"); + $ownerPath = $this->getOwnerPath($owner); + $projectPath = urlencode("{$ownerPath}/{$repositoryName}"); + $url = "/projects/{$projectPath}/repository/branches"; + + $response = $this->call(self::METHOD_GET, $url, ['PRIVATE-TOKEN' => $this->accessToken]); + + $responseHeaders = $response['headers'] ?? []; + $responseHeadersStatusCode = $responseHeaders['status-code'] ?? 0; + if ($responseHeadersStatusCode >= 400) { + return []; + } + + $responseBody = $response['body'] ?? []; + if (!is_array($responseBody)) { + return []; + } + + $branches = []; + foreach ($responseBody as $branch) { + $branches[] = [ + 'name' => $branch['name'] ?? '', + ]; + } + + return $branches; } public function getCommit(string $owner, string $repositoryName, string $commitHash): array @@ -342,7 +418,44 @@ public function getLatestCommit(string $owner, string $repositoryName, string $b public function updateCommitStatus(string $repositoryName, string $commitHash, string $owner, string $state, string $description = '', string $target_url = '', string $context = ''): void { - throw new Exception("Not implemented"); + $ownerPath = $this->getOwnerPath($owner); + $projectPath = urlencode("{$ownerPath}/{$repositoryName}"); + $url = "/projects/{$projectPath}/statuses/{$commitHash}"; + + // GitLab states: pending, running, success, failed, canceled + $stateMap = [ + 'pending' => 'pending', + 'success' => 'success', + 'failure' => 'failed', + 'error' => 'failed', + 'cancelled' => 'canceled', + ]; + + $gitlabState = $stateMap[$state] ?? $state; + + $payload = [ + 'state' => $gitlabState, + ]; + + if (!empty($description)) { + $payload['description'] = $description; + } + + if (!empty($target_url)) { + $payload['target_url'] = $target_url; + } + + if (!empty($context)) { + $payload['name'] = $context; + } + + $response = $this->call(self::METHOD_POST, $url, ['PRIVATE-TOKEN' => $this->accessToken], $payload); + + $responseHeaders = $response['headers'] ?? []; + $responseHeadersStatusCode = $responseHeaders['status-code'] ?? 0; + if ($responseHeadersStatusCode >= 400) { + throw new Exception("Failed to update commit status: HTTP {$responseHeadersStatusCode}"); + } } public function generateCloneCommand(string $owner, string $repositoryName, string $version, string $versionType, string $directory, string $rootDirectory): string @@ -433,6 +546,33 @@ public function createTag(string $owner, string $repositoryName, string $tagName public function getCommitStatuses(string $owner, string $repositoryName, string $commitHash): array { - throw new Exception("Not implemented"); + $ownerPath = $this->getOwnerPath($owner); + $projectPath = urlencode("{$ownerPath}/{$repositoryName}"); + $url = "/projects/{$projectPath}/repository/commits/{$commitHash}/statuses"; + + $response = $this->call(self::METHOD_GET, $url, ['PRIVATE-TOKEN' => $this->accessToken]); + + $responseHeaders = $response['headers'] ?? []; + $responseHeadersStatusCode = $responseHeaders['status-code'] ?? 0; + if ($responseHeadersStatusCode >= 400) { + return []; + } + + $responseBody = $response['body'] ?? []; + if (!is_array($responseBody)) { + return []; + } + + $statuses = []; + foreach ($responseBody as $status) { + $statuses[] = [ + 'state' => $status['status'] ?? '', + 'description' => $status['description'] ?? '', + 'target_url' => $status['target_url'] ?? '', + 'context' => $status['name'] ?? '', + ]; + } + + return $statuses; } } diff --git a/tests/VCS/Adapter/GitLabTest.php b/tests/VCS/Adapter/GitLabTest.php index 335d44e7..03783d46 100644 --- a/tests/VCS/Adapter/GitLabTest.php +++ b/tests/VCS/Adapter/GitLabTest.php @@ -119,12 +119,71 @@ public function testGetPullRequestFromBranch(): void public function testGetOwnerName(): void { - $this->markTestSkipped('Not implemented for GitLab yet'); + $result = $this->vcsAdapter->getOwnerName('', null); + + $this->assertIsString($result); + $this->assertNotEmpty($result); + } + + public function testGetOwnerNameWithRepositoryId(): void + { + $repositoryName = 'test-get-owner-name-' . \uniqid(); + $this->vcsAdapter->createRepository(static::$owner, $repositoryName, false); + + try { + $repo = $this->vcsAdapter->getRepository(static::$owner, $repositoryName); + $repositoryId = $repo['id'] ?? 0; + + $result = $this->vcsAdapter->getOwnerName('', $repositoryId); + + $this->assertIsString($result); + $this->assertNotEmpty($result); + } finally { + $this->vcsAdapter->deleteRepository(static::$owner, $repositoryName); + } } public function testSearchRepositories(): void { - $this->markTestSkipped('Not implemented for GitLab yet'); + $repositoryName = 'test-search-repositories-' . \uniqid(); + $this->vcsAdapter->createRepository(static::$owner, $repositoryName, false); + + try { + $result = $this->vcsAdapter->searchRepositories(static::$owner, 1, 10); + + $this->assertIsArray($result); + $this->assertNotEmpty($result); + + $names = array_column($result, 'name'); + $this->assertContains($repositoryName, $names); + + foreach ($result as $repo) { + $this->assertArrayHasKey('id', $repo); + $this->assertArrayHasKey('name', $repo); + $this->assertArrayHasKey('private', $repo); + } + } finally { + $this->vcsAdapter->deleteRepository(static::$owner, $repositoryName); + } + } + + public function testSearchRepositoriesWithSearch(): void + { + $uniqueId = \uniqid(); + $repositoryName = 'test-search-unique-' . $uniqueId; + $this->vcsAdapter->createRepository(static::$owner, $repositoryName, false); + + try { + $result = $this->vcsAdapter->searchRepositories(static::$owner, 1, 10, $uniqueId); + + $this->assertIsArray($result); + $this->assertNotEmpty($result); + + $names = array_column($result, 'name'); + $this->assertContains($repositoryName, $names); + } finally { + $this->vcsAdapter->deleteRepository(static::$owner, $repositoryName); + } } public function testCreateComment(): void @@ -202,6 +261,94 @@ public function testGenerateCloneCommandWithCommitHash(): void } } + public function testGetCommitStatuses(): void + { + $repositoryName = 'test-get-commit-statuses-' . \uniqid(); + $this->vcsAdapter->createRepository(static::$owner, $repositoryName, false); + + try { + $this->vcsAdapter->createFile(static::$owner, $repositoryName, 'README.md', '# Test'); + $commit = $this->vcsAdapter->getLatestCommit(static::$owner, $repositoryName, static::$defaultBranch); + $commitHash = $commit['commitHash']; + + $this->vcsAdapter->updateCommitStatus( + $repositoryName, + $commitHash, + static::$owner, + 'pending', + 'Build started', + '', + 'ci/test' + ); + + $result = $this->vcsAdapter->getCommitStatuses(static::$owner, $repositoryName, $commitHash); + + $this->assertIsArray($result); + $this->assertNotEmpty($result); + + foreach ($result as $status) { + $this->assertArrayHasKey('state', $status); + $this->assertArrayHasKey('description', $status); + $this->assertArrayHasKey('target_url', $status); + $this->assertArrayHasKey('context', $status); + } + } finally { + $this->vcsAdapter->deleteRepository(static::$owner, $repositoryName); + } + } + + + public function testUpdateCommitStatus(): void + { + $repositoryName = 'test-update-commit-status-' . \uniqid(); + $this->vcsAdapter->createRepository(static::$owner, $repositoryName, false); + + try { + $this->vcsAdapter->createFile(static::$owner, $repositoryName, 'README.md', '# Test'); + $commit = $this->vcsAdapter->getLatestCommit(static::$owner, $repositoryName, static::$defaultBranch); + $commitHash = $commit['commitHash']; + + $this->vcsAdapter->updateCommitStatus( + $repositoryName, + $commitHash, + static::$owner, + 'success', + 'Build passed', + 'https://example.com', + 'ci/build' + ); + + $statuses = $this->vcsAdapter->getCommitStatuses(static::$owner, $repositoryName, $commitHash); + + $this->assertIsArray($statuses); + $this->assertNotEmpty($statuses); + + $states = array_column($statuses, 'state'); + $this->assertContains('success', $states); + } finally { + $this->vcsAdapter->deleteRepository(static::$owner, $repositoryName); + } + } + + public function testGetCommitStatusesEmptyForNewCommit(): void + { + $repositoryName = 'test-get-commit-statuses-empty-' . \uniqid(); + $this->vcsAdapter->createRepository(static::$owner, $repositoryName, false); + + try { + $this->vcsAdapter->createFile(static::$owner, $repositoryName, 'README.md', '# Test'); + $commit = $this->vcsAdapter->getLatestCommit(static::$owner, $repositoryName, static::$defaultBranch); + $commitHash = $commit['commitHash']; + + $result = $this->vcsAdapter->getCommitStatuses(static::$owner, $repositoryName, $commitHash); + + $this->assertIsArray($result); + $this->assertEmpty($result); + } finally { + $this->vcsAdapter->deleteRepository(static::$owner, $repositoryName); + } + } + public function testGenerateCloneCommandWithTag(): void { $repositoryName = 'test-clone-tag-' . \uniqid(); @@ -361,7 +508,26 @@ public function testGetEventPush(): void public function testGetRepositoryName(): void { - $this->markTestSkipped('Not implemented for GitLab yet'); + $repositoryName = 'test-get-repository-name-' . \uniqid(); + $this->vcsAdapter->createRepository(static::$owner, $repositoryName, false); + + try { + $repo = $this->vcsAdapter->getRepository(static::$owner, $repositoryName); + $repositoryId = (string) ($repo['id'] ?? ''); + + $result = $this->vcsAdapter->getRepositoryName($repositoryId); + + $this->assertIsString($result); + $this->assertSame($repositoryName, $result); + } finally { + $this->vcsAdapter->deleteRepository(static::$owner, $repositoryName); + } + } + + public function testGetRepositoryNameWithInvalidId(): void + { + $this->expectException(\Exception::class); + $this->vcsAdapter->getRepositoryName('99999999'); } public function testGetComment(): void @@ -391,7 +557,26 @@ public function testGetRepositoryTree(): void public function testListBranches(): void { - $this->markTestSkipped('Not implemented for GitLab yet'); + $repositoryName = 'test-list-branches-' . \uniqid(); + $this->vcsAdapter->createRepository(static::$owner, $repositoryName, false); + + try { + $this->vcsAdapter->createFile(static::$owner, $repositoryName, 'README.md', '# Test'); + $this->vcsAdapter->createBranch(static::$owner, $repositoryName, 'feature-branch', static::$defaultBranch); + $this->vcsAdapter->createBranch(static::$owner, $repositoryName, 'another-branch', static::$defaultBranch); + + $result = $this->vcsAdapter->listBranches(static::$owner, $repositoryName); + + $this->assertIsArray($result); + $this->assertNotEmpty($result); + + $branchNames = array_column($result, 'name'); + $this->assertContains(static::$defaultBranch, $branchNames); + $this->assertContains('feature-branch', $branchNames); + $this->assertContains('another-branch', $branchNames); + } finally { + $this->vcsAdapter->deleteRepository(static::$owner, $repositoryName); + } } public function testListRepositoryLanguages(): void