[github] use ref update rules calculate PR mergeability and update min API version to 3.0

this allows us to avoid loading all user teams (costly) and avoid loading branch protection rules (fails for non-admins)

#Fixed IDEA-336300

GitOrigin-RevId: 79416686cf100b391a446ba26e218f457387eb2b
This commit is contained in:
Ivan Semenov
2023-10-26 15:10:55 +02:00
committed by intellij-monorepo-bot
parent a83debdab2
commit cf4dbb64ff
14 changed files with 67 additions and 107 deletions

View File

@@ -24,6 +24,12 @@ fragment pullRequestInfo on PullRequest {
isFork
}
baseRef {
refUpdateRule {
...refUpdateRule
}
}
headRefName
headRefOid
headRepository {

View File

@@ -0,0 +1,10 @@
fragment refUpdateRule on RefUpdateRule {
allowsDeletions
allowsForcePushes
pattern
requiredApprovingReviewCount
requiredStatusCheckContexts
requiresLinearHistory
requiresSignatures
viewerCanPush
}

View File

@@ -3,8 +3,8 @@ package org.jetbrains.plugins.github.api
object GHEServerVersionChecker {
private const val REQUIRED_VERSION_MAJOR = 2
private const val REQUIRED_VERSION_MINOR = 21
private const val REQUIRED_VERSION_MAJOR = 3
private const val REQUIRED_VERSION_MINOR = 0
const val ENTERPRISE_VERSION_HEADER = "x-github-enterprise-version"

View File

@@ -5,7 +5,6 @@ import com.intellij.util.ThrowableConvertor
import org.jetbrains.plugins.github.api.GithubApiRequest.*
import org.jetbrains.plugins.github.api.data.*
import org.jetbrains.plugins.github.api.data.request.*
import org.jetbrains.plugins.github.api.util.GHSchemaPreview
import org.jetbrains.plugins.github.api.util.GithubApiPagesLoader
import org.jetbrains.plugins.github.api.util.GithubApiSearchQueryBuilder
import org.jetbrains.plugins.github.api.util.GithubApiUrlQueryBuilder
@@ -142,10 +141,6 @@ object GithubApiRequests {
@JvmStatic
fun get(url: String) = Get.jsonPage<GithubBranch>(url).withOperationName("get branches")
@JvmStatic
fun getProtection(repository: GHRepositoryCoordinates, branchName: String): GithubApiRequest<GHBranchProtectionRules> =
Get.json(getUrl(repository, urlSuffix, "/$branchName", "/protection"), GHSchemaPreview.BRANCH_PROTECTION.mimeType)
}
object Commits : Entity("/commits") {

View File

@@ -1,20 +0,0 @@
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package org.jetbrains.plugins.github.api.data
class GHBranchProtectionRules(val requiredStatusChecks: RequiredStatusChecks?,
val enforceAdmins: EnforceAdmins?,
val requiredPullRequestReviews: RequiredPullRequestReviews?,
val restrictions: Restrictions?) {
class RequiredStatusChecks(val strict: Boolean, val contexts: List<String>)
class EnforceAdmins(val enabled: Boolean)
class RequiredPullRequestReviews(val requiredApprovingReviewCount: Int)
class Restrictions(val users: List<UserLogin>?, val teams: List<TeamSlug>?)
class UserLogin(val login: String)
class TeamSlug(val slug: String)
}

View File

@@ -0,0 +1,24 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.plugins.github.api.data
import com.intellij.collaboration.api.dto.GraphQLFragment
@GraphQLFragment("graphql/fragment/refUpdateRule.graphql")
data class GHRefUpdateRule(
//Can this branch be deleted.
val allowsDeletions: Boolean,
//Are force pushes allowed on this branch.
val allowsForcePushes: Boolean,
//Identifies the protection rule pattern.
val pattern: String,
//Number of approving reviews required to update matching branches.
val requiredApprovingReviewCount: Int?,
//List of required status check contexts that must pass for commits to be accepted to matching branches.
val requiredStatusCheckContexts: List<String?>,
//Are merge commits prohibited from being pushed to this branch.
val requiresLinearHistory: Boolean,
//Are commits required to be signed.
val requiresSignatures: Boolean,
//Can the viewer push to the branch
val viewerCanPush: Boolean
)

View File

@@ -8,6 +8,7 @@ import com.intellij.collaboration.api.dto.GraphQLNodesDTO
import com.intellij.openapi.util.NlsSafe
import org.jetbrains.plugins.github.api.data.GHActor
import org.jetbrains.plugins.github.api.data.GHLabel
import org.jetbrains.plugins.github.api.data.GHRefUpdateRule
import org.jetbrains.plugins.github.api.data.GHUser
import java.util.*
@@ -33,6 +34,7 @@ class GHPullRequest(id: String,
val baseRefName: String,
val baseRefOid: String,
val baseRepository: Repository?,
baseRef: BaseRef?,
val headRefName: String,
val headRefOid: String,
val headRepository: HeadRepository?)
@@ -42,6 +44,9 @@ class GHPullRequest(id: String,
@JsonIgnore
val reviews: List<GHPullRequestReview> = reviews.nodes
@JsonIgnore
val baseRefUpdateRule: GHRefUpdateRule? = baseRef?.refUpdateRule
open class Repository(val owner: Owner, val isFork: Boolean)
class HeadRepository(owner: Owner, isFork: Boolean,
@@ -50,6 +55,8 @@ class GHPullRequest(id: String,
val sshUrl: @NlsSafe String)
: Repository(owner, isFork)
data class BaseRef(val refUpdateRule: GHRefUpdateRule?)
class Owner(val login: String)
override fun equals(other: Any?): Boolean {

View File

@@ -22,7 +22,6 @@ import org.jetbrains.plugins.github.api.GHGQLRequests
import org.jetbrains.plugins.github.api.GHRepositoryCoordinates
import org.jetbrains.plugins.github.api.GithubApiRequestExecutor
import org.jetbrains.plugins.github.api.GithubApiRequests
import org.jetbrains.plugins.github.api.data.GHRepositoryOwnerName
import org.jetbrains.plugins.github.api.data.GHUser
import org.jetbrains.plugins.github.api.util.SimpleGHGQLPagesLoader
import org.jetbrains.plugins.github.authentication.accounts.GithubAccount
@@ -102,25 +101,13 @@ internal class GHPRDataContextRepository(private val project: Project, parentCs:
accountDetails.name)
val repoOwner = repositoryInfo.owner
val currentUserTeams = if (repoOwner is GHRepositoryOwnerName.Organization) {
suspendingApiCall { indicator ->
SimpleGHGQLPagesLoader(requestExecutor, {
GHGQLRequests.Organization.Team.findByUserLogins(account.server, repoOwner.login, listOf(currentUser.login), it)
}).loadAll(indicator)
}
}
else {
emptyList()
}
// repository might have been renamed/moved
val apiRepositoryPath = repositoryInfo.path
val apiRepositoryCoordinates = GHRepositoryCoordinates(account.server, apiRepositoryPath)
val securityService = GHPRSecurityServiceImpl(GithubSharedProjectSettings.getInstance(project),
ghostUserDetails,
account, currentUser, currentUserTeams,
account, currentUser,
repositoryInfo)
val detailsService = GHPRDetailsServiceImpl(ProgressManager.getInstance(), requestExecutor, apiRepositoryCoordinates)
val stateService = GHPRStateServiceImpl(ProgressManager.getInstance(), project, securityService,
@@ -150,7 +137,7 @@ internal class GHPRDataContextRepository(private val project: Project, parentCs:
val repoDataService = GHPRRepositoryDataServiceImpl(ProgressManager.getInstance(), requestExecutor,
remoteCoordinates, apiRepositoryCoordinates,
repoOwner,
repositoryInfo.owner,
repositoryInfo.id, repositoryInfo.defaultBranch, repositoryInfo.isFork)
val iconsScope = contextScope.childScope(Dispatchers.Main)

View File

@@ -3,16 +3,13 @@ package org.jetbrains.plugins.github.pullrequest.data
import com.intellij.collaboration.ui.codereview.details.data.CodeReviewCIJob
import com.intellij.collaboration.ui.codereview.details.data.CodeReviewCIJobState
import com.intellij.util.containers.nullize
import org.jetbrains.plugins.github.api.data.GHBranchProtectionRules
import org.jetbrains.plugins.github.api.data.GHCommitCheckSuiteConclusion
import org.jetbrains.plugins.github.api.data.GHCommitStatusContextState
import org.jetbrains.plugins.github.api.data.GHRepositoryPermissionLevel
import org.jetbrains.plugins.github.api.data.GHRefUpdateRule
import org.jetbrains.plugins.github.api.data.pullrequest.GHPullRequestMergeStateStatus
import org.jetbrains.plugins.github.api.data.pullrequest.GHPullRequestMergeabilityData
import org.jetbrains.plugins.github.api.data.pullrequest.GHPullRequestMergeableState
import org.jetbrains.plugins.github.pullrequest.data.GHPRMergeabilityState.ChecksState
import org.jetbrains.plugins.github.pullrequest.data.service.GHPRSecurityService
class GHPRMergeabilityStateBuilder(private val headRefOid: String, private val prHtmlUrl: String,
private val mergeabilityData: GHPullRequestMergeabilityData) {
@@ -22,18 +19,12 @@ class GHPRMergeabilityStateBuilder(private val headRefOid: String, private val p
private var isRestricted = false
private var requiredApprovingReviewsCount = 0
fun withRestrictions(securityService: GHPRSecurityService, baseBranchProtectionRules: GHBranchProtectionRules) {
canOverrideAsAdmin = baseBranchProtectionRules.enforceAdmins?.enabled == false &&
securityService.currentUserHasPermissionLevel(GHRepositoryPermissionLevel.ADMIN)
requiredContexts = baseBranchProtectionRules.requiredStatusChecks?.contexts.orEmpty()
val restrictions = baseBranchProtectionRules.restrictions
val allowedLogins = restrictions?.users?.map { it.login }.nullize()
val allowedTeams = restrictions?.teams?.map { it.slug }.nullize()
isRestricted = (allowedLogins != null && !allowedLogins.contains(securityService.currentUser.login)) ||
(allowedTeams != null && !securityService.isUserInAnyTeam(allowedTeams))
requiredApprovingReviewsCount = baseBranchProtectionRules.requiredPullRequestReviews?.requiredApprovingReviewCount ?: 0
fun withRestrictions(currentUserIsAdmin: Boolean, refUpdateRule: GHRefUpdateRule) {
// TODO: load via PullRequest.viewerCanMergeAsAdmin when we update the min version
canOverrideAsAdmin = /*baseBranchProtectionRules.enforceAdmins?.enabled == false &&*/currentUserIsAdmin
requiredContexts = refUpdateRule.requiredStatusCheckContexts.filterNotNull()
isRestricted = !refUpdateRule.viewerCanPush
requiredApprovingReviewsCount = refUpdateRule.requiredApprovingReviewCount ?: 0
}
fun build(): GHPRMergeabilityState {

View File

@@ -35,7 +35,6 @@ class GHPRStateDataProviderImpl(private val stateService: GHPRStateService,
val details = detailsData.loadedDetails ?: return@addDetailsLoadedListener
if (lastKnownBaseBranch != null && lastKnownBaseBranch != details.baseRefName) {
baseBranchProtectionRulesRequestValue.drop()
reloadMergeabilityState()
}
lastKnownBaseBranch = details.baseRefName
@@ -50,18 +49,9 @@ class GHPRStateDataProviderImpl(private val stateService: GHPRStateService,
}
}
private val baseBranchProtectionRulesRequestValue = LazyCancellableBackgroundProcessValue.create { indicator ->
detailsData.loadDetails().thenCompose {
stateService.loadBranchProtectionRules(indicator, pullRequestId, it.baseRefName)
}
}
private val mergeabilityStateRequestValue = LazyCancellableBackgroundProcessValue.create { indicator ->
val baseBranchProtectionRulesRequest = baseBranchProtectionRulesRequestValue.value
detailsData.loadDetails().thenCompose { details ->
baseBranchProtectionRulesRequest.thenCompose {
stateService.loadMergeabilityState(indicator, pullRequestId, details.headRefOid, details.url, it)
}
stateService.loadMergeabilityState(indicator, pullRequestId, details.headRefOid, details.url, details.baseRefUpdateRule)
}
}
@@ -89,8 +79,6 @@ class GHPRStateDataProviderImpl(private val stateService: GHPRStateService,
}
override fun reloadMergeabilityState() {
if (baseBranchProtectionRulesRequestValue.lastLoadedValue == null)
baseBranchProtectionRulesRequestValue.drop()
mergeabilityStateRequestValue.drop()
}
@@ -122,6 +110,5 @@ class GHPRStateDataProviderImpl(private val stateService: GHPRStateService,
override fun dispose() {
mergeabilityStateRequestValue.drop()
baseBranchProtectionRulesRequestValue.drop()
}
}

View File

@@ -20,5 +20,4 @@ interface GHPRSecurityService {
fun isSquashMergeAllowed(): Boolean
fun isMergeForbiddenForProject(): Boolean
fun isUserInAnyTeam(slugs: List<String>): Boolean
}

View File

@@ -5,7 +5,6 @@ import org.jetbrains.plugins.github.api.data.GHRepository
import org.jetbrains.plugins.github.api.data.GHRepositoryPermissionLevel
import org.jetbrains.plugins.github.api.data.GHUser
import org.jetbrains.plugins.github.api.data.GithubUser
import org.jetbrains.plugins.github.api.data.pullrequest.GHTeam
import org.jetbrains.plugins.github.authentication.accounts.GithubAccount
import org.jetbrains.plugins.github.util.GithubSharedProjectSettings
@@ -13,15 +12,12 @@ class GHPRSecurityServiceImpl(private val sharedProjectSettings: GithubSharedPro
override val ghostUser: GHUser,
override val account: GithubAccount,
override val currentUser: GHUser,
private val currentUserTeams: List<GHTeam>,
private val repo: GHRepository) : GHPRSecurityService {
override fun isCurrentUser(user: GithubUser) = user.nodeId == currentUser.id
override fun currentUserHasPermissionLevel(level: GHRepositoryPermissionLevel) =
(repo.viewerPermission?.ordinal ?: -1) >= level.ordinal
override fun isUserInAnyTeam(slugs: List<String>) = currentUserTeams.any { slugs.contains(it.slug) }
override fun isMergeAllowed() = repo.mergeCommitAllowed
override fun isRebaseMergeAllowed() = repo.rebaseMergeAllowed
override fun isSquashMergeAllowed() = repo.squashMergeAllowed

View File

@@ -3,23 +3,19 @@ package org.jetbrains.plugins.github.pullrequest.data.service
import com.intellij.openapi.progress.ProgressIndicator
import org.jetbrains.annotations.CalledInAny
import org.jetbrains.plugins.github.api.data.GHBranchProtectionRules
import org.jetbrains.plugins.github.api.data.GHRefUpdateRule
import org.jetbrains.plugins.github.pullrequest.data.GHPRIdentifier
import org.jetbrains.plugins.github.pullrequest.data.GHPRMergeabilityState
import java.util.concurrent.CompletableFuture
interface GHPRStateService {
@CalledInAny
fun loadBranchProtectionRules(progressIndicator: ProgressIndicator, pullRequestId: GHPRIdentifier, baseBranch: String)
: CompletableFuture<GHBranchProtectionRules?>
@CalledInAny
fun loadMergeabilityState(progressIndicator: ProgressIndicator,
pullRequestId: GHPRIdentifier,
headRefOid: String,
prHtmlUrl: String,
baseBranchProtectionRules: GHBranchProtectionRules?): CompletableFuture<GHPRMergeabilityState>
baseRefUpdateRule: GHRefUpdateRule?): CompletableFuture<GHPRMergeabilityState>
@CalledInAny

View File

@@ -3,17 +3,17 @@ package org.jetbrains.plugins.github.pullrequest.data.service
import com.intellij.collaboration.async.CompletableFutureUtil.submitIOTask
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.progress.ProcessCanceledException
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.project.Project
import org.jetbrains.plugins.github.api.*
import org.jetbrains.plugins.github.api.data.GHBranchProtectionRules
import org.jetbrains.plugins.github.api.data.GHRefUpdateRule
import org.jetbrains.plugins.github.api.data.GHRepositoryPermissionLevel
import org.jetbrains.plugins.github.api.data.GithubIssueState
import org.jetbrains.plugins.github.api.data.GithubPullRequestMergeMethod
import org.jetbrains.plugins.github.pullrequest.GHPRStatisticsCollector
import org.jetbrains.plugins.github.pullrequest.data.GHPRIdentifier
import org.jetbrains.plugins.github.pullrequest.data.GHPRMergeabilityState
import org.jetbrains.plugins.github.pullrequest.data.GHPRMergeabilityStateBuilder
import org.jetbrains.plugins.github.pullrequest.data.service.GHServiceUtil.logError
import java.util.concurrent.CompletableFuture
@@ -28,35 +28,17 @@ class GHPRStateServiceImpl internal constructor(private val progressManager: Pro
private val repository = GHRepositoryCoordinates(serverPath, repoPath)
override fun loadBranchProtectionRules(progressIndicator: ProgressIndicator,
pullRequestId: GHPRIdentifier,
baseBranch: String): CompletableFuture<GHBranchProtectionRules?> {
if (!securityService.currentUserHasPermissionLevel(GHRepositoryPermissionLevel.WRITE)) return CompletableFuture.completedFuture(null)
return progressManager.submitIOTask(progressIndicator) {
try {
requestExecutor.execute(it, GithubApiRequests.Repos.Branches.getProtection(repository, baseBranch))
}
catch (e: Exception) {
// assume there are no restrictions
if (e !is ProcessCanceledException) LOG.info("Error occurred while loading branch protection rules for $baseBranch", e)
null
}
}
}
override fun loadMergeabilityState(progressIndicator: ProgressIndicator,
pullRequestId: GHPRIdentifier,
headRefOid: String,
prHtmlUrl: String,
baseBranchProtectionRules: GHBranchProtectionRules?) =
baseRefUpdateRule: GHRefUpdateRule?): CompletableFuture<GHPRMergeabilityState> =
progressManager.submitIOTask(progressIndicator) {
val mergeabilityData = requestExecutor.execute(it, GHGQLRequests.PullRequest.mergeabilityData(repository, pullRequestId.number))
?: error("Could not find pull request $pullRequestId.number")
val builder = GHPRMergeabilityStateBuilder(headRefOid, prHtmlUrl,
mergeabilityData)
if (baseBranchProtectionRules != null) {
builder.withRestrictions(securityService, baseBranchProtectionRules)
val builder = GHPRMergeabilityStateBuilder(headRefOid, prHtmlUrl, mergeabilityData)
if (baseRefUpdateRule != null) {
builder.withRestrictions(securityService.currentUserHasPermissionLevel(GHRepositoryPermissionLevel.ADMIN), baseRefUpdateRule)
}
builder.build()
}.logError(LOG, "Error occurred while loading mergeability state data for PR ${pullRequestId.number}")