Skip to content

Commit

Permalink
add support for limiting updates through config
Browse files Browse the repository at this point in the history
change limiting to not count ignored updates

add license header

fix behaviour

refactor processUpdates

address pr comments
  • Loading branch information
Manuel cueto Rodriguez committed Oct 21, 2019
1 parent b409475 commit f5d3d80
Show file tree
Hide file tree
Showing 7 changed files with 134 additions and 32 deletions.
5 changes: 5 additions & 0 deletions docs/repo-specific-configuration.md
Expand Up @@ -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
Expand Down
@@ -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
}
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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] = {
Expand All @@ -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
}
Expand Up @@ -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)
Expand Down
Expand Up @@ -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
}
}
}
Expand Up @@ -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)
)
)
}
Expand Down

0 comments on commit f5d3d80

Please sign in to comment.