github: implement clone dialog extension

GitOrigin-RevId: d49a6e291e56c41374eb4ffd83b074cf04d3396a
This commit is contained in:
Aleksandr Krasilnikov
2019-08-06 17:02:33 +03:00
committed by intellij-monorepo-bot
parent 8485c2815d
commit e76aabe924
22 changed files with 1053 additions and 21 deletions
@@ -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);
@@ -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"
}
@@ -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)
@@ -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)
}
@@ -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()
}
}
}
@@ -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)
}
}
@@ -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())
}
}
@@ -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()
)
}
}
@@ -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
}
}
}
@@ -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)