diff --git a/platform/vcs-impl/src/com/intellij/util/ui/cloneDialog/VcsCloneDialogUiSpec.kt b/platform/vcs-impl/src/com/intellij/util/ui/cloneDialog/VcsCloneDialogUiSpec.kt
index 3340a031e464..cbf00328207f 100644
--- a/platform/vcs-impl/src/com/intellij/util/ui/cloneDialog/VcsCloneDialogUiSpec.kt
+++ b/platform/vcs-impl/src/com/intellij/util/ui/cloneDialog/VcsCloneDialogUiSpec.kt
@@ -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)
}
}
diff --git a/plugins/github/resources/META-INF/plugin.xml b/plugins/github/resources/META-INF/plugin.xml
index 504a78f07b97..25dbc5364518 100644
--- a/plugins/github/resources/META-INF/plugin.xml
+++ b/plugins/github/resources/META-INF/plugin.xml
@@ -45,6 +45,8 @@
implementation="org.jetbrains.plugins.github.authentication.accounts.GithubAccountsStatisticsCollector"/>
+
diff --git a/plugins/github/src/org/jetbrains/plugins/github/GithubShareAction.kt b/plugins/github/src/org/jetbrains/plugins/github/GithubShareAction.kt
index 62c34e85c516..e420ea376b73 100644
--- a/plugins/github/src/org/jetbrains/plugins/github/GithubShareAction.kt
+++ b/plugins/github/src/org/jetbrains/plugins/github/GithubShareAction.kt
@@ -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)
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 e2e10eed3464..e593913e36fc 100644
--- a/plugins/github/src/org/jetbrains/plugins/github/api/GithubApiRequests.kt
+++ b/plugins/github/src/org/jetbrains/plugins/github/api/GithubApiRequests.kt
@@ -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> {
+ 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(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(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(url).withOperationName("get organisation repositories")
diff --git a/plugins/github/src/org/jetbrains/plugins/github/api/GithubRepositoryPath.kt b/plugins/github/src/org/jetbrains/plugins/github/api/GithubRepositoryPath.kt
index 1705a5deeb5c..b00a5d9a5892 100644
--- a/plugins/github/src/org/jetbrains/plugins/github/api/GithubRepositoryPath.kt
+++ b/plugins/github/src/org/jetbrains/plugins/github/api/GithubRepositoryPath.kt
@@ -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
}
diff --git a/plugins/github/src/org/jetbrains/plugins/github/api/GithubServerPath.java b/plugins/github/src/org/jetbrains/plugins/github/api/GithubServerPath.java
index 26c8dfee3875..246fe0f82dd6 100644
--- a/plugins/github/src/org/jetbrains/plugins/github/api/GithubServerPath.java
+++ b/plugins/github/src/org/jetbrains/plugins/github/api/GithubServerPath.java
@@ -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);
diff --git a/plugins/github/src/org/jetbrains/plugins/github/api/data/request/GHRepoRequestParams.kt b/plugins/github/src/org/jetbrains/plugins/github/api/data/request/GHRepoRequestParams.kt
new file mode 100644
index 000000000000..4e1e1bc95301
--- /dev/null
+++ b/plugins/github/src/org/jetbrains/plugins/github/api/data/request/GHRepoRequestParams.kt
@@ -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"
+}
diff --git a/plugins/github/src/org/jetbrains/plugins/github/api/data/request/GithubRequestPagination.kt b/plugins/github/src/org/jetbrains/plugins/github/api/data/request/GithubRequestPagination.kt
index f41290c5e32e..05c1f37c2f48 100644
--- a/plugins/github/src/org/jetbrains/plugins/github/api/data/request/GithubRequestPagination.kt
+++ b/plugins/github/src/org/jetbrains/plugins/github/api/data/request/GithubRequestPagination.kt
@@ -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()
}
}
diff --git a/plugins/github/src/org/jetbrains/plugins/github/api/util/GithubApiPagesLoader.kt b/plugins/github/src/org/jetbrains/plugins/github/api/util/GithubApiPagesLoader.kt
index 82dcfd1560ee..86a9106d8027 100644
--- a/plugins/github/src/org/jetbrains/plugins/github/api/util/GithubApiPagesLoader.kt
+++ b/plugins/github/src/org/jetbrains/plugins/github/api/util/GithubApiPagesLoader.kt
@@ -14,13 +14,22 @@ object GithubApiPagesLoader {
@JvmStatic
fun loadAll(executor: GithubApiRequestExecutor, indicator: ProgressIndicator, pagesRequest: Request): List {
val result = mutableListOf()
+ loadAll(executor, indicator, pagesRequest) { result.addAll(it) }
+ return result
+ }
+
+ @Throws(IOException::class)
+ @JvmStatic
+ fun loadAll(executor: GithubApiRequestExecutor,
+ indicator: ProgressIndicator,
+ pagesRequest: Request,
+ pageItemsConsumer: (List) -> Unit) {
var request: GithubApiRequest>? = 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)
diff --git a/plugins/github/src/org/jetbrains/plugins/github/authentication/GithubAuthenticationManager.kt b/plugins/github/src/org/jetbrains/plugins/github/authentication/GithubAuthenticationManager.kt
index fbfbbd273031..14759c1e3a47 100644
--- a/plugins/github/src/org/jetbrains/plugins/github/authentication/GithubAuthenticationManager.kt
+++ b/plugins/github/src/org/jetbrains/plugins/github/authentication/GithubAuthenticationManager.kt
@@ -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)
}
diff --git a/plugins/github/src/org/jetbrains/plugins/github/authentication/accounts/GithubAccountInformationProvider.kt b/plugins/github/src/org/jetbrains/plugins/github/authentication/accounts/GithubAccountInformationProvider.kt
index 2dc16f00c220..5efdc7a8911a 100644
--- a/plugins/github/src/org/jetbrains/plugins/github/authentication/accounts/GithubAccountInformationProvider.kt
+++ b/plugins/github/src/org/jetbrains/plugins/github/authentication/accounts/GithubAccountInformationProvider.kt
@@ -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()
+ }
+ }
}
\ No newline at end of file
diff --git a/plugins/github/src/org/jetbrains/plugins/github/authentication/ui/GithubCredentialsUI.kt b/plugins/github/src/org/jetbrains/plugins/github/authentication/ui/GithubCredentialsUI.kt
index b302cef94843..0e0d377b5e6b 100644
--- a/plugins/github/src/org/jetbrains/plugins/github/authentication/ui/GithubCredentialsUI.kt
+++ b/plugins/github/src/org/jetbrains/plugins/github/authentication/ui/GithubCredentialsUI.kt
@@ -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()
+ }
+ }
})
diff --git a/plugins/github/src/org/jetbrains/plugins/github/authentication/ui/GithubLoginPanel.kt b/plugins/github/src/org/jetbrains/plugins/github/authentication/ui/GithubLoginPanel.kt
index 5b9b06a1e1d9..428fca2c8588 100644
--- a/plugins/github/src/org/jetbrains/plugins/github/authentication/ui/GithubLoginPanel.kt
+++ b/plugins/github/src/org/jetbrains/plugins/github/authentication/ui/GithubLoginPanel.kt
@@ -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)
+ }
}
\ No newline at end of file
diff --git a/plugins/github/src/org/jetbrains/plugins/github/ui/cloneDialog/GHCloneDialogAccountsPanel.kt b/plugins/github/src/org/jetbrains/plugins/github/ui/cloneDialog/GHCloneDialogAccountsPanel.kt
new file mode 100644
index 000000000000..0154e1f77a7c
--- /dev/null
+++ b/plugins/github/src/org/jetbrains/plugins/github/ui/cloneDialog/GHCloneDialogAccountsPanel.kt
@@ -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()
+ private val userDetails = HashMap>()
+
+ 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())
+ }
+}
\ No newline at end of file
diff --git a/plugins/github/src/org/jetbrains/plugins/github/ui/cloneDialog/GHCloneDialogExtension.kt b/plugins/github/src/org/jetbrains/plugins/github/ui/cloneDialog/GHCloneDialogExtension.kt
new file mode 100644
index 000000000000..6cd67a534e38
--- /dev/null
+++ b/plugins/github/src/org/jetbrains/plugins/github/ui/cloneDialog/GHCloneDialogExtension.kt
@@ -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 {
+ if (!authenticationManager.hasAccounts()) {
+ return listOf(VcsCloneDialogExtensionStatusLine.greyText("No accounts"))
+ }
+
+ val list = ArrayList()
+ 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()
+ )
+ }
+}
\ No newline at end of file
diff --git a/plugins/github/src/org/jetbrains/plugins/github/ui/cloneDialog/GHCloneDialogExtensionComponent.kt b/plugins/github/src/org/jetbrains/plugins/github/ui/cloneDialog/GHCloneDialogExtensionComponent.kt
new file mode 100644
index 000000000000..dc37d32d5df7
--- /dev/null
+++ b/plugins/github/src/org/jetbrains/plugins/github/ui/cloneDialog/GHCloneDialogExtensionComponent.kt
@@ -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()
+
+ 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()
+ private val repositoriesByAccount = hashMapOf>()
+ private val errorsByAccount = hashMapOf()
+ private val originListModel = CollectionListModel()
+ private var inLoginState = false
+ private var selectedUrl by Delegates.observable(null) { _, _, _ -> onSelectedUrlChanged() }
+
+ init {
+ val speedSearch = SpeedSearch()
+
+ val filteringListModel = NameFilteringListModel(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) -> 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 {
+ val list = ArrayList()
+ 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 : LinkedHashSet() {
+ 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
+ }
+ }
+}
diff --git a/plugins/github/src/org/jetbrains/plugins/github/ui/cloneDialog/GHLoginController.kt b/plugins/github/src/org/jetbrains/plugins/github/ui/cloneDialog/GHLoginController.kt
new file mode 100644
index 000000000000..6b46d29c9432
--- /dev/null
+++ b/plugins/github/src/org/jetbrains/plugins/github/ui/cloneDialog/GHLoginController.kt
@@ -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)
+}
\ No newline at end of file
diff --git a/plugins/github/src/org/jetbrains/plugins/github/ui/cloneDialog/GHRepositoryList.kt b/plugins/github/src/org/jetbrains/plugins/github/ui/cloneDialog/GHRepositoryList.kt
new file mode 100644
index 000000000000..ad2727e6b4a5
--- /dev/null
+++ b/plugins/github/src/org/jetbrains/plugins/github/ui/cloneDialog/GHRepositoryList.kt
@@ -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) : JBList(listModel) {
+ private val authenticationManager = GithubAuthenticationManager.getInstance()
+
+ init {
+ ScrollingUtil.installActions(this)
+
+ selectionModel = SingleSelectionModel()
+
+ cellRenderer = object : ColoredListCellRenderer() {
+ val nameRenderer = AccountNameRenderer()
+
+ override fun getListCellRendererComponent(list: JList,
+ 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, 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,
+ 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
+ }
+ }
+}
\ No newline at end of file
diff --git a/plugins/github/src/org/jetbrains/plugins/github/ui/cloneDialog/GHRepositoryListItem.kt b/plugins/github/src/org/jetbrains/plugins/github/ui/cloneDialog/GHRepositoryListItem.kt
new file mode 100644
index 000000000000..5a587ba57a65
--- /dev/null
+++ b/plugins/github/src/org/jetbrains/plugins/github/ui/cloneDialog/GHRepositoryListItem.kt
@@ -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,
+ list: JList)
+
+ 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,
+ list: JList): 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,
+ list: JList) =
+ 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
+ }
+ }
+}
\ No newline at end of file
diff --git a/plugins/github/src/org/jetbrains/plugins/github/util/CachingGithubUserAvatarLoader.kt b/plugins/github/src/org/jetbrains/plugins/github/util/CachingGithubUserAvatarLoader.kt
index f8de46588fcc..5bfe57a1f274 100644
--- a/plugins/github/src/org/jetbrains/plugins/github/util/CachingGithubUserAvatarLoader.kt
+++ b/plugins/github/src/org/jetbrains/plugins/github/util/CachingGithubUserAvatarLoader.kt
@@ -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
}
}
\ No newline at end of file
diff --git a/plugins/github/src/org/jetbrains/plugins/github/util/GithubImageResizer.kt b/plugins/github/src/org/jetbrains/plugins/github/util/GithubImageResizer.kt
index bc4023a2af40..4a15ecec6879 100644
--- a/plugins/github/src/org/jetbrains/plugins/github/util/GithubImageResizer.kt
+++ b/plugins/github/src/org/jetbrains/plugins/github/util/GithubImageResizer.kt
@@ -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()
+ }
}
}
diff --git a/plugins/github/test/org/jetbrains/plugins/github/GithubRequestQueryingTest.kt b/plugins/github/test/org/jetbrains/plugins/github/GithubRequestQueryingTest.kt
index 7783830f96be..7d8af50174bd 100644
--- a/plugins/github/test/org/jetbrains/plugins/github/GithubRequestQueryingTest.kt
+++ b/plugins/github/test/org/jetbrains/plugins/github/GithubRequestQueryingTest.kt
@@ -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)