Skip to content
Open
Show file tree
Hide file tree
Changes from all 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 @@ -40,7 +40,12 @@ import org.scalasteward.core.edit.update.ScannerAlg
import org.scalasteward.core.forge.{ForgeApiAlg, ForgeAuthAlg, ForgeRepoAlg, ForgeSelection}
import org.scalasteward.core.git.{GenGitAlg, GitAlg}
import org.scalasteward.core.io.{FileAlg, ProcessAlg, WorkspaceAlg}
import org.scalasteward.core.nurture.{NurtureAlg, PullRequestRepository, UpdateInfoUrlFinder}
import org.scalasteward.core.nurture.{
NurtureAlg,
PullRequestRepository,
PullRequestService,
UpdateInfoUrlFinder
}
import org.scalasteward.core.persistence.{CachingKeyValueStore, JsonKeyValueStore}
import org.scalasteward.core.repocache.*
import org.scalasteward.core.repoconfig.{RepoConfigAlg, RepoConfigLoader}
Expand Down Expand Up @@ -171,6 +176,8 @@ object Context {
implicit val updateInfoUrlFinder: UpdateInfoUrlFinder[F] = new UpdateInfoUrlFinder[F]
implicit val pullRequestRepository: PullRequestRepository[F] =
new PullRequestRepository[F](pullRequestsStore)
implicit val pullRequestService: PullRequestService[F] =
new PullRequestService[F](pullRequestRepository, forgeApiAlg)
implicit val scalafixCli: ScalafixCli[F] = new ScalafixCli[F]
implicit val scalafmtAlg: ScalafmtAlg[F] = new ScalafmtAlg[F](config.defaultResolvers)
implicit val selfCheckAlg: SelfCheckAlg[F] = new SelfCheckAlg[F](config)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ trait ForgeApiAlg[F[_]] {

def createPullRequest(repo: Repo, data: NewPullRequestData): F[PullRequestOut]

def getPullRequest(repo: Repo, number: PullRequestNumber): F[PullRequestOut]

def updatePullRequest(number: PullRequestNumber, repo: Repo, data: NewPullRequestData): F[Unit]

def closePullRequest(repo: Repo, number: PullRequestNumber): F[PullRequestOut]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ sealed trait ForgeType extends Product with Serializable {
def supportsForking: Boolean = true
def supportsLabels: Boolean = true

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ final class AzureReposApiAlg[F[_]](
): F[Unit] =
logger.warn("Updating PRs is not yet supported for Azure Repos")

// https://docs.microsoft.com/en-us/rest/api/azure/devops/git/pull-requests/get-pull-request?view=azure-devops-rest-7.1
override def getPullRequest(repo: Repo, number: PullRequestNumber): F[PullRequestOut] =
client.get[PullRequestOut](url.pullRequest(repo, number), modify)

// https://docs.microsoft.com/en-us/rest/api/azure/devops/git/pull-requests/update?view=azure-devops-rest-7.1
override def closePullRequest(repo: Repo, number: PullRequestNumber): F[PullRequestOut] =
client.patchWithBody[PullRequestOut, ClosePullRequestPayload](
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ class Url(apiHost: Uri, organization: String) {
def pullRequests(repo: Repo): Uri =
repos(repo) / "pullrequests"

def pullRequest(repo: Repo, number: PullRequestNumber): Uri =
pullRequests(repo) / number.value

def listPullRequests(repo: Repo, source: String, target: Branch): Uri =
pullRequests(repo).withQueryParams(
Map("searchCriteria.sourceRefName" -> source, "searchCriteria.targetRefName" -> target.name)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@ class BitbucketApiAlg[F[_]](
): F[Unit] =
logger.warn("Updating PRs is not yet supported for Bitbucket")

override def getPullRequest(repo: Repo, number: PullRequestNumber): F[PullRequestOut] =
client.get(url.pullRequest(repo, number), modify)

override def getBranch(repo: Repo, branch: Branch): F[BranchOut] =
client.get(url.branch(repo, branch), modify)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ final class BitbucketServerApiAlg[F[_]](
private val url = new Url(bitbucketApiHost)

override def closePullRequest(repo: Repo, number: PullRequestNumber): F[PullRequestOut] =
getPullRequest(repo, number).flatMap { pr =>
getBitbucketSpecificPullRequest(repo, number).flatMap { pr =>
val out = pr.toPullRequestOut.copy(state = PullRequestState.Closed)
declinePullRequest(repo, number, pr.version).as(out)
}
Expand Down Expand Up @@ -114,9 +114,12 @@ final class BitbucketServerApiAlg[F[_]](
private def getDefaultBranch(repo: Repo): F[Json.Branch] =
client.get[Json.Branch](url.defaultBranch(repo), modify)

private def getPullRequest(repo: Repo, number: PullRequestNumber): F[PR] =
private def getBitbucketSpecificPullRequest(repo: Repo, number: PullRequestNumber): F[PR] =
client.get[Json.PR](url.pullRequest(repo, number), modify)

override def getPullRequest(repo: Repo, number: PullRequestNumber): F[PullRequestOut] =
getBitbucketSpecificPullRequest(repo, number).map(_.toPullRequestOut)

override def getRepo(repo: Repo): F[RepoOut] =
for {
jRepo <- client.get[Json.Repo](url.repos(repo), modify)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,9 @@ final class GiteaApiAlg[F[_]: HttpJsonClient](
): F[Unit] =
logger.warn("Updating PRs is not yet supported for Gitea")

override def getPullRequest(repo: Repo, number: PullRequestNumber): F[PullRequestOut] =
client.get[PullRequestResp](url.pull(repo, number), modify).map(pullRequestOut)

override def closePullRequest(repo: Repo, number: PullRequestNumber): F[PullRequestOut] = {
val edit = EditPullRequestOption(state = "closed")
client
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ final class GitHubApiAlg[F[_]](
} yield ()
}

/** https://docs.github.com/en/rest/pulls/pulls?apiVersion=2022-11-28#get-a-pull-request */
override def getPullRequest(repo: Repo, number: PullRequestNumber): F[PullRequestOut] =
client.get(url.pull(repo, number), modify)

/** https://docs.github.com/en/rest/branches/branches?apiVersion=2022-11-28#get-a-branch */
override def getBranch(repo: Repo, branch: Branch): F[BranchOut] =
client.get(url.branches(repo, branch), modify)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,9 @@ final class GitLabApiAlg[F[_]: Parallel](
override def listPullRequests(repo: Repo, head: String, base: Branch): F[List[PullRequestOut]] =
client.get(url.listMergeRequests(repo, head, base.name), modify)

override def getPullRequest(repo: Repo, number: PullRequestNumber): F[PullRequestOut] =
client.get(url.existingMergeRequest(repo, number), modify)

override def createFork(repo: Repo): F[RepoOut] = {
val userOwnedRepo = repo.copy(owner = forgeCfg.login)
val data = ForkPayload(url.encodedProjectId(userOwnedRepo), forgeCfg.login)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ package object git {

val updateBranchPrefix = "update"

def branchFor(update: Update, baseBranch: Option[Branch]): Branch = {
def branchFor(update: Update, baseBranch: Option[Branch] = None): Branch = {
val base = baseBranch.fold("")(branch => s"${branch.name}/")
update.on(
update = u => Branch(s"$updateBranchPrefix/$base${u.name}-${u.nextVersion}"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ final class NurtureAlg[F[_]](config: ForgeCfg)(implicit
forgeRepoAlg: ForgeRepoAlg[F],
gitAlg: GitAlg[F],
logger: Logger[F],
pullRequestRepository: PullRequestRepository[F],
pullRequestService: PullRequestService[F],
updateInfoUrlFinder: UpdateInfoUrlFinder[F],
urlChecker: UrlChecker[F],
F: Concurrent[F]
Expand Down Expand Up @@ -92,8 +92,7 @@ final class NurtureAlg[F[_]](config: ForgeCfg)(implicit
private def processUpdate(data: UpdateData): F[ProcessResult] =
for {
_ <- logger.info(s"Process update ${data.update.show}")
head = config.tpe.pullRequestHeadFor(data.fork, data.updateBranch)
pullRequests <- forgeApiAlg.listPullRequests(data.repo, head, data.baseBranch)
pullRequests <- pullRequestService.listPullRequestsForUpdate(data, config.tpe)
result <- pullRequests.headOption match {
case Some(pr) if pr.state.isClosed && data.update.isInstanceOf[Update.Single] =>
logger.info(s"PR ${pr.html_url} is closed") >>
Expand All @@ -106,22 +105,11 @@ final class NurtureAlg[F[_]](config: ForgeCfg)(implicit
case _ => F.unit
}
}
_ <- pullRequests.headOption.traverse_ { pr =>
val prData = PullRequestData[Id](
pr.html_url,
data.baseSha1,
data.update,
pr.state,
pr.number,
data.updateBranch
)
pullRequestRepository.createOrUpdate(data.repo, prData)
}
} yield result

private def closeObsoletePullRequests(data: UpdateData, newNumber: PullRequestNumber): F[Unit] =
data.update.on(
update = pullRequestRepository
update = pullRequestService
.getObsoleteOpenPullRequests(data.repo, _)
Comment on lines -124 to 113
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is the key line that actually fixes issue #3797 - instead of just checking our file-based pullRequestRepository to query which of the pull-requests Scala Steward has opened are now obsolete, we now call our new pullRequestService - and that service additionally hits the Forge API to ensure that the PRs we get from the pullRequestRepository remain currently open.

.flatMap(_.traverse_(oldPr => closeObsoletePullRequest(data, newNumber, oldPr))),
// We don't support closing obsolete PRs for `GroupedUpdate`s
Expand All @@ -136,15 +124,14 @@ final class NurtureAlg[F[_]](config: ForgeCfg)(implicit
logger.attemptWarn.label_(
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"
) {
val oldRemoteBranch = oldPr.updateBranch.withPrefix("origin/")
for {
_ <- pullRequestRepository.changeState(data.repo, oldPr.url, PullRequestState.Closed)
oldRemoteBranch = oldPr.updateBranch.withPrefix("origin/")
oldBranchExists <- gitAlg.branchExists(data.repo, oldRemoteBranch)
authors <-
if (oldBranchExists) gitAlg.branchAuthors(data.repo, oldRemoteBranch, data.baseBranch)
else List.empty.pure[F]
_ <- F.whenA(authors.size <= 1) {
forgeApiAlg.closePullRequest(data.repo, oldPr.number) >>
pullRequestService.closePullRequest(data.repo, oldPr.number) >>
deleteRemoteBranch(data.repo, oldPr.updateBranch)
}
} yield ()
Expand Down Expand Up @@ -249,16 +236,7 @@ final class NurtureAlg[F[_]](config: ForgeCfg)(implicit
for {
_ <- logger.info(s"Create PR ${data.updateBranch.name}")
requestData <- preparePullRequest(data, edits)
pr <- forgeApiAlg.createPullRequest(data.repo, requestData)
prData = PullRequestData[Id](
pr.html_url,
data.baseSha1,
data.update,
pr.state,
pr.number,
data.updateBranch
)
_ <- pullRequestRepository.createOrUpdate(data.repo, prData)
pr <- pullRequestService.createPullRequest(data, requestData)
_ <- logger.info(s"Created PR ${pr.html_url}")
} yield Created(pr.number)

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

def closeRetractedPullRequests(data: RepoData): F[Unit] =
pullRequestRepository
.getRetractedPullRequests(data.repo, data.config.updatesOrDefault.retractedOrDefault)
pullRequestService
.getRetractedOpenPullRequests(data.repo, data.config.updatesOrDefault.retractedOrDefault)
.flatMap {
_.traverse_ { case (oldPr, retractedArtifact) =>
closeRetractedPullRequest(data, oldPr, retractedArtifact)
Expand All @@ -321,16 +299,14 @@ final class NurtureAlg[F[_]](config: ForgeCfg)(implicit
private def closeRetractedPullRequest(
data: RepoData,
oldPr: PullRequestData[Id],
retractedArtifact: RetractedArtifact
retraction: RetractedArtifact
): F[Unit] =
logger.attemptWarn.label_(
s"Closing retracted PR ${oldPr.url.renderString} for ${oldPr.update.show} because of '${retractedArtifact.reason}'"
s"Closing retracted PR ${oldPr.url.renderString} for ${oldPr.update.show} because of '${retraction.reason}'"
) {
for {
_ <- pullRequestRepository.changeState(data.repo, oldPr.url, PullRequestState.Closed)
comment = retractedArtifact.retractionMsg
_ <- forgeApiAlg.commentPullRequest(data.repo, oldPr.number, comment)
_ <- forgeApiAlg.closePullRequest(data.repo, oldPr.number)
_ <- forgeApiAlg.commentPullRequest(data.repo, oldPr.number, retraction.retractionMsg)
_ <- pullRequestService.closePullRequest(data.repo, oldPr.number)
_ <- deleteRemoteBranch(data.repo, oldPr.updateBranch)
} yield F.unit
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@

package org.scalasteward.core.nurture

import cats.Id
import org.http4s.Uri
import org.scalasteward.core.data.Update
import org.scalasteward.core.forge.data.{PullRequestNumber, PullRequestState}
import org.scalasteward.core.data.{Update, UpdateData}
import org.scalasteward.core.forge.data.{PullRequestNumber, PullRequestOut, PullRequestState}
import org.scalasteward.core.git.{Branch, Sha1}

final case class PullRequestData[F[_]](
Expand All @@ -29,3 +30,14 @@ final case class PullRequestData[F[_]](
number: F[PullRequestNumber],
updateBranch: F[Branch]
)

object PullRequestData {
def apply(pr: PullRequestOut, updateData: UpdateData): PullRequestData[Id] = PullRequestData[Id](
pr.html_url,
updateData.baseSha1,
updateData.update,
pr.state,
pr.number,
updateData.updateBranch
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ final class PullRequestRepository[F[_]](kvStore: KeyValueStore[F, Repo, Map[Uri,
}.flatten.toList.sortBy(_.number.value)
}

def getRetractedPullRequests(
def getRetractedOpenPullRequests(
repo: Repo,
allRetractedArtifacts: List[RetractedArtifact]
): F[List[(PullRequestData[Id], RetractedArtifact)]] =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* Copyright 2018-2025 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.nurture

import cats.*
import cats.syntax.all.*
import org.scalasteward.core.data.*
import org.scalasteward.core.forge.data.{
NewPullRequestData,
PullRequestNumber,
PullRequestOut,
PullRequestState
}
import org.scalasteward.core.forge.{ForgeApiAlg, ForgeType}
import org.scalasteward.core.repoconfig.RetractedArtifact

final class PullRequestService[F[_]](
pullRequestRepository: PullRequestRepository[F],
forgeApiAlg: ForgeApiAlg[F]
)(implicit
F: Monad[F]
) {

def createPullRequest(
updateData: UpdateData,
requestData: NewPullRequestData
): F[PullRequestOut] = for {
pr <- forgeApiAlg.createPullRequest(updateData.repo, requestData)
_ <- pullRequestRepository.createOrUpdate(updateData.repo, PullRequestData(pr, updateData))
} yield pr

def listPullRequestsForUpdate(data: UpdateData, forgeType: ForgeType): F[List[PullRequestOut]] = {
val head = forgeType.pullRequestHeadFor(data.fork, data.updateBranch)
for {
pullRequests <- forgeApiAlg.listPullRequests(data.repo, head, data.baseBranch)
_ <- pullRequests.headOption.traverse_ { pr =>
pullRequestRepository.createOrUpdate(data.repo, PullRequestData(pr, data))
}
} yield pullRequests
}

def getObsoleteOpenPullRequests(repo: Repo, update: Update.Single): F[List[PullRequestData[Id]]] =
flatTraverseOpts(pullRequestRepository.getObsoleteOpenPullRequests(repo, update))(
refreshAndEnsureStillOpen(repo)
)

def getRetractedOpenPullRequests(
repo: Repo,
allRetractedArtifacts: List[RetractedArtifact]
): F[List[(PullRequestData[Id], RetractedArtifact)]] =
flatTraverseOpts(
pullRequestRepository.getRetractedOpenPullRequests(repo, allRetractedArtifacts)
) { case (stalePr, retractedArtifact) =>
refreshAndEnsureStillOpen(repo)(stalePr).map(_.map(_ -> retractedArtifact))
}

private def flatTraverseOpts[T](items: F[List[T]])(f: T => F[Option[T]]): F[List[T]] =
items.flatMap(_.flatTraverse(f(_).map(_.toList)))

private def refreshAndEnsureStillOpen(
repo: Repo
): PullRequestData[Id] => F[Option[PullRequestData[Id]]] = { stalePrData =>
for {
freshPr <- forgeApiAlg.getPullRequest(repo, stalePrData.number)
_ <- pullRequestRepository.changeState(repo, stalePrData.url, freshPr.state)
updatedPrData = stalePrData.copy(state = freshPr.state)
} yield Option.when(updatedPrData.state == PullRequestState.Open)(updatedPrData)
}

def closePullRequest(repo: Repo, number: PullRequestNumber): F[PullRequestOut] = for {
pr <- forgeApiAlg.closePullRequest(repo, number)
_ <- pullRequestRepository.changeState(repo, pr.html_url, PullRequestState.Closed)
} yield pr
}
Loading