diff --git a/src/VCS/Adapter/Git/GitLab.php b/src/VCS/Adapter/Git/GitLab.php index 0cadaeeb..0a133d79 100644 --- a/src/VCS/Adapter/Git/GitLab.php +++ b/src/VCS/Adapter/Git/GitLab.php @@ -255,6 +255,45 @@ public function getInstallationRepository(string $repositoryName): array throw new Exception("getInstallationRepository is not applicable for this adapter"); } + /** + * List namespaces the current user can browse: their personal namespace + * plus every group they belong to. GitLab's own /namespaces endpoint + * already combines both with correct pagination, so this is a thin + * passthrough rather than a hand-rolled merge of /user + /groups. + * + * @return array{items: array, total: int} + */ + public function listNamespaces(int $page, int $per_page, string $search = ''): array + { + $url = "/namespaces?page={$page}&per_page={$per_page}"; + if (!empty($search)) { + $url .= '&search=' . urlencode($search); + } + + $response = $this->call(self::METHOD_GET, $url, ['Authorization' => 'Bearer ' . $this->accessToken]); + $responseHeaders = $response['headers'] ?? []; + $statusCode = $responseHeaders['status-code'] ?? 0; + if ($statusCode >= 400) { + throw new Exception("Failed to list namespaces: HTTP {$statusCode}", $statusCode); + } + + $items = $response['body'] ?? []; + $items = is_array($items) ? $items : []; + + $namespaces = array_map(fn (array $namespace) => [ + 'id' => (string) ($namespace['id'] ?? ''), + 'name' => $namespace['name'] ?? ($namespace['path'] ?? ''), + 'path' => $namespace['full_path'] ?? ($namespace['path'] ?? ''), + 'kind' => $namespace['kind'] ?? 'group', + 'avatarUrl' => $namespace['avatar_url'] ?? '', + ], $items); + + return [ + 'items' => $namespaces, + 'total' => (int) ($responseHeaders['x-total'] ?? \count($namespaces)), + ]; + } + public function searchRepositories(string $owner, int $page, int $per_page, string $search = ''): array { $ownerPath = $this->getOwnerPath($owner); diff --git a/tests/VCS/Adapter/GitLabTest.php b/tests/VCS/Adapter/GitLabTest.php index 3eba2ad5..f9504765 100644 --- a/tests/VCS/Adapter/GitLabTest.php +++ b/tests/VCS/Adapter/GitLabTest.php @@ -208,6 +208,44 @@ public function testGetOwnerNameWithRepositoryId(): void } } + public function testListNamespaces(): void + { + /** @var GitLab $adapter */ + $adapter = $this->vcsAdapter; + + $result = $adapter->listNamespaces(1, 20); + + $this->assertIsArray($result); + $this->assertArrayHasKey('items', $result); + $this->assertArrayHasKey('total', $result); + $this->assertNotEmpty($result['items']); + + $kinds = array_column($result['items'], 'kind'); + $this->assertContains('user', $kinds); + $this->assertContains('group', $kinds); + + foreach ($result['items'] as $namespace) { + $this->assertArrayHasKey('id', $namespace); + $this->assertArrayHasKey('name', $namespace); + $this->assertArrayHasKey('path', $namespace); + $this->assertArrayHasKey('kind', $namespace); + $this->assertNotEmpty($namespace['path']); + } + } + + public function testListNamespacesWithSearch(): void + { + /** @var GitLab $adapter */ + $adapter = $this->vcsAdapter; + $ownerPath = explode(':', static::$owner)[1] ?? static::$owner; + + $result = $adapter->listNamespaces(1, 20, $ownerPath); + + $this->assertNotEmpty($result['items']); + $paths = array_column($result['items'], 'path'); + $this->assertContains($ownerPath, $paths); + } + public function testSearchRepositories(): void { $repositoryName = 'test-search-repositories-' . \uniqid();