From 9aa6f3e3a2e565ce417c318a0971c309affa1210 Mon Sep 17 00:00:00 2001 From: Ivan Semenov Date: Mon, 1 Oct 2018 17:22:33 +0300 Subject: [PATCH] [github] Pull request merge actions --- .../com/intellij/util/io/HttpRequests.java | 31 ++++++---- plugins/github/resources/META-INF/plugin.xml | 16 ++++++ .../plugins/github/api/GithubApiRequest.kt | 18 ++++++ .../github/api/GithubApiRequestExecutor.kt | 1 + .../plugins/github/api/GithubApiRequests.kt | 20 +++++++ .../github/api/data/GithubPullRequest.java | 5 ++ .../api/data/GithubPullRequestDetailed.java | 15 ++++- .../api/data/GithubPullRequestMergeMethod.kt | 7 +++ .../GithubPullRequestMergeRebaseRequest.java | 16 ++++++ .../GithubPullRequestMergeRequest.java | 27 +++++++++ .../GithubPullRequestsComponentFactory.kt | 3 +- .../action/GithubPullRequestKeys.kt | 4 ++ .../GithubPullRequestMergeCommitAction.kt | 46 +++++++++++++++ .../action/GithubPullRequestRebaseAction.kt | 35 ++++++++++++ .../action/GithubPullRequestSquashAction.kt | 54 ++++++++++++++++++ .../merge/GithubPullRequestMergeActionBase.kt | 56 +++++++++++++++++++ .../GithubPullRequestMergeActionGroup.kt | 14 +++++ .../ui/GithubMergeCommitMessageDialog.kt | 47 ++++++++++++++++ .../plugins/github/util/GithubAsyncUtil.kt | 41 ++++++++++++++ 19 files changed, 442 insertions(+), 14 deletions(-) create mode 100644 plugins/github/src/org/jetbrains/plugins/github/api/data/GithubPullRequestMergeMethod.kt create mode 100644 plugins/github/src/org/jetbrains/plugins/github/api/requests/GithubPullRequestMergeRebaseRequest.java create mode 100644 plugins/github/src/org/jetbrains/plugins/github/api/requests/GithubPullRequestMergeRequest.java create mode 100644 plugins/github/src/org/jetbrains/plugins/github/pullrequest/action/GithubPullRequestMergeCommitAction.kt create mode 100644 plugins/github/src/org/jetbrains/plugins/github/pullrequest/action/GithubPullRequestRebaseAction.kt create mode 100644 plugins/github/src/org/jetbrains/plugins/github/pullrequest/action/GithubPullRequestSquashAction.kt create mode 100644 plugins/github/src/org/jetbrains/plugins/github/pullrequest/action/merge/GithubPullRequestMergeActionBase.kt create mode 100644 plugins/github/src/org/jetbrains/plugins/github/pullrequest/action/merge/GithubPullRequestMergeActionGroup.kt create mode 100644 plugins/github/src/org/jetbrains/plugins/github/pullrequest/action/ui/GithubMergeCommitMessageDialog.kt diff --git a/platform/platform-api/src/com/intellij/util/io/HttpRequests.java b/platform/platform-api/src/com/intellij/util/io/HttpRequests.java index 14ee50e31352..aa0e5ab055f7 100644 --- a/platform/platform-api/src/com/intellij/util/io/HttpRequests.java +++ b/platform/platform-api/src/com/intellij/util/io/HttpRequests.java @@ -175,14 +175,12 @@ public final class HttpRequests { @NotNull public static RequestBuilder post(@NotNull String url, @Nullable String contentType) { - return new RequestBuilderImpl(url, rawConnection -> { - HttpURLConnection connection = (HttpURLConnection)rawConnection; - connection.setRequestMethod("POST"); - connection.setDoOutput(true); - if (contentType != null) { - connection.setRequestProperty("Content-Type", contentType); - } - }); + return requestWithBody(url, "POST", contentType, null); + } + + @NotNull + public static RequestBuilder put(@NotNull String url, @Nullable String contentType) { + return requestWithBody(url, "PUT", contentType, null); } /** @@ -193,14 +191,23 @@ public final class HttpRequests { */ @NotNull public static RequestBuilder patch(@NotNull String url, @Nullable String contentType) { + return requestWithBody(url, "POST", contentType, + connection -> connection.setRequestProperty("X-HTTP-Method-Override", "PATCH")); + } + + @NotNull + private static RequestBuilder requestWithBody(@NotNull String url, + @NotNull String requestMethod, + @Nullable String contentType, + @Nullable ConnectionTuner tuner) { return new RequestBuilderImpl(url, rawConnection -> { HttpURLConnection connection = (HttpURLConnection)rawConnection; - connection.setRequestMethod("POST"); - connection.setRequestProperty("X-HTTP-Method-Override", "PATCH"); + connection.setRequestMethod(requestMethod); connection.setDoOutput(true); if (contentType != null) { connection.setRequestProperty("Content-Type", contentType); } + if (tuner != null) tuner.tune(connection); }); } @@ -592,12 +599,12 @@ public final class HttpRequests { HttpURLConnection httpURLConnection = (HttpURLConnection)connection; String method = httpURLConnection.getRequestMethod(); - if (method.equals("POST")) { + if (method.equals("POST") || method.equals("PUT")) { return connection; } LOG.assertTrue(method.equals("GET") || method.equals("HEAD") || method.equals("DELETE"), - "'" + method + "' not supported; please use GET, HEAD, DELETE or POST"); + "'" + method + "' not supported; please use GET, HEAD, DELETE, PUT or POST"); if (LOG.isDebugEnabled()) LOG.debug("connecting to " + url); int responseCode = httpURLConnection.getResponseCode(); diff --git a/plugins/github/resources/META-INF/plugin.xml b/plugins/github/resources/META-INF/plugin.xml index 12c5b47e8b85..fe0571d5aeff 100644 --- a/plugins/github/resources/META-INF/plugin.xml +++ b/plugins/github/resources/META-INF/plugin.xml @@ -75,11 +75,27 @@ + + + + + + + + + + + + diff --git a/plugins/github/src/org/jetbrains/plugins/github/api/GithubApiRequest.kt b/plugins/github/src/org/jetbrains/plugins/github/api/GithubApiRequest.kt index efb1d3dfb03e..6b906e41f335 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/api/GithubApiRequest.kt +++ b/plugins/github/src/org/jetbrains/plugins/github/api/GithubApiRequest.kt @@ -118,6 +118,24 @@ sealed class GithubApiRequest(val url: String) { } } + abstract class Put @JvmOverloads constructor(override val body: String, + override val bodyMimeType: String, + url: String, + override val acceptMimeType: String? = null) : GithubApiRequest.WithBody(url) { + companion object { + inline fun json(url: String, body: Any): Put = Json(url, body, T::class.java) + } + + open class Json(url: String, body: Any, clazz: Class) : Put(GithubApiContentHelper.toJson(body), + GithubApiContentHelper.JSON_MIME_TYPE, + url, + GithubApiContentHelper.V3_JSON_MIME_TYPE) { + private val typeToken = TypeToken.get(clazz) + + override fun extractResult(response: GithubApiResponse): T = parseJsonResponse(response, typeToken) + } + } + abstract class Patch @JvmOverloads constructor(override val body: String, override val bodyMimeType: String, url: String, diff --git a/plugins/github/src/org/jetbrains/plugins/github/api/GithubApiRequestExecutor.kt b/plugins/github/src/org/jetbrains/plugins/github/api/GithubApiRequestExecutor.kt index e6e6cf13e36b..bee1910250c1 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/api/GithubApiRequestExecutor.kt +++ b/plugins/github/src/org/jetbrains/plugins/github/api/GithubApiRequestExecutor.kt @@ -142,6 +142,7 @@ sealed class GithubApiRequestExecutor { return when (request) { is GithubApiRequest.Get -> HttpRequests.request(request.url) is GithubApiRequest.Post -> HttpRequests.post(request.url, request.bodyMimeType) + is GithubApiRequest.Put -> HttpRequests.put(request.url, request.bodyMimeType) is GithubApiRequest.Patch -> HttpRequests.patch(request.url, request.bodyMimeType) is GithubApiRequest.Head -> HttpRequests.head(request.url) is GithubApiRequest.Delete -> HttpRequests.delete(request.url) diff --git a/plugins/github/src/org/jetbrains/plugins/github/api/GithubApiRequests.kt b/plugins/github/src/org/jetbrains/plugins/github/api/GithubApiRequests.kt index 8c0b9ee0f380..1037e50dabef 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/api/GithubApiRequests.kt +++ b/plugins/github/src/org/jetbrains/plugins/github/api/GithubApiRequests.kt @@ -163,6 +163,26 @@ object GithubApiRequests { Post.json(getUrl(server, Repos.urlSuffix, "/$username/$repoName", urlSuffix), GithubPullRequestRequest(title, description, head, base)) .withOperationName("create pull request in $username/$repoName") + + @JvmStatic + fun merge(pullRequest: GithubPullRequest, commitSubject: String, commitBody: String, headSha: String) = + Put.json(getMergeUrl(pullRequest), + GithubPullRequestMergeRequest(commitSubject, commitBody, headSha, GithubPullRequestMergeMethod.merge)) + .withOperationName("merge pull request ${pullRequest.number}") + + @JvmStatic + fun squashMerge(pullRequest: GithubPullRequest, commitSubject: String, commitBody: String, headSha: String) = + Put.json(getMergeUrl(pullRequest), + GithubPullRequestMergeRequest(commitSubject, commitBody, headSha, GithubPullRequestMergeMethod.squash)) + .withOperationName("squash and merge pull request ${pullRequest.number}") + + @JvmStatic + fun rebaseMerge(pullRequest: GithubPullRequest, headSha: String) = + Put.json(getMergeUrl(pullRequest), + GithubPullRequestMergeRebaseRequest(headSha)) + .withOperationName("rebase and merge pull request ${pullRequest.number}") + + private fun getMergeUrl(pullRequest: GithubPullRequest) = pullRequest.url + "/merge" } } diff --git a/plugins/github/src/org/jetbrains/plugins/github/api/data/GithubPullRequest.java b/plugins/github/src/org/jetbrains/plugins/github/api/data/GithubPullRequest.java index f95a9cf5dae2..52b10199f9c5 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/api/data/GithubPullRequest.java +++ b/plugins/github/src/org/jetbrains/plugins/github/api/data/GithubPullRequest.java @@ -46,6 +46,11 @@ public class GithubPullRequest { @Mandatory private Links _links; private String authorAssociation; + @NotNull + public String getUrl() { + return url; + } + @NotNull public String getHtmlUrl() { return htmlUrl; diff --git a/plugins/github/src/org/jetbrains/plugins/github/api/data/GithubPullRequestDetailed.java b/plugins/github/src/org/jetbrains/plugins/github/api/data/GithubPullRequestDetailed.java index dd85d43c40cc..a4874bfe344a 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/api/data/GithubPullRequestDetailed.java +++ b/plugins/github/src/org/jetbrains/plugins/github/api/data/GithubPullRequestDetailed.java @@ -1,12 +1,13 @@ // Copyright 2000-2018 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; +import org.jetbrains.io.mandatory.Mandatory; import org.jetbrains.io.mandatory.RestModel; @RestModel @SuppressWarnings("UnusedDeclaration") public class GithubPullRequestDetailed extends GithubPullRequest { - private Boolean merged; + @Mandatory private Boolean merged; private Boolean mergeable; private Boolean rebaseable; private String mergeableState; @@ -19,4 +20,16 @@ public class GithubPullRequestDetailed extends GithubPullRequest { private Integer additions; private Integer deletions; private Integer changedFiles; + + public boolean getMerged() { + return merged; + } + + public boolean getMergeable() { + return mergeable != null && mergeable; + } + + public boolean getRebaseable() { + return rebaseable != null && rebaseable; + } } diff --git a/plugins/github/src/org/jetbrains/plugins/github/api/data/GithubPullRequestMergeMethod.kt b/plugins/github/src/org/jetbrains/plugins/github/api/data/GithubPullRequestMergeMethod.kt new file mode 100644 index 000000000000..7e1e7ac3e10e --- /dev/null +++ b/plugins/github/src/org/jetbrains/plugins/github/api/data/GithubPullRequestMergeMethod.kt @@ -0,0 +1,7 @@ +// Copyright 2000-2018 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 + +@Suppress("EnumEntryName") +enum class GithubPullRequestMergeMethod { + merge, squash, rebase +} diff --git a/plugins/github/src/org/jetbrains/plugins/github/api/requests/GithubPullRequestMergeRebaseRequest.java b/plugins/github/src/org/jetbrains/plugins/github/api/requests/GithubPullRequestMergeRebaseRequest.java new file mode 100644 index 000000000000..bc3623bcc609 --- /dev/null +++ b/plugins/github/src/org/jetbrains/plugins/github/api/requests/GithubPullRequestMergeRebaseRequest.java @@ -0,0 +1,16 @@ +// Copyright 2000-2018 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.requests; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.plugins.github.api.data.GithubPullRequestMergeMethod; + +@SuppressWarnings({"FieldCanBeLocal", "UnusedDeclaration"}) +public class GithubPullRequestMergeRebaseRequest { + @NotNull private final String sha; + @NotNull private final GithubPullRequestMergeMethod method; + + public GithubPullRequestMergeRebaseRequest(@NotNull String sha) { + this.sha = sha; + this.method = GithubPullRequestMergeMethod.rebase; + } +} diff --git a/plugins/github/src/org/jetbrains/plugins/github/api/requests/GithubPullRequestMergeRequest.java b/plugins/github/src/org/jetbrains/plugins/github/api/requests/GithubPullRequestMergeRequest.java new file mode 100644 index 000000000000..831fd13112e5 --- /dev/null +++ b/plugins/github/src/org/jetbrains/plugins/github/api/requests/GithubPullRequestMergeRequest.java @@ -0,0 +1,27 @@ +// Copyright 2000-2018 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.requests; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.plugins.github.api.data.GithubPullRequestMergeMethod; + +@SuppressWarnings({"FieldCanBeLocal", "UnusedDeclaration"}) +public class GithubPullRequestMergeRequest { + @NotNull private final String commitTitle; + @NotNull private final String commitMessage; + @NotNull private final String sha; + @NotNull private final GithubPullRequestMergeMethod method; + + public GithubPullRequestMergeRequest(@NotNull String commitTitle, + @NotNull String commitMessage, + @NotNull String sha, + @NotNull GithubPullRequestMergeMethod method) { + if (method != GithubPullRequestMergeMethod.merge && method != GithubPullRequestMergeMethod.squash) { + throw new IllegalArgumentException("Invalid merge method"); + } + + this.commitTitle = commitTitle; + this.commitMessage = commitMessage; + this.sha = sha; + this.method = method; + } +} diff --git a/plugins/github/src/org/jetbrains/plugins/github/pullrequest/GithubPullRequestsComponentFactory.kt b/plugins/github/src/org/jetbrains/plugins/github/pullrequest/GithubPullRequestsComponentFactory.kt index f769c7ddcd7c..d133d6fd2036 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/pullrequest/GithubPullRequestsComponentFactory.kt +++ b/plugins/github/src/org/jetbrains/plugins/github/pullrequest/GithubPullRequestsComponentFactory.kt @@ -48,7 +48,7 @@ class GithubPullRequestsComponentFactory(private val project: Project, return GithubPullRequestsComponent(requestExecutor, avatarIconsProviderFactory, repository, remote, repoDetails, account) } - private inner class GithubPullRequestsComponent(requestExecutor: GithubApiRequestExecutor, + private inner class GithubPullRequestsComponent(private val requestExecutor: GithubApiRequestExecutor, avatarIconsProviderFactory: CachingGithubAvatarIconsProvider.Factory, private val repository: GitRepository, private val remote: GitRemote, private val repoDetails: GithubRepoDetailed, @@ -99,6 +99,7 @@ class GithubPullRequestsComponentFactory(private val project: Project, GithubPullRequestKeys.REMOTE.`is`(dataId) -> remote GithubPullRequestKeys.REPO_DETAILS.`is`(dataId) -> repoDetails GithubPullRequestKeys.SERVER_PATH.`is`(dataId) -> account.server + GithubPullRequestKeys.API_REQUEST_EXECUTOR.`is`(dataId) -> requestExecutor GithubPullRequestKeys.PULL_REQUESTS_LIST_COMPONENT.`is`(dataId) -> list GithubPullRequestKeys.SELECTED_PULL_REQUEST_DATA_PROVIDER.`is`(dataId) -> list.selectionModel.current?.let(dataLoader::getDataProvider) diff --git a/plugins/github/src/org/jetbrains/plugins/github/pullrequest/action/GithubPullRequestKeys.kt b/plugins/github/src/org/jetbrains/plugins/github/pullrequest/action/GithubPullRequestKeys.kt index 388b0b8598c4..eb78386aeadf 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/pullrequest/action/GithubPullRequestKeys.kt +++ b/plugins/github/src/org/jetbrains/plugins/github/pullrequest/action/GithubPullRequestKeys.kt @@ -4,6 +4,7 @@ package org.jetbrains.plugins.github.pullrequest.action import com.intellij.openapi.actionSystem.DataKey import git4idea.repo.GitRemote import git4idea.repo.GitRepository +import org.jetbrains.plugins.github.api.GithubApiRequestExecutor import org.jetbrains.plugins.github.api.GithubServerPath import org.jetbrains.plugins.github.api.data.GithubRepoDetailed import org.jetbrains.plugins.github.api.data.GithubSearchedIssue @@ -12,6 +13,9 @@ import org.jetbrains.plugins.github.pullrequest.ui.GithubPullRequestsListCompone object GithubPullRequestKeys { @JvmStatic + val API_REQUEST_EXECUTOR = + DataKey.create("org.jetbrains.plugins.github.pullrequest.requestexecutor") + @JvmStatic val PULL_REQUESTS_LIST_COMPONENT = DataKey.create("org.jetbrains.plugins.github.pullrequest.list.component") @JvmStatic diff --git a/plugins/github/src/org/jetbrains/plugins/github/pullrequest/action/GithubPullRequestMergeCommitAction.kt b/plugins/github/src/org/jetbrains/plugins/github/pullrequest/action/GithubPullRequestMergeCommitAction.kt new file mode 100644 index 000000000000..fe15e590e860 --- /dev/null +++ b/plugins/github/src/org/jetbrains/plugins/github/pullrequest/action/GithubPullRequestMergeCommitAction.kt @@ -0,0 +1,46 @@ +// Copyright 2000-2018 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.pullrequest.action + +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.Task +import com.intellij.openapi.project.Project +import org.jetbrains.plugins.github.api.GithubApiRequestExecutor +import org.jetbrains.plugins.github.api.GithubApiRequests +import org.jetbrains.plugins.github.api.data.GithubPullRequestDetailed +import org.jetbrains.plugins.github.pullrequest.action.merge.GithubPullRequestMergeActionBase +import org.jetbrains.plugins.github.pullrequest.action.ui.GithubMergeCommitMessageDialog +import org.jetbrains.plugins.github.util.GithubNotifications + +class GithubPullRequestMergeCommitAction : GithubPullRequestMergeActionBase("Create a Merge Commit...") { + override fun canMerge(details: GithubPullRequestDetailed) = details.mergeable + + override fun mergePullRequest(e: AnActionEvent, + project: Project, requestExecutor: GithubApiRequestExecutor, details: GithubPullRequestDetailed) { + val dialog = GithubMergeCommitMessageDialog(project, + "Merge Pull Request", + "Merge pull request #${details.number} from ${details.head.label}", + details.title) + if (!dialog.showAndGet()) return + + val commitMessage = dialog.message + + object : Task.Backgroundable(project, "Merging Pull Request", true) { + override fun run(indicator: ProgressIndicator) { + requestExecutor.execute(indicator, + GithubApiRequests.Repos.PullRequests.merge(details, + commitMessage.first, commitMessage.second, + details.head.sha)) + } + + override fun onSuccess() { + GithubNotifications.showInfo(project, "Pull Request Merged", "Successfully merged pull request #${details.number}") + //TODO: refresh table and details + } + + override fun onThrowable(error: Throwable) { + GithubNotifications.showError(project, "Failed To Merge Pull Request", error) + } + }.queue() + } +} diff --git a/plugins/github/src/org/jetbrains/plugins/github/pullrequest/action/GithubPullRequestRebaseAction.kt b/plugins/github/src/org/jetbrains/plugins/github/pullrequest/action/GithubPullRequestRebaseAction.kt new file mode 100644 index 000000000000..2919c4a02c9e --- /dev/null +++ b/plugins/github/src/org/jetbrains/plugins/github/pullrequest/action/GithubPullRequestRebaseAction.kt @@ -0,0 +1,35 @@ +// Copyright 2000-2018 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.pullrequest.action + +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.Task +import com.intellij.openapi.project.Project +import org.jetbrains.plugins.github.api.GithubApiRequestExecutor +import org.jetbrains.plugins.github.api.GithubApiRequests +import org.jetbrains.plugins.github.api.data.GithubPullRequestDetailed +import org.jetbrains.plugins.github.pullrequest.action.merge.GithubPullRequestMergeActionBase +import org.jetbrains.plugins.github.util.GithubNotifications + +class GithubPullRequestRebaseAction : GithubPullRequestMergeActionBase("Rebase and Merge") { + override fun canMerge(details: GithubPullRequestDetailed) = details.rebaseable + + override fun mergePullRequest(e: AnActionEvent, + project: Project, requestExecutor: GithubApiRequestExecutor, details: GithubPullRequestDetailed) { + object : Task.Backgroundable(project, "Rebasing and Merging Pull Request", true) { + override fun run(indicator: ProgressIndicator) { + requestExecutor.execute(indicator, + GithubApiRequests.Repos.PullRequests.rebaseMerge(details, details.head.sha)) + } + + override fun onSuccess() { + GithubNotifications.showInfo(project, "Pull Request Rebased and Merged", + "Successfully rebased and merged pull request #${details.number}") + } + + override fun onThrowable(error: Throwable) { + GithubNotifications.showError(project, "Failed To Rebase and Merge Pull Request", error) + } + }.queue() + } +} \ No newline at end of file diff --git a/plugins/github/src/org/jetbrains/plugins/github/pullrequest/action/GithubPullRequestSquashAction.kt b/plugins/github/src/org/jetbrains/plugins/github/pullrequest/action/GithubPullRequestSquashAction.kt new file mode 100644 index 000000000000..7d14842941c2 --- /dev/null +++ b/plugins/github/src/org/jetbrains/plugins/github/pullrequest/action/GithubPullRequestSquashAction.kt @@ -0,0 +1,54 @@ +// Copyright 2000-2018 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.pullrequest.action + +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.Task +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.text.StringUtil +import org.jetbrains.plugins.github.api.GithubApiRequestExecutor +import org.jetbrains.plugins.github.api.GithubApiRequests +import org.jetbrains.plugins.github.api.data.GithubPullRequestDetailed +import org.jetbrains.plugins.github.pullrequest.action.merge.GithubPullRequestMergeActionBase +import org.jetbrains.plugins.github.pullrequest.action.ui.GithubMergeCommitMessageDialog +import org.jetbrains.plugins.github.util.GithubAsyncUtil +import org.jetbrains.plugins.github.util.GithubNotifications + +class GithubPullRequestSquashAction : GithubPullRequestMergeActionBase("Squash and Merge...") { + override fun canMerge(details: GithubPullRequestDetailed) = details.mergeable + + override fun mergePullRequest(e: AnActionEvent, + project: Project, requestExecutor: GithubApiRequestExecutor, details: GithubPullRequestDetailed) { + val commitsRequest = e.getRequiredData(GithubPullRequestKeys.SELECTED_PULL_REQUEST_DATA_PROVIDER).logCommitsRequest + + GithubAsyncUtil.awaitFutureAndRunOnEdt(commitsRequest, + project, "Loading Pull Request Commits", "Failed to Load Pull Request Commits") { commits -> + val body = "* " + StringUtil.join(commits, { it.subject }, "\n\n* ") + val dialog = GithubMergeCommitMessageDialog(project, + "Merge Pull Request", + "${details.title} (#${details.number})", + body) + if (!dialog.showAndGet()) return@awaitFutureAndRunOnEdt + + val commitMessage = dialog.message + + object : Task.Backgroundable(project, "Squashing and Merging Pull Request", true) { + override fun run(indicator: ProgressIndicator) { + requestExecutor.execute(indicator, + GithubApiRequests.Repos.PullRequests.squashMerge(details, + commitMessage.first, commitMessage.second, + details.head.sha)) + } + + override fun onSuccess() { + GithubNotifications.showInfo(project, "Pull Request Squashed and Merged", + "Successfully squashed amd merged pull request #${details.number}") + } + + override fun onThrowable(error: Throwable) { + GithubNotifications.showError(project, "Failed To Squash and Merge Pull Request", error) + } + }.queue() + } + } +} \ No newline at end of file diff --git a/plugins/github/src/org/jetbrains/plugins/github/pullrequest/action/merge/GithubPullRequestMergeActionBase.kt b/plugins/github/src/org/jetbrains/plugins/github/pullrequest/action/merge/GithubPullRequestMergeActionBase.kt new file mode 100644 index 000000000000..aa72443ecc6d --- /dev/null +++ b/plugins/github/src/org/jetbrains/plugins/github/pullrequest/action/merge/GithubPullRequestMergeActionBase.kt @@ -0,0 +1,56 @@ +// Copyright 2000-2018 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.pullrequest.action.merge + +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.openapi.project.DumbAwareAction +import com.intellij.openapi.project.Project +import org.jetbrains.plugins.github.api.GithubApiRequestExecutor +import org.jetbrains.plugins.github.api.data.GithubIssueState +import org.jetbrains.plugins.github.api.data.GithubPullRequestDetailed +import org.jetbrains.plugins.github.pullrequest.action.GithubPullRequestKeys +import org.jetbrains.plugins.github.util.GithubAsyncUtil +import org.jetbrains.plugins.github.util.GithubNotifications + +abstract class GithubPullRequestMergeActionBase(text: String) : DumbAwareAction(text) { + override fun update(e: AnActionEvent) { + //TODO: project-level switch + val permissions = e.getRequiredData(GithubPullRequestKeys.REPO_DETAILS).permissions + e.presentation.isEnabledAndVisible = (permissions.isPush || permissions.isAdmin) && isEnabled(e) + } + + private fun isEnabled(e: AnActionEvent): Boolean { + val pullRequest = e.getData(GithubPullRequestKeys.SELECTED_PULL_REQUEST) ?: return false + return pullRequest.state == GithubIssueState.open + } + + final override fun actionPerformed(e: AnActionEvent) { + val project = e.getRequiredData(CommonDataKeys.PROJECT) + val detailsRequest = e.getRequiredData(GithubPullRequestKeys.SELECTED_PULL_REQUEST_DATA_PROVIDER).detailsRequest + + GithubAsyncUtil.awaitFutureAndRunOnEdt(detailsRequest, + project, "Loading Pull Request Details", "Failed to Load Pull Request Details") { details -> + if (!preCheck(project, details)) return@awaitFutureAndRunOnEdt + mergePullRequest(e, project, e.getRequiredData(GithubPullRequestKeys.API_REQUEST_EXECUTOR), details) + } + } + + private fun preCheck(project: Project, details: GithubPullRequestDetailed): Boolean { + if (details.merged) { + GithubNotifications.showError(project, "Failed to Merge Pull Request", "Pull request #${details.number} is already merged") + return false + } + if (!canMerge(details)) { + GithubNotifications.showError(project, "Failed to Merge Pull Request", + "Cannot merge pull request #${details.number} due to conflicts") + return false + } + + return true + } + + protected abstract fun canMerge(details: GithubPullRequestDetailed): Boolean + + protected abstract fun mergePullRequest(e: AnActionEvent, + project: Project, requestExecutor: GithubApiRequestExecutor, details: GithubPullRequestDetailed) +} \ No newline at end of file diff --git a/plugins/github/src/org/jetbrains/plugins/github/pullrequest/action/merge/GithubPullRequestMergeActionGroup.kt b/plugins/github/src/org/jetbrains/plugins/github/pullrequest/action/merge/GithubPullRequestMergeActionGroup.kt new file mode 100644 index 000000000000..8381e659c065 --- /dev/null +++ b/plugins/github/src/org/jetbrains/plugins/github/pullrequest/action/merge/GithubPullRequestMergeActionGroup.kt @@ -0,0 +1,14 @@ +// Copyright 2000-2018 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.pullrequest.action.merge + +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.DefaultActionGroup +import com.intellij.openapi.project.DumbAware +import org.jetbrains.plugins.github.pullrequest.action.GithubPullRequestKeys + +class GithubPullRequestMergeActionGroup : DefaultActionGroup(), DumbAware { + override fun update(e: AnActionEvent) { + val permissions = e.getRequiredData(GithubPullRequestKeys.REPO_DETAILS).permissions + e.presentation.isVisible = (permissions.isPush || permissions.isAdmin) + } +} \ No newline at end of file diff --git a/plugins/github/src/org/jetbrains/plugins/github/pullrequest/action/ui/GithubMergeCommitMessageDialog.kt b/plugins/github/src/org/jetbrains/plugins/github/pullrequest/action/ui/GithubMergeCommitMessageDialog.kt new file mode 100644 index 000000000000..7ce18388a331 --- /dev/null +++ b/plugins/github/src/org/jetbrains/plugins/github/pullrequest/action/ui/GithubMergeCommitMessageDialog.kt @@ -0,0 +1,47 @@ +// Copyright 2000-2018 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.pullrequest.action.ui + +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.DialogWrapper +import com.intellij.openapi.util.Disposer +import com.intellij.openapi.vcs.ui.CommitMessage +import com.intellij.util.ui.JBDimension +import com.intellij.util.ui.JBUI +import com.intellij.util.ui.UIUtil +import javax.swing.JComponent +import javax.swing.JLabel + +class GithubMergeCommitMessageDialog(project: Project, title: String, subject: String, body: String) : DialogWrapper(project) { + + private val commitMessage = CommitMessage(project, false, false, true).apply { + setCommitMessage("$subject\n\n$body") + preferredSize = JBDimension(500, 85) + } + + init { + Disposer.register(disposable, commitMessage) + + setTitle(title) + setOKButtonText("Merge") + init() + } + + override fun createCenterPanel(): JComponent? { + return JBUI.Panels.simplePanel(0, UIUtil.DEFAULT_VGAP) + .addToTop(JLabel("Merge commit message:")) + .addToCenter(commitMessage) + } + + val message: Pair + get() { + val text = commitMessage.comment + + val idx = text.indexOf("\n\n") + return if (idx < 0) "" to text + else { + val subject = text.substring(0, idx) + if (subject.contains("\n")) "" to text + else subject to text.substring(idx + 2) + } + } +} \ No newline at end of file diff --git a/plugins/github/src/org/jetbrains/plugins/github/util/GithubAsyncUtil.kt b/plugins/github/src/org/jetbrains/plugins/github/util/GithubAsyncUtil.kt index 5f4c69adfb3b..78546015daf5 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/util/GithubAsyncUtil.kt +++ b/plugins/github/src/org/jetbrains/plugins/github/util/GithubAsyncUtil.kt @@ -2,12 +2,53 @@ package org.jetbrains.plugins.github.util import com.intellij.openapi.application.runInEdt +import com.intellij.openapi.progress.PerformInBackgroundOption import com.intellij.openapi.progress.ProcessCanceledException +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.Task +import com.intellij.openapi.project.Project import java.util.concurrent.* import java.util.function.BiFunction object GithubAsyncUtil { + /** + * Run [consumer] on EDT with the result of [future] + * If future is cancelled, [consumer] will not be executed + * + * This is a naive implementation with timeout waiting + */ + @JvmStatic + fun , T> awaitFutureAndRunOnEdt(future: R, + project: Project, title: String, errorTitle: String, + consumer: (T) -> Unit) { + object : Task.Backgroundable(project, title, true, PerformInBackgroundOption.DEAF) { + var result: T? = null + + override fun run(indicator: ProgressIndicator) { + while (true) { + try { + result = future.get(50, TimeUnit.MILLISECONDS) + break + } + catch (e: TimeoutException) { + indicator.checkCanceled() + } + } + indicator.checkCanceled() + } + + override fun onSuccess() { + result?.let(consumer) + } + + override fun onThrowable(error: Throwable) { + if (isCancellation(error)) return + GithubNotifications.showError(project, errorTitle, error) + } + }.queue() + } + fun isCancellation(error: Throwable): Boolean { return error is ProcessCanceledException || error is CancellationException