Repository: scala-steward-org/scala-steward
Pull request #2810 (2022-12-06 08:28:34)

Add Bitbucket default reviewers option
Hello,
this PR adds default reviewers option for the `bitbucket` vcs type the same way it's working with `bitbucket-server`. Would be great to get it supported. We have this implementation running in our CI and it seems to work well.


Patch:
diff --git a/docs/help.md b/docs/help.md
index a9737868..801eb1d5 100644
--- a/docs/help.md
+++ b/docs/help.md
@@ -5,7 +5,7 @@ All command line arguments for the `scala-steward` application.
 ```
 Usage:
     scala-steward validate-repo-config
-    scala-steward --workspace <file> --repos-file <file> [--git-author-name <string>] --git-author-email <string> [--git-author-signing-key <string>] --git-ask-pass <file> [--sign-commits] [--vcs-type <vcs-type>] [--vcs-api-host <uri>] --vcs-login <string> [--do-not-fork] [--add-labels] [--ignore-opts-files] [--env-var <name=value>]... [--process-timeout <duration>] [--whitelist <string>]... [--read-only <string>]... [--enable-sandbox | --disable-sandbox] [--max-buffer-size <integer>] [--repo-config <uri>]... [--disable-default-repo-config] [--scalafix-migrations <uri>]... [--disable-default-scalafix-migrations] [--artifact-migrations <uri>]... [--disable-default-artifact-migrations] [--cache-ttl <duration>] [--bitbucket-server-use-default-reviewers] [--gitlab-merge-when-pipeline-succeeds] [--gitlab-required-reviewers <integer>] [--github-app-id <integer> --github-app-key-file <file>] [--url-checker-test-url <uri>]... [--default-maven-repo <string>] [--refresh-backoff-period <duration>]
+    scala-steward --workspace <file> --repos-file <file> [--git-author-name <string>] --git-author-email <string> [--git-author-signing-key <string>] --git-ask-pass <file> [--sign-commits] [--vcs-type <vcs-type>] [--vcs-api-host <uri>] --vcs-login <string> [--do-not-fork] [--add-labels] [--ignore-opts-files] [--env-var <name=value>]... [--process-timeout <duration>] [--whitelist <string>]... [--read-only <string>]... [--enable-sandbox | --disable-sandbox] [--max-buffer-size <integer>] [--repo-config <uri>]... [--disable-default-repo-config] [--scalafix-migrations <uri>]... [--disable-default-scalafix-migrations] [--artifact-migrations <uri>]... [--disable-default-artifact-migrations] [--cache-ttl <duration>] [--bitbucket-use-default-reviewers] [--bitbucket-server-use-default-reviewers] [--gitlab-merge-when-pipeline-succeeds] [--gitlab-required-reviewers <integer>] [--github-app-id <integer> --github-app-key-file <file>] [--url-checker-test-url <uri>]... [--default-maven-repo <string>] [--refresh-backoff-period <duration>]
 
 
 
@@ -66,8 +66,10 @@ Options and flags:
         Whether to disable the default artifact migration file
     --cache-ttl <duration>
         TTL for the caches; default: 2hours
-    --bitbucket-server-use-default-reviewers
+    --bitbucket-use-default-reviewers
         Whether to assign the default reviewers to a bitbucket pull request; default: false
+    --bitbucket-server-use-default-reviewers
+        Whether to assign the default reviewers to a bitbucket server pull request; default: false
     --gitlab-merge-when-pipeline-succeeds
         Whether to merge a gitlab merge request when the pipeline succeeds
     --gitlab-required-reviewers <integer>
diff --git a/modules/core/src/main/scala/org/scalasteward/core/application/Cli.scala b/modules/core/src/main/scala/org/scalasteward/core/application/Cli.scala
index 0dd65fe0..b692b4c8 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/application/Cli.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/application/Cli.scala
@@ -216,12 +216,21 @@ object Cli {
   private val bitbucketServerUseDefaultReviewers: Opts[Boolean] =
     flag(
       "bitbucket-server-use-default-reviewers",
+      "Whether to assign the default reviewers to a bitbucket server pull request; default: false"
+    ).orFalse
+
+  private val bitbucketUseDefaultReviewers: Opts[Boolean] =
+    flag(
+      "bitbucket-use-default-reviewers",
       "Whether to assign the default reviewers to a bitbucket pull request; default: false"
     ).orFalse
 
   private val bitbucketServerCfg: Opts[BitbucketServerCfg] =
     bitbucketServerUseDefaultReviewers.map(BitbucketServerCfg.apply)
 
+  private val bitbucketCfg: Opts[BitbucketCfg] =
+    bitbucketUseDefaultReviewers.map(BitbucketCfg.apply)
+
   private val gitlabMergeWhenPipelineSucceeds: Opts[Boolean] =
     flag(
       "gitlab-merge-when-pipeline-succeeds",
@@ -288,6 +297,7 @@ object Cli {
     scalafixCfg,
     artifactCfg,
     cacheTtl,
+    bitbucketCfg,
     bitbucketServerCfg,
     gitLabCfg,
     gitHubApp,
diff --git a/modules/core/src/main/scala/org/scalasteward/core/application/Config.scala b/modules/core/src/main/scala/org/scalasteward/core/application/Config.scala
index a3b28147..305b7cb3 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/application/Config.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/application/Config.scala
@@ -63,6 +63,7 @@ final case class Config(
     scalafixCfg: ScalafixCfg,
     artifactCfg: ArtifactCfg,
     cacheTtl: FiniteDuration,
+    bitbucketCfg: BitbucketCfg,
     bitbucketServerCfg: BitbucketServerCfg,
     gitLabCfg: GitLabCfg,
     githubApp: Option[GitHubApp],
@@ -132,6 +133,10 @@ object Config {
       useDefaultReviewers: Boolean
   )
 
+  final case class BitbucketCfg(
+      useDefaultReviewers: Boolean
+  )
+
   final case class GitLabCfg(
       mergeWhenPipelineSucceeds: Boolean,
       requiredReviewers: Option[Int]
diff --git a/modules/core/src/main/scala/org/scalasteward/core/vcs/VCSSelection.scala b/modules/core/src/main/scala/org/scalasteward/core/vcs/VCSSelection.scala
index 67c20bdb..52ecfc4c 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/vcs/VCSSelection.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/vcs/VCSSelection.scala
@@ -48,7 +48,11 @@ final class VCSSelection[F[_]](config: Config, user: AuthenticatedUser)(implicit
     )
 
   private def bitbucketApiAlg: BitbucketApiAlg[F] =
-    new BitbucketApiAlg(config.vcsCfg, _ => bitbucket.authentication.addCredentials(user))
+    new BitbucketApiAlg(
+      config.vcsCfg,
+      config.bitbucketCfg,
+      _ => bitbucket.authentication.addCredentials(user)
+    )
 
   private def bitbucketServerApiAlg: BitbucketServerApiAlg[F] = {
     // Bypass the server-side XSRF check, see
diff --git a/modules/core/src/main/scala/org/scalasteward/core/vcs/bitbucket/BitbucketApiAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/vcs/bitbucket/BitbucketApiAlg.scala
index 33007697..e9fdf8ae 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/vcs/bitbucket/BitbucketApiAlg.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/vcs/bitbucket/BitbucketApiAlg.scala
@@ -26,9 +26,14 @@ import org.scalasteward.core.vcs.VCSApiAlg
 import org.scalasteward.core.vcs.bitbucket.json._
 import org.scalasteward.core.vcs.data._
 import org.typelevel.log4cats.Logger
+import org.scalasteward.core.application.Config.BitbucketCfg
 
 /** https://developer.atlassian.com/bitbucket/api/2/reference/ */
-class BitbucketApiAlg[F[_]](config: VCSCfg, modify: Repo => Request[F] => F[Request[F]])(implicit
+class BitbucketApiAlg[F[_]](
+    config: VCSCfg,
+    bitbucketCfg: BitbucketCfg,
+    modify: Repo => Request[F] => F[Request[F]]
+)(implicit
     client: HttpJsonClient[F],
     logger: Logger[F],
     F: MonadThrow[F]
@@ -59,17 +64,29 @@ class BitbucketApiAlg[F[_]](config: VCSCfg, modify: Repo => Request[F] => F[Requ
       repo.mainBranch
     )
 
+  private def getDefaultReviewers(repo: Repo): F[List[Reviewer]] =
+    client.get[DefaultReviewers](url.defaultReviewers(repo), modify(repo)).map(_.values)
+
   override def createPullRequest(repo: Repo, data: NewPullRequestData): F[PullRequestOut] = {
     val sourceBranchOwner = if (config.doNotFork) repo.owner else config.login
+    val defaultReviewers =
+      if (bitbucketCfg.useDefaultReviewers) getDefaultReviewers(repo)
+      else F.pure(List.empty[Reviewer])
 
-    val payload = CreatePullRequestRequest(
-      data.title,
-      Branch(data.head),
-      Repo(sourceBranchOwner, repo.repo, repo.branch),
-      data.base,
-      data.body
-    )
-    client.postWithBody(url.pullRequests(repo), payload, modify(repo))
+    defaultReviewers
+      .map(reviewers =>
+        CreatePullRequestRequest(
+          data.title,
+          Branch(data.head),
+          Repo(sourceBranchOwner, repo.repo, repo.branch),
+          data.base,
+          data.body,
+          reviewers
+        )
+      )
+      .flatMap { payload =>
+        client.postWithBody(url.pullRequests(repo), payload, modify(repo))
+      }
   }
 
   override def getBranch(repo: Repo, branch: Branch): F[BranchOut] =
diff --git a/modules/core/src/main/scala/org/scalasteward/core/vcs/bitbucket/CreatePullRequestRequest.scala b/modules/core/src/main/scala/org/scalasteward/core/vcs/bitbucket/CreatePullRequestRequest.scala
index e1a2bccc..127e3510 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/vcs/bitbucket/CreatePullRequestRequest.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/vcs/bitbucket/CreatePullRequestRequest.scala
@@ -25,7 +25,8 @@ private[bitbucket] case class CreatePullRequestRequest(
     sourceBranch: Branch,
     sourceRepo: Repo,
     destinationBranch: Branch,
-    description: String
+    description: String,
+    reviewers: List[Reviewer]
 )
 
 private[bitbucket] object CreatePullRequestRequest {
@@ -48,6 +49,12 @@ private[bitbucket] object CreatePullRequestRequest {
         Json.obj(
           ("branch", Json.obj(("name", Json.fromString(d.destinationBranch.name))))
         )
+      ),
+      (
+        "reviewers",
+        Json.fromValues(
+          d.reviewers.map(r => Json.obj(("uuid", Json.fromString(r.uuid))))
+        )
       )
     )
   }
diff --git a/modules/core/src/main/scala/org/scalasteward/core/vcs/bitbucket/Reviewer.scala b/modules/core/src/main/scala/org/scalasteward/core/vcs/bitbucket/Reviewer.scala
new file mode 100644
index 00000000..a2f54142
--- /dev/null
+++ b/modules/core/src/main/scala/org/scalasteward/core/vcs/bitbucket/Reviewer.scala
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2018-2022 Scala Steward contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.scalasteward.core.vcs.bitbucket
+import io.circe.Codec
+import io.circe.generic.semiauto.deriveCodec
+
+final private[bitbucket] case class DefaultReviewers(values: List[Reviewer])
+private[bitbucket] object DefaultReviewers {
+  implicit val defaultReviewersCodec: Codec[DefaultReviewers] = deriveCodec
+}
+
+final private[bitbucket] case class Reviewer(uuid: String)
+private[bitbucket] object Reviewer {
+  implicit val reviewerCodec: Codec[Reviewer] = deriveCodec
+}
diff --git a/modules/core/src/main/scala/org/scalasteward/core/vcs/bitbucket/Url.scala b/modules/core/src/main/scala/org/scalasteward/core/vcs/bitbucket/Url.scala
index e11f10c8..265b8ff3 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/vcs/bitbucket/Url.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/vcs/bitbucket/Url.scala
@@ -44,4 +44,7 @@ private[bitbucket] class Url(apiHost: Uri) {
 
   def decline(rep: Repo, number: PullRequestNumber): Uri =
     pullRequest(rep, number) / "decline"
+
+  def defaultReviewers(rep: Repo): Uri =
+    repo(rep) / "default-reviewers"
 }


Test patch:
diff --git a/modules/core/src/test/scala/org/scalasteward/core/application/CliTest.scala b/modules/core/src/test/scala/org/scalasteward/core/application/CliTest.scala
index 5211cc7a..de87b6aa 100644
--- a/modules/core/src/test/scala/org/scalasteward/core/application/CliTest.scala
+++ b/modules/core/src/test/scala/org/scalasteward/core/application/CliTest.scala
@@ -33,7 +33,8 @@ class CliTest extends FunSuite {
         List("--repo-config", "/opt/scala-steward/scala-steward.conf"),
         List("--github-app-id", "12345678"),
         List("--github-app-key-file", "example_app_key"),
-        List("--refresh-backoff-period", "1 day")
+        List("--refresh-backoff-period", "1 day"),
+        List("--bitbucket-use-default-reviewers")
       ).flatten
     )
 
@@ -64,6 +65,8 @@ class CliTest extends FunSuite {
     assertEquals(obtained.refreshBackoffPeriod, 1.day)
     assert(!obtained.gitLabCfg.mergeWhenPipelineSucceeds)
     assertEquals(obtained.gitLabCfg.requiredReviewers, None)
+    assert(obtained.bitbucketCfg.useDefaultReviewers)
+    assert(!obtained.bitbucketServerCfg.useDefaultReviewers)
   }
 
   val minimumRequiredParams = List(
diff --git a/modules/core/src/test/scala/org/scalasteward/core/vcs/bitbucket/BitbucketApiAlgTest.scala b/modules/core/src/test/scala/org/scalasteward/core/vcs/bitbucket/BitbucketApiAlgTest.scala
index 27aba1b0..49378ea8 100644
--- a/modules/core/src/test/scala/org/scalasteward/core/vcs/bitbucket/BitbucketApiAlgTest.scala
+++ b/modules/core/src/test/scala/org/scalasteward/core/vcs/bitbucket/BitbucketApiAlgTest.scala
@@ -15,6 +15,7 @@ import org.scalasteward.core.git._
 import org.scalasteward.core.mock.MockConfig.config
 import org.scalasteward.core.util.HttpJsonClient
 import org.scalasteward.core.vcs.data._
+import org.scalasteward.core.application.Config.BitbucketCfg
 
 class BitbucketApiAlgTest extends FunSuite {
   private val routes: HttpRoutes[IO] = HttpRoutes.of[IO] {
@@ -118,6 +119,19 @@ class BitbucketApiAlgTest extends FunSuite {
             }
           }"""
       )
+    case GET -> Root / "repositories" / "fthomas" / "base.g8" / "default-reviewers" =>
+      Ok(
+        json"""{
+          "values": [
+            {
+                "uuid": "{874fbd19-d98a-4f32-860c-476b9288b1b1}"
+            },
+            {
+                "uuid": "{af922393-90eb-4d59-a4f8-222f7bd45495}"
+            }
+          ]
+        }"""
+      )
     case GET -> Root / "repositories" / "fthomas" / "base.g8" / "pullrequests" =>
       Ok(
         json"""{
@@ -161,7 +175,9 @@ class BitbucketApiAlgTest extends FunSuite {
 
   implicit val client: Client[IO] = Client.fromHttpApp(routes.orNotFound)
   implicit val httpJsonClient: HttpJsonClient[IO] = new HttpJsonClient[IO]
-  private val bitbucketApiAlg = new BitbucketApiAlg[IO](config.vcsCfg, _ => IO.pure)
+  private val bitbucketCfg = BitbucketCfg(useDefaultReviewers = true)
+  private val bitbucketApiAlg =
+    new BitbucketApiAlg[IO](config.vcsCfg, bitbucketCfg, _ => IO.pure)
 
   private val prUrl = uri"https://bitbucket.org/fthomas/base.g8/pullrequests/2"
   private val repo = Repo("fthomas", "base.g8")