From 11b3c59215f49e033e45a243bce10a7d1c918711 Mon Sep 17 00:00:00 2001
From: RahulGautamSingh <rahultesnik@gmail.com>
Date: Thu, 14 Mar 2024 15:34:21 +0545
Subject: [PATCH] feat(platform/bitbucket): autodiscoverProjects (#27845)

Co-authored-by: HonkingGoose <34918129+HonkingGoose@users.noreply.github.com>
---
 docs/usage/self-hosted-configuration.md      | 15 ++++++++++
 lib/config/options/index.ts                  | 11 +++++++
 lib/config/types.ts                          |  1 +
 lib/modules/platform/bitbucket/index.spec.ts | 30 +++++++++++++++++++-
 lib/modules/platform/bitbucket/index.ts      | 21 ++++++++++++--
 lib/modules/platform/bitbucket/types.ts      |  3 ++
 lib/modules/platform/types.ts                |  1 +
 lib/workers/global/autodiscover.ts           |  1 +
 8 files changed, 79 insertions(+), 4 deletions(-)

diff --git a/docs/usage/self-hosted-configuration.md b/docs/usage/self-hosted-configuration.md
index c7f0c0e6a7..138d885a4d 100644
--- a/docs/usage/self-hosted-configuration.md
+++ b/docs/usage/self-hosted-configuration.md
@@ -213,6 +213,21 @@ For example:
 }
 ```
 
+## autodiscoverProjects
+
+You can use this option to filter the list of autodiscovered repositories by project names.
+This feature is useful for users who want Renovate to only work on repositories within specific projects or exclude certain repositories from being processed.
+
+```json title="Example for Bitbucket"
+{
+  "platform": "bitbucket",
+  "autodiscoverProjects": ["a-group", "!another-group/some-subgroup"]
+}
+```
+
+The `autodiscoverProjects` config option takes an array of minimatch-compatible globs or RE2-compatible regex strings.
+For more details on this syntax see Renovate's [string pattern matching documentation](./string-pattern-matching.md).
+
 ## autodiscoverTopics
 
 Some platforms allow you to add tags, or topics, to repositories and retrieve repository lists by specifying those
diff --git a/lib/config/options/index.ts b/lib/config/options/index.ts
index 447610d8a1..ca6a436fc8 100644
--- a/lib/config/options/index.ts
+++ b/lib/config/options/index.ts
@@ -882,6 +882,17 @@ const options: RenovateOptions[] = [
     globalOnly: true,
     supportedPlatforms: ['gitlab'],
   },
+  {
+    name: 'autodiscoverProjects',
+    description:
+      'Filter the list of autodiscovered repositories by project names.',
+    stage: 'global',
+    type: 'array',
+    subType: 'string',
+    default: null,
+    globalOnly: true,
+    supportedPlatforms: ['bitbucket'],
+  },
   {
     name: 'autodiscoverTopics',
     description: 'Filter the list of autodiscovered repositories by topics.',
diff --git a/lib/config/types.ts b/lib/config/types.ts
index bb5882b981..ae3c2364ed 100644
--- a/lib/config/types.ts
+++ b/lib/config/types.ts
@@ -100,6 +100,7 @@ export interface GlobalOnlyConfig {
   autodiscover?: boolean;
   autodiscoverFilter?: string[] | string;
   autodiscoverNamespaces?: string[];
+  autodiscoverProjects?: string[];
   autodiscoverTopics?: string[];
   baseDir?: string;
   cacheDir?: string;
diff --git a/lib/modules/platform/bitbucket/index.spec.ts b/lib/modules/platform/bitbucket/index.spec.ts
index e958ea7c07..9d66ff1757 100644
--- a/lib/modules/platform/bitbucket/index.spec.ts
+++ b/lib/modules/platform/bitbucket/index.spec.ts
@@ -133,9 +133,37 @@ describe('modules/platform/bitbucket/index', () => {
         .reply(200, {
           values: [{ full_name: 'foo/bar' }, { full_name: 'some/repo' }],
         });
-      const res = await bitbucket.getRepos();
+      const res = await bitbucket.getRepos({});
       expect(res).toEqual(['foo/bar', 'some/repo']);
     });
+
+    it('filters repos based on autodiscoverProjects patterns', async () => {
+      httpMock
+        .scope(baseUrl)
+        .get('/2.0/repositories?role=contributor&pagelen=100')
+        .reply(200, {
+          values: [
+            { full_name: 'foo/bar', project: { name: 'ignore' } },
+            { full_name: 'some/repo', project: { name: 'allow' } },
+          ],
+        });
+      const res = await bitbucket.getRepos({ projects: ['allow'] });
+      expect(res).toEqual(['some/repo']);
+    });
+
+    it('filters repos based on autodiscoverProjects patterns with negation', async () => {
+      httpMock
+        .scope(baseUrl)
+        .get('/2.0/repositories?role=contributor&pagelen=100')
+        .reply(200, {
+          values: [
+            { full_name: 'foo/bar', project: { name: 'ignore' } },
+            { full_name: 'some/repo', project: { name: 'allow' } },
+          ],
+        });
+      const res = await bitbucket.getRepos({ projects: ['!ignore'] });
+      expect(res).toEqual(['some/repo']);
+    });
   });
 
   describe('initRepo()', () => {
diff --git a/lib/modules/platform/bitbucket/index.ts b/lib/modules/platform/bitbucket/index.ts
index dc6301ae89..a2eef445a4 100644
--- a/lib/modules/platform/bitbucket/index.ts
+++ b/lib/modules/platform/bitbucket/index.ts
@@ -10,8 +10,9 @@ import { BitbucketHttp, setBaseUrl } from '../../../util/http/bitbucket';
 import type { HttpOptions } from '../../../util/http/types';
 import { regEx } from '../../../util/regex';
 import { sanitize } from '../../../util/sanitize';
-import { UUIDRegex } from '../../../util/string-match';
+import { UUIDRegex, matchRegexOrGlobList } from '../../../util/string-match';
 import type {
+  AutodiscoverConfig,
   BranchStatusConfig,
   CreatePRConfig,
   EnsureCommentConfig,
@@ -113,10 +114,10 @@ export async function initPlatform({
 }
 
 // Get all repositories that the user has access to
-export async function getRepos(): Promise<string[]> {
+export async function getRepos(config: AutodiscoverConfig): Promise<string[]> {
   logger.debug('Autodiscovering Bitbucket Cloud repositories');
   try {
-    const repos = (
+    let repos = (
       await bitbucketHttp.getJson<PagedResult<RepoInfoBody>>(
         `/2.0/repositories/?role=contributor`,
         {
@@ -124,6 +125,20 @@ export async function getRepos(): Promise<string[]> {
         },
       )
     ).body.values;
+
+    // if autodiscoverProjects is configured
+    // filter the repos list
+    const autodiscoverProjects = config.projects;
+    if (is.nonEmptyArray(autodiscoverProjects)) {
+      logger.debug(
+        { autodiscoverProjects: config.projects },
+        'Applying autodiscoverProjects filter',
+      );
+      repos = repos.filter((repo) =>
+        matchRegexOrGlobList(repo.project.name, autodiscoverProjects),
+      );
+    }
+
     return repos.map((repo) => repo.full_name);
   } catch (err) /* istanbul ignore next */ {
     logger.error({ err }, `bitbucket getRepos error`);
diff --git a/lib/modules/platform/bitbucket/types.ts b/lib/modules/platform/bitbucket/types.ts
index 35fba3d163..fb4638179e 100644
--- a/lib/modules/platform/bitbucket/types.ts
+++ b/lib/modules/platform/bitbucket/types.ts
@@ -66,6 +66,9 @@ export interface RepoInfoBody {
   uuid: string;
   full_name: string;
   is_private: boolean;
+  project: {
+    name: string;
+  };
 }
 
 export interface PrResponse {
diff --git a/lib/modules/platform/types.ts b/lib/modules/platform/types.ts
index eff914fb7d..b6f75f2a58 100644
--- a/lib/modules/platform/types.ts
+++ b/lib/modules/platform/types.ts
@@ -178,6 +178,7 @@ export interface AutodiscoverConfig {
   topics?: string[];
   includeMirrors?: boolean;
   namespaces?: string[];
+  projects?: string[];
 }
 
 export interface Platform {
diff --git a/lib/workers/global/autodiscover.ts b/lib/workers/global/autodiscover.ts
index ed74f350f3..e6e633df55 100644
--- a/lib/workers/global/autodiscover.ts
+++ b/lib/workers/global/autodiscover.ts
@@ -40,6 +40,7 @@ export async function autodiscoverRepositories(
     topics: config.autodiscoverTopics,
     includeMirrors: config.includeMirrors,
     namespaces: config.autodiscoverNamespaces,
+    projects: config.autodiscoverProjects,
   });
   if (!discovered?.length) {
     // Soft fail (no error thrown) if no accessible repositories
-- 
GitLab