Skip to content

Commit 280e306

Browse files
authored
Add auto completion to Gitlab and Gitea repos and branches (#919)
* Add auto completion to Gitlab and Gitea repos and branches * fix tests
1 parent 6a8c1b4 commit 280e306

File tree

4 files changed

+865
-0
lines changed

4 files changed

+865
-0
lines changed

app/SourceControlProviders/Gitea.php

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
use App\Exceptions\FailedToDeployGitKey;
77
use App\Exceptions\FailedToDestroyGitHook;
88
use Exception;
9+
use Illuminate\Support\Collection;
10+
use Illuminate\Support\Facades\Cache;
911
use Illuminate\Support\Facades\Http;
1012
use Illuminate\Support\Facades\Log;
1113
use Throwable;
@@ -16,6 +18,12 @@ class Gitea extends AbstractSourceControlProvider
1618

1719
protected string $apiVersion = 'api/v1';
1820

21+
private const int CACHE_TTL = 60 * 15; // 15 minutes
22+
23+
private const int MAX_LIMIT = 50; // Gitea's max limit per page
24+
25+
private const int MAX_PAGES = 25; // Safety limit
26+
1927
public static function id(): string
2028
{
2129
return 'gitea';
@@ -199,4 +207,111 @@ public function getApiUrl(): string
199207

200208
return $host.$this->apiVersion;
201209
}
210+
211+
public function getRepos(bool $useCache = true): array
212+
{
213+
$cacheKey = 'gitea_repos_'.md5($this->getApiUrl().$this->data()['token']);
214+
215+
if ($useCache && Cache::has($cacheKey)) {
216+
return Cache::get($cacheKey);
217+
}
218+
219+
try {
220+
$repos = $this->fetchAllPages('/user/repos', [
221+
'limit' => self::MAX_LIMIT,
222+
]);
223+
224+
$repoNames = $repos->pluck('full_name')->toArray();
225+
Cache::put($cacheKey, $repoNames, self::CACHE_TTL);
226+
227+
return $repoNames;
228+
229+
} catch (Throwable $e) {
230+
Log::error('Failed to fetch Gitea repositories', [
231+
'error' => $e->getMessage(),
232+
]);
233+
234+
return [];
235+
}
236+
}
237+
238+
public function getBranches(string $repo, bool $useCache = true): array
239+
{
240+
$cacheKey = 'gitea_branches_'.md5($repo.$this->getApiUrl().$this->data()['token']);
241+
242+
if ($useCache && Cache::has($cacheKey)) {
243+
return Cache::get($cacheKey);
244+
}
245+
246+
try {
247+
$branches = $this->fetchAllPages("/repos/$repo/branches", [
248+
'limit' => self::MAX_LIMIT,
249+
]);
250+
251+
$branchNames = $branches->pluck('name')->toArray();
252+
Cache::put($cacheKey, $branchNames, self::CACHE_TTL);
253+
254+
return $branchNames;
255+
256+
} catch (Throwable $e) {
257+
Log::error('Failed to fetch Gitea branches', [
258+
'repo' => $repo,
259+
'error' => $e->getMessage(),
260+
]);
261+
262+
return [];
263+
}
264+
}
265+
266+
/**
267+
* Fetch all pages from Gitea API
268+
* Gitea uses pagination with 'page' and 'limit' parameters
269+
*
270+
* @param string $endpoint API endpoint (without base URL)
271+
* @param array<string, mixed> $params Query parameters
272+
* @return Collection<int, mixed>
273+
*/
274+
private function fetchAllPages(string $endpoint, array $params = []): Collection
275+
{
276+
$allData = collect();
277+
$page = 1;
278+
$hasMore = true;
279+
280+
while ($hasMore) {
281+
$params['page'] = $page;
282+
$response = Http::withToken($this->data()['token'])
283+
->get($this->getApiUrl().$endpoint, $params);
284+
285+
if (! $response->successful()) {
286+
Log::error('Gitea API request failed', [
287+
'endpoint' => $endpoint,
288+
'status' => $response->status(),
289+
'body' => $response->body(),
290+
]);
291+
break;
292+
}
293+
294+
$pageData = $response->json();
295+
if (empty($pageData)) {
296+
$hasMore = false;
297+
} else {
298+
$allData = $allData->concat($pageData);
299+
300+
// Gitea pagination: if we got fewer items than the limit, we're done
301+
$limit = $params['limit'] ?? self::MAX_LIMIT;
302+
$hasMore = count($pageData) >= $limit;
303+
$page++;
304+
}
305+
306+
if ($page > self::MAX_PAGES) {
307+
Log::warning('Reached pagination limit', [
308+
'endpoint' => $endpoint,
309+
'pages_fetched' => $page - 1,
310+
]);
311+
break;
312+
}
313+
}
314+
315+
return $allData;
316+
}
202317
}

app/SourceControlProviders/Gitlab.php

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
use App\Exceptions\FailedToDeployGitKey;
77
use App\Exceptions\FailedToDestroyGitHook;
88
use Exception;
9+
use Illuminate\Support\Collection;
10+
use Illuminate\Support\Facades\Cache;
911
use Illuminate\Support\Facades\Http;
1012
use Illuminate\Support\Facades\Log;
1113
use Throwable;
@@ -16,6 +18,12 @@ class Gitlab extends AbstractSourceControlProvider
1618

1719
protected string $apiVersion = 'api/v4';
1820

21+
private const int CACHE_TTL = 60 * 15; // 15 minutes
22+
23+
private const int MAX_PER_PAGE = 100; // GitLab's max per_page
24+
25+
private const int MAX_PAGES = 25; // Safety limit
26+
1927
public static function id(): string
2028
{
2129
return 'gitlab';
@@ -208,4 +216,124 @@ public function getApiUrl(): string
208216

209217
return $host.$this->apiVersion;
210218
}
219+
220+
public function getRepos(bool $useCache = true): array
221+
{
222+
$cacheKey = 'gitlab_repos_'.md5($this->getApiUrl().$this->data()['token']);
223+
224+
if ($useCache && Cache::has($cacheKey)) {
225+
return Cache::get($cacheKey);
226+
}
227+
228+
try {
229+
$repos = $this->fetchAllPages('/projects', [
230+
'membership' => true, // Only repos where user is a member
231+
'per_page' => self::MAX_PER_PAGE,
232+
]);
233+
234+
$repoNames = $repos->pluck('path_with_namespace')->toArray();
235+
Cache::put($cacheKey, $repoNames, self::CACHE_TTL);
236+
237+
return $repoNames;
238+
239+
} catch (Throwable $e) {
240+
Log::error('Failed to fetch GitLab repositories', [
241+
'error' => $e->getMessage(),
242+
]);
243+
244+
return [];
245+
}
246+
}
247+
248+
public function getBranches(string $repo, bool $useCache = true): array
249+
{
250+
$cacheKey = 'gitlab_branches_'.md5($repo.$this->getApiUrl().$this->data()['token']);
251+
252+
if ($useCache && Cache::has($cacheKey)) {
253+
return Cache::get($cacheKey);
254+
}
255+
256+
try {
257+
$repository = urlencode($repo);
258+
$branches = $this->fetchAllPages("/projects/$repository/repository/branches", [
259+
'per_page' => self::MAX_PER_PAGE,
260+
]);
261+
262+
$branchNames = $branches->pluck('name')->toArray();
263+
Cache::put($cacheKey, $branchNames, self::CACHE_TTL);
264+
265+
return $branchNames;
266+
267+
} catch (Throwable $e) {
268+
Log::error('Failed to fetch GitLab branches', [
269+
'repo' => $repo,
270+
'error' => $e->getMessage(),
271+
]);
272+
273+
return [];
274+
}
275+
}
276+
277+
/**
278+
* Fetch all pages from GitLab API
279+
* GitLab uses pagination with 'page' parameter and Link headers
280+
*
281+
* @param string $endpoint API endpoint (without base URL)
282+
* @param array<string, mixed> $params Query parameters
283+
* @return Collection<int, mixed>
284+
*/
285+
private function fetchAllPages(string $endpoint, array $params = []): Collection
286+
{
287+
$allData = collect();
288+
$page = 1;
289+
$hasMore = true;
290+
291+
while ($hasMore) {
292+
$params['page'] = $page;
293+
$response = Http::withToken($this->data()['token'])
294+
->get($this->getApiUrl().$endpoint, $params);
295+
296+
if (! $response->successful()) {
297+
Log::error('GitLab API request failed', [
298+
'endpoint' => $endpoint,
299+
'status' => $response->status(),
300+
'body' => $response->body(),
301+
]);
302+
break;
303+
}
304+
305+
$pageData = $response->json();
306+
if (empty($pageData)) {
307+
$hasMore = false;
308+
} else {
309+
$allData = $allData->concat($pageData);
310+
311+
// GitLab pagination uses Link header or X-Total-Pages header
312+
$linkHeader = $response->header('Link');
313+
$totalPages = (int) $response->header('X-Total-Pages');
314+
315+
if ($totalPages > 0) {
316+
$hasMore = $page < $totalPages;
317+
} elseif ($linkHeader && str_contains($linkHeader, 'rel="next"')) {
318+
$hasMore = true;
319+
} else {
320+
// If we got fewer items than per_page, we've reached the end
321+
$perPage = $params['per_page'] ?? self::MAX_PER_PAGE;
322+
$hasMore = count($pageData) >= $perPage;
323+
}
324+
325+
$page++;
326+
}
327+
328+
if ($page > self::MAX_PAGES) {
329+
Log::warning('Reached pagination limit', [
330+
'endpoint' => $endpoint,
331+
'pages_fetched' => $page - 1,
332+
]);
333+
break;
334+
}
335+
}
336+
337+
return $allData;
338+
}
211339
}

0 commit comments

Comments
 (0)