mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[gitlab] Add statistics for number of MRs in the project (IJPL-185734)
#IJPL-185734 Fixed GitOrigin-RevId: 7613768d3b1288ef1be2cc0648bb8daa6e223e29
This commit is contained in:
committed by
intellij-monorepo-bot
parent
fb4ad9f7f6
commit
6b96ffc3c3
@@ -0,0 +1,23 @@
|
||||
query($projectId: ID!, $username: String!) {
|
||||
project(fullPath: $projectId) {
|
||||
allMRCount: mergeRequests(state: all) {
|
||||
count
|
||||
}
|
||||
|
||||
openMRCount: mergeRequests(state: opened) {
|
||||
count
|
||||
}
|
||||
|
||||
openAssignedMRCount: mergeRequests(state: opened, assigneeUsername: $username) {
|
||||
count
|
||||
}
|
||||
|
||||
openAuthoredMRCount: mergeRequests(state: opened, authorUsername: $username) {
|
||||
count
|
||||
}
|
||||
|
||||
openReviewAssignedMRCount: mergeRequests(state: opened, reviewerUsername: $username) {
|
||||
count
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
query($projectId: ID!) {
|
||||
project(fullPath: $projectId) {
|
||||
isForked
|
||||
}
|
||||
}
|
||||
@@ -70,12 +70,10 @@
|
||||
</extensions>
|
||||
|
||||
<extensions defaultExtensionNs="com.intellij.statistics">
|
||||
<applicationUsagesCollector
|
||||
implementation="org.jetbrains.plugins.gitlab.util.GitLabStatistics$GitLabAccountsStatisticsCollector"/>
|
||||
<counterUsagesCollector
|
||||
implementationClass="org.jetbrains.plugins.gitlab.util.GitLabStatistics$GitLabCountersCollector"/>
|
||||
<notificationIdsHolder
|
||||
implementation="org.jetbrains.plugins.gitlab.notification.GitLabNotificationIdsHolder"/>
|
||||
<applicationUsagesCollector implementation="org.jetbrains.plugins.gitlab.util.GitLabStatistics$GitLabAccountsStatisticsCollector"/>
|
||||
<counterUsagesCollector implementationClass="org.jetbrains.plugins.gitlab.util.GitLabStatistics$GitLabCountersCollector"/>
|
||||
<projectUsagesCollector implementation="org.jetbrains.plugins.gitlab.util.GitLabStatistics$GitLabProjectMetricsCollector"/>
|
||||
<notificationIdsHolder implementation="org.jetbrains.plugins.gitlab.notification.GitLabNotificationIdsHolder"/>
|
||||
<actionCustomPlaceAllowlist id="GitLabActionCustomPlaceAllowlist"
|
||||
places="GitLabMergeRequestListPopup;GitLabMergeRequestChangesTreePopup;GitLabMergeRequestDetailsPopup"/>
|
||||
</extensions>
|
||||
|
||||
@@ -9,6 +9,8 @@ enum class GitLabGQLQuery(val filePath: String) {
|
||||
GET_CURRENT_USER("graphql/query/getCurrentUser.graphql"),
|
||||
@SinceGitLab("12.0")
|
||||
GET_MERGE_REQUEST("graphql/query/getMergeRequest.graphql"),
|
||||
@SinceGitLab("14.0", note = "No exact version")
|
||||
GET_MERGE_REQUEST_METRICS("graphql/query/getMergeRequestMetrics.graphql"),
|
||||
@SinceGitLab("13.1")
|
||||
FIND_MERGE_REQUESTS("graphql/query/findProjectMergeRequests.graphql"),
|
||||
@SinceGitLab("14.7")
|
||||
@@ -21,6 +23,8 @@ enum class GitLabGQLQuery(val filePath: String) {
|
||||
GET_PROJECT_LABELS("graphql/query/getProjectLabels.graphql"),
|
||||
@SinceGitLab("15.2")
|
||||
GET_PROJECT_WORK_ITEMS("graphql/query/getProjectWidgets.graphql"),
|
||||
@SinceGitLab("16.9")
|
||||
GET_PROJECT_IS_FORKED("graphql/query/getProjectIsForked.graphql"),
|
||||
@SinceGitLab("13.0")
|
||||
GET_MEMBER_PROJECTS_FOR_SNIPPETS("graphql/query/getMemberProjectsForSnippets.graphql"),
|
||||
@SinceGitLab("13.0")
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package org.jetbrains.plugins.gitlab.api.dto
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
import org.jetbrains.plugins.gitlab.api.SinceGitLab
|
||||
|
||||
@SinceGitLab("12.0")
|
||||
data class GitLabProjectIsForkedDTO(
|
||||
private val forkedFromProject: Unit?
|
||||
) {
|
||||
@JsonIgnore
|
||||
val isForked: Boolean = forkedFromProject != null
|
||||
}
|
||||
+28
@@ -30,6 +30,34 @@ suspend fun GitLabApi.GraphQL.findProject(project: GitLabProjectCoordinates): Ht
|
||||
}
|
||||
}
|
||||
|
||||
@SinceGitLab("16.9")
|
||||
private suspend fun GitLabApi.GraphQL.isProjectForked(project: GitLabProjectCoordinates): HttpResponse<out Boolean> {
|
||||
val parameters = mapOf(
|
||||
"projectId" to project.projectPath.fullPath(),
|
||||
)
|
||||
val request = gitLabQuery(GitLabGQLQuery.GET_PROJECT_IS_FORKED, parameters)
|
||||
return withErrorStats(GitLabGQLQuery.GET_PROJECT_IS_FORKED) {
|
||||
loadResponse<Boolean>(request, "project", "isForked")
|
||||
}
|
||||
}
|
||||
|
||||
@SinceGitLab("12.0")
|
||||
private suspend fun GitLabApi.Rest.isProjectForked(project: GitLabProjectCoordinates): HttpResponse<out GitLabProjectIsForkedDTO> {
|
||||
val uri = project.restApiUri
|
||||
val request = request(uri).GET().build()
|
||||
return withErrorStats(GitLabApiRequestName.REST_GET_PROJECT_IS_FORKED) {
|
||||
loadJsonValue(request)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun GitLabApi.isProjectForked(project: GitLabProjectCoordinates): Boolean =
|
||||
if (getMetadata().version < GitLabVersion(16, 9)) {
|
||||
rest.isProjectForked(project).body().isForked
|
||||
}
|
||||
else {
|
||||
graphQL.isProjectForked(project).body()
|
||||
}
|
||||
|
||||
@SinceGitLab("13.1", note = "No exact version")
|
||||
fun GitLabApi.GraphQL.createAllProjectLabelsFlow(project: GitLabProjectCoordinates): Flow<List<GitLabLabelDTO>> =
|
||||
ApiPageUtil.createGQLPagesFlow { page ->
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package org.jetbrains.plugins.gitlab.mergerequest.api.dto
|
||||
|
||||
import com.intellij.collaboration.api.dto.GraphQLFragment
|
||||
|
||||
@GraphQLFragment("/graphql/query/getMergeRequestMetrics.graphql")
|
||||
data class GitLabMergeRequestMetricsDTO(
|
||||
val allMRCount: Count,
|
||||
val openMRCount: Count,
|
||||
val openAssignedMRCount: Count,
|
||||
val openAuthoredMRCount: Count,
|
||||
val openReviewAssignedMRCount: Count
|
||||
) {
|
||||
data class Count(val count: Int)
|
||||
}
|
||||
+18
@@ -7,12 +7,14 @@ import com.intellij.collaboration.api.graphql.loadResponse
|
||||
import com.intellij.collaboration.api.json.loadJsonValue
|
||||
import com.intellij.collaboration.util.resolveRelative
|
||||
import com.intellij.collaboration.util.withQuery
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import org.jetbrains.plugins.gitlab.api.*
|
||||
import org.jetbrains.plugins.gitlab.api.dto.GitLabGraphQLMutationResultDTO
|
||||
import org.jetbrains.plugins.gitlab.api.dto.GitLabReviewerDTO
|
||||
import org.jetbrains.plugins.gitlab.api.dto.GitLabUserDTO
|
||||
import org.jetbrains.plugins.gitlab.mergerequest.api.dto.GitLabMergeRequestByBranchDTO
|
||||
import org.jetbrains.plugins.gitlab.mergerequest.api.dto.GitLabMergeRequestDTO
|
||||
import org.jetbrains.plugins.gitlab.mergerequest.api.dto.GitLabMergeRequestMetricsDTO
|
||||
import org.jetbrains.plugins.gitlab.mergerequest.api.dto.GitLabMergeRequestRebaseDTO
|
||||
import org.jetbrains.plugins.gitlab.mergerequest.data.GitLabMergeRequestState
|
||||
import org.jetbrains.plugins.gitlab.mergerequest.data.asApiParameter
|
||||
@@ -41,6 +43,22 @@ suspend fun GitLabApi.GraphQL.loadMergeRequest(
|
||||
}
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
@SinceGitLab("14.0", note = "No exact version")
|
||||
suspend fun GitLabApi.GraphQL.getMergeRequestMetrics(
|
||||
project: GitLabProjectCoordinates,
|
||||
username: String,
|
||||
): HttpResponse<out GitLabMergeRequestMetricsDTO?> {
|
||||
val parameters = mapOf(
|
||||
"projectId" to project.projectPath.fullPath(),
|
||||
"username" to username
|
||||
)
|
||||
val request = gitLabQuery(GitLabGQLQuery.GET_MERGE_REQUEST_METRICS, parameters)
|
||||
return withErrorStats(GitLabGQLQuery.GET_MERGE_REQUEST_METRICS) {
|
||||
loadResponse(request, "project")
|
||||
}
|
||||
}
|
||||
|
||||
@SinceGitLab("13.1")
|
||||
suspend fun GitLabApi.GraphQL.findMergeRequestsByBranch(
|
||||
project: GitLabProjectCoordinates,
|
||||
|
||||
+128
-7
@@ -8,19 +8,29 @@ import com.intellij.internal.statistic.eventLog.events.EventId2
|
||||
import com.intellij.internal.statistic.eventLog.events.EventPair
|
||||
import com.intellij.internal.statistic.service.fus.collectors.ApplicationUsagesCollector
|
||||
import com.intellij.internal.statistic.service.fus.collectors.CounterUsagesCollector
|
||||
import com.intellij.internal.statistic.service.fus.collectors.ProjectUsagesCollector
|
||||
import com.intellij.internal.statistic.utils.StatisticsUtil.roundToPowerOfTwo
|
||||
import com.intellij.openapi.components.Service
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.components.serviceAsync
|
||||
import com.intellij.openapi.diagnostic.logger
|
||||
import com.intellij.openapi.project.Project
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import org.jetbrains.plugins.gitlab.api.GitLabEdition
|
||||
import org.jetbrains.plugins.gitlab.api.GitLabGQLQuery
|
||||
import org.jetbrains.plugins.gitlab.api.GitLabServerMetadata
|
||||
import org.jetbrains.plugins.gitlab.api.GitLabVersion
|
||||
import org.jetbrains.plugins.gitlab.GitLabProjectsManager
|
||||
import org.jetbrains.plugins.gitlab.api.*
|
||||
import org.jetbrains.plugins.gitlab.api.request.isProjectForked
|
||||
import org.jetbrains.plugins.gitlab.authentication.accounts.GitLabAccount
|
||||
import org.jetbrains.plugins.gitlab.authentication.accounts.GitLabAccountManager
|
||||
import org.jetbrains.plugins.gitlab.authentication.accounts.GitLabProjectDefaultAccountHolder
|
||||
import org.jetbrains.plugins.gitlab.mergerequest.api.dto.GitLabMergeRequestMetricsDTO
|
||||
import org.jetbrains.plugins.gitlab.mergerequest.api.request.getMergeRequestMetrics
|
||||
import org.jetbrains.plugins.gitlab.mergerequest.ui.filters.GitLabMergeRequestsFiltersValue
|
||||
|
||||
@ApiStatus.Internal
|
||||
object GitLabStatistics {
|
||||
//region State
|
||||
//region Application
|
||||
private val STATE_GROUP = EventLogGroup("vcs.gitlab", 1)
|
||||
|
||||
private val ACCOUNTS_EVENT: EventId2<Int, Boolean> =
|
||||
@@ -38,8 +48,62 @@ object GitLabStatistics {
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region Project
|
||||
private val PROJECT_METRICS_GROUP = EventLogGroup(
|
||||
"vcs.gitlab.project", 1,
|
||||
recorder = "FUS",
|
||||
description = "Collects metrics about GitLab merge requests in a project."
|
||||
)
|
||||
|
||||
private val PROJECT_METRICS_MR_STATISTICS_ALL = PROJECT_METRICS_GROUP.registerEvent(
|
||||
"mr.statistics.all",
|
||||
EventFields.Int("value", description = "Total number of MRs in project (rounded up to the first power of 2)."),
|
||||
description = "#MR statistics: open."
|
||||
)
|
||||
private val PROJECT_METRICS_MR_STATISTICS_OPEN = PROJECT_METRICS_GROUP.registerEvent(
|
||||
"mr.statistics.open",
|
||||
EventFields.Int("value", description = "Total number of open MRs in project (rounded up to the first power of 2)."),
|
||||
description = "#MR statistics: open."
|
||||
)
|
||||
private val PROJECT_METRICS_MR_STATISTICS_OPEN_AUTHOR = PROJECT_METRICS_GROUP.registerEvent(
|
||||
"mr.statistics.open.author",
|
||||
EventFields.Int("value",
|
||||
description = "Total number of open MRs in project authored by the current user (rounded up to the first power of 2)."),
|
||||
description = "#MR statistics: open > author."
|
||||
)
|
||||
private val PROJECT_METRICS_MR_STATISTICS_OPEN_ASSIGNEE = PROJECT_METRICS_GROUP.registerEvent(
|
||||
"mr.statistics.open.assignee",
|
||||
EventFields.Int("value",
|
||||
description = "Total number of open MRs in project assigned to the current user (rounded up to the first power of 2)."),
|
||||
description = "#MR statistics: open > assignee."
|
||||
)
|
||||
private val PROJECT_METRICS_MR_STATISTICS_OPEN_REVIEW_ASSIGNED = PROJECT_METRICS_GROUP.registerEvent(
|
||||
"mr.statistics.open.reviewer",
|
||||
EventFields.Int("value",
|
||||
description = "Total number of open MRs in project assigned to the current user as reviewer (rounded up to the first power of 2)."),
|
||||
description = "#MR statistics: open > reviewer."
|
||||
)
|
||||
|
||||
internal class GitLabProjectMetricsCollector : ProjectUsagesCollector() {
|
||||
override fun getGroup(): EventLogGroup = PROJECT_METRICS_GROUP
|
||||
|
||||
override suspend fun collect(project: Project): Set<MetricEvent> {
|
||||
val metricsLoader = project.serviceAsync<GitLabMetricsLoader>()
|
||||
val metrics = metricsLoader.getMetrics() ?: return emptySet()
|
||||
|
||||
return setOfNotNull(
|
||||
PROJECT_METRICS_MR_STATISTICS_ALL.metric(roundToPowerOfTwo(metrics.allMRCount.count)),
|
||||
PROJECT_METRICS_MR_STATISTICS_OPEN.metric(roundToPowerOfTwo(metrics.openMRCount.count)),
|
||||
PROJECT_METRICS_MR_STATISTICS_OPEN_AUTHOR.metric(roundToPowerOfTwo(metrics.openAuthoredMRCount.count)),
|
||||
PROJECT_METRICS_MR_STATISTICS_OPEN_ASSIGNEE.metric(roundToPowerOfTwo(metrics.openAssignedMRCount.count)),
|
||||
PROJECT_METRICS_MR_STATISTICS_OPEN_REVIEW_ASSIGNED.metric(roundToPowerOfTwo(metrics.openReviewAssignedMRCount.count)),
|
||||
)
|
||||
}
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region Counters
|
||||
private val COUNTERS_GROUP = EventLogGroup("vcs.gitlab.counters", version = 23)
|
||||
private val COUNTERS_GROUP = EventLogGroup("vcs.gitlab.counters", version = 24)
|
||||
|
||||
/**
|
||||
* Server metadata was fetched
|
||||
@@ -263,6 +327,7 @@ object GitLabStatistics {
|
||||
|
||||
enum class GitLabApiRequestName {
|
||||
REST_GET_CURRENT_USER,
|
||||
REST_GET_PROJECT_IS_FORKED,
|
||||
REST_GET_PROJECT_NAMESPACE,
|
||||
REST_GET_PROJECT_USERS,
|
||||
REST_GET_COMMIT,
|
||||
@@ -289,12 +354,13 @@ enum class GitLabApiRequestName {
|
||||
GQL_GET_CURRENT_USER,
|
||||
GQL_GET_MERGE_REQUEST,
|
||||
GQL_FIND_MERGE_REQUEST,
|
||||
GQL_GET_MERGE_REQUEST_METRICS,
|
||||
GQL_GET_MERGE_REQUEST_COMMITS,
|
||||
GQL_GET_MERGE_REQUEST_DISCUSSIONS,
|
||||
GQL_GET_PROJECT,
|
||||
GQL_GET_PROJECT_LABELS,
|
||||
GQL_GET_PROJECT_REPOSITORY,
|
||||
GQL_GET_PROJECT_WORK_ITEMS,
|
||||
GQL_GET_PROJECT_IS_FORKED,
|
||||
GQL_GET_MEMBER_PROJECTS_FOR_CLONE,
|
||||
GQL_GET_MEMBER_PROJECTS_FOR_SNIPPETS,
|
||||
GQL_TOGGLE_MERGE_REQUEST_DISCUSSION_RESOLVE,
|
||||
@@ -319,11 +385,13 @@ enum class GitLabApiRequestName {
|
||||
GitLabGQLQuery.GET_CURRENT_USER -> GQL_GET_CURRENT_USER
|
||||
GitLabGQLQuery.GET_MERGE_REQUEST -> GQL_GET_MERGE_REQUEST
|
||||
GitLabGQLQuery.FIND_MERGE_REQUESTS -> GQL_FIND_MERGE_REQUEST
|
||||
GitLabGQLQuery.GET_MERGE_REQUEST_METRICS -> GQL_GET_MERGE_REQUEST_METRICS
|
||||
GitLabGQLQuery.GET_MERGE_REQUEST_COMMITS -> GQL_GET_MERGE_REQUEST_COMMITS
|
||||
GitLabGQLQuery.GET_MERGE_REQUEST_DISCUSSIONS -> GQL_GET_MERGE_REQUEST_DISCUSSIONS
|
||||
GitLabGQLQuery.GET_PROJECT -> GQL_GET_PROJECT
|
||||
GitLabGQLQuery.GET_PROJECT_LABELS -> GQL_GET_PROJECT_LABELS
|
||||
GitLabGQLQuery.GET_PROJECT_WORK_ITEMS -> GQL_GET_PROJECT_WORK_ITEMS
|
||||
GitLabGQLQuery.GET_PROJECT_IS_FORKED -> GQL_GET_PROJECT_IS_FORKED
|
||||
GitLabGQLQuery.GET_MEMBER_PROJECTS_FOR_CLONE -> GQL_GET_MEMBER_PROJECTS_FOR_CLONE
|
||||
GitLabGQLQuery.GET_MEMBER_PROJECTS_FOR_SNIPPETS -> GQL_GET_MEMBER_PROJECTS_FOR_SNIPPETS
|
||||
GitLabGQLQuery.TOGGLE_MERGE_REQUEST_DISCUSSION_RESOLVE -> GQL_TOGGLE_MERGE_REQUEST_DISCUSSION_RESOLVE
|
||||
@@ -345,3 +413,56 @@ enum class GitLabApiRequestName {
|
||||
}
|
||||
}
|
||||
|
||||
@Service(Service.Level.PROJECT)
|
||||
internal class GitLabMetricsLoader(private val project: Project) {
|
||||
companion object {
|
||||
private val LOG = logger<GitLabMetricsLoader>()
|
||||
}
|
||||
|
||||
private suspend fun getApi(serverPath: GitLabServerPath): Pair<GitLabApi, GitLabAccount>? {
|
||||
val accountManager = serviceAsync<GitLabAccountManager>()
|
||||
val defaultAccountManager = project.serviceAsync<GitLabProjectDefaultAccountHolder>()
|
||||
|
||||
val account = defaultAccountManager.account.takeIf { it?.server == serverPath }
|
||||
?: accountManager.accountsState.value.firstOrNull { it.server == serverPath }
|
||||
?: return null
|
||||
val token = accountManager.findCredentials(account) ?: return null
|
||||
return service<GitLabApiManager>().getClient(serverPath, token) to account
|
||||
}
|
||||
|
||||
private suspend fun chooseRepo(): GitLabProjectMapping? {
|
||||
val repositoriesManager = project.serviceAsync<GitLabProjectsManager>()
|
||||
val repositoriesState = repositoriesManager.knownRepositoriesState
|
||||
|
||||
val knownRepos = repositoriesState.value
|
||||
|
||||
if (knownRepos.isEmpty()) return null
|
||||
if (knownRepos.size == 1) return knownRepos.single()
|
||||
|
||||
val nonFork = withContext(Dispatchers.IO) {
|
||||
knownRepos.firstOrNull { repo ->
|
||||
val (api, _) = getApi(repo.repository.serverPath) ?: return@firstOrNull false
|
||||
!api.isProjectForked(repo.repository)
|
||||
}
|
||||
}
|
||||
if (nonFork != null) return nonFork
|
||||
|
||||
return knownRepos.find { it.gitRemote.name == "origin" }
|
||||
?: knownRepos.firstOrNull()
|
||||
}
|
||||
|
||||
suspend fun getMetrics(): GitLabMergeRequestMetricsDTO? {
|
||||
try {
|
||||
val chosenRepoMapping = chooseRepo() ?: return null
|
||||
|
||||
val (api, account) = getApi(chosenRepoMapping.repository.serverPath) ?: return null
|
||||
return api.graphQL.getMergeRequestMetrics(chosenRepoMapping.repository, account.name).body()
|
||||
}
|
||||
catch (e: Exception) {
|
||||
LOG.warn("Failed to load metrics", e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user