git-branches-dashboard: add ability to open manage remotes dialog from Remote node...
[idea/community.git] / plugins / git4idea / src / git4idea / ui / branch / dashboard / BranchesDashboardActions.kt
1 // Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
2 package git4idea.ui.branch.dashboard
3
4 import com.intellij.dvcs.DvcsUtil
5 import com.intellij.dvcs.branch.GroupingKey
6 import com.intellij.dvcs.diverged
7 import com.intellij.dvcs.repo.Repository
8 import com.intellij.dvcs.ui.DvcsBundle
9 import com.intellij.icons.AllIcons
10 import com.intellij.openapi.actionSystem.*
11 import com.intellij.openapi.components.service
12 import com.intellij.openapi.project.DumbAware
13 import com.intellij.openapi.project.DumbAwareAction
14 import com.intellij.openapi.project.Project
15 import com.intellij.vcs.log.VcsLogProperties
16 import com.intellij.vcs.log.impl.VcsLogUiProperties
17 import com.intellij.vcs.log.impl.VcsProjectLog
18 import com.intellij.vcs.log.ui.VcsLogInternalDataKeys
19 import com.intellij.vcs.log.ui.actions.BooleanPropertyToggleAction
20 import git4idea.GitUtil
21 import git4idea.actions.GitFetch
22 import git4idea.branch.GitBranchType
23 import git4idea.branch.GitBrancher
24 import git4idea.config.GitVcsSettings
25 import git4idea.fetch.GitFetchResult
26 import git4idea.fetch.GitFetchSupport
27 import git4idea.i18n.GitBundle.message
28 import git4idea.i18n.GitBundleExtensions.messagePointer
29 import git4idea.isRemoteBranchProtected
30 import git4idea.remote.editRemote
31 import git4idea.remote.removeRemote
32 import git4idea.repo.GitRemote
33 import git4idea.repo.GitRepository
34 import git4idea.repo.GitRepositoryManager
35 import git4idea.ui.branch.*
36 import org.jetbrains.annotations.Nls
37 import org.jetbrains.annotations.NonNls
38 import javax.swing.Icon
39
40 internal object BranchesDashboardActions {
41
42   class BranchesTreeActionGroup(private val project: Project, private val tree: FilteringBranchesTree) : ActionGroup(), DumbAware {
43
44     init {
45       isPopup = true
46     }
47
48     override fun hideIfNoVisibleChildren() = true
49
50     override fun getChildren(e: AnActionEvent?): Array<AnAction> =
51       BranchActionsBuilder(project, tree).build()?.getChildren(e) ?: AnAction.EMPTY_ARRAY
52   }
53
54   class MultipleLocalBranchActions : ActionGroup(), DumbAware {
55     override fun getChildren(e: AnActionEvent?): Array<AnAction> =
56       arrayOf(ShowArbitraryBranchesDiffAction(), UpdateSelectedBranchAction(), DeleteBranchAction())
57   }
58
59   class CurrentBranchActions(project: Project,
60                              repositories: List<GitRepository>,
61                              branchName: String,
62                              currentRepository: GitRepository)
63     : GitBranchPopupActions.CurrentBranchActions(project, repositories, branchName, currentRepository) {
64
65     override fun getChildren(e: AnActionEvent?): Array<AnAction> {
66       val children = arrayListOf<AnAction>(NewBranchAction(), *super.getChildren(e))
67       if (myRepositories.diverged()) {
68         children.add(1, CheckoutAction(myProject, myRepositories, myBranchName))
69       }
70       return children.toTypedArray()
71     }
72   }
73
74   class LocalBranchActions(project: Project,
75                            repositories: List<GitRepository>,
76                            branchName: String,
77                            currentRepository: GitRepository)
78     : GitBranchPopupActions.LocalBranchActions(project, repositories, branchName, currentRepository) {
79
80     override fun getChildren(e: AnActionEvent?): Array<AnAction> =
81       arrayListOf<AnAction>(*super.getChildren(e)).toTypedArray()
82   }
83
84   class RemoteBranchActions(project: Project,
85                             repositories: List<GitRepository>,
86                             @NonNls branchName: String,
87                             private val currentRepository: GitRepository)
88     : GitBranchPopupActions.RemoteBranchActions(project, repositories, branchName, currentRepository) {
89
90     override fun getChildren(e: AnActionEvent?): Array<AnAction> =
91       arrayListOf<AnAction>(*super.getChildren(e), Separator(), EditRemoteAction(currentRepository), RemoveRemoteAction(currentRepository))
92         .toTypedArray()
93   }
94
95   class GroupActions(private val currentRepository: GitRepository) : ActionGroup(), DumbAware {
96
97     override fun getChildren(e: AnActionEvent?): Array<AnAction> =
98       arrayListOf<AnAction>(EditRemoteAction(currentRepository), RemoveRemoteAction(currentRepository)).toTypedArray()
99   }
100
101   class RemoteGlobalActions : ActionGroup(), DumbAware {
102
103     override fun getChildren(e: AnActionEvent?): Array<AnAction> =
104       arrayListOf<AnAction>(ActionManager.getInstance().getAction("Git.Configure.Remotes")).toTypedArray()
105   }
106
107   class BranchActionsBuilder(private val project: Project, private val tree: FilteringBranchesTree) {
108     fun build(): ActionGroup? {
109       val selectedBranches = tree.getSelectedBranches()
110       val multipleBranchSelection = selectedBranches.size > 1
111       val guessRepo = DvcsUtil.guessCurrentRepositoryQuick(project, GitUtil.getRepositoryManager(project),
112                                                            GitVcsSettings.getInstance(project).recentRootPath) ?: return null
113       if (multipleBranchSelection) {
114         return MultipleLocalBranchActions()
115       }
116
117       val branchInfo = selectedBranches.singleOrNull()
118       if (branchInfo != null) {
119         return when {
120           branchInfo.isCurrent -> CurrentBranchActions(project, branchInfo.repositories, branchInfo.branchName, guessRepo)
121           branchInfo.isLocal -> LocalBranchActions(project, branchInfo.repositories, branchInfo.branchName, guessRepo)
122           else -> RemoteBranchActions(project, branchInfo.repositories, branchInfo.branchName, guessRepo)
123         }
124       }
125
126       val selectedRemotes = tree.getSelectedRemotes()
127       if (selectedRemotes.size == 1) {
128         return GroupActions(guessRepo)
129       }
130
131       val selectedBranchNodes = tree.getSelectedBranchNodes()
132       if (selectedBranchNodes.size == 1 && selectedBranchNodes.first().type == NodeType.REMOTE_ROOT) {
133         return RemoteGlobalActions()
134       }
135
136       return null
137     }
138   }
139
140   class NewBranchAction : BranchesActionBase({ DvcsBundle.message("new.branch.action.text") },
141                                              { DvcsBundle.message("new.branch.action.text") },
142                                              com.intellij.dvcs.ui.NewBranchAction.icon) {
143
144     override fun update(e: AnActionEvent, project: Project, branches: Collection<BranchInfo>) {
145       if (branches.size > 1) {
146         e.presentation.isEnabled = false
147         e.presentation.description = message("action.Git.New.Branch.description")
148         return
149       }
150
151       val repositories = branches.flatMap(BranchInfo::repositories).distinct()
152       com.intellij.dvcs.ui.NewBranchAction.checkIfAnyRepositoryIsFresh(e, repositories)
153     }
154
155     override fun actionPerformed(e: AnActionEvent) {
156       val branches = e.getData(GIT_BRANCHES)!!
157       val project = e.project!!
158       val repositories = branches.flatMap(BranchInfo::repositories).distinct()
159       val branchName = branches.first().branchName
160       createOrCheckoutNewBranch(project, repositories, "$branchName^0", message("action.Git.New.Branch.dialog.title", branchName))
161     }
162   }
163
164   class UpdateSelectedBranchAction : BranchesActionBase(text = messagePointer("action.Git.Update.Selected.text"),
165                                                         icon = AllIcons.Actions.CheckOut) {
166     override fun update(e: AnActionEvent) {
167       val enabledAndVisible = e.project?.let(::hasRemotes) ?: false
168       e.presentation.isEnabledAndVisible = enabledAndVisible
169
170       if (enabledAndVisible) {
171         super.update(e)
172       }
173     }
174
175     override fun update(e: AnActionEvent, project: Project, branches: Collection<BranchInfo>) {
176       val presentation = e.presentation
177       if (GitFetchSupport.fetchSupport(project).isFetchRunning) {
178         presentation.isEnabled = false
179         presentation.description = message("action.Git.Update.Selected.description.already.running")
180         return
181       }
182       if (branches.any(BranchInfo::isCurrent)) {
183         presentation.isEnabled = false
184         presentation.description = message("action.Git.Update.Selected.description.select.non.current")
185         return
186       }
187       val repositories = branches.flatMap(BranchInfo::repositories).distinct()
188       val branchNames = branches.map(BranchInfo::branchName)
189       presentation.description = message("action.Git.Update.Selected.description", branches.size, branches.size)
190       val trackingInfosExist = isTrackingInfosExist(branchNames, repositories)
191       presentation.isEnabled = trackingInfosExist
192       if (!trackingInfosExist) {
193         presentation.description = message("action.Git.Update.Selected.description.tracking.not.configured", branches.size)
194       }
195     }
196
197     override fun actionPerformed(e: AnActionEvent) {
198       val branches = e.getData(GIT_BRANCHES)!!
199       val project = e.project!!
200       val repositories = branches.flatMap(BranchInfo::repositories).distinct()
201       val branchNames = branches.map(BranchInfo::branchName)
202       updateBranches(project, repositories, branchNames)
203     }
204   }
205
206   class DeleteBranchAction : BranchesActionBase(icon = AllIcons.Actions.GC) {
207     override fun update(e: AnActionEvent, project: Project, branches: Collection<BranchInfo>) {
208       e.presentation.text = message("action.Git.Delete.Branch.title", branches.size)
209       val disabled = branches.any { it.isCurrent || (!it.isLocal && isRemoteBranchProtected(it.repositories, it.branchName)) }
210       e.presentation.isEnabled = !disabled
211     }
212
213     override fun actionPerformed(e: AnActionEvent) {
214       val branches = e.getData(GIT_BRANCHES)!!
215       val project = e.project!!
216       delete(project, branches)
217     }
218
219     private fun delete(project: Project, branches: Collection<BranchInfo>) {
220       val gitBrancher = GitBrancher.getInstance(project)
221       val (localBranches, remoteBranches) = branches.partition { it.isLocal && !it.isCurrent }
222       with(gitBrancher) {
223         val branchesToContainingRepositories: Map<String, List<GitRepository>> = localBranches.associate { it.branchName to it.repositories }
224         val localBranchNames = branchesToContainingRepositories.keys
225         val deleteRemoteBranches = {
226           deleteRemoteBranches(remoteBranches.map(BranchInfo::branchName), remoteBranches.flatMap(BranchInfo::repositories).distinct())
227         }
228         if (localBranchNames.isNotEmpty()) { //delete local (possible tracked) branches first if any
229           deleteBranches(branchesToContainingRepositories, deleteRemoteBranches)
230         }
231         else {
232           deleteRemoteBranches()
233         }
234       }
235     }
236   }
237
238   class ShowBranchDiffAction : BranchesActionBase(text = messagePointer("action.Git.Compare.With.Current.title"),
239                                                   icon = AllIcons.Actions.Diff) {
240     override fun update(e: AnActionEvent, project: Project, branches: Collection<BranchInfo>) {
241       if (branches.none { !it.isCurrent }) {
242         e.presentation.isEnabled = false
243         e.presentation.description = message("action.Git.Update.Selected.description.select.non.current")
244       }
245     }
246
247     override fun actionPerformed(e: AnActionEvent) {
248       val branches = e.getData(GIT_BRANCHES)!!
249       val project = e.project!!
250       val gitBrancher = GitBrancher.getInstance(project)
251
252       for (branch in branches.filterNot(BranchInfo::isCurrent)) {
253         gitBrancher.compare(branch.branchName, branch.repositories)
254       }
255     }
256   }
257
258   class ShowArbitraryBranchesDiffAction : BranchesActionBase(text = messagePointer("action.Git.Compare.Selected.title"),
259                                                              icon = AllIcons.Actions.Diff) {
260     override fun update(e: AnActionEvent, project: Project, branches: Collection<BranchInfo>) {
261       if (branches.size != 2) {
262         e.presentation.isEnabledAndVisible = false
263         e.presentation.description = ""
264       }
265       else {
266         e.presentation.description=message("action.Git.Compare.Selected.description")
267         val commonRepositories = branches.elementAt(0).repositories intersect branches.elementAt(1).repositories
268         if (commonRepositories.isEmpty()) {
269           e.presentation.isEnabled = false
270           e.presentation.description = message("action.Git.Compare.Selected.description.disabled")
271         }
272       }
273     }
274
275     override fun actionPerformed(e: AnActionEvent) {
276       val branches = e.getData(GIT_BRANCHES)!!
277       val branchOne = branches.elementAt(0)
278       val branchTwo = branches.elementAt(1)
279       val commonRepositories = branchOne.repositories intersect branchTwo.repositories
280       val gitBrancher = GitBrancher.getInstance(e.project!!)
281
282       gitBrancher.compareAny(branchOne.branchName, branchTwo.branchName, commonRepositories.toList())
283     }
284   }
285
286   class ShowMyBranchesAction(private val uiController: BranchesDashboardController)
287     : ToggleAction(messagePointer("action.Git.Show.My.Branches.title"), AllIcons.Actions.Find), DumbAware {
288
289     override fun isSelected(e: AnActionEvent) = uiController.showOnlyMy
290
291     override fun setSelected(e: AnActionEvent, state: Boolean) {
292       uiController.showOnlyMy = state
293     }
294
295     override fun update(e: AnActionEvent) {
296       super.update(e)
297       val project = e.getData(CommonDataKeys.PROJECT)
298       if (project == null) {
299         e.presentation.isEnabled = false
300         return
301       }
302       val log = VcsProjectLog.getInstance(project)
303       val supportsIndexing = log.dataManager?.logProviders?.all {
304         VcsLogProperties.SUPPORTS_INDEXING.getOrDefault(it.value)
305       } ?: false
306
307       val isGraphReady = log.dataManager?.dataPack?.isFull ?: false
308
309       val allRootsIndexed = GitRepositoryManager.getInstance(project).repositories.all {
310         log.dataManager?.index?.isIndexed(it.root) ?: false
311       }
312
313       e.presentation.isEnabled = supportsIndexing && isGraphReady && allRootsIndexed
314       e.presentation.description = when {
315         !supportsIndexing -> {
316           message("action.Git.Show.My.Branches.description.not.support.indexing")
317         }
318         !allRootsIndexed -> {
319           message("action.Git.Show.My.Branches.description.not.all.roots.indexed")
320         }
321         !isGraphReady -> {
322           message("action.Git.Show.My.Branches.description.not.graph.ready")
323         }
324         else -> {
325           message("action.Git.Show.My.Branches.description.is.my.branch")
326         }
327       }
328     }
329   }
330
331   class FetchAction(private val ui: BranchesDashboardUi) : GitFetch() {
332     override fun update(e: AnActionEvent) {
333       super.update(e)
334       with(e.presentation) {
335         text = message("action.Git.Fetch.title")
336         icon = AllIcons.Actions.Refresh
337         description = ""
338         val project = e.project ?: return@with
339         if (GitFetchSupport.fetchSupport(project).isFetchRunning) {
340           isEnabled = false
341           description = message("action.Git.Fetch.description.fetch.in.progress")
342         }
343       }
344     }
345
346     override fun actionPerformed(e: AnActionEvent) {
347       ui.startLoadingBranches()
348       super.actionPerformed(e)
349     }
350
351     override fun onFetchFinished(result: GitFetchResult) {
352       ui.stopLoadingBranches()
353     }
354   }
355
356   class ToggleFavoriteAction : BranchesActionBase(text = messagePointer("action.Git.Toggle.Favorite.title"), icon = AllIcons.Nodes.Favorite) {
357     override fun actionPerformed(e: AnActionEvent) {
358       val project = e.project!!
359       val branches = e.getData(GIT_BRANCHES)!!
360
361       val gitBranchManager = project.service<GitBranchManager>()
362       for (branch in branches) {
363         val type = if (branch.isLocal) GitBranchType.LOCAL else GitBranchType.REMOTE
364         for (repository in branch.repositories) {
365           gitBranchManager.setFavorite(type, repository, branch.branchName, !branch.isFavorite)
366         }
367       }
368     }
369   }
370
371   class ChangeBranchFilterAction : BooleanPropertyToggleAction() {
372     override fun getProperty(): VcsLogUiProperties.VcsLogUiProperty<Boolean> = CHANGE_LOG_FILTER_ON_BRANCH_SELECTION_PROPERTY
373   }
374
375   class GroupBranchByDirectoryAction(private val tree: FilteringBranchesTree) : BranchGroupingAction(GroupingKey.GROUPING_BY_DIRECTORY,
376                                                                                                      AllIcons.Actions.GroupByPackage) {
377     override fun setSelected(key: GroupingKey, state: Boolean) {
378       tree.toggleDirectoryGrouping(state)
379     }
380   }
381
382   class HideBranchesAction : DumbAwareAction() {
383     override fun update(e: AnActionEvent) {
384       val properties = e.getData(VcsLogInternalDataKeys.LOG_UI_PROPERTIES)
385       e.presentation.isEnabledAndVisible = properties != null && properties.exists(SHOW_GIT_BRANCHES_LOG_PROPERTY)
386       super.update(e)
387     }
388
389     override fun actionPerformed(e: AnActionEvent) {
390       val properties = e.getData(VcsLogInternalDataKeys.LOG_UI_PROPERTIES)
391       if (properties != null && properties.exists(SHOW_GIT_BRANCHES_LOG_PROPERTY)) {
392         properties.set(SHOW_GIT_BRANCHES_LOG_PROPERTY, false)
393       }
394     }
395   }
396
397   class RemoveRemoteAction(private val repository: GitRepository) : RemoteActionBase(repository, messagePointer("action.Git.Log.Remove.Remote.text")) {
398
399     override fun doAction(e: AnActionEvent, project: Project, remotes: Set<GitRemote>) {
400       removeRemote(service(), repository, remotes.first())
401     }
402   }
403
404   class EditRemoteAction(private val repository: GitRepository) :
405     RemoteActionBase(repository, messagePointer("action.Git.Log.Edit.Remote.text")) {
406
407     override fun update(e: AnActionEvent, project: Project, remoteNames: Set<String>) {
408       if (remoteNames.size != 1) {
409         e.presentation.isEnabledAndVisible = false
410       }
411     }
412
413     override fun doAction(e: AnActionEvent, project: Project, remotes: Set<GitRemote>) {
414       editRemote(service(), repository, remotes.first())
415     }
416   }
417
418   abstract class RemoteActionBase(private val repository: GitRepository,
419                                   @Nls(capitalization = Nls.Capitalization.Title) text: () -> String = { "" },
420                                   @Nls(capitalization = Nls.Capitalization.Sentence) private val description: () -> String = { "" },
421                                   icon: Icon? = null) :
422     DumbAwareAction(text, description, icon) {
423
424     open fun update(e: AnActionEvent, project: Project, remoteNames: Set<String>) {}
425     abstract fun doAction(e: AnActionEvent, project: Project, remotes: Set<GitRemote>)
426
427     override fun update(e: AnActionEvent) {
428       val project = e.project
429       val remoteNames = getSelectedRemoteNames(e)
430       val enabled = project != null && remoteNames.isNotEmpty() && repository.remotes.any { remoteNames.contains(it.name) }
431       e.presentation.isEnabled = enabled
432       e.presentation.description = description()
433       if (enabled) {
434         update(e, project!!, remoteNames)
435       }
436     }
437
438     override fun actionPerformed(e: AnActionEvent) {
439       val project = e.project ?: return
440       val remoteNames = getSelectedRemoteNames(e)
441       val remotes = repository.remotes.filterTo(hashSetOf()) { remoteNames.contains(it.name) }
442
443       doAction(e, project, remotes)
444     }
445
446     private fun getSelectedRemoteNames(e: AnActionEvent): Set<String> {
447       val remoteNamesFromBranches =
448         e.getData(GIT_BRANCHES)
449           ?.asSequence()
450           ?.filterNot(BranchInfo::isLocal)
451           ?.mapNotNull { it.branchName.split("/").getOrNull(0) }?.toSet()
452       val selectedRemoteNames = e.getData(GIT_REMOTES)
453       return hashSetOf<String>().apply {
454         if (selectedRemoteNames != null) addAll(selectedRemoteNames)
455         if (remoteNamesFromBranches != null) addAll(remoteNamesFromBranches)
456       }
457     }
458   }
459
460   abstract class BranchesActionBase(@Nls(capitalization = Nls.Capitalization.Title) text: () -> String = { "" },
461                                     @Nls(capitalization = Nls.Capitalization.Sentence) private val description: () -> String = { "" },
462                                     icon: Icon? = null) :
463     DumbAwareAction(text, description, icon) {
464
465     open fun update(e: AnActionEvent, project: Project, branches: Collection<BranchInfo>) {}
466
467     override fun update(e: AnActionEvent) {
468       val branches = e.getData(GIT_BRANCHES)
469       val project = e.project
470       val enabled = project != null && branches != null && branches.isNotEmpty()
471       e.presentation.isEnabled = enabled
472       e.presentation.description = description()
473       if (enabled) {
474         update(e, project!!, branches!!)
475       }
476     }
477   }
478
479   class CheckoutSelectedBranchAction : BranchesActionBase() {
480
481     override fun update(e: AnActionEvent, project: Project, branches: Collection<BranchInfo>) {
482       if (branches.size > 1) {
483         e.presentation.isEnabled = false
484         return
485       }
486     }
487
488     override fun actionPerformed(e: AnActionEvent) {
489       val project = e.project!!
490       val branch = e.getData(GIT_BRANCHES)!!.firstOrNull() ?: return
491       if (branch.isLocal) {
492         GitBranchPopupActions.LocalBranchActions.CheckoutAction
493           .checkoutBranch(project, branch.repositories, branch.branchName)
494       }
495       else {
496         GitBranchPopupActions.RemoteBranchActions.CheckoutRemoteBranchAction
497           .checkoutRemoteBranch(project, branch.repositories, branch.branchName)
498       }
499     }
500   }
501
502   class UpdateBranchFilterInLogAction : DumbAwareAction() {
503
504     override fun update(e: AnActionEvent) {
505       val branchFilters = e.getData(GIT_BRANCH_FILTERS)
506       val uiController = e.getData(BRANCHES_UI_CONTROLLER)
507       val project = e.project
508       val enabled = project != null && uiController != null && branchFilters != null && branchFilters.isNotEmpty()
509                     && e.getData(PlatformDataKeys.CONTEXT_COMPONENT) is BranchesTreeComponent
510       e.presentation.isEnabled = enabled
511     }
512
513     override fun actionPerformed(e: AnActionEvent) {
514       e.getRequiredData(BRANCHES_UI_CONTROLLER).updateLogBranchFilter()
515     }
516   }
517
518   class RenameLocalBranch : BranchesActionBase() {
519
520     override fun update(e: AnActionEvent, project: Project, branches: Collection<BranchInfo>) {
521       if (branches.size > 1) {
522         e.presentation.isEnabled = false
523         return
524       }
525       val branch = branches.firstOrNull()
526       if (branch == null || !branch.isLocal || branch.repositories.any(Repository::isFresh)) {
527         e.presentation.isEnabled = false
528       }
529     }
530
531     override fun actionPerformed(e: AnActionEvent) {
532       val project = e.project!!
533       val branch = e.getData(GIT_BRANCHES)!!.firstOrNull() ?: return
534       GitBranchPopupActions.LocalBranchActions.RenameBranchAction.rename(project, branch.repositories, branch.branchName)
535     }
536   }
537 }