mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
Provide a dialog to configure Git remotes: add, remote and edit
The feature considers that a remote may have only single URL (the first fetch URL): only one URL is displayed in the table, only one URL can be changed. pushUrls and other fetchUrls can be supported later by request. IDEA-87099 IDEA-76454 IDEA-60389
This commit is contained in:
@@ -63,6 +63,7 @@
|
||||
<action id="Git.Unstash" class="git4idea.actions.GitUnstash" text="UnStash Changes..."/>
|
||||
<action id="Git.Reset" class="git4idea.actions.GitResetHead" text="Reset HEAD..." icon="AllIcons.Actions.Reset"/>
|
||||
<separator/>
|
||||
<action id="Git.Configure.Remotes" class="git4idea.remote.GitConfigureRemotesAction" text="Configure Remotes" />
|
||||
<action id="Git.Clone" class="git4idea.actions.GitCloneAction" text="Clone..." />
|
||||
<action id="Git.Fetch" class="git4idea.actions.GitFetch" text="Fetch"/>
|
||||
<action id="Git.Pull" class="git4idea.actions.GitPull" text="Pull..." icon="AllIcons.Actions.CheckOut"/>
|
||||
|
||||
@@ -164,6 +164,15 @@ public interface Git {
|
||||
@NotNull
|
||||
GitCommandResult addRemote(@NotNull GitRepository repository, @NotNull String name, @NotNull String url);
|
||||
|
||||
@NotNull
|
||||
GitCommandResult removeRemote(@NotNull GitRepository repository, @NotNull GitRemote remote);
|
||||
|
||||
@NotNull
|
||||
GitCommandResult renameRemote(@NotNull GitRepository repository, @NotNull String oldName, @NotNull String newName);
|
||||
|
||||
@NotNull
|
||||
GitCommandResult setRemoteUrl(@NotNull GitRepository repository, @NotNull String remoteName, @NotNull String newUrl);
|
||||
|
||||
@NotNull
|
||||
GitCommandResult lsRemote(@NotNull Project project, @NotNull File workingDir, @NotNull String url);
|
||||
|
||||
|
||||
@@ -524,6 +524,30 @@ public class GitImpl implements Git {
|
||||
return run(h);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public GitCommandResult removeRemote(@NotNull GitRepository repository, @NotNull GitRemote remote) {
|
||||
GitLineHandler h = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.REMOTE);
|
||||
h.addParameters("remove", remote.getName());
|
||||
return run(h);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public GitCommandResult renameRemote(@NotNull GitRepository repository, @NotNull String oldName, @NotNull String newName) {
|
||||
GitLineHandler h = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.REMOTE);
|
||||
h.addParameters("rename", oldName, newName);
|
||||
return run(h);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public GitCommandResult setRemoteUrl(@NotNull GitRepository repository, @NotNull String remoteName, @NotNull String newUrl) {
|
||||
GitLineHandler h = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.REMOTE);
|
||||
h.addParameters("set-url", remoteName, newUrl);
|
||||
return run(h);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public GitCommandResult lsRemote(@NotNull final Project project,
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2000-2016 JetBrains s.r.o.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package git4idea.remote
|
||||
|
||||
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||
import com.intellij.openapi.project.DumbAwareAction
|
||||
import git4idea.GitUtil.getRepositoryManager
|
||||
|
||||
class GitConfigureRemotesAction : DumbAwareAction() {
|
||||
override fun update(e: AnActionEvent) {
|
||||
e.presentation.isEnabledAndVisible = e.project != null && !getRepositoryManager(e.project!!).repositories.isEmpty()
|
||||
}
|
||||
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
val project = e.project!!
|
||||
GitConfigureRemotesDialog(project, getRepositoryManager(project).repositories).show()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
/*
|
||||
* Copyright 2000-2016 JetBrains s.r.o.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
/*
|
||||
* Copyright 2000-2016 JetBrains s.r.o.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package git4idea.remote
|
||||
|
||||
import com.intellij.dvcs.DvcsUtil
|
||||
import com.intellij.dvcs.DvcsUtil.sortRepositories
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import com.intellij.openapi.progress.ProgressIndicator
|
||||
import com.intellij.openapi.progress.ProgressManager
|
||||
import com.intellij.openapi.progress.Task
|
||||
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
|
||||
import com.intellij.ui.SimpleTextAttributes
|
||||
import com.intellij.ui.ToolbarDecorator
|
||||
import com.intellij.ui.table.JBTable
|
||||
import com.intellij.util.ui.JBUI
|
||||
import com.intellij.util.ui.UIUtil
|
||||
import git4idea.commands.Git
|
||||
import git4idea.commands.GitCommandResult
|
||||
import git4idea.repo.GitRemote
|
||||
import git4idea.repo.GitRemote.ORIGIN_NAME
|
||||
import git4idea.repo.GitRepository
|
||||
import java.util.*
|
||||
import javax.swing.*
|
||||
import javax.swing.table.AbstractTableModel
|
||||
|
||||
class GitConfigureRemotesDialog(val project: Project, val repositories: Collection<GitRepository>) :
|
||||
DialogWrapper(project, true, getModalityType()) {
|
||||
|
||||
private val git = service<Git>()
|
||||
private val LOG = Logger.getInstance(GitConfigureRemotesDialog::class.java)
|
||||
|
||||
private val NAME_COLUMN = 0
|
||||
private val REMOTE_PADDING = 30
|
||||
private val table = JBTable(RemotesTableModel())
|
||||
|
||||
private var nodes = buildNodes(repositories)
|
||||
|
||||
init {
|
||||
init()
|
||||
title = "Git Remotes"
|
||||
updateColumnSize()
|
||||
}
|
||||
|
||||
override fun createActions() = arrayOf(okAction)
|
||||
|
||||
override fun getPreferredFocusedComponent() = table
|
||||
|
||||
override fun createCenterPanel(): JComponent? {
|
||||
table.selectionModel = DefaultListSelectionModel()
|
||||
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
|
||||
table.intercellSpacing = JBUI.emptySize()
|
||||
table.setDefaultRenderer(Any::class.java, MyCellRenderer())
|
||||
|
||||
return ToolbarDecorator.createDecorator(table).
|
||||
setAddAction { addRemote() }.
|
||||
setRemoveAction { removeRemote() }.
|
||||
setEditAction { editRemote() }.
|
||||
setEditActionUpdater { isRemoteSelected() }.
|
||||
setRemoveActionUpdater { isRemoteSelected() }.
|
||||
disableUpDownActions().createPanel()
|
||||
}
|
||||
|
||||
private fun addRemote() {
|
||||
val repository = getSelectedRepo()
|
||||
val dialog = GitDefineRemoteDialog(repository, git)
|
||||
if (dialog.showAndGet()) {
|
||||
runInModalTask("Adding Remote...", repository,
|
||||
"Add Remote", "Couldn't add remote ${dialog.remoteName} '${dialog.remoteUrl}'") {
|
||||
git.addRemote(repository, dialog.remoteName, dialog.remoteUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeRemote() {
|
||||
val remoteNode = getSelectedRemote()!!
|
||||
val remote = remoteNode.remote
|
||||
if (YES == showYesNoDialog(rootPane, "Remove remote ${remote.name} '${getUrl(remote)}'?", "Remove Remote", getQuestionIcon())) {
|
||||
runInModalTask("Removing Remote...", remoteNode.repository, "Remove Remote", "Couldn't remove remote $remote") {
|
||||
git.removeRemote(remoteNode.repository, remote)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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("Changing Remote...", repository,
|
||||
"Change Remote", "Couldn't change remote $oldName to $newRemoteName '$newRemoteUrl'") {
|
||||
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
|
||||
}
|
||||
|
||||
private fun updateColumnSize() {
|
||||
var maxWidth = 0
|
||||
for (node in nodes) {
|
||||
val tableFont = UIManager.getFont("Table.font")
|
||||
val width = table.getFontMetrics(tableFont).stringWidth(node.getPresentableString())
|
||||
if (maxWidth < width) maxWidth = width
|
||||
}
|
||||
maxWidth += REMOTE_PADDING + UIUtil.DEFAULT_HGAP
|
||||
|
||||
val column = table.columnModel.getColumn(NAME_COLUMN)
|
||||
column.preferredWidth = maxWidth
|
||||
column.maxWidth = maxWidth
|
||||
}
|
||||
|
||||
private fun buildNodes(repositories: Collection<GitRepository>): List<Node> {
|
||||
val nodes = mutableListOf<Node>()
|
||||
for (repository in sortRepositories(repositories)) {
|
||||
if (repositories.size > 1) nodes.add(RepoNode(repository))
|
||||
for (remote in sortedRemotes(repository)) {
|
||||
nodes.add(RemoteNode(remote, repository))
|
||||
}
|
||||
}
|
||||
return nodes
|
||||
}
|
||||
|
||||
private fun sortedRemotes(repository: GitRepository): List<GitRemote> {
|
||||
return repository.remotes.sortedWith(Comparator<GitRemote> { r1, r2 ->
|
||||
if (r1.name == ORIGIN_NAME) {
|
||||
if (r2.name == ORIGIN_NAME) 0 else -1
|
||||
}
|
||||
else if (r2.name == ORIGIN_NAME) 1 else r1.name.compareTo(r2.name)
|
||||
})
|
||||
}
|
||||
|
||||
private fun rebuildTable() {
|
||||
nodes = buildNodes(repositories)
|
||||
(table.model as RemotesTableModel).fireTableDataChanged()
|
||||
}
|
||||
|
||||
private fun runInModalTask(title: String,
|
||||
repository: GitRepository,
|
||||
errorTitle: String,
|
||||
errorMessage: String,
|
||||
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) "operation was not executed" else result!!.errorOutputAsJoinedString
|
||||
val message = "$errorMessage in $repository:\n$errorDetails"
|
||||
LOG.warn(message)
|
||||
Messages.showErrorDialog(myProject, message, errorTitle)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun getSelectedRepo(): GitRepository {
|
||||
val selectedRow = table.selectedRow
|
||||
if (selectedRow < 0) return sortRepositories(repositories).first()
|
||||
val value = nodes[selectedRow]
|
||||
if (value is RepoNode) return value.repository
|
||||
if (value is RemoteNode) return value.repository
|
||||
throw IllegalStateException("Unexpected selected value: $value")
|
||||
}
|
||||
|
||||
private fun getSelectedRemote() : RemoteNode? {
|
||||
val selectedRow = table.selectedRow
|
||||
if (selectedRow < 0) return null
|
||||
return nodes[selectedRow] as? RemoteNode
|
||||
}
|
||||
|
||||
private fun isRemoteSelected() = getSelectedRemote() != null
|
||||
|
||||
private fun getUrl(remote: GitRemote) = remote.urls.firstOrNull() ?: ""
|
||||
|
||||
private abstract class Node {
|
||||
abstract fun getPresentableString() : String
|
||||
}
|
||||
private class RepoNode(val repository: GitRepository) : Node() {
|
||||
override fun toString() = repository.presentableUrl
|
||||
override fun getPresentableString() = DvcsUtil.getShortRepositoryName(repository)
|
||||
}
|
||||
private class RemoteNode(val remote: GitRemote, val repository: GitRepository) : Node() {
|
||||
override fun toString() = remote.name
|
||||
override fun getPresentableString() = remote.name
|
||||
}
|
||||
|
||||
private inner class RemotesTableModel() : AbstractTableModel() {
|
||||
override fun getRowCount() = nodes.size
|
||||
override fun getColumnCount() = 2
|
||||
|
||||
override fun getColumnName(column: Int): String {
|
||||
if (column == NAME_COLUMN) return "Name"
|
||||
else return "URL"
|
||||
}
|
||||
|
||||
override fun getValueAt(rowIndex: Int, columnIndex: Int): Any {
|
||||
val node = nodes[rowIndex]
|
||||
when {
|
||||
columnIndex == NAME_COLUMN -> return node
|
||||
node is RepoNode -> return ""
|
||||
node is RemoteNode -> return getUrl(node.remote)
|
||||
else -> {
|
||||
LOG.error("Unexpected position at row $rowIndex and column $columnIndex")
|
||||
return ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private inner class MyCellRenderer : ColoredTableCellRenderer() {
|
||||
override fun customizeCellRenderer(table: JTable?, value: Any?, selected: Boolean, hasFocus: Boolean, row: Int, column: Int) {
|
||||
if (value is RepoNode) {
|
||||
append(value.getPresentableString(), SimpleTextAttributes.REGULAR_BOLD_ATTRIBUTES)
|
||||
}
|
||||
else if (value is RemoteNode) {
|
||||
if (repositories.size > 1) append("", SimpleTextAttributes.REGULAR_ATTRIBUTES, REMOTE_PADDING, SwingConstants.LEFT)
|
||||
append(value.getPresentableString())
|
||||
}
|
||||
else if (value is String) {
|
||||
append(value)
|
||||
}
|
||||
border = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getModalityType() = if (Registry.`is`("ide.perProjectModality")) PROJECT else IDE
|
||||
@@ -50,11 +50,18 @@ public class GitDefineRemoteDialog extends DialogWrapper {
|
||||
@NotNull private final JTextField myRemoteUrl;
|
||||
|
||||
public GitDefineRemoteDialog(@NotNull GitRepository repository, @NotNull Git git) {
|
||||
this(repository, git, GitRemote.ORIGIN_NAME, "");
|
||||
}
|
||||
|
||||
public GitDefineRemoteDialog(@NotNull GitRepository repository,
|
||||
@NotNull Git git,
|
||||
@NotNull String defaultRemoteName,
|
||||
@NotNull String initialRemoteUrl) {
|
||||
super(repository.getProject());
|
||||
myRepository = repository;
|
||||
myGit = git;
|
||||
myRemoteName = new JTextField(GitRemote.ORIGIN_NAME, 20);
|
||||
myRemoteUrl = new JTextField(20);
|
||||
myRemoteName = new JTextField(defaultRemoteName, 20);
|
||||
myRemoteUrl = new JTextField(initialRemoteUrl, 20);
|
||||
setTitle("Define Remote" + mention(myRepository));
|
||||
init();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user