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

Issue:
Add new tag for PRs with changes from a Scalafix migration?
A PR with a Scalafix migration does not necessarily contains changes from that migration. Sometimes I'd like to check which of Scala Steward's PRs contains changes from a Scalafix migration but AFAIK there is now easy way to search for these PRs on GitHub. I'm therefore contemplating splitting the current `scalafix-migrations` label into two `scalafix-migrations:changes` and `scalafix-migrations:no-changes` labels to make searching for these easier.

Pull request description:
This adds the `commit-count` label to PR bodies. If the PR contains one
commit, the label will be `commit-count:1`. If it contains more than one
commit, the label will be `commit-count:n:$n` where `$n` is the number of
commits.

This allows to search for PRs with multiple but unspecified number of
commits and for PRs with a specified number of commits.

Closes #2383.

Patch:
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 d2fd33c410..7f6dfea1bf 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
@@ -49,16 +49,13 @@ object NewPullRequestData {
   ): String = {
     val artifacts = artifactsWithOptionalUrl(update, artifactIdToUrl)
     val migrations = edits.collect { case scalafixEdit: ScalafixEdit => scalafixEdit }
-    val (migrationLabel, appliedMigrations) = migrationNote(migrations)
-    val (oldVersionLabel, oldVersionDetails) = oldVersionNote(filesWithOldVersion, update)
-    val details = appliedMigrations.toList ++
-      oldVersionDetails.toList ++
-      List(ignoreFutureUpdates(update))
-    val labels = List(updateType(update)) ++
-      earlySemVerLabel(update).toList ++
-      semVerSpecLabel(update).toList ++
-      migrationLabel.toList ++
-      oldVersionLabel.toList
+    val appliedMigrations = migrationNote(migrations)
+    val oldVersionDetails = oldVersionNote(filesWithOldVersion, update)
+    val details = List(
+      appliedMigrations,
+      oldVersionDetails,
+      ignoreFutureUpdates(update).some
+    ).flatten
 
     s"""|Updates $artifacts ${fromTo(update)}.
         |${releaseNote(releaseRelatedUrls).getOrElse("")}
@@ -67,31 +64,18 @@ object NewPullRequestData {
         |
         |If you'd like to skip this version, you can just close this PR. If you have any feedback, just mention me in the comments below.
         |
-        |Configure Scala Steward for your repository with a [`${RepoConfigAlg.repoConfigBasename}`](https://github.com/scala-steward-org/scala-steward/blob/${org.scalasteward.core.BuildInfo.gitHeadCommit}/docs/repo-specific-configuration.md) file.
+        |Configure Scala Steward for your repository with a [`${RepoConfigAlg.repoConfigBasename}`](${org.scalasteward.core.BuildInfo.gitHubUrl}/blob/${org.scalasteward.core.BuildInfo.gitHeadCommit}/docs/repo-specific-configuration.md) file.
         |
         |Have a fantastic day writing Scala!
         |
         |${details.map(_.toHtml).mkString("\n")}
         |
-        |${labels.mkString("labels: ", ", ", "")}
+        |${labelsFor(update, edits, filesWithOldVersion).mkString("labels: ", ", ", "")}
         |""".stripMargin.trim
   }
 
-  def updateType(update: Update): String = {
-    val dependencies = update.dependencies
-    if (dependencies.forall(_.configurations.contains("test")))
-      "test-library-update"
-    else if (dependencies.forall(_.configurations.contains("scalafix-rule")))
-      "scalafix-rule-update"
-    else if (dependencies.forall(_.sbtVersion.isDefined))
-      "sbt-plugin-update"
-    else
-      "library-update"
-  }
-
   def releaseNote(releaseRelatedUrls: List[ReleaseRelatedUrl]): Option[String] =
-    if (releaseRelatedUrls.isEmpty) None
-    else
+    Option.when(releaseRelatedUrls.nonEmpty) {
       releaseRelatedUrls
         .map {
           case ReleaseRelatedUrl.CustomChangelog(url) =>
@@ -104,7 +88,7 @@ object NewPullRequestData {
             s"[Version Diff](${url.renderString})"
         }
         .mkString(" - ")
-        .some
+    }
 
   def fromTo(update: Update): String =
     s"from ${update.currentVersion} to ${update.nextVersion}"
@@ -131,23 +115,18 @@ object NewPullRequestData {
       case None      => s"$groupId:$artifactId"
     }
 
-  def oldVersionNote(files: List[String], update: Update): (Option[String], Option[Details]) =
-    if (files.isEmpty) (None, None)
-    else
-      (
-        Some("old-version-remains"),
-        Some(
-          Details(
-            "Files still referring to the old version number",
-            s"""The following files still refer to the old version number (${update.currentVersion}).
-               |You might want to review and update them manually.
-               |```
-               |${files.mkString("\n")}
-               |```
-               |""".stripMargin.trim
-          )
-        )
+  def oldVersionNote(files: List[String], update: Update): Option[Details] =
+    Option.when(files.nonEmpty) {
+      Details(
+        "Files still referring to the old version number",
+        s"""The following files still refer to the old version number (${update.currentVersion}).
+           |You might want to review and update them manually.
+           |```
+           |${files.mkString("\n")}
+           |```
+           |""".stripMargin.trim
       )
+    }
 
   def ignoreFutureUpdates(update: Update): Details =
     Details(
@@ -159,9 +138,8 @@ object NewPullRequestData {
           |""".stripMargin.trim
     )
 
-  def migrationNote(scalafixEdits: List[ScalafixEdit]): (Option[String], Option[Details]) =
-    if (scalafixEdits.isEmpty) (None, None)
-    else {
+  def migrationNote(scalafixEdits: List[ScalafixEdit]): Option[Details] =
+    Option.when(scalafixEdits.nonEmpty) {
       val body = scalafixEdits
         .map { scalafixEdit =>
           val migration = scalafixEdit.migration
@@ -173,36 +151,14 @@ object NewPullRequestData {
             case Nel(one, Nil) => one
             case multiple      => multiple.toList.mkString("{", ",", "}")
           }
-          val name = s"${migration.groupId.value}:${artifactName}:${migration.newVersion.value}"
+          val name = s"${migration.groupId.value}:$artifactName:${migration.newVersion.value}"
           val createdChange = scalafixEdit.maybeCommit.fold(" (created no change)")(_ => "")
           s"* $name$createdChange\n$listElements"
         }
         .mkString("\n")
-      (
-        Some("scalafix-migrations"),
-        Some(
-          Details(
-            "Applied Scalafix Migrations",
-            body
-          )
-        )
-      )
+      Details("Applied Scalafix Migrations", body)
     }
 
-  def earlySemVerLabel(update: Update): Option[String] =
-    for {
-      curr <- SemVer.parse(update.currentVersion)
-      next <- SemVer.parse(update.nextVersion)
-      change <- SemVer.getChangeEarly(curr, next)
-    } yield s"early-semver-${change.render}"
-
-  def semVerSpecLabel(update: Update): Option[String] =
-    for {
-      curr <- SemVer.parse(update.currentVersion)
-      next <- SemVer.parse(update.nextVersion)
-      change <- SemVer.getChangeSpec(curr, next)
-    } yield s"semver-spec-${change.render}"
-
   def from(
       data: UpdateData,
       branchName: String,
@@ -218,4 +174,41 @@ object NewPullRequestData {
       head = branchName,
       base = data.baseBranch
     )
+
+  def updateType(update: Update): String = {
+    val dependencies = update.dependencies
+    if (dependencies.forall(_.configurations.contains("test")))
+      "test-library-update"
+    else if (dependencies.forall(_.configurations.contains("scalafix-rule")))
+      "scalafix-rule-update"
+    else if (dependencies.forall(_.sbtVersion.isDefined))
+      "sbt-plugin-update"
+    else
+      "library-update"
+  }
+
+  def labelsFor(
+      update: Update,
+      edits: List[EditAttempt],
+      filesWithOldVersion: List[String]
+  ): List[String] = {
+    val commitCount = edits.flatMap(_.maybeCommit).size
+    val commitCountLabel = "commit-count:" + (commitCount match {
+      case n if n <= 1 => s"$n"
+      case n           => s"n:$n"
+    })
+    val semVerVersions =
+      (SemVer.parse(update.currentVersion), SemVer.parse(update.nextVersion)).tupled
+    val earlySemVerLabel = semVerVersions.flatMap { case (curr, next) =>
+      SemVer.getChangeEarly(curr, next).map(c => s"early-semver-${c.render}")
+    }
+    val semVerSpecLabel = semVerVersions.flatMap { case (curr, next) =>
+      SemVer.getChangeSpec(curr, next).map(c => s"semver-spec-${c.render}")
+    }
+    val scalafixLabel = edits.collectFirst { case _: ScalafixEdit => "scalafix-migrations" }
+    val oldVersionLabel = Option.when(filesWithOldVersion.nonEmpty)("old-version-remains")
+
+    updateType(update) :: commitCountLabel ::
+      List(earlySemVerLabel, semVerSpecLabel, scalafixLabel, oldVersionLabel).flatten
+  }
 }


Test patch:
diff --git a/modules/core/src/test/scala/org/scalasteward/core/vcs/data/NewPullRequestDataTest.scala b/modules/core/src/test/scala/org/scalasteward/core/vcs/data/NewPullRequestDataTest.scala
index 758a3d9826..dba3622bca 100644
--- a/modules/core/src/test/scala/org/scalasteward/core/vcs/data/NewPullRequestDataTest.scala
+++ b/modules/core/src/test/scala/org/scalasteward/core/vcs/data/NewPullRequestDataTest.scala
@@ -6,12 +6,13 @@ import org.http4s.syntax.literals._
 import org.scalasteward.core.TestInstances.dummyRepoCache
 import org.scalasteward.core.TestSyntax._
 import org.scalasteward.core.buildtool.sbt.data.SbtVersion
-import org.scalasteward.core.data._
-import org.scalasteward.core.edit.EditAttempt.ScalafixEdit
+import org.scalasteward.core.data.{ReleaseRelatedUrl, RepoData, UpdateData, Version}
+import org.scalasteward.core.edit.EditAttempt.{ScalafixEdit, UpdateEdit}
 import org.scalasteward.core.edit.scalafix.ScalafixMigration
 import org.scalasteward.core.git.{Branch, Commit, Sha1}
 import org.scalasteward.core.repoconfig.RepoConfig
 import org.scalasteward.core.util.Nel
+import org.scalasteward.core.vcs.data.NewPullRequestData._
 
 class NewPullRequestDataTest extends FunSuite {
   test("asJson") {
@@ -23,14 +24,11 @@ class NewPullRequestDataTest extends FunSuite {
       Sha1(Sha1.HexString.unsafeFrom("d6b6791d2ea11df1d156fe70979ab8c3a5ba3433")),
       Branch("update/logback-classic-1.2.3")
     )
-    val obtained = NewPullRequestData
-      .from(data, "scala-steward:update/logback-classic-1.2.3")
-      .asJson
-      .spaces2
+    val obtained = from(data, "scala-steward:update/logback-classic-1.2.3").asJson.spaces2
     val expected =
       raw"""|{
             |  "title" : "Update logback-classic to 1.2.3",
-            |  "body" : "Updates ch.qos.logback:logback-classic from 1.2.0 to 1.2.3.\n\n\nI'll automatically update this PR to resolve conflicts as long as you don't change it yourself.\n\nIf you'd like to skip this version, you can just close this PR. If you have any feedback, just mention me in the comments below.\n\nConfigure Scala Steward for your repository with a [`.scala-steward.conf`](https://github.com/scala-steward-org/scala-steward/blob/${org.scalasteward.core.BuildInfo.gitHeadCommit}/docs/repo-specific-configuration.md) file.\n\nHave a fantastic day writing Scala!\n\n<details>\n<summary>Ignore future updates</summary>\n\nAdd this to your `.scala-steward.conf` file to ignore future updates of this dependency:\n```\nupdates.ignore = [ { groupId = \"ch.qos.logback\", artifactId = \"logback-classic\" } ]\n```\n</details>\n\nlabels: library-update, early-semver-patch, semver-spec-patch",
+            |  "body" : "Updates ch.qos.logback:logback-classic from 1.2.0 to 1.2.3.\n\n\nI'll automatically update this PR to resolve conflicts as long as you don't change it yourself.\n\nIf you'd like to skip this version, you can just close this PR. If you have any feedback, just mention me in the comments below.\n\nConfigure Scala Steward for your repository with a [`.scala-steward.conf`](https://github.com/scala-steward-org/scala-steward/blob/${org.scalasteward.core.BuildInfo.gitHeadCommit}/docs/repo-specific-configuration.md) file.\n\nHave a fantastic day writing Scala!\n\n<details>\n<summary>Ignore future updates</summary>\n\nAdd this to your `.scala-steward.conf` file to ignore future updates of this dependency:\n```\nupdates.ignore = [ { groupId = \"ch.qos.logback\", artifactId = \"logback-classic\" } ]\n```\n</details>\n\nlabels: library-update, commit-count:0, early-semver-patch, semver-spec-patch",
             |  "head" : "scala-steward:update/logback-classic-1.2.3",
             |  "base" : "master",
             |  "draft" : false
@@ -39,24 +37,22 @@ class NewPullRequestDataTest extends FunSuite {
   }
 
   test("fromTo") {
-    assertEquals(
-      NewPullRequestData.fromTo(("com.example".g % "foo".a % "1.2.0" %> "1.2.3").single),
-      "from 1.2.0 to 1.2.3"
-    )
+    val obtained = fromTo(("com.example".g % "foo".a % "1.2.0" %> "1.2.3").single)
+    assertEquals(obtained, "from 1.2.0 to 1.2.3")
   }
 
   test("links to release notes/changelog") {
-    assertEquals(NewPullRequestData.releaseNote(List.empty), None)
+    assertEquals(releaseNote(List.empty), None)
 
     assertEquals(
-      NewPullRequestData.releaseNote(
+      releaseNote(
         List(ReleaseRelatedUrl.CustomChangelog(uri"https://github.com/foo/foo/CHANGELOG.rst"))
       ),
       Some("[Changelog](https://github.com/foo/foo/CHANGELOG.rst)")
     )
 
     assertEquals(
-      NewPullRequestData.releaseNote(
+      releaseNote(
         List(
           ReleaseRelatedUrl.CustomChangelog(uri"https://github.com/foo/foo/CHANGELOG.rst"),
           ReleaseRelatedUrl.GitHubReleaseNotes(uri"https://github.com/foo/foo/releases/tag/v1.2.3"),
@@ -77,14 +73,14 @@ class NewPullRequestDataTest extends FunSuite {
 
   test("showing artifacts with URL in Markdown format") {
     assertEquals(
-      NewPullRequestData.artifactsWithOptionalUrl(
+      artifactsWithOptionalUrl(
         ("com.example".g % "foo".a % "1.2.0" %> "1.2.3").single,
         Map("foo" -> uri"https://github.com/foo/foo")
       ),
       "[com.example:foo](https://github.com/foo/foo)"
     )
     assertEquals(
-      NewPullRequestData.artifactsWithOptionalUrl(
+      artifactsWithOptionalUrl(
         ("com.example".g % Nel.of("foo".a, "bar".a) % "1.2.0" %> "1.2.3").group,
         Map("foo" -> uri"https://github.com/foo/foo", "bar" -> uri"https://github.com/bar/bar")
       ),
@@ -97,16 +93,14 @@ class NewPullRequestDataTest extends FunSuite {
   }
 
   test("migrationNote: when no migrations") {
-    val (label, appliedMigrations) = NewPullRequestData.migrationNote(List.empty)
-
-    assertEquals(label, None)
+    val appliedMigrations = migrationNote(List.empty)
     assertEquals(appliedMigrations, None)
   }
 
   test("migrationNote: when artifact has migrations") {
     val scalafixEdit = ScalafixEdit(
       ScalafixMigration(
-        GroupId("com.spotify"),
+        "com.spotify".g,
         Nel.one("scio-core"),
         Version("0.7.0"),
         Nel.of("I am a rewrite rule")
@@ -114,9 +108,12 @@ class NewPullRequestDataTest extends FunSuite {
       Right(()),
       Some(Commit())
     )
-    val (label, appliedMigrations) = NewPullRequestData.migrationNote(List(scalafixEdit))
+    val edits = List(scalafixEdit)
+    val appliedMigrations = migrationNote(edits)
+    val update = ("a".g % "b".a % "1" -> "2").single
+    val labels = labelsFor(update, edits, List.empty)
 
-    assertEquals(label, Some("scalafix-migrations"))
+    assert(labels.contains("scalafix-migrations"))
     assertEquals(
       appliedMigrations.fold("")(_.toHtml),
       """<details>
@@ -132,7 +129,7 @@ class NewPullRequestDataTest extends FunSuite {
   test("migrationNote: when artifact has migrations with docs") {
     val scalafixEdit = ScalafixEdit(
       ScalafixMigration(
-        GroupId("com.spotify"),
+        "com.spotify".g,
         Nel.one("scio-core"),
         Version("0.7.0"),
         Nel.of("I am a rewrite rule", "I am a 2nd rewrite rule"),
@@ -141,9 +138,12 @@ class NewPullRequestDataTest extends FunSuite {
       Right(()),
       Some(Commit())
     )
-    val (label, detail) = NewPullRequestData.migrationNote(List(scalafixEdit))
+    val edits = List(scalafixEdit)
+    val detail = migrationNote(edits)
+    val update = ("a".g % "b".a % "1" -> "2").single
+    val labels = labelsFor(update, edits, List.empty)
 
-    assertEquals(label, Some("scalafix-migrations"))
+    assert(labels.contains("scalafix-migrations"))
     assertEquals(
       detail.fold("")(_.toHtml),
       """<details>
@@ -161,7 +161,7 @@ class NewPullRequestDataTest extends FunSuite {
   test("migrationNote: with 2 migrations where one didn't produce a change") {
     val scalafixEdit1 = ScalafixEdit(
       ScalafixMigration(
-        GroupId("com.spotify"),
+        "com.spotify".g,
         Nel.one("scio-core"),
         Version("0.7.0"),
         Nel.of("I am a rewrite rule", "I am a 2nd rewrite rule"),
@@ -170,21 +170,22 @@ class NewPullRequestDataTest extends FunSuite {
       Right(()),
       Some(Commit())
     )
-
     val scalafixEdit2 = ScalafixEdit(
       ScalafixMigration(
-        GroupId("org.typeleve"),
+        "org.typeleve".g,
         Nel.of("cats-effect", "cats-effect-laws"),
         Version("3.0.0"),
-        Nel.of("I am a rule without an effect"),
-        None
+        Nel.of("I am a rule without an effect")
       ),
       Right(()),
       None
     )
-    val (label, detail) = NewPullRequestData.migrationNote(List(scalafixEdit1, scalafixEdit2))
+    val edits = List(scalafixEdit1, scalafixEdit2)
+    val detail = migrationNote(edits)
+    val update = ("a".g % "b".a % "1" -> "2").single
+    val labels = labelsFor(update, edits, List.empty)
 
-    assertEquals(label, Some("scalafix-migrations"))
+    assert(labels.contains("scalafix-migrations"))
     assertEquals(
       detail.fold("")(_.toHtml),
       """<details>
@@ -204,40 +205,34 @@ class NewPullRequestDataTest extends FunSuite {
   test("updateType") {
     val dependency = "com.example".g % "foo".a % "0.1"
     val single = (dependency %> "0.2").single
+    assertEquals(updateType(single), "library-update")
+
     val group = ("com.example".g % Nel.of("foo".a, "bar".a) % "0.1" %> "0.2").group
-    assertEquals(NewPullRequestData.updateType(single), "library-update")
-    assertEquals(NewPullRequestData.updateType(group), "library-update")
+    assertEquals(updateType(group), "library-update")
 
-    assertEquals(
-      NewPullRequestData.updateType((dependency % "test" %> "0.2").single),
-      "test-library-update"
-    )
-    assertEquals(
-      NewPullRequestData.updateType(
-        (dependency.copy(sbtVersion = Some(SbtVersion("1.0"))) %> "0.2").single
-      ),
-      "sbt-plugin-update"
-    )
-    assertEquals(
-      NewPullRequestData.updateType((dependency % "scalafix-rule" %> "0.2").single),
-      "scalafix-rule-update"
-    )
+    val testUpdate = (dependency % "test" %> "0.2").single
+    assertEquals(updateType(testUpdate), "test-library-update")
+
+    val sbtPluginUpdate = (dependency.copy(sbtVersion = Some(SbtVersion("1.0"))) %> "0.2").single
+    assertEquals(updateType(sbtPluginUpdate), "sbt-plugin-update")
+
+    val scalafixRuleUpdate = (dependency % "scalafix-rule" %> "0.2").single
+    assertEquals(updateType(scalafixRuleUpdate), "scalafix-rule-update")
   }
 
   test("oldVersionNote without files") {
     val files = List.empty
     val update = ("com.example".g % "foo".a % "0.1" %> "0.2").single
-
-    assertEquals(NewPullRequestData.oldVersionNote(files, update), (None, None))
+    assertEquals(oldVersionNote(files, update), None)
   }
 
   test("oldVersionNote with files") {
     val files = List("Readme.md", "travis.yml")
     val update = ("com.example".g % "foo".a % "0.1" %> "0.2").single
+    val note = oldVersionNote(files, update)
+    val labels = labelsFor(update, List.empty, files)
 
-    val (label, note) = NewPullRequestData.oldVersionNote(files, update)
-
-    assertEquals(label, Some("old-version-remains"))
+    assert(labels.contains("old-version-remains"))
     assertEquals(
       note.fold("")(_.toHtml),
       """<details>
@@ -253,4 +248,26 @@ class NewPullRequestDataTest extends FunSuite {
       """.stripMargin.trim
     )
   }
+
+  test("commit-count label") {
+    val update = ("a".g % "b".a % "1" -> "2").single
+    val updateEdit = UpdateEdit(update, Commit())
+    val scalafixEdit = ScalafixEdit(
+      ScalafixMigration(
+        "com.spotify".g,
+        Nel.one("scio-core"),
+        Version("0.7.0"),
+        Nel.of("I am a rewrite rule", "I am a 2nd rewrite rule"),
+        Some("https://scalacenter.github.io/scalafix/")
+      ),
+      Right(()),
+      Some(Commit())
+    )
+
+    val oneEdit = labelsFor(update, List(updateEdit), List.empty)
+    assert(clue(oneEdit).contains("commit-count:1"))
+
+    val twoEdits = labelsFor(update, List(updateEdit, scalafixEdit), List.empty)
+    assert(clue(twoEdits).contains("commit-count:n:2"))
+  }
 }