Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor UpdateInfoUrlFinder, enhance ForgeType #3145

Merged
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import eu.timepit.refined.auto._
import org.http4s.Uri
import org.http4s.client.Client
import org.http4s.headers.`User-Agent`
import org.scalasteward.core.application.Config.StewardUsage
import org.scalasteward.core.application.Config.{ForgeCfg, StewardUsage}
import org.scalasteward.core.buildtool.BuildToolDispatcher
import org.scalasteward.core.buildtool.maven.MavenAlg
import org.scalasteward.core.buildtool.mill.MillAlg
Expand Down Expand Up @@ -211,8 +211,8 @@ object Context {
implicit val forgeApiAlg: ForgeApiAlg[F] =
ForgeSelection.forgeApiAlg[F](config.forgeCfg, config.forgeSpecificCfg, forgeUser)
implicit val forgeRepoAlg: ForgeRepoAlg[F] = new ForgeRepoAlg[F](config)
implicit val updateInfoUrlFinder: UpdateInfoUrlFinder[F] =
new UpdateInfoUrlFinder[F](config.forgeCfg)
implicit val forgeCfg: ForgeCfg = config.forgeCfg
implicit val updateInfoUrlFinder: UpdateInfoUrlFinder[F] = new UpdateInfoUrlFinder[F]
implicit val pullRequestRepository: PullRequestRepository[F] =
new PullRequestRepository[F](pullRequestsStore)
implicit val scalafixCli: ScalafixCli[F] = new ScalafixCli[F]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ package org.scalasteward.core.coursier
import cats.Monad
import cats.syntax.all._
import org.http4s.Uri
import org.scalasteward.core.application.Config.ForgeCfg
import org.scalasteward.core.forge.ForgeRepo
import org.scalasteward.core.util.uri

final case class DependencyMetadata(
Expand Down Expand Up @@ -46,6 +48,9 @@ final case class DependencyMetadata(
val urls = scmUrl.toList ++ homePage.toList
urls.find(_.scheme.exists(uri.httpSchemes)).orElse(urls.headOption)
}

def forgeRepo(implicit config: ForgeCfg): Option[ForgeRepo] =
repoUrl.flatMap(ForgeRepo.fromRepoUrl)
}

object DependencyMetadata {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,10 @@ final case class Version(value: String) {
}

object Version {
case class Update(currentVersion: Version, nextVersion: Version)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: 👏🏼 👏🏼

This comment follows the conventionalcomments.org standard


val tagNames: List[Version => String] = List("v" + _, _.value, "release-" + _)

implicit val versionCodec: Codec[Version] =
deriveUnwrappedCodec

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright 2018-2023 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.forge

import org.http4s.Uri
import org.scalasteward.core.application.Config.ForgeCfg

/** ForgeRepo encapsulates two concepts that are commonly considered together - the URI of a repo,
* and the 'type' of forge that url represents. Given a URI, once we know it's a GitHub or GitLab
* forge, etc, then we can know how to construct many of the urls for common resources existing at
* that repo host- for instance, the url to view a particular file, or to diff two commits.
*/
case class ForgeRepo(forgeType: ForgeType, repoUrl: Uri) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: 👏🏼 👏🏼 👏🏼

This comment follows the conventionalcomments.org standard

def diffUrlFor(from: String, to: String): Uri = forgeType.diffs.forDiff(from, to)(repoUrl)

def fileUrlFor(fileName: String): Uri = forgeType.files.forFile(fileName)(repoUrl)
}

object ForgeRepo {
def fromRepoUrl(repoUrl: Uri)(implicit config: ForgeCfg): Option[ForgeRepo] = for {
repoForgeType <- ForgeType.fromRepoUrl(repoUrl)
} yield ForgeRepo(repoForgeType, repoUrl)
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,16 @@ package org.scalasteward.core.forge

import cats.Eq
import cats.syntax.all._
import org.http4s.Uri
import org.http4s.syntax.literals._
import org.scalasteward.core.application.Config.ForgeCfg
import org.scalasteward.core.forge.ForgeType._
import org.scalasteward.core.util.unexpectedString

sealed trait ForgeType extends Product with Serializable {
def publicWebHost: Option[String]
val diffs: DiffUriPattern
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

quibble: Can we add some ScalaDocs here to explain what this method should do?

This comment follows the conventionalcomments.org standard

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, done with 905250c !

val files: FileUriPattern
def supportsForking: Boolean = true
def supportsLabels: Boolean = true

Expand All @@ -38,35 +42,64 @@ sealed trait ForgeType extends Product with Serializable {
}

object ForgeType {
trait DiffUriPattern { def forDiff(from: String, to: String): Uri => Uri }
trait FileUriPattern { def forFile(fileName: String): Uri => Uri }
Comment on lines +55 to +56
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FileUriPattern & DiffUriPattern have been designed so that they can be concisely implemented using the lambda syntax for Single Abstract Method (SAM) types, eg:

val diffs: DiffUriPattern = (from, to) => _ / "compare" / s"$from...$to"
val files: FileUriPattern = fileName => _ / "blob" / "master" / fileName

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


case object AzureRepos extends ForgeType {
override val publicWebHost: Some[String] = Some("dev.azure.com")
override def supportsForking: Boolean = false
val diffs: DiffUriPattern = (from, to) =>
_ / "branchCompare" withQueryParams Map(
"baseVersion" -> s"GT$from",
"targetVersion" -> s"GT$to"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: You can also use the operator to add query params +?

_ / "branchCompare" +? ("baseVersion", s"GT$from") +? ("targetVersion", s"GT$to")

This comment follows the conventionalcomments.org standard

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooh, nice one! Updated with fca39d4 👍

)
val files: FileUriPattern =
fileName =>
_.withQueryParam(
"path",
fileName
) // Azure's canonical value for the path is prefixed with a slash?
}

case object Bitbucket extends ForgeType {
override val publicWebHost: Some[String] = Some("bitbucket.org")
override def supportsLabels: Boolean = false
val publicApiBaseUrl = uri"https://api.bitbucket.org/2.0"
val diffs: DiffUriPattern = (from, to) => _ / "compare" / s"$to..$from" withFragment "diff"
val files: FileUriPattern = fileName => _ / "src" / "master" / fileName
}

/** Note Bitbucket Server will be End Of Service Life on 15th February 2024:
*
* https://www.atlassian.com/software/bitbucket/enterprise
* https://www.atlassian.com/migration/assess/journey-to-cloud
*/
case object BitbucketServer extends ForgeType {
override val publicWebHost: None.type = None
override def supportsForking: Boolean = false
override def supportsLabels: Boolean = false
val diffs: DiffUriPattern = Bitbucket.diffs
val files: FileUriPattern = fileName => _ / "browse" / fileName
}

case object GitHub extends ForgeType {
override val publicWebHost: Some[String] = Some("github.com")
val publicApiBaseUrl = uri"https://api.github.com"
val diffs: DiffUriPattern = (from, to) => _ / "compare" / s"$from...$to"
val files: FileUriPattern = fileName => _ / "blob" / "master" / fileName
}

case object GitLab extends ForgeType {
override val publicWebHost: Some[String] = Some("gitlab.com")
val publicApiBaseUrl = uri"https://gitlab.com/api/v4"
val diffs: DiffUriPattern = GitHub.diffs
val files: FileUriPattern = GitHub.files
}

case object Gitea extends ForgeType {
override val publicWebHost: Option[String] = None
val diffs: DiffUriPattern = GitHub.diffs
val files: FileUriPattern = fileName => _ / "src" / "branch" / "master" / fileName
}

val all: List[ForgeType] = List(AzureRepos, Bitbucket, BitbucketServer, GitHub, GitLab, Gitea)
Expand All @@ -83,6 +116,16 @@ object ForgeType {
def fromPublicWebHost(host: String): Option[ForgeType] =
all.find(_.publicWebHost.contains_(host))

/** Attempts to guess, based on the uri host and the config used to launch Scala Steward, what
* type of forge hosts the repo at the supplied uri.
*/
def fromRepoUrl(repoUrl: Uri)(implicit config: ForgeCfg): Option[ForgeType] =
repoUrl.host.flatMap { repoHost =>
Option
.when(config.apiHost.host.contains(repoHost))(config.tpe)
.orElse(fromPublicWebHost(repoHost.value))
}

implicit val forgeTypeEq: Eq[ForgeType] =
Eq.fromUniversalEquals
}
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ final class NurtureAlg[F[_]](config: ForgeCfg)(implicit
case (currentVersion, dependency) =>
dependencyToMetadata.get(dependency).toList.traverse { metadata =>
updateInfoUrlFinder
.findUpdateInfoUrls(metadata, currentVersion, dependency.version)
.findUpdateInfoUrls(metadata, Version.Update(currentVersion, dependency.version))
.tupleLeft(dependency.artifactId.name)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,26 +22,26 @@ import org.http4s.Uri
import org.scalasteward.core.application.Config.ForgeCfg
import org.scalasteward.core.coursier.DependencyMetadata
import org.scalasteward.core.data.Version
import org.scalasteward.core.forge.ForgeType
import org.scalasteward.core.forge.ForgeRepo
import org.scalasteward.core.forge.ForgeType._
import org.scalasteward.core.nurture.UpdateInfoUrl._
import org.scalasteward.core.nurture.UpdateInfoUrlFinder.possibleUpdateInfoUrls
import org.scalasteward.core.util.UrlChecker

final class UpdateInfoUrlFinder[F[_]](config: ForgeCfg)(implicit
final class UpdateInfoUrlFinder[F[_]](implicit
config: ForgeCfg,
urlChecker: UrlChecker[F],
F: Monad[F]
) {
def findUpdateInfoUrls(
metadata: DependencyMetadata,
currentVersion: Version,
nextVersion: Version
dependency: DependencyMetadata,
versionUpdate: Version.Update
): F[List[UpdateInfoUrl]] = {
val updateInfoUrls =
metadata.releaseNotesUrl.toList.map(CustomReleaseNotes.apply) ++
metadata.repoUrl.toList.flatMap { repoUrl =>
possibleUpdateInfoUrls(config.tpe, config.apiHost, repoUrl, currentVersion, nextVersion)
}
val updateInfoUrls: List[UpdateInfoUrl] =
dependency.releaseNotesUrl.toList.map(CustomReleaseNotes.apply) ++
dependency.forgeRepo.toSeq.flatMap(forgeRepo =>
possibleUpdateInfoUrls(forgeRepo, versionUpdate)
)

updateInfoUrls
.sorted(UpdateInfoUrl.updateInfoUrlOrder.toOrdering)
Expand All @@ -51,8 +51,6 @@ final class UpdateInfoUrlFinder[F[_]](config: ForgeCfg)(implicit
}

object UpdateInfoUrlFinder {
private def possibleTags(version: Version): List[String] =
List(s"v$version", version.value, s"release-$version")

private[nurture] val possibleChangelogFilenames: List[String] = {
val baseNames = List(
Expand All @@ -74,84 +72,41 @@ object UpdateInfoUrlFinder {
possibleFilenames(baseNames)
}

private def forgeTypeFromRepoUrl(
forgeType: ForgeType,
forgeUrl: Uri,
repoUrl: Uri
): Option[ForgeType] =
repoUrl.host.flatMap { repoHost =>
if (forgeUrl.host.contains(repoHost)) Some(forgeType)
else ForgeType.fromPublicWebHost(repoHost.value)
}

private[nurture] def possibleVersionDiffs(
forgeType: ForgeType,
forgeUrl: Uri,
repoUrl: Uri,
currentVersion: Version,
nextVersion: Version
): List[VersionDiff] =
forgeTypeFromRepoUrl(forgeType, forgeUrl, repoUrl).map {
case GitHub | GitLab | Gitea =>
possibleTags(currentVersion).zip(possibleTags(nextVersion)).map { case (from1, to1) =>
VersionDiff(repoUrl / "compare" / s"$from1...$to1")
}
case Bitbucket | BitbucketServer =>
possibleTags(currentVersion).zip(possibleTags(nextVersion)).map { case (from1, to1) =>
VersionDiff((repoUrl / "compare" / s"$to1..$from1").withFragment("diff"))
}
case AzureRepos =>
possibleTags(currentVersion).zip(possibleTags(nextVersion)).map { case (from1, to1) =>
VersionDiff(
(repoUrl / "branchCompare")
.withQueryParams(Map("baseVersion" -> from1, "targetVersion" -> to1))
)
}
}.orEmpty
repoForge: ForgeRepo,
update: Version.Update
): List[VersionDiff] = for {
tagName <- Version.tagNames
} yield VersionDiff(
repoForge.diffUrlFor(tagName(update.currentVersion), tagName(update.nextVersion))
)

private[nurture] def possibleUpdateInfoUrls(
forgeType: ForgeType,
forgeUrl: Uri,
repoUrl: Uri,
currentVersion: Version,
nextVersion: Version
forgeRepo: ForgeRepo,
update: Version.Update
): List[UpdateInfoUrl] = {
val repoForgeType = forgeTypeFromRepoUrl(forgeType, forgeUrl, repoUrl)

val githubReleaseNotes = repoForgeType
.collect { case GitHub =>
possibleTags(nextVersion).map(tag => GitHubReleaseNotes(repoUrl / "releases" / "tag" / tag))
}
.getOrElse(List.empty)

def files(fileNames: List[String]): List[Uri] = {
val maybeSegments = repoForgeType.collect {
case GitHub | GitLab => List("blob", "master")
case Bitbucket => List("master")
case BitbucketServer => List("browse")
case Gitea => List("src", "branch", "master")
}

val repoFiles = maybeSegments.toList.flatMap { segments =>
val base = segments.foldLeft(repoUrl)(_ / _)
fileNames.map(name => base / name)
}
def customUrls(wrap: Uri => UpdateInfoUrl, fileNames: List[String]): List[UpdateInfoUrl] =
fileNames.map(f => wrap(forgeRepo.fileUrlFor(f)))

val azureRepoFiles = repoForgeType
.collect { case AzureRepos => fileNames.map(name => repoUrl.withQueryParam("path", name)) }
.toList
.flatten
gitHubReleaseNotesFor(forgeRepo, update.nextVersion) ++
customUrls(CustomReleaseNotes, possibleReleaseNotesFilenames) ++
customUrls(CustomChangelog, possibleChangelogFilenames) ++
possibleVersionDiffs(forgeRepo, update)
}

repoFiles ++ azureRepoFiles
private def gitHubReleaseNotesFor(
forgeRepo: ForgeRepo,
version: Version
): List[UpdateInfoUrl] =
forgeRepo.forgeType match {
case GitHub =>
Version.tagNames
.map(tagName =>
GitHubReleaseNotes(forgeRepo.repoUrl / "releases" / "tag" / tagName(version))
)
case _ => Nil
}

val customChangelog = files(possibleChangelogFilenames).map(CustomChangelog)
val customReleaseNotes = files(possibleReleaseNotesFilenames).map(CustomReleaseNotes)

githubReleaseNotes ++ customReleaseNotes ++ customChangelog ++
possibleVersionDiffs(forgeType, forgeUrl, repoUrl, currentVersion, nextVersion)
}

private def possibleFilenames(baseNames: List[String]): List[String] = {
val extensions = List("md", "markdown", "rst")
(baseNames, extensions).mapN { case (base, ext) => s"$base.$ext" }
Expand Down
Loading