Add search and pagination for repository branches

This commit is contained in:
harsh mahajan
2026-05-08 13:25:35 +05:30
parent 602e1be296
commit 1a0a19a793
3 changed files with 122 additions and 2 deletions
@@ -1 +1 @@
Get a list of all branches from a GitHub repository in your installation. This endpoint returns the names of all branches in the repository and their total count. The GitHub installation must be properly configured and have access to the requested repository for this endpoint to work.
Get a list of branches from a GitHub repository in your installation. This endpoint supports filtering by a search term and pagination using query strings such as `Query.limit()`, `Query.offset()`, `Query.cursorAfter()`, and `Query.cursorBefore()`. It returns branch names along with the total number of matches. The GitHub installation must be properly configured and have access to the requested repository for this endpoint to work.
@@ -10,8 +10,11 @@ use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Query;
use Utopia\Platform\Scope\HTTP;
use Utopia\System\System;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Text;
use Utopia\VCS\Adapter\Git\GitHub;
use Utopia\VCS\Exception\RepositoryNotFound;
@@ -49,6 +52,8 @@ class XList extends Action
))
->param('installationId', '', new Text(256), 'Installation Id')
->param('providerRepositoryId', '', new Text(256), 'Repository Id')
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->param('queries', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit, offset, cursorAfter, and cursorBefore', true)
->inject('gitHub')
->inject('response')
->inject('dbForPlatform')
@@ -58,10 +63,31 @@ class XList extends Action
public function action(
string $installationId,
string $providerRepositoryId,
string $search,
array $queries,
GitHub $github,
Response $response,
Database $dbForPlatform
) {
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
$allowedQueryMethods = [
Query::TYPE_LIMIT,
Query::TYPE_OFFSET,
Query::TYPE_CURSOR_AFTER,
Query::TYPE_CURSOR_BEFORE,
];
foreach ($queries as $query) {
if (!\in_array($query->getMethod(), $allowedQueryMethods, true)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, 'Only limit, offset, cursorAfter, and cursorBefore queries are supported.');
}
}
$installation = $dbForPlatform->getDocument('installations', $installationId);
if ($installation->isEmpty()) {
@@ -85,11 +111,56 @@ class XList extends Action
$branches = $github->listBranches($owner, $repositoryName);
if (!empty($search)) {
$branches = \array_values(\array_filter($branches, fn (string $branch) => \stripos($branch, $search) !== false));
}
$total = \count($branches);
$limitQuery = \current(\array_filter($queries, fn (Query $query) => $query->getMethod() === Query::TYPE_LIMIT));
$offsetQuery = \current(\array_filter($queries, fn (Query $query) => $query->getMethod() === Query::TYPE_OFFSET));
$cursorQuery = \current(\array_filter($queries, fn (Query $query) => \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE], true)));
$limit = $limitQuery instanceof Query ? $limitQuery->getValue() : APP_LIMIT_LIST_DEFAULT;
$offset = $offsetQuery instanceof Query ? $offsetQuery->getValue() : 0;
if (!\is_int($limit) || $limit < 0) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, 'Invalid limit query.');
}
if (!\is_int($offset) || $offset < 0) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, 'Invalid offset query.');
}
if ($cursorQuery instanceof Query) {
$cursor = $cursorQuery->getValue();
if (!\is_string($cursor) || $cursor === '') {
throw new Exception(Exception::GENERAL_QUERY_INVALID, 'Invalid cursor query.');
}
$cursorIndex = \array_search($cursor, $branches, true);
if ($cursorIndex === false) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Branch '{$cursor}' for the 'cursor' value not found.");
}
$offset += $cursorQuery->getMethod() === Query::TYPE_CURSOR_AFTER ? $cursorIndex + 1 : 0;
if ($cursorQuery->getMethod() === Query::TYPE_CURSOR_BEFORE) {
$length = $limit === 0 ? 0 : $limit;
$start = \max(0, $cursorIndex - $length);
$branches = \array_slice($branches, $start, $length);
} else {
$branches = \array_slice($branches, $offset, $limit);
}
} else {
$branches = \array_slice($branches, $offset, $limit);
}
$response->dynamic(new Document([
'branches' => \array_map(function ($branch) {
return new Document(['name' => $branch]);
}, $branches),
'total' => \count($branches),
'total' => $total,
]), Response::MODEL_BRANCH_LIST);
}
}
@@ -513,6 +513,45 @@ class VCSConsoleClientTest extends Scope
$this->assertEquals($repositoryBranches['body']['branches'][0]['name'], 'main');
$this->assertEquals($repositoryBranches['body']['branches'][1]['name'], 'test');
$repositoryBranches = $this->client->call(Client::METHOD_GET, '/vcs/github/installations/' . $installationId . '/providerRepositories/' . $this->providerRepositoryId . '/branches', array_merge([
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'search' => 'tes',
]);
$this->assertEquals(200, $repositoryBranches['headers']['status-code']);
$this->assertEquals($repositoryBranches['body']['total'], 1);
$this->assertCount(1, $repositoryBranches['body']['branches']);
$this->assertEquals($repositoryBranches['body']['branches'][0]['name'], 'test');
$repositoryBranches = $this->client->call(Client::METHOD_GET, '/vcs/github/installations/' . $installationId . '/providerRepositories/' . $this->providerRepositoryId . '/branches', array_merge([
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::limit(1)->toString(),
Query::offset(1)->toString(),
],
]);
$this->assertEquals(200, $repositoryBranches['headers']['status-code']);
$this->assertEquals($repositoryBranches['body']['total'], 2);
$this->assertCount(1, $repositoryBranches['body']['branches']);
$this->assertEquals($repositoryBranches['body']['branches'][0]['name'], 'test');
$repositoryBranches = $this->client->call(Client::METHOD_GET, '/vcs/github/installations/' . $installationId . '/providerRepositories/' . $this->providerRepositoryId . '/branches', array_merge([
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::limit(1)->toString(),
Query::cursorAfter(new \Utopia\Database\Document(['$id' => 'main']))->toString(),
],
]);
$this->assertEquals(200, $repositoryBranches['headers']['status-code']);
$this->assertEquals($repositoryBranches['body']['total'], 2);
$this->assertCount(1, $repositoryBranches['body']['branches']);
$this->assertEquals($repositoryBranches['body']['branches'][0]['name'], 'test');
/**
* Test for FAILURE
*/
@@ -522,6 +561,16 @@ class VCSConsoleClientTest extends Scope
], $this->getHeaders()));
$this->assertEquals(404, $repositoryBranches['headers']['status-code']);
$repositoryBranches = $this->client->call(Client::METHOD_GET, '/vcs/github/installations/' . $installationId . '/providerRepositories/' . $this->providerRepositoryId . '/branches', array_merge([
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::cursorAfter(new \Utopia\Database\Document(['$id' => 'missing-branch']))->toString(),
],
]);
$this->assertEquals(400, $repositoryBranches['headers']['status-code']);
}
public function testCreateFunctionUsingVCS(): void