66use App \Exceptions \FailedToDeployGitKey ;
77use App \Exceptions \FailedToDestroyGitHook ;
88use Exception ;
9+ use Illuminate \Support \Collection ;
10+ use Illuminate \Support \Facades \Cache ;
911use Illuminate \Support \Facades \Http ;
1012use Illuminate \Support \Facades \Log ;
1113use 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