From 98be25f7871204e589d5048c0f353b38f5b0235a Mon Sep 17 00:00:00 2001
From: Adam Setch <adam.setch@outlook.com>
Date: Sun, 12 Mar 2023 08:50:54 -0400
Subject: [PATCH] feat(platform/bitbucket): add support for Bitbucket
 development branching model (#20860)

Co-authored-by: Michael Kriese <michael.kriese@visualon.de>
---
 docs/usage/self-hosted-configuration.md      |  8 +++
 lib/config/options/index.ts                  |  8 +++
 lib/modules/platform/bitbucket/index.spec.ts | 52 ++++++++++++++++++++
 lib/modules/platform/bitbucket/index.ts      | 23 ++++++++-
 lib/modules/platform/bitbucket/types.ts      |  9 ++++
 lib/modules/platform/types.ts                |  1 +
 6 files changed, 99 insertions(+), 2 deletions(-)

diff --git a/docs/usage/self-hosted-configuration.md b/docs/usage/self-hosted-configuration.md
index 61a3b3a44a..4c3c178ef7 100644
--- a/docs/usage/self-hosted-configuration.md
+++ b/docs/usage/self-hosted-configuration.md
@@ -139,6 +139,14 @@ For example:
 }
 ```
 
+## bbUseDevelopmentBranch
+
+By default, Renovate will use a repository's "main branch" (typically called `main` or `master`) as the "default branch".
+
+Configuring this to `true` means that Renovate will detect and use the Bitbucket [development branch](https://support.atlassian.com/bitbucket-cloud/docs/branch-a-repository/#The-branching-model) as defined by the repository's branching model.
+
+If the "development branch" is configured but the branch itself does not exist (e.g. it was deleted), Renovate will fall back to using the repository's "main branch". This fall back behavior matches that of the Bitbucket Cloud web interface.
+
 ## binarySource
 
 Renovate often needs to use third-party binaries in its PRs, like `npm` to update `package-lock.json` or `go` to update `go.sum`.
diff --git a/lib/config/options/index.ts b/lib/config/options/index.ts
index 1ed247e661..162271a35a 100644
--- a/lib/config/options/index.ts
+++ b/lib/config/options/index.ts
@@ -1623,6 +1623,14 @@ const options: RenovateOptions[] = [
     default: true,
     supportedPlatforms: ['bitbucket', 'bitbucket-server'],
   },
+  {
+    name: 'bbUseDevelopmentBranch',
+    description: `Use the repository's [development branch](https://support.atlassian.com/bitbucket-cloud/docs/branch-a-repository/#The-branching-model) as the repository's default branch.`,
+    type: 'boolean',
+    default: false,
+    supportedPlatforms: ['bitbucket'],
+    globalOnly: true,
+  },
   // Automatic merging
   {
     name: 'automerge',
diff --git a/lib/modules/platform/bitbucket/index.spec.ts b/lib/modules/platform/bitbucket/index.spec.ts
index 8724afef6c..b81c7b809a 100644
--- a/lib/modules/platform/bitbucket/index.spec.ts
+++ b/lib/modules/platform/bitbucket/index.spec.ts
@@ -171,6 +171,58 @@ describe('modules/platform/bitbucket/index', () => {
     });
   });
 
+  describe('bbUseDevelopmentBranch', () => {
+    it('not enabled: defaults to using main branch', async () => {
+      httpMock
+        .scope(baseUrl)
+        .get('/2.0/repositories/some/repo')
+        .reply(200, { owner: {}, mainbranch: { name: 'master' } });
+
+      const res = await bitbucket.initRepo({
+        repository: 'some/repo',
+        bbUseDevelopmentBranch: false,
+      });
+
+      expect(res.defaultBranch).toBe('master');
+    });
+
+    it('enabled: uses development branch when development branch exists', async () => {
+      httpMock
+        .scope(baseUrl)
+        .get('/2.0/repositories/some/repo')
+        .reply(200, { owner: {}, mainbranch: { name: 'master' } })
+        .get('/2.0/repositories/some/repo/branching-model')
+        .reply(200, {
+          development: { name: 'develop', branch: { name: 'develop' } },
+        });
+
+      const res = await bitbucket.initRepo({
+        repository: 'some/repo',
+        bbUseDevelopmentBranch: true,
+      });
+
+      expect(res.defaultBranch).toBe('develop');
+    });
+
+    it('enabled: falls back to mainbranch if development branch does not exist', async () => {
+      httpMock
+        .scope(baseUrl)
+        .get('/2.0/repositories/some/repo')
+        .reply(200, { owner: {}, mainbranch: { name: 'master' } })
+        .get('/2.0/repositories/some/repo/branching-model')
+        .reply(200, {
+          development: { name: 'develop' },
+        });
+
+      const res = await bitbucket.initRepo({
+        repository: 'some/repo',
+        bbUseDevelopmentBranch: true,
+      });
+
+      expect(res.defaultBranch).toBe('master');
+    });
+  });
+
   describe('getRepoForceRebase()', () => {
     it('always return false, since bitbucket does not support force rebase', async () => {
       const actual = await bitbucket.getRepoForceRebase();
diff --git a/lib/modules/platform/bitbucket/index.ts b/lib/modules/platform/bitbucket/index.ts
index a4eaa4016a..efe7852211 100644
--- a/lib/modules/platform/bitbucket/index.ts
+++ b/lib/modules/platform/bitbucket/index.ts
@@ -39,6 +39,7 @@ import type {
   EffectiveReviewer,
   PagedResult,
   PrResponse,
+  RepoBranchingModel,
   RepoInfo,
   RepoInfoBody,
 } from './types';
@@ -159,6 +160,7 @@ export async function initRepo({
   repository,
   cloneSubmodules,
   ignorePrAuthor,
+  bbUseDevelopmentBranch,
 }: RepoParams): Promise<RepoResult> {
   logger.debug(`initRepo("${repository}")`);
   const opts = hostRules.find({
@@ -171,6 +173,7 @@ export async function initRepo({
     ignorePrAuthor,
   } as Config;
   let info: RepoInfo;
+  let mainBranch: string;
   try {
     info = utils.repoInfoTransformer(
       (
@@ -179,7 +182,23 @@ export async function initRepo({
         )
       ).body
     );
-    config.defaultBranch = info.mainbranch;
+
+    mainBranch = info.mainbranch;
+
+    if (bbUseDevelopmentBranch) {
+      // Fetch Bitbucket development branch
+      const developmentBranch = (
+        await bitbucketHttp.getJson<RepoBranchingModel>(
+          `/2.0/repositories/${repository}/branching-model`
+        )
+      ).body.development?.branch?.name;
+
+      if (developmentBranch) {
+        mainBranch = developmentBranch;
+      }
+    }
+
+    config.defaultBranch = mainBranch;
 
     config = {
       ...config,
@@ -221,7 +240,7 @@ export async function initRepo({
     cloneSubmodules,
   });
   const repoConfig: RepoResult = {
-    defaultBranch: info.mainbranch,
+    defaultBranch: mainBranch,
     isFork: info.isFork,
     repoFingerprint: repoFingerprint(info.uuid, defaults.endpoint),
   };
diff --git a/lib/modules/platform/bitbucket/types.ts b/lib/modules/platform/bitbucket/types.ts
index 94e58f6167..7611dba4ed 100644
--- a/lib/modules/platform/bitbucket/types.ts
+++ b/lib/modules/platform/bitbucket/types.ts
@@ -36,6 +36,15 @@ export interface RepoInfo {
   uuid: string;
 }
 
+export interface RepoBranchingModel {
+  development: {
+    name: string;
+    branch?: {
+      name: string;
+    };
+  };
+}
+
 export interface BranchResponse {
   target: {
     hash: string;
diff --git a/lib/modules/platform/types.ts b/lib/modules/platform/types.ts
index dab52e1aa8..eeafd70cd2 100644
--- a/lib/modules/platform/types.ts
+++ b/lib/modules/platform/types.ts
@@ -42,6 +42,7 @@ export interface RepoParams {
   renovateUsername?: string;
   cloneSubmodules?: boolean;
   ignorePrAuthor?: boolean;
+  bbUseDevelopmentBranch?: boolean;
 }
 
 export interface PrDebugData {
-- 
GitLab