[github] extract share project functionality to a separate object

GitOrigin-RevId: 02c01a5e93959efaf8d16947cdd0ecd613d5f317
This commit is contained in:
Ivan Semenov
2023-02-02 18:21:08 +00:00
committed by intellij-monorepo-bot
parent c49c654267
commit fd04edf693
6 changed files with 461 additions and 429 deletions
@@ -0,0 +1,330 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.plugins.github
import com.intellij.openapi.application.invokeAndWaitIfNeeded
import com.intellij.openapi.components.service
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.progress.Task
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.NlsSafe
import com.intellij.openapi.util.ThrowableComputable
import com.intellij.openapi.vcs.FilePath
import com.intellij.openapi.vcs.ProjectLevelVcsManager
import com.intellij.openapi.vcs.VcsException
import com.intellij.openapi.vcs.VcsNotifier
import com.intellij.openapi.vcs.changes.ChangeListManager
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.util.containers.ContainerUtil
import com.intellij.util.containers.mapSmartSet
import com.intellij.vcsUtil.VcsFileUtil
import git4idea.DialogManager
import git4idea.GitUtil
import git4idea.actions.GitInit
import git4idea.commands.Git
import git4idea.commands.GitCommand
import git4idea.commands.GitLineHandler
import git4idea.i18n.GitBundle
import git4idea.remote.hosting.findKnownRepositories
import git4idea.repo.GitRepository
import git4idea.util.GitFileUtils
import kotlinx.coroutines.runBlocking
import org.jetbrains.plugins.github.api.GithubApiRequestExecutor
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.GHAccountsUtil
import org.jetbrains.plugins.github.authentication.accounts.GHAccountManager
import org.jetbrains.plugins.github.authentication.accounts.GithubAccount
import org.jetbrains.plugins.github.authentication.accounts.GithubAccountInformationProvider
import org.jetbrains.plugins.github.exceptions.GithubMissingTokenException
import org.jetbrains.plugins.github.i18n.GithubBundle
import org.jetbrains.plugins.github.ui.GithubShareDialog
import org.jetbrains.plugins.github.ui.dialog.GithubExistingRemotesDialog
import org.jetbrains.plugins.github.ui.dialog.GithubUntrackedFilesDialog
import org.jetbrains.plugins.github.util.*
import java.awt.Component
import java.io.IOException
object GHShareProjectUtil {
private val LOG = GithubUtil.LOG
// get gitRepository
// check for existing git repo
// check available repos and privateRepo access (net)
// Show dialog (window)
// create GitHub repo (net)
// create local git repo (if not exist)
// add GitHub as a remote host
// make first commit
// push everything (net)
@JvmStatic
fun shareProjectOnGithub(project: Project, file: VirtualFile?) {
FileDocumentManager.getInstance().saveAllDocuments()
val gitRepository = GithubGitHelper.findGitRepository(project, file)
val possibleRemotes = gitRepository
?.let(project.service<GHHostedRepositoriesManager>()::findKnownRepositories)
?.map { it.remote.url }.orEmpty()
if (possibleRemotes.isNotEmpty()) {
val existingRemotesDialog = GithubExistingRemotesDialog(project, possibleRemotes)
DialogManager.show(existingRemotesDialog)
if (!existingRemotesDialog.isOK) {
return
}
}
val progressManager = service<ProgressManager>()
val accountInformationProvider = service<GithubAccountInformationProvider>()
val gitHelper = service<GithubGitHelper>()
val git = service<Git>()
val accountInformationLoader = object : (GithubAccount, Component) -> Pair<Boolean, Set<String>> {
private val loadedInfo = mutableMapOf<GithubAccount, Pair<Boolean, Set<String>>>()
@Throws(IOException::class)
override fun invoke(account: GithubAccount, comp: Component) = loadedInfo.getOrPut(account) {
loadEnsuringTokenExistsToken(account, comp)
}
private fun loadEnsuringTokenExistsToken(account: GithubAccount, comp: Component): Pair<Boolean, Set<String>> {
while (true) {
try {
return progressManager.runProcessWithProgressSynchronously(ThrowableComputable<Pair<Boolean, Set<String>>, IOException> {
val token = runBlocking {
service<GHAccountManager>().findCredentials(account) ?: throw GithubMissingTokenException(account)
}
val requestExecutor = GithubApiRequestExecutor.Factory.getInstance().create(token)
val user = requestExecutor.execute(progressManager.progressIndicator, GithubApiRequests.CurrentUser.get(account.server))
val names = GithubApiPagesLoader
.loadAll(requestExecutor, progressManager.progressIndicator,
GithubApiRequests.CurrentUser.Repos.pages(account.server, Type.OWNER))
.mapSmartSet { it.name }
user.canCreatePrivateRepo() to names
}, GithubBundle.message("share.process.loading.account.info", account), true, project)
}
catch (mte: GithubMissingTokenException) {
GHAccountsUtil.requestNewToken(account, project, comp) ?: throw mte
}
}
}
}
val shareDialog = GithubShareDialog(project,
gitRepository?.remotes?.map { it.name }?.toSet() ?: emptySet(),
accountInformationLoader)
DialogManager.show(shareDialog)
if (!shareDialog.isOK) {
return
}
val name: String = shareDialog.getRepositoryName()
val isPrivate: Boolean = shareDialog.isPrivate()
val remoteName: String = shareDialog.getRemoteName()
val description: String = shareDialog.getDescription()
val account: GithubAccount = shareDialog.getAccount()!!
object : Task.Backgroundable(project, GithubBundle.message("share.process")) {
private lateinit var url: String
override fun run(indicator: ProgressIndicator) {
val token = GHCompatibilityUtil.getOrRequestToken(account, project) ?: return
val requestExecutor = GithubApiRequestExecutor.Factory.getInstance().create(token)
// create GitHub repo (network)
LOG.info("Creating GitHub repository")
indicator.text = GithubBundle.message("share.process.creating.repository")
url = requestExecutor
.execute(indicator, GithubApiRequests.CurrentUser.Repos.create(account.server, name, description, isPrivate)).htmlUrl
LOG.info("Successfully created GitHub repository")
val root = gitRepository?.root ?: project.baseDir
// creating empty git repo if git is not initialized
LOG.info("Binding local project with GitHub")
if (gitRepository == null) {
LOG.info("No git detected, creating empty git repo")
indicator.text = GithubBundle.message("share.process.creating.git.repository")
if (!createEmptyGitRepository(project, root)) {
return
}
}
val repositoryManager = GitUtil.getRepositoryManager(project)
val repository = repositoryManager.getRepositoryForRoot(root)
if (repository == null) {
GithubNotifications.showError(project,
GithubNotificationIdsHolder.SHARE_CANNOT_FIND_GIT_REPO,
GithubBundle.message("share.error.failed.to.create.repo"),
GithubBundle.message("cannot.find.git.repo"))
return
}
indicator.text = GithubBundle.message("share.process.retrieving.username")
val username = accountInformationProvider.getInformation(requestExecutor, indicator, account).login
val remoteUrl = gitHelper.getRemoteUrl(account.server, username, name)
//git remote add origin git@github.com:login/name.git
LOG.info("Adding GitHub as a remote host")
indicator.text = GithubBundle.message("share.process.adding.gh.as.remote.host")
git.addRemote(repository, remoteName, remoteUrl).throwOnError()
repository.update()
// create sample commit for binding project
if (!performFirstCommitIfRequired(project, root, repository, indicator, name, url)) {
return
}
//git push origin master
LOG.info("Pushing to github master")
indicator.text = GithubBundle.message("share.process.pushing.to.github.master")
if (!pushCurrentBranch(project, repository, remoteName, remoteUrl, name, url)) {
return
}
GithubNotifications.showInfoURL(project,
GithubNotificationIdsHolder.SHARE_PROJECT_SUCCESSFULLY_SHARED,
GithubBundle.message("share.process.successfully.shared"),
name,
url)
}
private fun createEmptyGitRepository(project: Project,
root: VirtualFile): Boolean {
val result = Git.getInstance().init(project, root)
if (!result.success()) {
VcsNotifier.getInstance(project).notifyError(GithubNotificationIdsHolder.GIT_REPO_INIT_REPO,
GitBundle.message("initializing.title"),
result.errorOutputAsHtmlString)
LOG.info("Failed to create empty git repo: " + result.errorOutputAsJoinedString)
return false
}
GitInit.refreshAndConfigureVcsMappings(project, root, root.path)
GitUtil.generateGitignoreFileIfNeeded(project, root)
return true
}
private fun performFirstCommitIfRequired(project: Project,
root: VirtualFile,
repository: GitRepository,
indicator: ProgressIndicator,
name: @NlsSafe String,
url: String): Boolean {
// check if there is no commits
if (!repository.isFresh) {
return true
}
LOG.info("Trying to commit")
try {
LOG.info("Adding files for commit")
indicator.text = GithubBundle.message("share.process.adding.files")
// ask for files to add
val trackedFiles = ChangeListManager.getInstance(project).affectedFiles
val untrackedFiles =
filterOutIgnored(project, repository.untrackedFilesHolder.retrieveUntrackedFilePaths().mapNotNull(FilePath::getVirtualFile))
trackedFiles.removeAll(untrackedFiles) // fix IDEA-119855
val allFiles = ArrayList<VirtualFile>()
allFiles.addAll(trackedFiles)
allFiles.addAll(untrackedFiles)
val dialog = invokeAndWaitIfNeeded(indicator.modalityState) {
GithubUntrackedFilesDialog(project, allFiles).apply {
if (!trackedFiles.isEmpty()) {
selectedFiles = trackedFiles
}
DialogManager.show(this)
}
}
val files2commit = dialog.selectedFiles
if (!dialog.isOK || files2commit.isEmpty()) {
GithubNotifications.showInfoURL(project,
GithubNotificationIdsHolder.SHARE_EMPTY_REPO_CREATED,
GithubBundle.message("share.process.empty.project.created"),
name, url)
return false
}
val files2add = ContainerUtil.intersection(untrackedFiles, files2commit)
val files2rm = ContainerUtil.subtract(trackedFiles, files2commit)
val modified = HashSet(trackedFiles)
modified.addAll(files2commit)
GitFileUtils.addFiles(project, root, files2add)
GitFileUtils.deleteFilesFromCache(project, root, files2rm)
// commit
LOG.info("Performing commit")
indicator.text = GithubBundle.message("share.process.performing.commit")
val handler = GitLineHandler(project, root, GitCommand.COMMIT)
handler.setStdoutSuppressed(false)
handler.addParameters("-m", dialog.commitMessage)
handler.endOptions()
Git.getInstance().runCommand(handler).throwOnError()
VcsFileUtil.markFilesDirty(project, modified)
}
catch (e: VcsException) {
LOG.warn(e)
GithubNotifications.showErrorURL(project, GithubNotificationIdsHolder.SHARE_PROJECT_INIT_COMMIT_FAILED,
GithubBundle.message("share.error.cannot.finish"),
GithubBundle.message("share.error.created.project"),
" '$name' ",
GithubBundle.message("share.error.init.commit.failed") + GithubUtil.getErrorTextFromException(
e),
url)
return false
}
LOG.info("Successfully created initial commit")
return true
}
private fun filterOutIgnored(project: Project, files: Collection<VirtualFile>): Collection<VirtualFile> {
val changeListManager = ChangeListManager.getInstance(project)
val vcsManager = ProjectLevelVcsManager.getInstance(project)
return ContainerUtil.filter(files) { file -> !changeListManager.isIgnoredFile(file) && !vcsManager.isIgnored(file) }
}
private fun pushCurrentBranch(project: Project,
repository: GitRepository,
remoteName: String,
remoteUrl: String,
name: String,
url: String): Boolean {
val currentBranch = repository.currentBranch
if (currentBranch == null) {
GithubNotifications.showErrorURL(project, GithubNotificationIdsHolder.SHARE_PROJECT_INIT_PUSH_FAILED,
GithubBundle.message("share.error.cannot.finish"),
GithubBundle.message("share.error.created.project"),
" '$name' ",
GithubBundle.message("share.error.push.no.current.branch"),
url)
return false
}
val result = git.push(repository, remoteName, remoteUrl, currentBranch.name, true)
if (!result.success()) {
GithubNotifications.showErrorURL(project, GithubNotificationIdsHolder.SHARE_PROJECT_INIT_PUSH_FAILED,
GithubBundle.message("share.error.cannot.finish"),
GithubBundle.message("share.error.created.project"),
" '$name' ",
GithubBundle.message("share.error.push.failed", result.errorOutputAsHtmlString),
url)
return false
}
return true
}
override fun onThrowable(error: Throwable) {
GithubNotifications.showError(project,
GithubNotificationIdsHolder.SHARE_CANNOT_CREATE_REPO,
GithubBundle.message("share.error.failed.to.create.repo"), error)
}
}.queue()
}
}
@@ -2,73 +2,15 @@
package org.jetbrains.plugins.github
import com.intellij.CommonBundle
import com.intellij.icons.AllIcons
import com.intellij.ide.impl.isTrusted
import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.actionSystem.DataProvider
import com.intellij.openapi.application.invokeAndWaitIfNeeded
import com.intellij.openapi.components.service
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.progress.ProcessCanceledException
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.progress.Task
import com.intellij.openapi.project.DumbAwareAction
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.ui.Messages
import com.intellij.openapi.ui.Splitter
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.NlsSafe
import com.intellij.openapi.util.ThrowableComputable
import com.intellij.openapi.vcs.*
import com.intellij.openapi.vcs.changes.ChangeListManager
import com.intellij.openapi.vcs.changes.ui.SelectFilesDialog
import com.intellij.openapi.vcs.ui.CommitMessage
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.ui.components.BrowserLink
import com.intellij.ui.components.JBLabel
import com.intellij.util.containers.ContainerUtil
import com.intellij.util.containers.mapSmartSet
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.UIUtil
import com.intellij.vcsUtil.VcsFileUtil
import git4idea.DialogManager
import git4idea.GitUtil
import git4idea.actions.GitInit
import git4idea.commands.Git
import git4idea.commands.GitCommand
import git4idea.commands.GitLineHandler
import git4idea.i18n.GitBundle
import git4idea.remote.hosting.findKnownRepositories
import git4idea.repo.GitRepository
import git4idea.util.GitFileUtils
import kotlinx.coroutines.runBlocking
import org.jetbrains.annotations.NonNls
import org.jetbrains.annotations.TestOnly
import org.jetbrains.plugins.github.api.GithubApiRequestExecutor
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.GHAccountsUtil
import org.jetbrains.plugins.github.authentication.accounts.GHAccountManager
import org.jetbrains.plugins.github.authentication.accounts.GithubAccount
import org.jetbrains.plugins.github.authentication.accounts.GithubAccountInformationProvider
import org.jetbrains.plugins.github.exceptions.GithubMissingTokenException
import org.jetbrains.plugins.github.i18n.GithubBundle
import org.jetbrains.plugins.github.ui.GithubShareDialog
import org.jetbrains.plugins.github.util.*
import java.awt.BorderLayout
import java.awt.Component
import java.awt.Container
import java.io.IOException
import javax.swing.BoxLayout
import javax.swing.JComponent
import javax.swing.JLabel
import javax.swing.JPanel
class GithubShareAction : DumbAwareAction(GithubBundle.messagePointer("share.action"),
GithubBundle.messagePointer("share.action.description"),
@@ -82,15 +24,6 @@ class GithubShareAction : DumbAwareAction(GithubBundle.messagePointer("share.act
e.presentation.isEnabledAndVisible = project != null && !project.isDefault && project.isTrusted()
}
// get gitRepository
// check for existing git repo
// check available repos and privateRepo access (net)
// Show dialog (window)
// create GitHub repo (net)
// create local git repo (if not exist)
// add GitHub as a remote host
// make first commit
// push everything (net)
override fun actionPerformed(e: AnActionEvent) {
val project = e.getData(CommonDataKeys.PROJECT)
val file = e.getData(CommonDataKeys.VIRTUAL_FILE)
@@ -99,362 +32,15 @@ class GithubShareAction : DumbAwareAction(GithubBundle.messagePointer("share.act
return
}
shareProjectOnGithub(project, file)
GHShareProjectUtil.shareProjectOnGithub(project, file)
}
companion object {
private val LOG = GithubUtil.LOG
@Deprecated("Extracted to utility",
ReplaceWith("GHShareProjectUtil.shareProjectOnGithub",
"org.jetbrains.plugins.github.GHShareProjectUtil"))
@JvmStatic
fun shareProjectOnGithub(project: Project, file: VirtualFile?) {
FileDocumentManager.getInstance().saveAllDocuments()
val gitRepository = GithubGitHelper.findGitRepository(project, file)
val possibleRemotes = gitRepository
?.let(project.service<GHHostedRepositoriesManager>()::findKnownRepositories)
?.map { it.remote.url }.orEmpty()
if (possibleRemotes.isNotEmpty()) {
val existingRemotesDialog = GithubExistingRemotesDialog(project, possibleRemotes)
DialogManager.show(existingRemotesDialog)
if (!existingRemotesDialog.isOK) {
return
}
}
val progressManager = service<ProgressManager>()
val accountInformationProvider = service<GithubAccountInformationProvider>()
val gitHelper = service<GithubGitHelper>()
val git = service<Git>()
val accountInformationLoader = object : (GithubAccount, Component) -> Pair<Boolean, Set<String>> {
private val loadedInfo = mutableMapOf<GithubAccount, Pair<Boolean, Set<String>>>()
@Throws(IOException::class)
override fun invoke(account: GithubAccount, comp: Component) = loadedInfo.getOrPut(account) {
loadEnsuringTokenExistsToken(account, comp)
}
private fun loadEnsuringTokenExistsToken(account: GithubAccount, comp: Component): Pair<Boolean, Set<String>> {
while (true) {
try {
return progressManager.runProcessWithProgressSynchronously(ThrowableComputable<Pair<Boolean, Set<String>>, IOException> {
val token = runBlocking {
service<GHAccountManager>().findCredentials(account) ?: throw GithubMissingTokenException(account)
}
val requestExecutor = GithubApiRequestExecutor.Factory.getInstance().create(token)
val user = requestExecutor.execute(progressManager.progressIndicator, GithubApiRequests.CurrentUser.get(account.server))
val names = GithubApiPagesLoader
.loadAll(requestExecutor, progressManager.progressIndicator,
GithubApiRequests.CurrentUser.Repos.pages(account.server, Type.OWNER))
.mapSmartSet { it.name }
user.canCreatePrivateRepo() to names
}, GithubBundle.message("share.process.loading.account.info", account), true, project)
}
catch (mte: GithubMissingTokenException) {
GHAccountsUtil.requestNewToken(account, project, comp) ?: throw mte
}
}
}
}
val shareDialog = GithubShareDialog(project,
gitRepository?.remotes?.map { it.name }?.toSet() ?: emptySet(),
accountInformationLoader)
DialogManager.show(shareDialog)
if (!shareDialog.isOK) {
return
}
val name: String = shareDialog.getRepositoryName()
val isPrivate: Boolean = shareDialog.isPrivate()
val remoteName: String = shareDialog.getRemoteName()
val description: String = shareDialog.getDescription()
val account: GithubAccount = shareDialog.getAccount()!!
object : Task.Backgroundable(project, GithubBundle.message("share.process")) {
private lateinit var url: String
override fun run(indicator: ProgressIndicator) {
val token = GHCompatibilityUtil.getOrRequestToken(account, project) ?: return
val requestExecutor = GithubApiRequestExecutor.Factory.getInstance().create(token)
// create GitHub repo (network)
LOG.info("Creating GitHub repository")
indicator.text = GithubBundle.message("share.process.creating.repository")
url = requestExecutor
.execute(indicator, GithubApiRequests.CurrentUser.Repos.create(account.server, name, description, isPrivate)).htmlUrl
LOG.info("Successfully created GitHub repository")
val root = gitRepository?.root ?: project.baseDir
// creating empty git repo if git is not initialized
LOG.info("Binding local project with GitHub")
if (gitRepository == null) {
LOG.info("No git detected, creating empty git repo")
indicator.text = GithubBundle.message("share.process.creating.git.repository")
if (!createEmptyGitRepository(project, root)) {
return
}
}
val repositoryManager = GitUtil.getRepositoryManager(project)
val repository = repositoryManager.getRepositoryForRoot(root)
if (repository == null) {
GithubNotifications.showError(project,
GithubNotificationIdsHolder.SHARE_CANNOT_FIND_GIT_REPO,
GithubBundle.message("share.error.failed.to.create.repo"),
GithubBundle.message("cannot.find.git.repo"))
return
}
indicator.text = GithubBundle.message("share.process.retrieving.username")
val username = accountInformationProvider.getInformation(requestExecutor, indicator, account).login
val remoteUrl = gitHelper.getRemoteUrl(account.server, username, name)
//git remote add origin git@github.com:login/name.git
LOG.info("Adding GitHub as a remote host")
indicator.text = GithubBundle.message("share.process.adding.gh.as.remote.host")
git.addRemote(repository, remoteName, remoteUrl).throwOnError()
repository.update()
// create sample commit for binding project
if (!performFirstCommitIfRequired(project, root, repository, indicator, name, url)) {
return
}
//git push origin master
LOG.info("Pushing to github master")
indicator.text = GithubBundle.message("share.process.pushing.to.github.master")
if (!pushCurrentBranch(project, repository, remoteName, remoteUrl, name, url)) {
return
}
GithubNotifications.showInfoURL(project,
GithubNotificationIdsHolder.SHARE_PROJECT_SUCCESSFULLY_SHARED,
GithubBundle.message("share.process.successfully.shared"),
name,
url)
}
private fun createEmptyGitRepository(project: Project,
root: VirtualFile): Boolean {
val result = Git.getInstance().init(project, root)
if (!result.success()) {
VcsNotifier.getInstance(project).notifyError(GithubNotificationIdsHolder.GIT_REPO_INIT_REPO,
GitBundle.message("initializing.title"),
result.errorOutputAsHtmlString)
LOG.info("Failed to create empty git repo: " + result.errorOutputAsJoinedString)
return false
}
GitInit.refreshAndConfigureVcsMappings(project, root, root.path)
GitUtil.generateGitignoreFileIfNeeded(project, root)
return true
}
private fun performFirstCommitIfRequired(project: Project,
root: VirtualFile,
repository: GitRepository,
indicator: ProgressIndicator,
name: @NlsSafe String,
url: String): Boolean {
// check if there is no commits
if (!repository.isFresh) {
return true
}
LOG.info("Trying to commit")
try {
LOG.info("Adding files for commit")
indicator.text = GithubBundle.message("share.process.adding.files")
// ask for files to add
val trackedFiles = ChangeListManager.getInstance(project).affectedFiles
val untrackedFiles =
filterOutIgnored(project, repository.untrackedFilesHolder.retrieveUntrackedFilePaths().mapNotNull(FilePath::getVirtualFile))
trackedFiles.removeAll(untrackedFiles) // fix IDEA-119855
val allFiles = ArrayList<VirtualFile>()
allFiles.addAll(trackedFiles)
allFiles.addAll(untrackedFiles)
val dialog = invokeAndWaitIfNeeded(indicator.modalityState) {
GithubUntrackedFilesDialog(project, allFiles).apply {
if (!trackedFiles.isEmpty()) {
selectedFiles = trackedFiles
}
DialogManager.show(this)
}
}
val files2commit = dialog.selectedFiles
if (!dialog.isOK || files2commit.isEmpty()) {
GithubNotifications.showInfoURL(project,
GithubNotificationIdsHolder.SHARE_EMPTY_REPO_CREATED,
GithubBundle.message("share.process.empty.project.created"),
name, url)
return false
}
val files2add = ContainerUtil.intersection(untrackedFiles, files2commit)
val files2rm = ContainerUtil.subtract(trackedFiles, files2commit)
val modified = HashSet(trackedFiles)
modified.addAll(files2commit)
GitFileUtils.addFiles(project, root, files2add)
GitFileUtils.deleteFilesFromCache(project, root, files2rm)
// commit
LOG.info("Performing commit")
indicator.text = GithubBundle.message("share.process.performing.commit")
val handler = GitLineHandler(project, root, GitCommand.COMMIT)
handler.setStdoutSuppressed(false)
handler.addParameters("-m", dialog.commitMessage)
handler.endOptions()
Git.getInstance().runCommand(handler).throwOnError()
VcsFileUtil.markFilesDirty(project, modified)
}
catch (e: VcsException) {
LOG.warn(e)
GithubNotifications.showErrorURL(project, GithubNotificationIdsHolder.SHARE_PROJECT_INIT_COMMIT_FAILED,
GithubBundle.message("share.error.cannot.finish"),
GithubBundle.message("share.error.created.project"),
" '$name' ",
GithubBundle.message("share.error.init.commit.failed") + GithubUtil.getErrorTextFromException(
e),
url)
return false
}
LOG.info("Successfully created initial commit")
return true
}
private fun filterOutIgnored(project: Project, files: Collection<VirtualFile>): Collection<VirtualFile> {
val changeListManager = ChangeListManager.getInstance(project)
val vcsManager = ProjectLevelVcsManager.getInstance(project)
return ContainerUtil.filter(files) { file -> !changeListManager.isIgnoredFile(file) && !vcsManager.isIgnored(file) }
}
private fun pushCurrentBranch(project: Project,
repository: GitRepository,
remoteName: String,
remoteUrl: String,
name: String,
url: String): Boolean {
val currentBranch = repository.currentBranch
if (currentBranch == null) {
GithubNotifications.showErrorURL(project, GithubNotificationIdsHolder.SHARE_PROJECT_INIT_PUSH_FAILED,
GithubBundle.message("share.error.cannot.finish"),
GithubBundle.message("share.error.created.project"),
" '$name' ",
GithubBundle.message("share.error.push.no.current.branch"),
url)
return false
}
val result = git.push(repository, remoteName, remoteUrl, currentBranch.name, true)
if (!result.success()) {
GithubNotifications.showErrorURL(project, GithubNotificationIdsHolder.SHARE_PROJECT_INIT_PUSH_FAILED,
GithubBundle.message("share.error.cannot.finish"),
GithubBundle.message("share.error.created.project"),
" '$name' ",
GithubBundle.message("share.error.push.failed", result.errorOutputAsHtmlString),
url)
return false
}
return true
}
override fun onThrowable(error: Throwable) {
GithubNotifications.showError(project,
GithubNotificationIdsHolder.SHARE_CANNOT_CREATE_REPO,
GithubBundle.message("share.error.failed.to.create.repo"), error)
}
}.queue()
}
}
@TestOnly
class GithubExistingRemotesDialog(project: Project, private val remotes: List<String>) : DialogWrapper(project) {
init {
title = GithubBundle.message("share.error.project.is.on.github")
setOKButtonText(GithubBundle.message("share.anyway.button"))
init()
}
override fun createCenterPanel(): JComponent {
val mainText = JBLabel(if (remotes.size == 1) GithubBundle.message("share.action.remote.is.on.github")
else GithubBundle.message("share.action.remotes.are.on.github"))
val remotesPanel = JPanel().apply {
layout = BoxLayout(this, BoxLayout.Y_AXIS)
}
for (remote in remotes) {
remotesPanel.add(BrowserLink(remote, remote))
}
val messagesPanel = JBUI.Panels.simplePanel(UIUtil.DEFAULT_HGAP, UIUtil.DEFAULT_VGAP)
.addToTop(mainText)
.addToCenter(remotesPanel)
val iconContainer = Container().apply {
layout = BorderLayout()
add(JLabel(Messages.getQuestionIcon()), BorderLayout.NORTH)
}
return JBUI.Panels.simplePanel(UIUtil.DEFAULT_HGAP, UIUtil.DEFAULT_VGAP)
.addToCenter(messagesPanel)
.addToLeft(iconContainer)
.apply { border = JBUI.Borders.emptyBottom(UIUtil.LARGE_VGAP) }
}
}
@TestOnly
class GithubUntrackedFilesDialog(private val myProject: Project, untrackedFiles: List<VirtualFile>) :
SelectFilesDialog(myProject, untrackedFiles, null, null, true, false),
DataProvider {
private var myCommitMessagePanel: CommitMessage? = null
val commitMessage: String
get() = myCommitMessagePanel!!.comment
init {
title = GithubBundle.message("untracked.files.dialog.title")
setOKButtonText(CommonBundle.getAddButtonText())
setCancelButtonText(CommonBundle.getCancelButtonText())
init()
}
override fun createNorthPanel(): JComponent? {
return null
}
override fun createCenterPanel(): JComponent {
val tree = super.createCenterPanel()
val commitMessage = CommitMessage(myProject)
Disposer.register(disposable, commitMessage)
commitMessage.setCommitMessage("Initial commit")
myCommitMessagePanel = commitMessage
val splitter = Splitter(true)
splitter.setHonorComponentsMinimumSize(true)
splitter.firstComponent = tree
splitter.secondComponent = myCommitMessagePanel
splitter.proportion = 0.7f
return splitter
}
override fun getData(@NonNls dataId: String): Any? {
return if (VcsDataKeys.COMMIT_MESSAGE_CONTROL.`is`(dataId)) {
myCommitMessagePanel
}
else null
}
override fun getDimensionServiceKey(): String {
return "Github.UntrackedFilesDialog"
}
fun shareProjectOnGithub(project: Project, file: VirtualFile?): Unit = GHShareProjectUtil.shareProjectOnGithub(project, file)
}
}
@@ -0,0 +1,50 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.plugins.github.ui.dialog
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.ui.Messages
import com.intellij.ui.components.BrowserLink
import com.intellij.ui.components.JBLabel
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.UIUtil
import org.jetbrains.plugins.github.i18n.GithubBundle
import java.awt.BorderLayout
import java.awt.Container
import javax.swing.BoxLayout
import javax.swing.JComponent
import javax.swing.JLabel
import javax.swing.JPanel
internal class GithubExistingRemotesDialog(project: Project, private val remotes: List<String>) : DialogWrapper(project) {
init {
title = GithubBundle.message("share.error.project.is.on.github")
setOKButtonText(GithubBundle.message("share.anyway.button"))
init()
}
override fun createCenterPanel(): JComponent {
val mainText = JBLabel(if (remotes.size == 1) GithubBundle.message("share.action.remote.is.on.github")
else GithubBundle.message("share.action.remotes.are.on.github"))
val remotesPanel = JPanel().apply {
layout = BoxLayout(this, BoxLayout.Y_AXIS)
}
for (remote in remotes) {
remotesPanel.add(BrowserLink(remote, remote))
}
val messagesPanel = JBUI.Panels.simplePanel(UIUtil.DEFAULT_HGAP, UIUtil.DEFAULT_VGAP)
.addToTop(mainText)
.addToCenter(remotesPanel)
val iconContainer = Container().apply {
layout = BorderLayout()
add(JLabel(Messages.getQuestionIcon()), BorderLayout.NORTH)
}
return JBUI.Panels.simplePanel(UIUtil.DEFAULT_HGAP, UIUtil.DEFAULT_VGAP)
.addToCenter(messagesPanel)
.addToLeft(iconContainer)
.apply { border = JBUI.Borders.emptyBottom(UIUtil.LARGE_VGAP) }
}
}
@@ -0,0 +1,64 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.plugins.github.ui.dialog
import com.intellij.CommonBundle
import com.intellij.openapi.actionSystem.DataProvider
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.Splitter
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.vcs.VcsDataKeys
import com.intellij.openapi.vcs.changes.ui.SelectFilesDialog
import com.intellij.openapi.vcs.ui.CommitMessage
import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.annotations.NonNls
import org.jetbrains.annotations.TestOnly
import org.jetbrains.plugins.github.i18n.GithubBundle
import javax.swing.JComponent
internal class GithubUntrackedFilesDialog(private val myProject: Project, untrackedFiles: List<VirtualFile>) :
SelectFilesDialog(myProject, untrackedFiles, null, null, true, false),
DataProvider {
private var myCommitMessagePanel: CommitMessage? = null
val commitMessage: String
get() = myCommitMessagePanel!!.comment
init {
title = GithubBundle.message("untracked.files.dialog.title")
setOKButtonText(CommonBundle.getAddButtonText())
setCancelButtonText(CommonBundle.getCancelButtonText())
init()
}
override fun createNorthPanel(): JComponent? {
return null
}
override fun createCenterPanel(): JComponent {
val tree = super.createCenterPanel()
val commitMessage = CommitMessage(myProject)
Disposer.register(disposable, commitMessage)
commitMessage.setCommitMessage("Initial commit")
myCommitMessagePanel = commitMessage
val splitter = Splitter(true)
splitter.setHonorComponentsMinimumSize(true)
splitter.firstComponent = tree
splitter.secondComponent = myCommitMessagePanel
splitter.proportion = 0.7f
return splitter
}
override fun getData(@NonNls dataId: String): Any? {
return if (VcsDataKeys.COMMIT_MESSAGE_CONTROL.`is`(dataId)) {
myCommitMessagePanel
}
else null
}
override fun getDimensionServiceKey(): String {
return "Github.UntrackedFilesDialog"
}
}
@@ -9,6 +9,7 @@ import git4idea.commands.Git
import git4idea.test.TestDialogHandler
import git4idea.test.git
import org.jetbrains.plugins.github.api.GHRepositoryPath
import org.jetbrains.plugins.github.ui.dialog.GithubExistingRemotesDialog
class GithubShareProjectTest : GithubShareProjectTestBase() {
@@ -19,7 +20,7 @@ class GithubShareProjectTest : GithubShareProjectTestBase() {
createProjectFiles()
GithubShareAction.shareProjectOnGithub(myProject, projectRoot)
GHShareProjectUtil.shareProjectOnGithub(myProject, projectRoot)
checkNotification(NotificationType.INFORMATION, "Successfully shared project on GitHub", null)
findGitRepo()
@@ -31,7 +32,7 @@ class GithubShareProjectTest : GithubShareProjectTestBase() {
fun testGithubAlreadyExists() {
val shown = Ref.create(false)
dialogManager.registerDialogHandler(GithubShareAction.GithubExistingRemotesDialog::class.java,
dialogManager.registerDialogHandler(GithubExistingRemotesDialog::class.java,
TestDialogHandler {
shown.set(true)
DialogWrapper.CANCEL_EXIT_CODE
@@ -41,11 +42,11 @@ class GithubShareProjectTest : GithubShareProjectTestBase() {
registerDefaultUntrackedFilesDialogHandler()
createProjectFiles()
GithubShareAction.shareProjectOnGithub(myProject, projectRoot)
GHShareProjectUtil.shareProjectOnGithub(myProject, projectRoot)
assertFalse(shown.get())
checkRepoExists(mainAccount, GHRepositoryPath(mainAccount.username, projectName))
GithubShareAction.shareProjectOnGithub(myProject, projectRoot)
GHShareProjectUtil.shareProjectOnGithub(myProject, projectRoot)
assertTrue(shown.get())
}
@@ -62,7 +63,7 @@ class GithubShareProjectTest : GithubShareProjectTestBase() {
git("add file.txt")
git("commit -m init")
GithubShareAction.shareProjectOnGithub(myProject, projectRoot)
GHShareProjectUtil.shareProjectOnGithub(myProject, projectRoot)
checkNotification(NotificationType.INFORMATION, "Successfully shared project on GitHub", null)
findGitRepo()
@@ -81,7 +82,7 @@ class GithubShareProjectTest : GithubShareProjectTestBase() {
Git.getInstance().init(myProject, projectRoot)
GithubShareAction.shareProjectOnGithub(myProject, projectRoot)
GHShareProjectUtil.shareProjectOnGithub(myProject, projectRoot)
checkNotification(NotificationType.INFORMATION, "Successfully shared project on GitHub", null)
findGitRepo()
@@ -96,7 +97,7 @@ class GithubShareProjectTest : GithubShareProjectTestBase() {
registerSelectNoneUntrackedFilesDialogHandler()
registerDefaultShareDialogHandler()
GithubShareAction.shareProjectOnGithub(myProject, projectRoot)
GHShareProjectUtil.shareProjectOnGithub(myProject, projectRoot)
checkNotification(NotificationType.INFORMATION, "Successfully created empty repository on GitHub", null)
findGitRepo()
@@ -7,6 +7,7 @@ import git4idea.GitUtil
import git4idea.test.TestDialogHandler
import org.jetbrains.plugins.github.test.GithubGitRepoTest
import org.jetbrains.plugins.github.ui.GithubShareDialog
import org.jetbrains.plugins.github.ui.dialog.GithubUntrackedFilesDialog
abstract class GithubShareProjectTestBase : GithubGitRepoTest() {
protected lateinit var projectName: String
@@ -25,7 +26,7 @@ abstract class GithubShareProjectTestBase : GithubGitRepoTest() {
}
protected fun registerDefaultUntrackedFilesDialogHandler() {
dialogManager.registerDialogHandler(GithubShareAction.GithubUntrackedFilesDialog::class.java,
dialogManager.registerDialogHandler(GithubUntrackedFilesDialog::class.java,
TestDialogHandler {
// actually we should ask user for name/email ourselves (like in CommitDialog)
for (repository in GitUtil.getRepositoryManager(myProject).repositories) {
@@ -36,7 +37,7 @@ abstract class GithubShareProjectTestBase : GithubGitRepoTest() {
}
protected fun registerSelectNoneUntrackedFilesDialogHandler() {
dialogManager.registerDialogHandler(GithubShareAction.GithubUntrackedFilesDialog::class.java,
dialogManager.registerDialogHandler(GithubUntrackedFilesDialog::class.java,
TestDialogHandler { dialog ->
// actually we should ask user for name/email ourselves (like in CommitDialog)
for (repository in GitUtil.getRepositoryManager(myProject).repositories) {