Repository: scala-steward-org/scala-steward (scala)

Issue:
Updated PRs could be broken after merge
https://github.com/typelevel/literally/pull/24 is a PR that was updated because of a merge conflict. The merge conflict was in `.github/workflows/ci.yml` which was changed by Scala Steward via a post-update hook. After the base branch was merged into the update branch, `EditAlg#applyUpdate` ran again but didn't produced any new commits because [the old version could not be found anymore](https://github.com/scala-steward-org/scala-steward/blob/062082487800ecdb172fe710acc1a72c052ba62b/modules/core/src/main/scala/org/scalasteward/core/edit/EditAlg.scala#L58). This broke the CI since `.github/workflows/ci.yml` now contained changes from the first post-update hook and the merge. It could be fixed by running the post-update hook again after the merge.

To fix issues like this, I propose to reset the update branch (i.e. reverting all changes by Scala Steward) before merging in the base branch. By reverting Scala Steward's prior changes and then merging the base branch, the update branch has no diff compared to the head of the base branch. Running `EditAlg#applyUpdate` then again will update the version number and run Scalafix migrations and post-update hooks. The changes Scala Steward would then make are the same as if it would create a completely new PR but on the old update branch.

Pull request description:
Closes #2503.

Patch:
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 b2e3f0aeb3..037c6dd675 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
@@ -95,7 +95,7 @@ final class FileGitAlg[F[_]](config: GitCfg)(implicit
     git("submodule", "update", "--init", "--recursive")(repo).void
 
   override def isMerged(repo: File, branch: Branch, base: Branch): F[Boolean] =
-    git("log", "--pretty=format:'%h'", dotdot(base, branch))(repo).map(_.isEmpty)
+    git("log", "--pretty=format:%h", dotdot(base, branch))(repo).map(_.isEmpty)
 
   override def latestSha1(repo: File, branch: Branch): F[Sha1] =
     git("rev-parse", "--verify", branch.name)(repo)
@@ -124,6 +124,19 @@ final class FileGitAlg[F[_]](config: GitCfg)(implicit
   override def removeClone(repo: File): F[Unit] =
     fileAlg.deleteForce(repo)
 
+  override def revertChanges(repo: File, base: Branch): F[Option[Commit]] = {
+    val range = dotdot(base, Branch.head)
+    git("log", "--pretty=format:%h %p", range)(repo).flatMap { commitsWithParents =>
+      val commitsUntilMerge = commitsWithParents.map(_.split(' ')).takeWhile(_.length === 2)
+      val commits = commitsUntilMerge.flatMap(_.headOption)
+      if (commits.isEmpty) F.pure(None)
+      else {
+        val msg = CommitMsg(s"Revert commit(s) " + commits.mkString(", "))
+        git("revert" :: "--no-commit" :: commits: _*)(repo) >> commitAllIfDirty(repo, msg)
+      }
+    }
+  }
+
   override def setAuthor(repo: File, author: Author): F[Unit] =
     for {
       _ <- git("config", "user.email", author.email)(repo)
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 ab79276985..1f530d2fe6 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 removeClone(repo: Repo): F[Unit]
 
+  def revertChanges(repo: Repo, base: Branch): F[Option[Commit]]
+
   def setAuthor(repo: Repo, author: Author): F[Unit]
 
   def syncFork(repo: Repo, upstreamUrl: Uri, defaultBranch: Branch): F[Unit]
@@ -149,6 +151,9 @@ trait GenGitAlg[F[_], Repo] {
       override def removeClone(repo: A): F[Unit] =
         f(repo).flatMap(self.removeClone)
 
+      override def revertChanges(repo: A, base: Branch): F[Option[Commit]] =
+        f(repo).flatMap(self.revertChanges(_, base))
+
       override def setAuthor(repo: A, author: Author): F[Unit] =
         f(repo).flatMap(self.setAuthor(_, author))
 
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 95f78ab4a9..828f077e0f 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
@@ -244,9 +244,11 @@ final class NurtureAlg[F[_]](config: VCSCfg)(implicit
       _ <- logger.info(
         s"Merge branch ${data.baseBranch.name} into ${data.updateBranch.name} and apply again"
       )
+      maybeRevertCommit <- gitAlg.revertChanges(data.repo, data.baseBranch)
       maybeMergeCommit <- gitAlg.mergeTheirs(data.repo, data.baseBranch)
       edits <- editAlg.applyUpdate(data.repoData, data.update)
       editCommits = edits.flatMap(_.maybeCommit)
-      result <- pushCommits(data, maybeMergeCommit.toList ++ editCommits)
+      commits = maybeRevertCommit.toList ++ maybeMergeCommit.toList ++ editCommits
+      result <- pushCommits(data, commits)
     } yield result
 }


Test patch:
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 8dbfc1f2bd..41cfe4b243 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
@@ -168,6 +168,22 @@ class FileGitAlgTest extends CatsEffectSuite {
     } yield ()
   }
 
+  test("revertChanges") {
+    val repo = rootDir / "revertChanges"
+    for {
+      _ <- ioAuxGitAlg.createRepo(repo)
+      _ <- ioAuxGitAlg.createConflict(repo)
+      branch = Branch("conflicts-yes")
+      c1 <- ioGitAlg.hasConflicts(repo, branch, master)
+      d1 <- ioGitAlg.branchesDiffer(repo, master, branch)
+      _ <- ioGitAlg.checkoutBranch(repo, branch)
+      _ <- ioGitAlg.revertChanges(repo, master)
+      c2 <- ioGitAlg.hasConflicts(repo, branch, master)
+      d2 <- ioGitAlg.branchesDiffer(repo, master, branch)
+      _ = assertEquals((c1, d1, c2, d2), (true, true, false, true))
+    } yield ()
+  }
+
   test("version") {
     assertIOBoolean(ioGitAlg.version.map(_.nonEmpty))
   }