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(); }