Skip to content

Commit 2157fae

Browse files
authored
Merge pull request #2863 from scala-steward-org/topic/grab-release-notes-url-from-pom
Show release notes URL from POM in PR body
2 parents 6df915e + b9b525a commit 2157fae

File tree

11 files changed

+380
-172
lines changed

11 files changed

+380
-172
lines changed

modules/core/src/main/scala/org/scalasteward/core/application/Context.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ final class Context[F[_]](implicit
6666
val logger: Logger[F],
6767
val mavenAlg: MavenAlg[F],
6868
val millAlg: MillAlg[F],
69+
val nurtureAlg: NurtureAlg[F],
6970
val pruningAlg: PruningAlg[F],
7071
val pullRequestRepository: PullRequestRepository[F],
7172
val refreshErrorAlg: RefreshErrorAlg[F],

modules/core/src/main/scala/org/scalasteward/core/coursier/CoursierAlg.scala

Lines changed: 63 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -16,32 +16,24 @@
1616

1717
package org.scalasteward.core.coursier
1818

19+
import cats.Parallel
1920
import cats.effect.Async
2021
import cats.implicits._
21-
import cats.{Applicative, Parallel}
2222
import coursier.cache.{CachePolicy, FileCache}
2323
import coursier.core.{Authentication, Project}
24-
import coursier.{Fetch, Info, Module, ModuleName, Organization}
25-
import org.http4s.Uri
24+
import coursier.{Fetch, Module, ModuleName, Organization}
2625
import org.scalasteward.core.data.Resolver.Credentials
27-
import org.scalasteward.core.data.{Dependency, Resolver, Scope, Version}
26+
import org.scalasteward.core.data.{Dependency, Resolver, Version}
2827
import org.scalasteward.core.util.uri
2928
import org.typelevel.log4cats.Logger
3029

3130
/** An interface to [[https://get-coursier.io Coursier]] used for fetching dependency versions and
3231
* metadata.
3332
*/
3433
trait CoursierAlg[F[_]] {
35-
def getArtifactUrl(dependency: Scope.Dependency): F[Option[Uri]]
34+
def getMetadata(dependency: Dependency, resolvers: List[Resolver]): F[DependencyMetadata]
3635

3736
def getVersions(dependency: Dependency, resolver: Resolver): F[List[Version]]
38-
39-
final def getArtifactIdUrlMapping(dependencies: Scope.Dependencies)(implicit
40-
F: Applicative[F]
41-
): F[Map[String, Uri]] =
42-
dependencies.sequence
43-
.traverseFilter(dep => getArtifactUrl(dep).map(_.map(dep.value.artifactId.name -> _)))
44-
.map(_.toMap)
4537
}
4638

4739
object CoursierAlg {
@@ -50,64 +42,68 @@ object CoursierAlg {
5042
parallel: Parallel[F],
5143
F: Async[F]
5244
): CoursierAlg[F] = {
53-
val fetch: Fetch[F] = Fetch[F](FileCache[F]())
45+
val fetch: Fetch[F] =
46+
Fetch[F](FileCache[F]())
5447

5548
val cacheNoTtl: FileCache[F] =
5649
FileCache[F]().withTtl(None).withCachePolicies(List(CachePolicy.Update))
5750

5851
new CoursierAlg[F] {
59-
override def getArtifactUrl(dependency: Scope.Dependency): F[Option[Uri]] =
60-
convertToCoursierTypes(dependency).flatMap((getArtifactUrlImpl _).tupled)
52+
override def getMetadata(
53+
dependency: Dependency,
54+
resolvers: List[Resolver]
55+
): F[DependencyMetadata] =
56+
resolvers.traverseFilter(convertResolver(_).attempt.map(_.toOption)).flatMap {
57+
repositories =>
58+
val csrDependency = toCoursierDependency(dependency)
59+
getMetadataImpl(csrDependency, repositories, DependencyMetadata.empty)
60+
}
6161

62-
private def getArtifactUrlImpl(
62+
private def getMetadataImpl(
6363
dependency: coursier.Dependency,
64-
repositories: List[coursier.Repository]
65-
): F[Option[Uri]] = {
64+
repositories: List[coursier.Repository],
65+
acc: DependencyMetadata
66+
): F[DependencyMetadata] = {
6667
val fetchArtifacts = fetch
6768
.withArtifactTypes(Set(coursier.Type.pom, coursier.Type.ivy))
6869
.withDependencies(List(dependency))
6970
.withRepositories(repositories)
71+
7072
fetchArtifacts.ioResult.attempt.flatMap {
7173
case Left(throwable) =>
72-
logger.debug(throwable)(s"Failed to fetch artifacts of $dependency").as(None)
74+
logger.debug(throwable)(s"Failed to fetch artifacts of $dependency").as(acc)
7375
case Right(result) =>
7476
val maybeProject = result.resolution.projectCache
7577
.get(dependency.moduleVersion)
7678
.map { case (_, project) => project }
77-
maybeProject.traverseFilter { project =>
78-
getScmUrlOrHomePage(project.info) match {
79-
case Some(url) => F.pure(Some(url))
80-
case None =>
81-
getParentDependency(project).traverseFilter(getArtifactUrlImpl(_, repositories))
79+
80+
maybeProject.fold(F.pure(acc)) { project =>
81+
val metadata = acc.enrichWith(metadataFrom(project))
82+
val recurse = Option.when(metadata.repoUrl.isEmpty)(())
83+
(recurse >> parentOf(project)).fold(F.pure(metadata)) { parent =>
84+
getMetadataImpl(parent, repositories, metadata)
8285
}
8386
}
8487
}
8588
}
8689

8790
override def getVersions(dependency: Dependency, resolver: Resolver): F[List[Version]] =
88-
toCoursierRepository(resolver) match {
89-
case Left(message) =>
90-
logger.error(message) >> F.raiseError(new Throwable(message))
91-
case Right(repository) =>
92-
val module = toCoursierModule(dependency)
93-
repository.versions(module, cacheNoTtl.fetch).run.flatMap {
94-
case Left(message) =>
95-
logger.debug(message) >> F.raiseError(new Throwable(message))
96-
case Right((versions, _)) => F.pure(versions.available.map(Version.apply).sorted)
97-
}
91+
convertResolver(resolver).flatMap { repository =>
92+
val module = toCoursierModule(dependency)
93+
repository.versions(module, cacheNoTtl.fetch).run.flatMap {
94+
case Left(message) =>
95+
logger.debug(message) >> F.raiseError[List[Version]](new Throwable(message))
96+
case Right((versions, _)) =>
97+
F.pure(versions.available.map(Version.apply).sorted)
98+
}
9899
}
99100

100-
private def convertToCoursierTypes(
101-
dependency: Scope.Dependency
102-
): F[(coursier.Dependency, List[coursier.Repository])] =
103-
dependency.resolvers.traverseFilter(convertResolver).map { repositories =>
104-
(toCoursierDependency(dependency.value), repositories)
105-
}
106-
107-
private def convertResolver(resolver: Resolver): F[Option[coursier.Repository]] =
101+
private def convertResolver(resolver: Resolver): F[coursier.Repository] =
108102
toCoursierRepository(resolver) match {
109-
case Right(repository) => F.pure(Some(repository))
110-
case Left(message) => logger.error(s"Failed to convert $resolver: $message").as(None)
103+
case Right(repository) => F.pure(repository)
104+
case Left(message) =>
105+
logger.error(s"Failed to convert $resolver: $message") >>
106+
F.raiseError[coursier.Repository](new Throwable(message))
111107
}
112108
}
113109
}
@@ -127,40 +123,40 @@ object CoursierAlg {
127123
private def toCoursierRepository(resolver: Resolver): Either[String, coursier.Repository] =
128124
resolver match {
129125
case Resolver.MavenRepository(_, location, creds, headers) =>
130-
Right(
131-
coursier.maven.MavenRepository
132-
.apply(location, toCoursierAuthentication(creds, headers))
133-
)
126+
val authentication = toCoursierAuthentication(creds, headers)
127+
Right(coursier.maven.MavenRepository.apply(location, authentication))
134128
case Resolver.IvyRepository(_, pattern, creds, headers) =>
135-
coursier.ivy.IvyRepository
136-
.parse(pattern, authentication = toCoursierAuthentication(creds, headers))
129+
val authentication = toCoursierAuthentication(creds, headers)
130+
coursier.ivy.IvyRepository.parse(pattern, authentication = authentication)
137131
}
138132

139133
private def toCoursierAuthentication(
140134
credentials: Option[Credentials],
141135
headers: List[Resolver.Header]
142136
): Option[Authentication] =
143-
if (credentials.isEmpty && headers.isEmpty) {
144-
None
145-
} else {
146-
Some(
147-
new Authentication(
148-
credentials.fold("")(_.user),
149-
credentials.map(_.pass),
150-
headers.map(h => (h.key, h.value)),
151-
optional = false,
152-
None,
153-
httpsOnly = true,
154-
passOnRedirect = false
155-
)
137+
Option.when(credentials.nonEmpty || headers.nonEmpty) {
138+
new Authentication(
139+
credentials.fold("")(_.user),
140+
credentials.map(_.pass),
141+
headers.map(h => (h.key, h.value)),
142+
optional = false,
143+
realmOpt = None,
144+
httpsOnly = true,
145+
passOnRedirect = false
156146
)
157147
}
158148

159-
private def getParentDependency(project: Project): Option[coursier.Dependency] =
149+
private def metadataFrom(project: Project): DependencyMetadata =
150+
DependencyMetadata(
151+
homePage = uri.fromStringWithScheme(project.info.homePage),
152+
scmUrl = project.info.scm.flatMap(_.url).flatMap(uri.fromStringWithScheme),
153+
releaseNotesUrl = project.properties
154+
.collectFirst { case (key, value) if key.equalsIgnoreCase("info.releaseNotesUrl") => value }
155+
.flatMap(uri.fromStringWithScheme)
156+
)
157+
158+
private def parentOf(project: Project): Option[coursier.Dependency] =
160159
project.parent.map { case (module, version) =>
161160
coursier.Dependency(module, version).withTransitive(false)
162161
}
163-
164-
private def getScmUrlOrHomePage(info: Info): Option[Uri] =
165-
uri.findBrowsableUrl(info.scm.flatMap(_.url).toList :+ info.homePage)
166162
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright 2018-2022 Scala Steward contributors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.scalasteward.core.coursier
18+
19+
import cats.Monad
20+
import cats.syntax.all._
21+
import org.http4s.Uri
22+
import org.scalasteward.core.util.uri
23+
24+
final case class DependencyMetadata(
25+
homePage: Option[Uri],
26+
scmUrl: Option[Uri],
27+
releaseNotesUrl: Option[Uri]
28+
) {
29+
def enrichWith(other: DependencyMetadata): DependencyMetadata =
30+
DependencyMetadata(
31+
homePage = homePage.orElse(other.homePage),
32+
scmUrl = scmUrl.orElse(other.scmUrl),
33+
releaseNotesUrl = releaseNotesUrl.orElse(other.releaseNotesUrl)
34+
)
35+
36+
def filterUrls[F[_]](f: Uri => F[Boolean])(implicit F: Monad[F]): F[DependencyMetadata] =
37+
for {
38+
homePage <- homePage.filterA(f)
39+
scmUrl <- scmUrl.filterA(f)
40+
releaseNotesUrl <- releaseNotesUrl.filterA(f)
41+
} yield DependencyMetadata(homePage, scmUrl, releaseNotesUrl)
42+
43+
def repoUrl: Option[Uri] = {
44+
val urls = scmUrl.toList ++ homePage.toList
45+
urls.find(_.scheme.exists(uri.httpSchemes)).orElse(urls.headOption)
46+
}
47+
}
48+
49+
object DependencyMetadata {
50+
val empty: DependencyMetadata =
51+
DependencyMetadata(None, None, None)
52+
}

modules/core/src/main/scala/org/scalasteward/core/nurture/NurtureAlg.scala

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
package org.scalasteward.core.nurture
1818

19-
import cats.Id
19+
import cats.{Applicative, Id}
2020
import cats.effect.Concurrent
2121
import cats.implicits._
2222
import org.scalasteward.core.application.Config.VCSCfg
@@ -26,13 +26,12 @@ import org.scalasteward.core.data._
2626
import org.scalasteward.core.edit.{EditAlg, EditAttempt}
2727
import org.scalasteward.core.git.{Branch, Commit, GitAlg}
2828
import org.scalasteward.core.repoconfig.PullRequestUpdateStrategy
29-
import org.scalasteward.core.util.{Nel, UrlChecker}
3029
import org.scalasteward.core.util.logger.LoggerOps
30+
import org.scalasteward.core.util.{Nel, UrlChecker}
3131
import org.scalasteward.core.vcs.data._
3232
import org.scalasteward.core.vcs.{VCSApiAlg, VCSExtraAlg, VCSRepoAlg}
3333
import org.scalasteward.core.{git, util, vcs}
3434
import org.typelevel.log4cats.Logger
35-
import cats.Applicative
3635

3736
final class NurtureAlg[F[_]](config: VCSCfg)(implicit
3837
coursierAlg: CoursierAlg[F],
@@ -194,30 +193,39 @@ final class NurtureAlg[F[_]](config: VCSCfg)(implicit
194193
_.updates.flatMap(dependenciesUpdatedWithNextAndCurrentVersion(_))
195194
)
196195

197-
private def createPullRequest(data: UpdateData, edits: List[EditAttempt]): F[ProcessResult] =
196+
private[nurture] def preparePullRequest(
197+
data: UpdateData,
198+
edits: List[EditAttempt]
199+
): F[NewPullRequestData] =
198200
for {
199-
_ <- logger.info(s"Create PR ${data.updateBranch.name}")
201+
_ <- F.unit
200202
dependenciesWithNextVersion = dependenciesUpdatedWithNextAndCurrentVersion(data.update)
201203
resolvers = data.repoData.cache.dependencyInfos.flatMap(_.resolvers)
202-
dependencyScope = Scope(
203-
value = dependenciesWithNextVersion.map { case (_, dependency) => dependency },
204-
resolvers = resolvers
205-
)
206-
artifactIdToUrl <- coursierAlg.getArtifactIdUrlMapping(dependencyScope)
207-
existingArtifactUrlsList <- artifactIdToUrl.toList.filterA { case (_, uri) =>
208-
urlChecker.exists(uri)
209-
}
210-
existingArtifactUrlsMap = existingArtifactUrlsList.toMap
204+
dependencyToMetadata <- dependenciesWithNextVersion
205+
.traverse { case (_, dependency) =>
206+
coursierAlg
207+
.getMetadata(dependency, resolvers)
208+
.flatMap(_.filterUrls(urlChecker.exists))
209+
.tupleLeft(dependency)
210+
}
211+
.map(_.toMap)
212+
artifactIdToUrl = dependencyToMetadata.toList.mapFilter { case (dependency, metadata) =>
213+
metadata.repoUrl.tupleLeft(dependency.artifactId.name)
214+
}.toMap
211215
releaseRelatedUrls <- dependenciesWithNextVersion.flatTraverse {
212216
case (currentVersion, dependency) =>
213-
existingArtifactUrlsMap
214-
.get(dependency.artifactId.name)
215-
.toList
216-
.traverse(uri =>
217+
dependencyToMetadata.get(dependency).toList.flatTraverse { metadata =>
218+
metadata.repoUrl.toList.traverse { uri =>
217219
vcsExtraAlg
218-
.getReleaseRelatedUrls(uri, currentVersion, dependency.version)
220+
.getReleaseRelatedUrls(
221+
uri,
222+
metadata.releaseNotesUrl,
223+
currentVersion,
224+
dependency.version
225+
)
219226
.tupleLeft(dependency.artifactId.name)
220-
)
227+
}
228+
}
221229
}
222230
filesWithOldVersion <-
223231
data.update
@@ -229,11 +237,17 @@ final class NurtureAlg[F[_]](config: VCSCfg)(implicit
229237
data,
230238
branchName,
231239
edits,
232-
existingArtifactUrlsMap,
240+
artifactIdToUrl,
233241
releaseRelatedUrls.toMap,
234242
filesWithOldVersion,
235243
data.repoData.config.pullRequests.includeMatchedLabels
236244
)
245+
} yield requestData
246+
247+
private def createPullRequest(data: UpdateData, edits: List[EditAttempt]): F[ProcessResult] =
248+
for {
249+
_ <- logger.info(s"Create PR ${data.updateBranch.name}")
250+
requestData <- preparePullRequest(data, edits)
237251
pr <- vcsApiAlg.createPullRequest(data.repo, requestData)
238252
_ <- vcsApiAlg
239253
.labelPullRequest(data.repo, pr.number, requestData.labels)

modules/core/src/main/scala/org/scalasteward/core/util/uri.scala

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,9 @@ object uri {
4141
val withUserInfo: Optional[Uri, UserInfo] =
4242
authorityWithUserInfo.compose(withAuthority)
4343

44-
private val httpSchemes: Set[Scheme] =
45-
Set(Scheme.https, Scheme.http)
44+
def fromStringWithScheme(s: String): Option[Uri] =
45+
Uri.fromString(s).toOption.filter(_.scheme.isDefined)
4646

47-
def findBrowsableUrl(xs: List[String]): Option[Uri] = {
48-
val urls = xs.flatMap(Uri.fromString(_).toList).filter(_.scheme.isDefined)
49-
urls.find(_.scheme.exists(httpSchemes)).orElse(urls.headOption)
50-
}
47+
val httpSchemes: Set[Scheme] =
48+
Set(Scheme.https, Scheme.http)
5149
}

0 commit comments

Comments
 (0)