3e3d44a924cdc287382725f83b2b9f2e32de5a9b
[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.repo.GitRepository
31 import git4idea.repo.GitRepositoryManager
32 import git4idea.ui.branch.*
33 import org.jetbrains.annotations.Nls
34 import javax.swing.Icon
35
36 internal object BranchesDashboardActions {
37
38   class BranchesTreeActionGroup(private val project: Project, private val tree: FilteringBranchesTree) : ActionGroup(), DumbAware {
39     override fun update(e: AnActionEvent) {
40       val enabledAndVisible = tree.getSelectedBranches().isNotEmpty()
41       e.presentation.isEnabledAndVisible = enabledAndVisible
42       isPopup = enabledAndVisible
43     }
44
45     override fun hideIfNoVisibleChildren() = true
46
47     override fun getChildren(e: AnActionEvent?): Array<AnAction> =
48       BranchActionsBuilder(project, tree).build()?.getChildren(e) ?: AnAction.EMPTY_ARRAY
49   }
50
51   class MultipleLocalBranchActions : ActionGroup(), DumbAware {
52     override fun getChildren(e: AnActionEvent?): Array<AnAction> = arrayOf(ShowArbitraryBranchesDiffAction(), UpdateSelectedBranchAction(), DeleteBranchAction())
53   }
54
55   class CurrentBranchActions(project: Project,
56                              repositories: List<GitRepository>,
57                              branchName: String,
58                              currentRepository: GitRepository)
59     : GitBranchPopupActions.CurrentBranchActions(project, repositories, branchName, currentRepository) {
60
61     override fun getChildren(e: AnActionEvent?): Array<AnAction> {
62       val children = arrayListOf<AnAction>(NewBranchAction(), *super.getChildren(e))
63       if (myRepositories.diverged()) {
64         children.add(1, CheckoutAction(myProject, myRepositories, myBranchName))
65       }
66       return children.toTypedArray()
67     }
68   }
69
70   class LocalBranchActions(project: Project,
71                            repositories: List<GitRepository>,
72                            branchName: String,
73                            currentRepository: GitRepository)
74     : GitBranchPopupActions.LocalBranchActions(project, repositories, branchName, currentRepository) {
75
76     override fun getChildren(e: AnActionEvent?): Array<AnAction> =
77       arrayListOf<AnAction>(*super.getChildren(e)).toTypedArray()
78   }
79
80   class BranchActionsBuilder(private val project: Project, private val tree: FilteringBranchesTree) {
81     fun build(): ActionGroup? {
82       val selectedBranches = tree.getSelectedBranches()
83       val multipleBranchSelection = selectedBranches.size > 1
84       val guessRepo = DvcsUtil.guessCurrentRepositoryQuick(project, GitUtil.getRepositoryManager(project),
85                                                            GitVcsSettings.getInstance(project).recentRootPath) ?: return null
86       if (multipleBranchSelection) {
87         return MultipleLocalBranchActions()
88       }
89       else {
90         val branchInfo = selectedBranches.singleOrNull() ?: return null
91         return when {
92           branchInfo.isCurrent -> CurrentBranchActions(project, branchInfo.repositories, branchInfo.branchName, guessRepo)
93           branchInfo.isLocal -> LocalBranchActions(project, branchInfo.repositories, branchInfo.branchName, guessRepo)
94           else -> GitBranchPopupActions.RemoteBranchActions(project, branchInfo.repositories, branchInfo.branchName, guessRepo)
95         }
96       }
97     }
98   }
99
100   class NewBranchAction : BranchesActionBase({ DvcsBundle.message("new.branch.action.text") },
101                                              { DvcsBundle.message("new.branch.action.text") },
102                                              com.intellij.dvcs.ui.NewBranchAction.icon) {
103
104     override fun update(e: AnActionEvent, project: Project, branches: Collection<BranchInfo>) {
105       if (branches.size > 1) {
106         e.presentation.isEnabled = false
107         e.presentation.description = message("action.Git.New.Branch.description")
108         return
109       }
110
111       val repositories = branches.flatMap(BranchInfo::repositories).distinct()
112       com.intellij.dvcs.ui.NewBranchAction.checkIfAnyRepositoryIsFresh(e, repositories)
113     }
114
115     override fun actionPerformed(e: AnActionEvent) {
116       val branches = e.getData(GIT_BRANCHES)!!
117       val project = e.project!!
118       val repositories = branches.flatMap(BranchInfo::repositories).distinct()
119       val branchName = branches.first().branchName
120       createOrCheckoutNewBranch(project, repositories, "$branchName^0", message("action.Git.New.Branch.dialog.title", branchName))
121     }
122   }
123
124   class UpdateSelectedBranchAction : BranchesActionBase(text = messagePointer("action.Git.Update.Selected.text"),
125                                                         icon = AllIcons.Actions.CheckOut) {
126     override fun update(e: AnActionEvent) {
127       val enabledAndVisible = e.project?.let(::hasRemotes) ?: false
128       e.presentation.isEnabledAndVisible = enabledAndVisible
129
130       if (enabledAndVisible) {
131         super.update(e)
132       }
133     }
134
135     override fun update(e: AnActionEvent, project: Project, branches: Collection<BranchInfo>) {
136       val presentation = e.presentation
137       if (GitFetchSupport.fetchSupport(project).isFetchRunning) {
138         presentation.isEnabled = false
139         presentation.description = message("action.Git.Update.Selected.description.already.running")
140         return
141       }
142       if (branches.any(BranchInfo::isCurrent)) {
143         presentation.isEnabled = false
144         presentation.description = message("action.Git.Update.Selected.description.select.non.current")
145         return
146       }
147       val repositories = branches.flatMap(BranchInfo::repositories).distinct()
148       val branchNames = branches.map(BranchInfo::branchName)
149       presentation.description = message("action.Git.Update.Selected.description", branches.size, branches.size)
150       val trackingInfosExist = isTrackingInfosExist(branchNames, repositories)
151       presentation.isEnabled = trackingInfosExist
152       if (!trackingInfosExist) {
153         presentation.description = message("action.Git.Update.Selected.description.tracking.not.configured", branches.size)
154       }
155     }
156
157     override fun actionPerformed(e: AnActionEvent) {
158       val branches = e.getData(GIT_BRANCHES)!!
159       val project = e.project!!
160       val repositories = branches.flatMap(BranchInfo::repositories).distinct()
161       val branchNames = branches.map(BranchInfo::branchName)
162       updateBranches(project, repositories, branchNames)
163     }
164   }
165
166   class DeleteBranchAction : BranchesActionBase(icon = AllIcons.Actions.GC) {
167     override fun update(e: AnActionEvent, project: Project, branches: Collection<BranchInfo>) {
168       e.presentation.text = message("action.Git.Delete.Branch.title", branches.size)
169       val disabled = branches.any { it.isCurrent || (!it.isLocal && isRemoteBranchProtected(it.repositories, it.branchName)) }
170       e.presentation.isEnabled = !disabled
171     }
172
173     override fun actionPerformed(e: AnActionEvent) {
174       val branches = e.getData(GIT_BRANCHES)!!
175       val project = e.project!!
176       delete(project, branches)
177     }
178
179     private fun delete(project: Project, branches: Collection<BranchInfo>) {
180       val gitBrancher = GitBrancher.getInstance(project)
181       val (localBranches, remoteBranches) = branches.partition { it.isLocal && !it.isCurrent }
182       with(gitBrancher) {
183         val branchesToContainingRepositories: Map<String, List<GitRepository>> = localBranches.associate { it.branchName to it.repositories }
184         val localBranchNames = branchesToContainingRepositories.keys
185         val deleteRemoteBranches = {
186           deleteRemoteBranches(remoteBranches.map(BranchInfo::branchName), remoteBranches.flatMap(BranchInfo::repositories).distinct())
187         }
188         if (localBranchNames.isNotEmpty()) { //delete local (possible tracked) branches first if any
189           deleteBranches(branchesToContainingRepositories, deleteRemoteBranches)
190         }
191         else {
192           deleteRemoteBranches()
193         }
194       }
195     }
196   }
197
198   class ShowBranchDiffAction : BranchesActionBase(text = messagePointer("action.Git.Compare.With.Current.title"),
199                                                   icon = AllIcons.Actions.Diff) {
200     override fun update(e: AnActionEvent, project: Project, branches: Collection<BranchInfo>) {
201       if (branches.none { !it.isCurrent }) {
202         e.presentation.isEnabled = false
203         e.presentation.description = message("action.Git.Update.Selected.description.select.non.current")
204       }
205     }
206
207     override fun actionPerformed(e: AnActionEvent) {
208       val branches = e.getData(GIT_BRANCHES)!!
209       val project = e.project!!
210       val gitBrancher = GitBrancher.getInstance(project)
211
212       for (branch in branches.filterNot(BranchInfo::isCurrent)) {
213         gitBrancher.compare(branch.branchName, branch.repositories)
214       }
215     }
216   }
217
218   class ShowArbitraryBranchesDiffAction : BranchesActionBase(text = messagePointer("action.Git.Compare.Selected.title"),
219                                                              icon = AllIcons.Actions.Diff) {
220     override fun update(e: AnActionEvent, project: Project, branches: Collection<BranchInfo>) {
221       if (branches.size != 2) {
222         e.presentation.isEnabledAndVisible = false
223         e.presentation.description = ""
224       }
225       else {
226         e.presentation.description=message("action.Git.Compare.Selected.description")
227         val commonRepositories = branches.elementAt(0).repositories intersect branches.elementAt(1).repositories
228         if (commonRepositories.isEmpty()) {
229           e.presentation.isEnabled = false
230           e.presentation.description = message("action.Git.Compare.Selected.description.disabled")
231         }
232       }
233     }
234
235     override fun actionPerformed(e: AnActionEvent) {
236       val branches = e.getData(GIT_BRANCHES)!!
237       val branchOne = branches.elementAt(0)
238       val branchTwo = branches.elementAt(1)
239       val commonRepositories = branchOne.repositories intersect branchTwo.repositories
240       val gitBrancher = GitBrancher.getInstance(e.project!!)
241
242       gitBrancher.compareAny(branchOne.branchName, branchTwo.branchName, commonRepositories.toList())
243     }
244   }
245
246   class ShowMyBranchesAction(private val uiController: BranchesDashboardController)
247     : ToggleAction(messagePointer("action.Git.Show.My.Branches.title"), AllIcons.Actions.Find), DumbAware {
248
249     override fun isSelected(e: AnActionEvent) = uiController.showOnlyMy
250
251     override fun setSelected(e: AnActionEvent, state: Boolean) {
252       uiController.showOnlyMy = state
253     }
254
255     override fun update(e: AnActionEvent) {
256       super.update(e)
257       val project = e.getData(CommonDataKeys.PROJECT)
258       if (project == null) {
259         e.presentation.isEnabled = false
260         return
261       }
262       val log = VcsProjectLog.getInstance(project)
263       val supportsIndexing = log.dataManager?.logProviders?.all {
264         VcsLogProperties.SUPPORTS_INDEXING.getOrDefault(it.value)
265       } ?: false
266
267       val isGraphReady = log.dataManager?.dataPack?.isFull ?: false
268
269       val allRootsIndexed = GitRepositoryManager.getInstance(project).repositories.all {
270         log.dataManager?.index?.isIndexed(it.root) ?: false
271       }
272
273       e.presentation.isEnabled = supportsIndexing && isGraphReady && allRootsIndexed
274       e.presentation.description = when {
275         !supportsIndexing -> {
276           message("action.Git.Show.My.Branches.description.not.support.indexing")
277         }
278         !allRootsIndexed -> {
279           message("action.Git.Show.My.Branches.description.not.all.roots.indexed")
280         }
281         !isGraphReady -> {
282           message("action.Git.Show.My.Branches.description.not.graph.ready")
283         }
284         else -> {
285           message("action.Git.Show.My.Branches.description.is.my.branch")
286         }
287       }
288     }
289   }
290
291   class FetchAction(private val ui: BranchesDashboardUi) : GitFetch() {
292     override fun update(e: AnActionEvent) {
293       super.update(e)
294       with(e.presentation) {
295         text = message("action.Git.Fetch.title")
296         icon = AllIcons.Actions.Refresh
297         description = ""
298         val project = e.project ?: return@with
299         if (GitFetchSupport.fetchSupport(project).isFetchRunning) {
300           isEnabled = false
301           description = message("action.Git.Fetch.description.fetch.in.progress")
302         }
303       }
304     }
305
306     override fun actionPerformed(e: AnActionEvent) {
307       ui.startLoadingBranches()
308       super.actionPerformed(e)
309     }
310
311     override fun onFetchFinished(result: GitFetchResult) {
312       ui.stopLoadingBranches()
313     }
314   }
315
316   class ToggleFavoriteAction : BranchesActionBase(text = messagePointer("action.Git.Toggle.Favorite.title"), icon = AllIcons.Nodes.Favorite) {
317     override fun actionPerformed(e: AnActionEvent) {
318       val project = e.project!!
319       val branches = e.getData(GIT_BRANCHES)!!
320
321       val gitBranchManager = project.service<GitBranchManager>()
322       for (branch in branches) {
323         val type = if (branch.isLocal) GitBranchType.LOCAL else GitBranchType.REMOTE
324         for (repository in branch.repositories) {
325           gitBranchManager.setFavorite(type, repository, branch.branchName, !branch.isFavorite)
326         }
327       }
328     }
329   }
330
331   class ChangeBranchFilterAction : BooleanPropertyToggleAction() {
332     override fun getProperty(): VcsLogUiProperties.VcsLogUiProperty<Boolean> = CHANGE_LOG_FILTER_ON_BRANCH_SELECTION_PROPERTY
333   }
334
335   class GroupBranchByDirectoryAction(private val tree: FilteringBranchesTree) : BranchGroupingAction(GroupingKey.GROUPING_BY_DIRECTORY,
336                                                                                                      AllIcons.Actions.GroupByPackage) {
337     override fun setSelected(key: GroupingKey, state: Boolean) {
338       tree.toggleDirectoryGrouping(state)
339     }
340   }
341
342   class HideBranchesAction : DumbAwareAction() {
343     override fun update(e: AnActionEvent) {
344       val properties = e.getData(VcsLogInternalDataKeys.LOG_UI_PROPERTIES)
345       e.presentation.isEnabledAndVisible = properties != null && properties.exists(SHOW_GIT_BRANCHES_LOG_PROPERTY)
346       super.update(e)
347     }
348
349     override fun actionPerformed(e: AnActionEvent) {
350       val properties = e.getData(VcsLogInternalDataKeys.LOG_UI_PROPERTIES)
351       if (properties != null && properties.exists(SHOW_GIT_BRANCHES_LOG_PROPERTY)) {
352         properties.set(SHOW_GIT_BRANCHES_LOG_PROPERTY, false)
353       }
354     }
355   }
356
357   abstract class BranchesActionBase(@Nls(capitalization = Nls.Capitalization.Title) text: () -> String = { "" },
358                                     @Nls(capitalization = Nls.Capitalization.Sentence) private val description: () -> String = { "" },
359                                     icon: Icon? = null) :
360     DumbAwareAction(text, description, icon) {
361
362     open fun update(e: AnActionEvent, project: Project, branches: Collection<BranchInfo>) {}
363
364     override fun update(e: AnActionEvent) {
365       val branches = e.getData(GIT_BRANCHES)
366       val project = e.project
367       val enabled = project != null && branches != null && branches.isNotEmpty()
368       e.presentation.isEnabled = enabled
369       e.presentation.description = description()
370       if (enabled) {
371         update(e, project!!, branches!!)
372       }
373     }
374   }
375
376   class CheckoutSelectedBranchAction : BranchesActionBase() {
377
378     override fun update(e: AnActionEvent, project: Project, branches: Collection<BranchInfo>) {
379       if (branches.size > 1) {
380         e.presentation.isEnabled = false
381         return
382       }
383     }
384
385     override fun actionPerformed(e: AnActionEvent) {
386       val project = e.project!!
387       val branch = e.getData(GIT_BRANCHES)!!.firstOrNull() ?: return
388       if (branch.isLocal) {
389         GitBranchPopupActions.LocalBranchActions.CheckoutAction
390           .checkoutBranch(project, branch.repositories, branch.branchName)
391       }
392       else {
393         GitBranchPopupActions.RemoteBranchActions.CheckoutRemoteBranchAction
394           .checkoutRemoteBranch(project, branch.repositories, branch.branchName)
395       }
396     }
397   }
398
399   class UpdateBranchFilterInLogAction : DumbAwareAction() {
400
401     override fun update(e: AnActionEvent) {
402       val branchFilters = e.getData(GIT_BRANCH_FILTERS)
403       val uiController = e.getData(BRANCHES_UI_CONTROLLER)
404       val project = e.project
405       val enabled = project != null && uiController != null && branchFilters != null && branchFilters.isNotEmpty()
406                     && e.getData(PlatformDataKeys.CONTEXT_COMPONENT) is BranchesTreeComponent
407       e.presentation.isEnabled = enabled
408     }
409
410     override fun actionPerformed(e: AnActionEvent) {
411       e.getRequiredData(BRANCHES_UI_CONTROLLER).updateLogBranchFilter()
412     }
413   }
414
415   class RenameLocalBranch : BranchesActionBase() {
416
417     override fun update(e: AnActionEvent, project: Project, branches: Collection<BranchInfo>) {
418       if (branches.size > 1) {
419         e.presentation.isEnabled = false
420         return
421       }
422       val branch = branches.firstOrNull()
423       if (branch == null || !branch.isLocal || branch.repositories.any(Repository::isFresh)) {
424         e.presentation.isEnabled = false
425       }
426     }
427
428     override fun actionPerformed(e: AnActionEvent) {
429       val project = e.project!!
430       val branch = e.getData(GIT_BRANCHES)!!.firstOrNull() ?: return
431       GitBranchPopupActions.LocalBranchActions.RenameBranchAction.rename(project, branch.repositories, branch.branchName)
432     }
433   }
434 }