mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
git: add an internal "Cleanup Branches" Action to quickly review & delete existing branches
The action is made internal, since the UI is of prototype quality for now.
This commit is contained in:
@@ -105,6 +105,7 @@
|
||||
<action id="Git.CherryPick.Abort" class="git4idea.actions.GitAbortOperationAction$CherryPick" text="Abort Cherry-Pick"/>
|
||||
<action id="Git.Rebase.Continue" class="git4idea.actions.GitRebaseContinue" text="Continue Rebase"/>
|
||||
<action id="Git.Rebase.Skip" class="git4idea.actions.GitRebaseSkip" text="Skip Commit"/>
|
||||
<action id="Git.Cleanup.Branches" class="git4idea.branch.GitCleanupBranchesAction" text="Cleanup Branches" internal="true"/>.
|
||||
</group>
|
||||
|
||||
<group id="Git.Ongoing.Rebase.Actions" popup="false">
|
||||
|
||||
@@ -0,0 +1,450 @@
|
||||
// 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.
|
||||
package git4idea.branch
|
||||
|
||||
import com.intellij.dvcs.DvcsUtil
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.actionSystem.*
|
||||
import com.intellij.openapi.diagnostic.logger
|
||||
import com.intellij.openapi.keymap.KeymapUtil.*
|
||||
import com.intellij.openapi.progress.ProgressIndicator
|
||||
import com.intellij.openapi.progress.Task
|
||||
import com.intellij.openapi.progress.util.ProgressWindow
|
||||
import com.intellij.openapi.project.DumbAware
|
||||
import com.intellij.openapi.project.DumbAwareAction
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.openapi.wm.IdeFocusManager
|
||||
import com.intellij.openapi.wm.ToolWindowId
|
||||
import com.intellij.openapi.wm.ToolWindowManager
|
||||
import com.intellij.ui.*
|
||||
import com.intellij.ui.components.JBList
|
||||
import com.intellij.ui.components.JBLoadingPanel
|
||||
import com.intellij.ui.content.ContentFactory
|
||||
import com.intellij.ui.speedSearch.NameFilteringListModel
|
||||
import com.intellij.ui.speedSearch.SpeedSearch
|
||||
import com.intellij.util.ContentsUtil
|
||||
import com.intellij.util.ThreeState
|
||||
import com.intellij.util.ThreeState.UNSURE
|
||||
import com.intellij.util.ui.components.BorderLayoutPanel
|
||||
import com.intellij.vcs.log.VcsLogProperties
|
||||
import com.intellij.vcs.log.data.DataPackChangeListener
|
||||
import com.intellij.vcs.log.graph.impl.facade.PermanentGraphImpl
|
||||
import com.intellij.vcs.log.graph.impl.facade.ReachableNodes
|
||||
import com.intellij.vcs.log.graph.utils.DfsWalk
|
||||
import com.intellij.vcs.log.graph.utils.LinearGraphUtils
|
||||
import com.intellij.vcs.log.impl.VcsProjectLog
|
||||
import com.intellij.vcs.log.util.VcsLogUtil
|
||||
import com.intellij.vcs.log.util.findBranch
|
||||
import com.intellij.vcs.log.visible.filters.VcsLogFilterObject
|
||||
import com.intellij.vcs.log.visible.filters.VcsLogUserFilterImpl
|
||||
import git4idea.config.GitSharedSettings
|
||||
import git4idea.repo.GitRepository
|
||||
import git4idea.repo.GitRepositoryManager
|
||||
import gnu.trove.TIntHashSet
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.event.KeyEvent
|
||||
import javax.swing.*
|
||||
import javax.swing.event.DocumentEvent
|
||||
|
||||
private val LOG = logger<GitCleanupBranchesAction>()
|
||||
|
||||
class GitCleanupBranchesAction : DumbAwareAction() {
|
||||
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
val project = e.project!!
|
||||
val branches = getAllBranches(project)
|
||||
|
||||
val toolWindow = ToolWindowManager.getInstance(project).getToolWindow(ToolWindowId.VCS)
|
||||
val ui = BranchesUi(project, VcsProjectLog.getInstance(project), branches)
|
||||
val content = ContentFactory.SERVICE.getInstance().createContent(ui.getComponent(), "Cleanup Branches", true)
|
||||
ContentsUtil.addContent(toolWindow.contentManager, content, true)
|
||||
Disposer.register(content, ui)
|
||||
toolWindow.activate(null, true, true)
|
||||
|
||||
VcsLogUtil.runWhenLogIsReady(project) { _, _ ->
|
||||
ui.stopLoading()
|
||||
} // schedule initialization: need the log for other actions
|
||||
}
|
||||
}
|
||||
|
||||
private data class BranchInfo(val branchName: String,
|
||||
val isLocal: Boolean,
|
||||
val repositories: List<GitRepository>) {
|
||||
var isMy: ThreeState = UNSURE
|
||||
}
|
||||
|
||||
private class BranchesUi(val project: Project, val log: VcsProjectLog, initialBranches: List<BranchInfo>) : Disposable {
|
||||
val branches = HashSet<BranchInfo>(initialBranches)
|
||||
var showOnlyMy: Boolean = false
|
||||
|
||||
val model = CollectionListModel<BranchInfo>(sort(branches))
|
||||
val list = BranchesList(project)
|
||||
val panel = MyPanel(list)
|
||||
val loadingPanel = JBLoadingPanel(BorderLayout(), this, ProgressWindow.DEFAULT_PROGRESS_DIALOG_POSTPONE_TIME_MILLIS)
|
||||
|
||||
val speedSearch = SpeedSearch()
|
||||
val filteringListModel = NameFilteringListModel<BranchInfo>(
|
||||
model, { it.branchName }, speedSearch::shouldBeShowing, { speedSearch.filter ?: "" })
|
||||
|
||||
val changeListener = DataPackChangeListener {
|
||||
val newBranches = getAllBranches(project)
|
||||
branches.retainAll(newBranches)
|
||||
branches.addAll(newBranches)
|
||||
updateMyBranchesIfNeeded()
|
||||
}
|
||||
|
||||
init {
|
||||
filteringListModel.setFilter { t: BranchInfo? -> t != null && speedSearch.shouldBeShowing(t.branchName)
|
||||
&& (!showOnlyMy || t.isMy == ThreeState.YES) }
|
||||
|
||||
list.model = filteringListModel
|
||||
|
||||
speedSearch.addChangeListener {
|
||||
filteringListModel.refilter()
|
||||
}
|
||||
|
||||
log.dataManager?.addDataPackChangeListener(changeListener)
|
||||
}
|
||||
|
||||
fun getComponent() : JComponent {
|
||||
val searchField = object : SearchTextField(false) {
|
||||
override fun preprocessEventForTextField(e: KeyEvent): Boolean {
|
||||
if (e.keyCode == KeyEvent.VK_DOWN || e.keyCode == KeyEvent.VK_UP) {
|
||||
list.dispatchEvent(e)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
searchField.addDocumentListener(object : DocumentAdapter() {
|
||||
override fun textChanged(e: DocumentEvent) {
|
||||
speedSearch.updatePattern(searchField.text)
|
||||
}
|
||||
})
|
||||
|
||||
val diffAction = ShowBranchDiffAction()
|
||||
diffAction.registerCustomShortcutSet(getActiveKeymapShortcuts("Diff.ShowDiff"), panel)
|
||||
|
||||
val deleteAction = DeleteBranchAction()
|
||||
val shortcuts = getActiveKeymapShortcuts("SafeDelete").shortcuts + getActiveKeymapShortcuts("EditorDeleteToLineStart").shortcuts
|
||||
deleteAction.registerCustomShortcutSet(CustomShortcutSet(*shortcuts), panel)
|
||||
|
||||
createFocusFilterFieldAction(searchField)
|
||||
|
||||
val showMyBranchesAction = ShowMyBranchesAction()
|
||||
|
||||
val group = DefaultActionGroup()
|
||||
group.add(deleteAction)
|
||||
group.add(diffAction)
|
||||
group.add(showMyBranchesAction)
|
||||
val toolbar = ActionManager.getInstance().createActionToolbar("Git.Cleanup.Branches", group, false)
|
||||
toolbar.setTargetComponent(list)
|
||||
|
||||
panel.addToTop(searchField)
|
||||
panel.addToLeft(toolbar.component)
|
||||
panel.addToCenter(ScrollPaneFactory.createScrollPane(list, true))
|
||||
|
||||
loadingPanel.add(panel)
|
||||
loadingPanel.startLoading()
|
||||
return loadingPanel
|
||||
}
|
||||
|
||||
private fun createFocusFilterFieldAction(searchField: SearchTextField) {
|
||||
DumbAwareAction.create { e ->
|
||||
val project = e.getRequiredData(CommonDataKeys.PROJECT)
|
||||
if (IdeFocusManager.getInstance(project).getFocusedDescendantFor(list) != null) {
|
||||
IdeFocusManager.getInstance(project).requestFocus(searchField, true)
|
||||
}
|
||||
else {
|
||||
IdeFocusManager.getInstance(project).requestFocus(list, true)
|
||||
}
|
||||
}.registerCustomShortcutSet(getActiveKeymapShortcuts("Find"), panel)
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
log.dataManager?.removeDataPackChangeListener(changeListener)
|
||||
}
|
||||
|
||||
fun stopLoading() {
|
||||
loadingPanel.stopLoading()
|
||||
}
|
||||
|
||||
fun updateMyBranchesIfNeeded() {
|
||||
if (showOnlyMy) {
|
||||
loadingPanel.startLoading()
|
||||
panel.isEnabled = false
|
||||
|
||||
val branchesToCheck = branches.filter { it.isMy == UNSURE }
|
||||
object: Task.Backgroundable(project, "Calculating My Branches", true) {
|
||||
var myBranches : List<BranchInfo>? = null
|
||||
override fun run(indicator: ProgressIndicator) {
|
||||
myBranches = checkBranchesSynchronously(log, branchesToCheck, indicator)
|
||||
}
|
||||
|
||||
override fun onSuccess() {
|
||||
if (myBranches == null) return
|
||||
|
||||
for (branch in branches) {
|
||||
if (branch.isMy == UNSURE) {
|
||||
branch.isMy = if (myBranches!!.contains(branch)) ThreeState.YES else ThreeState.NO
|
||||
}
|
||||
}
|
||||
|
||||
updateListAccordingToCurrentlyKnownBranches()
|
||||
}
|
||||
|
||||
override fun onFinished() {
|
||||
panel.isEnabled = true
|
||||
loadingPanel.stopLoading()
|
||||
}
|
||||
}.queue()
|
||||
}
|
||||
else {
|
||||
updateListAccordingToCurrentlyKnownBranches()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateListAccordingToCurrentlyKnownBranches() {
|
||||
preserveSelection {
|
||||
model.replaceAll(sort(branches))
|
||||
filteringListModel.refilter()
|
||||
}
|
||||
}
|
||||
|
||||
private fun preserveSelection(action: () -> Unit) {
|
||||
val selectedValue = list.selectedValue
|
||||
val selectedIndex = list.selectedIndex
|
||||
|
||||
action()
|
||||
|
||||
var newSelectedIndex = -1
|
||||
for (i in 0 until list.model.size) {
|
||||
val element = list.model.getElementAt(i)
|
||||
if (element == selectedValue) {
|
||||
newSelectedIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
list.selectedIndex = if (newSelectedIndex > 0) newSelectedIndex else selectedIndex
|
||||
}
|
||||
|
||||
private inner class ShowMyBranchesAction : ToggleAction("Show My Branches", null, AllIcons.Actions.Find), DumbAware {
|
||||
override fun isSelected(e: AnActionEvent): Boolean {
|
||||
return showOnlyMy
|
||||
}
|
||||
|
||||
override fun setSelected(e: AnActionEvent, state: Boolean) {
|
||||
showOnlyMy = state
|
||||
updateMyBranchesIfNeeded()
|
||||
}
|
||||
|
||||
override fun update(e: AnActionEvent) {
|
||||
super.update(e)
|
||||
val log = VcsProjectLog.getInstance(project)
|
||||
val supportsIndexing = log.dataManager?.logProviders?.all {
|
||||
VcsLogProperties.get(it.value, VcsLogProperties.SUPPORTS_INDEXING) } ?: false
|
||||
|
||||
val isGraphReady = log.dataManager?.dataPack?.isFull ?: false
|
||||
|
||||
val allRootsIndexed = GitRepositoryManager.getInstance(project).repositories.all {
|
||||
log.dataManager?.index?.isIndexed(it.root) ?: false
|
||||
}
|
||||
|
||||
e.presentation.isEnabled = supportsIndexing && isGraphReady && allRootsIndexed
|
||||
e.presentation.description = if (!supportsIndexing) {
|
||||
"Some of repositories doesn't support indexing."
|
||||
}
|
||||
else if (!allRootsIndexed) {
|
||||
"Not all repositories are indexed."
|
||||
}
|
||||
else if (!isGraphReady) {
|
||||
"The log is not ready yet, please wait a bit."
|
||||
}
|
||||
else {
|
||||
"A branch is 'My' if all exclusive commits of this branch are made by 'me', i.e. by current Git author."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class MyPanel(val list: BranchesList) : BorderLayoutPanel(), DataProvider {
|
||||
override fun getData(dataId: String): Any? {
|
||||
if (GIT_BRANCH.`is`(dataId)) {
|
||||
return list.selectedValue
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private class BranchesList(project: Project) : JBList<BranchInfo>() {
|
||||
init {
|
||||
selectionMode = ListSelectionModel.SINGLE_SELECTION
|
||||
cellRenderer = object : ColoredListCellRenderer<BranchInfo>() {
|
||||
override fun customizeCellRenderer(list: JList<out BranchInfo>, value: BranchInfo?, index: Int, selected: Boolean, hasFocus: Boolean) {
|
||||
if (value == null) {
|
||||
return
|
||||
}
|
||||
append(value.branchName)
|
||||
if (value.repositories.size < GitRepositoryManager.getInstance(project).repositories.size) {
|
||||
append(" (${DvcsUtil.getShortNames(value.repositories)})", SimpleTextAttributes.GRAYED_ATTRIBUTES)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class DeleteBranchAction : CleanupBranchesActionBase("Delete Branch", null, AllIcons.Actions.GC) {
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
val branch = e.getData(GIT_BRANCH)!!
|
||||
val project = e.project!!
|
||||
val brancher = GitBrancher.getInstance(project)
|
||||
|
||||
if (branch.isLocal) {
|
||||
brancher.deleteBranch(branch.branchName, branch.repositories)
|
||||
}
|
||||
else {
|
||||
brancher.deleteRemoteBranch(branch.branchName, branch.repositories)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ShowBranchDiffAction : CleanupBranchesActionBase("Compare with Current", null, AllIcons.Actions.Diff) {
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
val branch = e.getData(GIT_BRANCH)!!
|
||||
val project = e.project!!
|
||||
|
||||
VcsLogUtil.runWhenLogIsReady(project) { log, logManager ->
|
||||
val filters = VcsLogFilterObject.fromRange("HEAD", branch.branchName)
|
||||
log.tabsManager.openAnotherLogTab(logManager, VcsLogFilterObject.collection(filters))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private abstract class CleanupBranchesActionBase(text: String, description: String?, icon: Icon) :
|
||||
DumbAwareAction(text, description, icon) {
|
||||
|
||||
override fun update(e: AnActionEvent) {
|
||||
super.update(e)
|
||||
e.presentation.isEnabled = e.project != null && e.getData(GIT_BRANCH) != null
|
||||
}
|
||||
}
|
||||
|
||||
private val GIT_BRANCH = DataKey.create<BranchInfo>("GitBranchKey")
|
||||
|
||||
private fun getAllBranches(project: Project): List<BranchInfo> {
|
||||
val settings = GitSharedSettings.getInstance(project)
|
||||
|
||||
val localMap = mutableMapOf<String, MutableList<GitRepository>>()
|
||||
for (repo in GitRepositoryManager.getInstance(project).repositories) {
|
||||
for (branch in repo.branches.localBranches) {
|
||||
if (repo.currentBranch != branch) {
|
||||
val trackedBranch = branch.findTrackedBranch(repo)
|
||||
if (trackedBranch == null || !settings.isBranchProtected(trackedBranch.nameForRemoteOperations)) {
|
||||
localMap.computeIfAbsent(branch.name) { mutableListOf() }.add(repo)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val local = localMap.map { (branchName, repos) -> BranchInfo(branchName, true, repos) }
|
||||
|
||||
val remoteMap = mutableMapOf<String, MutableList<GitRepository>>()
|
||||
for (repo in GitRepositoryManager.getInstance(project).repositories) {
|
||||
for (remoteBranch in repo.branches.remoteBranches) {
|
||||
if (!settings.isBranchProtected(remoteBranch.nameForRemoteOperations)) {
|
||||
remoteMap.computeIfAbsent(remoteBranch.name) { mutableListOf() }.add(repo)
|
||||
}
|
||||
}
|
||||
}
|
||||
val remote = remoteMap.map { (branchName, repos) -> BranchInfo(branchName, false, repos) }
|
||||
|
||||
return local + remote
|
||||
}
|
||||
|
||||
private fun sort(branches: HashSet<BranchInfo>): List<BranchInfo> {
|
||||
return branches.sortedWith(Comparator { o1, o2 ->
|
||||
if (o1.isLocal && !o2.isLocal) -1
|
||||
else if (!o1.isLocal && o2.isLocal) 1
|
||||
else {
|
||||
o1.branchName.compareTo(o2.branchName)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun checkBranchesSynchronously(log: VcsProjectLog,
|
||||
branchesToCheck: Collection<BranchInfo>,
|
||||
indicator: ProgressIndicator): List<BranchInfo>? {
|
||||
val myCommits = findMyCommits(log)
|
||||
if (myCommits == null) return null
|
||||
|
||||
indicator.isIndeterminate = false
|
||||
val branches = mutableListOf<BranchInfo>()
|
||||
for ((step, branch) in branchesToCheck.withIndex()) {
|
||||
indicator.fraction = step.toDouble() / branchesToCheck.size
|
||||
|
||||
for (repo in branch.repositories) {
|
||||
indicator.checkCanceled()
|
||||
if (isMyBranch(log, branch.branchName, repo, myCommits)) {
|
||||
branches.add(branch)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return branches
|
||||
}
|
||||
|
||||
fun findMyCommits(log: VcsProjectLog): Set<Int>? {
|
||||
val filterByMe = VcsLogFilterObject.fromUserNames(listOf(VcsLogUserFilterImpl.ME), log.dataManager!!)
|
||||
return log.dataManager!!.index.dataGetter!!.filter(listOf(filterByMe))
|
||||
}
|
||||
|
||||
private fun isMyBranch(log: VcsProjectLog,
|
||||
branchName: String,
|
||||
repo: GitRepository,
|
||||
myCommits: Set<Int>): Boolean {
|
||||
// branch is "my" if all its exclusive commits are made by me
|
||||
val exclusiveCommits = findExclusiveCommits(log, branchName, repo) ?: return false
|
||||
if (exclusiveCommits.isEmpty) return false
|
||||
|
||||
for (commit in exclusiveCommits) {
|
||||
if (!myCommits.contains(commit)) {
|
||||
LOG.debug("Commit ${log.dataManager!!.storage.getCommitId(commit)} is not mine")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun findExclusiveCommits(log: VcsProjectLog, branchName: String, repo: GitRepository): TIntHashSet? {
|
||||
val dataPack = log.dataManager!!.dataPack
|
||||
val permanentGraph = dataPack.permanentGraph as PermanentGraphImpl<Int>
|
||||
val storage = log.dataManager!!.storage
|
||||
|
||||
val ref = dataPack.refsModel.findBranch(branchName, repo.root) ?: return null
|
||||
if (!ref.type.isBranch) return null
|
||||
|
||||
val branchRefIndex = storage.getCommitIndex(ref.commitHash, ref.root)
|
||||
val branchNodeId = permanentGraph.permanentCommitsInfo.getNodeId(branchRefIndex)
|
||||
|
||||
val exclusiveCommits = TIntHashSet()
|
||||
DfsWalk(listOf(branchNodeId), permanentGraph.linearGraph).walk(true) { node: Int ->
|
||||
val reachableNodes = ReachableNodes(LinearGraphUtils.asLiteLinearGraph(permanentGraph.linearGraph))
|
||||
val containingBranchesIndexes = reachableNodes.getContainingBranches(node, permanentGraph.branchNodeIds)
|
||||
|
||||
if (!containingBranchesIndexes.contains(branchNodeId)) {
|
||||
LOG.error("The branch $branchName doesn't contain commit reachable from it")
|
||||
}
|
||||
|
||||
if (containingBranchesIndexes.size > 1) {
|
||||
// there are other containing branches
|
||||
false
|
||||
}
|
||||
else {
|
||||
val commitId = permanentGraph.permanentCommitsInfo.getCommitId(node)
|
||||
exclusiveCommits.add(commitId)
|
||||
true
|
||||
}
|
||||
}
|
||||
return exclusiveCommits
|
||||
}
|
||||
Reference in New Issue
Block a user