From c7737befb7336ea855cc9a0705fcc6018dbfc8fb Mon Sep 17 00:00:00 2001
From: Michael Kriese <michael.kriese@visualon.de>
Date: Fri, 21 Feb 2025 14:47:43 +0100
Subject: [PATCH] feat(gitea/forgejo): set poster server filter for pr cache
 (#34374)

Co-authored-by: Sebastian Poxhofer <secustor@users.noreply.github.com>
---
 lib/modules/platform/gitea/index.spec.ts | 69 +++++++++++++-----------
 lib/modules/platform/gitea/index.ts      | 34 ++++++++++--
 lib/modules/platform/gitea/pr-cache.ts   | 29 ++++++----
 3 files changed, 86 insertions(+), 46 deletions(-)

diff --git a/lib/modules/platform/gitea/index.spec.ts b/lib/modules/platform/gitea/index.spec.ts
index 49c236fe55..f9a52788f0 100644
--- a/lib/modules/platform/gitea/index.spec.ts
+++ b/lib/modules/platform/gitea/index.spec.ts
@@ -271,7 +271,7 @@ describe('modules/platform/gitea/index', () => {
     const repoResult = { ...mockRepo, ...repo };
     const repository = repoResult.full_name;
     scope.get(`/repos/${repository}`).reply(200, repoResult);
-    await gitea.initRepo({ repository, ...config });
+    await gitea.initRepo({ repository, ignorePrAuthor: true, ...config });
   }
 
   describe('initPlatform()', () => {
@@ -1160,7 +1160,7 @@ describe('modules/platform/gitea/index', () => {
       const scope = httpMock
         .scope('https://gitea.com/api/v1')
         .get('/repos/some/repo/pulls')
-        .query({ state: 'all', sort: 'recentupdate' })
+        .query({ state: 'all', sort: 'recentupdate', limit: 100 })
         .reply(200, mockPRs);
       await initFakePlatform(scope);
       await initFakeRepo(scope);
@@ -1196,7 +1196,7 @@ describe('modules/platform/gitea/index', () => {
       const scope = httpMock
         .scope('https://gitea.com/api/v1')
         .get('/repos/some/repo/pulls')
-        .query({ state: 'all', sort: 'recentupdate' })
+        .query({ state: 'all', sort: 'recentupdate', limit: 100 })
         .reply(200, [
           thirdPartyPr,
           ...mockPRs.map((pr) => ({
@@ -1221,16 +1221,21 @@ describe('modules/platform/gitea/index', () => {
       const scope = httpMock
         .scope('https://gitea.com/api/v1')
         .get('/repos/some/repo/pulls')
-        .query({ state: 'all', sort: 'recentupdate' })
+        .query({
+          state: 'all',
+          sort: 'recentupdate',
+          limit: 100,
+          poster: mockUser.username,
+        })
         .reply(200, mockPRs.slice(0, 2), {
           // test correct pagination handling, domain should be ignored
-          link: '<https://domain.test/api/v1/repos/some/repo/pulls?state=all&sort=recentupdate&page=2>; rel="next",<https://domain.test/api/v1/repos/some/repo/pulls?state=all&sort=recentupdate&page=4512>; rel="last"',
+          link: '<https://domain.test/some/path/api/v1/repos/some/repo/pulls?state=all&sort=recentupdate&limit=50&page=2>; rel="next",<https://domain.test/some/path/api/v1/repos/some/repo/pulls?state=all&sort=recentupdate&page=4512>; rel="last"',
         })
         .get('/repos/some/repo/pulls')
-        .query({ state: 'all', sort: 'recentupdate', page: 2 })
+        .query({ state: 'all', sort: 'recentupdate', limit: 50, page: 2 })
         .reply(200, mockPRs.slice(2));
       await initFakePlatform(scope);
-      await initFakeRepo(scope);
+      await initFakeRepo(scope, undefined, { ignorePrAuthor: false });
 
       const res1 = await gitea.getPrList();
       const res2 = await gitea.getPrList();
@@ -1242,10 +1247,10 @@ describe('modules/platform/gitea/index', () => {
       const scope = httpMock
         .scope('https://gitea.com/api/v1')
         .get('/repos/some/repo/pulls')
-        .query({ state: 'all', sort: 'recentupdate' })
+        .query({ state: 'all', sort: 'recentupdate', limit: 100 })
         .reply(200, mockPRs.slice(0, 2))
         .get('/repos/some/repo/pulls')
-        .query({ state: 'all', sort: 'recentupdate' })
+        .query({ state: 'all', sort: 'recentupdate', limit: 20 })
         .reply(200, mockPRs.slice(1));
       await initFakePlatform(scope);
       await initFakeRepo(scope);
@@ -1274,7 +1279,7 @@ describe('modules/platform/gitea/index', () => {
       const scope = httpMock
         .scope('https://gitea.com/api/v1')
         .get('/repos/some/repo/pulls')
-        .query({ state: 'all', sort: 'recentupdate' })
+        .query({ state: 'all', sort: 'recentupdate', limit: 100 })
         .reply(200, mockPRs);
       await initFakePlatform(scope);
       await initFakeRepo(scope);
@@ -1289,7 +1294,7 @@ describe('modules/platform/gitea/index', () => {
       const scope = httpMock
         .scope('https://gitea.com/api/v1')
         .get('/repos/some/repo/pulls')
-        .query({ state: 'all', sort: 'recentupdate' })
+        .query({ state: 'all', sort: 'recentupdate', limit: 100 })
         .reply(200, [])
         .get('/repos/some/repo/pulls/1')
         .reply(200, pr);
@@ -1305,7 +1310,7 @@ describe('modules/platform/gitea/index', () => {
       const scope = httpMock
         .scope('https://gitea.com/api/v1')
         .get('/repos/some/repo/pulls')
-        .query({ state: 'all', sort: 'recentupdate' })
+        .query({ state: 'all', sort: 'recentupdate', limit: 100 })
         .reply(200, [])
         .get('/repos/some/repo/pulls/42')
         .reply(200); // TODO: 404 should be handled
@@ -1321,7 +1326,7 @@ describe('modules/platform/gitea/index', () => {
       const scope = httpMock
         .scope('https://gitea.com/api/v1')
         .get('/repos/some/repo/pulls')
-        .query({ state: 'all', sort: 'recentupdate' })
+        .query({ state: 'all', sort: 'recentupdate', limit: 100 })
         .reply(200, [null]); // TODO: 404 should be handled
       await initFakePlatform(scope);
       await initFakeRepo(scope);
@@ -1335,7 +1340,7 @@ describe('modules/platform/gitea/index', () => {
       const scope = httpMock
         .scope('https://gitea.com/api/v1')
         .get('/repos/some/repo/pulls')
-        .query({ state: 'all', sort: 'recentupdate' })
+        .query({ state: 'all', sort: 'recentupdate', limit: 100 })
         .reply(200, mockPRs);
       await initFakePlatform(scope);
       await initFakeRepo(scope);
@@ -1352,7 +1357,7 @@ describe('modules/platform/gitea/index', () => {
       const scope = httpMock
         .scope('https://gitea.com/api/v1')
         .get('/repos/some/repo/pulls')
-        .query({ state: 'all', sort: 'recentupdate' })
+        .query({ state: 'all', sort: 'recentupdate', limit: 100 })
         .reply(200, mockPRs);
       await initFakePlatform(scope);
       await initFakeRepo(scope);
@@ -1372,7 +1377,7 @@ describe('modules/platform/gitea/index', () => {
       const scope = httpMock
         .scope('https://gitea.com/api/v1')
         .get('/repos/some/repo/pulls')
-        .query({ state: 'all', sort: 'recentupdate' })
+        .query({ state: 'all', sort: 'recentupdate', limit: 100 })
         .reply(200, mockPRs);
       await initFakePlatform(scope);
       await initFakeRepo(scope);
@@ -1392,7 +1397,7 @@ describe('modules/platform/gitea/index', () => {
       const scope = httpMock
         .scope('https://gitea.com/api/v1')
         .get('/repos/some/repo/pulls')
-        .query({ state: 'all', sort: 'recentupdate' })
+        .query({ state: 'all', sort: 'recentupdate', limit: 100 })
         .reply(200, mockPRs);
       await initFakePlatform(scope);
       await initFakeRepo(scope);
@@ -1413,7 +1418,7 @@ describe('modules/platform/gitea/index', () => {
       const scope = httpMock
         .scope('https://gitea.com/api/v1')
         .get('/repos/some/repo/pulls')
-        .query({ state: 'all', sort: 'recentupdate' })
+        .query({ state: 'all', sort: 'recentupdate', limit: 100 })
         .reply(200, mockPRs);
       await initFakePlatform(scope);
       await initFakeRepo(scope);
@@ -1435,7 +1440,7 @@ describe('modules/platform/gitea/index', () => {
       const scope = httpMock
         .scope('https://gitea.com/api/v1')
         .get('/repos/some/repo/pulls')
-        .query({ state: 'all', sort: 'recentupdate' })
+        .query({ state: 'all', sort: 'recentupdate', limit: 100 })
         .reply(200, mockPRs);
       await initFakePlatform(scope);
       await initFakeRepo(scope);
@@ -1457,7 +1462,7 @@ describe('modules/platform/gitea/index', () => {
       const scope = httpMock
         .scope('https://gitea.com/api/v1')
         .get('/repos/some/repo/pulls')
-        .query({ state: 'all', sort: 'recentupdate' })
+        .query({ state: 'all', sort: 'recentupdate', limit: 100 })
         .reply(200, mockPRs);
       await initFakePlatform(scope);
       await initFakeRepo(scope);
@@ -1475,7 +1480,7 @@ describe('modules/platform/gitea/index', () => {
       const scope = httpMock
         .scope('https://gitea.com/api/v1')
         .get('/repos/some/repo/pulls')
-        .query({ state: 'all', sort: 'recentupdate' })
+        .query({ state: 'all', sort: 'recentupdate', limit: 100 })
         .reply(200, mockPRs);
       await initFakePlatform(scope);
       await initFakeRepo(scope);
@@ -1613,7 +1618,7 @@ describe('modules/platform/gitea/index', () => {
         .post('/repos/some/repo/pulls')
         .reply(409)
         .get('/repos/some/repo/pulls')
-        .query({ state: 'all', sort: 'recentupdate' })
+        .query({ state: 'all', sort: 'recentupdate', limit: 100 })
         .reply(200, [mockNewPR]);
       await initFakePlatform(scope);
       await initFakeRepo(scope);
@@ -1637,7 +1642,7 @@ describe('modules/platform/gitea/index', () => {
         .post('/repos/some/repo/pulls')
         .reply(409)
         .get('/repos/some/repo/pulls')
-        .query({ state: 'all', sort: 'recentupdate' })
+        .query({ state: 'all', sort: 'recentupdate', limit: 100 })
         .reply(200, [mockNewPR])
         .patch('/repos/some/repo/pulls/42')
         .reply(200);
@@ -1890,7 +1895,7 @@ describe('modules/platform/gitea/index', () => {
       const scope = httpMock
         .scope('https://gitea.com/api/v1')
         .get('/repos/some/repo/pulls')
-        .query({ state: 'all', sort: 'recentupdate' })
+        .query({ state: 'all', sort: 'recentupdate', limit: 100 })
         .reply(200, mockPRs)
         .patch('/repos/some/repo/pulls/1', { title: 'New Title' })
         .reply(200, pr);
@@ -1906,7 +1911,7 @@ describe('modules/platform/gitea/index', () => {
       const scope = httpMock
         .scope('https://gitea.com/api/v1')
         .get('/repos/some/repo/pulls')
-        .query({ state: 'all', sort: 'recentupdate' })
+        .query({ state: 'all', sort: 'recentupdate', limit: 100 })
         .reply(200, mockPRs)
         .patch('/repos/some/repo/pulls/1', {
           title: 'New Title',
@@ -1929,7 +1934,7 @@ describe('modules/platform/gitea/index', () => {
       const scope = httpMock
         .scope('https://gitea.com/api/v1')
         .get('/repos/some/repo/pulls')
-        .query({ state: 'all', sort: 'recentupdate' })
+        .query({ state: 'all', sort: 'recentupdate', limit: 100 })
         .reply(200, mockPRs)
         .patch('/repos/some/repo/pulls/1', {
           title: 'New Title',
@@ -1952,7 +1957,7 @@ describe('modules/platform/gitea/index', () => {
       const scope = httpMock
         .scope('https://gitea.com/api/v1')
         .get('/repos/some/repo/pulls')
-        .query({ state: 'all', sort: 'recentupdate' })
+        .query({ state: 'all', sort: 'recentupdate', limit: 100 })
         .reply(200, mockPRs)
         .patch('/repos/some/repo/pulls/3', {
           title: 'WIP: New Title',
@@ -1975,7 +1980,7 @@ describe('modules/platform/gitea/index', () => {
       const scope = httpMock
         .scope('https://gitea.com/api/v1')
         .get('/repos/some/repo/pulls')
-        .query({ state: 'all', sort: 'recentupdate' })
+        .query({ state: 'all', sort: 'recentupdate', limit: 100 })
         .reply(200, mockPRs)
         .patch('/repos/some/repo/pulls/1', {
           title: 'New Title',
@@ -2013,7 +2018,7 @@ describe('modules/platform/gitea/index', () => {
       const scope = httpMock
         .scope('https://gitea.com/api/v1')
         .get('/repos/some/repo/pulls')
-        .query({ state: 'all', sort: 'recentupdate' })
+        .query({ state: 'all', sort: 'recentupdate', limit: 100 })
         .reply(200, mockPRs)
         .get('/repos/some/repo/labels')
         .reply(200, mockRepoLabels)
@@ -2052,7 +2057,7 @@ describe('modules/platform/gitea/index', () => {
       const scope = httpMock
         .scope('https://gitea.com/api/v1')
         .get('/repos/some/repo/pulls')
-        .query({ state: 'all', sort: 'recentupdate' })
+        .query({ state: 'all', sort: 'recentupdate', limit: 100 })
         .reply(200, mockPRs)
         .get('/repos/some/repo/labels')
         .reply(200, mockRepoLabels)
@@ -2807,7 +2812,7 @@ describe('modules/platform/gitea/index', () => {
       const scope = httpMock
         .scope('https://gitea.com/api/v1')
         .get('/repos/some/repo/pulls')
-        .query({ state: 'all', sort: 'recentupdate' })
+        .query({ state: 'all', sort: 'recentupdate', limit: 100 })
         .reply(200, mockPRs);
       await initFakePlatform(scope);
       await initFakeRepo(scope);
@@ -2821,7 +2826,7 @@ describe('modules/platform/gitea/index', () => {
       const scope = httpMock
         .scope('https://gitea.com/api/v1')
         .get('/repos/some/repo/pulls')
-        .query({ state: 'all', sort: 'recentupdate' })
+        .query({ state: 'all', sort: 'recentupdate', limit: 100 })
         .reply(200, mockPRs);
       await initFakePlatform(scope);
       await initFakeRepo(scope);
diff --git a/lib/modules/platform/gitea/index.ts b/lib/modules/platform/gitea/index.ts
index 1c8326c541..c30f508b1a 100644
--- a/lib/modules/platform/gitea/index.ts
+++ b/lib/modules/platform/gitea/index.ts
@@ -63,6 +63,7 @@ import {
 } from './utils';
 
 interface GiteaRepoConfig {
+  ignorePrAuthor: boolean;
   repository: string;
   mergeMethod: PRMergeMethod;
 
@@ -263,6 +264,7 @@ const platform: Platform = {
     cloneSubmodules,
     cloneSubmodulesFilter,
     gitUrl,
+    ignorePrAuthor,
   }: RepoParams): Promise<RepoResult> {
     let repo: Repo;
 
@@ -270,6 +272,7 @@ const platform: Platform = {
     config.repository = repository;
     config.cloneSubmodules = !!cloneSubmodules;
     config.cloneSubmodulesFilter = cloneSubmodulesFilter;
+    config.ignorePrAuthor = !!ignorePrAuthor;
 
     // Try to fetch information about repository
     try {
@@ -475,7 +478,12 @@ const platform: Platform = {
   },
 
   getPrList(): Promise<Pr[]> {
-    return GiteaPrCache.getPrs(giteaHttp, config.repository, botUserName);
+    return GiteaPrCache.getPrs(
+      giteaHttp,
+      config.repository,
+      config.ignorePrAuthor,
+      botUserName,
+    );
   },
 
   async getPr(number: number): Promise<Pr | null> {
@@ -491,7 +499,13 @@ const platform: Platform = {
 
       // Add pull request to cache for further lookups / queries
       if (pr) {
-        await GiteaPrCache.setPr(giteaHttp, config.repository, botUserName, pr);
+        await GiteaPrCache.setPr(
+          giteaHttp,
+          config.repository,
+          config.ignorePrAuthor,
+          botUserName,
+          pr,
+        );
       }
     }
 
@@ -593,7 +607,13 @@ const platform: Platform = {
         throw new Error('Can not parse newly created Pull Request');
       }
 
-      await GiteaPrCache.setPr(giteaHttp, config.repository, botUserName, pr);
+      await GiteaPrCache.setPr(
+        giteaHttp,
+        config.repository,
+        config.ignorePrAuthor,
+        botUserName,
+        pr,
+      );
       return pr;
     } catch (err) {
       // When the user manually deletes a branch from Renovate, the PR remains but is no longer linked to any branch. In
@@ -687,7 +707,13 @@ const platform: Platform = {
     );
     const pr = toRenovatePR(gpr, botUserName);
     if (pr) {
-      await GiteaPrCache.setPr(giteaHttp, config.repository, botUserName, pr);
+      await GiteaPrCache.setPr(
+        giteaHttp,
+        config.repository,
+        config.ignorePrAuthor,
+        botUserName,
+        pr,
+      );
     }
   },
 
diff --git a/lib/modules/platform/gitea/pr-cache.ts b/lib/modules/platform/gitea/pr-cache.ts
index 7f6268dcdb..00e0e33acb 100644
--- a/lib/modules/platform/gitea/pr-cache.ts
+++ b/lib/modules/platform/gitea/pr-cache.ts
@@ -17,6 +17,7 @@ export class GiteaPrCache {
 
   private constructor(
     private repo: string,
+    private readonly ignorePrAuthor: boolean,
     private author: string | null,
   ) {
     const repoCache = getCache();
@@ -44,9 +45,10 @@ export class GiteaPrCache {
   private static async init(
     http: GiteaHttp,
     repo: string,
+    ignorePrAuthor: boolean,
     author: string | null,
   ): Promise<GiteaPrCache> {
-    const res = new GiteaPrCache(repo, author);
+    const res = new GiteaPrCache(repo, ignorePrAuthor, author);
     const isSynced = memCache.get<true | undefined>('gitea-pr-cache-synced');
 
     if (!isSynced) {
@@ -64,9 +66,10 @@ export class GiteaPrCache {
   static async getPrs(
     http: GiteaHttp,
     repo: string,
+    ignorePrAuthor: boolean,
     author: string,
   ): Promise<Pr[]> {
-    const prCache = await GiteaPrCache.init(http, repo, author);
+    const prCache = await GiteaPrCache.init(http, repo, ignorePrAuthor, author);
     return prCache.getPrs();
   }
 
@@ -78,10 +81,11 @@ export class GiteaPrCache {
   static async setPr(
     http: GiteaHttp,
     repo: string,
+    ignorePrAuthor: boolean,
     author: string,
     item: Pr,
   ): Promise<void> {
-    const prCache = await GiteaPrCache.init(http, repo, author);
+    const prCache = await GiteaPrCache.init(http, repo, ignorePrAuthor, author);
     prCache.setPr(item);
   }
 
@@ -126,18 +130,23 @@ export class GiteaPrCache {
   }
 
   private async sync(http: GiteaHttp): Promise<GiteaPrCache> {
-    const query = getQueryString({
+    let query: string | null = getQueryString({
       state: 'all',
       sort: 'recentupdate',
+      // Fetch 100 PRs on the first run to ensure we have the most recent PRs.
+      // Gitea / Forgejo will cap appropriate (50 by default, see `MAX_RESPONSE_ITEMS`).
+      // https://docs.gitea.com/administration/config-cheat-sheet#api-api
+      // https://forgejo.org/docs/latest/admin/config-cheat-sheet/#api-api
+      limit: this.items.length ? 20 : 100,
+      // Supported since Gitea 1.23.0 and Forgejo v10.0.0.
+      // Will be ignoded by older instances.
+      ...(this.ignorePrAuthor ? {} : { poster: this.author }),
     });
 
-    let url: string | undefined =
-      `${API_PATH}/repos/${this.repo}/pulls?${query}`;
-
-    while (url) {
+    while (query) {
       // TODO: use zod, typescript can't infer the type of the response #22198
       const res: HttpResponse<(PR | null)[]> = await http.getJsonUnchecked(
-        url,
+        `${API_PATH}/repos/${this.repo}/pulls?${query}`,
         {
           memCache: false,
           paginate: false,
@@ -150,7 +159,7 @@ export class GiteaPrCache {
       }
 
       const uri = parseUrl(parseLinkHeader(res.headers.link)?.next?.url);
-      url = uri ? `${uri.pathname}${uri.search}` : undefined;
+      query = uri ? uri.search : null;
     }
 
     this.updateItems();
-- 
GitLab