diff --git a/build.sbt b/build.sbt index 518819d..7a61a13 100644 --- a/build.sbt +++ b/build.sbt @@ -1,7 +1,7 @@ name := "make-release-notes" -scalaVersion := "2.13.11" -scalacOptions ++= Seq("-feature", "-deprecation", "-Werror") +scalaVersion := "3.3.0" +scalacOptions ++= Seq("-feature", "-deprecation", "-Werror", "-source:future") libraryDependencies ++= Seq( "org.pegdown" % "pegdown" % "1.6.0", diff --git a/src/main/scala/CommunityProjects.scala b/src/main/scala/CommunityProjects.scala deleted file mode 100755 index cc4ffb1..0000000 --- a/src/main/scala/CommunityProjects.scala +++ /dev/null @@ -1,72 +0,0 @@ - -case class ProjectInfo(name: String, desc: String, url: String, loc: String) { - - def toHtml =
  • {name}
    - {desc}
    - Location: { - for(l <- (loc split "\n")) - yield {l}
    - }
    -  
  • -} - -/** This class encapsulates parsing/loading community project HTML from - * the formatted emails people write. These emails are usually dumped - * into a file called community-projects.txt with a one-line separator between them. - */ -object CommunityProjects { - - def loadHtmlFromFile(file: java.io.File = new java.io.File("community-projects.txt")): String = - loadCommunityProjects(loadLines(file)) match { - case projects if projects.nonEmpty => renderProjectHtml(projects) - case _ => "" - } - - - def loadLines(file: java.io.File): Seq[String] = { - val in = new java.io.BufferedReader(new java.io.FileReader(file)) - def read(lines: Vector[String] = Vector.empty): Vector[String] = in.readLine match { - case null => lines - case line => - read(lines :+ line) - } - try read() - finally in.close() - } - - def loadCommunityProjects(lines: Seq[String]): Seq[ProjectInfo] = { - val groups: Seq[Seq[String]] = { - def grouper(left: Seq[String], current: Seq[Seq[String]] = Seq.empty): Seq[Seq[String]] = - if(left.isEmpty) current - else grouper(left dropWhile (x => !x.isEmpty) drop 1, current :+ (left takeWhile (x => !x.isEmpty))) - grouper(lines) - } - groups map parseProject - } - - - - def fix(header: String)(content: String): String = - if(content.toLowerCase startsWith header.toLowerCase) content drop header.length - else sys.error("Expected [" + header + "] found [" + content + "]") - - private def parseProject(content: Seq[String]): ProjectInfo = - content match { - case Seq(name, desc, url, location @ _*) if !location.isEmpty => - ProjectInfo(fix("Name:")(name), - fix("Description:")(desc), - fix("URL:")(url), - fix("Location:")(location.head) + "\n" + (location.tail mkString "\n")) - case _ => sys.error("Bad project group: " + content.mkString("\n")) - } - - def renderProjectHtml(projects: Seq[ProjectInfo]): String = { - def list = - val sb = new StringBuffer - sb append ("

    Community Projects

    ") - sb append (s"

    Special thanks to the ${projects.size} projects that have made releases available for this version of Scala!

    ") - sb append (list.toString) - sb.toString - } - -} \ No newline at end of file diff --git a/src/main/scala/GitInfo.scala b/src/main/scala/GitInfo.scala index 2551f9f..56f0a10 100644 --- a/src/main/scala/GitInfo.scala +++ b/src/main/scala/GitInfo.scala @@ -1,45 +1,42 @@ -import scala.collection.parallel.CollectionConverters._ // for .par +import scala.collection.parallel.CollectionConverters.* // for .par -case class Commit(sha: String, author: String, header: String, body: String) { +case class Commit(sha: String, author: String, header: String, body: String): def trimmedHeader = header.take(80) override def toString = " * " + sha + " (" + author + ") " + header + " - " + body.take(5) + " ..." -} -/** Gobal functions for dealing with git. */ -object GitHelper { - def processGitCommits(gitDir: java.io.File, previousTag: String, currentTag: String): IndexedSeq[Commit] = { - import sys.process._ +/** Global functions for dealing with git. */ +object GitHelper: + def processGitCommits(gitDir: java.io.File, previousTag: String, currentTag: String): IndexedSeq[Commit] = + import sys.process.* val gitFormat = "%h %s" // sha and subject val log = Process(Seq("git", "--no-pager", "log", s"${previousTag}..${currentTag}", "--format=format:" + gitFormat, "--no-merges", "--topo-order"), gitDir).lazyLines log.par.map(_.split(" ", 2)).collect { case Array(sha, title) => - val (author :: body) = Process(Seq("git", "--no-pager", "show", sha, "--format=format:%aN%n%b", "--quiet"), gitDir).lazyLines.toList + val author :: body = + Process(Seq("git", "--no-pager", "show", sha, "--format=format:%aN%n%b", "--quiet"), gitDir) + .lazyLines.toList: @unchecked Commit(sha, author, title, body.mkString("\n")) }.toVector - } def hasFixins(msg: String): Boolean = ( - (msg contains "SI-") /*&& ((msg.toLowerCase contains "fix") || (msg.toLowerCase contains "close"))*/ + msg.contains("SI-") /*&& ((msg.toLowerCase contains "fix") || msg.toLowerCase.contains("close"))*/ ) val siPattern = java.util.regex.Pattern.compile("(SI-[0-9]+)") - def fixLinks(commit: Commit)(implicit targetLanguage: TargetLanguage): String = { + def fixLinks(commit: Commit)(implicit targetLanguage: TargetLanguage): String = val searchString = commit.body + commit.header - val m = siPattern matcher searchString + val m = siPattern.matcher(searchString) val issues = new collection.mutable.ArrayBuffer[String] - while (m.find()) { - issues += (m group 1) - } + while m.find() do + issues += (m.group(1)) issues map (si => targetLanguage.createHyperLink(s"https://issues.scala-lang.org/browse/$si", si)) mkString ", " - } def htmlEncode(s: String) = org.apache.commons.text.StringEscapeUtils.escapeHtml4(s) -} -class GitInfo(gitDir: java.io.File, val previousTag: String, val currentTag: String)(implicit targetLanguage: TargetLanguage) { - import GitHelper._ +class GitInfo(gitDir: java.io.File, val previousTag: String, val currentTag: String)(implicit targetLanguage: TargetLanguage): + import GitHelper.* val commits = processGitCommits(gitDir, previousTag, currentTag) val authors: Seq[(String, Int)] = @@ -50,11 +47,11 @@ class GitInfo(gitDir: java.io.File, val previousTag: String, val currentTag: Str .sortBy(_._2) val fixCommits = - for { + for commit <- commits searchString = commit.body + commit.header if hasFixins(searchString) - } yield commit + yield commit private def commitShaLink(sha: String) = targetLanguage.createHyperLink(s"https://github.com/scala/scala/commit/${sha}", sha) @@ -62,38 +59,34 @@ class GitInfo(gitDir: java.io.File, val previousTag: String, val currentTag: Str private def blankLine(): String = targetLanguage.blankLine() private def header4(msg: String): String = targetLanguage.header4(msg) - def renderCommitterList: String = { + def renderCommitterList: String = val sb = new StringBuffer - sb append blankLine() - sb append header4("A big thank you to all the contributors!") - sb append targetLanguage.tableHeader("#", "Author") - for ((author, count) <- authors) - sb append targetLanguage.tableRow(count.toString, author) - sb append targetLanguage.tableEnd + sb.append(blankLine()) + sb.append(header4("A big thank you to all the contributors!")) + sb.append(targetLanguage.tableHeader("#", "Author")) + for (author, count) <- authors do + sb.append(targetLanguage.tableRow(count.toString, author)) + sb.append(targetLanguage.tableEnd) sb.toString - } - def renderCommitList: String = { + def renderCommitList: String = val sb = new StringBuffer - sb append blankLine() - sb append header4("Complete commit list!") - sb append targetLanguage.tableHeader("sha", "Title") - for (commit <- commits) - sb append targetLanguage.tableRow(commitShaLink(commit.sha), commit.trimmedHeader) - sb append targetLanguage.tableEnd + sb.append(blankLine()) + sb.append(header4("Complete commit list!")) + sb.append(targetLanguage.tableHeader("sha", "Title")) + for commit <- commits do + sb.append(targetLanguage.tableRow(commitShaLink(commit.sha), commit.trimmedHeader)) + sb.append(targetLanguage.tableEnd) sb.toString - } - def renderFixedIssues: String = { + def renderFixedIssues: String = val sb = new StringBuffer - sb append blankLine() - sb append header4(s"Commits and the issues they fixed since ${previousTag}") - sb append targetLanguage.tableHeader("Issue(s)", "Commit", "Message") - for (commit <- fixCommits) - sb append targetLanguage.tableRow(fixLinks(commit), commitShaLink(commit.sha), commit.trimmedHeader) - sb append targetLanguage.tableEnd - sb append blankLine() + sb.append(blankLine()) + sb.append(header4(s"Commits and the issues they fixed since ${previousTag}")) + sb.append(targetLanguage.tableHeader("Issue(s)", "Commit", "Message")) + for commit <- fixCommits do + sb.append(targetLanguage.tableRow(fixLinks(commit), commitShaLink(commit.sha), commit.trimmedHeader)) + sb.append(targetLanguage.tableEnd) + sb.append(blankLine()) sb.toString - } -} diff --git a/src/main/scala/IO.scala b/src/main/scala/IO.scala deleted file mode 100644 index 6fd889a..0000000 --- a/src/main/scala/IO.scala +++ /dev/null @@ -1,9 +0,0 @@ -import java.io.File - -object IO { - def write(f: java.io.File, contents: String): Unit = { - val buf = new java.io.BufferedWriter(new java.io.FileWriter(f)) - try buf.write(contents) - finally buf.close() - } -} diff --git a/src/main/scala/JiraIssues.scala b/src/main/scala/JiraIssues.scala deleted file mode 100644 index 66647b1..0000000 --- a/src/main/scala/JiraIssues.scala +++ /dev/null @@ -1,39 +0,0 @@ - -//curl -D- -X GET https://issues.scala-lang.org/sr/jira.issueviews:searchrequest-printable/11907/SearchRequest-11907.html?tempMax=1000 - -object JiraIssues { - - def slurp(in: java.io.InputStream): String = { - val wrapped = new java.io.BufferedReader(new java.io.InputStreamReader(in)) - def read(buf: StringBuffer): String = - wrapped.readLine match { - case null => buf.toString - case line => - buf append s"$line\n" - read(buf) - } - try read(new StringBuffer) - finally in.close() - } - - def parseXml(in: String): scala.xml.NodeSeq = { - import scala.xml.parsing.XhtmlParser - XhtmlParser(scala.io.Source.fromString(in)) - } - - def makeOpenIssuesString = { - import java.net.URL - val url = new URL("https://issues.scala-lang.org/sr/jira.issueviews:searchrequest-printable/11907/SearchRequest-11907.html?tempMax=1000") - - val htmlString = slurp(url.openStream) - - val dropFront = htmlString drop (htmlString.indexOfSlice("""", dropFront.length)) - - //val html = parseXml(htmlString) - - //val div = html \\ "div#printable-content" - //div.toString - dropEnd + "" - } -} \ No newline at end of file diff --git a/src/main/scala/MakeDownloadPage.scala b/src/main/scala/MakeDownloadPage.scala index 1664805..8cc23c2 100644 --- a/src/main/scala/MakeDownloadPage.scala +++ b/src/main/scala/MakeDownloadPage.scala @@ -1,21 +1,21 @@ import java.util.Date -import java.text._ -import scala.concurrent._ -import scala.concurrent.duration._ +import java.text.* +import scala.concurrent.* +import scala.concurrent.duration.* import ExecutionContext.Implicits.global +import java.nio.file.{Files, Paths} -class MakeDownloadPage(version: String, releaseDate: Date = new Date()) { - def write() = { +class MakeDownloadPage(version: String, releaseDate: Date = new Date()): + def write() = require(!version.startsWith("v"), "version should *not* start with 'v'") val fileName = s"${format("yyyy-MM-dd")}-$version.md" - IO.write(new java.io.File(fileName), page) + Files.write(Paths.get(fileName), page.getBytes) println(s"cp $fileName ../scala-lang/_downloads/") println("# to prepare your scala-lang PR") - } // get size of `url` without actually downloading it - def humanSize(url: String): Future[String] = Future { - import scala.sys.process._ + def humanSize(url: String): Future[String] = Future: + import scala.sys.process.* println("## fetching size of "+ url) scala.util.Try { val responseHeader = Process(s"curl -m 5 --silent -D - -X HEAD $url").lazyLines @@ -26,27 +26,23 @@ class MakeDownloadPage(version: String, releaseDate: Date = new Date()) { case meh if meh < 1024 => "" case kilo if kilo < 1024*1024 => f"${bytes.toDouble/1024}%.0fK" case big => f"${bytes.toDouble/(1024*1024)}%.2fM" - })} match { + })} match case Some((status, humanSize)) if isGoodStatus(status) => humanSize case _ => println(s"## warning: could not fetch $url") "" - } - } def isGoodStatus(status: String): Boolean = Seq("200 OK", "302 found", "HTTP/2 200").exists(status.contains) - def resourceArchive(cls: String, name: String, ext: String, desc: String): Future[String] = { + def resourceArchive(cls: String, name: String, ext: String, desc: String): Future[String] = val fileName = s"$name-$version.$ext" val fullUrl = s"https://downloads.lightbend.com/scala/$version/$fileName" resource(cls, fileName, desc, fullUrl, fullUrl) - } - def resource(cls: String, fileName: String, desc: String, fullUrl: String, urlForSize: String): Future[String] = { + def resource(cls: String, fileName: String, desc: String, fullUrl: String, urlForSize: String): Future[String] = humanSize(urlForSize) map (size => s"""[$cls, "$fileName", "$fullUrl", "$desc", "$size"]""") - } def defaultClass = """"-non-main-sys"""" def unixClass = """"-main-unixsys"""" @@ -74,8 +70,7 @@ class MakeDownloadPage(version: String, releaseDate: Date = new Date()) { )).map(_.mkString(",\n ")), 30.seconds) // note: first and last lines must be exactly "---" - def page: String = { -s"""--- + def page: String = s"""--- title: Scala $version start: ${format("dd MMMM yyyy")} layout: downloadpage @@ -90,5 +85,3 @@ resources: [ ] --- """ - } -} diff --git a/src/main/scala/MakeReleaseNotes.scala b/src/main/scala/MakeReleaseNotes.scala index 6c4b337..19a3cbc 100644 --- a/src/main/scala/MakeReleaseNotes.scala +++ b/src/main/scala/MakeReleaseNotes.scala @@ -1,97 +1,86 @@ import java.util.Date -import java.text._ +import java.text.* import java.io.BufferedReader +import java.nio.file.{Files, Paths} import scala.io.Source -object MakeReleaseNotes { +object MakeReleaseNotes: def main(args: Array[String]): Unit = - args match { + args match case Array(prevVersion, version, release) => genPR(prevVersion, version, release) case Array(prevVersion, version, release, gitDir) => genPR(prevVersion, version, release, gitDir) - } - def genPR(prevVersion: String, version: String, release: String, gitDir: String = s"${sys.env("HOME")}/git/scala") = { + def genPR(prevVersion: String, version: String, release: String, gitDir: String = s"${sys.env("HOME")}/git/scala") = val date = new SimpleDateFormat("yyyy/MM/dd").parse(release) new MakeDownloadPage(version, date).write() MakeReleaseNotes(new java.io.File(gitDir), version, s"v$prevVersion", s"v$version", MarkDown, date) - } - def write(page: String, version: String, releaseDate: Date, ext: String) = { + def write(page: String, version: String, releaseDate: Date, ext: String) = def format(fmt: String) = new SimpleDateFormat(fmt).format(releaseDate) require(!version.startsWith("v"), "version should *not* start with 'v'") val fileName = s"${format("yyyy-MM-dd")}-release-notes-$version.$ext" - IO.write(new java.io.File(fileName), page) + Files.write(Paths.get(fileName), page.getBytes) println("# generated " + fileName) - if (ext == "md") { + if ext == "md" then println(s"cp $fileName ../scala-lang/_posts/") println(s"# don't forget to\n${scala.util.Properties.envOrElse("EDITOR", "mate")} ../scala-lang/download/scala2.md ../scala-lang/_config.yml") println("# and, to prepare and sanity check your scala-lang PR:") println(s"maruku --html $fileName") - } - } def apply(scalaDir: String, version: String, previousTag: String, currentTag: String, releaseDate: Date): Unit = Seq(Html, MarkDown).foreach(fmt => apply(new java.io.File(scalaDir), version, previousTag, currentTag, fmt, releaseDate)) - def apply(scalaDir: java.io.File, version: String, previousTag: String, currentTag: String, targetLanguage: TargetLanguage = MarkDown, releaseDate: Date = new Date()): Unit = { - val out = targetLanguage match { + def apply(scalaDir: java.io.File, version: String, previousTag: String, currentTag: String, targetLanguage: TargetLanguage = MarkDown, releaseDate: Date = new Date()): Unit = + val out = targetLanguage match case Html => new java.io.File("release-notes.html") case MarkDown => new java.io.File(s"release-notes-${currentTag}.md") - } val notes = makeReleaseNotes(scalaDir, version, previousTag, currentTag)(targetLanguage) write(notes, currentTag.dropWhile(_ == 'v'), releaseDate, targetLanguage.ext) - } - private def parseHandWrittenNotes(file: java.io.File = new java.io.File("hand-written.md")): String = { - import org.pegdown._ + private def parseHandWrittenNotes(file: java.io.File = new java.io.File("hand-written.md")): String = + import org.pegdown.* val parser = new PegDownProcessor val in = new java.io.BufferedReader(new java.io.FileReader(file)) def read(buf: StringBuffer): String = - in.readLine match { + in.readLine match case null => buf.toString case line => - buf append s"${line}\n" + buf.append(s"${line}\n") read(buf) - } val content = try read(new StringBuffer) finally in.close() - parser markdownToHtml content - } + parser.markdownToHtml(content) private def stripTripleDashedHtmlComments(s: String): String = s.replaceAll("""(?ims)""", "") - private def makeReleaseNotes(scalaDir: java.io.File, version: String, previousTag: String, currentTag: String)(implicit targetLanguage: TargetLanguage): String = { - def rawHandWrittenNotes(file: java.io.File = new java.io.File(s"hand-written.md")): String = { - val lines: List[String] = if (file.exists) { + private def makeReleaseNotes(scalaDir: java.io.File, version: String, previousTag: String, currentTag: String)(implicit targetLanguage: TargetLanguage): String = + def rawHandWrittenNotes(file: java.io.File = new java.io.File(s"hand-written.md")): String = + val lines: List[String] = if file.exists then val src = Source.fromFile(file) src.getLines().toList - } else Nil + else Nil // if you don't have the next line, sub-bullets would be screwed! // please take this case into account and comment out 2 next lines and uncomment the line after! - val newLines = lines.map(x => if (x.startsWith(" *")) "\n" + x.stripPrefix(" ") else x) + val newLines = lines.map(x => if x.startsWith(" *") then "\n" + x.stripPrefix(" ") else x) val bulletFixed = newLines.mkString("\n") val commentsStripped = stripTripleDashedHtmlComments(bulletFixed) commentsStripped.replaceAll("\\$version", version) - } val info = new GitInfo(scalaDir, previousTag, currentTag) - // val communityProjects = CommunityProjects.loadHtmlFromFile() - import info.{ currentTag => _, _ } - //

    Known issues

    - // ${JiraIssues.makeOpenIssuesString} + import info.{ currentTag as _, * } - targetLanguage match { + targetLanguage match case Html => s""" @@ -116,7 +105,3 @@ title: "Scala ${currentTag drop 1} is now available!" --- ${rawHandWrittenNotes()} """ - } - - } -} diff --git a/src/main/scala/TargetLanguage.scala b/src/main/scala/TargetLanguage.scala index f4861dc..5dd9ff3 100644 --- a/src/main/scala/TargetLanguage.scala +++ b/src/main/scala/TargetLanguage.scala @@ -1,4 +1,4 @@ -sealed trait TargetLanguage { +sealed trait TargetLanguage: def createHyperLink(link: String, content: String): String def blankLine(): String def header4(msg: String): String @@ -8,8 +8,8 @@ sealed trait TargetLanguage { def tableRow(firstColumn: String, secondColumn: String, thirdColumn: String): String def tableEnd: String def ext: String -} -case object MarkDown extends TargetLanguage { + +case object MarkDown extends TargetLanguage: val ext = "md" def createHyperLink(link: String, content: String): String = s"[$content]($link)" @@ -29,17 +29,16 @@ $firstColumn | $secondColumn | $thirdColumn def tableRow(firstColumn: String, secondColumn: String, thirdColumn: String): String = s"$firstColumn | $secondColumn | ${escapeHtml(thirdColumn)}\n" def tableEnd: String = "\n" - def markdownEncode(s: String): String = s.flatMap { + def markdownEncode(s: String): String = s.flatMap: case c if (List('*', '`', '[', ']', '#').contains(c)) => "\\" + c case x => x.toString - } - def escapeHtml(s: String): String = Html.htmlEncode(s).flatMap { + def escapeHtml(s: String): String = Html.htmlEncode(s).flatMap: case '|' => "|" // it would destroy tables! case c => c.toString - } -} -case object Html extends TargetLanguage { +end MarkDown + +case object Html extends TargetLanguage: val ext = "html" def createHyperLink(link: String, content: String): String = s"""$content""" @@ -56,4 +55,4 @@ case object Html extends TargetLanguage { def tableEnd: String = "" def htmlEncode(s: String) = org.apache.commons.text.StringEscapeUtils.escapeHtml4(s) -} +end Html