From fd04edf6938b3a58123abdd4c0b3b3961bc76c85 Mon Sep 17 00:00:00 2001 From: Ivan Semenov Date: Thu, 2 Feb 2023 13:39:39 +0100 Subject: [PATCH] [github] extract share project functionality to a separate object GitOrigin-RevId: 02c01a5e93959efaf8d16947cdd0ecd613d5f317 --- .../plugins/github/GHShareProjectUtil.kt | 330 ++++++++++++++ .../plugins/github/GithubShareAction.kt | 426 +----------------- .../ui/dialog/GithubExistingRemotesDialog.kt | 50 ++ .../ui/dialog/GithubUntrackedFilesDialog.kt | 64 +++ .../plugins/github/GithubShareProjectTest.kt | 15 +- .../github/GithubShareProjectTestBase.kt | 5 +- 6 files changed, 461 insertions(+), 429 deletions(-) create mode 100644 plugins/github/src/org/jetbrains/plugins/github/GHShareProjectUtil.kt create mode 100644 plugins/github/src/org/jetbrains/plugins/github/ui/dialog/GithubExistingRemotesDialog.kt create mode 100644 plugins/github/src/org/jetbrains/plugins/github/ui/dialog/GithubUntrackedFilesDialog.kt diff --git a/plugins/github/src/org/jetbrains/plugins/github/GHShareProjectUtil.kt b/plugins/github/src/org/jetbrains/plugins/github/GHShareProjectUtil.kt new file mode 100644 index 000000000000..b99da7b38533 --- /dev/null +++ b/plugins/github/src/org/jetbrains/plugins/github/GHShareProjectUtil.kt @@ -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()::findKnownRepositories) + ?.map { it.remote.url }.orEmpty() + if (possibleRemotes.isNotEmpty()) { + val existingRemotesDialog = GithubExistingRemotesDialog(project, possibleRemotes) + DialogManager.show(existingRemotesDialog) + if (!existingRemotesDialog.isOK) { + return + } + } + + val progressManager = service() + val accountInformationProvider = service() + val gitHelper = service() + val git = service() + + val accountInformationLoader = object : (GithubAccount, Component) -> Pair> { + private val loadedInfo = mutableMapOf>>() + + @Throws(IOException::class) + override fun invoke(account: GithubAccount, comp: Component) = loadedInfo.getOrPut(account) { + loadEnsuringTokenExistsToken(account, comp) + } + + private fun loadEnsuringTokenExistsToken(account: GithubAccount, comp: Component): Pair> { + while (true) { + try { + return progressManager.runProcessWithProgressSynchronously(ThrowableComputable>, IOException> { + val token = runBlocking { + service().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() + 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): Collection { + 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() + } +} \ No newline at end of file diff --git a/plugins/github/src/org/jetbrains/plugins/github/GithubShareAction.kt b/plugins/github/src/org/jetbrains/plugins/github/GithubShareAction.kt index 8f743ac3acc2..af2288826617 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/GithubShareAction.kt +++ b/plugins/github/src/org/jetbrains/plugins/github/GithubShareAction.kt @@ -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()::findKnownRepositories) - ?.map { it.remote.url }.orEmpty() - if (possibleRemotes.isNotEmpty()) { - val existingRemotesDialog = GithubExistingRemotesDialog(project, possibleRemotes) - DialogManager.show(existingRemotesDialog) - if (!existingRemotesDialog.isOK) { - return - } - } - - val progressManager = service() - val accountInformationProvider = service() - val gitHelper = service() - val git = service() - - val accountInformationLoader = object : (GithubAccount, Component) -> Pair> { - private val loadedInfo = mutableMapOf>>() - - @Throws(IOException::class) - override fun invoke(account: GithubAccount, comp: Component) = loadedInfo.getOrPut(account) { - loadEnsuringTokenExistsToken(account, comp) - } - - private fun loadEnsuringTokenExistsToken(account: GithubAccount, comp: Component): Pair> { - while (true) { - try { - return progressManager.runProcessWithProgressSynchronously(ThrowableComputable>, IOException> { - val token = runBlocking { - service().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() - 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): Collection { - 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) : 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) : - 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) } } \ No newline at end of file diff --git a/plugins/github/src/org/jetbrains/plugins/github/ui/dialog/GithubExistingRemotesDialog.kt b/plugins/github/src/org/jetbrains/plugins/github/ui/dialog/GithubExistingRemotesDialog.kt new file mode 100644 index 000000000000..f512c79d967c --- /dev/null +++ b/plugins/github/src/org/jetbrains/plugins/github/ui/dialog/GithubExistingRemotesDialog.kt @@ -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) : 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) } + } +} \ No newline at end of file diff --git a/plugins/github/src/org/jetbrains/plugins/github/ui/dialog/GithubUntrackedFilesDialog.kt b/plugins/github/src/org/jetbrains/plugins/github/ui/dialog/GithubUntrackedFilesDialog.kt new file mode 100644 index 000000000000..4c3f632bd5fb --- /dev/null +++ b/plugins/github/src/org/jetbrains/plugins/github/ui/dialog/GithubUntrackedFilesDialog.kt @@ -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) : + 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" + } +} \ No newline at end of file diff --git a/plugins/github/test/org/jetbrains/plugins/github/GithubShareProjectTest.kt b/plugins/github/test/org/jetbrains/plugins/github/GithubShareProjectTest.kt index 3dee93cf7867..2f69a8eeb86e 100644 --- a/plugins/github/test/org/jetbrains/plugins/github/GithubShareProjectTest.kt +++ b/plugins/github/test/org/jetbrains/plugins/github/GithubShareProjectTest.kt @@ -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() diff --git a/plugins/github/test/org/jetbrains/plugins/github/GithubShareProjectTestBase.kt b/plugins/github/test/org/jetbrains/plugins/github/GithubShareProjectTestBase.kt index 12b4b7c7555a..89af4acfdc9a 100644 --- a/plugins/github/test/org/jetbrains/plugins/github/GithubShareProjectTestBase.kt +++ b/plugins/github/test/org/jetbrains/plugins/github/GithubShareProjectTestBase.kt @@ -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) {