Skip to content

Commit 79a97e5

Browse files
authored
Apply updates that differ only in their artifactId at the same time (#32)
Split Update into two cases to support update groups
1 parent f7099cf commit 79a97e5

File tree

7 files changed

+143
-48
lines changed

7 files changed

+143
-48
lines changed

build.sbt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ lazy val core = myCrossProject("core")
2929
Dependencies.catsEffect,
3030
Dependencies.circeParser,
3131
Dependencies.fs2Core,
32+
Dependencies.refined,
3233
Dependencies.scalaTest % Test
3334
),
3435
assembly / test := {}

modules/core/src/main/scala/eu/timepit/scalasteward/model/Update.scala

Lines changed: 69 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -18,57 +18,91 @@ package eu.timepit.scalasteward.model
1818

1919
import cats.data.NonEmptyList
2020
import cats.implicits._
21+
import eu.timepit.refined.types.string.NonEmptyString
22+
import eu.timepit.scalasteward.model.Update.{Group, Single}
23+
import eu.timepit.scalasteward.util
2124

2225
import scala.util.matching.Regex
2326

24-
final case class Update(
25-
groupId: String,
26-
artifactId: String,
27-
currentVersion: String,
28-
newerVersions: NonEmptyList[String]
29-
) {
30-
31-
/** Returns true if the changes of applying `other` would include the changes
32-
* of applying `this`.
33-
*/
34-
def isImpliedBy(other: Update): Boolean =
35-
groupId === other.groupId &&
36-
artifactId =!= other.artifactId &&
37-
artifactId.startsWith(Update.removeIgnorableSuffix(other.artifactId)) &&
38-
currentVersion === other.currentVersion &&
39-
newerVersions === other.newerVersions
27+
sealed trait Update extends Product with Serializable {
28+
def groupId: String
29+
def artifactId: String
30+
def currentVersion: String
31+
def newerVersions: NonEmptyList[String]
4032

4133
def name: String =
42-
artifactId match {
43-
case "core" => groupId.split('.').lastOption.getOrElse(groupId)
44-
case _ => artifactId
45-
}
34+
if (Update.commonSuffixes.contains(artifactId))
35+
groupId.split('.').lastOption.getOrElse(groupId)
36+
else
37+
artifactId
4638

4739
def nextVersion: String =
4840
newerVersions.head
4941

50-
def replaceAllIn(str: String): Option[String] = {
51-
def normalize(searchTerm: String): String =
42+
def replaceAllIn(target: String): Option[String] = {
43+
val quotedSearchTerms = searchTerms.map { term =>
5244
Regex
53-
.quoteReplacement(Update.removeIgnorableSuffix(searchTerm))
45+
.quoteReplacement(Update.removeCommonSuffix(term))
5446
.replace("-", ".?")
55-
56-
val regex =
57-
s"(?i)(${normalize(name)}.*?)${Regex.quote(currentVersion)}".r
47+
}
48+
val searchTerm = quotedSearchTerms.mkString_("(", "|", ")")
49+
val regex = s"(?i)($searchTerm.*?)${Regex.quote(currentVersion)}".r
5850
var updated = false
59-
val result = regex.replaceAllIn(str, m => {
51+
val result = regex.replaceAllIn(target, m => {
6052
updated = true
6153
m.group(1) + nextVersion
6254
})
6355
if (updated) Some(result) else None
6456
}
6557

66-
def show: String =
67-
s"$groupId:$artifactId : ${(currentVersion :: newerVersions).mkString_("", " -> ", "")}"
58+
def searchTerms: NonEmptyList[String] =
59+
this match {
60+
case s: Single => NonEmptyList.one(s.artifactId)
61+
case g: Group => g.artifactIds.concat(g.artifactIdsPrefix.map(_.value).toList)
62+
}
63+
64+
def show: String = {
65+
val artifacts = this match {
66+
case s: Single => s.artifactId
67+
case g: Group => g.artifactIds.mkString_("{", ", ", "}")
68+
}
69+
val versions = (currentVersion :: newerVersions).mkString_("", " -> ", "")
70+
s"$groupId:$artifacts : $versions"
71+
}
6872
}
6973

7074
object Update {
71-
def fromString(str: String): Either[Throwable, Update] =
75+
final case class Single(
76+
groupId: String,
77+
artifactId: String,
78+
currentVersion: String,
79+
newerVersions: NonEmptyList[String]
80+
) extends Update
81+
82+
final case class Group(
83+
groupId: String,
84+
artifactIds: NonEmptyList[String],
85+
currentVersion: String,
86+
newerVersions: NonEmptyList[String]
87+
) extends Update {
88+
override def artifactId: String =
89+
artifactIds.head
90+
91+
def artifactIdsPrefix: Option[NonEmptyString] =
92+
util.longestCommonNonEmptyPrefix(artifactIds)
93+
}
94+
95+
///
96+
97+
def apply(
98+
groupId: String,
99+
artifactId: String,
100+
currentVersion: String,
101+
newerVersions: NonEmptyList[String]
102+
): Single =
103+
Single(groupId, artifactId, currentVersion, newerVersions)
104+
105+
def fromString(str: String): Either[Throwable, Single] =
72106
Either.catchNonFatal {
73107
val regex = """([^\s:]+):([^\s:]+)[^\s]*\s+:\s+([^\s]+)\s+->(.+)""".r
74108
str match {
@@ -78,8 +112,9 @@ object Update {
78112
}
79113
}
80114

81-
def removeIgnorableSuffix(str: String): String =
82-
List("-core", "-server")
83-
.find(suffix => str.endsWith(suffix))
84-
.fold(str)(suffix => str.substring(0, str.length - suffix.length))
115+
val commonSuffixes: List[String] =
116+
List("core", "server")
117+
118+
def removeCommonSuffix(str: String): String =
119+
util.removeSuffix(str, commonSuffixes)
85120
}

modules/core/src/main/scala/eu/timepit/scalasteward/sbt.scala

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package eu.timepit.scalasteward
1919
import better.files.File
2020
import cats.effect.IO
2121
import eu.timepit.scalasteward.model.Update
22+
import cats.implicits._
2223

2324
object sbt {
2425
def addGlobalPlugins(home: File): IO[Unit] =
@@ -38,17 +39,29 @@ object sbt {
3839
io.firejail(sbtCmd :+ ";dependencyUpdates ;reload plugins; dependencyUpdates", dir)
3940
.map(lines => sanitizeUpdates(toUpdates(lines)))
4041

41-
def sanitizeUpdates(updates: List[Update]): List[Update] = {
42-
val distinctUpdates = updates.distinct
43-
distinctUpdates
44-
.filterNot(update => distinctUpdates.exists(other => update.isImpliedBy(other)))
42+
def sanitizeUpdates(updates: List[Update.Single]): List[Update] =
43+
updates.distinct
44+
.groupByNel(update => (update.groupId, update.currentVersion, update.newerVersions))
45+
.values
46+
.map { nel =>
47+
val head = nel.head
48+
if (nel.tail.nonEmpty)
49+
Update.Group(
50+
head.groupId,
51+
nel.map(_.artifactId).sorted,
52+
head.currentVersion,
53+
head.newerVersions
54+
)
55+
else
56+
head
57+
}
58+
.toList
4559
.sortBy(update => (update.groupId, update.artifactId))
46-
}
4760

4861
val sbtCmd: List[String] =
4962
List("sbt", "-no-colors")
5063

51-
def toUpdates(lines: List[String]): List[Update] =
64+
def toUpdates(lines: List[String]): List[Update.Single] =
5265
lines.flatMap { line =>
5366
val trimmed = line.replace("[info]", "").trim
5467
Update.fromString(trimmed).toSeq

modules/core/src/main/scala/eu/timepit/scalasteward/util.scala

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,29 @@
1717
package eu.timepit.scalasteward
1818

1919
import cats.Monad
20+
import cats.data.NonEmptyList
2021
import cats.implicits._
22+
import eu.timepit.refined.types.string.NonEmptyString
2123

2224
object util {
2325
def ifTrue[F[_]: Monad](fb: F[Boolean])(f: F[Unit]): F[Unit] =
2426
fb.ifM(f, Monad[F].unit)
27+
28+
def longestCommonPrefix(s1: String, s2: String): String = {
29+
var i = 0
30+
val min = math.min(s1.length, s2.length)
31+
while (i < min && s1(i) == s2(i)) i = i + 1
32+
s1.substring(0, i)
33+
}
34+
35+
def longestCommonPrefix(xs: NonEmptyList[String]): String =
36+
xs.reduceLeft(longestCommonPrefix)
37+
38+
def longestCommonNonEmptyPrefix(xs: NonEmptyList[String]): Option[NonEmptyString] =
39+
NonEmptyString.unapply(longestCommonPrefix(xs))
40+
41+
def removeSuffix(target: String, suffixes: List[String]): String =
42+
suffixes
43+
.find(suffix => target.endsWith(suffix))
44+
.fold(target)(suffix => target.substring(0, target.length - suffix.length))
2545
}

modules/core/src/test/scala/eu/timepit/scalasteward/model/UpdateTest.scala

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -106,12 +106,30 @@ class UpdateTest extends FunSuite with Matchers {
106106
.replaceAllIn(original) shouldBe Some(expected)
107107
}
108108

109-
test("isImpliedBy") {
110-
val update0 = Update("org.specs2", "specs2-core", "3.9.4", Nel.of("3.9.5"))
111-
val update1 = update0.copy(artifactId = "specs2-scalacheck")
112-
update0.isImpliedBy(update0) shouldBe false
113-
update0.isImpliedBy(update1) shouldBe false
114-
update1.isImpliedBy(update0) shouldBe true
115-
update1.isImpliedBy(update1) shouldBe false
109+
test("replaceAllIn: group with prefix val") {
110+
val original = """ val circe = "0.10.0-M1" """
111+
val expected = """ val circe = "0.10.0-M2" """
112+
Update
113+
.Group(
114+
"io.circe",
115+
Nel.of("circe-generic", "circe-literal", "circe-parser", "circe-testing"),
116+
"0.10.0-M1",
117+
Nel.of("0.10.0-M2")
118+
)
119+
.replaceAllIn(original) shouldBe Some(expected)
120+
}
121+
122+
test("replaceAllIn: group with repeated version") {
123+
val original =
124+
""" "com.pepegar" %% "hammock-core" % "0.8.1",
125+
| "com.pepegar" %% "hammock-circe" % "0.8.1"
126+
""".stripMargin.trim
127+
val expected =
128+
""" "com.pepegar" %% "hammock-core" % "0.8.5",
129+
| "com.pepegar" %% "hammock-circe" % "0.8.5"
130+
""".stripMargin.trim
131+
Update
132+
.Group("com.pepegar", Nel.of("hammock-core", "hammock-circe"), "0.8.1", Nel.of("0.8.5"))
133+
.replaceAllIn(original) shouldBe Some(expected)
116134
}
117135
}

modules/core/src/test/scala/eu/timepit/scalasteward/sbtTest.scala

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,14 @@ class sbtTest extends FunSuite with Matchers {
88
test("sanitizeUpdates") {
99
val update0 = Update("org.specs2", "specs2-core", "3.9.4", Nel.of("3.9.5"))
1010
val update1 = update0.copy(artifactId = "specs2-scalacheck")
11-
sbt.sanitizeUpdates(List(update0, update1)) shouldBe List(update0)
11+
sbt.sanitizeUpdates(List(update0, update1)) shouldBe List(
12+
Update.Group(
13+
"org.specs2",
14+
Nel.of("specs2-core", "specs2-scalacheck"),
15+
"3.9.4",
16+
Nel.of("3.9.5")
17+
)
18+
)
1219
}
1320

1421
test("toUpdates") {

project/Dependencies.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ object Dependencies {
55
val catsEffect = "org.typelevel" %% "cats-effect" % "1.0.0"
66
val circeParser = "io.circe" %% "circe-parser" % "0.10.0-M2"
77
val fs2Core = "co.fs2" %% "fs2-core" % "1.0.0-M5"
8+
val refined = "eu.timepit" %% "refined" % "0.9.2"
89
val scalaTest = "org.scalatest" %% "scalatest" % "3.0.5"
910
}

0 commit comments

Comments
 (0)