mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
github: implement clone dialog extension
GitOrigin-RevId: d49a6e291e56c41374eb4ffd83b074cf04d3396a
This commit is contained in:
committed by
intellij-monorepo-bot
parent
8485c2815d
commit
e76aabe924
@@ -26,5 +26,6 @@ object VcsCloneDialogUiSpec {
|
||||
object Components {
|
||||
// insets for component without full-height scrollbar
|
||||
val rightInsets = JBUI.insetsRight((UIUtil.PANEL_REGULAR_INSETS.right))
|
||||
val avatarSize = JBValue.UIInteger("VcsCloneDialog.Accounts.AvatarSize", 22)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,8 @@
|
||||
implementation="org.jetbrains.plugins.github.authentication.accounts.GithubAccountsStatisticsCollector"/>
|
||||
|
||||
<diff.DiffExtension implementation="org.jetbrains.plugins.github.pullrequest.comment.GithubPullRequestCommentsDiffExtension"/>
|
||||
<openapi.vcs.ui.cloneDialog.VcsCloneDialogExtension
|
||||
implementation="org.jetbrains.plugins.github.ui.cloneDialog.GHCloneDialogExtension"/>
|
||||
</extensions>
|
||||
|
||||
<extensions defaultExtensionNs="Git4Idea">
|
||||
|
||||
@@ -50,6 +50,7 @@ import org.jetbrains.annotations.NonNls
|
||||
import org.jetbrains.annotations.TestOnly
|
||||
import org.jetbrains.plugins.github.api.GithubApiRequestExecutorManager
|
||||
import org.jetbrains.plugins.github.api.GithubApiRequests
|
||||
import org.jetbrains.plugins.github.api.data.request.Type
|
||||
import org.jetbrains.plugins.github.api.util.GithubApiPagesLoader
|
||||
import org.jetbrains.plugins.github.authentication.GithubAuthenticationManager
|
||||
import org.jetbrains.plugins.github.authentication.accounts.GithubAccount
|
||||
@@ -136,7 +137,7 @@ class GithubShareAction : DumbAwareAction("Share Project on GitHub", "Easily sha
|
||||
val user = requestExecutor.execute(progressManager.progressIndicator, GithubApiRequests.CurrentUser.get(account.server))
|
||||
val names = GithubApiPagesLoader
|
||||
.loadAll(requestExecutor, progressManager.progressIndicator,
|
||||
GithubApiRequests.CurrentUser.Repos.pages(account.server, false))
|
||||
GithubApiRequests.CurrentUser.Repos.pages(account.server, Type.OWNER))
|
||||
.mapSmartSet { it.name }
|
||||
user.canCreatePrivateRepo() to names
|
||||
}, "Loading Account Information For $account", true, project)
|
||||
|
||||
@@ -35,14 +35,27 @@ object GithubApiRequests {
|
||||
object Repos : Entity("/repos") {
|
||||
@JvmOverloads
|
||||
@JvmStatic
|
||||
fun pages(server: GithubServerPath, allAssociated: Boolean = true, pagination: GithubRequestPagination? = null) =
|
||||
GithubApiPagesLoader.Request(get(server, allAssociated, pagination), ::get)
|
||||
fun pages(server: GithubServerPath,
|
||||
type: Type = Type.DEFAULT,
|
||||
visibility: Visibility = Visibility.DEFAULT,
|
||||
affiliation: Affiliation = Affiliation.DEFAULT,
|
||||
pagination: GithubRequestPagination? = null) =
|
||||
GithubApiPagesLoader.Request(get(server, type, visibility, affiliation, pagination), ::get)
|
||||
|
||||
@JvmOverloads
|
||||
@JvmStatic
|
||||
fun get(server: GithubServerPath, allAssociated: Boolean = true, pagination: GithubRequestPagination? = null) =
|
||||
get(getUrl(server, CurrentUser.urlSuffix, urlSuffix,
|
||||
getQuery(if (allAssociated) "" else "type=owner", pagination?.toString().orEmpty())))
|
||||
fun get(server: GithubServerPath,
|
||||
type: Type = Type.DEFAULT,
|
||||
visibility: Visibility = Visibility.DEFAULT,
|
||||
affiliation: Affiliation = Affiliation.DEFAULT,
|
||||
pagination: GithubRequestPagination? = null): GithubApiRequest<GithubResponsePage<GithubRepo>> {
|
||||
if (type != Type.DEFAULT && (visibility != Visibility.DEFAULT || affiliation != Affiliation.DEFAULT)) {
|
||||
throw IllegalArgumentException("Param 'type' should not be used together with 'visibility' or 'affiliation'")
|
||||
}
|
||||
|
||||
return get(getUrl(server, CurrentUser.urlSuffix, urlSuffix,
|
||||
getQuery(type.toString(), visibility.toString(), affiliation.toString(), pagination?.toString().orEmpty())))
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun get(url: String) = Get.jsonPage<GithubRepo>(url).withOperationName("get user repositories")
|
||||
@@ -54,6 +67,18 @@ object GithubApiRequests {
|
||||
.withOperationName("create user repository")
|
||||
}
|
||||
|
||||
object Orgs : Entity("/orgs") {
|
||||
@JvmOverloads
|
||||
@JvmStatic
|
||||
fun pages(server: GithubServerPath, pagination: GithubRequestPagination? = null) =
|
||||
GithubApiPagesLoader.Request(get(server, pagination), ::get)
|
||||
|
||||
fun get(server: GithubServerPath, pagination: GithubRequestPagination? = null) =
|
||||
get(getUrl(server, CurrentUser.urlSuffix, urlSuffix, getQuery(pagination?.toString().orEmpty())))
|
||||
|
||||
fun get(url: String) = Get.jsonPage<GithubOrg>(url).withOperationName("get user organizations")
|
||||
}
|
||||
|
||||
object RepoSubs : Entity("/subscriptions") {
|
||||
@JvmStatic
|
||||
fun pages(server: GithubServerPath) = GithubApiPagesLoader.Request(get(server), ::get)
|
||||
@@ -72,12 +97,13 @@ object GithubApiRequests {
|
||||
|
||||
object Repos : Entity("/repos") {
|
||||
@JvmStatic
|
||||
fun pages(server: GithubServerPath, organisation: String) = GithubApiPagesLoader.Request(get(server, organisation), ::get)
|
||||
fun pages(server: GithubServerPath, organisation: String, pagination: GithubRequestPagination? = null) =
|
||||
GithubApiPagesLoader.Request(get(server, organisation, pagination), ::get)
|
||||
|
||||
@JvmOverloads
|
||||
@JvmStatic
|
||||
fun get(server: GithubServerPath, organisation: String, pagination: GithubRequestPagination? = null) =
|
||||
get(getUrl(server, Organisations.urlSuffix, "/", organisation, urlSuffix, pagination?.toString().orEmpty()))
|
||||
get(getUrl(server, Organisations.urlSuffix, "/", organisation, urlSuffix, getQuery(pagination?.toString().orEmpty())))
|
||||
|
||||
@JvmStatic
|
||||
fun get(url: String) = Get.jsonPage<GithubRepo>(url).withOperationName("get organisation repositories")
|
||||
|
||||
@@ -1,7 +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 class GithubRepositoryPath(private val serverPath: GithubServerPath, private val repositoryPath: GithubFullPath) {
|
||||
data class GithubRepositoryPath(internal val serverPath: GithubServerPath, internal val repositoryPath: GithubFullPath) {
|
||||
fun toUrl(): String {
|
||||
return serverPath.toUrl() + "/" + repositoryPath.fullName
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ public class GithubServerPath {
|
||||
@NotNull
|
||||
public String toApiUrl() {
|
||||
StringBuilder builder = new StringBuilder(getSchemaUrlPart());
|
||||
if (myHost.equalsIgnoreCase(DEFAULT_HOST)) {
|
||||
if (isGithubDotCom()) {
|
||||
builder.append(API_PREFIX).append(myHost).append(getPortUrlPart()).append(StringUtil.notNullize(mySuffix));
|
||||
}
|
||||
else {
|
||||
@@ -133,7 +133,7 @@ public class GithubServerPath {
|
||||
@NotNull
|
||||
public String toGraphQLUrl() {
|
||||
StringBuilder builder = new StringBuilder(getSchemaUrlPart());
|
||||
if (myHost.equalsIgnoreCase(DEFAULT_HOST)) {
|
||||
if (isGithubDotCom()) {
|
||||
builder.append(API_PREFIX).append(myHost).append(getPortUrlPart()).append(StringUtil.notNullize(mySuffix)).append(GRAPHQL_SUFFIX);
|
||||
}
|
||||
else {
|
||||
@@ -142,6 +142,10 @@ public class GithubServerPath {
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
public boolean isGithubDotCom() {
|
||||
return myHost.equalsIgnoreCase(DEFAULT_HOST);
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
String schema = myUseHttp != null ? getSchemaUrlPart() : "";
|
||||
return schema + myHost + getPortUrlPart() + StringUtil.notNullize(mySuffix);
|
||||
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
// Copyright 2000-2019 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.request
|
||||
|
||||
/**
|
||||
* Additional params for [`GET /user/repos`](https://developer.github.com/v3/repos/#list-your-repositories) request
|
||||
*/
|
||||
enum class Type(private val value: String) {
|
||||
ALL(""),
|
||||
OWNER("owner"),
|
||||
PUBLIC("public"),
|
||||
PRIVATE("private"),
|
||||
@Suppress("unused") // API
|
||||
MEMBER("member");
|
||||
|
||||
companion object {
|
||||
val DEFAULT = ALL
|
||||
}
|
||||
|
||||
override fun toString() = if (value.isEmpty()) value else "type=$value"
|
||||
}
|
||||
|
||||
enum class Visibility(private val value: String) {
|
||||
ALL(""),
|
||||
PUBLIC("public"),
|
||||
PRIVATE("private");
|
||||
|
||||
companion object {
|
||||
val DEFAULT = ALL
|
||||
}
|
||||
|
||||
override fun toString() = if (value.isEmpty()) value else "visibility=$value"
|
||||
}
|
||||
|
||||
class Affiliation private constructor(private val value: String) {
|
||||
companion object {
|
||||
val OWNER = Affiliation("owner")
|
||||
val COLLABORATOR = Affiliation("collaborator")
|
||||
@Suppress("unused") // API
|
||||
val ORG_MEMBER = Affiliation("organization_member")
|
||||
val DEFAULT = Affiliation("")
|
||||
|
||||
fun combine(vararg affiliations: Affiliation): Affiliation {
|
||||
return Affiliation(affiliations.toSet().joinToString(",") { it.value })
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString() = if (value.isEmpty()) value else "affiliation=$value"
|
||||
}
|
||||
+5
@@ -7,6 +7,11 @@ class GithubRequestPagination @JvmOverloads constructor(val pageNumber: Int = 1,
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Max page size
|
||||
*/
|
||||
const val DEFAULT_PAGE_SIZE = 100
|
||||
|
||||
val DEFAULT = GithubRequestPagination()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,13 +14,22 @@ object GithubApiPagesLoader {
|
||||
@JvmStatic
|
||||
fun <T> loadAll(executor: GithubApiRequestExecutor, indicator: ProgressIndicator, pagesRequest: Request<T>): List<T> {
|
||||
val result = mutableListOf<T>()
|
||||
loadAll(executor, indicator, pagesRequest) { result.addAll(it) }
|
||||
return result
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
@JvmStatic
|
||||
fun <T> loadAll(executor: GithubApiRequestExecutor,
|
||||
indicator: ProgressIndicator,
|
||||
pagesRequest: Request<T>,
|
||||
pageItemsConsumer: (List<T>) -> Unit) {
|
||||
var request: GithubApiRequest<GithubResponsePage<T>>? = pagesRequest.initialRequest
|
||||
while (request != null) {
|
||||
val page = executor.execute(indicator, request)
|
||||
result.addAll(page.items)
|
||||
pageItemsConsumer(page.items)
|
||||
request = page.nextLink?.let(pagesRequest.urlRequestProvider)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
|
||||
+13
-3
@@ -78,7 +78,17 @@ class GithubAuthenticationManager internal constructor(private val accountManage
|
||||
return registerAccount(dialog.getLogin(), dialog.getServer(), dialog.getToken())
|
||||
}
|
||||
|
||||
private fun isAccountUnique(name: String, server: GithubServerPath) = accountManager.accounts.none { it.name == name && it.server == server }
|
||||
internal fun isAccountUnique(name: String, server: GithubServerPath) = accountManager.accounts.none { it.name == name && it.server == server }
|
||||
|
||||
@CalledInAwt
|
||||
internal fun removeAccount(githubAccount: GithubAccount) {
|
||||
accountManager.accounts -= githubAccount
|
||||
}
|
||||
|
||||
@CalledInAwt
|
||||
internal fun updateAccountToken(account: GithubAccount, newToken: String) {
|
||||
accountManager.updateAccountToken(account, newToken)
|
||||
}
|
||||
|
||||
private fun registerAccount(name: String, server: GithubServerPath, token: String): GithubAccount {
|
||||
val account = GithubAccountManager.createAccount(name, server)
|
||||
@@ -87,8 +97,8 @@ class GithubAuthenticationManager internal constructor(private val accountManage
|
||||
return account
|
||||
}
|
||||
|
||||
@TestOnly
|
||||
fun registerAccount(name: String, host: String, token: String): GithubAccount {
|
||||
@CalledInAwt
|
||||
internal fun registerAccount(name: String, host: String, token: String): GithubAccount {
|
||||
return registerAccount(name, GithubServerPath.from(host), token)
|
||||
}
|
||||
|
||||
|
||||
+8
@@ -3,6 +3,7 @@ package org.jetbrains.plugins.github.authentication.accounts
|
||||
|
||||
import com.google.common.cache.CacheBuilder
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.progress.ProgressIndicator
|
||||
import org.jetbrains.annotations.CalledInBackground
|
||||
import org.jetbrains.plugins.github.api.GithubApiRequestExecutor
|
||||
@@ -36,4 +37,11 @@ class GithubAccountInformationProvider {
|
||||
fun getInformation(executor: GithubApiRequestExecutor, indicator: ProgressIndicator, account: GithubAccount): GithubAuthenticatedUser {
|
||||
return informationCache.get(account) { executor.execute(indicator, GithubApiRequests.CurrentUser.get(account.server)) }
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun getInstance(): GithubAccountInformationProvider {
|
||||
return service()
|
||||
}
|
||||
}
|
||||
}
|
||||
+35
-2
@@ -18,8 +18,10 @@ import org.jetbrains.plugins.github.exceptions.GithubAuthenticationException
|
||||
import org.jetbrains.plugins.github.exceptions.GithubParseException
|
||||
import org.jetbrains.plugins.github.ui.util.DialogValidationUtils
|
||||
import org.jetbrains.plugins.github.ui.util.Validator
|
||||
import java.awt.event.ActionListener
|
||||
import java.net.UnknownHostException
|
||||
import java.util.function.Supplier
|
||||
import javax.swing.JButton
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JPanel
|
||||
import javax.swing.JPasswordField
|
||||
@@ -36,6 +38,25 @@ sealed class GithubCredentialsUI {
|
||||
abstract fun handleAcquireError(error: Throwable): ValidationInfo
|
||||
abstract fun setBusy(busy: Boolean)
|
||||
|
||||
protected val loginButton = JButton("Log In").apply { isVisible = false }
|
||||
protected val cancelButton = JButton("Cancel").apply { isVisible = false }
|
||||
|
||||
fun setLoginAction(actionListener: ActionListener) {
|
||||
loginButton.addActionListener(actionListener)
|
||||
}
|
||||
|
||||
fun setCancelAction(actionListener: ActionListener) {
|
||||
cancelButton.addActionListener(actionListener)
|
||||
}
|
||||
|
||||
fun setLoginButtonVisible(visible: Boolean) {
|
||||
loginButton.isVisible = visible
|
||||
}
|
||||
|
||||
fun setCancelButtonVisible(visible: Boolean) {
|
||||
cancelButton.isVisible = visible
|
||||
}
|
||||
|
||||
internal class PasswordUI(private val serverTextField: ExtendableTextField,
|
||||
private val clientName: String,
|
||||
switchUi: () -> Unit,
|
||||
@@ -58,7 +79,7 @@ sealed class GithubCredentialsUI {
|
||||
override fun getPanel(): JPanel = JBUI.Panels.simplePanel()
|
||||
.addToTop(panel {
|
||||
row {
|
||||
if (!dialogMode) label("Log in to GitHub", bold = true)
|
||||
if (!dialogMode) label("Log In to GitHub", bold = true)
|
||||
right { switchUiLink() }
|
||||
}
|
||||
})
|
||||
@@ -66,6 +87,12 @@ sealed class GithubCredentialsUI {
|
||||
row("Server:") { serverTextField() }
|
||||
row("Login:") { loginTextField() }
|
||||
row("Password:") { passwordField(comment = "Password is not saved and used only to acquire GitHub token") }
|
||||
row("") {
|
||||
cell {
|
||||
loginButton()
|
||||
cancelButton()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
override fun getPreferredFocus() = if (loginTextField.isEditable && loginTextField.text.isEmpty()) loginTextField else passwordField
|
||||
@@ -132,13 +159,19 @@ sealed class GithubCredentialsUI {
|
||||
override fun getPanel() = JBUI.Panels.simplePanel()
|
||||
.addToTop(panel {
|
||||
row {
|
||||
if (!dialogMode) label("Log in to GitHub", bold = true)
|
||||
if (!dialogMode) label("Log In to GitHub", bold = true)
|
||||
right { switchUiLink() }
|
||||
}
|
||||
})
|
||||
.addToCenter(panel {
|
||||
row("Server:") { serverTextField() }
|
||||
row("Token:") { tokenTextField() }
|
||||
row("") {
|
||||
cell {
|
||||
loginButton()
|
||||
cancelButton()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import org.jetbrains.plugins.github.ui.util.DialogValidationUtils
|
||||
import org.jetbrains.plugins.github.ui.util.Validator
|
||||
import org.jetbrains.plugins.github.util.GithubAsyncUtil
|
||||
import org.jetbrains.plugins.github.util.submitBackgroundTask
|
||||
import java.awt.event.ActionListener
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import javax.swing.JTextField
|
||||
|
||||
@@ -140,4 +141,24 @@ class GithubLoginPanel(executorFactory: GithubApiRequestExecutor.Factory,
|
||||
fun setError(exception: Throwable) {
|
||||
tokenAcquisitionError = currentUi.handleAcquireError(exception)
|
||||
}
|
||||
|
||||
fun setLoginListener(listener: ActionListener) {
|
||||
passwordUi.setLoginAction(listener)
|
||||
tokenUi.setLoginAction(listener)
|
||||
}
|
||||
|
||||
fun setCancelListener(listener: ActionListener) {
|
||||
passwordUi.setCancelAction(listener)
|
||||
tokenUi.setCancelAction(listener)
|
||||
}
|
||||
|
||||
fun setLoginButtonVisible(visible: Boolean) {
|
||||
passwordUi.setLoginButtonVisible(visible)
|
||||
tokenUi.setLoginButtonVisible(visible)
|
||||
}
|
||||
|
||||
fun setCancelButtonVisible(visible: Boolean) {
|
||||
passwordUi.setCancelButtonVisible(visible)
|
||||
tokenUi.setCancelButtonVisible(visible)
|
||||
}
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
// Copyright 2000-2019 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.ui.cloneDialog
|
||||
|
||||
import com.intellij.ide.BrowserUtil
|
||||
import com.intellij.openapi.ui.JBPopupMenu
|
||||
import com.intellij.ui.JBColor
|
||||
import com.intellij.ui.SizedIcon
|
||||
import com.intellij.ui.components.JBMenu
|
||||
import com.intellij.util.ui.JBUI
|
||||
import com.intellij.util.ui.cloneDialog.VcsCloneDialogUiSpec
|
||||
import icons.GithubIcons
|
||||
import org.jetbrains.plugins.github.api.GithubApiRequestExecutorManager
|
||||
import org.jetbrains.plugins.github.api.data.GithubUser
|
||||
import org.jetbrains.plugins.github.authentication.GithubAuthenticationManager
|
||||
import org.jetbrains.plugins.github.authentication.accounts.GithubAccount
|
||||
import org.jetbrains.plugins.github.pullrequest.avatars.CachingGithubAvatarIconsProvider
|
||||
import org.jetbrains.plugins.github.util.CachingGithubUserAvatarLoader
|
||||
import org.jetbrains.plugins.github.util.GithubImageResizer
|
||||
import java.awt.FlowLayout
|
||||
import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
import javax.swing.Icon
|
||||
import javax.swing.JLabel
|
||||
import javax.swing.JPanel
|
||||
|
||||
internal class GHCloneDialogAccountsPanel(
|
||||
private val loginController: GHLoginController,
|
||||
private val authenticationManager: GithubAuthenticationManager,
|
||||
private val apiRequestExecutorManager: GithubApiRequestExecutorManager,
|
||||
private val avatarLoader: CachingGithubUserAvatarLoader,
|
||||
private val imageResizer: GithubImageResizer
|
||||
) : JPanel(FlowLayout(FlowLayout.LEADING, JBUI.scale(1), 0)) {
|
||||
|
||||
private val avatarSize = VcsCloneDialogUiSpec.Components.avatarSize
|
||||
private val defaultIcon = SizedIcon(GithubIcons.DefaultAvatar, avatarSize.get(), avatarSize.get())
|
||||
|
||||
private val accountComponents = HashMap<GithubAccount, JLabel>()
|
||||
private val userDetails = HashMap<GithubAccount, Pair<GithubUser, Icon>>()
|
||||
|
||||
private val popupMenuMouseAdapter = object : MouseAdapter() {
|
||||
override fun mouseClicked(e: MouseEvent?) = showPopupMenu()
|
||||
}
|
||||
|
||||
init {
|
||||
addMouseListener(popupMenuMouseAdapter)
|
||||
}
|
||||
|
||||
fun addAccount(account: GithubAccount) {
|
||||
if (accountComponents.isEmpty()) removeAll()
|
||||
|
||||
val label = accountComponents.getOrPut(account, {
|
||||
JLabel().apply {
|
||||
border = JBUI.Borders.customLine(JBColor.lightGray)
|
||||
icon = defaultIcon
|
||||
toolTipText = account.name
|
||||
isOpaque = !isOpaque
|
||||
addMouseListener(popupMenuMouseAdapter)
|
||||
}
|
||||
})
|
||||
add(label)
|
||||
}
|
||||
|
||||
fun removeAccount(removedAccount: GithubAccount) {
|
||||
remove(accountComponents.remove(removedAccount))
|
||||
userDetails.remove(removedAccount)
|
||||
|
||||
revalidate()
|
||||
repaint()
|
||||
}
|
||||
|
||||
fun updateUserDetails(account: GithubAccount, user: GithubUser) {
|
||||
val iconsProvider = CachingGithubAvatarIconsProvider.Factory(
|
||||
avatarLoader,
|
||||
imageResizer,
|
||||
apiRequestExecutorManager.getExecutor(account)
|
||||
).create(avatarSize, this)
|
||||
val avatar = iconsProvider.getIcon(user.avatarUrl)
|
||||
userDetails[account] = Pair(user, avatar)
|
||||
accountComponents[account]?.icon = avatar
|
||||
}
|
||||
|
||||
private fun showPopupMenu() {
|
||||
// TODO: replace with custom popup action list
|
||||
val popupMenu = JBPopupMenu()
|
||||
for (account in authenticationManager.getAccounts()) {
|
||||
val accountSubmenu = JBMenu().apply {
|
||||
val pair = userDetails[account]
|
||||
if (pair == null) {
|
||||
text = account.name
|
||||
icon = defaultIcon
|
||||
add("Log in").addActionListener { loginController.reLogin(account) }
|
||||
addSeparator()
|
||||
add("Remove account").addActionListener { loginController.logout(account) }
|
||||
}
|
||||
else {
|
||||
val (user, avatar) = pair
|
||||
text = if (account.server.isGithubDotCom) account.name else ("${account.server.host}/${user.login}")
|
||||
icon = avatar
|
||||
add("Open on GitHub").addActionListener { BrowserUtil.browse(user.htmlUrl) }
|
||||
addSeparator()
|
||||
add("Log Out\u2026").addActionListener { loginController.logout(account) }
|
||||
}
|
||||
}
|
||||
popupMenu.add(accountSubmenu)
|
||||
}
|
||||
popupMenu.addSeparator()
|
||||
popupMenu.add("Add account\u2026").addActionListener { loginController.addAccount() }
|
||||
popupMenu.show(this, 0, bounds.maxY.toInt())
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
// Copyright 2000-2019 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.ui.cloneDialog
|
||||
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.vcs.ui.cloneDialog.VcsCloneDialogExtension
|
||||
import com.intellij.openapi.vcs.ui.cloneDialog.VcsCloneDialogExtensionComponent
|
||||
import com.intellij.openapi.vcs.ui.cloneDialog.VcsCloneDialogExtensionStatusLine
|
||||
import org.jetbrains.plugins.github.api.GithubApiRequestExecutor
|
||||
import org.jetbrains.plugins.github.api.GithubApiRequestExecutorManager
|
||||
import org.jetbrains.plugins.github.authentication.GithubAuthenticationManager
|
||||
import org.jetbrains.plugins.github.authentication.accounts.GithubAccountInformationProvider
|
||||
import org.jetbrains.plugins.github.util.CachingGithubUserAvatarLoader
|
||||
import org.jetbrains.plugins.github.util.GithubImageResizer
|
||||
import org.jetbrains.plugins.github.util.GithubUtil
|
||||
import javax.swing.Icon
|
||||
|
||||
class GHCloneDialogExtension : VcsCloneDialogExtension {
|
||||
private val authenticationManager = GithubAuthenticationManager.getInstance()
|
||||
|
||||
override fun getName() = GithubUtil.SERVICE_DISPLAY_NAME
|
||||
|
||||
override fun getIcon(): Icon = AllIcons.Vcs.Vendors.Github
|
||||
|
||||
override fun getAdditionalStatusLines(): List<VcsCloneDialogExtensionStatusLine> {
|
||||
if (!authenticationManager.hasAccounts()) {
|
||||
return listOf(VcsCloneDialogExtensionStatusLine.greyText("No accounts"))
|
||||
}
|
||||
|
||||
val list = ArrayList<VcsCloneDialogExtensionStatusLine>()
|
||||
for (account in authenticationManager.getAccounts()) {
|
||||
val accName = if (account.server.isGithubDotCom) account.name else ("${account.server.host}/${account.name}")
|
||||
list.add(VcsCloneDialogExtensionStatusLine.greyText(accName))
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
override fun createMainComponent(project: Project): VcsCloneDialogExtensionComponent {
|
||||
return GHCloneDialogExtensionComponent(project,
|
||||
GithubAuthenticationManager.getInstance(),
|
||||
GithubApiRequestExecutorManager.getInstance(),
|
||||
GithubApiRequestExecutor.Factory.getInstance(),
|
||||
GithubAccountInformationProvider.getInstance(),
|
||||
CachingGithubUserAvatarLoader.getInstance(),
|
||||
GithubImageResizer.getInstance()
|
||||
)
|
||||
}
|
||||
}
|
||||
+455
@@ -0,0 +1,455 @@
|
||||
// Copyright 2000-2019 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.ui.cloneDialog
|
||||
|
||||
import com.intellij.dvcs.repo.ClonePathProvider
|
||||
import com.intellij.dvcs.ui.CloneDvcsValidationUtils
|
||||
import com.intellij.dvcs.ui.DvcsBundle.getString
|
||||
import com.intellij.dvcs.ui.SelectChildTextFieldWithBrowseButton
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.application.ModalityState
|
||||
import com.intellij.openapi.application.runInEdt
|
||||
import com.intellij.openapi.diagnostic.logger
|
||||
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
|
||||
import com.intellij.openapi.progress.EmptyProgressIndicator
|
||||
import com.intellij.openapi.progress.ProgressIndicator
|
||||
import com.intellij.openapi.progress.Task
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.rd.attachChild
|
||||
import com.intellij.openapi.ui.DialogPanel
|
||||
import com.intellij.openapi.ui.ValidationInfo
|
||||
import com.intellij.openapi.util.text.StringUtil
|
||||
import com.intellij.openapi.vcs.ProjectLevelVcsManager
|
||||
import com.intellij.openapi.vcs.ui.cloneDialog.VcsCloneDialogExtensionComponent
|
||||
import com.intellij.openapi.vfs.LocalFileSystem
|
||||
import com.intellij.ui.*
|
||||
import com.intellij.ui.components.panels.VerticalLayout
|
||||
import com.intellij.ui.components.panels.Wrapper
|
||||
import com.intellij.ui.layout.*
|
||||
import com.intellij.ui.speedSearch.NameFilteringListModel
|
||||
import com.intellij.ui.speedSearch.SpeedSearch
|
||||
import com.intellij.util.containers.ContainerUtil
|
||||
import com.intellij.util.progress.ProgressVisibilityManager
|
||||
import com.intellij.util.ui.JBEmptyBorder
|
||||
import com.intellij.util.ui.JBUI
|
||||
import com.intellij.util.ui.UIUtil
|
||||
import git4idea.GitUtil
|
||||
import git4idea.checkout.GitCheckoutProvider
|
||||
import git4idea.commands.Git
|
||||
import git4idea.remote.GitRememberedInputs
|
||||
import org.jetbrains.plugins.github.api.*
|
||||
import org.jetbrains.plugins.github.api.data.GithubAuthenticatedUser
|
||||
import org.jetbrains.plugins.github.api.data.GithubRepo
|
||||
import org.jetbrains.plugins.github.api.data.request.Affiliation
|
||||
import org.jetbrains.plugins.github.api.data.request.GithubRequestPagination
|
||||
import org.jetbrains.plugins.github.api.util.GithubApiPagesLoader
|
||||
import org.jetbrains.plugins.github.authentication.GithubAuthenticationManager
|
||||
import org.jetbrains.plugins.github.authentication.accounts.*
|
||||
import org.jetbrains.plugins.github.authentication.ui.GithubLoginPanel
|
||||
import org.jetbrains.plugins.github.exceptions.GithubMissingTokenException
|
||||
import org.jetbrains.plugins.github.util.CachingGithubUserAvatarLoader
|
||||
import org.jetbrains.plugins.github.util.GithubImageResizer
|
||||
import org.jetbrains.plugins.github.util.GithubUrlUtil
|
||||
import org.jetbrains.plugins.github.util.handleOnEdt
|
||||
import java.awt.event.ActionListener
|
||||
import java.nio.file.Paths
|
||||
import javax.swing.JPanel
|
||||
import javax.swing.JSeparator
|
||||
import javax.swing.event.DocumentEvent
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
internal class GHCloneDialogExtensionComponent(
|
||||
private val project: Project,
|
||||
private val authenticationManager: GithubAuthenticationManager,
|
||||
private val executorManager: GithubApiRequestExecutorManager,
|
||||
private val apiExecutorFactory: GithubApiRequestExecutor.Factory,
|
||||
private val accountInformationProvider: GithubAccountInformationProvider,
|
||||
avatarLoader: CachingGithubUserAvatarLoader,
|
||||
imageResizer: GithubImageResizer
|
||||
) : VcsCloneDialogExtensionComponent() {
|
||||
private val LOG = logger<GHCloneDialogExtensionComponent>()
|
||||
|
||||
private val progressManager: ProgressVisibilityManager
|
||||
|
||||
// UI
|
||||
private val wrapper: Wrapper = Wrapper()
|
||||
private val repositoriesPanel: DialogPanel
|
||||
private val repositoryList: GHRepositoryList
|
||||
|
||||
private val accountsPanel: GHCloneDialogAccountsPanel = GHCloneDialogAccountsPanel(
|
||||
object : GHLoginController {
|
||||
override fun addAccount() = switchToLogin()
|
||||
override fun reLogin(account: GithubAccount) = switchToLogin(account)
|
||||
override fun logout(account: GithubAccount) = authenticationManager.removeAccount(account)
|
||||
},
|
||||
authenticationManager,
|
||||
executorManager,
|
||||
avatarLoader,
|
||||
imageResizer
|
||||
)
|
||||
|
||||
private val searchField: SearchTextField = SearchTextField(false).apply {
|
||||
textEditor.emptyText.appendText("Search or enter a GitHub repository URL")
|
||||
}
|
||||
private val directoryField = SelectChildTextFieldWithBrowseButton(
|
||||
ClonePathProvider.defaultParentDirectoryPath(project, GitRememberedInputs.getInstance())).apply {
|
||||
val fcd = FileChooserDescriptorFactory.createSingleFolderDescriptor()
|
||||
fcd.isShowFileSystemRoots = true
|
||||
fcd.isHideIgnored = false
|
||||
addBrowseFolderListener(getString("clone.destination.directory.browser.title"),
|
||||
getString("clone.destination.directory.browser.description"),
|
||||
project,
|
||||
fcd)
|
||||
}
|
||||
|
||||
// state
|
||||
private val userDetailsByAccount = hashMapOf<GithubAccount, GithubAuthenticatedUser>()
|
||||
private val repositoriesByAccount = hashMapOf<GithubAccount, LinkedHashSet<GithubRepo>>()
|
||||
private val errorsByAccount = hashMapOf<GithubAccount, GHRepositoryListItem.Error>()
|
||||
private val originListModel = CollectionListModel<GHRepositoryListItem>()
|
||||
private var inLoginState = false
|
||||
private var selectedUrl by Delegates.observable<String?>(null) { _, _, _ -> onSelectedUrlChanged() }
|
||||
|
||||
init {
|
||||
val speedSearch = SpeedSearch()
|
||||
|
||||
val filteringListModel = NameFilteringListModel<GHRepositoryListItem>(originListModel,
|
||||
{ it.stringToSearch },
|
||||
speedSearch::shouldBeShowing,
|
||||
{ speedSearch.filter ?: "" }
|
||||
).apply {
|
||||
setFilter { item ->
|
||||
item != null && when (item) {
|
||||
is GHRepositoryListItem.Repo -> speedSearch.shouldBeShowing(item.repo.fullName)
|
||||
else -> speedSearch.filter.isEmpty()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
searchField.addDocumentListener(object : DocumentAdapter() {
|
||||
override fun textChanged(e: DocumentEvent) {
|
||||
speedSearch.updatePattern(searchField.text)
|
||||
filteringListModel.refilter()
|
||||
updateSelectedUrl()
|
||||
}
|
||||
})
|
||||
|
||||
repositoryList = GHRepositoryList(filteringListModel).apply {
|
||||
addListSelectionListener {
|
||||
if (it.valueIsAdjusting) return@addListSelectionListener
|
||||
updateSelectedUrl()
|
||||
}
|
||||
}
|
||||
|
||||
progressManager = object : ProgressVisibilityManager() {
|
||||
override fun setProgressVisible(visible: Boolean) = repositoryList.setPaintBusy(visible)
|
||||
|
||||
override fun getModalityState() = ModalityState.any()
|
||||
}
|
||||
|
||||
this.attachChild(progressManager)
|
||||
|
||||
ApplicationManager.getApplication().messageBus.connect(this).apply {
|
||||
subscribe(GithubAccountManager.ACCOUNT_REMOVED_TOPIC, object : AccountRemovedListener {
|
||||
override fun accountRemoved(removedAccount: GithubAccount) {
|
||||
removeAccount(removedAccount)
|
||||
dialogStateListener.onListItemChanged()
|
||||
}
|
||||
})
|
||||
|
||||
subscribe(GithubAccountManager.ACCOUNT_TOKEN_CHANGED_TOPIC, object : AccountTokenChangedListener {
|
||||
override fun tokenChanged(account: GithubAccount) {
|
||||
if (repositoriesByAccount[account] != null)
|
||||
return
|
||||
dialogStateListener.onListItemChanged()
|
||||
addAccount(account)
|
||||
switchToRepositories()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
repositoriesPanel = panel {
|
||||
row {
|
||||
cell(isFullWidth = true) {
|
||||
searchField(pushX, growX)
|
||||
JSeparator(JSeparator.VERTICAL)(growY)
|
||||
accountsPanel()
|
||||
}
|
||||
}
|
||||
row {
|
||||
ScrollPaneFactory.createScrollPane(repositoryList)(push, grow)
|
||||
}
|
||||
row("Directory:") {
|
||||
directoryField(growX, pushX)
|
||||
}
|
||||
}
|
||||
repositoriesPanel.border = JBEmptyBorder(UIUtil.PANEL_REGULAR_INSETS)
|
||||
|
||||
if (authenticationManager.hasAccounts()) {
|
||||
switchToRepositories()
|
||||
authenticationManager.getAccounts().forEach(this@GHCloneDialogExtensionComponent::addAccount)
|
||||
}
|
||||
else {
|
||||
switchToLogin()
|
||||
}
|
||||
}
|
||||
|
||||
private fun switchToLogin(account: GithubAccount? = null) {
|
||||
val errorPanel = JPanel(VerticalLayout(10))
|
||||
val githubLoginPanel = buildGitHubLoginPanel(account, errorPanel)
|
||||
val loginPanel = JBUI.Panels.simplePanel()
|
||||
.addToTop(githubLoginPanel)
|
||||
.addToCenter(errorPanel)
|
||||
wrapper.setContent(loginPanel)
|
||||
wrapper.repaint()
|
||||
inLoginState = true
|
||||
updateSelectedUrl()
|
||||
}
|
||||
|
||||
private fun switchToRepositories() {
|
||||
wrapper.setContent(repositoriesPanel)
|
||||
wrapper.repaint()
|
||||
inLoginState = false
|
||||
updateSelectedUrl()
|
||||
}
|
||||
|
||||
private fun addAccount(account: GithubAccount) {
|
||||
repositoriesByAccount.remove(account)
|
||||
accountsPanel.addAccount(account)
|
||||
|
||||
try {
|
||||
val executor = executorManager.getExecutor(account)
|
||||
loadUserDetails(account, executor)
|
||||
loadRepositories(account, executor)
|
||||
}
|
||||
catch (e: GithubMissingTokenException) {
|
||||
errorsByAccount[account] = GHRepositoryListItem.Error(account,
|
||||
"Missing access token",
|
||||
"Log in",
|
||||
Runnable { switchToLogin(account) })
|
||||
refillRepositories()
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeAccount(account: GithubAccount) {
|
||||
repositoriesByAccount.remove(account)
|
||||
accountsPanel.removeAccount(account)
|
||||
refillRepositories()
|
||||
if (!authenticationManager.hasAccounts()) switchToLogin()
|
||||
}
|
||||
|
||||
private fun loadUserDetails(account: GithubAccount,
|
||||
executor: GithubApiRequestExecutor.WithTokenAuth) {
|
||||
progressManager.run(object : Task.Backgroundable(project, "Not Visible") {
|
||||
lateinit var user: GithubAuthenticatedUser
|
||||
|
||||
override fun run(indicator: ProgressIndicator) {
|
||||
user = accountInformationProvider.getInformation(executor, indicator, account)
|
||||
}
|
||||
|
||||
override fun onSuccess() {
|
||||
userDetailsByAccount[account] = user
|
||||
accountsPanel.updateUserDetails(account, user)
|
||||
refillRepositories()
|
||||
}
|
||||
|
||||
override fun onThrowable(error: Throwable) {
|
||||
LOG.error(error)
|
||||
errorsByAccount[account] = GHRepositoryListItem.Error(account,
|
||||
"Unable to load repositories",
|
||||
"Retry",
|
||||
Runnable { addAccount(account) })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun loadRepositories(account: GithubAccount,
|
||||
executor: GithubApiRequestExecutor.WithTokenAuth) {
|
||||
repositoriesByAccount.remove(account)
|
||||
errorsByAccount.remove(account)
|
||||
|
||||
progressManager.run(object : Task.Backgroundable(project, "Not Visible") {
|
||||
override fun run(indicator: ProgressIndicator) {
|
||||
val repoPagesRequest = GithubApiRequests.CurrentUser.Repos.pages(account.server,
|
||||
affiliation = Affiliation.combine(Affiliation.OWNER,
|
||||
Affiliation.COLLABORATOR),
|
||||
pagination = GithubRequestPagination.DEFAULT)
|
||||
val pageItemsConsumer: (List<GithubRepo>) -> Unit = {
|
||||
runInEdt {
|
||||
repositoriesByAccount.getOrPut(account, { UpdateOrderLinkedHashSet() }).addAll(it)
|
||||
refillRepositories()
|
||||
}
|
||||
}
|
||||
GithubApiPagesLoader.loadAll(executor, indicator, repoPagesRequest, pageItemsConsumer)
|
||||
|
||||
val orgsRequest = GithubApiRequests.CurrentUser.Orgs.pages(account.server)
|
||||
val userOrganizations = GithubApiPagesLoader.loadAll(executor, indicator, orgsRequest).sortedBy { it.login }
|
||||
|
||||
for (org in userOrganizations) {
|
||||
val orgRepoRequest = GithubApiRequests.Organisations.Repos.pages(account.server, org.login, GithubRequestPagination.DEFAULT)
|
||||
GithubApiPagesLoader.loadAll(executor, indicator, orgRepoRequest, pageItemsConsumer)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onThrowable(error: Throwable) {
|
||||
LOG.error(error)
|
||||
errorsByAccount[account] = GHRepositoryListItem.Error(account,
|
||||
"Unable to load repositories",
|
||||
"Retry",
|
||||
Runnable { loadRepositories(account, executor) })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun refillRepositories() {
|
||||
val selectedValue = repositoryList.selectedValue
|
||||
originListModel.removeAll()
|
||||
for (account in authenticationManager.getAccounts()) {
|
||||
if (errorsByAccount[account] != null) {
|
||||
originListModel.add(errorsByAccount[account])
|
||||
}
|
||||
val user = userDetailsByAccount[account] ?: continue
|
||||
val repos = repositoriesByAccount[account] ?: continue
|
||||
for (repo in repos) {
|
||||
originListModel.add(GHRepositoryListItem.Repo(account, user, repo))
|
||||
}
|
||||
}
|
||||
repositoryList.setSelectedValue(selectedValue, false)
|
||||
}
|
||||
|
||||
override fun getView() = wrapper
|
||||
|
||||
override fun doValidateAll(): List<ValidationInfo> {
|
||||
val list = ArrayList<ValidationInfo>()
|
||||
ContainerUtil.addIfNotNull(list, CloneDvcsValidationUtils.checkDirectory(directoryField.text, directoryField.textField))
|
||||
ContainerUtil.addIfNotNull(list, CloneDvcsValidationUtils.createDestination(directoryField.text))
|
||||
return list
|
||||
}
|
||||
|
||||
override fun doClone() {
|
||||
val parent = Paths.get(directoryField.text).toAbsolutePath().parent
|
||||
|
||||
val lfs = LocalFileSystem.getInstance()
|
||||
var destinationParent = lfs.findFileByIoFile(parent.toFile())
|
||||
if (destinationParent == null) {
|
||||
destinationParent = lfs.refreshAndFindFileByIoFile(parent.toFile())
|
||||
}
|
||||
if (destinationParent == null) {
|
||||
return
|
||||
}
|
||||
val directoryName = Paths.get(directoryField.text).fileName.toString()
|
||||
val parentDirectory = parent.toAbsolutePath().toString()
|
||||
|
||||
GitCheckoutProvider.clone(project, Git.getInstance(), ProjectLevelVcsManager.getInstance(project).compositeCheckoutListener,
|
||||
destinationParent, selectedUrl, directoryName, parentDirectory)
|
||||
}
|
||||
|
||||
override fun onComponentSelected() {
|
||||
dialogStateListener.onOkActionNameChanged("Clone")
|
||||
updateSelectedUrl()
|
||||
}
|
||||
|
||||
private fun buildGitHubLoginPanel(account: GithubAccount?,
|
||||
errorPanel: JPanel): GithubLoginPanel {
|
||||
val alwaysUnique: (name: String, server: GithubServerPath) -> Boolean = { _, _ -> true }
|
||||
return GithubLoginPanel(
|
||||
apiExecutorFactory,
|
||||
if (account == null) authenticationManager::isAccountUnique else alwaysUnique,
|
||||
project,
|
||||
false
|
||||
).apply {
|
||||
border = JBEmptyBorder(UIUtil.PANEL_REGULAR_INSETS)
|
||||
if (account != null) {
|
||||
setCredentials(account.name, null, false)
|
||||
setServer(account.server.toUrl(), false)
|
||||
}
|
||||
|
||||
setLoginListener(ActionListener {
|
||||
acquireLoginAndToken(EmptyProgressIndicator(ModalityState.stateForComponent(this)))
|
||||
.handleOnEdt { loginToken, throwable ->
|
||||
errorPanel.removeAll()
|
||||
if (throwable != null) {
|
||||
for (validationInfo in doValidateAll()) {
|
||||
val component = SimpleColoredComponent()
|
||||
component.append(validationInfo.message, SimpleTextAttributes.ERROR_ATTRIBUTES)
|
||||
errorPanel.add(component)
|
||||
errorPanel.revalidate()
|
||||
}
|
||||
errorPanel.repaint()
|
||||
}
|
||||
if (loginToken != null) {
|
||||
val login = loginToken.first
|
||||
val token = loginToken.second
|
||||
if (account != null) {
|
||||
authenticationManager.updateAccountToken(account, token)
|
||||
}
|
||||
else {
|
||||
authenticationManager.registerAccount(login, getServer().host, token)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
setCancelListener(ActionListener { switchToRepositories() })
|
||||
setLoginButtonVisible(true)
|
||||
setCancelButtonVisible(authenticationManager.hasAccounts())
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateSelectedUrl() {
|
||||
repositoryList.emptyText.clear()
|
||||
if (inLoginState) {
|
||||
selectedUrl = null
|
||||
return
|
||||
}
|
||||
val githubRepoPath = getGithubRepoPath(searchField.text)
|
||||
if (githubRepoPath != null) {
|
||||
selectedUrl = githubRepoPath.toUrl()
|
||||
repositoryList.emptyText.appendText("Clone '$githubRepoPath'")
|
||||
return
|
||||
}
|
||||
val selectedValue = repositoryList.selectedValue
|
||||
if (selectedValue is GHRepositoryListItem.Repo) {
|
||||
selectedUrl = selectedValue.repo.cloneUrl
|
||||
return
|
||||
}
|
||||
selectedUrl = null
|
||||
}
|
||||
|
||||
private fun getGithubRepoPath(url: String): GithubRepositoryPath? {
|
||||
try {
|
||||
if (!url.endsWith(GitUtil.DOT_GIT, true)) return null
|
||||
|
||||
var serverPath = GithubServerPath.from(url)
|
||||
serverPath = GithubServerPath.from(serverPath.toUrl().removeSuffix(serverPath.suffix ?: ""))
|
||||
|
||||
val githubFullPath = GithubUrlUtil.getUserAndRepositoryFromRemoteUrl(url) ?: return null
|
||||
return GithubRepositoryPath(serverPath, githubFullPath)
|
||||
}
|
||||
catch (e: Throwable) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSelectedUrlChanged() {
|
||||
val urlSelected = selectedUrl != null
|
||||
dialogStateListener.onOkActionEnabled(urlSelected)
|
||||
directoryField.isEnabled = urlSelected
|
||||
if (urlSelected) {
|
||||
val path = StringUtil.trimEnd(ClonePathProvider.relativeDirectoryPathForVcsUrl(project, selectedUrl!!), GitUtil.DOT_GIT)
|
||||
directoryField.trySetChildPath(path)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Since each repository can be in several states at the same time (shared access for a collaborator and shared access for org member) and
|
||||
* repositories for collaborators are loaded in separate request before repositories for org members, we need to update order of re-added
|
||||
* repo in order to place it close to other organization repos
|
||||
*/
|
||||
private class UpdateOrderLinkedHashSet<T> : LinkedHashSet<T>() {
|
||||
override fun add(element: T): Boolean {
|
||||
val wasThere = remove(element)
|
||||
super.add(element)
|
||||
// Contract is "true if this set did not already contain the specified element"
|
||||
return !wasThere
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// Copyright 2000-2019 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.ui.cloneDialog
|
||||
|
||||
import org.jetbrains.plugins.github.authentication.accounts.GithubAccount
|
||||
|
||||
internal interface GHLoginController {
|
||||
fun addAccount()
|
||||
|
||||
fun reLogin(account: GithubAccount)
|
||||
|
||||
fun logout(account: GithubAccount)
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
// Copyright 2000-2019 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.ui.cloneDialog
|
||||
|
||||
import com.intellij.ui.*
|
||||
import com.intellij.ui.components.JBList
|
||||
import com.intellij.util.ui.JBUI
|
||||
import com.intellij.util.ui.UIUtil
|
||||
import org.jetbrains.plugins.github.authentication.GithubAuthenticationManager
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Component
|
||||
import java.awt.Cursor
|
||||
import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
import javax.swing.JList
|
||||
import javax.swing.ListModel
|
||||
|
||||
internal class GHRepositoryList(listModel: ListModel<GHRepositoryListItem>) : JBList<GHRepositoryListItem>(listModel) {
|
||||
private val authenticationManager = GithubAuthenticationManager.getInstance()
|
||||
|
||||
init {
|
||||
ScrollingUtil.installActions(this)
|
||||
|
||||
selectionModel = SingleSelectionModel()
|
||||
|
||||
cellRenderer = object : ColoredListCellRenderer<GHRepositoryListItem>() {
|
||||
val nameRenderer = AccountNameRenderer()
|
||||
|
||||
override fun getListCellRendererComponent(list: JList<out GHRepositoryListItem>,
|
||||
value: GHRepositoryListItem,
|
||||
index: Int,
|
||||
selected: Boolean,
|
||||
hasFocus: Boolean): Component {
|
||||
val component = super.getListCellRendererComponent(list, value, index, selected, hasFocus)
|
||||
if (showAccountNameAbove(list, index)) {
|
||||
val name = with(value) {
|
||||
if (account.server.isGithubDotCom) account.name else ("${account.server.host}/${account.name}")
|
||||
}
|
||||
return nameRenderer.withName(name, component, index != 0)
|
||||
}
|
||||
return component
|
||||
}
|
||||
|
||||
private fun showAccountNameAbove(list: JList<out GHRepositoryListItem>, index: Int): Boolean {
|
||||
return authenticationManager.getAccounts().size > 1
|
||||
&& (index == 0 || list.model.getElementAt(index).account != list.model.getElementAt(index - 1).account)
|
||||
}
|
||||
|
||||
override fun customizeCellRenderer(list: JList<out GHRepositoryListItem>,
|
||||
value: GHRepositoryListItem,
|
||||
index: Int,
|
||||
selected: Boolean,
|
||||
hasFocus: Boolean) = value.customizeRenderer(this, list)
|
||||
}
|
||||
|
||||
val mouseAdapter = createMouseAdapter()
|
||||
addMouseListener(mouseAdapter)
|
||||
addMouseMotionListener(mouseAdapter)
|
||||
}
|
||||
|
||||
private fun createMouseAdapter(): MouseAdapter {
|
||||
return object : MouseAdapter() {
|
||||
fun getRunnableAt(e: MouseEvent): Runnable? {
|
||||
val point = e.point
|
||||
val renderer = ListUtil.getDeepestRendererChildComponentAt(this@GHRepositoryList, point)
|
||||
if (renderer !is SimpleColoredComponent) return null
|
||||
val tag = renderer.getFragmentTagAt(point.x)
|
||||
return if (tag is Runnable) tag else null
|
||||
}
|
||||
|
||||
override fun mouseMoved(e: MouseEvent) {
|
||||
val runnable = getRunnableAt(e)
|
||||
if (runnable != null) {
|
||||
UIUtil.setCursor(this@GHRepositoryList, Cursor.getPredefinedCursor(Cursor.HAND_CURSOR))
|
||||
}
|
||||
else {
|
||||
UIUtil.setCursor(this@GHRepositoryList, Cursor.getDefaultCursor())
|
||||
}
|
||||
}
|
||||
|
||||
override fun mouseClicked(e: MouseEvent) {
|
||||
getRunnableAt(e)?.run()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class AccountNameRenderer : CellRendererPanel() {
|
||||
private val titleLabel = SimpleColoredComponent().apply {
|
||||
background = UIUtil.getListBackground()
|
||||
}
|
||||
private val topLine = JBUI.Borders.customLine(JBUI.CurrentTheme.BigPopup.listSeparatorColor(), 1, 0, 0, 0)
|
||||
private val borderLayout = BorderLayout()
|
||||
|
||||
init {
|
||||
layout = borderLayout
|
||||
add(titleLabel, BorderLayout.NORTH)
|
||||
background = UIUtil.getListBackground()
|
||||
}
|
||||
|
||||
fun withName(title: String, itemContent: Component, withBorder: Boolean): AccountNameRenderer {
|
||||
titleLabel.border = null
|
||||
titleLabel.clear()
|
||||
titleLabel.append(title, SimpleTextAttributes.GRAY_SMALL_ATTRIBUTES)
|
||||
if (withBorder) {
|
||||
titleLabel.border = topLine
|
||||
}
|
||||
val prevContent = borderLayout.getLayoutComponent(BorderLayout.CENTER)
|
||||
if (prevContent != null) {
|
||||
remove(prevContent)
|
||||
}
|
||||
add(itemContent, BorderLayout.CENTER)
|
||||
return this
|
||||
}
|
||||
}
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
// Copyright 2000-2019 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.ui.cloneDialog
|
||||
|
||||
import com.intellij.ui.ColoredListCellRenderer
|
||||
import com.intellij.ui.SimpleTextAttributes
|
||||
import org.jetbrains.plugins.github.api.data.GithubRepo
|
||||
import org.jetbrains.plugins.github.api.data.GithubUser
|
||||
import org.jetbrains.plugins.github.authentication.accounts.GithubAccount
|
||||
import javax.swing.JList
|
||||
|
||||
internal sealed class GHRepositoryListItem(
|
||||
val account: GithubAccount
|
||||
) {
|
||||
open val stringToSearch: String?
|
||||
get() = null
|
||||
|
||||
abstract fun customizeRenderer(renderer: ColoredListCellRenderer<GHRepositoryListItem>,
|
||||
list: JList<out GHRepositoryListItem>)
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as GHRepositoryListItem
|
||||
|
||||
if (account != other.account) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return account.hashCode()
|
||||
}
|
||||
|
||||
class Repo(
|
||||
account: GithubAccount,
|
||||
val user: GithubUser,
|
||||
val repo: GithubRepo
|
||||
) : GHRepositoryListItem(account) {
|
||||
override val stringToSearch get() = repo.fullName
|
||||
|
||||
override fun customizeRenderer(renderer: ColoredListCellRenderer<GHRepositoryListItem>,
|
||||
list: JList<out GHRepositoryListItem>): Unit =
|
||||
with(renderer) {
|
||||
ipad.left = 10
|
||||
toolTipText = repo.description
|
||||
append(if (repo.owner.login == user.login) repo.name else repo.fullName)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
if (!super.equals(other)) return false
|
||||
|
||||
other as Repo
|
||||
|
||||
if (user != other.user) return false
|
||||
if (repo != other.repo) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = super.hashCode()
|
||||
result = 31 * result + user.hashCode()
|
||||
result = 31 * result + repo.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
class Error(
|
||||
account: GithubAccount,
|
||||
private val errorText: String,
|
||||
private val linkText: String,
|
||||
private val linkHandler: Runnable
|
||||
) : GHRepositoryListItem(account) {
|
||||
|
||||
override fun customizeRenderer(renderer: ColoredListCellRenderer<GHRepositoryListItem>,
|
||||
list: JList<out GHRepositoryListItem>) =
|
||||
with(renderer) {
|
||||
ipad.left = 10
|
||||
toolTipText = null
|
||||
append(errorText, SimpleTextAttributes.ERROR_ATTRIBUTES)
|
||||
append(" ")
|
||||
append(linkText, SimpleTextAttributes.LINK_ATTRIBUTES, linkHandler)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
if (!super.equals(other)) return false
|
||||
|
||||
other as Error
|
||||
|
||||
if (errorText != other.errorText) return false
|
||||
if (linkText != other.linkText) return false
|
||||
if (linkHandler != other.linkHandler) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = super.hashCode()
|
||||
result = 31 * result + errorText.hashCode()
|
||||
result = 31 * result + linkText.hashCode()
|
||||
result = 31 * result + linkHandler.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package org.jetbrains.plugins.github.util
|
||||
|
||||
import com.google.common.cache.CacheBuilder
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.diagnostic.logger
|
||||
import com.intellij.openapi.progress.EmptyProgressIndicator
|
||||
import com.intellij.openapi.progress.ProcessCanceledException
|
||||
@@ -70,6 +71,11 @@ class CachingGithubUserAvatarLoader(private val progressManager: ProgressManager
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun getInstance(): CachingGithubUserAvatarLoader {
|
||||
return service()
|
||||
}
|
||||
|
||||
private const val MAXIMUM_ICON_SIZE = 40
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
package org.jetbrains.plugins.github.util
|
||||
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.progress.EmptyProgressIndicator
|
||||
import com.intellij.openapi.progress.ProgressManager
|
||||
import com.intellij.openapi.util.Computable
|
||||
@@ -38,5 +39,10 @@ class GithubImageResizer(private val progressManager: ProgressManager) : Disposa
|
||||
|
||||
companion object {
|
||||
private fun getThreadPoolSize() = Math.max(Runtime.getRuntime().availableProcessors() / 2, 1)
|
||||
|
||||
@JvmStatic
|
||||
fun getInstance(): GithubImageResizer {
|
||||
return service()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.intellij.openapi.progress.EmptyProgressIndicator
|
||||
import org.jetbrains.plugins.github.api.GithubApiRequests
|
||||
import org.jetbrains.plugins.github.api.data.GithubRepo
|
||||
import org.jetbrains.plugins.github.api.data.request.GithubRequestPagination
|
||||
import org.jetbrains.plugins.github.api.data.request.Type
|
||||
import org.jetbrains.plugins.github.api.util.GithubApiPagesLoader
|
||||
import org.jetbrains.plugins.github.test.GithubTest
|
||||
|
||||
@@ -39,8 +40,9 @@ class GithubRequestQueryingTest : GithubTest() {
|
||||
retry {
|
||||
val result = GithubApiPagesLoader
|
||||
.loadAll(mainAccount.executor, EmptyProgressIndicator(),
|
||||
GithubApiRequests.CurrentUser.Repos.pages(mainAccount.account.server, false,
|
||||
GithubRequestPagination(pageSize = 2)))
|
||||
GithubApiRequests.CurrentUser.Repos.pages(mainAccount.account.server,
|
||||
type = Type.OWNER,
|
||||
pagination = GithubRequestPagination(pageSize = 2)))
|
||||
|
||||
assertContainsElements(result, mainRepos)
|
||||
assertDoesntContain(result, secondaryRepos)
|
||||
@@ -52,7 +54,8 @@ class GithubRequestQueryingTest : GithubTest() {
|
||||
retry {
|
||||
val result = GithubApiPagesLoader
|
||||
.loadAll(mainAccount.executor, EmptyProgressIndicator(),
|
||||
GithubApiRequests.CurrentUser.Repos.pages(mainAccount.account.server, false))
|
||||
GithubApiRequests.CurrentUser.Repos.pages(mainAccount.account.server,
|
||||
type = Type.OWNER))
|
||||
|
||||
assertContainsElements(result, mainRepos)
|
||||
assertDoesntContain(result, orgRepo)
|
||||
|
||||
Reference in New Issue
Block a user