diff --git a/plugins/git4idea/src/META-INF/plugin.xml b/plugins/git4idea/src/META-INF/plugin.xml
index 97215c651c1f..11a0df157fe8 100644
--- a/plugins/git4idea/src/META-INF/plugin.xml
+++ b/plugins/git4idea/src/META-INF/plugin.xml
@@ -63,6 +63,7 @@
+
diff --git a/plugins/git4idea/src/git4idea/commands/Git.java b/plugins/git4idea/src/git4idea/commands/Git.java
index 2be6b2a03cec..733fc83821af 100644
--- a/plugins/git4idea/src/git4idea/commands/Git.java
+++ b/plugins/git4idea/src/git4idea/commands/Git.java
@@ -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);
diff --git a/plugins/git4idea/src/git4idea/commands/GitImpl.java b/plugins/git4idea/src/git4idea/commands/GitImpl.java
index 1cd33d996c27..13c2d84c13c0 100644
--- a/plugins/git4idea/src/git4idea/commands/GitImpl.java
+++ b/plugins/git4idea/src/git4idea/commands/GitImpl.java
@@ -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,
diff --git a/plugins/git4idea/src/git4idea/remote/GitConfigureRemotesAction.kt b/plugins/git4idea/src/git4idea/remote/GitConfigureRemotesAction.kt
new file mode 100644
index 000000000000..5b3ba0e5170d
--- /dev/null
+++ b/plugins/git4idea/src/git4idea/remote/GitConfigureRemotesAction.kt
@@ -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()
+ }
+}
diff --git a/plugins/git4idea/src/git4idea/remote/GitConfigureRemotesDialog.kt b/plugins/git4idea/src/git4idea/remote/GitConfigureRemotesDialog.kt
new file mode 100644
index 000000000000..3d46997f8e48
--- /dev/null
+++ b/plugins/git4idea/src/git4idea/remote/GitConfigureRemotesDialog.kt
@@ -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) :
+ DialogWrapper(project, true, getModalityType()) {
+
+ private val git = service()
+ 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): List {
+ val nodes = mutableListOf()
+ 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 {
+ return repository.remotes.sortedWith(Comparator { 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
diff --git a/plugins/git4idea/src/git4idea/remote/GitDefineRemoteDialog.java b/plugins/git4idea/src/git4idea/remote/GitDefineRemoteDialog.java
index 9f37b63242f4..e316b0e925cd 100644
--- a/plugins/git4idea/src/git4idea/remote/GitDefineRemoteDialog.java
+++ b/plugins/git4idea/src/git4idea/remote/GitDefineRemoteDialog.java
@@ -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();
}