Repository: scala-steward-org/scala-steward
Pull request #2135 (2021-05-22 21:35:24)

Support reading and updating .mill-version file
This PR adds support to read and update `.mill-version` files.

----
Initial but outdate comment:

_This is a draft, as the feature isn't complete yet_

@fthomas Thanks for the detailed steps in https://github.com/scala-steward-org/scala-steward/issues/1504#issuecomment-832950344. I managed to implement step 1 and 2, but fail at step 3. I think I'm not familiar enough with the used APIs. I could imagine it's just a finger exercise for you, in which case I'd be glad if you could help me out with a snippet. Please don't take it as a demand, but probably it speeds things up, at least on my side. ;-) Thank you!

I tried to add tests for step 1 and 2, but have no idea how to test step 3.



codecov[bot]: # [Codecov](https://codecov.io/gh/scala-steward-org/scala-steward/pull/2135?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scala-steward-org) Report
> Merging [#2135](https://codecov.io/gh/scala-steward-org/scala-steward/pull/2135?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scala-steward-org) (e04a7c5) into [master](https://codecov.io/gh/scala-steward-org/scala-steward/commit/f1fddce7d8d16439ffabf61e56b596878267b63e?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scala-steward-org) (f1fddce) will **decrease** coverage by `0.01%`.
> The diff coverage is `78.57%`.

[![Impacted file tree graph](https://codecov.io/gh/scala-steward-org/scala-steward/pull/2135/graphs/tree.svg?width=650&height=150&src=pr&token=udzMsc1Dne&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scala-steward-org)](https://codecov.io/gh/scala-steward-org/scala-steward/pull/2135?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scala-steward-org)

```diff
@@            Coverage Diff             @@
##           master    #2135      +/-   ##
==========================================
- Coverage   78.16%   78.15%   -0.02%     
==========================================
  Files         131      131              
  Lines        2249     2261      +12     
  Branches       57       63       +6     
==========================================
+ Hits         1758     1767       +9     
- Misses        491      494       +3     
```


| [Impacted Files](https://codecov.io/gh/scala-steward-org/scala-steward/pull/2135?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scala-steward-org) | Coverage Δ | |
|---|---|---|
| [...a/org/scalasteward/core/edit/UpdateHeuristic.scala](https://codecov.io/gh/scala-steward-org/scala-steward/pull/2135/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scala-steward-org#diff-bW9kdWxlcy9jb3JlL3NyYy9tYWluL3NjYWxhL29yZy9zY2FsYXN0ZXdhcmQvY29yZS9lZGl0L1VwZGF0ZUhldXJpc3RpYy5zY2FsYQ==) | `98.80% <ø> (ø)` | |
| [...org/scalasteward/core/buildtool/mill/MillAlg.scala](https://codecov.io/gh/scala-steward-org/scala-steward/pull/2135/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scala-steward-org#diff-bW9kdWxlcy9jb3JlL3NyYy9tYWluL3NjYWxhL29yZy9zY2FsYXN0ZXdhcmQvY29yZS9idWlsZHRvb2wvbWlsbC9NaWxsQWxnLnNjYWxh) | `81.81% <75.00%> (-9.10%)` | :arrow_down: |
| [.../org/scalasteward/core/buildtool/mill/parser.scala](https://codecov.io/gh/scala-steward-org/scala-steward/pull/2135/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scala-steward-org#diff-bW9kdWxlcy9jb3JlL3NyYy9tYWluL3NjYWxhL29yZy9zY2FsYXN0ZXdhcmQvY29yZS9idWlsZHRvb2wvbWlsbC9wYXJzZXIuc2NhbGE=) | `68.18% <100.00%> (+1.51%)` | :arrow_up: |
| [...g/scalasteward/core/repoconfig/UpdatesConfig.scala](https://codecov.io/gh/scala-steward-org/scala-steward/pull/2135/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scala-steward-org#diff-bW9kdWxlcy9jb3JlL3NyYy9tYWluL3NjYWxhL29yZy9zY2FsYXN0ZXdhcmQvY29yZS9yZXBvY29uZmlnL1VwZGF0ZXNDb25maWcuc2NhbGE=) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/scala-steward-org/scala-steward/pull/2135?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scala-steward-org).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scala-steward-org)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/scala-steward-org/scala-steward/pull/2135?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scala-steward-org). Last update [f1fddce...e04a7c5](https://codecov.io/gh/scala-steward-org/scala-steward/pull/2135?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scala-steward-org). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scala-steward-org).

fthomas: For step 3 https://github.com/scala-steward-org/scala-steward/blob/9c921461694d68ce1d65a9e3ce36c6050b7e0837/modules/core/src/main/scala/org/scalasteward/core/edit/UpdateHeuristic.scala#L209-L216
needs to be changed to something like this:
```scala
  val specific: UpdateHeuristic = UpdateHeuristic(
    name = "specific",
    replaceVersion = {
      case update: Update.Single if isScalafmtUpdate(update) =>
        defaultReplaceVersion(_ => List("version"))(update)
      case update: Update.Single if isMillUpdate(update) =>
        content => if (content === update.currentVersion) Some(update.nextVersion) else None
      case _ =>
        _ => None
    }
```

You could then test it with a test similar to this one: https://github.com/scala-steward-org/scala-steward/blob/9c921461694d68ce1d65a9e3ce36c6050b7e0837/modules/core/src/test/scala/org/scalasteward/core/edit/EditAlgTest.scala#L168-L179
lefou: Thank you, @fthomas ! I finally found time to complete this feature, thanks to your detailed help!

Patch:
diff --git a/modules/core/src/main/scala/org/scalasteward/core/buildtool/mill/MillAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/buildtool/mill/MillAlg.scala
index d5a5f839..f640412c 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/buildtool/mill/MillAlg.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/buildtool/mill/MillAlg.scala
@@ -16,10 +16,11 @@
 
 package org.scalasteward.core.buildtool.mill
 
+import better.files.File
 import cats.effect.MonadCancelThrow
 import cats.syntax.all._
 import org.scalasteward.core.buildtool.BuildToolAlg
-import org.scalasteward.core.data.Scope
+import org.scalasteward.core.data.{ArtifactId, Dependency, GroupId, Resolver, Scope, Update}
 import org.scalasteward.core.data.Scope.Dependencies
 import org.scalasteward.core.edit.scalafix.ScalafixMigration
 import org.scalasteward.core.io.{FileAlg, ProcessAlg, WorkspaceAlg}
@@ -64,9 +65,32 @@ object MillAlg {
           parsed <- F.fromEither(
             parser.parseModules(extracted.dropWhile(!_.startsWith("{")).mkString("\n"))
           )
-        } yield parsed.map(module => Scope(module.dependencies, module.repositories))
+          dependencies = parsed.map(module => Scope(module.dependencies, module.repositories))
+          millBuildVersion <- getMillVersion(buildRootDir)
+          millBuildDeps = millBuildVersion.toSeq.map(version =>
+            Scope(List(millMainArtifact(version)), List(millMainResolver))
+          )
+        } yield dependencies ++ millBuildDeps
 
       override def runMigration(buildRoot: BuildRoot, migration: ScalafixMigration): F[Unit] =
         F.unit
+
+      def getMillVersion(buildRootDir: File): F[Option[String]] =
+        for {
+          millVersionFileContent <- fileAlg.readFile(buildRootDir / ".mill-version")
+          version = millVersionFileContent.flatMap(parser.parseMillVersion)
+        } yield version
+
     }
+
+  private[this] val millMainResolver: Resolver = Resolver.mavenCentral
+  private[this] val millMainGroupId = GroupId("com.lihaoyi")
+  private[this] val millMainArtifactId = ArtifactId("mill-main", "mill-main_2.13")
+
+  private def millMainArtifact(version: String): Dependency =
+    Dependency(millMainGroupId, millMainArtifactId, version)
+
+  def isMillMainUpdate(update: Update.Single): Boolean =
+    update.groupId === millMainGroupId && update.artifactId.name === millMainArtifactId.name
+
 }
diff --git a/modules/core/src/main/scala/org/scalasteward/core/buildtool/mill/parser.scala b/modules/core/src/main/scala/org/scalasteward/core/buildtool/mill/parser.scala
index c10825e2..a7e4dae7 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/buildtool/mill/parser.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/buildtool/mill/parser.scala
@@ -40,6 +40,9 @@ object parser {
           .leftMap(CirceParseError("Failed to decode Modules", _): ParseError)
     } yield json.modules
 
+  def parseMillVersion(s: String): Option[String] =
+    Option(s.trim()).filter(_.nonEmpty)
+
 }
 
 case class Modules(modules: List[MillModule])
diff --git a/modules/core/src/main/scala/org/scalasteward/core/edit/UpdateHeuristic.scala b/modules/core/src/main/scala/org/scalasteward/core/edit/UpdateHeuristic.scala
index d458dc7d..14aa5dc5 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/edit/UpdateHeuristic.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/edit/UpdateHeuristic.scala
@@ -18,10 +18,12 @@ package org.scalasteward.core.edit
 
 import cats.Foldable
 import cats.syntax.all._
+import org.scalasteward.core.buildtool.mill.MillAlg
 import org.scalasteward.core.data.Update
 import org.scalasteward.core.scalafmt.isScalafmtUpdate
 import org.scalasteward.core.util
 import org.scalasteward.core.util.Nel
+
 import scala.util.matching.Regex
 
 /** `UpdateHeuristic` is a wrapper for a function that takes an `Update` and
@@ -211,6 +213,8 @@ object UpdateHeuristic {
     replaceVersion = {
       case update: Update.Single if isScalafmtUpdate(update) =>
         defaultReplaceVersion(_ => List("version"))(update)
+      case update: Update.Single if MillAlg.isMillMainUpdate(update) =>
+        content => if (content === update.currentVersion) Some(update.nextVersion) else None
       case _ =>
         _ => None
     }
diff --git a/modules/core/src/main/scala/org/scalasteward/core/repoconfig/UpdatesConfig.scala b/modules/core/src/main/scala/org/scalasteward/core/repoconfig/UpdatesConfig.scala
index 4155a40f..53b144bf 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/repoconfig/UpdatesConfig.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/repoconfig/UpdatesConfig.scala
@@ -74,7 +74,7 @@ final case class UpdatesConfig(
 
 object UpdatesConfig {
   val defaultFileExtensions: Set[String] =
-    Set(".scala", ".sbt", ".sbt.shared", ".sc", ".yml", "pom.xml")
+    Set(".mill-version", ".scala", ".sbt", ".sbt.shared", ".sc", ".yml", "pom.xml")
 
   implicit val updatesConfigEq: Eq[UpdatesConfig] =
     Eq.fromUniversalEquals


Test patch:
diff --git a/modules/core/src/test/scala/org/scalasteward/core/buildtool/mill/MillAlgTest.scala b/modules/core/src/test/scala/org/scalasteward/core/buildtool/mill/MillAlgTest.scala
index fa3ef4d1..f4c61b62 100644
--- a/modules/core/src/test/scala/org/scalasteward/core/buildtool/mill/MillAlgTest.scala
+++ b/modules/core/src/test/scala/org/scalasteward/core/buildtool/mill/MillAlgTest.scala
@@ -34,7 +34,8 @@ class MillAlgTest extends FunSuite {
       trace = Vector(
         Cmd("write", predef),
         Cmd(repoDir.toString :: millCmd),
-        Cmd("rm", "-rf", predef)
+        Cmd("rm", "-rf", predef),
+        Cmd("read", s"$repoDir/.mill-version")
       )
     )
     assertEquals(state, expected)
diff --git a/modules/core/src/test/scala/org/scalasteward/core/buildtool/mill/MillVersionParserTest.scala b/modules/core/src/test/scala/org/scalasteward/core/buildtool/mill/MillVersionParserTest.scala
new file mode 100644
index 00000000..e3f76f6e
--- /dev/null
+++ b/modules/core/src/test/scala/org/scalasteward/core/buildtool/mill/MillVersionParserTest.scala
@@ -0,0 +1,24 @@
+package org.scalasteward.core.buildtool.mill
+
+import munit.FunSuite
+
+class MillVersionParserTest extends FunSuite {
+  val data = Seq(
+    "" -> None,
+    "0.6.0" -> Some("0.6.0"),
+    "0.9.6-16-a5da34" -> Some("0.9.6-16-a5da34"),
+    """0.6.0
+      |""".stripMargin -> Some("0.6.0"),
+    "\r\n0.6.0\r\n".stripMargin -> Some("0.6.0"),
+    " 0.6.0 " -> Some("0.6.0")
+  )
+
+  for {
+    (versionFileContent, expected) <- data
+  } yield test(
+    s"parse version from .mill-version file with content '$versionFileContent'"
+  ) {
+    val parsed = parser.parseMillVersion(versionFileContent)
+    assertEquals(parsed, expected)
+  }
+}
diff --git a/modules/core/src/test/scala/org/scalasteward/core/edit/EditAlgTest.scala b/modules/core/src/test/scala/org/scalasteward/core/edit/EditAlgTest.scala
index de4d04a9..6fd7c275 100644
--- a/modules/core/src/test/scala/org/scalasteward/core/edit/EditAlgTest.scala
+++ b/modules/core/src/test/scala/org/scalasteward/core/edit/EditAlgTest.scala
@@ -252,6 +252,20 @@ class EditAlgTest extends FunSuite {
     assertEquals(runApplyUpdate(Repo("edit-alg", "test-9"), update, original), expected)
   }
 
+  test("mill version file update") {
+    val update = Update.Single(
+      crossDependency = "com.lihaoyi" % "mill-main" % "0.9.5",
+      newerVersions = Nel.of("0.9.9")
+    )
+    val original = Map(
+      ".mill-version" -> """0.9.5"""
+    )
+    val expected = Map(
+      ".mill-version" -> """0.9.9"""
+    )
+    assertEquals(runApplyUpdate(Repo("edit-alg", "test-10"), update, original), expected)
+  }
+
   private def runApplyUpdate(
       repo: Repo,
       update: Update,