Skip to content

Commit 1d3163f

Browse files
committed
Introduce PullRequestService to provide refreshed PR data
This change introduces `PullRequestService`, encapsulating `PullRequestRepository` & `ForgeApiAlg`
1 parent 35b0b74 commit 1d3163f

File tree

12 files changed

+331
-49
lines changed

12 files changed

+331
-49
lines changed

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,12 @@ import org.scalasteward.core.edit.update.ScannerAlg
4040
import org.scalasteward.core.forge.{ForgeApiAlg, ForgeAuthAlg, ForgeRepoAlg, ForgeSelection}
4141
import org.scalasteward.core.git.{GenGitAlg, GitAlg}
4242
import org.scalasteward.core.io.{FileAlg, ProcessAlg, WorkspaceAlg}
43-
import org.scalasteward.core.nurture.{NurtureAlg, PullRequestRepository, UpdateInfoUrlFinder}
43+
import org.scalasteward.core.nurture.{
44+
NurtureAlg,
45+
PullRequestRepository,
46+
PullRequestService,
47+
UpdateInfoUrlFinder
48+
}
4449
import org.scalasteward.core.persistence.{CachingKeyValueStore, JsonKeyValueStore}
4550
import org.scalasteward.core.repocache.*
4651
import org.scalasteward.core.repoconfig.{RepoConfigAlg, RepoConfigLoader}
@@ -171,6 +176,8 @@ object Context {
171176
implicit val updateInfoUrlFinder: UpdateInfoUrlFinder[F] = new UpdateInfoUrlFinder[F]
172177
implicit val pullRequestRepository: PullRequestRepository[F] =
173178
new PullRequestRepository[F](pullRequestsStore)
179+
implicit val pullRequestService: PullRequestService[F] =
180+
new PullRequestService[F](pullRequestRepository, forgeApiAlg)
174181
implicit val scalafixCli: ScalafixCli[F] = new ScalafixCli[F]
175182
implicit val scalafmtAlg: ScalafmtAlg[F] = new ScalafmtAlg[F](config.defaultResolvers)
176183
implicit val selfCheckAlg: SelfCheckAlg[F] = new SelfCheckAlg[F](config)

modules/core/src/main/scala/org/scalasteward/core/forge/ForgeType.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ sealed trait ForgeType extends Product with Serializable {
4444
def supportsForking: Boolean = true
4545
def supportsLabels: Boolean = true
4646

47-
/** Determines the `head` (GitHub) / `source_branch` (GitLab, Bitbucket) parameter for searching
48-
* for already existing pull requests or creating new pull requests.
47+
/** Determines the `head` (GitHub) / `source_branch` (GitLab, Bitbucket) parameter value for
48+
* searching for already existing pull requests or creating new pull requests.
4949
*/
5050
def pullRequestHeadFor(@nowarn fork: Repo, updateBranch: Branch): String = updateBranch.name
5151

modules/core/src/main/scala/org/scalasteward/core/git/package.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ package object git {
2626

2727
val updateBranchPrefix = "update"
2828

29-
def branchFor(update: Update, baseBranch: Option[Branch]): Branch = {
29+
def branchFor(update: Update, baseBranch: Option[Branch] = None): Branch = {
3030
val base = baseBranch.fold("")(branch => s"${branch.name}/")
3131
update.on(
3232
update = u => Branch(s"$updateBranchPrefix/$base${u.name}-${u.nextVersion}"),

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

Lines changed: 14 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ final class NurtureAlg[F[_]](config: ForgeCfg)(implicit
4141
forgeRepoAlg: ForgeRepoAlg[F],
4242
gitAlg: GitAlg[F],
4343
logger: Logger[F],
44-
pullRequestRepository: PullRequestRepository[F],
44+
pullRequestService: PullRequestService[F],
4545
updateInfoUrlFinder: UpdateInfoUrlFinder[F],
4646
urlChecker: UrlChecker[F],
4747
F: Concurrent[F]
@@ -92,8 +92,7 @@ final class NurtureAlg[F[_]](config: ForgeCfg)(implicit
9292
private def processUpdate(data: UpdateData): F[ProcessResult] =
9393
for {
9494
_ <- logger.info(s"Process update ${data.update.show}")
95-
head = config.tpe.pullRequestHeadFor(data.fork, data.updateBranch)
96-
pullRequests <- forgeApiAlg.listPullRequests(data.repo, head, data.baseBranch)
95+
pullRequests <- pullRequestService.listPullRequestsForUpdate(data, config.tpe)
9796
result <- pullRequests.headOption match {
9897
case Some(pr) if pr.state.isClosed && data.update.isInstanceOf[Update.Single] =>
9998
logger.info(s"PR ${pr.html_url} is closed") >>
@@ -106,22 +105,11 @@ final class NurtureAlg[F[_]](config: ForgeCfg)(implicit
106105
case _ => F.unit
107106
}
108107
}
109-
_ <- pullRequests.headOption.traverse_ { pr =>
110-
val prData = PullRequestData[Id](
111-
pr.html_url,
112-
data.baseSha1,
113-
data.update,
114-
pr.state,
115-
pr.number,
116-
data.updateBranch
117-
)
118-
pullRequestRepository.createOrUpdate(data.repo, prData)
119-
}
120108
} yield result
121109

122110
private def closeObsoletePullRequests(data: UpdateData, newNumber: PullRequestNumber): F[Unit] =
123111
data.update.on(
124-
update = pullRequestRepository
112+
update = pullRequestService
125113
.getObsoleteOpenPullRequests(data.repo, _)
126114
.flatMap(_.traverse_(oldPr => closeObsoletePullRequest(data, newNumber, oldPr))),
127115
// We don't support closing obsolete PRs for `GroupedUpdate`s
@@ -136,15 +124,14 @@ final class NurtureAlg[F[_]](config: ForgeCfg)(implicit
136124
logger.attemptWarn.label_(
137125
s"Closing obsolete PR ${oldPr.url.renderString} for ${oldPr.update.show} in PR #$newNumber - will not comment due to https://github.com/scala-steward-org/scala-steward/issues/3797"
138126
) {
127+
val oldRemoteBranch = oldPr.updateBranch.withPrefix("origin/")
139128
for {
140-
_ <- pullRequestRepository.changeState(data.repo, oldPr.url, PullRequestState.Closed)
141-
oldRemoteBranch = oldPr.updateBranch.withPrefix("origin/")
142129
oldBranchExists <- gitAlg.branchExists(data.repo, oldRemoteBranch)
143130
authors <-
144131
if (oldBranchExists) gitAlg.branchAuthors(data.repo, oldRemoteBranch, data.baseBranch)
145132
else List.empty.pure[F]
146133
_ <- F.whenA(authors.size <= 1) {
147-
forgeApiAlg.closePullRequest(data.repo, oldPr.number) >>
134+
pullRequestService.closePullRequest(data.repo, oldPr.number) >>
148135
deleteRemoteBranch(data.repo, oldPr.updateBranch)
149136
}
150137
} yield ()
@@ -249,16 +236,7 @@ final class NurtureAlg[F[_]](config: ForgeCfg)(implicit
249236
for {
250237
_ <- logger.info(s"Create PR ${data.updateBranch.name}")
251238
requestData <- preparePullRequest(data, edits)
252-
pr <- forgeApiAlg.createPullRequest(data.repo, requestData)
253-
prData = PullRequestData[Id](
254-
pr.html_url,
255-
data.baseSha1,
256-
data.update,
257-
pr.state,
258-
pr.number,
259-
data.updateBranch
260-
)
261-
_ <- pullRequestRepository.createOrUpdate(data.repo, prData)
239+
pr <- pullRequestService.createPullRequest(data, requestData)
262240
_ <- logger.info(s"Created PR ${pr.html_url}")
263241
} yield Created(pr.number)
264242

@@ -310,8 +288,8 @@ final class NurtureAlg[F[_]](config: ForgeCfg)(implicit
310288
} yield result
311289

312290
def closeRetractedPullRequests(data: RepoData): F[Unit] =
313-
pullRequestRepository
314-
.getRetractedPullRequests(data.repo, data.config.updatesOrDefault.retractedOrDefault)
291+
pullRequestService
292+
.getRetractedOpenPullRequests(data.repo, data.config.updatesOrDefault.retractedOrDefault)
315293
.flatMap {
316294
_.traverse_ { case (oldPr, retractedArtifact) =>
317295
closeRetractedPullRequest(data, oldPr, retractedArtifact)
@@ -327,10 +305,12 @@ final class NurtureAlg[F[_]](config: ForgeCfg)(implicit
327305
s"Closing retracted PR ${oldPr.url.renderString} for ${oldPr.update.show} because of '${retractedArtifact.reason}'"
328306
) {
329307
for {
330-
_ <- pullRequestRepository.changeState(data.repo, oldPr.url, PullRequestState.Closed)
331-
comment = retractedArtifact.retractionMsg
332-
_ <- forgeApiAlg.commentPullRequest(data.repo, oldPr.number, comment)
333-
_ <- forgeApiAlg.closePullRequest(data.repo, oldPr.number)
308+
_ <- forgeApiAlg.commentPullRequest(
309+
data.repo,
310+
oldPr.number,
311+
comment = retractedArtifact.retractionMsg
312+
)
313+
_ <- pullRequestService.closePullRequest(data.repo, oldPr.number)
334314
_ <- deleteRemoteBranch(data.repo, oldPr.updateBranch)
335315
} yield F.unit
336316
}

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

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,10 @@
1616

1717
package org.scalasteward.core.nurture
1818

19+
import cats.Id
1920
import org.http4s.Uri
20-
import org.scalasteward.core.data.Update
21-
import org.scalasteward.core.forge.data.{PullRequestNumber, PullRequestState}
21+
import org.scalasteward.core.data.{Update, UpdateData}
22+
import org.scalasteward.core.forge.data.{PullRequestNumber, PullRequestOut, PullRequestState}
2223
import org.scalasteward.core.git.{Branch, Sha1}
2324

2425
final case class PullRequestData[F[_]](
@@ -29,3 +30,14 @@ final case class PullRequestData[F[_]](
2930
number: F[PullRequestNumber],
3031
updateBranch: F[Branch]
3132
)
33+
34+
object PullRequestData {
35+
def apply(pr: PullRequestOut, updateData: UpdateData): PullRequestData[Id] = PullRequestData[Id](
36+
pr.html_url,
37+
updateData.baseSha1,
38+
updateData.update,
39+
pr.state,
40+
pr.number,
41+
updateData.updateBranch
42+
)
43+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ final class PullRequestRepository[F[_]](kvStore: KeyValueStore[F, Repo, Map[Uri,
7979
}.flatten.toList.sortBy(_.number.value)
8080
}
8181

82-
def getRetractedPullRequests(
82+
def getRetractedOpenPullRequests(
8383
repo: Repo,
8484
allRetractedArtifacts: List[RetractedArtifact]
8585
): F[List[(PullRequestData[Id], RetractedArtifact)]] =
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
* Copyright 2018-2025 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.nurture
18+
19+
import cats.*
20+
import cats.syntax.all.*
21+
import org.scalasteward.core.data.*
22+
import org.scalasteward.core.forge.data.{
23+
NewPullRequestData,
24+
PullRequestNumber,
25+
PullRequestOut,
26+
PullRequestState
27+
}
28+
import org.scalasteward.core.forge.{ForgeApiAlg, ForgeType}
29+
import org.scalasteward.core.repoconfig.RetractedArtifact
30+
31+
final class PullRequestService[F[_]](
32+
pullRequestRepository: PullRequestRepository[F],
33+
forgeApiAlg: ForgeApiAlg[F]
34+
)(implicit
35+
F: Monad[F]
36+
) {
37+
38+
def createPullRequest(
39+
updateData: UpdateData,
40+
requestData: NewPullRequestData
41+
): F[PullRequestOut] = for {
42+
pr <- forgeApiAlg.createPullRequest(updateData.repo, requestData)
43+
_ <- pullRequestRepository.createOrUpdate(updateData.repo, PullRequestData(pr, updateData))
44+
} yield pr
45+
46+
def listPullRequestsForUpdate(data: UpdateData, forgeType: ForgeType): F[List[PullRequestOut]] = {
47+
val head = forgeType.pullRequestHeadFor(data.fork, data.updateBranch)
48+
for {
49+
pullRequests <- forgeApiAlg.listPullRequests(data.repo, head, data.baseBranch)
50+
_ <- pullRequests.headOption.traverse_ { pr =>
51+
pullRequestRepository.createOrUpdate(data.repo, PullRequestData(pr, data))
52+
}
53+
} yield pullRequests
54+
}
55+
56+
def getObsoleteOpenPullRequests(repo: Repo, update: Update.Single): F[List[PullRequestData[Id]]] =
57+
flatTraverseOpts(pullRequestRepository.getObsoleteOpenPullRequests(repo, update))(
58+
refreshAndEnsureStillOpen(repo)
59+
)
60+
61+
def getRetractedOpenPullRequests(
62+
repo: Repo,
63+
allRetractedArtifacts: List[RetractedArtifact]
64+
): F[List[(PullRequestData[Id], RetractedArtifact)]] =
65+
flatTraverseOpts(
66+
pullRequestRepository.getRetractedOpenPullRequests(repo, allRetractedArtifacts)
67+
) { case (stalePr, retractedArtifact) =>
68+
refreshAndEnsureStillOpen(repo)(stalePr).map(_.map(_ -> retractedArtifact))
69+
}
70+
71+
private def flatTraverseOpts[T](items: F[List[T]])(f: T => F[Option[T]]): F[List[T]] =
72+
items.flatMap(_.flatTraverse(f(_).map(_.toList)))
73+
74+
private def refreshAndEnsureStillOpen(
75+
repo: Repo
76+
): PullRequestData[Id] => F[Option[PullRequestData[Id]]] = { stalePrData =>
77+
for {
78+
freshPr <- forgeApiAlg.getPullRequest(repo, stalePrData.number)
79+
_ <- pullRequestRepository.changeState(repo, stalePrData.url, freshPr.state)
80+
updatedPrData = stalePrData.copy(state = freshPr.state)
81+
} yield Option.when(updatedPrData.state == PullRequestState.Open)(updatedPrData)
82+
}
83+
84+
def closePullRequest(repo: Repo, number: PullRequestNumber): F[PullRequestOut] = for {
85+
pr <- forgeApiAlg.closePullRequest(repo, number)
86+
_ <- pullRequestRepository.changeState(repo, pr.html_url, PullRequestState.Closed)
87+
} yield pr
88+
}

modules/core/src/test/scala/org/scalasteward/core/forge/azurerepos/AzureReposApiAlgTest.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package org.scalasteward.core.forge.azurerepos
22

3-
import io.circe.literal.JsonStringContext
3+
import io.circe.literal.*
44
import munit.CatsEffectSuite
55
import org.http4s.circe.*
66
import org.http4s.dsl.Http4sDsl
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package org.scalasteward.core.mock
2+
3+
import cats.Endo
4+
import cats.data.Kleisli
5+
import org.http4s.Uri
6+
import org.scalasteward.core.data.Repo
7+
import org.scalasteward.core.forge.ForgeApiAlg
8+
import org.scalasteward.core.forge.data.*
9+
import org.scalasteward.core.forge.data.PullRequestState.{Closed, Open}
10+
import org.scalasteward.core.git.Branch
11+
12+
object MockForgeApiAlg {
13+
14+
case class PRState(initialData: NewPullRequestData, current: PullRequestOut) {
15+
def close: PRState = copy(current = current.copy(state = Closed))
16+
17+
def update(newData: NewPullRequestData): PRState =
18+
PRState(newData, current.copy(title = newData.title))
19+
}
20+
case class RepoState(highestPrNumber: Int = 0, prs: Map[PullRequestNumber, PRState] = Map.empty) {
21+
def createPullRequest(data: NewPullRequestData): (RepoState, PullRequestOut) = {
22+
val number = PullRequestNumber(highestPrNumber + 1)
23+
val pr = PullRequestOut(
24+
html_url = Uri.unsafeFromString(s"https://example.com/${number.value}"),
25+
state = Open,
26+
number,
27+
title = data.title
28+
)
29+
(copy(highestPrNumber = pr.number.value, prs = prs + (pr.number -> PRState(data, pr))), pr)
30+
}
31+
32+
def getPullRequest(num: PullRequestNumber): PullRequestOut = prs(num).current
33+
34+
def listPullRequests(head: String, base: Branch): List[PullRequestOut] = prs.values
35+
.filter(state => state.initialData.head == head && state.initialData.base == base)
36+
.map(_.current)
37+
.toList
38+
39+
private def update(number: PullRequestNumber, f: Endo[PRState]): (RepoState, PullRequestOut) = {
40+
val state = copy(prs = prs.updatedWith(number)(_.map(f)))
41+
(state, state.getPullRequest(number))
42+
}
43+
44+
def closePullRequest(number: PullRequestNumber): (RepoState, PullRequestOut) =
45+
update(number, _.close)
46+
47+
def updatePullRequest(
48+
number: PullRequestNumber,
49+
data: NewPullRequestData
50+
): (RepoState, PullRequestOut) =
51+
update(number, _.update(data))
52+
53+
}
54+
55+
case class MockForgeState(repos: Map[Repo, RepoState])
56+
57+
object MockForgeState {
58+
val empty = MockForgeState(Map.empty)
59+
}
60+
61+
implicit val mockApiAlg: ForgeApiAlg[MockEff] = new ForgeApiAlg[MockEff] {
62+
63+
private def modifyRepo[A](repo: Repo, f: RepoState => (RepoState, A)): MockEff[A] = Kleisli {
64+
_.modify { state =>
65+
val (repoState, output) = f(state.forgeState.repos(repo))
66+
state.copy(forgeState =
67+
state.forgeState.copy(repos = state.forgeState.repos + (repo -> repoState))
68+
) -> output
69+
}
70+
}
71+
72+
private def readRepo[A](repo: Repo, f: RepoState => A): MockEff[A] =
73+
Kleisli(_.get.map(state => f(state.forgeState.repos(repo))))
74+
75+
override def createPullRequest(repo: Repo, data: NewPullRequestData): MockEff[PullRequestOut] =
76+
modifyRepo(repo, _.createPullRequest(data))
77+
78+
override def closePullRequest(repo: Repo, number: PullRequestNumber): MockEff[PullRequestOut] =
79+
modifyRepo(repo, _.closePullRequest(number))
80+
81+
override def listPullRequests(
82+
repo: Repo,
83+
head: String,
84+
base: Branch
85+
): MockEff[List[PullRequestOut]] = readRepo(repo, _.listPullRequests(head, base))
86+
87+
override def getPullRequest(repo: Repo, number: PullRequestNumber): MockEff[PullRequestOut] =
88+
readRepo(repo, _.getPullRequest(number))
89+
90+
override def createFork(repo: Repo): MockEff[RepoOut] = ???
91+
92+
override def updatePullRequest(
93+
number: PullRequestNumber,
94+
repo: Repo,
95+
data: NewPullRequestData
96+
): MockEff[Unit] = ???
97+
98+
override def getBranch(repo: Repo, branch: Branch): MockEff[BranchOut] = ???
99+
100+
override def getRepo(repo: Repo): MockEff[RepoOut] = ???
101+
102+
override def commentPullRequest(
103+
repo: Repo,
104+
number: PullRequestNumber,
105+
comment: String
106+
): MockEff[Comment] = ???
107+
}
108+
}

0 commit comments

Comments
 (0)