git-branches-dashboard: add edit/remove actions for Git remotes
authorDmitry Zhuravlev <dmitry.zhuravlev@jetbrains.com>
Mon, 3 Aug 2020 16:22:27 +0000 (19:22 +0300)
committerintellij-monorepo-bot <intellij-monorepo-bot-no-reply@jetbrains.com>
Tue, 11 Aug 2020 14:36:31 +0000 (14:36 +0000)
GitOrigin-RevId: 0e2eaaf2fcce59dec35751b2342e7a61603a9504

plugins/git4idea/resources/messages/GitBundle.properties
plugins/git4idea/src/git4idea/remote/GitConfigureRemotesDialog.kt
plugins/git4idea/src/git4idea/ui/branch/dashboard/BranchesDashboardActions.kt
plugins/git4idea/src/git4idea/ui/branch/dashboard/BranchesDashboardUi.kt
plugins/git4idea/src/git4idea/ui/branch/dashboard/BranchesTree.kt
plugins/git4idea/src/git4idea/ui/branch/dashboard/BranchesTreeModel.kt

index 39afa6f7693b44ba74e6993664879a3f3ffdc3d6..27aec5e72f3e3a04d1e200f0ea70672cbd534aa9 100644 (file)
@@ -515,6 +515,8 @@ action.Git.Log.Branches.Change.Branch.Filter.On.Selection.text=Update Branch Fil
 action.Git.Log.Branches.Change.Branch.Filter.On.Selection.description=When a branch is selected, set branch filter in log
 action.Git.Log.Branches.Change.Branch.Filter.text=Update Branch Filter in Log
 action.Git.Log.Branches.Change.Branch.Filter.description=Update branch filter in log with selected branches
+action.Git.Log.Edit.Remote.text=Edit Remote 
+action.Git.Log.Remove.Remote.text=Remove Remote
 group.Git.HEAD.Branch.Filter.title=HEAD (Current Branch)
 group.Git.Local.Branch.title=Local
 group.Git.Remote.Branch.title=Remote
index 4923253ee72288622e0e7e0a97d1e3f79335ae44..3a17123215b51f8b44235e0b5f69d1268e4f13ac 100644 (file)
@@ -13,7 +13,6 @@ import com.intellij.openapi.project.Project
 import com.intellij.openapi.ui.DialogWrapper
 import com.intellij.openapi.ui.DialogWrapper.IdeModalityType.IDE
 import com.intellij.openapi.ui.DialogWrapper.IdeModalityType.PROJECT
-import com.intellij.openapi.ui.Messages
 import com.intellij.openapi.ui.Messages.*
 import com.intellij.openapi.util.registry.Registry
 import com.intellij.ui.ColoredTableCellRenderer
@@ -29,17 +28,19 @@ import git4idea.repo.GitRemote
 import git4idea.repo.GitRemote.ORIGIN
 import git4idea.repo.GitRepository
 import org.jetbrains.annotations.Nls
+import java.awt.Component
 import java.awt.Font
 import java.util.*
 import javax.swing.*
 import javax.swing.table.AbstractTableModel
 import kotlin.math.min
 
+private val LOG = logger<GitConfigureRemotesDialog>()
+
 class GitConfigureRemotesDialog(val project: Project, val repositories: Collection<GitRepository>) :
     DialogWrapper(project, true, getModalityType()) {
 
   private val git = service<Git>()
-  private val LOG = logger<GitConfigureRemotesDialog>()
 
   private val NAME_COLUMN = 0
   private val URL_COLUMN = 1
@@ -83,7 +84,7 @@ class GitConfigureRemotesDialog(val project: Project, val repositories: Collecti
       runInModalTask(message("remotes.dialog.adding.remote"),
                      message("remote.dialog.add.remote"),
                      message("remotes.dialog.cannot.add.remote.error.message", dialog.remoteName, dialog.remoteUrl),
-                     repository) {
+                     repository, rebuildTreeOnSuccess) {
         git.addRemote(repository, dialog.remoteName, dialog.remoteUrl)
       }
     }
@@ -92,49 +93,17 @@ class GitConfigureRemotesDialog(val project: Project, val repositories: Collecti
   private fun removeRemote() {
     val remoteNode = getSelectedRemote()!!
     val remote = remoteNode.remote
-    if (YES == showYesNoDialog(rootPane,
-                               message("remotes.dialog.remove.remote.message", remote.name, getUrl(remote)),
-                               message("remotes.dialog.remove.remote.title"), getQuestionIcon())) {
-      runInModalTask(message("remotes.dialog.removing.remote.progress"),
-                     message("remotes.dialog.removing.remote.error.title"),
-                     message("remotes.dialog.removing.remote.error.message", remote),
-                     remoteNode.repository) {
-        git.removeRemote(remoteNode.repository, remote)
-      }
-    }
+    val repository = remoteNode.repository
+
+    removeRemote(git, repository, remote, rootPane, rebuildTreeOnSuccess)
   }
 
   private fun editRemote() {
     val remoteNode = getSelectedRemote()!!
     val remote = remoteNode.remote
     val repository = remoteNode.repository
-    val oldName = remote.name
-    val oldUrl = getUrl(remote)
 
-    val dialog = GitDefineRemoteDialog(repository, git, oldName, oldUrl)
-    if (dialog.showAndGet()) {
-      val newRemoteName = dialog.remoteName
-      val newRemoteUrl = dialog.remoteUrl
-      if (newRemoteName == oldName && newRemoteUrl == oldUrl) return
-      runInModalTask(message("remotes.changing.remote.progress"),
-                     message("remotes.changing.remote.error.title"),
-                     message("remotes.changing.remote.error.message", oldName, newRemoteName, newRemoteUrl),
-                     repository) {
-        changeRemote(repository, oldName, oldUrl, newRemoteName, newRemoteUrl)
-      }
-    }
-  }
-
-  private fun changeRemote(repo: GitRepository, oldName: String, oldUrl: String, newName: String, newUrl: String): GitCommandResult {
-    var result : GitCommandResult? = null
-    if (newName != oldName) {
-      result = git.renameRemote(repo, oldName, newName)
-      if (!result.success()) return result
-    }
-    if (newUrl != oldUrl) {
-      result = git.setRemoteUrl(repo, newName, newUrl) // NB: remote name has just been changed
-    }
-    return result!! // at least one of two has changed
+    editRemote(git, repository, remote, rebuildTreeOnSuccess)
   }
 
   private fun updateTableWidth() {
@@ -182,30 +151,7 @@ class GitConfigureRemotesDialog(val project: Project, val repositories: Collecti
     (table.model as RemotesTableModel).fireTableDataChanged()
   }
 
-  private fun runInModalTask(@Nls(capitalization = Nls.Capitalization.Title) title: String,
-                             @Nls(capitalization = Nls.Capitalization.Title) errorTitle: String,
-                             @Nls(capitalization = Nls.Capitalization.Sentence) errorMessage: String,
-                             repository: GitRepository,
-                             operation: () -> GitCommandResult) {
-    ProgressManager.getInstance().run(object : Task.Modal(project, title, true) {
-      private var result: GitCommandResult? = null
-
-      override fun run(indicator: ProgressIndicator) {
-        result = operation()
-        repository.update()
-      }
-
-      override fun onSuccess() {
-        rebuildTable()
-        if (result == null || !result!!.success()) {
-          val errorDetails = if (result == null) message("remotes.operation.not.executed.message") else result!!.errorOutputAsJoinedString
-          val message = message("remotes.operation.error.message", errorMessage, repository, errorDetails)
-          LOG.warn(message)
-          Messages.showErrorDialog(myProject, message, errorTitle)
-        }
-      }
-    })
-  }
+  private val rebuildTreeOnSuccess: () -> Unit = { rebuildTable() }
 
   private fun getSelectedRepo(): GitRepository {
     val selectedRow = table.selectedRow
@@ -224,8 +170,6 @@ class GitConfigureRemotesDialog(val project: Project, val repositories: Collecti
 
   private fun isRemoteSelected() = getSelectedRemote() != null
 
-  private fun getUrl(remote: GitRemote) = remote.urls.firstOrNull() ?: ""
-
   private abstract class Node {
     abstract fun getPresentableString() : String
   }
@@ -278,4 +222,74 @@ class GitConfigureRemotesDialog(val project: Project, val repositories: Collecti
   }
 }
 
+fun removeRemote(git: Git, repository: GitRepository, remote: GitRemote, parent: Component? = null, onSuccess: () -> Unit = {}) {
+  if (YES == showYesNoDialog(if (parent == null) parent else repository.project,
+                             message("remotes.dialog.remove.remote.message", remote.name, getUrl(remote)),
+                             message("remotes.dialog.remove.remote.title"), getQuestionIcon())) {
+    runInModalTask(message("remotes.dialog.removing.remote.progress"),
+                   message("remotes.dialog.removing.remote.error.title"),
+                   message("remotes.dialog.removing.remote.error.message", remote),
+                   repository, onSuccess) {
+      git.removeRemote(repository, remote)
+    }
+  }
+}
+
+fun editRemote(git: Git, repository: GitRepository, remote: GitRemote, onSuccess: () -> Unit = {}) {
+  val oldName = remote.name
+  val oldUrl = getUrl(remote)
+  val dialog = GitDefineRemoteDialog(repository, git, oldName, oldUrl)
+  if (dialog.showAndGet()) {
+    val newRemoteName = dialog.remoteName
+    val newRemoteUrl = dialog.remoteUrl
+    if (newRemoteName == oldName && newRemoteUrl == oldUrl) return
+    runInModalTask(message("remotes.changing.remote.progress"),
+                   message("remotes.changing.remote.error.title"),
+                   message("remotes.changing.remote.error.message", oldName, newRemoteName, newRemoteUrl),
+                   repository, onSuccess) {
+      changeRemote(git, repository, oldName, oldUrl, newRemoteName, newRemoteUrl)
+    }
+  }
+}
+
+private fun getUrl(remote: GitRemote) = remote.urls.firstOrNull() ?: ""
+
+private fun changeRemote(git: Git, repo: GitRepository, oldName: String, oldUrl: String, newName: String, newUrl: String): GitCommandResult {
+  var result : GitCommandResult? = null
+  if (newName != oldName) {
+    result = git.renameRemote(repo, oldName, newName)
+    if (!result.success()) return result
+  }
+  if (newUrl != oldUrl) {
+    result = git.setRemoteUrl(repo, newName, newUrl) // NB: remote name has just been changed
+  }
+  return result!! // at least one of two has changed
+}
+
+private fun runInModalTask(@Nls(capitalization = Nls.Capitalization.Title) title: String,
+                           @Nls(capitalization = Nls.Capitalization.Title) errorTitle: String,
+                           @Nls(capitalization = Nls.Capitalization.Sentence) errorMessage: String,
+                           repository: GitRepository,
+                           onSuccess: () -> Unit,
+                           operation: () -> GitCommandResult) {
+  ProgressManager.getInstance().run(object : Task.Modal(repository.project, title, true) {
+    private var result: GitCommandResult? = null
+
+    override fun run(indicator: ProgressIndicator) {
+      result = operation()
+      repository.update()
+    }
+
+    override fun onSuccess() {
+      onSuccess()
+      if (result == null || !result!!.success()) {
+        val errorDetails = if (result == null) message("remotes.operation.not.executed.message") else result!!.errorOutputAsJoinedString
+        val message = message("remotes.operation.error.message", errorMessage, repository, errorDetails)
+        LOG.warn(message)
+        showErrorDialog(myProject, message, errorTitle)
+      }
+    }
+  })
+}
+
 private fun getModalityType() = if (Registry.`is`("ide.perProjectModality")) PROJECT else IDE
index 3e3d44a924cdc287382725f83b2b9f2e32de5a9b..c43f0d13b387fe6283bcfa310891f861ad9a7dd7 100644 (file)
@@ -27,19 +27,22 @@ import git4idea.fetch.GitFetchSupport
 import git4idea.i18n.GitBundle.message
 import git4idea.i18n.GitBundleExtensions.messagePointer
 import git4idea.isRemoteBranchProtected
+import git4idea.remote.editRemote
+import git4idea.remote.removeRemote
+import git4idea.repo.GitRemote
 import git4idea.repo.GitRepository
 import git4idea.repo.GitRepositoryManager
 import git4idea.ui.branch.*
 import org.jetbrains.annotations.Nls
+import org.jetbrains.annotations.NonNls
 import javax.swing.Icon
 
 internal object BranchesDashboardActions {
 
   class BranchesTreeActionGroup(private val project: Project, private val tree: FilteringBranchesTree) : ActionGroup(), DumbAware {
-    override fun update(e: AnActionEvent) {
-      val enabledAndVisible = tree.getSelectedBranches().isNotEmpty()
-      e.presentation.isEnabledAndVisible = enabledAndVisible
-      isPopup = enabledAndVisible
+
+    init {
+      isPopup = true
     }
 
     override fun hideIfNoVisibleChildren() = true
@@ -49,7 +52,8 @@ internal object BranchesDashboardActions {
   }
 
   class MultipleLocalBranchActions : ActionGroup(), DumbAware {
-    override fun getChildren(e: AnActionEvent?): Array<AnAction> = arrayOf(ShowArbitraryBranchesDiffAction(), UpdateSelectedBranchAction(), DeleteBranchAction())
+    override fun getChildren(e: AnActionEvent?): Array<AnAction> =
+      arrayOf(ShowArbitraryBranchesDiffAction(), UpdateSelectedBranchAction(), DeleteBranchAction())
   }
 
   class CurrentBranchActions(project: Project,
@@ -77,6 +81,23 @@ internal object BranchesDashboardActions {
       arrayListOf<AnAction>(*super.getChildren(e)).toTypedArray()
   }
 
+  class RemoteBranchActions(project: Project,
+                            repositories: List<GitRepository>,
+                            @NonNls branchName: String,
+                            private val currentRepository: GitRepository)
+    : GitBranchPopupActions.RemoteBranchActions(project, repositories, branchName, currentRepository) {
+
+    override fun getChildren(e: AnActionEvent?): Array<AnAction> =
+      arrayListOf<AnAction>(*super.getChildren(e), Separator(), EditRemoteAction(currentRepository), RemoveRemoteAction(currentRepository))
+        .toTypedArray()
+  }
+
+  class GroupActions(private val currentRepository: GitRepository) : ActionGroup(), DumbAware {
+
+    override fun getChildren(e: AnActionEvent?): Array<AnAction> =
+      arrayListOf<AnAction>(EditRemoteAction(currentRepository), RemoveRemoteAction(currentRepository)).toTypedArray()
+  }
+
   class BranchActionsBuilder(private val project: Project, private val tree: FilteringBranchesTree) {
     fun build(): ActionGroup? {
       val selectedBranches = tree.getSelectedBranches()
@@ -86,14 +107,22 @@ internal object BranchesDashboardActions {
       if (multipleBranchSelection) {
         return MultipleLocalBranchActions()
       }
-      else {
-        val branchInfo = selectedBranches.singleOrNull() ?: return null
+
+      val branchInfo = selectedBranches.singleOrNull()
+      if (branchInfo != null) {
         return when {
           branchInfo.isCurrent -> CurrentBranchActions(project, branchInfo.repositories, branchInfo.branchName, guessRepo)
           branchInfo.isLocal -> LocalBranchActions(project, branchInfo.repositories, branchInfo.branchName, guessRepo)
-          else -> GitBranchPopupActions.RemoteBranchActions(project, branchInfo.repositories, branchInfo.branchName, guessRepo)
+          else -> RemoteBranchActions(project, branchInfo.repositories, branchInfo.branchName, guessRepo)
         }
       }
+
+      val selectedRemotes = tree.getSelectedRemotes()
+      if (selectedRemotes.size == 1) {
+        return GroupActions(guessRepo)
+      }
+
+      return null
     }
   }
 
@@ -354,6 +383,69 @@ internal object BranchesDashboardActions {
     }
   }
 
+  class RemoveRemoteAction(private val repository: GitRepository) : RemoteActionBase(repository, messagePointer("action.Git.Log.Remove.Remote.text")) {
+
+    override fun doAction(e: AnActionEvent, project: Project, remotes: Set<GitRemote>) {
+      removeRemote(service(), repository, remotes.first())
+    }
+  }
+
+  class EditRemoteAction(private val repository: GitRepository) :
+    RemoteActionBase(repository, messagePointer("action.Git.Log.Edit.Remote.text")) {
+
+    override fun update(e: AnActionEvent, project: Project, remoteNames: Set<String>) {
+      if (remoteNames.size != 1) {
+        e.presentation.isEnabledAndVisible = false
+      }
+    }
+
+    override fun doAction(e: AnActionEvent, project: Project, remotes: Set<GitRemote>) {
+      editRemote(service(), repository, remotes.first())
+    }
+  }
+
+  abstract class RemoteActionBase(private val repository: GitRepository,
+                                  @Nls(capitalization = Nls.Capitalization.Title) text: () -> String = { "" },
+                                  @Nls(capitalization = Nls.Capitalization.Sentence) private val description: () -> String = { "" },
+                                  icon: Icon? = null) :
+    DumbAwareAction(text, description, icon) {
+
+    open fun update(e: AnActionEvent, project: Project, remoteNames: Set<String>) {}
+    abstract fun doAction(e: AnActionEvent, project: Project, remotes: Set<GitRemote>)
+
+    override fun update(e: AnActionEvent) {
+      val project = e.project
+      val remoteNames = getSelectedRemoteNames(e)
+      val enabled = project != null && remoteNames.isNotEmpty() && repository.remotes.any { remoteNames.contains(it.name) }
+      e.presentation.isEnabled = enabled
+      e.presentation.description = description()
+      if (enabled) {
+        update(e, project!!, remoteNames)
+      }
+    }
+
+    override fun actionPerformed(e: AnActionEvent) {
+      val project = e.project ?: return
+      val remoteNames = getSelectedRemoteNames(e)
+      val remotes = repository.remotes.filterTo(hashSetOf()) { remoteNames.contains(it.name) }
+
+      doAction(e, project, remotes)
+    }
+
+    private fun getSelectedRemoteNames(e: AnActionEvent): Set<String> {
+      val remoteNamesFromBranches =
+        e.getData(GIT_BRANCHES)
+          ?.asSequence()
+          ?.filterNot(BranchInfo::isLocal)
+          ?.mapNotNull { it.branchName.split("/").getOrNull(0) }?.toSet()
+      val selectedRemoteNames = e.getData(GIT_REMOTES)
+      return hashSetOf<String>().apply {
+        if (selectedRemoteNames != null) addAll(selectedRemoteNames)
+        if (remoteNamesFromBranches != null) addAll(remoteNamesFromBranches)
+      }
+    }
+  }
+
   abstract class BranchesActionBase(@Nls(capitalization = Nls.Capitalization.Title) text: () -> String = { "" },
                                     @Nls(capitalization = Nls.Capitalization.Sentence) private val description: () -> String = { "" },
                                     icon: Icon? = null) :
index 37c3d1ab3efe7bd4f78536861ce5a3d765a7cc80..76a84adce7b2f0eec955c3d80af6b4732db789b8 100644 (file)
@@ -223,6 +223,7 @@ internal class BranchesDashboardUi(project: Project, private val logUi: Branches
       return when {
         GIT_BRANCHES.`is`(dataId) -> tree.getSelectedBranches()
         GIT_BRANCH_FILTERS.`is`(dataId) -> tree.getSelectedBranchFilters()
+        GIT_REMOTES.`is`(dataId) -> tree.getSelectedRemotes()
         BRANCHES_UI_CONTROLLER.`is`(dataId) -> uiController
         VcsLogInternalDataKeys.LOG_UI_PROPERTIES.`is`(dataId) -> logUi.properties
         else -> null
index b36c35732c9887c4f22496233fe278ef2603a906..ba9230a42d5f20c80b160d1a18b215c114971a64 100644 (file)
@@ -138,6 +138,16 @@ internal class BranchesTreeComponent(project: Project) : DnDAwareTree() {
       .map(TreePath::getLastPathComponent)
       .mapNotNull { it as? BranchTreeNode }
   }
+
+  fun getSelectedRemotes(): Set<String> {
+    val paths = selectionPaths ?: return emptySet()
+    return paths.asSequence()
+      .map(TreePath::getLastPathComponent)
+      .mapNotNull { it as? BranchTreeNode }
+      .filter { it.getNodeDescriptor().type == NodeType.GROUP_NODE && it.getNodeDescriptor().parent?.type == NodeType.REMOTE_ROOT }
+      .mapNotNull { it.getNodeDescriptor().displayName }
+      .toSet()
+  }
 }
 
 internal class FilteringBranchesTree(project: Project,
@@ -239,6 +249,8 @@ internal class FilteringBranchesTree(project: Project,
       .toList()
   }
 
+  fun getSelectedRemotes() = component.getSelectedRemotes()
+
   private fun restorePreviouslyExpandedPaths() {
     TreeUtil.restoreExpandedPaths(component, expandedPaths.toList())
   }
index 5b226a688094a655c50708436034bde64823ae91..5b5f96891a6804b94a16009c3a879486b5ec6bd3 100644 (file)
@@ -10,6 +10,7 @@ import javax.swing.tree.DefaultMutableTreeNode
 
 internal val GIT_BRANCHES = DataKey.create<Set<BranchInfo>>("GitBranchKey")
 internal val GIT_BRANCH_FILTERS = DataKey.create<List<String>>("GitBranchFilterKey")
+internal val GIT_REMOTES = DataKey.create<Set<String>>("GitRemoteKey")
 
 internal data class BranchInfo(val branchName: String,
                                val isLocal: Boolean,