Repository: scala-steward-org/scala-steward (scala)

Issue:
Add formatting options to commit messages
We have a convention to name all our dependency-bump commits as `Bump some-dependency to A.B.C (close #XYZ)` (where `XYZ` can be a PR number).

Scala Steward creates `Update some-dependency to A.B.C` commits.

Wondering if this is something that can be configured via `.scala-steward.conf`.

Pull request description:
Closes https://github.com/fthomas/scala-steward/issues/994

Patch:
diff --git a/docs/repo-specific-configuration.md b/docs/repo-specific-configuration.md
index 0637daf0ca..827149695e 100644
--- a/docs/repo-specific-configuration.md
+++ b/docs/repo-specific-configuration.md
@@ -63,6 +63,11 @@ updates.limit = 5
 # If "never", Scala Steward will never update the PR
 # Default: "on-conflicts"
 updatePullRequests = "always" | "on-conflicts" | "never"
+
+# If set, Scala Steward will use this message template for the commit messages and PR titles.
+# Supported variables: ${artifactName}, ${currentVersion}, ${nextVersion} and ${default}
+# Default: "${default}" which is equivalent to "Update ${artifactName} to ${nextVersion}" 
+commits.message = "Update ${artifactName} from ${currentVersion} to ${nextVersion}"
 ```
 
 The version information given in the patterns above can be in two formats:
diff --git a/modules/core/src/main/scala/org/scalasteward/core/git/package.scala b/modules/core/src/main/scala/org/scalasteward/core/git/package.scala
index e32915c31f..5acdf8526d 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/git/package.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/git/package.scala
@@ -18,13 +18,21 @@ package org.scalasteward.core
 
 import org.scalasteward.core.data.Update
 import org.scalasteward.core.update.show
+import org.scalasteward.core.repoconfig.CommitsConfig
 
 package object git {
   def branchFor(update: Update): Branch =
     Branch(s"update/${update.name}-${update.nextVersion}")
 
-  def commitMsgFor(update: Update): String =
-    s"Update ${show.oneLiner(update)} to ${update.nextVersion}"
+  def commitMsgFor(update: Update, commitConfig: CommitsConfig): String = {
+    val artifact = show.oneLiner(update)
+    val defaultMessage = s"Update $artifact to ${update.nextVersion}"
+    commitConfig.message
+      .replace("${default}", defaultMessage)
+      .replace("${artifactName}", artifact)
+      .replace("${currentVersion}", update.currentVersion)
+      .replace("${nextVersion}", update.nextVersion)
+  }
 
   // man 7 gitrevisions:
   // When you have two commits r1 and r2 you can ask for commits that are
diff --git a/modules/core/src/main/scala/org/scalasteward/core/nurture/NurtureAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/nurture/NurtureAlg.scala
index ed41b50a9f..a9f713a2e5 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/nurture/NurtureAlg.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/nurture/NurtureAlg.scala
@@ -134,7 +134,8 @@ final class NurtureAlg[F[_]](
   def commitAndPush(data: UpdateData): F[ProcessResult] =
     for {
       _ <- logger.info("Commit and push changes")
-      _ <- gitAlg.commitAll(data.repo, git.commitMsgFor(data.update))
+      commitMsgConfig = data.repoConfig.commits
+      _ <- gitAlg.commitAll(data.repo, git.commitMsgFor(data.update, commitMsgConfig))
       _ <- gitAlg.push(data.repo, data.updateBranch)
     } yield Updated
 
diff --git a/modules/core/src/main/scala/org/scalasteward/core/repoconfig/CommitsConfig.scala b/modules/core/src/main/scala/org/scalasteward/core/repoconfig/CommitsConfig.scala
new file mode 100644
index 0000000000..01a218db01
--- /dev/null
+++ b/modules/core/src/main/scala/org/scalasteward/core/repoconfig/CommitsConfig.scala
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2018-2020 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.repoconfig
+
+import io.circe.generic.extras.Configuration
+import io.circe.generic.extras.semiauto._
+import io.circe.{Decoder, Encoder}
+
+final case class CommitsConfig(
+    message: String = "${default}"
+)
+
+object CommitsConfig {
+  implicit val customConfig: Configuration =
+    Configuration.default.withDefaults
+
+  implicit val commitsConfigDecoder: Decoder[CommitsConfig] =
+    deriveConfiguredDecoder
+
+  implicit val commitsConfigEncoder: Encoder[CommitsConfig] =
+    deriveConfiguredEncoder
+}
diff --git a/modules/core/src/main/scala/org/scalasteward/core/repoconfig/RepoConfig.scala b/modules/core/src/main/scala/org/scalasteward/core/repoconfig/RepoConfig.scala
index 5cf24439d8..d1f99c375c 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/repoconfig/RepoConfig.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/repoconfig/RepoConfig.scala
@@ -24,7 +24,8 @@ import PullRequestUpdateStrategy.{prUpdateStrategyDecoder, prUpdateStrategyEncod
 final case class RepoConfig(
     pullRequests: PullRequestsConfig = PullRequestsConfig(),
     updates: UpdatesConfig = UpdatesConfig(),
-    updatePullRequests: PullRequestUpdateStrategy = PullRequestUpdateStrategy.default
+    updatePullRequests: PullRequestUpdateStrategy = PullRequestUpdateStrategy.default,
+    commits: CommitsConfig = CommitsConfig()
 )
 
 object RepoConfig {
diff --git a/modules/core/src/main/scala/org/scalasteward/core/vcs/data/NewPullRequestData.scala b/modules/core/src/main/scala/org/scalasteward/core/vcs/data/NewPullRequestData.scala
index 2332977379..16ff70ca24 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/vcs/data/NewPullRequestData.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/vcs/data/NewPullRequestData.scala
@@ -156,7 +156,7 @@ object NewPullRequestData {
       migrations: List[Migration] = List.empty
   ): NewPullRequestData =
     NewPullRequestData(
-      title = git.commitMsgFor(data.update),
+      title = git.commitMsgFor(data.update, data.repoConfig.commits),
       body = bodyFor(
         data.update,
         artifactIdToUrl,


Test patch:
diff --git a/modules/core/src/test/scala/org/scalasteward/core/git/gitTest.scala b/modules/core/src/test/scala/org/scalasteward/core/git/gitTest.scala
new file mode 100644
index 0000000000..3f22df4ee9
--- /dev/null
+++ b/modules/core/src/test/scala/org/scalasteward/core/git/gitTest.scala
@@ -0,0 +1,42 @@
+package org.scalasteward.core.git
+
+import org.scalatest.funsuite.AnyFunSuite
+import org.scalatest.matchers.should.Matchers
+import org.scalasteward.core.TestSyntax._
+import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks
+import org.scalacheck.{Arbitrary, Gen}
+import org.scalasteward.core.data.Update.Single
+import org.scalasteward.core.data.Update
+import org.scalasteward.core.util.Nel
+import org.scalasteward.core.update.show
+import org.scalasteward.core.repoconfig.CommitsConfig
+
+class gitTest extends AnyFunSuite with Matchers with ScalaCheckPropertyChecks {
+  implicit val updateArbitrary: Arbitrary[Update] = Arbitrary(for {
+    groupId <- Gen.alphaStr
+    artifactId <- Gen.alphaStr
+    currentVersion <- Gen.alphaStr
+    newerVersion <- Gen.alphaStr
+  } yield Single(groupId % artifactId % currentVersion, Nel.one(newerVersion)))
+
+  test("commitMsgFor should work with static message") {
+    val commitsConfig = CommitsConfig("Static message")
+    forAll { update: Update => commitMsgFor(update, commitsConfig) shouldBe "Static message" }
+  }
+
+  test("commitMsgFor should work with default message") {
+    val commitsConfig = CommitsConfig("${default}")
+    forAll { update: Update =>
+      commitMsgFor(update, commitsConfig) shouldBe s"Update ${show.oneLiner(update)} to ${update.nextVersion}"
+    }
+  }
+
+  test("commitMsgFor should work with templated message") {
+    val commitsConfig =
+      CommitsConfig("Update ${artifactName} from ${currentVersion} to ${nextVersion}")
+    forAll { update: Update =>
+      commitMsgFor(update, commitsConfig) shouldBe s"Update ${show.oneLiner(update)} from ${update.currentVersion} to ${update.nextVersion}"
+    }
+  }
+
+}
diff --git a/modules/core/src/test/scala/org/scalasteward/core/repoconfig/RepoConfigAlgTest.scala b/modules/core/src/test/scala/org/scalasteward/core/repoconfig/RepoConfigAlgTest.scala
index 16cd3630ed..671c0201b3 100644
--- a/modules/core/src/test/scala/org/scalasteward/core/repoconfig/RepoConfigAlgTest.scala
+++ b/modules/core/src/test/scala/org/scalasteward/core/repoconfig/RepoConfigAlgTest.scala
@@ -25,6 +25,7 @@ class RepoConfigAlgTest extends AnyFunSuite with Matchers {
          |updates.ignore = [ { groupId = "org.acme", version = "1.0" } ]
          |updates.limit = 4
          |pullRequests.frequency = "@weekly"
+         |commits.message = "Update ${artifactName} from ${currentVersion} to ${nextVersion}"
          |""".stripMargin
     val initialState = MockState.empty.add(configFile, content)
     val config = repoConfigAlg.readRepoConfigOrDefault(repo).runA(initialState).unsafeRunSync()
@@ -59,6 +60,9 @@ class RepoConfigAlgTest extends AnyFunSuite with Matchers {
           UpdatePattern(GroupId("org.acme"), None, Some(UpdatePattern.Version(Some("1.0"), None)))
         ),
         limit = Some(4)
+      ),
+      commits = CommitsConfig(
+        message = "Update ${artifactName} from ${currentVersion} to ${nextVersion}"
       )
     )
   }