Skip to content

Commit c2f8651

Browse files
authored
Merge pull request #1488 from eugeniyk/mergeable-repo-configurations
Mergeable repo configurations
2 parents c74f7f7 + 86a7fa1 commit c2f8651

File tree

15 files changed

+383
-13
lines changed

15 files changed

+383
-13
lines changed

docs/running.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,4 +129,4 @@ docker run -v $PWD:/opt/scala-steward \
129129
### Running On-premise (GitHub Enterprise)
130130

131131
There is an article on how they run Scala Steward on-premise at Avast:
132-
* [Running Scala Steward On-premise](https://engineering.avast.io/running-scala-steward-on-premise)
132+
* [Running Scala Steward On-premise](https://engineering.avast.io/running-scala-steward-on-premise)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ final class NurtureAlg[F[_]](implicit
8181
updates: List[Update.Single]
8282
): F[Unit] =
8383
for {
84-
repoConfig <- repoConfigAlg.readRepoConfigOrDefault(repo)
84+
repoConfig <- repoConfigAlg.readRepoConfigWithDefault(repo)
8585
grouped = Update.groupByGroupId(updates)
8686
sorted = grouped.sortBy(migrationAlg.findMigrations(_).size)
8787
_ <- logger.info(util.logger.showUpdates(sorted))

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package org.scalasteward.core.repoconfig
1818

19+
import cats.kernel.{Eq, Semigroup}
1920
import io.circe.Codec
2021
import io.circe.generic.extras.Configuration
2122
import io.circe.generic.extras.semiauto._
@@ -33,6 +34,13 @@ object CommitsConfig {
3334
implicit val customConfig: Configuration =
3435
Configuration.default.withDefaults
3536

37+
implicit val eqPullRequestsConfig: Eq[CommitsConfig] = Eq.fromUniversalEquals
38+
3639
implicit val commitsConfigCodec: Codec[CommitsConfig] =
3740
deriveConfiguredCodec
41+
42+
implicit val semigroup: Semigroup[CommitsConfig] = new Semigroup[CommitsConfig] {
43+
override def combine(x: CommitsConfig, y: CommitsConfig): CommitsConfig =
44+
CommitsConfig(x.message.orElse(y.message))
45+
}
3846
}

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package org.scalasteward.core.repoconfig
1818

19+
import cats.kernel.{Eq, Semigroup}
1920
import io.circe.Codec
2021
import io.circe.generic.extras.Configuration
2122
import io.circe.generic.extras.semiauto.deriveConfiguredCodec
@@ -31,6 +32,13 @@ object PullRequestsConfig {
3132
implicit val customConfig: Configuration =
3233
Configuration.default.withDefaults
3334

35+
implicit val eqPullRequestsConfig: Eq[PullRequestsConfig] = Eq.fromUniversalEquals
36+
3437
implicit val pullRequestsConfigCodec: Codec[PullRequestsConfig] =
3538
deriveConfiguredCodec
39+
40+
implicit val semigroup: Semigroup[PullRequestsConfig] = new Semigroup[PullRequestsConfig] {
41+
override def combine(x: PullRequestsConfig, y: PullRequestsConfig): PullRequestsConfig =
42+
PullRequestsConfig(x.frequency.orElse(y.frequency))
43+
}
3644
}

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
package org.scalasteward.core.repoconfig
1818

19+
import cats.kernel.Semigroup
20+
import cats.syntax.semigroup._
1921
import io.circe.Codec
2022
import io.circe.generic.extras.Configuration
2123
import io.circe.generic.extras.semiauto._
@@ -38,4 +40,19 @@ object RepoConfig {
3840

3941
implicit val repoConfigCodec: Codec[RepoConfig] =
4042
deriveConfiguredCodec
43+
44+
implicit val semigroup: Semigroup[RepoConfig] = new Semigroup[RepoConfig] {
45+
override def combine(x: RepoConfig, y: RepoConfig): RepoConfig =
46+
(x, y) match {
47+
case (`empty`, _) => y
48+
case (_, `empty`) => x
49+
case _ =>
50+
RepoConfig(
51+
commits = x.commits |+| y.commits,
52+
pullRequests = x.pullRequests |+| y.pullRequests,
53+
updates = x.updates |+| y.updates,
54+
updatePullRequests = x.updatePullRequests.orElse(y.updatePullRequests)
55+
)
56+
}
57+
}
4158
}

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,14 @@ final class RepoConfigAlg[F[_]](implicit
3535
workspaceAlg: WorkspaceAlg[F],
3636
F: MonadThrowable[F]
3737
) {
38-
def readRepoConfigOrDefault(repo: Repo): F[RepoConfig] =
39-
readRepoConfig(repo).flatMap { config =>
40-
config.map(F.pure).getOrElse(defaultRepoConfig)
41-
}
38+
39+
def readRepoConfigWithDefault(repo: Repo): F[RepoConfig] =
40+
for {
41+
config <- readRepoConfig(repo)
42+
defaultCfg <- defaultRepoConfig
43+
} yield config
44+
.map(_ |+| defaultCfg)
45+
.getOrElse(defaultCfg)
4246

4347
/**
4448
* Default configuration will try to read file specified in config.defaultRepoConfigFile first;

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616

1717
package org.scalasteward.core.repoconfig
1818

19-
import cats.syntax.all._
19+
import cats.implicits._
20+
import cats.kernel.Eq
2021
import io.circe.generic.semiauto._
2122
import io.circe.{Decoder, Encoder, HCursor}
2223
import org.scalasteward.core.data.{GroupId, Update}
@@ -25,7 +26,9 @@ final case class UpdatePattern(
2526
groupId: GroupId,
2627
artifactId: Option[String],
2728
version: Option[UpdatePattern.Version]
28-
)
29+
) {
30+
def isWholeGroupIdAllowed: Boolean = artifactId.isEmpty && version.isEmpty
31+
}
2932

3033
object UpdatePattern {
3134
final case class MatchResult(
@@ -67,6 +70,8 @@ object UpdatePattern {
6770
} yield Version(prefix, suffix)
6871
)
6972

73+
implicit val eqVersion: Eq[Version] = Eq.fromUniversalEquals
74+
7075
implicit val updatePatternVersionEncoder: Encoder[Version] =
7176
deriveEncoder
7277
}

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

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,14 @@
1616

1717
package org.scalasteward.core.repoconfig
1818

19+
import cats.implicits._
20+
import cats.kernel.Semigroup
1921
import eu.timepit.refined.types.numeric.PosInt
2022
import io.circe.generic.extras.Configuration
2123
import io.circe.generic.extras.semiauto._
2224
import io.circe.refined._
2325
import io.circe.{Decoder, Encoder}
24-
import org.scalasteward.core.data.Update
26+
import org.scalasteward.core.data.{GroupId, Update}
2527
import org.scalasteward.core.update.FilterAlg.{
2628
FilterResult,
2729
IgnoredByConfig,
@@ -89,6 +91,109 @@ object UpdatesConfig {
8991
val defaultFileExtensions: Set[String] =
9092
Set(".scala", ".sbt", ".sbt.shared", ".sc", ".yml", "pom.xml")
9193

94+
private[repoconfig] val nonExistingFileExtension: List[String] = List(".non-exist")
95+
private[repoconfig] val nonExistingUpdatePattern: List[UpdatePattern] = List(
96+
UpdatePattern(GroupId("non-exist"), None, None)
97+
)
98+
9299
// prevent IntelliJ from removing the import of io.circe.refined._
93100
locally(refinedDecoder: Decoder[PosInt])
101+
102+
implicit val semigroup: Semigroup[UpdatesConfig] = new Semigroup[UpdatesConfig] {
103+
override def combine(x: UpdatesConfig, y: UpdatesConfig): UpdatesConfig =
104+
UpdatesConfig(
105+
pin = mergePin(x.pin, y.pin),
106+
allow = mergeAllow(x.allow, y.allow),
107+
ignore = mergeIgnore(x.ignore, y.ignore),
108+
limit = x.limit.orElse(y.limit),
109+
includeScala = x.includeScala.orElse(y.includeScala),
110+
fileExtensions = mergeFileExtensions(x.fileExtensions, y.fileExtensions)
111+
)
112+
}
113+
114+
// Strategy: union with repo preference in terms of revision
115+
private[repoconfig] def mergePin(
116+
x: List[UpdatePattern],
117+
y: List[UpdatePattern]
118+
): List[UpdatePattern] =
119+
(x ::: y).distinctBy(up => up.groupId -> up.artifactId)
120+
121+
// Strategy: superset
122+
// Xa.Ya.Za |+| Xb.Yb.Zb
123+
private[repoconfig] def mergeAllow(
124+
x: List[UpdatePattern],
125+
y: List[UpdatePattern]
126+
): List[UpdatePattern] =
127+
(x, y) match {
128+
case (Nil, second) => second
129+
case (first, Nil) => first
130+
case _ =>
131+
// remove duplicates first by calling .distinct
132+
val xm: Map[GroupId, List[UpdatePattern]] = x.distinct.groupBy(_.groupId)
133+
val ym: Map[GroupId, List[UpdatePattern]] = y.distinct.groupBy(_.groupId)
134+
val builder = new collection.mutable.ListBuffer[UpdatePattern]()
135+
136+
// first of all, we only allow intersection (superset)
137+
val keys = xm.keySet.intersect(ym.keySet)
138+
139+
keys.foreach { groupId =>
140+
builder ++= mergeAllowGroupId(xm(groupId), ym(groupId))
141+
}
142+
143+
if (builder.isEmpty) nonExistingUpdatePattern
144+
else builder.distinct.toList
145+
}
146+
147+
// merge UpdatePattern for same group id
148+
private[this] def mergeAllowGroupId(
149+
x: List[UpdatePattern],
150+
y: List[UpdatePattern]
151+
): List[UpdatePattern] =
152+
(x.exists(_.isWholeGroupIdAllowed), y.exists(_.isWholeGroupIdAllowed)) match {
153+
case (true, _) => y
154+
case (_, true) => x
155+
case _ =>
156+
// case with concrete artifacts / versions
157+
val builder = new collection.mutable.ListBuffer[UpdatePattern]()
158+
val xByArtifacts = x.groupBy(_.artifactId)
159+
val yByArtifacts = y.groupBy(_.artifactId)
160+
161+
x.foreach { updatePattern =>
162+
if (satisfyUpdatePattern(updatePattern, yByArtifacts))
163+
builder += updatePattern
164+
}
165+
y.foreach { updatePattern =>
166+
if (satisfyUpdatePattern(updatePattern, xByArtifacts))
167+
builder += updatePattern
168+
}
169+
170+
if (builder.isEmpty) nonExistingUpdatePattern
171+
else builder.toList
172+
}
173+
174+
private[this] def satisfyUpdatePattern(
175+
targetUpdatePattern: UpdatePattern,
176+
comparedUpdatePatternsByArtifact: Map[Option[String], List[UpdatePattern]]
177+
): Boolean =
178+
comparedUpdatePatternsByArtifact.get(targetUpdatePattern.artifactId).exists { matchedVersions =>
179+
// For simplicity I'm using direct equals here between versions. Feel free to make it more advanced
180+
matchedVersions.exists(up => up.version.isEmpty || up.version === targetUpdatePattern.version)
181+
}
182+
183+
// Strategy: union
184+
private[repoconfig] def mergeIgnore(
185+
x: List[UpdatePattern],
186+
y: List[UpdatePattern]
187+
): List[UpdatePattern] =
188+
(x ::: y).distinct
189+
private[repoconfig] def mergeFileExtensions(x: List[String], y: List[String]): List[String] =
190+
(x, y) match {
191+
case (Nil, second) => second
192+
case (first, Nil) => first
193+
case _ =>
194+
val result = x.intersect(y)
195+
// Since empty result represents [*] any extension, we gonna set artificial extension instead.
196+
if (result.nonEmpty) result
197+
else nonExistingFileExtension
198+
}
94199
}

modules/core/src/main/scala/org/scalasteward/core/update/PruningAlg.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,8 @@ final class PruningAlg[F[_]](implicit
7474
val depsWithoutResolvers = dependencies.map(_.value).distinct
7575
for {
7676
_ <- logger.info(s"Find updates for ${repo.show}")
77-
repoConfig <-
78-
OptionT.fromOption[F](repoCache.maybeRepoConfig).getOrElseF(repoConfigAlg.defaultRepoConfig)
77+
defaultConfig <- repoConfigAlg.defaultRepoConfig
78+
repoConfig = repoCache.maybeRepoConfig.map(_ |+| defaultConfig).getOrElse(defaultConfig)
7979
updates0 <- updateAlg.findUpdates(dependencies, repoConfig, None)
8080
updateStates0 <- findAllUpdateStates(repo, repoCache, depsWithoutResolvers, updates0)
8181
outdatedDeps = collectOutdatedDependencies(updateStates0)
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package org.scalasteward.core.repoconfig
2+
3+
import cats.kernel.laws.discipline.SemigroupTests
4+
import org.scalacheck.Arbitrary
5+
import org.scalatest.funsuite.AnyFunSuite
6+
import org.scalatest.matchers.should.Matchers
7+
import org.scalatest.prop.Configuration
8+
import org.typelevel.discipline.scalatest.FunSuiteDiscipline
9+
10+
class CommitsConfigTest
11+
extends AnyFunSuite
12+
with Matchers
13+
with FunSuiteDiscipline
14+
with Configuration {
15+
implicit val commitsConfigArbitrary: Arbitrary[CommitsConfig] = Arbitrary(for {
16+
e <- Arbitrary.arbitrary[Option[String]]
17+
} yield CommitsConfig(e))
18+
19+
checkAll("Semigroup[CommitsConfig]", SemigroupTests[CommitsConfig].semigroup)
20+
}

0 commit comments

Comments
 (0)