Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add handling for opening existing pull requests and merge request from notifications #2954

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
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 @@ -165,6 +165,7 @@ pull.request.create.creating=Creating a pull request
pull.request.create.setting.metadata=Updating pull request fields
pull.request.create.error.details=Details
pull.request.notification.create.action=Create pull request
pull.request.notification.open.action=Open pull request

# TW content
toolwindow.stripe.Pull_Requests=Pull Requests
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// 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.github.notification

import com.intellij.dvcs.push.VcsPushOptionValue
Expand All @@ -16,11 +17,13 @@ import org.jetbrains.plugins.github.api.GithubApiRequestExecutor
import org.jetbrains.plugins.github.api.GithubApiRequests.Repos.PullRequests
import org.jetbrains.plugins.github.api.data.GithubIssueState
import org.jetbrains.plugins.github.api.data.pullrequest.GHPullRequestRestIdOnly
import org.jetbrains.plugins.github.api.data.pullrequest.toPRIdentifier
import org.jetbrains.plugins.github.api.executeSuspend
import org.jetbrains.plugins.github.authentication.accounts.GHAccountManager
import org.jetbrains.plugins.github.authentication.accounts.GithubAccount
import org.jetbrains.plugins.github.authentication.accounts.GithubProjectDefaultAccountHolder
import org.jetbrains.plugins.github.pullrequest.GHRepositoryConnectionManager
import org.jetbrains.plugins.github.pullrequest.action.GHOpenPullRequestExistingTabNotificationAction
import org.jetbrains.plugins.github.pullrequest.action.GHPRCreatePullRequestNotificationAction
import org.jetbrains.plugins.github.pullrequest.config.GithubPullRequestsProjectUISettings
import org.jetbrains.plugins.github.util.GHGitRepositoryMapping
Expand All @@ -35,80 +38,106 @@ internal class GHPushNotificationCustomizer(private val project: Project) : GitP
pushResult: GitPushRepoResult,
customParams: Map<String, VcsPushOptionValue>,
): List<AnAction> {

if (!pushResult.isSuccessful) return emptyList()
val remoteBranch = pushResult.findRemoteBranch(repository) ?: return emptyList()

// If we already have a GH connection open, make sure it matches
val connection = project.serviceAsync<GHRepositoryConnectionManager>().connectionState.value
if (connection != null && (connection.repo.gitRepository != repository || connection.repo.gitRemote != remoteBranch.remote)) {
return emptyList()
}

val (projectMapping, account) = connection?.let {
it.repo to it.account
} ?: GitPushNotificationUtil.findRepositoryAndAccount(
project.serviceAsync<GHHostedRepositoriesManager>().knownRepositories,
repository, remoteBranch.remote,
serviceAsync<GHAccountManager>().accountsState.value,
project.serviceAsync<GithubPullRequestsProjectUISettings>().selectedUrlAndAccount?.second,
project.serviceAsync<GithubProjectDefaultAccountHolder>().account
) ?: return emptyList()

val canCreate = canCreateReview(projectMapping, account, remoteBranch)
if (!canCreate) return emptyList()

return listOf(GHPRCreatePullRequestNotificationAction(project, projectMapping, account))
}
?: GitPushNotificationUtil.findRepositoryAndAccount(project.serviceAsync<GHHostedRepositoriesManager>().knownRepositories, repository, remoteBranch.remote, project.serviceAsync<GHAccountManager>().accountsState.value, project.serviceAsync<GithubPullRequestsProjectUISettings>().selectedUrlAndAccount?.second, project.serviceAsync<GithubProjectDefaultAccountHolder>().account)
?: return emptyList()


if (!canCreateReview(projectMapping, account, remoteBranch)) {
return emptyList()
}

val existingPrs = findExistingPullRequests(projectMapping, account, remoteBranch)
return when (existingPrs.size) {
0 -> {
listOf(GHPRCreatePullRequestNotificationAction(project, projectMapping, account))
}
1 -> {
val singlePr = existingPrs.first()
listOf(GHOpenPullRequestExistingTabNotificationAction(project, projectMapping, account, singlePr.toPRIdentifier()))
}
else -> {
emptyList()
}
}
}

private suspend fun canCreateReview(repositoryMapping: GHGitRepositoryMapping, account: GithubAccount, branch: GitRemoteBranch): Boolean {
/**
* Checks if it's even valid to create a PR.
* For instance:
* - The repository must exist
* - The branch cannot be the default branch (we don't allow creating a PR from default -> default)
*/
private suspend fun canCreateReview(
repositoryMapping: GHGitRepositoryMapping,
account: GithubAccount,
branch: GitRemoteBranch,
): Boolean {
val accountManager = serviceAsync<GHAccountManager>()
val token = accountManager.findCredentials(account) ?: return false
val executor = GithubApiRequestExecutor.Factory.getInstance().create(account.server, token)

val repository = repositoryMapping.repository
val repositoryInfo = getRepositoryInfo(executor, repository) ?: run {
LOG.warn("Repository not found $repository")
LOG.warn("Repository not found: $repository")
return false
}

// Don't allow creating PRs targeting the default branch
val remoteBranchName = branch.nameForRemoteOperations
if (repositoryInfo.defaultBranch == remoteBranchName) {
return false
}

if (findExistingPullRequests(executor, repository, remoteBranchName).isNotEmpty()) {
return false
}

return true
return repositoryInfo.defaultBranch != remoteBranchName
}

private suspend fun getRepositoryInfo(executor: GithubApiRequestExecutor, repository: GHRepositoryCoordinates) =
try {
executor.executeSuspend(GHGQLRequests.Repo.find(repository))
}
catch (ce: CancellationException) {
throw ce
}
catch (e: Exception) {
LOG.warn("Failed to lookup a repository $repository", e)
null
}

private suspend fun findExistingPullRequests(
private suspend fun getRepositoryInfo(
executor: GithubApiRequestExecutor,
repository: GHRepositoryCoordinates,
remoteBranchName: String,
): List<GHPullRequestRestIdOnly> = try {
executor.executeSuspend(PullRequests.find(repository,
GithubIssueState.open,
null,
repository.repositoryPath.owner + ":" + remoteBranchName)).items
) = try {
executor.executeSuspend(GHGQLRequests.Repo.find(repository))
}
catch (ce: CancellationException) {
throw ce
}
catch (e: Exception) {
LOG.warn("Failed to lookup an existing pull request for $remoteBranchName in $repository", e)
emptyList()
LOG.warn("Failed to lookup repository $repository", e)
null
}

/**
* Look up any existing open pull requests on the given remote branch.
*/
private suspend fun findExistingPullRequests(
repositoryMapping: GHGitRepositoryMapping,
account: GithubAccount,
branch: GitRemoteBranch,
): List<GHPullRequestRestIdOnly> {
val accountManager = serviceAsync<GHAccountManager>()
val token = accountManager.findCredentials(account) ?: return emptyList()
val executor = GithubApiRequestExecutor.Factory.getInstance().create(account.server, token)

val repository = repositoryMapping.repository
val remoteBranchName = branch.nameForRemoteOperations

return try {
executor.executeSuspend(PullRequests.find(repository, GithubIssueState.open, baseRef = null, headRef = repository.repositoryPath.owner + ":" + remoteBranchName)).items
}
catch (ce: CancellationException) {
throw ce
}
catch (e: Exception) {
LOG.warn("Failed to lookup existing pull requests for branch $remoteBranchName in $repository", e)
emptyList()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import com.intellij.openapi.project.DumbAwareAction
import com.intellij.openapi.project.Project
import org.jetbrains.plugins.github.authentication.accounts.GithubAccount
import org.jetbrains.plugins.github.i18n.GithubBundle
import org.jetbrains.plugins.github.pullrequest.data.GHPRIdentifier
import org.jetbrains.plugins.github.pullrequest.ui.toolwindow.GHPRToolWindowTab
import org.jetbrains.plugins.github.pullrequest.ui.toolwindow.model.GHPRToolWindowViewModel
import org.jetbrains.plugins.github.util.GHGitRepositoryMapping

Expand Down Expand Up @@ -40,14 +42,35 @@ internal class GHPRCreatePullRequestAction : DumbAwareAction() {
override fun actionPerformed(e: AnActionEvent) = tryToCreatePullRequest(e)
}

// NOTE: no need to register in plugin.xml
internal class GHOpenPullRequestExistingTabNotificationAction(
private val project: Project,
private val projectMapping: GHGitRepositoryMapping,
private val account: GithubAccount,
private val existingPrOrNull: GHPRIdentifier,
) : NotificationAction(GithubBundle.message("pull.request.notification.open.action")) {
override fun actionPerformed(e: AnActionEvent, notification: Notification) {
val twVm = project.service<GHPRToolWindowViewModel>()
val selectorVm = twVm.selectorVm
selectorVm.selectRepoAndAccount(projectMapping, account)
selectorVm.submitSelection()
openExistingTab(e, existingPrOrNull)
}
}

private fun openExistingTab(event: AnActionEvent, prId: GHPRIdentifier) {
event.project!!.service<GHPRToolWindowViewModel>().activateAndAwaitProject {
selectTab(GHPRToolWindowTab.PullRequest(prId))
}
}


// NOTE: no need to register in plugin.xml
internal class GHPRCreatePullRequestNotificationAction(
private val project: Project,
private val projectMapping: GHGitRepositoryMapping,
private val account: GithubAccount
) : NotificationAction(
GithubBundle.message("pull.request.notification.create.action")
) {
private val account: GithubAccount,
) : NotificationAction(GithubBundle.message("pull.request.notification.create.action")) {
override fun actionPerformed(e: AnActionEvent, notification: Notification) {
val twVm = project.service<GHPRToolWindowViewModel>()
val selectorVm = twVm.selectorVm
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,3 +199,4 @@ snippet.create.error.log-in=Log In
snippet.create.error.no-contents=No non-empty contents selected
snippet.create.error.some-empty-contents=Some files have empty contents
snippet.create.error.some-empty-contents.tooltip=Empty files are excluded from snippet: {0}
merge.request.notification.open.action=Open merge request
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import com.intellij.openapi.project.DumbAwareAction
import com.intellij.openapi.project.Project
import org.jetbrains.plugins.gitlab.GitlabIcons
import org.jetbrains.plugins.gitlab.authentication.accounts.GitLabAccount
import org.jetbrains.plugins.gitlab.mergerequest.ui.toolwindow.GitLabReviewTab
import org.jetbrains.plugins.gitlab.mergerequest.ui.toolwindow.model.GitLabToolWindowViewModel
import org.jetbrains.plugins.gitlab.util.GitLabBundle
import org.jetbrains.plugins.gitlab.util.GitLabProjectMapping
Expand Down Expand Up @@ -40,22 +41,39 @@ internal class GitLabMergeRequestOpenCreateTabAction : DumbAwareAction() {
}

override fun actionPerformed(e: AnActionEvent) {
val place = if (e.place == ActionPlaces.TOOLWINDOW_TITLE)
GitLabStatistics.ToolWindowOpenTabActionPlace.TOOLWINDOW
else
GitLabStatistics.ToolWindowOpenTabActionPlace.ACTION
val place = if (e.place == ActionPlaces.TOOLWINDOW_TITLE) GitLabStatistics.ToolWindowOpenTabActionPlace.TOOLWINDOW
else GitLabStatistics.ToolWindowOpenTabActionPlace.ACTION
openCreationTab(e, place)
}
}

internal class GitLabOpenMergeRequestExistingTabNotificationAction(
private val project: Project,
private val projectMapping: GitLabProjectMapping,
private val account: GitLabAccount,
private val existingMrOrNull: String,
) : NotificationAction(GitLabBundle.message("merge.request.notification.open.action")) {
override fun actionPerformed(e: AnActionEvent, notification: Notification) {
val twVm = project.service<GitLabToolWindowViewModel>()
val selectorVm = twVm.selectorVm.value ?: error("Tool window has not been initialized")
selectorVm.selectRepoAndAccount(projectMapping, account)
selectorVm.submitSelection()
openExistingTab(e, existingMrOrNull)
}
}

private fun openExistingTab(event: AnActionEvent, mrId: String) {
event.project!!.service<GitLabToolWindowViewModel>().activateAndAwaitProject {
selectTab(GitLabReviewTab.ReviewSelected(mrId))
}
}

// NOTE: no need to register in plugin.xml (or any xml-file replacing plugin.xml)
internal class GitLabMergeRequestOpenCreateTabNotificationAction(
private val project: Project,
private val projectMapping: GitLabProjectMapping,
private val account: GitLabAccount
) : NotificationAction(
GitLabBundle.message("merge.request.create.notification.action.text")
) {
private val account: GitLabAccount,
) : NotificationAction(GitLabBundle.message("merge.request.create.notification.action.text")) {
override fun actionPerformed(e: AnActionEvent, notification: Notification) {
val twVm = project.service<GitLabToolWindowViewModel>()
val selectorVm = twVm.selectorVm.value ?: error("Tool window has not been initialized")
Expand Down
Loading