Skip to content
Permalink
Browse files

add support for limiting updates through config

change limiting to not count ignored updates

add license header

fix behaviour

refactor processUpdates

address pr comments
  • Loading branch information...
manuelcueto committed Oct 18, 2019
1 parent b409475 commit f5d3d8042f027e6b39cb15471b5747e09909a7c7
@@ -13,6 +13,11 @@ updates.allow = [ { groupId = "com.example" } ]
# Defaults to empty `[]` which mean Scala Steward will not ignore dependencies.
updates.ignore = [ { groupId = "org.acme", artifactId="foo", version = "1.0" } ]
# If set, Scala Steward will only attempt to create or update `n` PRs.
# Useful if running frequently and/or CI build are costly
# Default: None
updates.limit = 5
# If true, Scala Steward will update the PR it created to resolve conflicts as
# long as you don't change it yourself.
# Default: true
@@ -0,0 +1,24 @@
/*
* Copyright 2018-2019 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.data

sealed trait ProcessResult extends Product with Serializable

object ProcessResult {
case object Ignored extends ProcessResult
case object Updated extends ProcessResult
}
@@ -21,7 +21,8 @@ import cats.implicits._
import io.chrisdavenport.log4cats.Logger
import org.scalasteward.core.application.Config
import org.scalasteward.core.coursier.CoursierAlg
import org.scalasteward.core.data.{Dependency, Update}
import org.scalasteward.core.data.ProcessResult.{Ignored, Updated}
import org.scalasteward.core.data.{Dependency, ProcessResult, Update}
import org.scalasteward.core.edit.EditAlg
import org.scalasteward.core.git.{Branch, GitAlg}
import org.scalasteward.core.repoconfig.RepoConfigAlg
@@ -81,60 +82,57 @@ final class NurtureAlg[F[_]](
memoizedGetDependencies <- Async.memoize {
sbtAlg.getDependencies(repo).handleError(_ => List.empty)
}
_ <- grouped.traverse_ { update =>
val data =
UpdateData(
repo,
fork,
repoConfig,
update,
baseBranch,
baseSha1,
git.branchFor(update)
)
processUpdate(data, memoizedGetDependencies)
}
_ <- NurtureAlg.processUpdates(
grouped,
update =>
processUpdate(
UpdateData(repo, fork, repoConfig, update, baseBranch, baseSha1, git.branchFor(update)),
memoizedGetDependencies
),
repoConfig.updates.limit
)
} yield ()

def processUpdate(data: UpdateData, getDependencies: F[List[Dependency]]): F[Unit] =
def processUpdate(data: UpdateData, getDependencies: F[List[Dependency]]): F[ProcessResult] =
for {
_ <- logger.info(s"Process update ${data.update.show}")
head = vcs.listingBranch(config.vcsType, data.fork, data.update)
pullRequests <- vcsApiAlg.listPullRequests(data.repo, head, data.baseBranch)
_ <- pullRequests.headOption match {
result <- pullRequests.headOption match {
case Some(pr) if pr.isClosed =>
logger.info(s"PR ${pr.html_url} is closed")
logger.info(s"PR ${pr.html_url} is closed") >> F.pure[ProcessResult](Ignored)
case Some(pr) if data.repoConfig.updatePullRequests =>
logger.info(s"Found PR ${pr.html_url}") >> updatePullRequest(data)
case Some(pr) =>
logger.info(s"Found PR ${pr.html_url}, but updates are disabled by flag")
logger.info(s"Found PR ${pr.html_url}, but updates are disabled by flag") >> F
.pure[ProcessResult](Ignored)
case None =>
applyNewUpdate(data, getDependencies)
}
_ <- pullRequests.headOption.fold(F.unit) { pr =>
pullRequestRepo.createOrUpdate(data.repo, pr.html_url, data.baseSha1, data.update, pr.state)
}
} yield ()
} yield result

def applyNewUpdate(data: UpdateData, getDependencies: F[List[Dependency]]): F[Unit] =
def applyNewUpdate(data: UpdateData, getDependencies: F[List[Dependency]]): F[ProcessResult] =
(editAlg.applyUpdate(data.repo, data.update) >> gitAlg.containsChanges(data.repo)).ifM(
gitAlg.returnToCurrentBranch(data.repo) {
for {
_ <- logger.info(s"Create branch ${data.updateBranch.name}")
_ <- gitAlg.createBranch(data.repo, data.updateBranch)
_ <- commitAndPush(data)
_ <- createPullRequest(data, getDependencies)
} yield ()
} yield Updated
},
logger.warn("No files were changed")
logger.warn("No files were changed") >> F.pure[ProcessResult](Ignored)
)

def commitAndPush(data: UpdateData): F[Unit] =
def commitAndPush(data: UpdateData): F[ProcessResult] =
for {
_ <- logger.info("Commit and push changes")
_ <- gitAlg.commitAll(data.repo, git.commitMsgFor(data.update))
_ <- gitAlg.push(data.repo, data.updateBranch)
} yield ()
} yield Updated

def createPullRequest(data: UpdateData, getDependencies: F[List[Dependency]]): F[Unit] =
for {
@@ -183,13 +181,13 @@ final class NurtureAlg[F[_]](
)
}

def updatePullRequest(data: UpdateData): F[Unit] =
def updatePullRequest(data: UpdateData): F[ProcessResult] =
gitAlg.returnToCurrentBranch(data.repo) {
for {
_ <- gitAlg.checkoutBranch(data.repo, data.updateBranch)
updated <- shouldBeUpdated(data)
_ <- if (updated) mergeAndApplyAgain(data) else F.unit
} yield ()
result <- if (updated) mergeAndApplyAgain(data) else F.pure[ProcessResult](Ignored)
} yield result
}

def shouldBeUpdated(data: UpdateData): F[Boolean] = {
@@ -210,14 +208,33 @@ final class NurtureAlg[F[_]](
result.flatMap { case (reset, msg) => logger.info(msg).as(reset) }
}

def mergeAndApplyAgain(data: UpdateData): F[Unit] =
def mergeAndApplyAgain(data: UpdateData): F[ProcessResult] =
for {
_ <- logger.info(
s"Merge branch '${data.baseBranch.name}' into ${data.updateBranch.name} and apply again"
)
_ <- gitAlg.mergeTheirs(data.repo, data.baseBranch)
_ <- editAlg.applyUpdate(data.repo, data.update)
containsChanges <- gitAlg.containsChanges(data.repo)
_ <- if (containsChanges) commitAndPush(data) else F.unit
} yield ()
result <- if (containsChanges) commitAndPush(data) else F.pure[ProcessResult](Ignored)
} yield result
}

object NurtureAlg {
def processUpdates[F[_]: Async](
updates: List[Update],
updateF: Update => F[ProcessResult],
updatesLimit: Option[Int]
): F[Unit] =
fs2.Stream
.emits(updates)
.evalMap(updateF)
.map {
case Ignored => 0
case Updated => 1
}
.scanMonoid
.takeWhile(updateCount => updatesLimit.fold(true)(_ > updateCount))
.compile
.drain
}
@@ -25,7 +25,8 @@ import org.scalasteward.core.update.FilterAlg.{FilterResult, IgnoredByConfig, No

final case class UpdatesConfig(
allow: List[UpdatePattern] = List.empty,
ignore: List[UpdatePattern] = List.empty
ignore: List[UpdatePattern] = List.empty,
limit: Option[Int] = None
) {
def keep(update: Update.Single): FilterResult =
isAllowed(update) *> isIgnored(update)
@@ -146,4 +146,5 @@ package object util {

outerLoop(list, ListBuffer.empty, ListBuffer.empty).toList
}

}
@@ -0,0 +1,52 @@
package org.scalasteward.core.nurture

import cats.data.{NonEmptyList, StateT}
import cats.effect.{Async, IO}
import cats.syntax.option._
import org.scalacheck.{Arbitrary, Gen}
import org.scalasteward.core.data.ProcessResult.{Ignored, Updated}
import org.scalasteward.core.data.Update.Single
import org.scalasteward.core.data.{GroupId, ProcessResult, Update}
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks

class NurtureAlgTest extends AnyFunSuite with Matchers with ScalaCheckPropertyChecks {

implicit val updateArbitrary: Arbitrary[Update] = Arbitrary(for {
groupId <- Gen.alphaStr.map(GroupId.apply)
artifactId <- Gen.alphaStr
currentVersion <- Gen.alphaStr
newerVersion <- Gen.alphaStr
} yield Single(groupId, artifactId, currentVersion, NonEmptyList.one(newerVersion)))

test("processUpdates with No Limiting") {
forAll { updates: List[Update] =>
NurtureAlg
.processUpdates(
updates,
_ => StateT[IO, Int, ProcessResult](actionAcc => IO.pure(actionAcc + 1 -> Ignored)),
None
)
.runS(0)
.unsafeRunSync() shouldBe updates.size
}
}

test("processUpdates with Limiting should process all updates up to the limit") {
forAll { updates: Set[Update] =>
val (ignorableUpdates, appliableUpdates) = updates.toList.splitAt(updates.size / 2)

implicit val stateAsync = Async.catsStateTAsync[IO, Int]
val f: Update => StateT[IO, Int, ProcessResult] = update =>
StateT[IO, Int, ProcessResult](
actionAcc =>
IO.pure(actionAcc + 1 -> (if (ignorableUpdates.contains(update)) Ignored else Updated))
)
NurtureAlg
.processUpdates(ignorableUpdates ++ appliableUpdates, f, appliableUpdates.size.some)
.runS(0)
.unsafeRunSync() shouldBe updates.size
}
}
}
@@ -16,14 +16,16 @@ class RepoConfigAlgTest extends AnyFunSuite with Matchers {
val content =
"""|updates.allow = [ { groupId = "eu.timepit", artifactId = "refined", version = "0.8." } ]
|updates.ignore = [ { groupId = "org.acme", version = "1.0" } ]
|updates.limit = 4
|""".stripMargin
val initialState = MockState.empty.add(configFile, content)
val config = repoConfigAlg.readRepoConfigOrDefault(repo).runA(initialState).unsafeRunSync()

config shouldBe RepoConfig(
updates = UpdatesConfig(
allow = List(UpdatePattern(GroupId("eu.timepit"), Some("refined"), Some("0.8."))),
ignore = List(UpdatePattern(GroupId("org.acme"), None, Some("1.0")))
ignore = List(UpdatePattern(GroupId("org.acme"), None, Some("1.0"))),
limit = Some(4)
)
)
}

0 comments on commit f5d3d80

Please sign in to comment.
You can’t perform that action at this time.