Repository: scala-steward-org/scala-steward
Pull request #1868 (2021-01-05 21:47:22)

Add Git, GitLab, and BitbucketServer config classes

codecov[bot]: # [Codecov](https://codecov.io/gh/scala-steward-org/scala-steward/pull/1868?src=pr&el=h1) Report
> Merging [#1868](https://codecov.io/gh/scala-steward-org/scala-steward/pull/1868?src=pr&el=desc) (5b75b18) into [master](https://codecov.io/gh/scala-steward-org/scala-steward/commit/e47151a71ea868c1191bd62eb862adda87aa87c2?el=desc) (e47151a) will **decrease** coverage by `0.10%`.
> The diff coverage is `39.39%`.

[![Impacted file tree graph](https://codecov.io/gh/scala-steward-org/scala-steward/pull/1868/graphs/tree.svg?width=650&height=150&src=pr&token=udzMsc1Dne)](https://codecov.io/gh/scala-steward-org/scala-steward/pull/1868?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master    #1868      +/-   ##
==========================================
- Coverage   75.55%   75.45%   -0.11%     
==========================================
  Files         127      127              
  Lines        2148     2155       +7     
  Branches       59       62       +3     
==========================================
+ Hits         1623     1626       +3     
- Misses        525      529       +4     
```


| [Impacted Files](https://codecov.io/gh/scala-steward-org/scala-steward/pull/1868?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [...la/org/scalasteward/core/application/Context.scala](https://codecov.io/gh/scala-steward-org/scala-steward/pull/1868/diff?src=pr&el=tree#diff-bW9kdWxlcy9jb3JlL3NyYy9tYWluL3NjYWxhL29yZy9zY2FsYXN0ZXdhcmQvY29yZS9hcHBsaWNhdGlvbi9Db250ZXh0LnNjYWxh) | `0.00% <0.00%> (ø)` | |
| [...n/scala/org/scalasteward/core/git/FileGitAlg.scala](https://codecov.io/gh/scala-steward-org/scala-steward/pull/1868/diff?src=pr&el=tree#diff-bW9kdWxlcy9jb3JlL3NyYy9tYWluL3NjYWxhL29yZy9zY2FsYXN0ZXdhcmQvY29yZS9naXQvRmlsZUdpdEFsZy5zY2FsYQ==) | `98.11% <ø> (ø)` | |
| [...in/scala/org/scalasteward/core/git/GenGitAlg.scala](https://codecov.io/gh/scala-steward-org/scala-steward/pull/1868/diff?src=pr&el=tree#diff-bW9kdWxlcy9jb3JlL3NyYy9tYWluL3NjYWxhL29yZy9zY2FsYXN0ZXdhcmQvY29yZS9naXQvR2VuR2l0QWxnLnNjYWxh) | `92.00% <ø> (ø)` | |
| [...scala/org/scalasteward/core/vcs/VCSSelection.scala](https://codecov.io/gh/scala-steward-org/scala-steward/pull/1868/diff?src=pr&el=tree#diff-bW9kdWxlcy9jb3JlL3NyYy9tYWluL3NjYWxhL29yZy9zY2FsYXN0ZXdhcmQvY29yZS92Y3MvVkNTU2VsZWN0aW9uLnNjYWxh) | `0.00% <0.00%> (ø)` | |
| [...re/vcs/bitbucketserver/BitbucketServerApiAlg.scala](https://codecov.io/gh/scala-steward-org/scala-steward/pull/1868/diff?src=pr&el=tree#diff-bW9kdWxlcy9jb3JlL3NyYy9tYWluL3NjYWxhL29yZy9zY2FsYXN0ZXdhcmQvY29yZS92Y3MvYml0YnVja2V0c2VydmVyL0JpdGJ1Y2tldFNlcnZlckFwaUFsZy5zY2FsYQ==) | `48.64% <0.00%> (ø)` | |
| [...rg/scalasteward/core/vcs/gitlab/GitLabApiAlg.scala](https://codecov.io/gh/scala-steward-org/scala-steward/pull/1868/diff?src=pr&el=tree#diff-bW9kdWxlcy9jb3JlL3NyYy9tYWluL3NjYWxhL29yZy9zY2FsYXN0ZXdhcmQvY29yZS92Y3MvZ2l0bGFiL0dpdExhYkFwaUFsZy5zY2FsYQ==) | `65.47% <66.66%> (ø)` | |
| [...ala/org/scalasteward/core/application/Config.scala](https://codecov.io/gh/scala-steward-org/scala-steward/pull/1868/diff?src=pr&el=tree#diff-bW9kdWxlcy9jb3JlL3NyYy9tYWluL3NjYWxhL29yZy9zY2FsYXN0ZXdhcmQvY29yZS9hcHBsaWNhdGlvbi9Db25maWcuc2NhbGE=) | `91.17% <88.88%> (+0.85%)` | :arrow_up: |
| [...n/scala/org/scalasteward/core/vcs/VCSRepoAlg.scala](https://codecov.io/gh/scala-steward-org/scala-steward/pull/1868/diff?src=pr&el=tree#diff-bW9kdWxlcy9jb3JlL3NyYy9tYWluL3NjYWxhL29yZy9zY2FsYXN0ZXdhcmQvY29yZS92Y3MvVkNTUmVwb0FsZy5zY2FsYQ==) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/scala-steward-org/scala-steward/pull/1868?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/scala-steward-org/scala-steward/pull/1868?src=pr&el=footer). Last update [e47151a...5b75b18](https://codecov.io/gh/scala-steward-org/scala-steward/pull/1868?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
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 1a257d61..9a46dde9 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
@@ -22,7 +22,7 @@ import cats.effect.Sync
 import org.http4s.Uri
 import org.http4s.Uri.UserInfo
 import org.scalasteward.core.application.Cli.EnvVar
-import org.scalasteward.core.application.Config.{ProcessCfg, ScalafixCfg}
+import org.scalasteward.core.application.Config._
 import org.scalasteward.core.git.Author
 import org.scalasteward.core.util
 import org.scalasteward.core.vcs.data.AuthenticatedUser
@@ -46,7 +46,7 @@ import scala.sys.process.Process
   * For GitHub Enterprise this is "http(s)://[hostname]/api/v3", see
   * [[https://developer.github.com/enterprise/v3/]].
   *
-  * == [[gitAskPass]] ==
+  * == gitCfg.gitAskPass ==
   * Program that is invoked by scala-steward and git (via the `GIT_ASKPASS`
   * environment variable) to request the password for the user [[vcsLogin]].
   *
@@ -58,33 +58,37 @@ final case class Config(
     workspace: File,
     reposFile: File,
     defaultRepoConfigFile: Option[File],
-    gitAuthor: Author,
+    gitCfg: GitCfg,
     vcsType: SupportedVCS,
     vcsApiHost: Uri,
     vcsLogin: String,
-    gitAskPass: File,
-    signCommits: Boolean,
     doNotFork: Boolean,
     ignoreOptsFiles: Boolean,
     processCfg: ProcessCfg,
     scalafixCfg: ScalafixCfg,
     artifactMigrations: Option[File],
     cacheTtl: FiniteDuration,
-    bitbucketServerUseDefaultReviewers: Boolean,
-    gitlabMergeWhenPipelineSucceeds: Boolean,
+    bitbucketServerCfg: BitbucketServerCfg,
+    gitLabCfg: GitLabCfg,
     githubApp: Option[GitHubApp]
 ) {
   def vcsUser[F[_]](implicit F: Sync[F]): F[AuthenticatedUser] = {
     val urlWithUser = util.uri.withUserInfo.set(UserInfo(vcsLogin, None))(vcsApiHost).renderString
     val prompt = s"Password for '$urlWithUser': "
     F.delay {
-      val password = Process(List(gitAskPass.pathAsString, prompt)).!!.trim
+      val password = Process(List(gitCfg.gitAskPass.pathAsString, prompt)).!!.trim
       AuthenticatedUser(vcsLogin, password)
     }
   }
 }
 
 object Config {
+  final case class GitCfg(
+      gitAuthor: Author,
+      gitAskPass: File,
+      signCommits: Boolean
+  )
+
   final case class ProcessCfg(
       envVars: List[EnvVar],
       processTimeout: FiniteDuration,
@@ -103,17 +107,27 @@ object Config {
       disableDefaults: Boolean
   )
 
+  final case class BitbucketServerCfg(
+      useDefaultReviewers: Boolean
+  )
+
+  final case class GitLabCfg(
+      mergeWhenPipelineSucceeds: Boolean
+  )
+
   def from(args: Cli.Args): Config =
     Config(
       workspace = args.workspace,
       reposFile = args.reposFile,
       defaultRepoConfigFile = args.defaultRepoConf,
-      gitAuthor = Author(args.gitAuthorName, args.gitAuthorEmail, args.gitAuthorSigningKey),
+      gitCfg = GitCfg(
+        gitAuthor = Author(args.gitAuthorName, args.gitAuthorEmail, args.gitAuthorSigningKey),
+        gitAskPass = args.gitAskPass,
+        signCommits = args.signCommits
+      ),
       vcsType = args.vcsType,
       vcsApiHost = args.vcsApiHost,
       vcsLogin = args.vcsLogin,
-      gitAskPass = args.gitAskPass,
-      signCommits = args.signCommits,
       doNotFork = args.doNotFork,
       ignoreOptsFiles = args.ignoreOptsFiles,
       processCfg = ProcessCfg(
@@ -132,8 +146,12 @@ object Config {
       ),
       artifactMigrations = args.artifactMigrations,
       cacheTtl = args.cacheTtl,
-      bitbucketServerUseDefaultReviewers = args.bitbucketServerUseDefaultReviewers,
-      gitlabMergeWhenPipelineSucceeds = args.gitlabMergeWhenPipelineSucceeds,
+      bitbucketServerCfg = BitbucketServerCfg(
+        useDefaultReviewers = args.bitbucketServerUseDefaultReviewers
+      ),
+      gitLabCfg = GitLabCfg(
+        mergeWhenPipelineSucceeds = args.gitlabMergeWhenPipelineSucceeds
+      ),
       githubApp = Apply[Option].map2(args.githubAppId, args.githubAppKeyFile)(GitHubApp)
     )
 }
diff --git a/modules/core/src/main/scala/org/scalasteward/core/application/Context.scala b/modules/core/src/main/scala/org/scalasteward/core/application/Context.scala
index 4742d8c3..5d8fe514 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/application/Context.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/application/Context.scala
@@ -42,7 +42,7 @@ import org.scalasteward.core.scalafmt.ScalafmtAlg
 import org.scalasteward.core.update.{ArtifactMigrations, FilterAlg, PruningAlg, UpdateAlg}
 import org.scalasteward.core.util._
 import org.scalasteward.core.util.uri._
-import org.scalasteward.core.vcs.data.{AuthenticatedUser, Repo}
+import org.scalasteward.core.vcs.data.Repo
 import org.scalasteward.core.vcs.github.{GitHubAppApiAlg, GitHubAuthAlg}
 import org.scalasteward.core.vcs.{VCSApiAlg, VCSExtraAlg, VCSRepoAlg, VCSSelection}
 
@@ -57,7 +57,7 @@ object Context {
       config <- Resource.pure(Config.from(args))
       implicit0(client: Client[F]) <- OkHttpBuilder.withDefaultClient[F](blocker).map(_.create)
       implicit0(urlChecker: UrlChecker[F]) <- UrlChecker.create[F](config)
-      implicit0(user: AuthenticatedUser) <- Resource.liftF(config.vcsUser[F])
+      vcsUser <- Resource.liftF(config.vcsUser[F])
       implicit0(fileAlg: FileAlg[F]) = FileAlg.create[F]
       implicit0(migrationAlg: MigrationAlg) <-
         Resource.liftF(new MigrationsLoader[F].loadAll(config.scalafixCfg).map(new MigrationAlg(_)))
@@ -75,14 +75,13 @@ object Context {
       implicit val processAlg: ProcessAlg[F] = ProcessAlg.create[F](blocker, config.processCfg)
       implicit val repoConfigAlg: RepoConfigAlg[F] = new RepoConfigAlg[F](config)
       implicit val filterAlg: FilterAlg[F] = new FilterAlg[F]
-      implicit val gitAlg: GitAlg[F] = GenGitAlg.create[F](config)
+      implicit val gitAlg: GitAlg[F] = GenGitAlg.create[F](config.gitCfg)
       implicit val gitHubAuthAlg: GitHubAuthAlg[F] = GitHubAuthAlg.create[F]
       implicit val hookExecutor: HookExecutor[F] = new HookExecutor[F]
       implicit val httpJsonClient: HttpJsonClient[F] = new HttpJsonClient[F]
       implicit val repoCacheRepository: RepoCacheRepository[F] =
         new RepoCacheRepository[F](new JsonKeyValueStore("repo_cache", "1", kvsPrefix))
-      val vcsSelection = new VCSSelection[F]
-      implicit val vcsApiAlg: VCSApiAlg[F] = vcsSelection.getAlg(config)
+      implicit val vcsApiAlg: VCSApiAlg[F] = new VCSSelection[F](config, vcsUser).vcsApiAlg
       implicit val vcsRepoAlg: VCSRepoAlg[F] = new VCSRepoAlg[F](config)
       implicit val vcsExtraAlg: VCSExtraAlg[F] = VCSExtraAlg.create[F](config)
       implicit val pullRequestRepository: PullRequestRepository[F] =
diff --git a/modules/core/src/main/scala/org/scalasteward/core/git/FileGitAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/git/FileGitAlg.scala
index 1569806c..d6e696a5 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/git/FileGitAlg.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/git/FileGitAlg.scala
@@ -20,12 +20,12 @@ import better.files.File
 import cats.effect.BracketThrow
 import cats.syntax.all._
 import org.http4s.Uri
-import org.scalasteward.core.application.Config
+import org.scalasteward.core.application.Config.GitCfg
 import org.scalasteward.core.git.FileGitAlg.dotdot
 import org.scalasteward.core.io.{FileAlg, ProcessAlg, WorkspaceAlg}
 import org.scalasteward.core.util.Nel
 
-final class FileGitAlg[F[_]](config: Config)(implicit
+final class FileGitAlg[F[_]](config: GitCfg)(implicit
     fileAlg: FileAlg[F],
     processAlg: ProcessAlg[F],
     workspaceAlg: WorkspaceAlg[F],
diff --git a/modules/core/src/main/scala/org/scalasteward/core/git/GenGitAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/git/GenGitAlg.scala
index 5133b3d8..bba23a1d 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/git/GenGitAlg.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/git/GenGitAlg.scala
@@ -20,7 +20,7 @@ import cats.effect.{Bracket, BracketThrow}
 import cats.syntax.all._
 import cats.{FlatMap, Monad}
 import org.http4s.Uri
-import org.scalasteward.core.application.Config
+import org.scalasteward.core.application.Config.GitCfg
 import org.scalasteward.core.io.{FileAlg, ProcessAlg, WorkspaceAlg}
 
 trait GenGitAlg[F[_], Repo] {
@@ -147,7 +147,7 @@ trait GenGitAlg[F[_], Repo] {
 }
 
 object GenGitAlg {
-  def create[F[_]](config: Config)(implicit
+  def create[F[_]](config: GitCfg)(implicit
       fileAlg: FileAlg[F],
       processAlg: ProcessAlg[F],
       workspaceAlg: WorkspaceAlg[F],
diff --git a/modules/core/src/main/scala/org/scalasteward/core/vcs/VCSRepoAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/vcs/VCSRepoAlg.scala
index 7e4c6ff4..c5d80f32 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/vcs/VCSRepoAlg.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/vcs/VCSRepoAlg.scala
@@ -47,7 +47,7 @@ final class VCSRepoAlg[F[_]](config: Config)(implicit
   private def clone(repo: Repo, repoOut: RepoOut): F[Unit] =
     for {
       _ <- gitAlg.clone(repo, withLogin(repoOut.clone_url))
-      _ <- gitAlg.setAuthor(repo, config.gitAuthor)
+      _ <- gitAlg.setAuthor(repo, config.gitCfg.gitAuthor)
     } yield ()
 
   private[vcs] def syncFork(repo: Repo, repoOut: RepoOut): F[Unit] =
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 c8444813..74b1231a 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
@@ -28,54 +28,49 @@ import org.scalasteward.core.vcs.data.AuthenticatedUser
 import org.scalasteward.core.vcs.github.GitHubApiAlg
 import org.scalasteward.core.vcs.gitlab.GitLabApiAlg
 
-final class VCSSelection[F[_]](implicit
+final class VCSSelection[F[_]](config: Config, user: AuthenticatedUser)(implicit
     client: HttpJsonClient[F],
-    user: AuthenticatedUser,
     logger: Logger[F],
     F: MonadThrow[F]
 ) {
-  private def github(config: Config): GitHubApiAlg[F] = {
-    import org.scalasteward.core.vcs.github.authentication.addCredentials
+  private def gitHubApiAlg: GitHubApiAlg[F] =
+    new GitHubApiAlg[F](config.vcsApiHost, _ => github.authentication.addCredentials(user))
 
-    new GitHubApiAlg[F](config.vcsApiHost, _ => addCredentials(user))
-  }
-
-  private def gitlab(config: Config): GitLabApiAlg[F] = {
-    import org.scalasteward.core.vcs.gitlab.authentication.addCredentials
+  private def gitLabApiAlg: GitLabApiAlg[F] =
     new GitLabApiAlg[F](
       config.vcsApiHost,
-      user,
-      _ => addCredentials(user),
       config.doNotFork,
-      config.gitlabMergeWhenPipelineSucceeds
+      config.gitLabCfg,
+      user,
+      _ => gitlab.authentication.addCredentials(user)
     )
-  }
-
-  private def bitbucket(config: Config): BitbucketApiAlg[F] = {
-    import org.scalasteward.core.vcs.bitbucket.authentication.addCredentials
 
-    new BitbucketApiAlg(config.vcsApiHost, user, _ => addCredentials(user), config.doNotFork)
-  }
-
-  private def bitbucketServer(config: Config): BitbucketServerApiAlg[F] = {
-    import org.scalasteward.core.vcs.bitbucket.authentication.addCredentials
+  private def bitbucketApiAlg: BitbucketApiAlg[F] =
+    new BitbucketApiAlg(
+      config.vcsApiHost,
+      user,
+      _ => bitbucket.authentication.addCredentials(user),
+      config.doNotFork
+    )
 
+  private def bitbucketServerApiAlg: BitbucketServerApiAlg[F] = {
     // Bypass the server-side XSRF check, see
     // https://github.com/scala-steward-org/scala-steward/pull/1863#issuecomment-754538364
     val xAtlassianToken = Header("X-Atlassian-Token", "no-check")
 
     new BitbucketServerApiAlg[F](
       config.vcsApiHost,
-      _ => req => addCredentials(user).apply(req.putHeaders(xAtlassianToken)),
-      config.bitbucketServerUseDefaultReviewers
+      config.bitbucketServerCfg,
+      _ =>
+        req => bitbucket.authentication.addCredentials(user).apply(req.putHeaders(xAtlassianToken))
     )
   }
 
-  def getAlg(config: Config): VCSApiAlg[F] =
+  def vcsApiAlg: VCSApiAlg[F] =
     config.vcsType match {
-      case GitHub          => github(config)
-      case GitLab          => gitlab(config)
-      case Bitbucket       => bitbucket(config)
-      case BitbucketServer => bitbucketServer(config)
+      case GitHub          => gitHubApiAlg
+      case GitLab          => gitLabApiAlg
+      case Bitbucket       => bitbucketApiAlg
+      case BitbucketServer => bitbucketServerApiAlg
     }
 }
diff --git a/modules/core/src/main/scala/org/scalasteward/core/vcs/bitbucketserver/BitbucketServerApiAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/vcs/bitbucketserver/BitbucketServerApiAlg.scala
index 012d036c..b16c02c2 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/vcs/bitbucketserver/BitbucketServerApiAlg.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/vcs/bitbucketserver/BitbucketServerApiAlg.scala
@@ -19,6 +19,7 @@ package org.scalasteward.core.vcs.bitbucketserver
 import cats.MonadThrow
 import cats.syntax.all._
 import org.http4s.{Request, Uri}
+import org.scalasteward.core.application.Config.BitbucketServerCfg
 import org.scalasteward.core.git.Branch
 import org.scalasteward.core.util.HttpJsonClient
 import org.scalasteward.core.vcs.VCSApiAlg
@@ -29,8 +30,8 @@ import org.scalasteward.core.vcs.data._
 /** https://docs.atlassian.com/bitbucket-server/rest/latest/bitbucket-rest.html */
 final class BitbucketServerApiAlg[F[_]](
     bitbucketApiHost: Uri,
-    modify: Repo => Request[F] => F[Request[F]],
-    useReviewers: Boolean
+    config: BitbucketServerCfg,
+    modify: Repo => Request[F] => F[Request[F]]
 )(implicit client: HttpJsonClient[F], F: MonadThrow[F])
     extends VCSApiAlg[F] {
   private val url = new Url(bitbucketApiHost)
@@ -81,7 +82,7 @@ final class BitbucketServerApiAlg[F[_]](
   }
 
   private def useDefaultReviewers(repo: Repo): F[List[Reviewer]] =
-    if (useReviewers) getDefaultReviewers(repo) else F.pure(List.empty[Reviewer])
+    if (config.useDefaultReviewers) getDefaultReviewers(repo) else F.pure(List.empty[Reviewer])
 
   private def declinePullRequest(repo: Repo, number: PullRequestNumber, version: Int): F[Unit] =
     client.post_(url.declinePullRequest(repo, number, version), modify(repo))
diff --git a/modules/core/src/main/scala/org/scalasteward/core/vcs/gitlab/GitLabApiAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/vcs/gitlab/GitLabApiAlg.scala
index fef0b882..3865fd24 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/vcs/gitlab/GitLabApiAlg.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/vcs/gitlab/GitLabApiAlg.scala
@@ -23,6 +23,7 @@ import io.circe._
 import io.circe.generic.semiauto._
 import io.circe.syntax._
 import org.http4s.{Request, Status, Uri}
+import org.scalasteward.core.application.Config.GitLabCfg
 import org.scalasteward.core.git.{Branch, Sha1}
 import org.scalasteward.core.util.uri.uriDecoder
 import org.scalasteward.core.util.{HttpJsonClient, UnexpectedResponse}
@@ -117,12 +118,12 @@ private[gitlab] object GitLabJsonCodec {
   implicit val branchOutDecoder: Decoder[BranchOut] = deriveDecoder[BranchOut]
 }
 
-class GitLabApiAlg[F[_]](
+final class GitLabApiAlg[F[_]](
     gitlabApiHost: Uri,
-    user: AuthenticatedUser,
-    modify: Repo => Request[F] => F[Request[F]],
     doNotFork: Boolean,
-    mergeWhenPipelineSucceeds: Boolean
+    config: GitLabCfg,
+    user: AuthenticatedUser,
+    modify: Repo => Request[F] => F[Request[F]]
 )(implicit
     client: HttpJsonClient[F],
     logger: Logger[F],
@@ -130,12 +131,12 @@ class GitLabApiAlg[F[_]](
 ) extends VCSApiAlg[F] {
   import GitLabJsonCodec._
 
-  val url = new Url(gitlabApiHost)
+  private val url = new Url(gitlabApiHost)
 
   override def listPullRequests(repo: Repo, head: String, base: Branch): F[List[PullRequestOut]] =
     client.get(url.listMergeRequests(repo, head, base.name), modify(repo))
 
-  def createFork(repo: Repo): F[RepoOut] = {
+  override def createFork(repo: Repo): F[RepoOut] = {
     val userOwnedRepo = repo.copy(owner = user.login)
     val data = ForkPayload(url.encodedProjectId(userOwnedRepo), user.login)
     client
@@ -148,7 +149,7 @@ class GitLabApiAlg[F[_]](
       }
   }
 
-  def createPullRequest(repo: Repo, data: NewPullRequestData): F[PullRequestOut] = {
+  override def createPullRequest(repo: Repo, data: NewPullRequestData): F[PullRequestOut] = {
     val targetRepo = if (doNotFork) repo else repo.copy(owner = user.login)
     val mergeRequest = for {
       projectId <- client.get[ProjectId](url.repos(repo), modify(repo))
@@ -167,19 +168,19 @@ class GitLabApiAlg[F[_]](
       client
         .get[MergeRequestOut](url.existingMergeRequest(repo, number), modify(repo))
         .flatMap {
-          case mr if (mr.mergeStatus =!= GitLabMergeStatus.Checking) => F.pure(mr)
-          case _ if (retries > 0)                                    => waitForMergeRequestStatus(number, retries - 1)
-          case other                                                 => F.pure(other)
+          case mr if mr.mergeStatus =!= GitLabMergeStatus.Checking => F.pure(mr)
+          case _ if retries > 0                                    => waitForMergeRequestStatus(number, retries - 1)
+          case other                                               => F.pure(other)
         }
 
     val updatedMergeRequest =
-      if (!mergeWhenPipelineSucceeds)
+      if (!config.mergeWhenPipelineSucceeds)
         mergeRequest
       else
         mergeRequest
           .flatMap(mr => waitForMergeRequestStatus(mr.iid))
           .flatMap {
-            case mr if (mr.mergeStatus === GitLabMergeStatus.CanBeMerged) =>
+            case mr if mr.mergeStatus === GitLabMergeStatus.CanBeMerged =>
               for {
                 _ <- logger.info(s"Setting ${mr.webUrl} to merge when pipeline succeeds")
                 res <-
@@ -203,7 +204,7 @@ class GitLabApiAlg[F[_]](
     updatedMergeRequest.map(_.pullRequestOut)
   }
 
-  def closePullRequest(repo: Repo, number: PullRequestNumber): F[PullRequestOut] =
+  override def closePullRequest(repo: Repo, number: PullRequestNumber): F[PullRequestOut] =
     client
       .putWithBody[MergeRequestOut, UpdateState](
         url.existingMergeRequest(repo, number),
@@ -212,10 +213,10 @@ class GitLabApiAlg[F[_]](
       )
       .map(_.pullRequestOut)
 
-  def getBranch(repo: Repo, branch: Branch): F[BranchOut] =
+  override def getBranch(repo: Repo, branch: Branch): F[BranchOut] =
     client.get(url.getBranch(repo, branch), modify(repo))
 
-  def getRepo(repo: Repo): F[RepoOut] =
+  override def getRepo(repo: Repo): F[RepoOut] =
     client.get(url.repos(repo), modify(repo))
 
   override def referencePullRequest(number: PullRequestNumber): String =
diff --git a/project/Dependencies.scala b/project/Dependencies.scala
index 60020f96..2cd8e0af 100644
--- a/project/Dependencies.scala
+++ b/project/Dependencies.scala
@@ -3,7 +3,7 @@ import sbt.Keys._
 import sbt.librarymanagement.syntax.ExclusionRule
 
 object Dependencies {
-  val attoCore = "org.tpolecat" %% "atto-core" % "0.8.0"
+  val attoCore = "org.tpolecat" %% "atto-core" % "0.9.0"
   val bcprovJdk15to18 = "org.bouncycastle" % "bcprov-jdk15to18" % "1.68"
   val betterFiles = "com.github.pathikrit" %% "better-files" % "3.9.1"
   val betterMonadicFor = "com.olegpy" %% "better-monadic-for" % "0.3.1"


Test patch:
diff --git a/modules/core/src/test/scala/org/scalasteward/core/edit/EditAlgTest.scala b/modules/core/src/test/scala/org/scalasteward/core/edit/EditAlgTest.scala
index 6196a3e2..0b4469f1 100644
--- a/modules/core/src/test/scala/org/scalasteward/core/edit/EditAlgTest.scala
+++ b/modules/core/src/test/scala/org/scalasteward/core/edit/EditAlgTest.scala
@@ -4,7 +4,7 @@ import better.files.File
 import munit.FunSuite
 import org.scalasteward.core.TestSyntax._
 import org.scalasteward.core.data.{GroupId, Update}
-import org.scalasteward.core.mock.MockContext.{config, editAlg}
+import org.scalasteward.core.mock.MockContext.{config, editAlg, envVars}
 import org.scalasteward.core.mock.MockState
 import org.scalasteward.core.repoconfig.RepoConfig
 import org.scalasteward.core.scalafmt.scalafmtBinary
@@ -12,7 +12,6 @@ import org.scalasteward.core.util.Nel
 import org.scalasteward.core.vcs.data.Repo
 
 class EditAlgTest extends FunSuite {
-  private val envVars = List(s"GIT_ASKPASS=${config.gitAskPass}", "VAR1=val1", "VAR2=val2")
   private val gitStatus =
     List("git", "status", "--porcelain", "--untracked-files=no", "--ignore-submodules")
 
diff --git a/modules/core/src/test/scala/org/scalasteward/core/edit/hooks/HookExecutorTest.scala b/modules/core/src/test/scala/org/scalasteward/core/edit/hooks/HookExecutorTest.scala
index 92467c04..5cebb6b0 100644
--- a/modules/core/src/test/scala/org/scalasteward/core/edit/hooks/HookExecutorTest.scala
+++ b/modules/core/src/test/scala/org/scalasteward/core/edit/hooks/HookExecutorTest.scala
@@ -3,7 +3,7 @@ package org.scalasteward.core.edit.hooks
 import munit.FunSuite
 import org.scalasteward.core.TestSyntax._
 import org.scalasteward.core.data.Update
-import org.scalasteward.core.mock.MockContext.{config, hookExecutor, workspaceAlg}
+import org.scalasteward.core.mock.MockContext.{envVars, hookExecutor, workspaceAlg}
 import org.scalasteward.core.mock.MockState
 import org.scalasteward.core.repoconfig.{RepoConfig, ScalafmtConfig}
 import org.scalasteward.core.scalafmt.{scalafmtArtifactId, scalafmtBinary, scalafmtGroupId}
@@ -13,7 +13,6 @@ import org.scalasteward.core.vcs.data.Repo
 class HookExecutorTest extends FunSuite {
   private val repo = Repo("scala-steward-org", "scala-steward")
   private val repoDir = workspaceAlg.repoDir(repo).runA(MockState.empty).unsafeRunSync()
-  private val envVars = List(s"GIT_ASKPASS=${config.gitAskPass}", "VAR1=val1", "VAR2=val2")
 
   test("no hook") {
     val update = Update.Single("org.typelevel" % "cats-core" % "1.2.0", Nel.of("1.3.0"))
diff --git a/modules/core/src/test/scala/org/scalasteward/core/git/FileGitAlgTest.scala b/modules/core/src/test/scala/org/scalasteward/core/git/FileGitAlgTest.scala
index 32d6983a..dbb8dbb9 100644
--- a/modules/core/src/test/scala/org/scalasteward/core/git/FileGitAlgTest.scala
+++ b/modules/core/src/test/scala/org/scalasteward/core/git/FileGitAlgTest.scala
@@ -16,7 +16,7 @@ import org.scalasteward.core.util.Nel
 class FileGitAlgTest extends FunSuite {
   implicit private val ioWorkspaceAlg: WorkspaceAlg[IO] = WorkspaceAlg.create[IO](config)
   implicit private val ioGitAlg: GenGitAlg[IO, File] =
-    new FileGitAlg[IO](config).contramapRepoF(IO.pure)
+    new FileGitAlg[IO](config.gitCfg).contramapRepoF(IO.pure)
   private val supplement = new Supplement[IO]
   private val rootDir = File.temp / "scala-steward" / "git-tests"
 
@@ -162,7 +162,7 @@ object FileGitAlgTest {
         _ <- gitAlg.removeClone(repo)
         _ <- fileAlg.ensureExists(repo)
         _ <- git("init", ".")(repo)
-        _ <- gitAlg.setAuthor(repo, config.gitAuthor)
+        _ <- gitAlg.setAuthor(repo, config.gitCfg.gitAuthor)
       } yield ()
 
     def createConflict(repo: File): F[Unit] =
diff --git a/modules/core/src/test/scala/org/scalasteward/core/mock/MockContext.scala b/modules/core/src/test/scala/org/scalasteward/core/mock/MockContext.scala
index 58871ded..dd37e653 100644
--- a/modules/core/src/test/scala/org/scalasteward/core/mock/MockContext.scala
+++ b/modules/core/src/test/scala/org/scalasteward/core/mock/MockContext.scala
@@ -54,6 +54,8 @@ object MockContext {
       )
     )
 
+  val envVars = List(s"GIT_ASKPASS=${config.gitCfg.gitAskPass}", "VAR1=val1", "VAR2=val2")
+
   implicit val mockEffBracketThrow: BracketThrow[MockEff] = Sync[MockEff]
   implicit val mockEffParallel: Parallel[MockEff] = Parallel.identity
 
@@ -64,7 +66,7 @@ object MockContext {
 
   implicit val coursierAlg: CoursierAlg[MockEff] = CoursierAlg.create
   implicit val dateTimeAlg: DateTimeAlg[MockEff] = DateTimeAlg.create
-  implicit val gitAlg: GitAlg[MockEff] = GenGitAlg.create(config)
+  implicit val gitAlg: GitAlg[MockEff] = GenGitAlg.create(config.gitCfg)
   implicit val hookExecutor: HookExecutor[MockEff] = new HookExecutor[MockEff]
   implicit val user: AuthenticatedUser = AuthenticatedUser("scala-steward", "token")
   implicit val vcsRepoAlg: VCSRepoAlg[MockEff] = new VCSRepoAlg[MockEff](config)
diff --git a/modules/core/src/test/scala/org/scalasteward/core/vcs/VCSRepoAlgTest.scala b/modules/core/src/test/scala/org/scalasteward/core/vcs/VCSRepoAlgTest.scala
index 3cde2ef0..29fc54a8 100644
--- a/modules/core/src/test/scala/org/scalasteward/core/vcs/VCSRepoAlgTest.scala
+++ b/modules/core/src/test/scala/org/scalasteward/core/vcs/VCSRepoAlgTest.scala
@@ -3,15 +3,13 @@ package org.scalasteward.core.vcs
 import munit.FunSuite
 import org.http4s.syntax.literals._
 import org.scalasteward.core.git.Branch
-import org.scalasteward.core.mock.MockContext.{config, gitAlg, mockLogger, vcsRepoAlg}
+import org.scalasteward.core.mock.MockContext.{config, envVars, gitAlg, mockLogger, vcsRepoAlg}
 import org.scalasteward.core.mock.{MockContext, MockEff, MockState}
 import org.scalasteward.core.vcs.data.{Repo, RepoOut, UserOut}
 
 class VCSRepoAlgTest extends FunSuite {
   val repo: Repo = Repo("fthomas", "datapackage")
   val repoDir: String = (config.workspace / "fthomas/datapackage").toString
-  val envVars = List(s"GIT_ASKPASS=${config.gitAskPass}", "VAR1=val1", "VAR2=val2")
-
   val parentRepoOut: RepoOut = RepoOut(
     "datapackage",
     UserOut("fthomas"),
diff --git a/modules/core/src/test/scala/org/scalasteward/core/vcs/bitbucketserver/BitbucketServerApiAlgTest.scala b/modules/core/src/test/scala/org/scalasteward/core/vcs/bitbucketserver/BitbucketServerApiAlgTest.scala
index 2fce2e83..8dd2b7bc 100644
--- a/modules/core/src/test/scala/org/scalasteward/core/vcs/bitbucketserver/BitbucketServerApiAlgTest.scala
+++ b/modules/core/src/test/scala/org/scalasteward/core/vcs/bitbucketserver/BitbucketServerApiAlgTest.scala
@@ -6,6 +6,7 @@ import org.http4s.client.Client
 import org.http4s.dsl.io._
 import org.http4s.implicits._
 import org.http4s.{HttpRoutes, Uri}
+import org.scalasteward.core.application.Config.BitbucketServerCfg
 import org.scalasteward.core.git.Branch
 import org.scalasteward.core.mock.MockContext.config
 import org.scalasteward.core.util.HttpJsonClient
@@ -58,8 +59,9 @@ class BitbucketServerApiAlgTest extends FunSuite {
 
   implicit private val client: Client[IO] = Client.fromHttpApp(routes.orNotFound)
   implicit private val httpJsonClient: HttpJsonClient[IO] = new HttpJsonClient[IO]
+  private val bitbucketServerCfg = BitbucketServerCfg(useDefaultReviewers = false)
   private val bitbucketServerApiAlg: BitbucketServerApiAlg[IO] =
-    new BitbucketServerApiAlg[IO](config.vcsApiHost, _ => IO.pure, useReviewers = false)
+    new BitbucketServerApiAlg[IO](config.vcsApiHost, bitbucketServerCfg, _ => IO.pure)
 
   test("listPullRequests") {
     val pullRequests = bitbucketServerApiAlg
diff --git a/modules/core/src/test/scala/org/scalasteward/core/vcs/gitlab/GitLabApiAlgTest.scala b/modules/core/src/test/scala/org/scalasteward/core/vcs/gitlab/GitLabApiAlgTest.scala
index 459187dc..9b477ed2 100644
--- a/modules/core/src/test/scala/org/scalasteward/core/vcs/gitlab/GitLabApiAlgTest.scala
+++ b/modules/core/src/test/scala/org/scalasteward/core/vcs/gitlab/GitLabApiAlgTest.scala
@@ -14,6 +14,7 @@ import org.http4s.dsl.io._
 import org.http4s.headers.Allow
 import org.http4s.implicits._
 import org.scalasteward.core.TestSyntax._
+import org.scalasteward.core.application.Config.GitLabCfg
 import org.scalasteward.core.data.Update
 import org.scalasteward.core.git.{Branch, Sha1}
 import org.scalasteward.core.mock.MockContext.{config, user}
@@ -80,10 +81,10 @@ class GitLabApiAlgTest extends FunSuite {
   val gitlabApiAlg =
     new GitLabApiAlg[IO](
       config.vcsApiHost,
-      user,
-      _ => IO.pure,
       doNotFork = false,
-      mergeWhenPipelineSucceeds = false
+      GitLabCfg(mergeWhenPipelineSucceeds = false),
+      user,
+      _ => IO.pure
     )
 
   private val data = UpdateData(
@@ -115,10 +116,10 @@ class GitLabApiAlgTest extends FunSuite {
     val gitlabApiAlgNoFork =
       new GitLabApiAlg[IO](
         config.vcsApiHost,
-        user,
-        _ => IO.pure,
         doNotFork = true,
-        mergeWhenPipelineSucceeds = false
+        GitLabCfg(mergeWhenPipelineSucceeds = false),
+        user,
+        _ => IO.pure
       )
     val prOut =
       gitlabApiAlgNoFork
@@ -154,10 +155,10 @@ class GitLabApiAlgTest extends FunSuite {
     val gitlabApiAlgNoFork =
       new GitLabApiAlg[IO](
         config.vcsApiHost,
-        user,
-        _ => IO.pure,
         doNotFork = true,
-        mergeWhenPipelineSucceeds = true
+        GitLabCfg(mergeWhenPipelineSucceeds = true),
+        user,
+        _ => IO.pure
       )
 
     val prOut = gitlabApiAlgNoFork
@@ -189,10 +190,10 @@ class GitLabApiAlgTest extends FunSuite {
     val gitlabApiAlgNoFork =
       new GitLabApiAlg[IO](
         config.vcsApiHost,
-        user,
-        _ => IO.pure,
         doNotFork = true,
-        mergeWhenPipelineSucceeds = true
+        GitLabCfg(mergeWhenPipelineSucceeds = true),
+        user,
+        _ => IO.pure
       )(errorJsonClient, implicitly, implicitly)
 
     val prOut =