Skip to content

Commit df28ac4

Browse files
committed
Refactor: Separate modelling of candidate newerVersions from Updates
_Addresses https://github.com/scala-steward-org/scala-steward/issues/3781_ The Scala Steward codebase deals with the concept of 'updates' in two areas: * **Multiple `newerVersions`**: Initially, evaluating _candidate_ updates (in `UpdateAlg` & `FilterAlg`), where for any given artifact, there can be many possible versions we might want to upgrade to. The list of versions is reduced down with filtering (based on config & other rules), and eventually a single next version is selected. * **Single `nextVersion`**: Subsequently, retrieving information on the updates represented by the pull requests Scala Steward created - each artifact being updated to a specific _single_ new version. ...Scala Steward used the `Update` type for _both_ areas (specifically, `Update.ForArtifactId` for the first phase, _requiring_ it to support multiple `newerVersions`). Looking at a single `Update.ForArtifactId` in code, it's not always clear to the newcomer if the update contains many newer versions or not - ie where we are in the pipeline. ## Changes * `Update.ForArtifactId` → `ArtifactUpdateCandidates`🆕 within `UpdateAlg` & `FilterAlg` where candidate versions are being evaluated * `Update.ForArtifactId` & `ArtifactUpdateCandidates` both extend `ArtifactUpdateVersions`🆕 to allow `UpdatePattern.findMatch()` to filter lists of either of them: * Filtering the candidate versions of `ArtifactUpdateCandidates` in `UpdatesConfig.isAllowed`, etc * Filtering the `Update`s of existing PRs in `PruningAlg.newPullRequestsAllowed()` & `RetractedArtifact.isRetracted()` * The fields from `Update.ForArtifactId` also required by `ArtifactUpdateCandidates` have been placed in the `ArtifactForUpdate`🆕 case class * Restrict `Update.Single` types to just `nextVersion`, not `newerVersions` Remaining unchanged: * `PullRequestRepository` still persists instances of `Update` * The JSON serialisation format of `Update` remains unchanged for now (the encoder & decoders have been adapted so that they still output the same format, despite the case classes having changed), so there's currently no need to introduce a new fallback 'legacy' decoder (several of which were recently deleted with #3785). This also means the JSON metadata added to PR descriptions (originally introduced in #3466) also remains the same for now, though I think it will be reasonable to adjust the format for `Update.ForArtifactId` to something more logical in future.
1 parent d4e87c7 commit df28ac4

File tree

17 files changed

+284
-164
lines changed

17 files changed

+284
-164
lines changed

modules/benchmark/src/main/scala/org/scalasteward/benchmark/UpdatesConfigBenchmark.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ class UpdatesConfigBenchmark {
3333
val newerVersions = Nel
3434
.of("2.0.0", "2.1.0", "2.1.1", "2.2.0", "3.0.0", "3.1.0", "3.2.1", "3.3.3", "4.0", "5.0")
3535
.map(Version.apply)
36-
val update = Update.ForArtifactId(dependency, newerVersions)
36+
val update = ArtifactUpdateCandidates(ArtifactForUpdate(dependency), newerVersions)
3737

3838
UpdatesConfig().keep(update)
3939
UpdatesConfig(allow = Some(List(UpdatePattern(groupId, None, None)))).keep(update)

modules/core/src/main/scala/org/scalasteward/core/data/Update.scala

Lines changed: 117 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,43 @@ import org.scalasteward.core.repoconfig.PullRequestGroup
2323
import org.scalasteward.core.util
2424
import org.scalasteward.core.util.Nel
2525

26+
case class ArtifactForUpdate(
27+
crossDependency: CrossDependency,
28+
newerGroupId: Option[GroupId] = None,
29+
newerArtifactId: Option[String] = None
30+
) {
31+
private val headDependency: Dependency = crossDependency.head
32+
def groupId: GroupId = headDependency.groupId
33+
def artifactId: ArtifactId = headDependency.artifactId
34+
def currentVersion: Version = headDependency.version
35+
}
36+
37+
trait ArtifactUpdateVersions {
38+
val artifactForUpdate: ArtifactForUpdate
39+
40+
val refersToUpdateVersions: Nel[Version]
41+
42+
def show: String
43+
}
44+
45+
/** Captures possible ''candidate'' newer versions that we may update an artifact to.
46+
*
47+
* Compare with the other subclass of [[ArtifactUpdateVersions]], [[Update.ForArtifactId]], which
48+
* denotes the specific ''single'' next version ultimately used in a PR.
49+
*/
50+
case class ArtifactUpdateCandidates(
51+
artifactForUpdate: ArtifactForUpdate,
52+
newerVersions: Nel[Version]
53+
) extends ArtifactUpdateVersions {
54+
override val refersToUpdateVersions: Nel[Version] = newerVersions
55+
56+
def asSpecificUpdate(nextVersion: Version): Update.ForArtifactId =
57+
Update.ForArtifactId(artifactForUpdate, nextVersion)
58+
59+
override def show: String =
60+
s"${artifactForUpdate.groupId}:${artifactForUpdate.crossDependency.showArtifactNames} : ${Version.show((artifactForUpdate.currentVersion +: refersToUpdateVersions.toList)*)}"
61+
}
62+
2663
sealed trait Update {
2764

2865
def on[A](update: Update.Single => A, grouped: Update.Grouped => A): A = this match {
@@ -37,6 +74,11 @@ sealed trait Update {
3774

3875
object Update {
3976

77+
/** Denotes the update of one or more artifacts, which have all matched the same
78+
* `pullRequests.grouping` config rule.
79+
*
80+
* An `Update.Grouped` PR looks like this: [[https://github.com/guardian/etag-caching/pull/62]]
81+
*/
4082
final case class Grouped(
4183
name: String,
4284
title: Option[String],
@@ -49,47 +91,54 @@ object Update {
4991

5092
sealed trait Single extends Product with Serializable with Update {
5193
override val asSingleUpdates: List[Update.Single] = List(this)
94+
def artifactsForUpdate: Nel[ArtifactForUpdate]
5295
def forArtifactIds: Nel[ForArtifactId]
5396
def crossDependencies: Nel[CrossDependency]
5497
def dependencies: Nel[Dependency]
5598
def groupId: GroupId
5699
def artifactIds: Nel[ArtifactId]
57100
def mainArtifactId: String
101+
def showArtifacts: String
58102
def groupAndMainArtifactId: (GroupId, String) = (groupId, mainArtifactId)
59103
def currentVersion: Version
60-
def newerVersions: Nel[Version]
104+
def nextVersion: Version
61105

62106
final def name: String = Update.nameOf(groupId, mainArtifactId)
63107

64-
final def nextVersion: Version = newerVersions.head
65-
66-
final override def show: String = {
67-
val artifacts = this match {
68-
case s: ForArtifactId => s.crossDependency.showArtifactNames
69-
case g: ForGroupId => g.crossDependencies.map(_.showArtifactNames).mkString_("{", ", ", "}")
70-
}
71-
val versions = {
72-
val vs0 = (currentVersion :: newerVersions).toList
73-
val vs1 = if (vs0.size > 6) vs0.take(3) ++ ("..." :: vs0.takeRight(3)) else vs0
74-
vs1.mkString("", " -> ", "")
75-
}
76-
s"$groupId:$artifacts : $versions"
77-
}
108+
final override def show: String =
109+
s"$groupId:$showArtifacts : ${Version.show(currentVersion, nextVersion)}"
78110

79-
def withNewerVersions(versions: Nel[Version]): Update.Single = this match {
80-
case s @ ForArtifactId(_, _, _, _) =>
81-
s.copy(newerVersions = versions)
82-
case ForGroupId(forArtifactIds) =>
83-
ForGroupId(forArtifactIds.map(_.copy(newerVersions = versions)))
111+
def withNextVersion(nextVersion: Version): Update.Single = this match {
112+
case s: ForArtifactId =>
113+
s.copy(nextVersion = nextVersion)
114+
case g: ForGroupId =>
115+
g.copy(nextVersion = nextVersion)
84116
}
85117
}
86118

119+
/** Denotes the update of a specific single artifact to some particular chosen next version.
120+
*
121+
* An update PR with a single `Update.ForArtifactId` looks like this:
122+
* [[https://github.com/guardian/etag-caching/pull/125]]
123+
*
124+
* In the other subclass of [[ArtifactUpdateVersions]], [[ArtifactUpdateCandidates]], _multiple_
125+
* possible candidate newer versions are stored.
126+
*/
87127
final case class ForArtifactId(
88-
crossDependency: CrossDependency,
89-
newerVersions: Nel[Version],
90-
newerGroupId: Option[GroupId] = None,
91-
newerArtifactId: Option[String] = None
92-
) extends Single {
128+
artifactForUpdate: ArtifactForUpdate,
129+
nextVersion: Version
130+
) extends Single
131+
with ArtifactUpdateVersions {
132+
val crossDependency: CrossDependency = artifactForUpdate.crossDependency
133+
134+
override val refersToUpdateVersions: Nel[Version] = Nel.one(nextVersion)
135+
136+
override def artifactsForUpdate: Nel[ArtifactForUpdate] = Nel.one(artifactForUpdate)
137+
138+
override def showArtifacts: String = crossDependency.showArtifactNames
139+
140+
lazy val versionUpdate: Version.Update = Version.Update(currentVersion, nextVersion)
141+
93142
override def forArtifactIds: Nel[ForArtifactId] =
94143
Nel.one(this)
95144

@@ -100,7 +149,7 @@ object Update {
100149
crossDependency.dependencies
101150

102151
override def groupId: GroupId =
103-
crossDependency.head.groupId
152+
artifactForUpdate.groupId
104153

105154
override def artifactIds: Nel[ArtifactId] =
106155
dependencies.map(_.artifactId)
@@ -109,17 +158,28 @@ object Update {
109158
artifactId.name
110159

111160
override def currentVersion: Version =
112-
crossDependency.head.version
161+
artifactForUpdate.currentVersion
113162

114163
def artifactId: ArtifactId =
115-
crossDependency.head.artifactId
164+
artifactForUpdate.artifactId
116165
}
117166

167+
/** Denotes the update of several artifacts which all have the same Maven group-id, and also are
168+
* all are being updated ''from'' the same version, and updated ''to'' the same `nextVersion`.
169+
*
170+
* An `Update.ForGroupId` PR looks like this:
171+
* [[https://github.com/guardian/etag-caching/pull/128]]
172+
*/
118173
final case class ForGroupId(
119-
forArtifactIds: Nel[ForArtifactId]
174+
artifactsForUpdate: Nel[ArtifactForUpdate],
175+
nextVersion: Version
120176
) extends Single {
177+
178+
override def forArtifactIds: Nel[ForArtifactId] =
179+
artifactsForUpdate.map(ForArtifactId(_, nextVersion))
180+
121181
override def crossDependencies: Nel[CrossDependency] =
122-
forArtifactIds.map(_.crossDependency)
182+
artifactsForUpdate.map(_.crossDependency)
123183

124184
override def dependencies: Nel[Dependency] =
125185
crossDependencies.flatMap(_.dependencies)
@@ -142,12 +202,12 @@ object Update {
142202
.getOrElse(artifactIds.head.name)
143203
}
144204

205+
override def showArtifacts: String =
206+
crossDependencies.map(_.showArtifactNames).mkString_("{", ", ", "}")
207+
145208
override def currentVersion: Version =
146209
dependencies.head.version
147210

148-
override def newerVersions: Nel[Version] =
149-
forArtifactIds.head.newerVersions
150-
151211
def artifactIdsPrefix: Option[String] =
152212
util.string.longestCommonPrefixGteq(artifactIds.map(_.name), 3)
153213
}
@@ -163,19 +223,23 @@ object Update {
163223

164224
def groupByArtifactIdName(updates: List[ForArtifactId]): List[ForArtifactId] = {
165225
val groups0 =
166-
updates.groupByNel(s => (s.groupId, s.artifactId.name, s.currentVersion, s.newerVersions))
226+
updates.groupByNel(s => (s.groupId, s.artifactId.name, s.currentVersion, s.nextVersion))
167227
val groups1 = groups0.values.map { group =>
168-
val dependencies = group.flatMap(_.crossDependency.dependencies).distinct.sorted
169-
group.head.copy(crossDependency = CrossDependency(dependencies))
228+
val dependencies = group.flatMap(_.dependencies).distinct.sorted
229+
val update: Update.ForArtifactId = group.head
230+
update.copy(artifactForUpdate =
231+
update.artifactForUpdate.copy(crossDependency = CrossDependency(dependencies))
232+
)
170233
}
171234
groups1.toList.distinct.sortBy(u => u: Update.Single)
172235
}
173236

174237
def groupByGroupId(updates: List[ForArtifactId]): List[Single] = {
175238
val groups0 =
176-
updates.groupByNel(s => (s.groupId, s.currentVersion, s.newerVersions))
177-
val groups1 = groups0.values.map { group =>
178-
if (group.tail.isEmpty) group.head else ForGroupId(group)
239+
updates.groupByNel(s => (s.groupId, s.versionUpdate))
240+
val groups1 = groups0.map { case ((_, versionUpdate), group) =>
241+
if (group.tail.isEmpty) group.head
242+
else ForGroupId(group.map(_.artifactForUpdate), versionUpdate.nextVersion)
179243
}
180244
groups1.toList.distinct.sorted
181245
}
@@ -197,7 +261,7 @@ object Update {
197261
}
198262

199263
implicit val SingleOrder: Order[Single] =
200-
Order.by((u: Single) => (u.crossDependencies, u.newerVersions))
264+
Order.by((u: Single) => (u.crossDependencies, u.nextVersion))
201265

202266
// Encoder and Decoder instances
203267

@@ -206,19 +270,28 @@ object Update {
206270
implicit private val forArtifactIdEncoder: Encoder[ForArtifactId] =
207271
Encoder.forProduct1("ForArtifactId")(identity[ForArtifactId]) {
208272
Encoder.forProduct4("crossDependency", "newerVersions", "newerGroupId", "newerArtifactId") {
209-
s => (s.crossDependency, s.newerVersions, s.newerGroupId, s.newerArtifactId)
273+
s =>
274+
(
275+
s.crossDependency,
276+
Seq(s.nextVersion),
277+
s.artifactForUpdate.newerGroupId,
278+
s.artifactForUpdate.newerArtifactId
279+
)
210280
}
211281
}
212282

213283
private val unwrappedForArtifactIdDecoder: Decoder[ForArtifactId] =
214284
Decoder.forProduct4("crossDependency", "newerVersions", "newerGroupId", "newerArtifactId") {
215285
(
216286
crossDependency: CrossDependency,
217-
newerVersions: Nel[Version],
287+
newerVersions: List[Version],
218288
newerGroupId: Option[GroupId],
219289
newerArtifactId: Option[String]
220290
) =>
221-
ForArtifactId(crossDependency, newerVersions, newerGroupId, newerArtifactId)
291+
ForArtifactId(
292+
ArtifactForUpdate(crossDependency, newerGroupId, newerArtifactId),
293+
newerVersions.head
294+
)
222295
}
223296

224297
private val forArtifactIdDecoderV2 =
@@ -236,7 +309,7 @@ object Update {
236309

237310
private val unwrappedForGroupIdDecoderV3: Decoder[ForGroupId] =
238311
Decoder.forProduct1("forArtifactIds") { (forArtifactIds: Nel[ForArtifactId]) =>
239-
ForGroupId(forArtifactIds)
312+
ForGroupId(forArtifactIds.map(_.artifactForUpdate), forArtifactIds.head.nextVersion)
240313
}
241314

242315
private val forGroupIdDecoderV3: Decoder[ForGroupId] =

modules/core/src/main/scala/org/scalasteward/core/data/Version.scala

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,12 @@ final case class Version(value: String) {
124124
object Version {
125125
case class Update(currentVersion: Version, nextVersion: Version)
126126

127+
def show(versions: Version*): String = {
128+
val vs0 = versions.map(_.value)
129+
val vs1: Seq[String] = if (vs0.size > 6) (vs0.take(3) :+ "...") ++ vs0.takeRight(3) else vs0
130+
vs1.mkString(" -> ")
131+
}
132+
127133
val tagNames: List[Version => String] = List("v" + _, _.value, "release-" + _)
128134

129135
implicit val versionDecoder: Decoder[Version] =
@@ -138,6 +144,9 @@ object Version {
138144
c1.compare(c2)
139145
}
140146

147+
implicit val versionUpdateOrder: Order[Version.Update] =
148+
Order.by((vu: Version.Update) => (vu.currentVersion, vu.nextVersion))
149+
141150
private def padToSameLength[A](l1: List[A], l2: List[A], elem: A): (List[A], List[A]) = {
142151
val maxLength = math.max(l1.length, l2.length)
143152
(l1.padTo(maxLength, elem), l2.padTo(maxLength, elem))

modules/core/src/main/scala/org/scalasteward/core/edit/update/Selector.scala

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -204,21 +204,19 @@ object Selector {
204204
update: Update.Single,
205205
modulePositions: List[ModulePosition]
206206
): List[Substring.Replacement] =
207-
update.forArtifactIds.toList.flatMap { forArtifactId =>
208-
val newerGroupId = forArtifactId.newerGroupId
209-
val newerArtifactId = forArtifactId.newerArtifactId
207+
update.artifactsForUpdate.toList.flatMap { artifactForUpdate =>
208+
val newerGroupId = artifactForUpdate.newerGroupId
209+
val newerArtifactId = artifactForUpdate.newerArtifactId
210210
if (newerGroupId.isEmpty && newerArtifactId.isEmpty) List.empty
211211
else {
212-
val currentGroupId = forArtifactId.groupId
213-
val currentArtifactId = forArtifactId.artifactIds.head
214212
modulePositions
215213
.filter { p =>
216-
p.groupId.value === currentGroupId.value &&
217-
currentArtifactId.names.contains_(p.artifactId.value)
214+
p.groupId.value === artifactForUpdate.groupId.value &&
215+
artifactForUpdate.artifactId.names.contains_(p.artifactId.value)
218216
}
219217
.flatMap { p =>
220-
newerGroupId.map(g => p.groupId.replaceWith(g.value)).toList ++
221-
newerArtifactId.map(a => p.artifactId.replaceWith(a)).toList
218+
newerGroupId.map(g => p.groupId.replaceWith(g.value)) ++
219+
newerArtifactId.map(a => p.artifactId.replaceWith(a))
222220
}
223221
}
224222
}

modules/core/src/main/scala/org/scalasteward/core/forge/data/NewPullRequestData.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,7 @@ object NewPullRequestData {
363363

364364
val artifactMigrationsLabel = Option.when {
365365
update.asSingleUpdates
366-
.flatMap(_.forArtifactIds.toList)
366+
.flatMap(_.artifactsForUpdate.toList)
367367
.exists(u => u.newerGroupId.nonEmpty || u.newerArtifactId.nonEmpty)
368368
}("artifact-migrations")
369369
val scalafixLabel = edits.collectFirst { case _: ScalafixEdit => "scalafix-migrations" }

modules/core/src/main/scala/org/scalasteward/core/nurture/PullRequestRepository.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ final class PullRequestRepository[F[_]](kvStore: KeyValueStore[F, Repo, Map[Uri,
7272
_.collect {
7373
case (url, Entry(baseSha1, u: Update.Single, state, _, number, updateBranch))
7474
if state === PullRequestState.Open &&
75-
u.withNewerVersions(update.newerVersions) === update &&
75+
u.withNextVersion(update.nextVersion) === update &&
7676
u.nextVersion < update.nextVersion =>
7777
for {
7878
number <- number

modules/core/src/main/scala/org/scalasteward/core/repoconfig/UpdatePattern.scala

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ package org.scalasteward.core.repoconfig
1919
import cats.syntax.all.*
2020
import io.circe.Codec
2121
import io.circe.generic.semiauto.*
22-
import org.scalasteward.core.data.{GroupId, Update, Version}
22+
import org.scalasteward.core.data.{ArtifactUpdateVersions, GroupId, Version}
2323

2424
final case class UpdatePattern(
2525
groupId: GroupId,
@@ -37,12 +37,14 @@ object UpdatePattern {
3737

3838
def findMatch(
3939
patterns: List[UpdatePattern],
40-
update: Update.ForArtifactId,
40+
update: ArtifactUpdateVersions,
4141
include: Boolean
4242
): MatchResult = {
43-
val byGroupId = patterns.filter(_.groupId === update.groupId)
44-
val byArtifactId = byGroupId.filter(_.artifactId.forall(_ === update.artifactId.name))
45-
val filteredVersions = update.newerVersions.filter(newVersion =>
43+
val artifactForUpdate = update.artifactForUpdate
44+
val byGroupId = patterns.filter(_.groupId === artifactForUpdate.groupId)
45+
val byArtifactId =
46+
byGroupId.filter(_.artifactId.forall(_ === artifactForUpdate.artifactId.name))
47+
val filteredVersions = update.refersToUpdateVersions.filter(newVersion =>
4648
byArtifactId.exists(_.version.forall(_.matches(newVersion.value))) === include
4749
)
4850
MatchResult(byArtifactId, filteredVersions)

0 commit comments

Comments
 (0)