4923253ee72288622e0e7e0a97d1e3f79335ae44
[idea/community.git] / plugins / git4idea / src / git4idea / remote / GitConfigureRemotesDialog.kt
1 // Copyright 2000-2020 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
3 package git4idea.remote
4
5 import com.intellij.dvcs.DvcsUtil
6 import com.intellij.dvcs.DvcsUtil.sortRepositories
7 import com.intellij.openapi.components.service
8 import com.intellij.openapi.diagnostic.logger
9 import com.intellij.openapi.progress.ProgressIndicator
10 import com.intellij.openapi.progress.ProgressManager
11 import com.intellij.openapi.progress.Task
12 import com.intellij.openapi.project.Project
13 import com.intellij.openapi.ui.DialogWrapper
14 import com.intellij.openapi.ui.DialogWrapper.IdeModalityType.IDE
15 import com.intellij.openapi.ui.DialogWrapper.IdeModalityType.PROJECT
16 import com.intellij.openapi.ui.Messages
17 import com.intellij.openapi.ui.Messages.*
18 import com.intellij.openapi.util.registry.Registry
19 import com.intellij.ui.ColoredTableCellRenderer
20 import com.intellij.ui.SimpleTextAttributes
21 import com.intellij.ui.ToolbarDecorator
22 import com.intellij.ui.table.JBTable
23 import com.intellij.util.ui.JBUI
24 import com.intellij.util.ui.UIUtil.DEFAULT_HGAP
25 import git4idea.commands.Git
26 import git4idea.commands.GitCommandResult
27 import git4idea.i18n.GitBundle.message
28 import git4idea.repo.GitRemote
29 import git4idea.repo.GitRemote.ORIGIN
30 import git4idea.repo.GitRepository
31 import org.jetbrains.annotations.Nls
32 import java.awt.Font
33 import java.util.*
34 import javax.swing.*
35 import javax.swing.table.AbstractTableModel
36 import kotlin.math.min
37
38 class GitConfigureRemotesDialog(val project: Project, val repositories: Collection<GitRepository>) :
39     DialogWrapper(project, true, getModalityType()) {
40
41   private val git = service<Git>()
42   private val LOG = logger<GitConfigureRemotesDialog>()
43
44   private val NAME_COLUMN = 0
45   private val URL_COLUMN = 1
46   private val REMOTE_PADDING = 30
47   private val table = JBTable(RemotesTableModel())
48
49   private var nodes = buildNodes(repositories)
50
51   init {
52     init()
53     title = message("remotes.dialog.title")
54     updateTableWidth()
55   }
56
57   override fun getDimensionServiceKey(): String = javaClass.name
58
59   override fun createActions(): Array<Action> = arrayOf(okAction)
60
61   override fun getPreferredFocusedComponent(): JBTable = table
62
63   override fun createCenterPanel(): JComponent? {
64     table.selectionModel = DefaultListSelectionModel()
65     table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
66     table.intercellSpacing = JBUI.emptySize()
67     table.setDefaultRenderer(Any::class.java, MyCellRenderer())
68
69     return ToolbarDecorator.createDecorator(table).
70         setAddAction { addRemote() }.
71         setRemoveAction { removeRemote() }.
72         setEditAction { editRemote() }.
73         setEditActionUpdater { isRemoteSelected() }.
74         setRemoveActionUpdater { isRemoteSelected() }.
75         disableUpDownActions().createPanel()
76   }
77
78   private fun addRemote() {
79     val repository = getSelectedRepo()
80     val proposedName = if (repository.remotes.any { it.name == ORIGIN }) "" else ORIGIN
81     val dialog = GitDefineRemoteDialog(repository, git, proposedName, "")
82     if (dialog.showAndGet()) {
83       runInModalTask(message("remotes.dialog.adding.remote"),
84                      message("remote.dialog.add.remote"),
85                      message("remotes.dialog.cannot.add.remote.error.message", dialog.remoteName, dialog.remoteUrl),
86                      repository) {
87         git.addRemote(repository, dialog.remoteName, dialog.remoteUrl)
88       }
89     }
90   }
91
92   private fun removeRemote() {
93     val remoteNode = getSelectedRemote()!!
94     val remote = remoteNode.remote
95     if (YES == showYesNoDialog(rootPane,
96                                message("remotes.dialog.remove.remote.message", remote.name, getUrl(remote)),
97                                message("remotes.dialog.remove.remote.title"), getQuestionIcon())) {
98       runInModalTask(message("remotes.dialog.removing.remote.progress"),
99                      message("remotes.dialog.removing.remote.error.title"),
100                      message("remotes.dialog.removing.remote.error.message", remote),
101                      remoteNode.repository) {
102         git.removeRemote(remoteNode.repository, remote)
103       }
104     }
105   }
106
107   private fun editRemote() {
108     val remoteNode = getSelectedRemote()!!
109     val remote = remoteNode.remote
110     val repository = remoteNode.repository
111     val oldName = remote.name
112     val oldUrl = getUrl(remote)
113
114     val dialog = GitDefineRemoteDialog(repository, git, oldName, oldUrl)
115     if (dialog.showAndGet()) {
116       val newRemoteName = dialog.remoteName
117       val newRemoteUrl = dialog.remoteUrl
118       if (newRemoteName == oldName && newRemoteUrl == oldUrl) return
119       runInModalTask(message("remotes.changing.remote.progress"),
120                      message("remotes.changing.remote.error.title"),
121                      message("remotes.changing.remote.error.message", oldName, newRemoteName, newRemoteUrl),
122                      repository) {
123         changeRemote(repository, oldName, oldUrl, newRemoteName, newRemoteUrl)
124       }
125     }
126   }
127
128   private fun changeRemote(repo: GitRepository, oldName: String, oldUrl: String, newName: String, newUrl: String): GitCommandResult {
129     var result : GitCommandResult? = null
130     if (newName != oldName) {
131       result = git.renameRemote(repo, oldName, newName)
132       if (!result.success()) return result
133     }
134     if (newUrl != oldUrl) {
135       result = git.setRemoteUrl(repo, newName, newUrl) // NB: remote name has just been changed
136     }
137     return result!! // at least one of two has changed
138   }
139
140   private fun updateTableWidth() {
141     var maxNameWidth = 30
142     var maxUrlWidth = 250
143     for (node in nodes) {
144       val fontMetrics = table.getFontMetrics(UIManager.getFont("Table.font").deriveFont(Font.BOLD))
145       val nameWidth = fontMetrics.stringWidth(node.getPresentableString())
146       val remote = (node as? RemoteNode)?.remote
147       val urlWidth = if (remote == null) 0 else fontMetrics.stringWidth(getUrl(remote))
148       if (maxNameWidth < nameWidth) maxNameWidth = nameWidth
149       if (maxUrlWidth < urlWidth) maxUrlWidth = urlWidth
150     }
151     maxNameWidth += REMOTE_PADDING + DEFAULT_HGAP
152
153     table.columnModel.getColumn(NAME_COLUMN).preferredWidth = maxNameWidth
154     table.columnModel.getColumn(URL_COLUMN).preferredWidth = maxUrlWidth
155
156     table.preferredScrollableViewportSize = JBUI.size(maxNameWidth + maxUrlWidth + DEFAULT_HGAP, -1)
157     table.visibleRowCount = min(nodes.size + 3, 8)
158   }
159
160   private fun buildNodes(repositories: Collection<GitRepository>): List<Node> {
161     val nodes = mutableListOf<Node>()
162     for (repository in sortRepositories(repositories)) {
163       if (repositories.size > 1) nodes.add(RepoNode(repository))
164       for (remote in sortedRemotes(repository)) {
165         nodes.add(RemoteNode(remote, repository))
166       }
167     }
168     return nodes
169   }
170
171   private fun sortedRemotes(repository: GitRepository): List<GitRemote> {
172     return repository.remotes.sortedWith(Comparator<GitRemote> { r1, r2 ->
173       if (r1.name == ORIGIN) {
174         if (r2.name == ORIGIN) 0 else -1
175       }
176       else if (r2.name == ORIGIN) 1 else r1.name.compareTo(r2.name)
177     })
178   }
179
180   private fun rebuildTable() {
181     nodes = buildNodes(repositories)
182     (table.model as RemotesTableModel).fireTableDataChanged()
183   }
184
185   private fun runInModalTask(@Nls(capitalization = Nls.Capitalization.Title) title: String,
186                              @Nls(capitalization = Nls.Capitalization.Title) errorTitle: String,
187                              @Nls(capitalization = Nls.Capitalization.Sentence) errorMessage: String,
188                              repository: GitRepository,
189                              operation: () -> GitCommandResult) {
190     ProgressManager.getInstance().run(object : Task.Modal(project, title, true) {
191       private var result: GitCommandResult? = null
192
193       override fun run(indicator: ProgressIndicator) {
194         result = operation()
195         repository.update()
196       }
197
198       override fun onSuccess() {
199         rebuildTable()
200         if (result == null || !result!!.success()) {
201           val errorDetails = if (result == null) message("remotes.operation.not.executed.message") else result!!.errorOutputAsJoinedString
202           val message = message("remotes.operation.error.message", errorMessage, repository, errorDetails)
203           LOG.warn(message)
204           Messages.showErrorDialog(myProject, message, errorTitle)
205         }
206       }
207     })
208   }
209
210   private fun getSelectedRepo(): GitRepository {
211     val selectedRow = table.selectedRow
212     if (selectedRow < 0) return sortRepositories(repositories).first()
213     val value = nodes[selectedRow]
214     if (value is RepoNode) return value.repository
215     if (value is RemoteNode) return value.repository
216     throw IllegalStateException("Unexpected selected value: $value")
217   }
218
219   private fun getSelectedRemote() : RemoteNode? {
220     val selectedRow = table.selectedRow
221     if (selectedRow < 0) return null
222     return nodes[selectedRow] as? RemoteNode
223   }
224
225   private fun isRemoteSelected() = getSelectedRemote() != null
226
227   private fun getUrl(remote: GitRemote) = remote.urls.firstOrNull() ?: ""
228
229   private abstract class Node {
230     abstract fun getPresentableString() : String
231   }
232   private class RepoNode(val repository: GitRepository) : Node() {
233     override fun toString() = repository.presentableUrl
234     override fun getPresentableString() = DvcsUtil.getShortRepositoryName(repository)
235   }
236   private class RemoteNode(val remote: GitRemote, val repository: GitRepository) : Node() {
237     override fun toString() = remote.name
238     override fun getPresentableString() = remote.name
239   }
240
241   private inner class RemotesTableModel : AbstractTableModel() {
242     override fun getRowCount() = nodes.size
243     override fun getColumnCount() = 2
244
245     override fun getColumnName(column: Int): String {
246       if (column == NAME_COLUMN) return message("remotes.remote.column.name")
247       else return message("remotes.remote.column.url")
248     }
249
250     override fun getValueAt(rowIndex: Int, columnIndex: Int): Any {
251       val node = nodes[rowIndex]
252       when {
253         columnIndex == NAME_COLUMN -> return node
254         node is RepoNode -> return ""
255         node is RemoteNode -> return getUrl(node.remote)
256         else -> {
257           LOG.error("Unexpected position at row $rowIndex and column $columnIndex")
258           return ""
259         }
260       }
261     }
262   }
263
264   private inner class MyCellRenderer : ColoredTableCellRenderer() {
265     override fun customizeCellRenderer(table: JTable, value: Any?, selected: Boolean, hasFocus: Boolean, row: Int, column: Int) {
266       if (value is RepoNode) {
267         append(value.getPresentableString(), SimpleTextAttributes.REGULAR_BOLD_ATTRIBUTES)
268       }
269       else if (value is RemoteNode) {
270         if (repositories.size > 1) append("", SimpleTextAttributes.REGULAR_ATTRIBUTES, REMOTE_PADDING, SwingConstants.LEFT)
271         append(value.getPresentableString())
272       }
273       else if (value is String) {
274         append(value)
275       }
276       border = null
277     }
278   }
279 }
280
281 private fun getModalityType() = if (Registry.`is`("ide.perProjectModality")) PROJECT else IDE