From 5348ba00ca52f8309a252ca5e612279b48734c8d Mon Sep 17 00:00:00 2001 From: Devon Stewart Date: Fri, 19 Mar 2021 23:15:49 -0700 Subject: [PATCH 1/9] Adding GitAlg.diff --- .../org/scalasteward/core/git/FileGitAlg.scala | 3 +++ .../org/scalasteward/core/git/GenGitAlg.scala | 5 +++++ .../scalasteward/core/git/FileGitAlgTest.scala | 17 +++++++++++++++++ 3 files changed, 25 insertions(+) diff --git a/modules/core/src/main/scala/org/scalasteward/core/git/FileGitAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/git/FileGitAlg.scala index d6e696a5ad..b870b6d0dd 100644 --- a/modules/core/src/main/scala/org/scalasteward/core/git/FileGitAlg.scala +++ b/modules/core/src/main/scala/org/scalasteward/core/git/FileGitAlg.scala @@ -138,6 +138,9 @@ final class FileGitAlg[F[_]](config: GitCfg)(implicit override def version: F[String] = workspaceAlg.rootDir.flatMap(git("--version")).map(_.mkString.trim) + override def diff(repo: File, branch: Branch): F[List[String]] = + git("diff", branch.name)(repo) + private def git(args: String*)(repo: File): F[List[String]] = processAlg.exec(Nel.of("git", args: _*), repo, "GIT_ASKPASS" -> config.gitAskPass.pathAsString) diff --git a/modules/core/src/main/scala/org/scalasteward/core/git/GenGitAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/git/GenGitAlg.scala index bba23a1db3..595681b4d9 100644 --- a/modules/core/src/main/scala/org/scalasteward/core/git/GenGitAlg.scala +++ b/modules/core/src/main/scala/org/scalasteward/core/git/GenGitAlg.scala @@ -69,6 +69,8 @@ trait GenGitAlg[F[_], Repo] { def version: F[String] + def diff(repo: Repo, branch: Branch): F[List[String]] + final def commitAllIfDirty(repo: Repo, message: String, messages: String*)(implicit F: Monad[F] ): F[Option[Commit]] = @@ -142,6 +144,9 @@ trait GenGitAlg[F[_], Repo] { override def version: F[String] = self.version + + override def diff(repo: A, branch: Branch): F[List[String]] = + f(repo).flatMap(self.diff(_, branch)) } } } diff --git a/modules/core/src/test/scala/org/scalasteward/core/git/FileGitAlgTest.scala b/modules/core/src/test/scala/org/scalasteward/core/git/FileGitAlgTest.scala index 0a5bff0207..a15ef6fd4d 100644 --- a/modules/core/src/test/scala/org/scalasteward/core/git/FileGitAlgTest.scala +++ b/modules/core/src/test/scala/org/scalasteward/core/git/FileGitAlgTest.scala @@ -143,6 +143,23 @@ class FileGitAlgTest extends FunSuite { test("version") { assert(ioGitAlg.version.unsafeRunSync().nonEmpty) } + + test("diff") { + val repo = rootDir / "diff" + val wip = Branch("wip") + val p = for { + _ <- supplement.createRepo(repo) + _ <- ioFileAlg.writeFile(repo / "test.txt", "hello") + _ <- supplement.git("add", "test.txt")(repo) + _ <- ioGitAlg.commitAll(repo, "Add test.txt") + // work on wip + _ <- ioGitAlg.createBranch(repo, wip) + c1 <- ioGitAlg.diff(repo, master) + _ <- ioFileAlg.writeFile(repo / "test.txt", "hello world") + c2 <- ioGitAlg.diff(repo, master) + } yield (c1.isEmpty, c2.isEmpty) + assertEquals(p.unsafeRunSync(), (true, false)) + } } object FileGitAlgTest { From 00151c90f316635cf1e9c714c583afeb216d3aba Mon Sep 17 00:00:00 2001 From: Devon Stewart Date: Fri, 19 Mar 2021 23:17:51 -0700 Subject: [PATCH 2/9] Keeping track of branches we have seen so far --- .../core/nurture/NurtureAlg.scala | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/modules/core/src/main/scala/org/scalasteward/core/nurture/NurtureAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/nurture/NurtureAlg.scala index 1ef56fe630..0dcec22fc9 100644 --- a/modules/core/src/main/scala/org/scalasteward/core/nurture/NurtureAlg.scala +++ b/modules/core/src/main/scala/org/scalasteward/core/nurture/NurtureAlg.scala @@ -17,7 +17,8 @@ package org.scalasteward.core.nurture import cats.Applicative -import cats.effect.BracketThrow +import cats.effect.{BracketThrow, Sync} +import cats.effect.concurrent.Ref import cats.implicits._ import eu.timepit.refined.types.numeric.NonNegInt import fs2.Stream @@ -49,13 +50,15 @@ final class NurtureAlg[F[_]](config: Config)(implicit vcsRepoAlg: VCSRepoAlg[F], streamCompiler: Stream.Compiler[F, F], urlChecker: UrlChecker[F], - F: BracketThrow[F] + F: BracketThrow[F], + FS: Sync[F] ) { def nurture(data: RepoData, fork: RepoOut, updates: List[Update.Single]): F[Unit] = for { _ <- logger.info(s"Nurture ${data.repo.show}") baseBranch <- cloneAndSync(data.repo, fork) - _ <- updateDependencies(data, fork.repo, baseBranch, updates) + seenBranches <- Ref[F].of(List.empty[Branch]) + _ <- updateDependencies(data, fork.repo, baseBranch, updates, seenBranches) } yield () def cloneAndSync(repo: Repo, fork: RepoOut): F[Branch] = @@ -68,7 +71,8 @@ final class NurtureAlg[F[_]](config: Config)(implicit data: RepoData, fork: Repo, baseBranch: Branch, - updates: List[Update.Single] + updates: List[Update.Single], + seenBranches: Ref[F, List[Branch]] ): F[Unit] = for { _ <- F.unit @@ -82,8 +86,13 @@ final class NurtureAlg[F[_]](config: Config)(implicit UpdateData(data, fork, update, baseBranch, baseSha1, git.branchFor(update)) processUpdate(updateData).flatMap { case result @ Created(newPrNumber) => - closeObsoletePullRequests(updateData, newPrNumber).as[ProcessResult](result) - case result @ _ => F.pure(result) + (for { + _ <- closeObsoletePullRequests(updateData, newPrNumber) + _ <- seenBranches.update(updateData.updateBranch :: _) + } yield ()).as[ProcessResult](result) + case result @ Updated => + seenBranches.update(updateData.updateBranch :: _).as[ProcessResult](result) + case result @ Ignored => F.pure(result) } }, data.config.updates.limit From b68d179a33d57792169386c6e4cb24606613de65 Mon Sep 17 00:00:00 2001 From: Devon Stewart Date: Fri, 19 Mar 2021 23:27:21 -0700 Subject: [PATCH 3/9] Before pushing updates or branches, ensure we haven't already made these changes --- .../core/nurture/NurtureAlg.scala | 42 ++++++++++++++----- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/modules/core/src/main/scala/org/scalasteward/core/nurture/NurtureAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/nurture/NurtureAlg.scala index 0dcec22fc9..cd690c644b 100644 --- a/modules/core/src/main/scala/org/scalasteward/core/nurture/NurtureAlg.scala +++ b/modules/core/src/main/scala/org/scalasteward/core/nurture/NurtureAlg.scala @@ -84,7 +84,7 @@ final class NurtureAlg[F[_]](config: Config)(implicit update => { val updateData = UpdateData(data, fork, update, baseBranch, baseSha1, git.branchFor(update)) - processUpdate(updateData).flatMap { + processUpdate(updateData, seenBranches).flatMap { case result @ Created(newPrNumber) => (for { _ <- closeObsoletePullRequests(updateData, newPrNumber) @@ -99,7 +99,7 @@ final class NurtureAlg[F[_]](config: Config)(implicit ) } yield () - def processUpdate(data: UpdateData): F[ProcessResult] = + def processUpdate(data: UpdateData, seenBranches: Ref[F, List[Branch]]): F[ProcessResult] = for { _ <- logger.info(s"Process update ${data.update.show}") head = vcs.listingBranch(config.vcsType, data.fork, data.update) @@ -109,9 +109,9 @@ final class NurtureAlg[F[_]](config: Config)(implicit logger.info(s"PR ${pr.html_url} is closed") >> removeRemoteBranch(data.repo, data.updateBranch).as(Ignored) case Some(pr) => - logger.info(s"Found PR ${pr.html_url}") >> updatePullRequest(data) + logger.info(s"Found PR ${pr.html_url}") >> updatePullRequest(data, seenBranches) case None => - applyNewUpdate(data) + applyNewUpdate(data, seenBranches) } _ <- pullRequests.headOption.traverse_ { pr => pullRequestRepository.createOrUpdate( @@ -155,13 +155,30 @@ final class NurtureAlg[F[_]](config: Config)(implicit gitAlg.removeBranch(repo, branch) } - def applyNewUpdate(data: UpdateData): F[ProcessResult] = + def ensureDistinctBranch( + data: UpdateData, + seenBranches: Ref[F, List[Branch]], + whenDistinct: F[ProcessResult] + ): F[ProcessResult] = + seenBranches.get + .flatMap(_.forallM(gitAlg.diff(data.repo, _).map(_.nonEmpty))) + .ifM( + whenDistinct, + logger.warn("Discovered a duplicate branch, not pushing").as(Ignored) + ) + + def applyNewUpdate(data: UpdateData, seenBranches: Ref[F, List[Branch]]): F[ProcessResult] = gitAlg.returnToCurrentBranch(data.repo) { val createBranch = logger.info(s"Create branch ${data.updateBranch.name}") >> gitAlg.createBranch(data.repo, data.updateBranch) editAlg.applyUpdate(data.repoData, data.update, createBranch).flatMap { editCommits => if (editCommits.isEmpty) logger.warn("No commits created").as(Ignored) - else pushCommits(data, editCommits) >> createPullRequest(data) + else + ensureDistinctBranch( + data, + seenBranches, + pushCommits(data, editCommits) >> createPullRequest(data) + ) } } @@ -210,13 +227,14 @@ final class NurtureAlg[F[_]](config: Config)(implicit _ <- logger.info(s"Created PR ${pr.html_url}") } yield Created(pr.number) - def updatePullRequest(data: UpdateData): F[ProcessResult] = + def updatePullRequest(data: UpdateData, seenBranches: Ref[F, List[Branch]]): F[ProcessResult] = if (data.repoConfig.updatePullRequestsOrDefault =!= PullRequestUpdateStrategy.Never) gitAlg.returnToCurrentBranch(data.repo) { for { _ <- gitAlg.checkoutBranch(data.repo, data.updateBranch) update <- shouldBeUpdated(data) - result <- if (update) mergeAndApplyAgain(data) else F.pure[ProcessResult](Ignored) + result <- + if (update) mergeAndApplyAgain(data, seenBranches) else F.pure[ProcessResult](Ignored) } yield result } else @@ -242,14 +260,18 @@ final class NurtureAlg[F[_]](config: Config)(implicit result.flatMap { case (update, msg) => logger.info(msg).as(update) } } - def mergeAndApplyAgain(data: UpdateData): F[ProcessResult] = + def mergeAndApplyAgain(data: UpdateData, seenBranches: Ref[F, List[Branch]]): F[ProcessResult] = for { _ <- logger.info( s"Merge branch ${data.baseBranch.name} into ${data.updateBranch.name} and apply again" ) maybeMergeCommit <- gitAlg.mergeTheirs(data.repo, data.baseBranch) editCommits <- editAlg.applyUpdate(data.repoData, data.update) - result <- pushCommits(data, maybeMergeCommit.toList ++ editCommits) + result <- ensureDistinctBranch( + data, + seenBranches, + pushCommits(data, maybeMergeCommit.toList ++ editCommits) + ) } yield result } From 2d345080bb74e1f34b0fc2f848e3d028867f59e4 Mon Sep 17 00:00:00 2001 From: Devon Stewart Date: Sat, 20 Mar 2021 10:41:06 -0700 Subject: [PATCH 4/9] Exploratory commit to rough out what could constitute a useful test I don't like this strategy, but I'm interested to see what codecov (and others) think about this approach. I've basically just reimplemented the business logic in the test, but short of mocking out the whole git repo in order to extract state, I'm not sure how best to represent what I'm trying to test. --- .../core/nurture/NurtureAlg.scala | 36 +++++++++---------- .../org/scalasteward/core/TestInstances.scala | 9 ++++- .../core/nurture/NurtureAlgTest.scala | 29 +++++++++++++++ 3 files changed, 55 insertions(+), 19 deletions(-) diff --git a/modules/core/src/main/scala/org/scalasteward/core/nurture/NurtureAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/nurture/NurtureAlg.scala index cd690c644b..88b385c89c 100644 --- a/modules/core/src/main/scala/org/scalasteward/core/nurture/NurtureAlg.scala +++ b/modules/core/src/main/scala/org/scalasteward/core/nurture/NurtureAlg.scala @@ -155,18 +155,6 @@ final class NurtureAlg[F[_]](config: Config)(implicit gitAlg.removeBranch(repo, branch) } - def ensureDistinctBranch( - data: UpdateData, - seenBranches: Ref[F, List[Branch]], - whenDistinct: F[ProcessResult] - ): F[ProcessResult] = - seenBranches.get - .flatMap(_.forallM(gitAlg.diff(data.repo, _).map(_.nonEmpty))) - .ifM( - whenDistinct, - logger.warn("Discovered a duplicate branch, not pushing").as(Ignored) - ) - def applyNewUpdate(data: UpdateData, seenBranches: Ref[F, List[Branch]]): F[ProcessResult] = gitAlg.returnToCurrentBranch(data.repo) { val createBranch = logger.info(s"Create branch ${data.updateBranch.name}") >> @@ -174,10 +162,11 @@ final class NurtureAlg[F[_]](config: Config)(implicit editAlg.applyUpdate(data.repoData, data.update, createBranch).flatMap { editCommits => if (editCommits.isEmpty) logger.warn("No commits created").as(Ignored) else - ensureDistinctBranch( - data, + NurtureAlg.ensureDistinctBranch( seenBranches, - pushCommits(data, editCommits) >> createPullRequest(data) + gitAlg.diff(data.repo, _).map(_.nonEmpty), + pushCommits(data, editCommits) >> createPullRequest(data), + logger.warn("Discovered a duplicate branch, not pushing").as[ProcessResult](Ignored) ) } } @@ -267,10 +256,11 @@ final class NurtureAlg[F[_]](config: Config)(implicit ) maybeMergeCommit <- gitAlg.mergeTheirs(data.repo, data.baseBranch) editCommits <- editAlg.applyUpdate(data.repoData, data.update) - result <- ensureDistinctBranch( - data, + result <- NurtureAlg.ensureDistinctBranch( seenBranches, - pushCommits(data, maybeMergeCommit.toList ++ editCommits) + gitAlg.diff(data.repo, _).map(_.nonEmpty), + pushCommits(data, maybeMergeCommit.toList ++ editCommits), + logger.warn("Discovered a duplicate branch, not pushing").as[ProcessResult](Ignored) ) } yield result } @@ -295,4 +285,14 @@ object NurtureAlg { .compile .drain } + + def ensureDistinctBranch[F[_]: Sync: cats.FlatMap]( + seenBranches: Ref[F, List[Branch]], + isDistinct: Branch => F[Boolean], + whenDistinct: F[ProcessResult], + whenDuplicate: F[ProcessResult] + ): F[ProcessResult] = + seenBranches.get + .flatMap(_.forallM(isDistinct)) + .ifM(whenDistinct, whenDuplicate) } diff --git a/modules/core/src/test/scala/org/scalasteward/core/TestInstances.scala b/modules/core/src/test/scala/org/scalasteward/core/TestInstances.scala index 3e18ba239a..908d9a5168 100644 --- a/modules/core/src/test/scala/org/scalasteward/core/TestInstances.scala +++ b/modules/core/src/test/scala/org/scalasteward/core/TestInstances.scala @@ -9,7 +9,7 @@ import org.scalacheck.{Arbitrary, Cogen, Gen} import org.scalasteward.core.TestSyntax._ import org.scalasteward.core.data.Update.Single import org.scalasteward.core.data._ -import org.scalasteward.core.git.Sha1 +import org.scalasteward.core.git.{Branch, Sha1} import org.scalasteward.core.git.Sha1.HexString import org.scalasteward.core.repocache.RepoCache import org.scalasteward.core.repoconfig.PullRequestFrequency.{Asap, Timespan} @@ -59,6 +59,13 @@ object TestInstances { } yield Single(groupId % artifactId % currentVersion, Nel.one(newerVersion)) ) + implicit val branchArbitrary: Arbitrary[Branch] = + Arbitrary( + for { + name <- Gen.alphaStr + } yield Branch(name) + ) + private val hashGen: Gen[String] = for { sep <- Gen.oneOf('-', '+') diff --git a/modules/core/src/test/scala/org/scalasteward/core/nurture/NurtureAlgTest.scala b/modules/core/src/test/scala/org/scalasteward/core/nurture/NurtureAlgTest.scala index cdc80497d5..2f8d16f3a6 100644 --- a/modules/core/src/test/scala/org/scalasteward/core/nurture/NurtureAlgTest.scala +++ b/modules/core/src/test/scala/org/scalasteward/core/nurture/NurtureAlgTest.scala @@ -1,13 +1,17 @@ package org.scalasteward.core.nurture +import cats.Applicative import cats.data.StateT import cats.effect.IO +import cats.effect.concurrent.Ref +import cats.syntax.all._ import eu.timepit.refined.types.numeric.NonNegInt import munit.ScalaCheckSuite import org.scalacheck.Prop._ import org.scalasteward.core.TestInstances._ import org.scalasteward.core.data.ProcessResult.{Ignored, Updated} import org.scalasteward.core.data.{ProcessResult, Update} +import org.scalasteward.core.git.Branch class NurtureAlgTest extends ScalaCheckSuite { test("processUpdates with No Limiting") { @@ -42,4 +46,29 @@ class NurtureAlgTest extends ScalaCheckSuite { assertEquals(obtained, updates.size) } } + + test( + "Ensure distinct should not push branches that are not different to other visited branches" + ) { + forAll { branches: Set[Branch] => + val (duplicateBranches, distinctBranches) = branches.toList.splitAt(branches.size / 2) + type F[A] = StateT[IO, Int, A] + val F = Applicative[F] + val f: StateT[IO, Int, ProcessResult] = + StateT[IO, Int, ProcessResult](actionAcc => IO.pure(actionAcc + 1 -> Updated)) + val obtained = (for { + seenBranches <- Ref[F].of(duplicateBranches) + res <- (duplicateBranches ++ distinctBranches).traverse { branch => + NurtureAlg + .ensureDistinctBranch( + seenBranches, + _ => F.pure(distinctBranches.contains(branch)), + f, + F.pure[ProcessResult](Ignored) + ) + } + } yield res).runS(0).unsafeRunSync() + assertEquals(obtained, distinctBranches.length) + } + } } From 8b37606defc5350eb4b8c33efa5905d0d7ee671d Mon Sep 17 00:00:00 2001 From: Devon Stewart Date: Sat, 20 Mar 2021 14:06:36 -0700 Subject: [PATCH 5/9] Revert "Exploratory commit to rough out what could constitute a useful test" This reverts commit ced9ed1749de3d36bb2aa7bef22a3ff525a30bb4. --- .../core/nurture/NurtureAlg.scala | 36 +++++++++---------- .../org/scalasteward/core/TestInstances.scala | 9 +---- .../core/nurture/NurtureAlgTest.scala | 29 --------------- 3 files changed, 19 insertions(+), 55 deletions(-) diff --git a/modules/core/src/main/scala/org/scalasteward/core/nurture/NurtureAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/nurture/NurtureAlg.scala index 88b385c89c..cd690c644b 100644 --- a/modules/core/src/main/scala/org/scalasteward/core/nurture/NurtureAlg.scala +++ b/modules/core/src/main/scala/org/scalasteward/core/nurture/NurtureAlg.scala @@ -155,6 +155,18 @@ final class NurtureAlg[F[_]](config: Config)(implicit gitAlg.removeBranch(repo, branch) } + def ensureDistinctBranch( + data: UpdateData, + seenBranches: Ref[F, List[Branch]], + whenDistinct: F[ProcessResult] + ): F[ProcessResult] = + seenBranches.get + .flatMap(_.forallM(gitAlg.diff(data.repo, _).map(_.nonEmpty))) + .ifM( + whenDistinct, + logger.warn("Discovered a duplicate branch, not pushing").as(Ignored) + ) + def applyNewUpdate(data: UpdateData, seenBranches: Ref[F, List[Branch]]): F[ProcessResult] = gitAlg.returnToCurrentBranch(data.repo) { val createBranch = logger.info(s"Create branch ${data.updateBranch.name}") >> @@ -162,11 +174,10 @@ final class NurtureAlg[F[_]](config: Config)(implicit editAlg.applyUpdate(data.repoData, data.update, createBranch).flatMap { editCommits => if (editCommits.isEmpty) logger.warn("No commits created").as(Ignored) else - NurtureAlg.ensureDistinctBranch( + ensureDistinctBranch( + data, seenBranches, - gitAlg.diff(data.repo, _).map(_.nonEmpty), - pushCommits(data, editCommits) >> createPullRequest(data), - logger.warn("Discovered a duplicate branch, not pushing").as[ProcessResult](Ignored) + pushCommits(data, editCommits) >> createPullRequest(data) ) } } @@ -256,11 +267,10 @@ final class NurtureAlg[F[_]](config: Config)(implicit ) maybeMergeCommit <- gitAlg.mergeTheirs(data.repo, data.baseBranch) editCommits <- editAlg.applyUpdate(data.repoData, data.update) - result <- NurtureAlg.ensureDistinctBranch( + result <- ensureDistinctBranch( + data, seenBranches, - gitAlg.diff(data.repo, _).map(_.nonEmpty), - pushCommits(data, maybeMergeCommit.toList ++ editCommits), - logger.warn("Discovered a duplicate branch, not pushing").as[ProcessResult](Ignored) + pushCommits(data, maybeMergeCommit.toList ++ editCommits) ) } yield result } @@ -285,14 +295,4 @@ object NurtureAlg { .compile .drain } - - def ensureDistinctBranch[F[_]: Sync: cats.FlatMap]( - seenBranches: Ref[F, List[Branch]], - isDistinct: Branch => F[Boolean], - whenDistinct: F[ProcessResult], - whenDuplicate: F[ProcessResult] - ): F[ProcessResult] = - seenBranches.get - .flatMap(_.forallM(isDistinct)) - .ifM(whenDistinct, whenDuplicate) } diff --git a/modules/core/src/test/scala/org/scalasteward/core/TestInstances.scala b/modules/core/src/test/scala/org/scalasteward/core/TestInstances.scala index 908d9a5168..3e18ba239a 100644 --- a/modules/core/src/test/scala/org/scalasteward/core/TestInstances.scala +++ b/modules/core/src/test/scala/org/scalasteward/core/TestInstances.scala @@ -9,7 +9,7 @@ import org.scalacheck.{Arbitrary, Cogen, Gen} import org.scalasteward.core.TestSyntax._ import org.scalasteward.core.data.Update.Single import org.scalasteward.core.data._ -import org.scalasteward.core.git.{Branch, Sha1} +import org.scalasteward.core.git.Sha1 import org.scalasteward.core.git.Sha1.HexString import org.scalasteward.core.repocache.RepoCache import org.scalasteward.core.repoconfig.PullRequestFrequency.{Asap, Timespan} @@ -59,13 +59,6 @@ object TestInstances { } yield Single(groupId % artifactId % currentVersion, Nel.one(newerVersion)) ) - implicit val branchArbitrary: Arbitrary[Branch] = - Arbitrary( - for { - name <- Gen.alphaStr - } yield Branch(name) - ) - private val hashGen: Gen[String] = for { sep <- Gen.oneOf('-', '+') diff --git a/modules/core/src/test/scala/org/scalasteward/core/nurture/NurtureAlgTest.scala b/modules/core/src/test/scala/org/scalasteward/core/nurture/NurtureAlgTest.scala index 2f8d16f3a6..cdc80497d5 100644 --- a/modules/core/src/test/scala/org/scalasteward/core/nurture/NurtureAlgTest.scala +++ b/modules/core/src/test/scala/org/scalasteward/core/nurture/NurtureAlgTest.scala @@ -1,17 +1,13 @@ package org.scalasteward.core.nurture -import cats.Applicative import cats.data.StateT import cats.effect.IO -import cats.effect.concurrent.Ref -import cats.syntax.all._ import eu.timepit.refined.types.numeric.NonNegInt import munit.ScalaCheckSuite import org.scalacheck.Prop._ import org.scalasteward.core.TestInstances._ import org.scalasteward.core.data.ProcessResult.{Ignored, Updated} import org.scalasteward.core.data.{ProcessResult, Update} -import org.scalasteward.core.git.Branch class NurtureAlgTest extends ScalaCheckSuite { test("processUpdates with No Limiting") { @@ -46,29 +42,4 @@ class NurtureAlgTest extends ScalaCheckSuite { assertEquals(obtained, updates.size) } } - - test( - "Ensure distinct should not push branches that are not different to other visited branches" - ) { - forAll { branches: Set[Branch] => - val (duplicateBranches, distinctBranches) = branches.toList.splitAt(branches.size / 2) - type F[A] = StateT[IO, Int, A] - val F = Applicative[F] - val f: StateT[IO, Int, ProcessResult] = - StateT[IO, Int, ProcessResult](actionAcc => IO.pure(actionAcc + 1 -> Updated)) - val obtained = (for { - seenBranches <- Ref[F].of(duplicateBranches) - res <- (duplicateBranches ++ distinctBranches).traverse { branch => - NurtureAlg - .ensureDistinctBranch( - seenBranches, - _ => F.pure(distinctBranches.contains(branch)), - f, - F.pure[ProcessResult](Ignored) - ) - } - } yield res).runS(0).unsafeRunSync() - assertEquals(obtained, distinctBranches.length) - } - } } From 5e4e6b9279dff8f71af744fb6a5dabe3999c3aa9 Mon Sep 17 00:00:00 2001 From: Devon Stewart Date: Sat, 20 Mar 2021 13:56:15 -0700 Subject: [PATCH 6/9] Bubbling Ref out of instances of seenBranches --- .../core/nurture/NurtureAlg.scala | 75 +++++++++---------- 1 file changed, 35 insertions(+), 40 deletions(-) diff --git a/modules/core/src/main/scala/org/scalasteward/core/nurture/NurtureAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/nurture/NurtureAlg.scala index cd690c644b..c89eca5146 100644 --- a/modules/core/src/main/scala/org/scalasteward/core/nurture/NurtureAlg.scala +++ b/modules/core/src/main/scala/org/scalasteward/core/nurture/NurtureAlg.scala @@ -57,8 +57,7 @@ final class NurtureAlg[F[_]](config: Config)(implicit for { _ <- logger.info(s"Nurture ${data.repo.show}") baseBranch <- cloneAndSync(data.repo, fork) - seenBranches <- Ref[F].of(List.empty[Branch]) - _ <- updateDependencies(data, fork.repo, baseBranch, updates, seenBranches) + _ <- updateDependencies(data, fork.repo, baseBranch, updates) } yield () def cloneAndSync(repo: Repo, fork: RepoOut): F[Branch] = @@ -71,35 +70,40 @@ final class NurtureAlg[F[_]](config: Config)(implicit data: RepoData, fork: Repo, baseBranch: Branch, - updates: List[Update.Single], - seenBranches: Ref[F, List[Branch]] + updates: List[Update.Single] ): F[Unit] = for { _ <- F.unit grouped = Update.groupByGroupId(updates) _ <- logger.info(util.logger.showUpdates(grouped)) baseSha1 <- gitAlg.latestSha1(data.repo, baseBranch) + seenBranches <- Ref[F].of(List.empty[Branch]) _ <- NurtureAlg.processUpdates( grouped, update => { val updateData = UpdateData(data, fork, update, baseBranch, baseSha1, git.branchFor(update)) - processUpdate(updateData, seenBranches).flatMap { - case result @ Created(newPrNumber) => - (for { - _ <- closeObsoletePullRequests(updateData, newPrNumber) - _ <- seenBranches.update(updateData.updateBranch :: _) - } yield ()).as[ProcessResult](result) - case result @ Updated => - seenBranches.update(updateData.updateBranch :: _).as[ProcessResult](result) - case result @ Ignored => F.pure(result) - } + seenBranches + .getAndUpdate( + identity + ) // Suppress Codacity's faulty `.get` detection, https://twitter.com/blast_hardchese/status/1373376444827508737 + .flatMap(processUpdate(updateData, _)) + .flatMap { + case result @ Created(newPrNumber) => + (for { + _ <- closeObsoletePullRequests(updateData, newPrNumber) + _ <- seenBranches.update(updateData.updateBranch :: _) + } yield ()).as[ProcessResult](result) + case result @ Updated => + seenBranches.update(updateData.updateBranch :: _).as[ProcessResult](result) + case result @ Ignored => F.pure(result) + } }, data.config.updates.limit ) } yield () - def processUpdate(data: UpdateData, seenBranches: Ref[F, List[Branch]]): F[ProcessResult] = + def processUpdate(data: UpdateData, seenBranches: List[Branch]): F[ProcessResult] = for { _ <- logger.info(s"Process update ${data.update.show}") head = vcs.listingBranch(config.vcsType, data.fork, data.update) @@ -155,30 +159,19 @@ final class NurtureAlg[F[_]](config: Config)(implicit gitAlg.removeBranch(repo, branch) } - def ensureDistinctBranch( - data: UpdateData, - seenBranches: Ref[F, List[Branch]], - whenDistinct: F[ProcessResult] - ): F[ProcessResult] = - seenBranches.get - .flatMap(_.forallM(gitAlg.diff(data.repo, _).map(_.nonEmpty))) - .ifM( - whenDistinct, - logger.warn("Discovered a duplicate branch, not pushing").as(Ignored) - ) - - def applyNewUpdate(data: UpdateData, seenBranches: Ref[F, List[Branch]]): F[ProcessResult] = + def applyNewUpdate(data: UpdateData, seenBranches: List[Branch]): F[ProcessResult] = gitAlg.returnToCurrentBranch(data.repo) { val createBranch = logger.info(s"Create branch ${data.updateBranch.name}") >> gitAlg.createBranch(data.repo, data.updateBranch) editAlg.applyUpdate(data.repoData, data.update, createBranch).flatMap { editCommits => if (editCommits.isEmpty) logger.warn("No commits created").as(Ignored) else - ensureDistinctBranch( - data, - seenBranches, - pushCommits(data, editCommits) >> createPullRequest(data) - ) + seenBranches + .forallM(gitAlg.diff(data.repo, _).map(_.nonEmpty)) + .ifM( + pushCommits(data, editCommits) >> createPullRequest(data), + logger.warn("Discovered a duplicate branch, not pushing").as[ProcessResult](Ignored) + ) } } @@ -227,7 +220,7 @@ final class NurtureAlg[F[_]](config: Config)(implicit _ <- logger.info(s"Created PR ${pr.html_url}") } yield Created(pr.number) - def updatePullRequest(data: UpdateData, seenBranches: Ref[F, List[Branch]]): F[ProcessResult] = + def updatePullRequest(data: UpdateData, seenBranches: List[Branch]): F[ProcessResult] = if (data.repoConfig.updatePullRequestsOrDefault =!= PullRequestUpdateStrategy.Never) gitAlg.returnToCurrentBranch(data.repo) { for { @@ -260,18 +253,20 @@ final class NurtureAlg[F[_]](config: Config)(implicit result.flatMap { case (update, msg) => logger.info(msg).as(update) } } - def mergeAndApplyAgain(data: UpdateData, seenBranches: Ref[F, List[Branch]]): F[ProcessResult] = + def mergeAndApplyAgain(data: UpdateData, seenBranches: List[Branch]): F[ProcessResult] = for { _ <- logger.info( s"Merge branch ${data.baseBranch.name} into ${data.updateBranch.name} and apply again" ) maybeMergeCommit <- gitAlg.mergeTheirs(data.repo, data.baseBranch) editCommits <- editAlg.applyUpdate(data.repoData, data.update) - result <- ensureDistinctBranch( - data, - seenBranches, - pushCommits(data, maybeMergeCommit.toList ++ editCommits) - ) + result <- + seenBranches + .forallM(gitAlg.diff(data.repo, _).map(_.nonEmpty)) + .ifM( + pushCommits(data, maybeMergeCommit.toList ++ editCommits), + logger.warn("Discovered a duplicate branch, not pushing").as[ProcessResult](Ignored) + ) } yield result } From 98a386c2203338f7d019807bcb92d30eb3ca5e1c Mon Sep 17 00:00:00 2001 From: Devon Stewart Date: Sun, 21 Mar 2021 17:04:15 -0700 Subject: [PATCH 7/9] Break out ApplyAlg --- .../core/application/Context.scala | 4 +- .../scalasteward/core/nurture/ApplyAlg.scala | 73 +++++++++++++++++++ .../core/nurture/NurtureAlg.scala | 39 +--------- 3 files changed, 80 insertions(+), 36 deletions(-) create mode 100644 modules/core/src/main/scala/org/scalasteward/core/nurture/ApplyAlg.scala diff --git a/modules/core/src/main/scala/org/scalasteward/core/application/Context.scala b/modules/core/src/main/scala/org/scalasteward/core/application/Context.scala index 8d4499451c..e01d27f89b 100644 --- a/modules/core/src/main/scala/org/scalasteward/core/application/Context.scala +++ b/modules/core/src/main/scala/org/scalasteward/core/application/Context.scala @@ -32,7 +32,7 @@ import org.scalasteward.core.edit.hooks.HookExecutor import org.scalasteward.core.edit.scalafix.{ScalafixMigrationsFinder, ScalafixMigrationsLoader} import org.scalasteward.core.git.{GenGitAlg, GitAlg} import org.scalasteward.core.io.{FileAlg, ProcessAlg, WorkspaceAlg} -import org.scalasteward.core.nurture.{NurtureAlg, PullRequestData, PullRequestRepository} +import org.scalasteward.core.nurture.{ApplyAlg, NurtureAlg, PullRequestData, PullRequestRepository} import org.scalasteward.core.persistence.{CachingKeyValueStore, JsonKeyValueStore} import org.scalasteward.core.repocache._ import org.scalasteward.core.repoconfig.RepoConfigAlg @@ -47,6 +47,7 @@ import org.typelevel.log4cats.Logger import org.typelevel.log4cats.slf4j.Slf4jLogger final class Context[F[_]](implicit + val applyAlg: ApplyAlg[F], val buildToolDispatcher: BuildToolDispatcher[F], val coursierAlg: CoursierAlg[F], val dateTimeAlg: DateTimeAlg[F], @@ -146,6 +147,7 @@ object Context { implicit val refreshErrorAlg: RefreshErrorAlg[F] = new RefreshErrorAlg[F](refreshErrorStore) implicit val repoCacheAlg: RepoCacheAlg[F] = new RepoCacheAlg[F](config) implicit val editAlg: EditAlg[F] = new EditAlg[F] + implicit val applyAlg: ApplyAlg[F] = new ApplyAlg[F] implicit val nurtureAlg: NurtureAlg[F] = new NurtureAlg[F](config) implicit val pruningAlg: PruningAlg[F] = new PruningAlg[F] implicit val gitHubAppApiAlg: GitHubAppApiAlg[F] = new GitHubAppApiAlg[F](config.vcsApiHost) diff --git a/modules/core/src/main/scala/org/scalasteward/core/nurture/ApplyAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/nurture/ApplyAlg.scala new file mode 100644 index 0000000000..260849ff21 --- /dev/null +++ b/modules/core/src/main/scala/org/scalasteward/core/nurture/ApplyAlg.scala @@ -0,0 +1,73 @@ +/* + * Copyright 2018-2021 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.nurture + +import cats.effect.BracketThrow +import cats.implicits._ +import org.typelevel.log4cats.Logger +import org.scalasteward.core.data.ProcessResult.Ignored +import org.scalasteward.core.data._ +import org.scalasteward.core.edit.EditAlg +import org.scalasteward.core.git.{Branch, Commit, GitAlg} + +final class ApplyAlg[F[_]](implicit + editAlg: EditAlg[F], + gitAlg: GitAlg[F], + logger: Logger[F], + F: BracketThrow[F] +) { + def applyNewUpdate( + data: UpdateData, + seenBranches: List[Branch], + pushCommits: (UpdateData, List[Commit]) => F[ProcessResult], + createPullRequest: UpdateData => F[ProcessResult] + ): F[ProcessResult] = + gitAlg.returnToCurrentBranch(data.repo) { + val createBranch = logger.info(s"Create branch ${data.updateBranch.name}") >> + gitAlg.createBranch(data.repo, data.updateBranch) + editAlg.applyUpdate(data.repoData, data.update, createBranch).flatMap { editCommits => + if (editCommits.isEmpty) logger.warn("No commits created").as(Ignored) + else + seenBranches + .forallM(gitAlg.diff(data.repo, _).map(_.nonEmpty)) + .ifM( + pushCommits(data, editCommits) >> createPullRequest(data), + logger.warn("Discovered a duplicate branch, not pushing").as[ProcessResult](Ignored) + ) + } + } + + def mergeAndApplyAgain( + data: UpdateData, + seenBranches: List[Branch], + pushCommits: (UpdateData, List[Commit]) => F[ProcessResult] + ): F[ProcessResult] = + for { + _ <- logger.info( + s"Merge branch ${data.baseBranch.name} into ${data.updateBranch.name} and apply again" + ) + maybeMergeCommit <- gitAlg.mergeTheirs(data.repo, data.baseBranch) + editCommits <- editAlg.applyUpdate(data.repoData, data.update) + result <- + seenBranches + .forallM(gitAlg.diff(data.repo, _).map(_.nonEmpty)) + .ifM( + pushCommits(data, maybeMergeCommit.toList ++ editCommits), + logger.warn("Discovered a duplicate branch, not pushing").as[ProcessResult](Ignored) + ) + } yield result +} diff --git a/modules/core/src/main/scala/org/scalasteward/core/nurture/NurtureAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/nurture/NurtureAlg.scala index c89eca5146..9f8069eabf 100644 --- a/modules/core/src/main/scala/org/scalasteward/core/nurture/NurtureAlg.scala +++ b/modules/core/src/main/scala/org/scalasteward/core/nurture/NurtureAlg.scala @@ -28,7 +28,6 @@ import org.scalasteward.core.application.Config import org.scalasteward.core.coursier.CoursierAlg import org.scalasteward.core.data.ProcessResult.{Created, Ignored, Updated} import org.scalasteward.core.data._ -import org.scalasteward.core.edit.EditAlg import org.scalasteward.core.git.{Branch, Commit, GitAlg} import org.scalasteward.core.repoconfig.PullRequestUpdateStrategy import org.scalasteward.core.edit.scalafix.ScalafixMigrationsFinder @@ -40,7 +39,6 @@ import org.scalasteward.core.{git, util, vcs} final class NurtureAlg[F[_]](config: Config)(implicit coursierAlg: CoursierAlg[F], - editAlg: EditAlg[F], gitAlg: GitAlg[F], logger: Logger[F], pullRequestRepository: PullRequestRepository[F], @@ -50,6 +48,7 @@ final class NurtureAlg[F[_]](config: Config)(implicit vcsRepoAlg: VCSRepoAlg[F], streamCompiler: Stream.Compiler[F, F], urlChecker: UrlChecker[F], + applyAlg: ApplyAlg[F], F: BracketThrow[F], FS: Sync[F] ) { @@ -115,7 +114,7 @@ final class NurtureAlg[F[_]](config: Config)(implicit case Some(pr) => logger.info(s"Found PR ${pr.html_url}") >> updatePullRequest(data, seenBranches) case None => - applyNewUpdate(data, seenBranches) + applyAlg.applyNewUpdate(data, seenBranches, pushCommits, createPullRequest) } _ <- pullRequests.headOption.traverse_ { pr => pullRequestRepository.createOrUpdate( @@ -159,22 +158,6 @@ final class NurtureAlg[F[_]](config: Config)(implicit gitAlg.removeBranch(repo, branch) } - def applyNewUpdate(data: UpdateData, seenBranches: List[Branch]): F[ProcessResult] = - gitAlg.returnToCurrentBranch(data.repo) { - val createBranch = logger.info(s"Create branch ${data.updateBranch.name}") >> - gitAlg.createBranch(data.repo, data.updateBranch) - editAlg.applyUpdate(data.repoData, data.update, createBranch).flatMap { editCommits => - if (editCommits.isEmpty) logger.warn("No commits created").as(Ignored) - else - seenBranches - .forallM(gitAlg.diff(data.repo, _).map(_.nonEmpty)) - .ifM( - pushCommits(data, editCommits) >> createPullRequest(data), - logger.warn("Discovered a duplicate branch, not pushing").as[ProcessResult](Ignored) - ) - } - } - def pushCommits(data: UpdateData, commits: List[Commit]): F[ProcessResult] = if (commits.isEmpty) F.pure[ProcessResult](Ignored) else @@ -227,7 +210,8 @@ final class NurtureAlg[F[_]](config: Config)(implicit _ <- gitAlg.checkoutBranch(data.repo, data.updateBranch) update <- shouldBeUpdated(data) result <- - if (update) mergeAndApplyAgain(data, seenBranches) else F.pure[ProcessResult](Ignored) + if (update) applyAlg.mergeAndApplyAgain(data, seenBranches, pushCommits) + else F.pure[ProcessResult](Ignored) } yield result } else @@ -253,21 +237,6 @@ final class NurtureAlg[F[_]](config: Config)(implicit result.flatMap { case (update, msg) => logger.info(msg).as(update) } } - def mergeAndApplyAgain(data: UpdateData, seenBranches: List[Branch]): F[ProcessResult] = - for { - _ <- logger.info( - s"Merge branch ${data.baseBranch.name} into ${data.updateBranch.name} and apply again" - ) - maybeMergeCommit <- gitAlg.mergeTheirs(data.repo, data.baseBranch) - editCommits <- editAlg.applyUpdate(data.repoData, data.update) - result <- - seenBranches - .forallM(gitAlg.diff(data.repo, _).map(_.nonEmpty)) - .ifM( - pushCommits(data, maybeMergeCommit.toList ++ editCommits), - logger.warn("Discovered a duplicate branch, not pushing").as[ProcessResult](Ignored) - ) - } yield result } object NurtureAlg { From 952ed264009784ea9a84294a22f12d03b53b3b54 Mon Sep 17 00:00:00 2001 From: Devon Stewart Date: Mon, 29 Mar 2021 11:26:37 -0700 Subject: [PATCH 8/9] Add ensureExecutable to FileAlg --- .../scala/org/scalasteward/core/io/FileAlg.scala | 14 ++++++++++++++ .../org/scalasteward/core/io/MockFileAlg.scala | 5 +++++ 2 files changed, 19 insertions(+) diff --git a/modules/core/src/main/scala/org/scalasteward/core/io/FileAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/io/FileAlg.scala index 5f90e89493..5141cd1803 100644 --- a/modules/core/src/main/scala/org/scalasteward/core/io/FileAlg.scala +++ b/modules/core/src/main/scala/org/scalasteward/core/io/FileAlg.scala @@ -26,6 +26,7 @@ import org.http4s.Uri import org.http4s.implicits.http4sLiteralsSyntax import org.typelevel.log4cats.Logger import scala.io.Source +import java.nio.file.attribute.PosixFilePermission trait FileAlg[F[_]] { def deleteForce(file: File): F[Unit] @@ -50,6 +51,8 @@ trait FileAlg[F[_]] { def writeFile(file: File, content: String): F[Unit] + def ensureExecutable(file: File): F[Unit] + final def createTemporarily[A, E](file: File, content: String)( fa: F[A] )(implicit F: Bracket[F, E]): F[A] = { @@ -147,5 +150,16 @@ object FileAlg { logger.debug(s"Write $file") >> file.parentOption.fold(F.unit)(ensureExists(_).void) >> F.delay(file.write(content)).void + + override def ensureExecutable(file: File): F[Unit] = + F.delay( + file.setPermissions( + file.permissions ++ Set( + PosixFilePermission.OWNER_EXECUTE, + PosixFilePermission.GROUP_EXECUTE, + PosixFilePermission.OTHERS_EXECUTE + ) + ) + ) } } diff --git a/modules/core/src/test/scala/org/scalasteward/core/io/MockFileAlg.scala b/modules/core/src/test/scala/org/scalasteward/core/io/MockFileAlg.scala index 7f7ba79c51..56e42f441f 100644 --- a/modules/core/src/test/scala/org/scalasteward/core/io/MockFileAlg.scala +++ b/modules/core/src/test/scala/org/scalasteward/core/io/MockFileAlg.scala @@ -66,4 +66,9 @@ class MockFileAlg extends FileAlg[MockEff] { StateT.modifyF[IO, MockState]( _.exec(List("write", file.pathAsString)).addFiles(file -> content) ) + + override def ensureExecutable(file: File): MockEff[Unit] = + StateT.modify[IO, MockState]( + _.exec(List("chmod", "u+x,g+x,o+x", file.pathAsString)) + ) >> StateT.liftF(ioFileAlg.ensureExecutable(file)) } From b1c949d34da76e3b49df6369d5d2b27aed0e2eca Mon Sep 17 00:00:00 2001 From: Devon Stewart Date: Mon, 22 Mar 2021 23:16:10 -0700 Subject: [PATCH 9/9] Wrote integration-style test for ApplyAlg --- .../core/nurture/ApplyAlgTest.scala | 193 ++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 modules/core/src/test/scala/org/scalasteward/core/nurture/ApplyAlgTest.scala diff --git a/modules/core/src/test/scala/org/scalasteward/core/nurture/ApplyAlgTest.scala b/modules/core/src/test/scala/org/scalasteward/core/nurture/ApplyAlgTest.scala new file mode 100644 index 0000000000..8a33507807 --- /dev/null +++ b/modules/core/src/test/scala/org/scalasteward/core/nurture/ApplyAlgTest.scala @@ -0,0 +1,193 @@ +package org.scalasteward.core.nurture + +import munit.ScalaCheckSuite +import org.scalasteward.core.TestInstances._ +import org.scalasteward.core.data.{ProcessResult, RepoData, Update, UpdateData} + +import better.files.File +import cats.Applicative +import cats.effect._ +import cats.effect.concurrent.Ref +import org.http4s.HttpApp +import org.http4s.client.Client +import org.scalasteward.core.TestSyntax._ +import org.scalasteward.core.application.{Config, Context} +import org.scalasteward.core.git.FileGitAlgTest.{master, Supplement} +import org.scalasteward.core.git.{Branch, Commit, FileGitAlg, GenGitAlg, Sha1} +import org.scalasteward.core.io.{FileAlg, ProcessAlg, WorkspaceAlg} +import org.scalasteward.core.mock.MockContext +import org.scalasteward.core.mock.MockContext.{config, mockRoot} +import org.scalasteward.core.repocache._ +import org.scalasteward.core.repoconfig.RepoConfig +import org.scalasteward.core.util.Nel +import org.scalasteward.core.vcs.data.Repo + +class ApplyAlgTest extends ScalaCheckSuite { + def step0(implicit + CE: ConcurrentEffect[IO] + ): Resource[IO, (ProcessAlg[IO], FileAlg[IO], WorkspaceAlg[IO], Context[IO])] = for { + blocker <- Blocker[IO] + config = Config.from(MockContext.args) + implicit0(client: Client[IO]) = Client.fromHttpApp[IO](HttpApp.notFound) + implicit0(fileAlg: FileAlg[IO]) = FileAlg.create[IO] + _ <- Resource.eval(fileAlg.ensureExists(config.gitCfg.gitAskPass.parent)) + _ <- Resource.eval( + fileAlg.writeFile( + config.gitCfg.gitAskPass, + """ echo bogus-password """ + ) + ) + _ <- Resource.eval(fileAlg.ensureExecutable(config.gitCfg.gitAskPass)) + implicit0(processAlg: ProcessAlg[IO]) = ProcessAlg.create[IO](blocker, config.processCfg) + implicit0(workspaceAlg: WorkspaceAlg[IO]) = WorkspaceAlg.create[IO](config) + context <- Resource.eval(Context.step1[IO](config)) + } yield (processAlg, fileAlg, workspaceAlg, context) + + def setupRepo(repo: Repo, identicalBranch: (Branch, Update.Single)): IO[Unit] = + step0.use { + case ( + implicit0(processAlg: ProcessAlg[IO]), + implicit0(fileAlg: FileAlg[IO]), + implicit0(workspaceAlg: WorkspaceAlg[IO]), + context + ) => + val (branch, update) = identicalBranch + implicit val ioGitAlg: GenGitAlg[IO, File] = + new FileGitAlg[IO](config.gitCfg).contramapRepoF(Applicative[IO].pure) + val supplement = new Supplement[IO] + val repoDir = mockRoot / "workspace" / "repos" / repo.owner / repo.repo + for { + _ <- supplement.createRepo(repoDir) + _ <- fileAlg.writeFile( + repoDir / "build.sbt", + """libraryDependency += "foo" % "bar" % "1.2.3" """ + ) + _ <- supplement.git("add", "build.sbt")(repoDir) + _ <- context.gitAlg.commitAll(repo, "Initial commit") + // Create another simulated curated update branch with + _ <- context.gitAlg.createBranch(repo, branch) + _ <- fileAlg.writeFile( + repoDir / "build.sbt", + s"""libraryDependency += "foo" % "bar" % "${update.newerVersions.head}" """ + ) + _ <- supplement.git("add", "build.sbt")(repoDir) + _ <- context.gitAlg.commitAll(repo, "Update bar to 1.2.4") + _ <- context.gitAlg.checkoutBranch(repo, master) + } yield () + } + + test("Ensure unique patchesets are pushed") { + val firstBranch = Branch("update/foo-1.2.4") + val duplicateBranch = Branch("update/foo-duplicate-1.2.4") + val update = Update.Single("foo" % "bar" % "1.2.3", Nel.one("1.2.4")) + val firstChangeset = (firstBranch, update) + val res = ({ + def pushCommits( + seenBranchesRef: Ref[IO, List[Branch]] + ): (UpdateData, List[Commit]) => IO[ProcessResult] = { (data, _) => + for { + _ <- seenBranchesRef.update(data.updateBranch :: _) + } yield ProcessResult.Updated + } + + val createPullRequest: UpdateData => IO[ProcessResult] = _ => IO.pure(ProcessResult.Updated) + + val repo = Repo("myorg", "myrepo") + val fork = Repo("myfork", "myrepo") + step0.use { + case ( + implicit0(processAlg: ProcessAlg[IO]), + implicit0(fileAlg: FileAlg[IO]), + implicit0(workspaceAlg: WorkspaceAlg[IO]), + context + ) => + for { + _ <- setupRepo(repo, firstChangeset) + seenBranchesRef <- Ref[IO].of(List.empty[Branch]) + sha1 <- IO.fromEither(Sha1.from("adc83b19e793491b1c6ea0fd8b46cd9f32e592fc")) + firstData = UpdateData( + RepoData( + repo, + RepoCache(sha1, List.empty, Option.empty), + RepoConfig() + ), + fork, + update, + master, + sha1, + Branch("bump") + ) + secondData = firstData.copy( + updateBranch = duplicateBranch, + update = update + ) + seenBranches <- seenBranchesRef.getAndUpdate(identity _) + res1 <- context.applyAlg.applyNewUpdate( + firstData, + seenBranches, + pushCommits(seenBranchesRef), + createPullRequest + ) + seenBranches <- seenBranchesRef.getAndUpdate(identity _) + res2 <- context.applyAlg.applyNewUpdate( + secondData, + seenBranches, + pushCommits(seenBranchesRef), + createPullRequest + ) + } yield (res1, res2) + } + }).unsafeRunSync() + + assertEquals(res, (ProcessResult.Updated, ProcessResult.Ignored)) + } + + test("Ensure non-unique patchesets are not pushed") { + val branch = Branch("update/foo-1.2.4") + val update = Update.Single("foo" % "bar" % "1.2.3", Nel.one("1.2.4")) + val identicalBranch = (branch, update) + val res = ({ + val pushCommits: (UpdateData, List[Commit]) => IO[ProcessResult] = + (_, _) => IO.pure(ProcessResult.Updated) + + val createPullRequest: UpdateData => IO[ProcessResult] = _ => IO.pure(ProcessResult.Updated) + + val repo = Repo("myorg", "myrepo") + val fork = Repo("myfork", "myrepo") + step0.use { + case ( + implicit0(processAlg: ProcessAlg[IO]), + implicit0(fileAlg: FileAlg[IO]), + implicit0(workspaceAlg: WorkspaceAlg[IO]), + context + ) => + for { + _ <- setupRepo(repo, identicalBranch) + seenBranchesRef <- Ref[IO].of(List(branch)) + sha1 <- IO.fromEither(Sha1.from("adc83b19e793491b1c6ea0fd8b46cd9f32e592fc")) + data = UpdateData( + RepoData( + repo, + RepoCache(sha1, List.empty, Option.empty), + RepoConfig() + ), + fork, + update, + master, + sha1, + Branch("bump") + ) + seenBranches <- seenBranchesRef.getAndUpdate(identity _) + res <- context.applyAlg.applyNewUpdate( + data, + seenBranches, + pushCommits, + createPullRequest + ) + } yield res + } + }).unsafeRunSync() + + assertEquals(res, ProcessResult.Ignored) + } +}