summaryrefslogtreecommitdiff
path: root/platform/dvcs-impl/src/com
diff options
context:
space:
mode:
Diffstat (limited to 'platform/dvcs-impl/src/com')
-rw-r--r--platform/dvcs-impl/src/com/intellij/dvcs/DvcsCommitAdditionalComponent.java182
-rw-r--r--platform/dvcs-impl/src/com/intellij/dvcs/DvcsPlatformFacade.java102
-rw-r--r--platform/dvcs-impl/src/com/intellij/dvcs/DvcsPlatformFacadeImpl.java144
-rw-r--r--platform/dvcs-impl/src/com/intellij/dvcs/DvcsRememberedInputs.java96
-rw-r--r--platform/dvcs-impl/src/com/intellij/dvcs/DvcsUtil.java205
-rw-r--r--platform/dvcs-impl/src/com/intellij/dvcs/push/PushController.java445
-rw-r--r--platform/dvcs-impl/src/com/intellij/dvcs/push/RepositoryNodeListener.java23
-rw-r--r--platform/dvcs-impl/src/com/intellij/dvcs/push/ui/CustomRenderedTreeNode.java24
-rw-r--r--platform/dvcs-impl/src/com/intellij/dvcs/push/ui/DvcsStrategyPanel.java43
-rw-r--r--platform/dvcs-impl/src/com/intellij/dvcs/push/ui/EditableTreeNode.java33
-rw-r--r--platform/dvcs-impl/src/com/intellij/dvcs/push/ui/LoadingTreeNode.java51
-rw-r--r--platform/dvcs-impl/src/com/intellij/dvcs/push/ui/NodeImageObserver.java46
-rw-r--r--platform/dvcs-impl/src/com/intellij/dvcs/push/ui/PushLog.java289
-rw-r--r--platform/dvcs-impl/src/com/intellij/dvcs/push/ui/PushLogTreeUtil.java52
-rw-r--r--platform/dvcs-impl/src/com/intellij/dvcs/push/ui/RepositoryNode.java97
-rw-r--r--platform/dvcs-impl/src/com/intellij/dvcs/push/ui/RepositoryWithBranchPanel.java185
-rw-r--r--platform/dvcs-impl/src/com/intellij/dvcs/push/ui/SingleRepositoryNode.java52
-rw-r--r--platform/dvcs-impl/src/com/intellij/dvcs/push/ui/TextWithLinkNode.java52
-rw-r--r--platform/dvcs-impl/src/com/intellij/dvcs/push/ui/TooltipNode.java21
-rw-r--r--platform/dvcs-impl/src/com/intellij/dvcs/push/ui/VcsBranchEditorListener.java61
-rw-r--r--platform/dvcs-impl/src/com/intellij/dvcs/push/ui/VcsFullCommitDetailsNode.java54
-rw-r--r--platform/dvcs-impl/src/com/intellij/dvcs/push/ui/VcsLinkListener.java24
-rw-r--r--platform/dvcs-impl/src/com/intellij/dvcs/push/ui/VcsLinkedText.java70
-rw-r--r--platform/dvcs-impl/src/com/intellij/dvcs/push/ui/VcsPushDialog.java144
-rw-r--r--platform/dvcs-impl/src/com/intellij/dvcs/repo/AbstractRepositoryManager.java223
-rw-r--r--platform/dvcs-impl/src/com/intellij/dvcs/repo/RepoStateException.java30
-rw-r--r--platform/dvcs-impl/src/com/intellij/dvcs/repo/RepositoryImpl.java106
-rw-r--r--platform/dvcs-impl/src/com/intellij/dvcs/repo/RepositoryUtil.java145
-rw-r--r--platform/dvcs-impl/src/com/intellij/dvcs/ui/BranchActionGroupPopup.java114
-rw-r--r--platform/dvcs-impl/src/com/intellij/dvcs/ui/CloneDvcsDialog.form84
-rw-r--r--platform/dvcs-impl/src/com/intellij/dvcs/ui/CloneDvcsDialog.java332
-rw-r--r--platform/dvcs-impl/src/com/intellij/dvcs/ui/DvcsBundle.java55
-rw-r--r--platform/dvcs-impl/src/com/intellij/dvcs/ui/DvcsBundle.properties19
-rw-r--r--platform/dvcs-impl/src/com/intellij/dvcs/ui/NewBranchAction.java49
-rw-r--r--platform/dvcs-impl/src/com/intellij/dvcs/ui/RootAction.java75
-rw-r--r--platform/dvcs-impl/src/com/intellij/dvcs/ui/VcsLogAction.java85
-rw-r--r--platform/dvcs-impl/src/com/intellij/dvcs/ui/VcsLogOneCommitPerRepoAction.java69
-rw-r--r--platform/dvcs-impl/src/com/intellij/dvcs/ui/VcsLogSingleCommitAction.java46
38 files changed, 3927 insertions, 0 deletions
diff --git a/platform/dvcs-impl/src/com/intellij/dvcs/DvcsCommitAdditionalComponent.java b/platform/dvcs-impl/src/com/intellij/dvcs/DvcsCommitAdditionalComponent.java
new file mode 100644
index 000000000000..26c87c7de627
--- /dev/null
+++ b/platform/dvcs-impl/src/com/intellij/dvcs/DvcsCommitAdditionalComponent.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright 2000-2014 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 com.intellij.dvcs;
+
+import com.intellij.dvcs.ui.DvcsBundle;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.progress.ProgressManager;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.Messages;
+import com.intellij.openapi.util.Ref;
+import com.intellij.openapi.util.ThrowableComputable;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.openapi.vcs.CheckinProjectPanel;
+import com.intellij.openapi.vcs.FilePath;
+import com.intellij.openapi.vcs.FilePathImpl;
+import com.intellij.openapi.vcs.VcsException;
+import com.intellij.openapi.vcs.ui.RefreshableOnComponent;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.ui.NonFocusableCheckBox;
+import com.intellij.util.Function;
+import com.intellij.util.containers.ContainerUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.io.File;
+import java.util.*;
+import java.util.List;
+
+/**
+ * @author Nadya Zabrodina
+ */
+public abstract class DvcsCommitAdditionalComponent implements RefreshableOnComponent {
+
+ private static final Logger log = Logger.getInstance(DvcsCommitAdditionalComponent.class);
+
+ protected final JPanel myPanel;
+ protected final JCheckBox myAmend;
+ @Nullable private String myPreviousMessage;
+ @Nullable private String myAmendedMessage;
+ @NotNull protected final CheckinProjectPanel myCheckinPanel;
+ @Nullable private Map<VirtualFile, String> myMessagesForRoots;
+
+ public DvcsCommitAdditionalComponent(@NotNull final Project project, @NotNull CheckinProjectPanel panel) {
+ myCheckinPanel = panel;
+ myPanel = new JPanel(new GridBagLayout());
+ final Insets insets = new Insets(2, 2, 2, 2);
+ // add amend checkbox
+ GridBagConstraints c = new GridBagConstraints();
+ //todo change to MigLayout
+ c.gridx = 0;
+ c.gridy = 1;
+ c.gridwidth = 2;
+ c.anchor = GridBagConstraints.CENTER;
+ c.insets = insets;
+ c.weightx = 1;
+ c.fill = GridBagConstraints.HORIZONTAL;
+
+ myAmend = new NonFocusableCheckBox(DvcsBundle.message("commit.amend"));
+ myAmend.setMnemonic('m');
+ myAmend.setToolTipText(DvcsBundle.message("commit.amend.tooltip"));
+ myPreviousMessage = myCheckinPanel.getCommitMessage();
+
+ myAmend.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ if (myAmend.isSelected()) {
+ if (myPreviousMessage.equals(myCheckinPanel.getCommitMessage())) { // if user has already typed something, don't revert it
+ if (myMessagesForRoots == null) {
+ loadMessagesInModalTask(project); //load all commit messages for all repositories
+ }
+ String message = constructAmendedMessage();
+ if (!StringUtil.isEmptyOrSpaces(message)) {
+ myAmendedMessage = message;
+ substituteCommitMessage(myAmendedMessage);
+ }
+ }
+ }
+ else {
+ // there was the amended message, but user has changed it => not reverting
+ if (myCheckinPanel.getCommitMessage().equals(myAmendedMessage)) {
+ myCheckinPanel.setCommitMessage(myPreviousMessage);
+ }
+ }
+ }
+ });
+ myPanel.add(myAmend, c);
+ }
+
+ private String constructAmendedMessage() {
+ Set<VirtualFile> selectedRoots = getVcsRoots(getSelectedFilePaths()); // get only selected files
+ LinkedHashSet<String> messages = ContainerUtil.newLinkedHashSet();
+ if (myMessagesForRoots != null) {
+ for (VirtualFile root : selectedRoots) {
+ String message = myMessagesForRoots.get(root);
+ if (message != null) {
+ messages.add(message);
+ }
+ }
+ }
+ return DvcsUtil.joinMessagesOrNull(messages);
+ }
+
+ public JComponent getComponent() {
+ return myPanel;
+ }
+
+ public void refresh() {
+ myAmend.setSelected(false);
+ }
+
+ private void loadMessagesInModalTask(@NotNull Project project) {
+ try {
+ myMessagesForRoots =
+ ProgressManager.getInstance().runProcessWithProgressSynchronously(new ThrowableComputable<Map<VirtualFile,String>, VcsException>() {
+ @Override
+ public Map<VirtualFile, String> compute() throws VcsException {
+ return getLastCommitMessages();
+ }
+ }, "Reading commit message...", false, project);
+ }
+ catch (VcsException e) {
+ Messages.showErrorDialog(getComponent(), "Couldn't load commit message of the commit to amend.\n" + e.getMessage(),
+ "Commit Message not Loaded");
+ log.info(e);
+ }
+ }
+
+ private void substituteCommitMessage(@NotNull String newMessage) {
+ myPreviousMessage = myCheckinPanel.getCommitMessage();
+ if (!myPreviousMessage.trim().equals(newMessage.trim())) {
+ myCheckinPanel.setCommitMessage(newMessage);
+ }
+ }
+
+ @Nullable
+ private Map<VirtualFile, String> getLastCommitMessages() throws VcsException {
+ Map<VirtualFile, String> messagesForRoots = new HashMap<VirtualFile, String>();
+ Collection<VirtualFile> roots = myCheckinPanel.getRoots(); //all committed vcs roots, not only selected
+ final Ref<VcsException> exception = Ref.create();
+ for (VirtualFile root : roots) {
+ String message = getLastCommitMessage(root);
+ messagesForRoots.put(root, message);
+ }
+ if (!exception.isNull()) {
+ throw exception.get();
+ }
+ return messagesForRoots;
+ }
+
+ @NotNull
+ private List<FilePath> getSelectedFilePaths() {
+ return ContainerUtil.map(myCheckinPanel.getFiles(), new Function<File, FilePath>() {
+ @Override
+ public FilePath fun(File file) {
+ return new FilePathImpl(file, file.isDirectory());
+ }
+ });
+ }
+
+ @NotNull
+ protected abstract Set<VirtualFile> getVcsRoots(@NotNull Collection<FilePath> files);
+
+ @Nullable
+ protected abstract String getLastCommitMessage(@NotNull VirtualFile repo) throws VcsException;
+}
diff --git a/platform/dvcs-impl/src/com/intellij/dvcs/DvcsPlatformFacade.java b/platform/dvcs-impl/src/com/intellij/dvcs/DvcsPlatformFacade.java
new file mode 100644
index 000000000000..91adfa23dd92
--- /dev/null
+++ b/platform/dvcs-impl/src/com/intellij/dvcs/DvcsPlatformFacade.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2000-2012 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 com.intellij.dvcs;
+
+import com.intellij.ide.SaveAndSyncHandler;
+import com.intellij.ide.plugins.IdeaPluginDescriptor;
+import com.intellij.openapi.application.ModalityState;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.project.ex.ProjectManagerEx;
+import com.intellij.openapi.roots.ProjectRootManager;
+import com.intellij.openapi.ui.DialogWrapper;
+import com.intellij.openapi.util.Computable;
+import com.intellij.openapi.vcs.AbstractVcs;
+import com.intellij.openapi.vcs.AbstractVcsHelper;
+import com.intellij.openapi.vcs.ProjectLevelVcsManager;
+import com.intellij.openapi.vcs.changes.ChangeListManagerEx;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.openapi.vfs.VirtualFile;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * IntelliJ code provides a lot of statical bindings to the interested pieces of data. For example we need to execute code
+ * like below to get list of modules for the target project:
+ * <pre>
+ * ModuleManager.getInstance(project).getModules()
+ * </pre>
+ * That means that it's not possible to test target classes in isolation if corresponding infrastructure is not set up.
+ * However, we don't want to set it up if we execute a simple standalone test.
+ * <p/>
+ * This interface is intended to encapsulate access to the underlying IntelliJ functionality.
+ * <p/>
+ * Implementations of this interface are expected to be thread-safe.
+ *
+ * @author Kirill Likhodedov
+ */
+public interface DvcsPlatformFacade {
+
+ @NotNull
+ AbstractVcs getVcs(@NotNull Project project);
+
+ @NotNull
+ ProjectLevelVcsManager getVcsManager(@NotNull Project project);
+
+ void showDialog(@NotNull DialogWrapper dialog);
+
+ @NotNull
+ ProjectRootManager getProjectRootManager(@NotNull Project project);
+
+ /**
+ * Invokes {@link com.intellij.openapi.application.Application#runReadAction(Computable)}.
+ */
+ <T> T runReadAction(@NotNull Computable<T> computable);
+
+ void runReadAction(@NotNull Runnable runnable);
+
+ void runWriteAction(@NotNull Runnable runnable);
+
+ void invokeAndWait(@NotNull Runnable runnable, @NotNull ModalityState modalityState);
+
+ void executeOnPooledThread(@NotNull Runnable runnable);
+
+ ChangeListManagerEx getChangeListManager(@NotNull Project project);
+
+ LocalFileSystem getLocalFileSystem();
+
+ @NotNull
+ AbstractVcsHelper getVcsHelper(@NotNull Project project);
+
+ @Nullable
+ IdeaPluginDescriptor getPluginByClassName(@NotNull String name);
+
+ /**
+ * Gets line separator of the given virtual file.
+ * If {@code detect} is set {@code true}, and the information about line separator wasn't retrieved yet, loads the file and detects.
+ */
+ @Nullable
+ String getLineSeparator(@NotNull VirtualFile file, boolean detect);
+
+ void saveAllDocuments();
+
+ @NotNull
+ ProjectManagerEx getProjectManager();
+
+ @NotNull
+ SaveAndSyncHandler getSaveAndSyncHandler();
+
+ void hardRefresh(@NotNull VirtualFile root);
+}
diff --git a/platform/dvcs-impl/src/com/intellij/dvcs/DvcsPlatformFacadeImpl.java b/platform/dvcs-impl/src/com/intellij/dvcs/DvcsPlatformFacadeImpl.java
new file mode 100644
index 000000000000..9f10311f68d3
--- /dev/null
+++ b/platform/dvcs-impl/src/com/intellij/dvcs/DvcsPlatformFacadeImpl.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2000-2012 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 com.intellij.dvcs;
+
+import com.intellij.ide.SaveAndSyncHandler;
+import com.intellij.ide.SaveAndSyncHandlerImpl;
+import com.intellij.ide.plugins.IdeaPluginDescriptor;
+import com.intellij.ide.plugins.PluginManager;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.application.ModalityState;
+import com.intellij.openapi.fileEditor.FileDocumentManager;
+import com.intellij.openapi.fileEditor.impl.LoadTextUtil;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.project.ex.ProjectManagerEx;
+import com.intellij.openapi.roots.ProjectRootManager;
+import com.intellij.openapi.ui.DialogWrapper;
+import com.intellij.openapi.util.Computable;
+import com.intellij.openapi.vcs.AbstractVcsHelper;
+import com.intellij.openapi.vcs.ProjectLevelVcsManager;
+import com.intellij.openapi.vcs.changes.ChangeListManager;
+import com.intellij.openapi.vcs.changes.ChangeListManagerEx;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.openapi.vfs.VfsUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.util.ui.UIUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * @author Kirill Likhodedov
+ */
+public abstract class DvcsPlatformFacadeImpl implements DvcsPlatformFacade {
+
+ @NotNull
+ @Override
+ public ProjectLevelVcsManager getVcsManager(@NotNull Project project) {
+ return ProjectLevelVcsManager.getInstance(project);
+ }
+
+ @Override
+ public void showDialog(@NotNull DialogWrapper dialog) {
+ dialog.show();
+ }
+
+ @NotNull
+ @Override
+ public ProjectRootManager getProjectRootManager(@NotNull Project project) {
+ return ProjectRootManager.getInstance(project);
+ }
+
+ @Override
+ public <T> T runReadAction(@NotNull Computable<T> computable) {
+ return ApplicationManager.getApplication().runReadAction(computable);
+ }
+
+ @Override
+ public void runReadAction(@NotNull Runnable runnable) {
+ ApplicationManager.getApplication().runReadAction(runnable);
+ }
+
+ @Override
+ public void runWriteAction(@NotNull Runnable runnable) {
+ ApplicationManager.getApplication().runWriteAction(runnable);
+ }
+
+ @Override
+ public void invokeAndWait(@NotNull Runnable runnable, @NotNull ModalityState modalityState) {
+ ApplicationManager.getApplication().invokeAndWait(runnable, modalityState);
+ }
+
+ @Override
+ public void executeOnPooledThread(@NotNull Runnable runnable) {
+ ApplicationManager.getApplication().executeOnPooledThread(runnable);
+ }
+
+ @Override
+ public ChangeListManagerEx getChangeListManager(@NotNull Project project) {
+ return (ChangeListManagerEx)ChangeListManager.getInstance(project);
+ }
+
+ @Override
+ public LocalFileSystem getLocalFileSystem() {
+ return LocalFileSystem.getInstance();
+ }
+
+ @NotNull
+ @Override
+ public AbstractVcsHelper getVcsHelper(@NotNull Project project) {
+ return AbstractVcsHelper.getInstance(project);
+ }
+
+ @Nullable
+ @Override
+ public IdeaPluginDescriptor getPluginByClassName(@NotNull String name) {
+ return PluginManager.getPlugin(PluginManager.getPluginByClassName(name));
+ }
+
+ @Nullable
+ @Override
+ public String getLineSeparator(@NotNull VirtualFile file, boolean detect) {
+ return LoadTextUtil.detectLineSeparator(file, detect);
+ }
+
+ @Override
+ public void saveAllDocuments() {
+ UIUtil.invokeAndWaitIfNeeded(new Runnable() {
+ @Override
+ public void run() {
+ FileDocumentManager.getInstance().saveAllDocuments();
+ }
+ });
+ }
+
+ @NotNull
+ @Override
+ public ProjectManagerEx getProjectManager() {
+ return ProjectManagerEx.getInstanceEx();
+ }
+
+ @NotNull
+ @Override
+ public SaveAndSyncHandler getSaveAndSyncHandler() {
+ return SaveAndSyncHandlerImpl.getInstance();
+ }
+
+ @Override
+ public void hardRefresh(@NotNull VirtualFile root) {
+ VfsUtil.markDirtyAndRefresh(true, true, false, root);
+ }
+
+}
diff --git a/platform/dvcs-impl/src/com/intellij/dvcs/DvcsRememberedInputs.java b/platform/dvcs-impl/src/com/intellij/dvcs/DvcsRememberedInputs.java
new file mode 100644
index 000000000000..58292aa17f91
--- /dev/null
+++ b/platform/dvcs-impl/src/com/intellij/dvcs/DvcsRememberedInputs.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2000-2013 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 com.intellij.dvcs;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author Kirill Likhodedov
+ */
+public class DvcsRememberedInputs {
+
+ private State myState = new State();
+
+ public static class State {
+ public List<UrlAndUserName> visitedUrls = new ArrayList<UrlAndUserName>();
+ public String cloneParentDir = "";
+ }
+
+ public static class UrlAndUserName {
+ public String url;
+ public String userName;
+ }
+
+ @NotNull
+ public State getState() {
+ return myState;
+ }
+
+ public void loadState(State state) {
+ myState = state;
+ }
+
+ public void addUrl(@NotNull String url) {
+ addUrl(url, "");
+ }
+
+ public void addUrl(@NotNull String url, @NotNull String userName) {
+ for (UrlAndUserName visitedUrl : myState.visitedUrls) {
+ if (visitedUrl.url.equalsIgnoreCase(url)) { // don't add multiple entries for a single url
+ if (!userName.isEmpty()) { // rewrite username, unless no username is specified
+ visitedUrl.userName = userName;
+ }
+ return;
+ }
+ }
+
+ UrlAndUserName urlAndUserName = new UrlAndUserName();
+ urlAndUserName.url = url;
+ urlAndUserName.userName = userName;
+ myState.visitedUrls.add(urlAndUserName);
+ }
+
+ @Nullable
+ public String getUserNameForUrl(@NotNull String url) {
+ for (UrlAndUserName urlAndUserName : myState.visitedUrls) {
+ if (urlAndUserName.url.equalsIgnoreCase(url)) {
+ return urlAndUserName.userName;
+ }
+ }
+ return null;
+ }
+
+ @NotNull
+ public List<String> getVisitedUrls() {
+ List<String> urls = new ArrayList<String>(myState.visitedUrls.size());
+ for (UrlAndUserName urlAndUserName : myState.visitedUrls) {
+ urls.add(urlAndUserName.url);
+ }
+ return urls;
+ }
+
+ public String getCloneParentDir() {
+ return myState.cloneParentDir;
+ }
+
+ public void setCloneParentDir(String cloneParentDir) {
+ myState.cloneParentDir = cloneParentDir;
+ }
+}
diff --git a/platform/dvcs-impl/src/com/intellij/dvcs/DvcsUtil.java b/platform/dvcs-impl/src/com/intellij/dvcs/DvcsUtil.java
new file mode 100644
index 000000000000..1fba1827084c
--- /dev/null
+++ b/platform/dvcs-impl/src/com/intellij/dvcs/DvcsUtil.java
@@ -0,0 +1,205 @@
+/*
+ * Copyright 2000-2013 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 com.intellij.dvcs;
+
+import com.intellij.dvcs.repo.Repository;
+import com.intellij.dvcs.repo.RepositoryManager;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.fileEditor.FileDocumentManager;
+import com.intellij.openapi.fileEditor.FileEditor;
+import com.intellij.openapi.fileEditor.FileEditorManager;
+import com.intellij.openapi.fileEditor.TextEditor;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Condition;
+import com.intellij.openapi.util.SystemInfo;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.openapi.vcs.AbstractVcs;
+import com.intellij.openapi.vcs.ProjectLevelVcsManager;
+import com.intellij.openapi.vcs.VcsKey;
+import com.intellij.openapi.vfs.VfsUtilCore;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.openapi.wm.StatusBar;
+import com.intellij.openapi.wm.StatusBarWidget;
+import com.intellij.openapi.wm.WindowManager;
+import com.intellij.openapi.wm.impl.status.StatusBarUtil;
+import com.intellij.util.Function;
+import com.intellij.util.containers.ContainerUtil;
+import com.intellij.util.text.DateFormatUtil;
+import com.intellij.vcs.log.TimedVcsCommit;
+import com.intellij.vcs.log.VcsLog;
+import com.intellij.vcs.log.VcsLogProvider;
+import org.intellij.images.editor.ImageFileEditor;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * @author Kirill Likhodedov
+ */
+public class DvcsUtil {
+
+ private static final int SHORT_HASH_LENGTH = 8;
+
+ public static void installStatusBarWidget(@NotNull Project project, @NotNull StatusBarWidget widget) {
+ StatusBar statusBar = WindowManager.getInstance().getStatusBar(project);
+ if (statusBar != null) {
+ statusBar.addWidget(widget, "after " + (SystemInfo.isMac ? "Encoding" : "InsertOverwrite"), project);
+ }
+ }
+
+ public static void removeStatusBarWidget(@NotNull Project project, @NotNull StatusBarWidget widget) {
+ StatusBar statusBar = WindowManager.getInstance().getStatusBar(project);
+ if (statusBar != null) {
+ statusBar.removeWidget(widget.ID());
+ }
+ }
+
+ @NotNull
+ public static String getShortRepositoryName(@NotNull Project project, @NotNull VirtualFile root) {
+ VirtualFile projectDir = project.getBaseDir();
+
+ String repositoryPath = root.getPresentableUrl();
+ if (projectDir != null) {
+ String relativePath = VfsUtilCore.getRelativePath(root, projectDir, File.separatorChar);
+ if (relativePath != null) {
+ repositoryPath = relativePath;
+ }
+ }
+
+ return repositoryPath.isEmpty() ? root.getName() : repositoryPath;
+ }
+
+ @NotNull
+ public static String getShortRepositoryName(@NotNull Repository repository) {
+ return getShortRepositoryName(repository.getProject(), repository.getRoot());
+ }
+
+ @NotNull
+ public static String getShortNames(@NotNull Collection<? extends Repository> repositories) {
+ return StringUtil.join(repositories, new Function<Repository, String>() {
+ @Override
+ public String fun(Repository repository) {
+ return getShortRepositoryName(repository);
+ }
+ }, ", ");
+ }
+
+ @NotNull
+ public static String joinRootsPaths(@NotNull Collection<VirtualFile> roots) {
+ return StringUtil.join(roots, new Function<VirtualFile, String>() {
+ @Override
+ public String fun(VirtualFile virtualFile) {
+ return virtualFile.getPresentableUrl();
+ }
+ }, ", ");
+ }
+
+ public static boolean anyRepositoryIsFresh(Collection<? extends Repository> repositories) {
+ for (Repository repository : repositories) {
+ if (repository.isFresh()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Report a warning that the given root has no associated Repositories.
+ */
+ public static void noVcsRepositoryForRoot(@NotNull Logger log,
+ @NotNull VirtualFile root,
+ @NotNull Project project,
+ @NotNull RepositoryManager repositoryManager,
+ @Nullable AbstractVcs vcs) {
+ if (vcs == null) {
+ return;
+ }
+ ProjectLevelVcsManager vcsManager = ProjectLevelVcsManager.getInstance(project);
+ List<VirtualFile> roots = Arrays.asList(vcsManager.getRootsUnderVcs(vcs));
+ log.warn(String.format("Repository not found for root: %s. All roots: %s, all repositories: %s", root, roots,
+ repositoryManager.getRepositories()));
+ }
+
+ /**
+ * Checks if there are hg roots in the VCS log.
+ */
+ public static boolean logHasRootForVcs(@NotNull VcsLog log, @Nullable final VcsKey vcsKey) {
+ return ContainerUtil.find(log.getLogProviders(), new Condition<VcsLogProvider>() {
+ @Override
+ public boolean value(VcsLogProvider logProvider) {
+ return logProvider.getSupportedVcs().equals(vcsKey);
+ }
+ }) != null;
+ }
+
+ @Nullable
+ public static String joinMessagesOrNull(@NotNull Collection<String> messages) {
+ String joined = StringUtil.join(messages, "\n");
+ return StringUtil.isEmptyOrSpaces(joined) ? null : joined;
+ }
+
+ /**
+ * Returns the currently selected file, based on which VcsBranch or StatusBar components will identify the current repository root.
+ */
+ @Nullable
+ public static VirtualFile getSelectedFile(@NotNull Project project) {
+ StatusBar statusBar = WindowManager.getInstance().getStatusBar(project);
+ final FileEditor fileEditor = StatusBarUtil.getCurrentFileEditor(project, statusBar);
+ VirtualFile result = null;
+ if (fileEditor != null) {
+ if (fileEditor instanceof TextEditor) {
+ Document document = ((TextEditor)fileEditor).getEditor().getDocument();
+ result = FileDocumentManager.getInstance().getFile(document);
+ }
+ else if (fileEditor instanceof ImageFileEditor) {
+ result = ((ImageFileEditor)fileEditor).getImageEditor().getFile();
+ }
+ }
+
+ if (result == null) {
+ final FileEditorManager manager = FileEditorManager.getInstance(project);
+ if (manager != null) {
+ Editor editor = manager.getSelectedTextEditor();
+ if (editor != null) {
+ result = FileDocumentManager.getInstance().getFile(editor.getDocument());
+ }
+ }
+ }
+ return result;
+ }
+
+ @NotNull
+ public static String getShortHash(@NotNull String hash) {
+ if (hash.length() == 0) return "";
+ if (hash.length() == 40) return hash.substring(0, SHORT_HASH_LENGTH);
+ if (hash.length() > 40) // revision string encoded with date too
+ {
+ return hash.substring(hash.indexOf("[") + 1, SHORT_HASH_LENGTH);
+ }
+ return hash;
+ }
+
+ @NotNull
+ public static String getDateString(@NotNull TimedVcsCommit commit) {
+ return DateFormatUtil.formatPrettyDateTime(commit.getTimestamp()) + " ";
+ }
+}
diff --git a/platform/dvcs-impl/src/com/intellij/dvcs/push/PushController.java b/platform/dvcs-impl/src/com/intellij/dvcs/push/PushController.java
new file mode 100644
index 000000000000..2a409e12c854
--- /dev/null
+++ b/platform/dvcs-impl/src/com/intellij/dvcs/push/PushController.java
@@ -0,0 +1,445 @@
+/*
+ * Copyright 2000-2014 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 com.intellij.dvcs.push;
+
+import com.intellij.dvcs.DvcsUtil;
+import com.intellij.dvcs.push.ui.*;
+import com.intellij.dvcs.repo.Repository;
+import com.intellij.dvcs.repo.RepositoryManager;
+import com.intellij.openapi.Disposable;
+import com.intellij.openapi.application.ModalityState;
+import com.intellij.openapi.extensions.Extensions;
+import com.intellij.openapi.progress.ProgressIndicator;
+import com.intellij.openapi.progress.Task;
+import com.intellij.openapi.progress.impl.ProgressManagerImpl;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.ValidationInfo;
+import com.intellij.openapi.util.Condition;
+import com.intellij.openapi.util.Disposer;
+import com.intellij.openapi.vcs.AbstractVcs;
+import com.intellij.ui.CheckedTreeNode;
+import com.intellij.util.Function;
+import com.intellij.util.containers.ContainerUtil;
+import com.intellij.util.containers.hash.HashMap;
+import com.intellij.vcs.log.VcsFullCommitDetails;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import javax.swing.tree.DefaultMutableTreeNode;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.util.*;
+import java.util.concurrent.atomic.AtomicReference;
+
+public class PushController implements Disposable {
+
+ @NotNull private final Project myProject;
+ @NotNull private final List<PushSupport<? extends Repository>> myPushSupports;
+ @NotNull private final PushLog myPushLog;
+ @NotNull private final VcsPushDialog myDialog;
+ private boolean mySingleRepoProject;
+ private static final int DEFAULT_CHILDREN_PRESENTATION_NUMBER = 20;
+ private final Map<PushSupport, MyPushOptionValueModel> myAdditionalValuesMap;
+
+ private final Map<RepositoryNode, MyRepoModel> myView2Model = new HashMap<RepositoryNode, MyRepoModel>();
+
+
+ public PushController(@NotNull Project project,
+ @NotNull VcsPushDialog dialog,
+ @NotNull List<? extends Repository> preselectedRepositories) {
+ myProject = project;
+ //todo what would be in case of null
+ myPushSupports = Arrays.asList(Extensions.getExtensions(PushSupport.PUSH_SUPPORT_EP, myProject));
+ CheckedTreeNode rootNode = new CheckedTreeNode(null);
+ mySingleRepoProject = createTreeModel(rootNode, preselectedRepositories);
+ myPushLog = new PushLog(myProject, rootNode);
+ myAdditionalValuesMap = new HashMap<PushSupport, MyPushOptionValueModel>();
+ myDialog = dialog;
+ myDialog.updateButtons();
+ startLoadingCommits();
+ Disposer.register(dialog.getDisposable(), this);
+ }
+
+ @Nullable
+ public ValidationInfo validate() {
+ ValidationInfo validInfo = new ValidationInfo("There are no selected repository to push!");
+ for (Map.Entry<RepositoryNode, MyRepoModel> entry : myView2Model.entrySet()) {
+ MyRepoModel model = entry.getValue();
+ if (model.isSelected()) {
+ //has one or more selected roots
+ validInfo = null;
+ RepositoryNode node = entry.getKey();
+ PushTarget target = model.getSpec().getTarget();
+ //todo add validation for model -> hasErrors, too
+ if (target == null) {
+ JComponent editingComponent = myPushLog.startEditNode(node);
+ return new ValidationInfo("Invalid remote for repository " + DvcsUtil.getShortRepositoryName(model.getRepository()),
+ editingComponent);
+ }
+ }
+ }
+ return validInfo;
+ }
+
+ private void startLoadingCommits() {
+ //todo should be reworked
+ Map<RepositoryNode, MyRepoModel> priorityLoading = new HashMap<RepositoryNode, MyRepoModel>();
+ Map<RepositoryNode, MyRepoModel> others = new HashMap<RepositoryNode, MyRepoModel>();
+ for (Map.Entry<RepositoryNode, MyRepoModel> entry : myView2Model.entrySet()) {
+ MyRepoModel model = entry.getValue();
+ if (model.isSelected()) {
+ priorityLoading.put(entry.getKey(), model);
+ }
+ else {
+ others.put(entry.getKey(), model);
+ }
+ }
+ loadCommitsFromMap(priorityLoading);
+ loadCommitsFromMap(others);
+ }
+
+ private void loadCommitsFromMap(@NotNull Map<RepositoryNode, MyRepoModel> items) {
+ for (Map.Entry<RepositoryNode, MyRepoModel> entry : items.entrySet()) {
+ RepositoryNode node = entry.getKey();
+ loadCommits(entry.getValue(), node, true);
+ }
+ }
+
+ //return is single repository project or not
+ private boolean createTreeModel(@NotNull CheckedTreeNode rootNode, @NotNull List<? extends Repository> preselectedRepositories) {
+ if (myPushSupports.isEmpty()) return true;
+ int repoCount = 0;
+ for (PushSupport<? extends Repository> support : myPushSupports) {
+ repoCount += createNodesForVcs(support, rootNode, preselectedRepositories);
+ }
+ return repoCount == 1;
+ }
+
+ private <T extends Repository> int createNodesForVcs(@NotNull PushSupport<T> pushSupport,
+ @NotNull CheckedTreeNode rootNode,
+ @NotNull List<? extends Repository> preselectedRepositories) {
+ RepositoryManager<T> repositoryManager = pushSupport.getRepositoryManager();
+ List<T> repositories = repositoryManager.getRepositories();
+ for (T repository : repositories) {
+ createRepoNode(pushSupport, repository, rootNode, preselectedRepositories.contains(repository), repositories.size() == 1);
+ }
+ return repositories.size();
+ }
+
+ private <T extends Repository> void createRepoNode(@NotNull final PushSupport<T> support,
+ @NotNull final T repository,
+ @NotNull CheckedTreeNode rootNode,
+ boolean isSelected,
+ boolean isSingleRepositoryProject) {
+ PushTarget target = support.getDefaultTarget(repository);
+ final MyRepoModel model = new MyRepoModel(repository, support, isSelected, new PushSpec(support.getSource(repository), target),
+ DEFAULT_CHILDREN_PRESENTATION_NUMBER);
+ RepositoryWithBranchPanel repoPanel = new RepositoryWithBranchPanel(myProject, DvcsUtil.getShortRepositoryName(repository),
+ support.getSource(repository).getPresentation(),
+ target == null ? "" : target.getPresentation(),
+ support.getTargetNames(repository));
+ final RepositoryNode repoNode = isSingleRepositoryProject ? new SingleRepositoryNode(repoPanel) : new RepositoryNode(repoPanel);
+ myView2Model.put(repoNode, model);
+ repoNode.setChecked(model.isSelected());
+ repoPanel.addRepoNodeListener(new RepositoryNodeListener() {
+ @Override
+ public void onTargetChanged(String newValue) {
+ VcsError validationError = support.validate(model.getRepository(), newValue);
+ if (validationError == null) {
+ myView2Model.get(repoNode).setSpec(new PushSpec(model.getSpec().getSource(), support.createTarget(repository, newValue)));
+ loadCommits(model, repoNode, false);
+ }
+ else {
+ //todo may be should store validation errors in model and get errors during dialog validation
+ myView2Model.get(repoNode).setSpec(new PushSpec(model.getSpec().getSource(), null));
+ }
+ myDialog.updateButtons();
+ }
+
+ @Override
+ public void onSelectionChanged(boolean isSelected) {
+ myView2Model.get(repoNode).setSelected(isSelected);
+ repoNode.setChecked(isSelected);
+ myDialog.updateButtons();
+ }
+ });
+ rootNode.add(repoNode);
+ }
+
+ private void loadCommits(@NotNull final MyRepoModel model,
+ @NotNull final RepositoryNode node,
+ final boolean initial) {
+ node.stopLoading();
+ myPushLog.startLoading(node);
+ final ProgressIndicator indicator = node.startLoading();
+ final PushSupport support = model.getSupport();
+ final AtomicReference<OutgoingResult> result = new AtomicReference<OutgoingResult>();
+ Task.Backgroundable task = new Task.Backgroundable(myProject, "Loading Commits", true) {
+
+ @Override
+ public void onCancel() {
+ node.stopLoading();
+ }
+
+ @Override
+ public void onSuccess() {
+ OutgoingResult outgoing = result.get();
+ List<VcsError> errors = outgoing.getErrors();
+ if (!errors.isEmpty()) {
+ myPushLog.setChildren(node, ContainerUtil.map(errors, new Function<VcsError, DefaultMutableTreeNode>() {
+ @Override
+ public DefaultMutableTreeNode fun(final VcsError error) {
+ VcsLinkedText errorLinkText = new VcsLinkedText(error.getText(), new VcsLinkListener() {
+ @Override
+ public void hyperlinkActivated(@NotNull DefaultMutableTreeNode sourceNode) {
+ error.handleError(new CommitLoader() {
+ @Override
+ public void reloadCommits() {
+ loadCommits(model, node, false);
+ }
+ });
+ }
+ });
+ return new TextWithLinkNode(errorLinkText);
+ }
+ }), model.isSelected());
+ }
+ else {
+ model.setLoadedCommits(outgoing.getCommits());
+ myPushLog.setChildren(node,
+ getPresentationForCommits(PushController.this.myProject, model.getLoadedCommits(),
+ model.getNumberOfShownCommits()), model.isSelected());
+ }
+ }
+
+ @Override
+ public void run(@NotNull ProgressIndicator indicator) {
+ OutgoingResult outgoing = support.getOutgoingCommitsProvider()
+ .getOutgoingCommits(model.getRepository(), model.getSpec(), initial);
+ result.compareAndSet(null, outgoing);
+ }
+ };
+
+ ProgressManagerImpl.runProcessWithProgressAsynchronously(task, indicator, null, ModalityState.any());
+ }
+
+
+ public PushLog getPushPanelInfo() {
+ return myPushLog;
+ }
+
+ public void push(final boolean force) {
+ Task.Backgroundable task = new Task.Backgroundable(myProject, "Pushing...", false) {
+ @Override
+ public void run(@NotNull ProgressIndicator indicator) {
+ for (PushSupport support : myPushSupports) {
+ MyPushOptionValueModel additionalOptionsModel = myAdditionalValuesMap.get(support);
+ support.getPusher()
+ .push(collectPushInfoForVcs(support), additionalOptionsModel == null ? null : additionalOptionsModel.getCurrentValue(), force);
+ }
+ }
+ };
+ task.queue();
+ }
+
+ @NotNull
+ private Map<Repository, PushSpec> collectPushInfoForVcs(@NotNull final PushSupport pushSupport) {
+ Map<Repository, PushSpec> pushSpecs = new HashMap<Repository, PushSpec>();
+ Collection<MyRepoModel> repositoriesInformation = getSelectedRepoNode();
+ for (MyRepoModel repoModel : repositoriesInformation) {
+ if (pushSupport.equals(repoModel.getSupport())) {
+ pushSpecs.put(repoModel.getRepository(), repoModel.getSpec());
+ }
+ }
+ return pushSpecs;
+ }
+
+ public Collection<MyRepoModel> getSelectedRepoNode() {
+ if (mySingleRepoProject) {
+ return myView2Model.values();
+ }
+ return ContainerUtil.filter(myView2Model.values(), new Condition<MyRepoModel>() {
+ @Override
+ public boolean value(MyRepoModel model) {
+ return model.isSelected();
+ }
+ });
+ }
+
+ @Override
+ public void dispose() {
+ for (RepositoryNode node : myView2Model.keySet()) {
+ node.stopLoading();
+ }
+ }
+
+ private void addMoreCommits(RepositoryNode repositoryNode) {
+ MyRepoModel repoModel = myView2Model.get(repositoryNode);
+ repoModel.increaseShownCommits();
+ myPushLog.setChildren(repositoryNode,
+ getPresentationForCommits(
+ myProject,
+ repoModel.getLoadedCommits(),
+ repoModel.getNumberOfShownCommits()
+ ));
+ }
+
+
+ @NotNull
+ public List<DefaultMutableTreeNode> getPresentationForCommits(@NotNull final Project project,
+ @NotNull List<? extends VcsFullCommitDetails> commits,
+ int commitsNum) {
+ Function<VcsFullCommitDetails, DefaultMutableTreeNode> commitToNode = new Function<VcsFullCommitDetails, DefaultMutableTreeNode>() {
+ @Override
+ public DefaultMutableTreeNode fun(VcsFullCommitDetails commit) {
+ return new VcsFullCommitDetailsNode(project, commit);
+ }
+ };
+ List<DefaultMutableTreeNode> childrenToShown = new ArrayList<DefaultMutableTreeNode>();
+ for (int i = 0; i < commits.size(); ++i) {
+ if (i >= commitsNum) {
+ final VcsLinkedText moreCommitsLink = new VcsLinkedText("<a href='loadMore'>...</a>", new VcsLinkListener() {
+ @Override
+ public void hyperlinkActivated(@NotNull DefaultMutableTreeNode sourceNode) {
+ addMoreCommits((RepositoryNode)sourceNode);
+ }
+ });
+ childrenToShown.add(new TextWithLinkNode(moreCommitsLink));
+ break;
+ }
+ childrenToShown.add(commitToNode.fun(commits.get(i)));
+ }
+ return childrenToShown;
+ }
+
+ @NotNull
+ public List<VcsPushOptionsPanel> getAdditionalPanels() {
+ List<VcsPushOptionsPanel> additionalPanels = new ArrayList<VcsPushOptionsPanel>();
+ for (final PushSupport support : myPushSupports) {
+ if (hasRepoForPushSupport(support)) {
+ final VcsPushOptionsPanel panel = support.getVcsPushOptionsPanel();
+ if (panel != null) {
+ additionalPanels.add(panel);
+ myAdditionalValuesMap.put(support, new MyPushOptionValueModel(panel.getValue()));
+ panel.addValueChangeListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ myAdditionalValuesMap.get(support).setCurrentValue(panel.getValue());
+ }
+ });
+ }
+ }
+ }
+ return additionalPanels;
+ }
+
+ private boolean hasRepoForPushSupport(@NotNull final PushSupport support) {
+ return ContainerUtil.exists(myView2Model.values(), new Condition<MyRepoModel>() {
+ @Override
+ public boolean value(MyRepoModel model) {
+ return support.equals(model.getSupport());
+ }
+ });
+ }
+
+ private static class MyRepoModel {
+ @NotNull final Repository myRepository;
+ @NotNull private PushSupport mySupport;
+
+ @NotNull PushSpec mySpec;
+ int myNumberOfShownCommits;
+
+ List<? extends VcsFullCommitDetails> myLoadedCommits;
+ boolean myIsSelected;
+
+ public MyRepoModel(@NotNull Repository repository,
+ @NotNull PushSupport supportForRepo,
+ boolean isSelected,
+ @NotNull PushSpec spec,
+ int num) {
+ myRepository = repository;
+ mySupport = supportForRepo;
+ myIsSelected = isSelected;
+ mySpec = spec;
+ myNumberOfShownCommits = num;
+ }
+
+ @NotNull
+ public Repository getRepository() {
+ return myRepository;
+ }
+
+ @NotNull
+ public PushSupport getSupport() {
+ return mySupport;
+ }
+
+ public boolean isSelected() {
+ return myIsSelected;
+ }
+
+ public AbstractVcs<?> getVcs() {
+ return myRepository.getVcs();
+ }
+
+ @NotNull
+ public PushSpec getSpec() {
+ return mySpec;
+ }
+
+ public void setSpec(@NotNull PushSpec spec) {
+ mySpec = spec;
+ }
+
+ public void setSelected(boolean isSelected) {
+ myIsSelected = isSelected;
+ }
+
+ public int getNumberOfShownCommits() {
+ return myNumberOfShownCommits;
+ }
+
+ public void increaseShownCommits() {
+ myNumberOfShownCommits *= 2;
+ }
+
+ public List<? extends VcsFullCommitDetails> getLoadedCommits() {
+ return myLoadedCommits;
+ }
+
+ public void setLoadedCommits(List<? extends VcsFullCommitDetails> loadedCommits) {
+ myLoadedCommits = loadedCommits;
+ }
+ }
+
+ private static class MyPushOptionValueModel {
+ @NotNull private VcsPushOptionValue myCurrentValue;
+
+ public MyPushOptionValueModel(@NotNull VcsPushOptionValue currentValue) {
+ myCurrentValue = currentValue;
+ }
+
+ public void setCurrentValue(@NotNull VcsPushOptionValue currentValue) {
+ myCurrentValue = currentValue;
+ }
+
+ @NotNull
+ public VcsPushOptionValue getCurrentValue() {
+ return myCurrentValue;
+ }
+ }
+}
diff --git a/platform/dvcs-impl/src/com/intellij/dvcs/push/RepositoryNodeListener.java b/platform/dvcs-impl/src/com/intellij/dvcs/push/RepositoryNodeListener.java
new file mode 100644
index 000000000000..7ca285fc95aa
--- /dev/null
+++ b/platform/dvcs-impl/src/com/intellij/dvcs/push/RepositoryNodeListener.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2000-2014 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 com.intellij.dvcs.push;
+
+public interface RepositoryNodeListener {
+
+ void onTargetChanged(String newValue);
+
+ void onSelectionChanged(boolean isSelected);
+}
diff --git a/platform/dvcs-impl/src/com/intellij/dvcs/push/ui/CustomRenderedTreeNode.java b/platform/dvcs-impl/src/com/intellij/dvcs/push/ui/CustomRenderedTreeNode.java
new file mode 100644
index 000000000000..18a69bae53be
--- /dev/null
+++ b/platform/dvcs-impl/src/com/intellij/dvcs/push/ui/CustomRenderedTreeNode.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2000-2014 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 com.intellij.dvcs.push.ui;
+
+import com.intellij.ui.ColoredTreeCellRenderer;
+import org.jetbrains.annotations.NotNull;
+
+interface CustomRenderedTreeNode {
+
+ void render(@NotNull ColoredTreeCellRenderer renderer);
+} \ No newline at end of file
diff --git a/platform/dvcs-impl/src/com/intellij/dvcs/push/ui/DvcsStrategyPanel.java b/platform/dvcs-impl/src/com/intellij/dvcs/push/ui/DvcsStrategyPanel.java
new file mode 100644
index 000000000000..5b3aa4b153ed
--- /dev/null
+++ b/platform/dvcs-impl/src/com/intellij/dvcs/push/ui/DvcsStrategyPanel.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2000-2014 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 com.intellij.dvcs.push.ui;
+
+import com.intellij.dvcs.push.VcsPushReferenceStrategy;
+import com.intellij.openapi.ui.ComboBox;
+
+import javax.swing.*;
+import java.awt.*;
+
+public class DvcsStrategyPanel extends JPanel {
+
+ private ComboBox myReferenceStrategyCombobox;
+
+ public DvcsStrategyPanel() {
+ setLayout(new BorderLayout());
+ myReferenceStrategyCombobox = new ComboBox();
+ DefaultComboBoxModel comboModel = new DefaultComboBoxModel(VcsPushReferenceStrategy.values());
+ myReferenceStrategyCombobox.setModel(comboModel);
+ JPanel bottomPanel = new JPanel(new FlowLayout());
+ JLabel referenceStrategyLabel = new JLabel("Push Reference Strategy: ");
+ bottomPanel.add(referenceStrategyLabel, FlowLayout.LEFT);
+ bottomPanel.add(myReferenceStrategyCombobox);
+ add(bottomPanel, BorderLayout.WEST);
+ }
+
+ public VcsPushReferenceStrategy getStrategy() {
+ return (VcsPushReferenceStrategy)myReferenceStrategyCombobox.getSelectedItem();
+ }
+}
diff --git a/platform/dvcs-impl/src/com/intellij/dvcs/push/ui/EditableTreeNode.java b/platform/dvcs-impl/src/com/intellij/dvcs/push/ui/EditableTreeNode.java
new file mode 100644
index 000000000000..5d0d20cdc4c0
--- /dev/null
+++ b/platform/dvcs-impl/src/com/intellij/dvcs/push/ui/EditableTreeNode.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2000-2014 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 com.intellij.dvcs.push.ui;
+
+import com.intellij.openapi.progress.ProgressIndicator;
+import org.jetbrains.annotations.NotNull;
+
+public interface EditableTreeNode extends CustomRenderedTreeNode {
+
+ void fireOnChange(@NotNull String value);
+
+ void fireOnSelectionChange(boolean isSelected);
+
+ void stopLoading();
+
+ @NotNull
+ ProgressIndicator startLoading();
+
+ String getValue();
+}
diff --git a/platform/dvcs-impl/src/com/intellij/dvcs/push/ui/LoadingTreeNode.java b/platform/dvcs-impl/src/com/intellij/dvcs/push/ui/LoadingTreeNode.java
new file mode 100644
index 000000000000..dc424df43b81
--- /dev/null
+++ b/platform/dvcs-impl/src/com/intellij/dvcs/push/ui/LoadingTreeNode.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2000-2014 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 com.intellij.dvcs.push.ui;
+
+import com.intellij.ui.ColoredTreeCellRenderer;
+import com.intellij.ui.JBColor;
+import com.intellij.ui.SimpleTextAttributes;
+import com.intellij.util.ImageLoader;
+import com.intellij.util.ui.JBImageIcon;
+import org.jetbrains.annotations.NotNull;
+
+import javax.swing.*;
+import javax.swing.tree.DefaultMutableTreeNode;
+import java.awt.*;
+import java.net.URL;
+
+public class LoadingTreeNode extends DefaultMutableTreeNode implements CustomRenderedTreeNode {
+ @NotNull protected ImageIcon myLoadingIcon;
+ private static final String LOADING_ICON = "/icons/loading.gif";
+
+ @NotNull
+ public ImageIcon getIcon() {
+ return myLoadingIcon;
+ }
+
+ public LoadingTreeNode() {
+ super(null, false);
+ URL loadingIconUrl = getClass().getResource(LOADING_ICON);
+ Image image = ImageLoader.loadFromUrl(loadingIconUrl);
+ myLoadingIcon = new JBImageIcon(image);
+ }
+
+ @Override
+ public void render(@NotNull ColoredTreeCellRenderer renderer) {
+ renderer.setIcon(myLoadingIcon);
+ renderer.append("Loading Commits...", new SimpleTextAttributes(SimpleTextAttributes.STYLE_SMALLER, JBColor.GRAY));
+ }
+}
diff --git a/platform/dvcs-impl/src/com/intellij/dvcs/push/ui/NodeImageObserver.java b/platform/dvcs-impl/src/com/intellij/dvcs/push/ui/NodeImageObserver.java
new file mode 100644
index 000000000000..1e3d82015afe
--- /dev/null
+++ b/platform/dvcs-impl/src/com/intellij/dvcs/push/ui/NodeImageObserver.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2000-2014 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 com.intellij.dvcs.push.ui;
+
+import javax.swing.*;
+import javax.swing.tree.DefaultTreeModel;
+import javax.swing.tree.TreeNode;
+import javax.swing.tree.TreePath;
+import java.awt.*;
+import java.awt.image.ImageObserver;
+
+class NodeImageObserver implements ImageObserver {
+ JTree tree;
+ DefaultTreeModel model;
+ TreeNode node;
+
+ NodeImageObserver(JTree tree, TreeNode node) {
+ this.tree = tree;
+ this.model = (DefaultTreeModel)tree.getModel();
+ this.node = node;
+ }
+
+ public boolean imageUpdate(Image img, int flags, int x, int y, int w, int h) {
+ if ((flags & (FRAMEBITS | ALLBITS)) != 0) {
+ TreePath path = new TreePath(model.getPathToRoot(node));
+ Rectangle rect = tree.getPathBounds(path);
+ if (rect != null) {
+ tree.repaint(rect);
+ }
+ }
+ return (flags & (ALLBITS | ABORT)) == 0;
+ }
+} \ No newline at end of file
diff --git a/platform/dvcs-impl/src/com/intellij/dvcs/push/ui/PushLog.java b/platform/dvcs-impl/src/com/intellij/dvcs/push/ui/PushLog.java
new file mode 100644
index 000000000000..b07bf9243f8a
--- /dev/null
+++ b/platform/dvcs-impl/src/com/intellij/dvcs/push/ui/PushLog.java
@@ -0,0 +1,289 @@
+/*
+ * Copyright 2000-2014 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 com.intellij.dvcs.push.ui;
+
+import com.intellij.openapi.actionSystem.CommonShortcuts;
+import com.intellij.openapi.actionSystem.DataKey;
+import com.intellij.openapi.actionSystem.DataSink;
+import com.intellij.openapi.actionSystem.TypeSafeDataProvider;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.Splitter;
+import com.intellij.openapi.vcs.VcsDataKeys;
+import com.intellij.openapi.vcs.changes.Change;
+import com.intellij.openapi.vcs.changes.committed.CommittedChangesTreeBrowser;
+import com.intellij.openapi.vcs.changes.ui.ChangesBrowser;
+import com.intellij.ui.*;
+import com.intellij.ui.components.JBTextField;
+import com.intellij.util.ArrayUtil;
+import com.intellij.util.ui.tree.TreeUtil;
+import com.intellij.vcs.log.VcsFullCommitDetails;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import javax.swing.event.CellEditorListener;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.TreeSelectionEvent;
+import javax.swing.event.TreeSelectionListener;
+import javax.swing.tree.*;
+import java.awt.*;
+import java.awt.event.KeyEvent;
+import java.awt.event.MouseEvent;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EventObject;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+public class PushLog extends JPanel implements TypeSafeDataProvider {
+
+ private final ReentrantReadWriteLock TREE_CONSTRUCTION_LOCK = new ReentrantReadWriteLock();
+
+ private static final String START_EDITING = "startEditing";
+ private final ChangesBrowser myChangesBrowser;
+ private final CheckboxTree myTree;
+ private final MyTreeCellRenderer myTreeCellRenderer;
+
+ public PushLog(Project project, CheckedTreeNode root) {
+ DefaultTreeModel treeModel = new DefaultTreeModel(root);
+ treeModel.nodeStructureChanged(root);
+ myTreeCellRenderer = new MyTreeCellRenderer();
+ myTree = new CheckboxTree(myTreeCellRenderer, root) {
+
+ public boolean isPathEditable(TreePath path) {
+ return isEditable() && path.getLastPathComponent() instanceof DefaultMutableTreeNode;
+ }
+
+ @Override
+ protected void onNodeStateChanged(CheckedTreeNode node) {
+ if (node instanceof EditableTreeNode) {
+ ((EditableTreeNode)node).fireOnSelectionChange(node.isChecked());
+ }
+ }
+
+ @Override
+ public String getToolTipText(MouseEvent event) {
+ final TreePath path = myTree.getPathForLocation(event.getX(), event.getY());
+ if (path == null) {
+ return "";
+ }
+ Object node = path.getLastPathComponent();
+ if (node == null || (!(node instanceof DefaultMutableTreeNode))) {
+ return "";
+ }
+ if (node instanceof TooltipNode) {
+ return ((TooltipNode)node).getTooltip();
+ }
+ return "";
+ }
+ };
+ myTree.setEditable(true);
+ MyTreeCellEditor treeCellEditor = new MyTreeCellEditor(new JBTextField());
+ myTree.setCellEditor(treeCellEditor);
+ treeCellEditor.addCellEditorListener(new CellEditorListener() {
+ @Override
+ public void editingStopped(ChangeEvent e) {
+ }
+
+ @Override
+ public void editingCanceled(ChangeEvent e) {
+ DefaultMutableTreeNode node = (DefaultMutableTreeNode)myTree.getLastSelectedPathComponent();
+ if (node != null && node instanceof EditableTreeNode) {
+ //todo restore from appropriate editor
+ ((EditableTreeNode)node).fireOnChange(((EditableTreeNode)node).getValue());
+ }
+ }
+ });
+ myTree.setRootVisible(false);
+ TreeUtil.expandAll(myTree);
+ final VcsBranchEditorListener linkMouseListener = new VcsBranchEditorListener(myTreeCellRenderer);
+ linkMouseListener.installOn(myTree);
+
+ myTree.getSelectionModel().setSelectionMode(TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION);
+ myTree.addTreeSelectionListener(new TreeSelectionListener() {
+ @Override
+ public void valueChanged(TreeSelectionEvent e) {
+ TreePath[] nodes = myTree.getSelectionPaths();
+ if (nodes != null) {
+ ArrayList<Change> changes = new ArrayList<Change>();
+ for (TreePath node : nodes) {
+ Object nodeInfo = ((DefaultMutableTreeNode)node.getLastPathComponent()).getUserObject();
+ if (nodeInfo instanceof VcsFullCommitDetails) {
+ changes.addAll(((VcsFullCommitDetails)nodeInfo).getChanges());
+ }
+ }
+ myChangesBrowser.getViewer().setEmptyText("No differences");
+ myChangesBrowser.setChangesToDisplay(CommittedChangesTreeBrowser.zipChanges(changes));
+ return;
+ }
+ setDefaultEmptyText();
+ myChangesBrowser.setChangesToDisplay(Collections.<Change>emptyList());
+ }
+ });
+ myTree.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_F2, 0), START_EDITING);
+ myTree.setRowHeight(0);
+ ToolTipManager.sharedInstance().registerComponent(myTree);
+
+ myChangesBrowser =
+ new ChangesBrowser(project, null, Collections.<Change>emptyList(), null, false, true, null, ChangesBrowser.MyUseCase.LOCAL_CHANGES,
+ null);
+ myChangesBrowser.getDiffAction().registerCustomShortcutSet(CommonShortcuts.getDiff(), myTree);
+ setDefaultEmptyText();
+
+ Splitter splitter = new Splitter(false, 0.7f);
+ splitter.setFirstComponent(ScrollPaneFactory.createScrollPane(myTree));
+ splitter.setSecondComponent(myChangesBrowser);
+
+ setLayout(new BorderLayout());
+ add(splitter);
+ }
+
+ private void setDefaultEmptyText() {
+ myChangesBrowser.getViewer().setEmptyText("No commits selected");
+ }
+
+ // Make changes available for diff action
+ @Override
+ public void calcData(DataKey key, DataSink sink) {
+ if (VcsDataKeys.CHANGES.equals(key)) {
+ DefaultMutableTreeNode[] selectedNodes = myTree.getSelectedNodes(DefaultMutableTreeNode.class, null);
+ if (selectedNodes.length == 0) {
+ return;
+ }
+ Object object = selectedNodes[0].getUserObject();
+ if (object instanceof VcsFullCommitDetails) {
+ sink.put(key, ArrayUtil.toObjectArray(((VcsFullCommitDetails)object).getChanges(), Change.class));
+ }
+ }
+ }
+
+ @Override
+ protected boolean processKeyBinding(KeyStroke ks, KeyEvent e, int condition, boolean pressed) {
+ if (e.getKeyCode() == KeyEvent.VK_ENTER && myTree.isEditing()) {
+ myTree.cancelEditing();
+ return true;
+ }
+ return super.processKeyBinding(ks, e, condition, pressed);
+ }
+
+ public void startLoading(DefaultMutableTreeNode parentNode) {
+ LoadingTreeNode loading = new LoadingTreeNode();
+ loading.getIcon().setImageObserver(new NodeImageObserver(myTree, loading));
+ setChildren(parentNode, Collections.singleton(loading));
+ }
+
+ private class MyTreeCellEditor extends DefaultCellEditor {
+
+ public MyTreeCellEditor(JTextField field) {
+ super(field);
+ setClickCountToStart(1);
+ }
+
+ @Override
+ public Component getTreeCellEditorComponent(JTree tree, Object value, boolean isSelected, boolean expanded, boolean leaf, int row) {
+ final Object node = ((DefaultMutableTreeNode)value).getUserObject();
+ editorComponent =
+ (JComponent)((RepositoryWithBranchPanel)node).getTreeCellRendererComponent(tree, value, isSelected, expanded, leaf, row, true);
+ return editorComponent;
+ }
+
+ @Override
+ public boolean isCellEditable(EventObject anEvent) {
+ if (anEvent instanceof MouseEvent) {
+ MouseEvent me = ((MouseEvent)anEvent);
+ final TreePath path = myTree.getClosestPathForLocation(me.getX(), me.getY());
+ final int row = myTree.getRowForLocation(me.getX(), me.getY());
+ myTree.getCellRenderer().getTreeCellRendererComponent(myTree, path.getLastPathComponent(), false, false, true, row, true);
+ Object tag = me.getClickCount() >= clickCountToStart
+ ? PushLogTreeUtil.getTagAtForRenderer(myTreeCellRenderer, me)
+ : null;
+ return tag instanceof EditorTextField;
+ }
+ //if keyboard event - then anEvent will be null =( See BasicTreeUi
+ TreePath treePath = myTree.getAnchorSelectionPath();
+ //there is no selection path if we start editing during initial validation//
+ if (treePath == null) return true;
+ Object treeNode = treePath.getLastPathComponent();
+ return treeNode instanceof EditableTreeNode;
+ }
+
+ //Implement the one CellEditor method that AbstractCellEditor doesn't.
+ public Object getCellEditorValue() {
+ return ((RepositoryWithBranchPanel)editorComponent).getRemoteTargetName();
+ }
+ }
+
+ private static class MyTreeCellRenderer extends CheckboxTree.CheckboxTreeCellRenderer {
+
+ @Override
+ public void customizeRenderer(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) {
+ if (!(value instanceof DefaultMutableTreeNode)) {
+ return;
+ }
+ if (value instanceof RepositoryNode) {
+ //todo simplify, remove instance of
+ myCheckbox.setVisible(((RepositoryNode)value).isCheckboxVisible());
+ }
+ Object userObject = ((DefaultMutableTreeNode)value).getUserObject();
+ ColoredTreeCellRenderer renderer = getTextRenderer();
+ renderer.setBorder(null);
+ if (value instanceof CustomRenderedTreeNode) {
+ ((CustomRenderedTreeNode)value).render(renderer);
+ }
+ else {
+ renderer.append(userObject == null ? "" : userObject.toString());
+ }
+ }
+ }
+
+ public void setChildren(DefaultMutableTreeNode parentNode, @NotNull Collection<? extends DefaultMutableTreeNode> childrenNodes) {
+ setChildren(parentNode, childrenNodes, true);
+ }
+
+ public void setChildren(DefaultMutableTreeNode parentNode,
+ @NotNull Collection<? extends DefaultMutableTreeNode> childrenNodes,
+ boolean shouldExpand) {
+ try {
+ TREE_CONSTRUCTION_LOCK.writeLock().lock();
+ parentNode.removeAllChildren();
+ for (DefaultMutableTreeNode child : childrenNodes) {
+ parentNode.add(child);
+ }
+ final DefaultTreeModel model = ((DefaultTreeModel)myTree.getModel());
+ model.nodeStructureChanged(parentNode);
+ TreePath path = TreeUtil.getPathFromRoot(parentNode);
+ if (shouldExpand) {
+ myTree.expandPath(path);
+ }
+ else {
+ myTree.collapsePath(path);
+ }
+ }
+ finally {
+ TREE_CONSTRUCTION_LOCK.writeLock().unlock();
+ }
+ }
+
+ @Nullable
+ public JComponent startEditNode(@NotNull TreeNode node) {
+ TreePath path = TreeUtil.getPathFromRoot(node);
+ if (!myTree.isEditing()) {
+ myTree.startEditingAtPath(path);
+ }
+ return (JComponent)myTree.getCellEditor()
+ .getTreeCellEditorComponent(myTree, node, false, false, false, myTree.getRowForPath(path));
+ }
+}
diff --git a/platform/dvcs-impl/src/com/intellij/dvcs/push/ui/PushLogTreeUtil.java b/platform/dvcs-impl/src/com/intellij/dvcs/push/ui/PushLogTreeUtil.java
new file mode 100644
index 000000000000..c7da9247ae74
--- /dev/null
+++ b/platform/dvcs-impl/src/com/intellij/dvcs/push/ui/PushLogTreeUtil.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2000-2014 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 com.intellij.dvcs.push.ui;
+
+import com.intellij.ui.CheckboxTree;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import javax.swing.tree.TreeNode;
+import javax.swing.tree.TreePath;
+import java.awt.*;
+import java.awt.event.MouseEvent;
+
+public class PushLogTreeUtil {
+
+ @Nullable
+ public static Object getTagAtForRenderer(CheckboxTree.CheckboxTreeCellRenderer renderer, MouseEvent e) {
+ JTree tree = (JTree)e.getSource();
+ Object tag = null;
+ final TreePath path = tree.getPathForLocation(e.getX(), e.getY());
+ if (path != null) {
+ final Rectangle rectangle = tree.getPathBounds(path);
+ assert rectangle != null;
+ int dx = e.getX() - rectangle.x;
+ final TreeNode treeNode = (TreeNode)path.getLastPathComponent();
+ final int row = tree.getRowForLocation(e.getX(), e.getY());
+ tree.getCellRenderer().getTreeCellRendererComponent(tree, treeNode, false, false, true, row, true);
+ if (treeNode instanceof RepositoryNode) {
+ RepositoryNode repositoryNode = (RepositoryNode)treeNode;
+ int checkBoxWidth = repositoryNode.isCheckboxVisible() ? renderer.getCheckbox().getWidth() : 0;
+ tag = renderer.getTextRenderer().getFragmentTagAt(dx - checkBoxWidth);
+ }
+ else {
+ tag = renderer.getTextRenderer().getFragmentTagAt(dx);
+ }
+ }
+ return tag;
+ }
+}
diff --git a/platform/dvcs-impl/src/com/intellij/dvcs/push/ui/RepositoryNode.java b/platform/dvcs-impl/src/com/intellij/dvcs/push/ui/RepositoryNode.java
new file mode 100644
index 000000000000..24a26ece96d9
--- /dev/null
+++ b/platform/dvcs-impl/src/com/intellij/dvcs/push/ui/RepositoryNode.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2000-2014 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 com.intellij.dvcs.push.ui;
+
+import com.intellij.openapi.progress.EmptyProgressIndicator;
+import com.intellij.openapi.progress.ProgressIndicator;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.ui.CheckedTreeNode;
+import com.intellij.ui.ColoredTreeCellRenderer;
+import com.intellij.ui.EditorTextField;
+import com.intellij.ui.SimpleTextAttributes;
+import org.jetbrains.annotations.NotNull;
+
+import javax.swing.*;
+import javax.swing.border.EmptyBorder;
+import java.awt.*;
+
+public class RepositoryNode extends CheckedTreeNode implements EditableTreeNode {
+ protected final static String ENTER_REMOTE = "Enter Remote";
+ @NotNull private final RepositoryWithBranchPanel myRepositoryPanel;
+
+ private ProgressIndicator myCurrentIndicator;
+
+ public RepositoryNode(@NotNull RepositoryWithBranchPanel repositoryPanel) {
+ super(repositoryPanel);
+ myRepositoryPanel = repositoryPanel;
+ }
+
+ public boolean isCheckboxVisible() {
+ return true;
+ }
+
+ @Override
+ public void render(@NotNull ColoredTreeCellRenderer renderer) {
+ String repositoryPath = myRepositoryPanel.getRepositoryName();
+ renderer.append(repositoryPath, SimpleTextAttributes.GRAY_ATTRIBUTES);
+ renderer.appendFixedTextFragmentWidth(120);
+ renderer.append(myRepositoryPanel.getSourceName(), SimpleTextAttributes.REGULAR_ATTRIBUTES);
+ renderer.append(myRepositoryPanel.getArrow(), SimpleTextAttributes.REGULAR_ATTRIBUTES);
+ EditorTextField textField = myRepositoryPanel.getRemoteTextFiled();
+ renderTargetName(renderer, textField, myRepositoryPanel.getRemoteTargetName());
+ Insets insets = BorderFactory.createEmptyBorder().getBorderInsets(textField);
+ renderer.setBorder(new EmptyBorder(insets));
+ }
+
+ protected void renderTargetName(@NotNull ColoredTreeCellRenderer renderer, @NotNull EditorTextField textField,
+ @NotNull String targetName) {
+ if (StringUtil.isEmptyOrSpaces(targetName)) {
+ renderer.append(ENTER_REMOTE, SimpleTextAttributes.GRAY_ITALIC_ATTRIBUTES, textField);
+ }
+ else {
+ renderer.append(targetName, SimpleTextAttributes.SYNTHETIC_ATTRIBUTES, textField);
+ }
+ }
+
+ @Override
+ @NotNull
+ public String getValue() {
+ return myRepositoryPanel.getRemoteTargetName();
+ }
+
+ @Override
+ public void fireOnChange(@NotNull String value) {
+ myRepositoryPanel.fireOnChange(value);
+ }
+
+ @Override
+ public void fireOnSelectionChange(boolean isSelected) {
+ myRepositoryPanel.fireOnSelectionChange(isSelected);
+ }
+
+ @Override
+ public void stopLoading() {
+ if (myCurrentIndicator != null && myCurrentIndicator.isRunning()) {
+ myCurrentIndicator.cancel();
+ }
+ }
+
+ @Override
+ @NotNull
+ public ProgressIndicator startLoading() {
+ return myCurrentIndicator = new EmptyProgressIndicator();
+ }
+}
diff --git a/platform/dvcs-impl/src/com/intellij/dvcs/push/ui/RepositoryWithBranchPanel.java b/platform/dvcs-impl/src/com/intellij/dvcs/push/ui/RepositoryWithBranchPanel.java
new file mode 100644
index 000000000000..c1afcdd52839
--- /dev/null
+++ b/platform/dvcs-impl/src/com/intellij/dvcs/push/ui/RepositoryWithBranchPanel.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright 2000-2014 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 com.intellij.dvcs.push.ui;
+
+import com.intellij.dvcs.push.RepositoryNodeListener;
+import com.intellij.openapi.editor.ex.EditorEx;
+import com.intellij.openapi.project.Project;
+import com.intellij.ui.ColoredTreeCellRenderer;
+import com.intellij.ui.SimpleTextAttributes;
+import com.intellij.ui.TextFieldWithAutoCompletion;
+import com.intellij.ui.TextFieldWithAutoCompletionListProvider;
+import com.intellij.ui.components.JBCheckBox;
+import com.intellij.ui.components.JBLabel;
+import com.intellij.ui.components.panels.NonOpaquePanel;
+import com.intellij.util.containers.ContainerUtil;
+import com.intellij.util.ui.UIUtil;
+import org.jetbrains.annotations.NotNull;
+
+import javax.swing.*;
+import javax.swing.tree.TreeCellRenderer;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.FocusAdapter;
+import java.awt.event.FocusEvent;
+import java.util.Collection;
+import java.util.List;
+
+public class RepositoryWithBranchPanel extends NonOpaquePanel implements TreeCellRenderer {
+
+ private final JBCheckBox myRepositoryCheckbox;
+ private final TextFieldWithAutoCompletion myDestBranchTextField;
+ private final JBLabel myLocalBranch;
+ private final JLabel myArrowLabel;
+ private final JLabel myRepositoryLabel;
+ private final ColoredTreeCellRenderer myTextRenderer;
+ @NotNull private final List<RepositoryNodeListener> myListeners = ContainerUtil.createLockFreeCopyOnWriteList();
+
+ public RepositoryWithBranchPanel(Project project, @NotNull String repoName,
+ @NotNull String sourceName, String targetName, @NotNull Collection<String> targetVariants) {
+ super();
+ setLayout(new BorderLayout());
+ myRepositoryCheckbox = new JBCheckBox();
+ myRepositoryCheckbox.setOpaque(false);
+ myRepositoryCheckbox.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ fireOnSelectionChange(myRepositoryCheckbox.isSelected());
+ }
+ });
+ myRepositoryLabel = new JLabel(repoName);
+ myLocalBranch = new JBLabel(sourceName);
+ myArrowLabel = new JLabel(" -> ");
+ TextFieldWithAutoCompletionListProvider<String> provider =
+ new TextFieldWithAutoCompletion.StringsCompletionProvider(targetVariants, null);
+ myDestBranchTextField = new TextFieldWithAutoCompletion<String>(project, provider, true, targetName) {
+
+ @Override
+ public boolean shouldHaveBorder() {
+ return false;
+ }
+
+ @Override
+ protected void updateBorder(@NotNull final EditorEx editor) {
+ }
+ };
+ myDestBranchTextField.setBorder(UIUtil.getTableFocusCellHighlightBorder());//getTextFieldBorder());
+ myDestBranchTextField.setOneLineMode(true);
+ myDestBranchTextField.setOpaque(true);
+ myDestBranchTextField.addFocusListener(new FocusAdapter() {
+ @Override
+ public void focusGained(FocusEvent e) {
+ myDestBranchTextField.selectAll();
+ }
+ });
+
+ myTextRenderer = new ColoredTreeCellRenderer() {
+ public void customizeCellRenderer(@NotNull JTree tree,
+ Object value,
+ boolean selected,
+ boolean expanded,
+ boolean leaf,
+ int row,
+ boolean hasFocus) {
+
+ }
+ };
+ myTextRenderer.setOpaque(false);
+ layoutComponents();
+ }
+
+ private void layoutComponents() {
+ add(myRepositoryCheckbox, BorderLayout.WEST);
+ JPanel panel = new NonOpaquePanel(new BorderLayout());
+ panel.add(myTextRenderer, BorderLayout.WEST);
+ panel.add(myDestBranchTextField, BorderLayout.CENTER);
+ add(panel, BorderLayout.CENTER);
+ }
+
+ @NotNull
+ public String getRepositoryName() {
+ return myRepositoryLabel.getText();
+ }
+
+ public String getSourceName() {
+ return myLocalBranch.getText();
+ }
+
+ public String getArrow() {
+ return myArrowLabel.getText();
+ }
+
+ public TextFieldWithAutoCompletion getRemoteTextFiled() {
+ return myDestBranchTextField;
+ }
+
+ @NotNull
+ public String getRemoteTargetName() {
+ return myDestBranchTextField.getText();
+ }
+
+ @Override
+ public Component getTreeCellRendererComponent(JTree tree,
+ Object value,
+ boolean selected,
+ boolean expanded,
+ boolean leaf,
+ int row,
+ boolean hasFocus) {
+ Rectangle bounds = tree.getPathBounds(tree.getPathForRow(row));
+ invalidate();
+ if (!(value instanceof SingleRepositoryNode)) {
+ RepositoryNode node = (RepositoryNode)value;
+ myRepositoryCheckbox.setSelected(node.isChecked());
+ myRepositoryCheckbox.setVisible(true);
+ myTextRenderer.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus);
+ myTextRenderer.append(getRepositoryName(), SimpleTextAttributes.GRAY_ATTRIBUTES);
+ myTextRenderer.appendFixedTextFragmentWidth(120);
+ }
+ else {
+ myTextRenderer.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus);
+ myRepositoryCheckbox.setVisible(false);
+ }
+ myTextRenderer.append(getSourceName(), SimpleTextAttributes.REGULAR_ATTRIBUTES);
+ myTextRenderer.append(getArrow(), SimpleTextAttributes.REGULAR_ATTRIBUTES);
+ if (bounds != null) {
+ setPreferredSize(new Dimension(tree.getWidth() - bounds.x, bounds.height));
+ }
+ myDestBranchTextField.grabFocus();
+ myDestBranchTextField.requestFocus();
+ revalidate();
+ return this;
+ }
+
+ public void addRepoNodeListener(@NotNull RepositoryNodeListener listener) {
+ myListeners.add(listener);
+ }
+
+ public void fireOnChange(@NotNull String newValue) {
+ for (RepositoryNodeListener listener : myListeners) {
+ listener.onTargetChanged(newValue);
+ }
+ }
+
+ public void fireOnSelectionChange(boolean isSelected) {
+ for (RepositoryNodeListener listener : myListeners) {
+ listener.onSelectionChanged(isSelected);
+ }
+ }
+}
+
+
diff --git a/platform/dvcs-impl/src/com/intellij/dvcs/push/ui/SingleRepositoryNode.java b/platform/dvcs-impl/src/com/intellij/dvcs/push/ui/SingleRepositoryNode.java
new file mode 100644
index 000000000000..c566dd6f2867
--- /dev/null
+++ b/platform/dvcs-impl/src/com/intellij/dvcs/push/ui/SingleRepositoryNode.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2000-2014 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 com.intellij.dvcs.push.ui;
+
+import com.intellij.ui.ColoredTreeCellRenderer;
+import com.intellij.ui.EditorTextField;
+import com.intellij.ui.SimpleTextAttributes;
+import org.jetbrains.annotations.NotNull;
+
+import javax.swing.*;
+import javax.swing.border.EmptyBorder;
+import java.awt.*;
+
+public class SingleRepositoryNode extends RepositoryNode {
+
+ @NotNull private final RepositoryWithBranchPanel myRepositoryPanel;
+
+ public SingleRepositoryNode(@NotNull RepositoryWithBranchPanel repositoryPanel) {
+ super(repositoryPanel);
+ myRepositoryPanel = repositoryPanel;
+ }
+
+ @Override
+ public boolean isCheckboxVisible() {
+ return false;
+ }
+
+ @Override
+ public void render(@NotNull ColoredTreeCellRenderer renderer) {
+ renderer.append(myRepositoryPanel.getSourceName(), SimpleTextAttributes.REGULAR_ATTRIBUTES);
+ renderer.append(myRepositoryPanel.getArrow(), SimpleTextAttributes.REGULAR_ATTRIBUTES);
+ EditorTextField textField = myRepositoryPanel.getRemoteTextFiled();
+ String targetName = myRepositoryPanel.getRemoteTargetName();
+ renderTargetName(renderer, textField, targetName);
+ Insets insets = BorderFactory.createEmptyBorder().getBorderInsets(textField);
+ renderer.setBorder(new EmptyBorder(insets));
+ }
+
+}
diff --git a/platform/dvcs-impl/src/com/intellij/dvcs/push/ui/TextWithLinkNode.java b/platform/dvcs-impl/src/com/intellij/dvcs/push/ui/TextWithLinkNode.java
new file mode 100644
index 000000000000..685fcb81fd05
--- /dev/null
+++ b/platform/dvcs-impl/src/com/intellij/dvcs/push/ui/TextWithLinkNode.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2000-2014 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 com.intellij.dvcs.push.ui;
+
+
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.ui.ColoredTreeCellRenderer;
+import com.intellij.ui.SimpleTextAttributes;
+import org.jetbrains.annotations.NotNull;
+
+import javax.swing.tree.DefaultMutableTreeNode;
+import javax.swing.tree.TreeNode;
+
+public class TextWithLinkNode extends DefaultMutableTreeNode implements CustomRenderedTreeNode {
+
+ @NotNull protected VcsLinkedText myLinkedText;
+
+ public TextWithLinkNode(@NotNull VcsLinkedText linkedText) {
+ myLinkedText = linkedText;
+ }
+
+ public void fireOnClick(@NotNull TextWithLinkNode relatedNode) {
+ TreeNode parent = relatedNode.getParent();
+ if (parent instanceof RepositoryNode) {
+ myLinkedText.hyperLinkActivate((RepositoryNode)parent);
+ }
+ }
+
+ @Override
+ public void render(@NotNull ColoredTreeCellRenderer renderer) {
+ renderer.append(myLinkedText.getTextBefore(), SimpleTextAttributes.REGULAR_ATTRIBUTES);
+ String linkedText = myLinkedText.getLinkText();
+ if (!StringUtil.isEmptyOrSpaces(linkedText)) {
+ renderer.append(" ");
+ renderer.append(myLinkedText.getLinkText(), SimpleTextAttributes.SYNTHETIC_ATTRIBUTES, this);
+ }
+ renderer.append(myLinkedText.getTextAfter(), SimpleTextAttributes.REGULAR_ATTRIBUTES);
+ }
+} \ No newline at end of file
diff --git a/platform/dvcs-impl/src/com/intellij/dvcs/push/ui/TooltipNode.java b/platform/dvcs-impl/src/com/intellij/dvcs/push/ui/TooltipNode.java
new file mode 100644
index 000000000000..fef83cc12f63
--- /dev/null
+++ b/platform/dvcs-impl/src/com/intellij/dvcs/push/ui/TooltipNode.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2000-2014 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 com.intellij.dvcs.push.ui;
+
+public interface TooltipNode {
+
+ String getTooltip();
+}
diff --git a/platform/dvcs-impl/src/com/intellij/dvcs/push/ui/VcsBranchEditorListener.java b/platform/dvcs-impl/src/com/intellij/dvcs/push/ui/VcsBranchEditorListener.java
new file mode 100644
index 000000000000..d8e35fd25d53
--- /dev/null
+++ b/platform/dvcs-impl/src/com/intellij/dvcs/push/ui/VcsBranchEditorListener.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2000-2014 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 com.intellij.dvcs.push.ui;
+
+import com.intellij.openapi.vcs.changes.issueLinks.LinkMouseListenerBase;
+import com.intellij.ui.CheckboxTree;
+import com.intellij.ui.EditorTextField;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.awt.*;
+import java.awt.event.MouseEvent;
+
+public class VcsBranchEditorListener extends LinkMouseListenerBase {
+ private final CheckboxTree.CheckboxTreeCellRenderer myRenderer;
+
+ public VcsBranchEditorListener(final CheckboxTree.CheckboxTreeCellRenderer renderer) {
+ myRenderer = renderer;
+ }
+
+ @Override
+ public void mouseMoved(MouseEvent e) {
+ Component component = (Component)e.getSource();
+ Object tag = getTagAt(e);
+ if (tag != null && tag instanceof EditorTextField) {
+ component.setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR));
+ }
+ else if (tag != null && tag instanceof TextWithLinkNode) {
+ component.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
+ }
+ else {
+ component.setCursor(Cursor.getDefaultCursor());
+ }
+ }
+
+ @Nullable
+ @Override
+ protected Object getTagAt(@NotNull final MouseEvent e) {
+ return PushLogTreeUtil.getTagAtForRenderer(myRenderer, e);
+ }
+
+ protected void handleTagClick(@Nullable Object tag, @NotNull MouseEvent event) {
+ if (tag instanceof TextWithLinkNode) {
+ TextWithLinkNode textWithLink = (TextWithLinkNode)tag;
+ textWithLink.fireOnClick(textWithLink);
+ }
+ }
+}
diff --git a/platform/dvcs-impl/src/com/intellij/dvcs/push/ui/VcsFullCommitDetailsNode.java b/platform/dvcs-impl/src/com/intellij/dvcs/push/ui/VcsFullCommitDetailsNode.java
new file mode 100644
index 000000000000..0df7af05d496
--- /dev/null
+++ b/platform/dvcs-impl/src/com/intellij/dvcs/push/ui/VcsFullCommitDetailsNode.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2000-2014 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 com.intellij.dvcs.push.ui;
+
+import com.intellij.dvcs.DvcsUtil;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vcs.changes.issueLinks.IssueLinkHtmlRenderer;
+import com.intellij.ui.ColoredTreeCellRenderer;
+import com.intellij.ui.SimpleTextAttributes;
+import com.intellij.vcs.log.VcsFullCommitDetails;
+import org.jetbrains.annotations.NotNull;
+
+import javax.swing.tree.DefaultMutableTreeNode;
+
+public class VcsFullCommitDetailsNode extends DefaultMutableTreeNode implements CustomRenderedTreeNode, TooltipNode {
+
+ @NotNull private final Project myProject;
+ private final VcsFullCommitDetails myCommit;
+
+ public VcsFullCommitDetailsNode(@NotNull Project project, VcsFullCommitDetails commit) {
+ super(commit, false);
+ myProject = project;
+ myCommit = commit;
+ }
+
+ @Override
+ public void render(@NotNull ColoredTreeCellRenderer renderer) {
+ renderer
+ .append(myCommit.getSubject(), new SimpleTextAttributes(SimpleTextAttributes.STYLE_SMALLER, renderer.getForeground()));
+ }
+
+ public String getTooltip() {
+ return DvcsUtil.getShortHash(myCommit.getId().toString()) +
+ " " +
+ DvcsUtil.getDateString(myCommit) +
+ " by " +
+ myCommit.getAuthor().getName() +
+ "\n\n" +
+ IssueLinkHtmlRenderer.formatTextWithLinks(myProject, myCommit.getFullMessage());
+ }
+}
diff --git a/platform/dvcs-impl/src/com/intellij/dvcs/push/ui/VcsLinkListener.java b/platform/dvcs-impl/src/com/intellij/dvcs/push/ui/VcsLinkListener.java
new file mode 100644
index 000000000000..76578165b37c
--- /dev/null
+++ b/platform/dvcs-impl/src/com/intellij/dvcs/push/ui/VcsLinkListener.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2000-2014 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 com.intellij.dvcs.push.ui;
+
+import org.jetbrains.annotations.NotNull;
+
+import javax.swing.tree.DefaultMutableTreeNode;
+
+public interface VcsLinkListener {
+ void hyperlinkActivated(@NotNull DefaultMutableTreeNode sourceNode);
+}
diff --git a/platform/dvcs-impl/src/com/intellij/dvcs/push/ui/VcsLinkedText.java b/platform/dvcs-impl/src/com/intellij/dvcs/push/ui/VcsLinkedText.java
new file mode 100644
index 000000000000..35c4989f160e
--- /dev/null
+++ b/platform/dvcs-impl/src/com/intellij/dvcs/push/ui/VcsLinkedText.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2000-2014 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 com.intellij.dvcs.push.ui;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.tree.DefaultMutableTreeNode;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class VcsLinkedText {
+
+ private static final Pattern HREF_PATTERN = Pattern.compile("<a(?:\\s+href\\s*=\\s*[\"']([^\"']*)[\"'])?\\s*>([^<]*)</a>");
+
+ @NotNull private String myTextBefore;
+ @NotNull private String myTextAfter;
+ @NotNull private String myHandledLink;
+
+ @Nullable private final VcsLinkListener myLinkListener;
+
+ public VcsLinkedText(@NotNull String text, @Nullable VcsLinkListener listener) {
+ Matcher aMatcher = HREF_PATTERN.matcher(text);
+ if (aMatcher.find()) {
+ myTextBefore = text.substring(0, aMatcher.start());
+ myHandledLink = aMatcher.group(2);
+ myTextAfter = text.substring(aMatcher.end(), text.length());
+ }
+ else {
+ myTextBefore = text;
+ myHandledLink = "";
+ myTextAfter = "";
+ }
+ myLinkListener = listener;
+ }
+
+ @NotNull
+ public String getTextBefore() {
+ return myTextBefore;
+ }
+
+ @NotNull
+ public String getTextAfter() {
+ return myTextAfter;
+ }
+
+ @NotNull
+ public String getLinkText() {
+ return myHandledLink;
+ }
+
+ public void hyperLinkActivate(@NotNull DefaultMutableTreeNode relatedNode) {
+ if (myLinkListener != null) {
+ myLinkListener.hyperlinkActivated(relatedNode);
+ }
+ }
+}
diff --git a/platform/dvcs-impl/src/com/intellij/dvcs/push/ui/VcsPushDialog.java b/platform/dvcs-impl/src/com/intellij/dvcs/push/ui/VcsPushDialog.java
new file mode 100644
index 000000000000..b441952f7a43
--- /dev/null
+++ b/platform/dvcs-impl/src/com/intellij/dvcs/push/ui/VcsPushDialog.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2000-2014 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 com.intellij.dvcs.push.ui;
+
+import com.intellij.dvcs.push.PushController;
+import com.intellij.dvcs.push.VcsPushOptionsPanel;
+import com.intellij.dvcs.repo.Repository;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.DialogWrapper;
+import com.intellij.openapi.ui.OptionAction;
+import com.intellij.openapi.ui.ValidationInfo;
+import net.miginfocom.swing.MigLayout;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.util.ArrayList;
+import java.util.List;
+
+public class VcsPushDialog extends DialogWrapper {
+
+ private final PushLog myListPanel;
+ private final PushController myController;
+ private final Action[] myExecutorActions = {new DvcsPushAction("&Force Push", true)};
+ @NotNull private final JPanel myAdditionalOptionsFromVcsPanel;
+
+ private DvcsPushAction myPushAction;
+
+ public VcsPushDialog(@NotNull Project project, @NotNull List<? extends Repository> selectedRepositories) {
+ super(project);
+ myController = new PushController(project, this, selectedRepositories);
+ myListPanel = myController.getPushPanelInfo();
+ myAdditionalOptionsFromVcsPanel = new JPanel(new MigLayout("ins 0 0, flowx"));
+ init();
+ setOKButtonText("Push");
+ setOKButtonMnemonic('P');
+ setTitle("Push Dialog");
+ }
+
+ @Override
+ protected JComponent createCenterPanel() {
+
+ JComponent rootPanel = new JPanel(new BorderLayout(0, 15));
+ rootPanel.add(myListPanel, BorderLayout.CENTER);
+ for (VcsPushOptionsPanel panel : myController.getAdditionalPanels()) {
+ myAdditionalOptionsFromVcsPanel.add(panel);
+ }
+ rootPanel.add(myAdditionalOptionsFromVcsPanel, BorderLayout.SOUTH);
+ return rootPanel;
+ }
+
+ @Override
+ protected String getDimensionServiceKey() {
+ return VcsPushDialog.class.getName();
+ }
+
+ @Override
+ @NotNull
+ protected Action[] createActions() {
+ final List<Action> actions = new ArrayList<Action>();
+ myPushAction = new DvcsPushAction("&Push", false);
+ myPushAction.putValue(DEFAULT_ACTION, Boolean.TRUE);
+ actions.add(myPushAction);
+ myPushAction.setOptions(myExecutorActions);
+ actions.add(getCancelAction());
+ actions.add(getHelpAction());
+ return actions.toArray(new Action[actions.size()]);
+ }
+
+ @NotNull
+ @Override
+ protected Action getOKAction() {
+ return myPushAction;
+ }
+
+ @Nullable
+ @Override
+ protected ValidationInfo doValidate() {
+ return myController.validate();
+ }
+
+ @Override
+ protected String getHelpId() {
+ return "reference.mercurial.push.dialog";
+ }
+
+ public void updateButtons() {
+ initValidation();
+ }
+
+ @Override
+ protected boolean postponeValidation() {
+ return false;
+ }
+
+ private class DvcsPushAction extends AbstractAction implements OptionAction {
+ private Action[] myOptions = new Action[0];
+ private final boolean myForce;
+
+ private DvcsPushAction(String title, boolean force) {
+ super(title);
+ myForce = force;
+ }
+
+ @Override
+ public void setEnabled(boolean isEnabled) {
+ super.setEnabled(isEnabled);
+ for (Action optionAction : myOptions) {
+ optionAction.setEnabled(isEnabled);
+ }
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ myController.push(myForce);
+ close(OK_EXIT_CODE);
+ }
+
+ @NotNull
+ @Override
+ public Action[] getOptions() {
+ return myOptions;
+ }
+
+ public void setOptions(Action[] actions) {
+ myOptions = actions;
+ }
+ }
+}
diff --git a/platform/dvcs-impl/src/com/intellij/dvcs/repo/AbstractRepositoryManager.java b/platform/dvcs-impl/src/com/intellij/dvcs/repo/AbstractRepositoryManager.java
new file mode 100644
index 000000000000..992654248e83
--- /dev/null
+++ b/platform/dvcs-impl/src/com/intellij/dvcs/repo/AbstractRepositoryManager.java
@@ -0,0 +1,223 @@
+package com.intellij.dvcs.repo;
+
+import com.intellij.openapi.Disposable;
+import com.intellij.openapi.components.AbstractProjectComponent;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Disposer;
+import com.intellij.openapi.vcs.*;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.util.ArrayUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+/**
+ * @author Nadya Zabrodina
+ */
+public abstract class AbstractRepositoryManager<T extends Repository> extends AbstractProjectComponent
+ implements Disposable, RepositoryManager<T>, VcsListener {
+
+ private static final Logger LOG = Logger.getInstance(RepositoryManager.class);
+
+ @NotNull private final ProjectLevelVcsManager myVcsManager;
+ @NotNull private final AbstractVcs myVcs;
+ @NotNull private final String myRepoDirName;
+
+ @NotNull protected final Map<VirtualFile, T> myRepositories = new HashMap<VirtualFile, T>();
+
+ @NotNull protected final ReentrantReadWriteLock REPO_LOCK = new ReentrantReadWriteLock();
+ @NotNull private final CountDownLatch myInitializationWaiter = new CountDownLatch(1);
+
+ protected AbstractRepositoryManager(@NotNull Project project, @NotNull ProjectLevelVcsManager vcsManager, @NotNull AbstractVcs vcs,
+ @NotNull String repoDirName) {
+ super(project);
+ myVcsManager = vcsManager;
+ myVcs = vcs;
+ myRepoDirName = repoDirName;
+ }
+
+ @Override
+ public void initComponent() {
+ Disposer.register(myProject, this);
+ myProject.getMessageBus().connect().subscribe(ProjectLevelVcsManager.VCS_CONFIGURATION_CHANGED, this);
+ }
+
+ @Override
+ public void dispose() {
+ try {
+ REPO_LOCK.writeLock().lock();
+ myRepositories.clear();
+ }
+ finally {
+ REPO_LOCK.writeLock().unlock();
+ }
+ }
+
+ @Override
+ public void directoryMappingChanged() {
+ updateRepositoriesCollection();
+ }
+
+ @Override
+ @Nullable
+ public T getRepositoryForRoot(@Nullable VirtualFile root) {
+ if (root == null) {
+ return null;
+ }
+ try {
+ REPO_LOCK.readLock().lock();
+ return myRepositories.get(root);
+ }
+ finally {
+ REPO_LOCK.readLock().unlock();
+ }
+ }
+
+ @Override
+ @Nullable
+ public T getRepositoryForFile(@NotNull VirtualFile file) {
+ final VcsRoot vcsRoot = myVcsManager.getVcsRootObjectFor(file);
+ return getRepositoryForVcsRoot(vcsRoot, file.getPath());
+ }
+
+ @Override
+ public T getRepositoryForFile(@NotNull FilePath file) {
+ final VcsRoot vcsRoot = myVcsManager.getVcsRootObjectFor(file);
+ return getRepositoryForVcsRoot(vcsRoot, file.getPath());
+ }
+
+ @Nullable
+ private T getRepositoryForVcsRoot(@Nullable VcsRoot vcsRoot, @NotNull String filePath) {
+ if (vcsRoot == null) {
+ return null;
+ }
+ final AbstractVcs vcs = vcsRoot.getVcs();
+ if (!myVcs.equals(vcs)) {
+ if (vcs != null) {
+ LOG.debug(String.format("getRepositoryForFile returned non-(%s) root for file %s", myVcs.getDisplayName(), filePath));
+ }
+ return null;
+ }
+ return getRepositoryForRoot(vcsRoot.getPath());
+ }
+
+ @Override
+ @NotNull
+ public List<T> getRepositories() {
+ try {
+ REPO_LOCK.readLock().lock();
+ return RepositoryUtil.sortRepositories(myRepositories.values());
+ }
+ finally {
+ REPO_LOCK.readLock().unlock();
+ }
+ }
+
+ @Override
+ public boolean moreThanOneRoot() {
+ return myRepositories.size() > 1;
+ }
+
+ @Override
+ public void updateRepository(@Nullable VirtualFile root) {
+ T repo = getRepositoryForRoot(root);
+ if (repo != null) {
+ repo.update();
+ }
+ }
+
+ @Override
+ public void updateAllRepositories() {
+ Map<VirtualFile, T> repositories;
+ try {
+ REPO_LOCK.readLock().lock();
+ repositories = new HashMap<VirtualFile, T>(myRepositories);
+ }
+ finally {
+ REPO_LOCK.readLock().unlock();
+ }
+
+ for (VirtualFile root : repositories.keySet()) {
+ updateRepository(root);
+ }
+ }
+
+ // note: we are not calling this method during the project startup - it is called anyway by f.e the GitRootTracker
+ private void updateRepositoriesCollection() {
+ Map<VirtualFile, T> repositories;
+ try {
+ REPO_LOCK.readLock().lock();
+ repositories = new HashMap<VirtualFile, T>(myRepositories);
+ }
+ finally {
+ REPO_LOCK.readLock().unlock();
+ }
+
+ final VirtualFile[] roots = myVcsManager.getRootsUnderVcs(myVcs);
+ // remove repositories that are not in the roots anymore
+ for (Iterator<Map.Entry<VirtualFile, T>> iterator = repositories.entrySet().iterator(); iterator.hasNext(); ) {
+ if (!ArrayUtil.contains(iterator.next().getValue().getRoot(), roots)) {
+ iterator.remove();
+ }
+ }
+ // add Repositories for all roots that don't have correspondent appropriate Git or Hg Repositories yet.
+ for (VirtualFile root : roots) {
+ if (!repositories.containsKey(root)) {
+ if (isRootValid(root)) {
+ try {
+ T repository = createRepository(root);
+ repositories.put(root, repository);
+ }
+ catch (RepoStateException e) {
+ LOG.error("Couldn't initialize Repository in " + root.getPresentableUrl(), e);
+ }
+ }
+ else {
+ LOG.info("Invalid vcs root: " + root);
+ }
+ }
+ }
+
+ REPO_LOCK.writeLock().lock();
+ try {
+ myRepositories.clear();
+ myRepositories.putAll(repositories);
+ myInitializationWaiter.countDown();
+ }
+ finally {
+ REPO_LOCK.writeLock().unlock();
+ }
+ }
+
+ private boolean isRootValid(@NotNull VirtualFile root) {
+ VirtualFile vcsDir = root.findChild(myRepoDirName);
+ return vcsDir != null && vcsDir.exists();
+ }
+
+ @NotNull
+ protected abstract T createRepository(@NotNull VirtualFile root);
+
+ @Override
+ @NotNull
+ public String toString() {
+ return "RepositoryManager{myRepositories: " + myRepositories + '}';
+ }
+
+ @Override
+ public void waitUntilInitialized() {
+ try {
+ myInitializationWaiter.await();
+ }
+ catch (InterruptedException e) {
+ LOG.error(e);
+ }
+ }
+
+}
diff --git a/platform/dvcs-impl/src/com/intellij/dvcs/repo/RepoStateException.java b/platform/dvcs-impl/src/com/intellij/dvcs/repo/RepoStateException.java
new file mode 100644
index 000000000000..cfe1f0e5c014
--- /dev/null
+++ b/platform/dvcs-impl/src/com/intellij/dvcs/repo/RepoStateException.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2000-2013 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 com.intellij.dvcs.repo;
+
+/**
+ * @author Nadya Zabrodina
+ */
+public class RepoStateException extends RuntimeException {
+
+ public RepoStateException(String message) {
+ super(message);
+ }
+
+ public RepoStateException(String message, Throwable cause) {
+ super(message, cause);
+ }
+} \ No newline at end of file
diff --git a/platform/dvcs-impl/src/com/intellij/dvcs/repo/RepositoryImpl.java b/platform/dvcs-impl/src/com/intellij/dvcs/repo/RepositoryImpl.java
new file mode 100644
index 000000000000..5a605c566710
--- /dev/null
+++ b/platform/dvcs-impl/src/com/intellij/dvcs/repo/RepositoryImpl.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2000-2013 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 com.intellij.dvcs.repo;
+
+import com.intellij.openapi.Disposable;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Disposer;
+import com.intellij.openapi.vfs.VirtualFile;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * @author Nadya Zabrodina
+ */
+public abstract class RepositoryImpl implements Repository {
+
+ @NotNull private final Project myProject;
+ @NotNull private final VirtualFile myRootDir;
+
+
+ @NotNull protected volatile State myState;
+ @Nullable protected volatile String myCurrentRevision;
+
+ protected RepositoryImpl(@NotNull Project project,
+ @NotNull VirtualFile dir,
+ @NotNull Disposable parentDisposable) {
+ myProject = project;
+ myRootDir = dir;
+ Disposer.register(parentDisposable, this);
+ }
+
+ @Override
+ @NotNull
+ public VirtualFile getRoot() {
+ return myRootDir;
+ }
+
+ @Override
+ @NotNull
+ public String getPresentableUrl() {
+ return getRoot().getPresentableUrl();
+ }
+
+ @Override
+ public String toString() {
+ return getPresentableUrl();
+ }
+
+ @Override
+ @NotNull
+ public Project getProject() {
+ return myProject;
+ }
+
+ @NotNull
+ @Override
+ public State getState() {
+ return myState;
+ }
+
+
+ @Override
+ @Nullable
+ public String getCurrentRevision() {
+ return myCurrentRevision;
+ }
+
+ @Override
+ public void dispose() {
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ Repository that = (Repository)o;
+
+ if (!getProject().equals(that.getProject())) return false;
+ if (!getRoot().equals(that.getRoot())) return false;
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = getProject().hashCode();
+ result = 31 * result + (getRoot().hashCode());
+ return result;
+ }
+}
+
+
diff --git a/platform/dvcs-impl/src/com/intellij/dvcs/repo/RepositoryUtil.java b/platform/dvcs-impl/src/com/intellij/dvcs/repo/RepositoryUtil.java
new file mode 100644
index 000000000000..5a5a555768ba
--- /dev/null
+++ b/platform/dvcs-impl/src/com/intellij/dvcs/repo/RepositoryUtil.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright 2000-2013 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 com.intellij.dvcs.repo;
+
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.util.Condition;
+import com.intellij.openapi.util.Disposer;
+import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.openapi.vfs.VfsUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.util.Consumer;
+import com.intellij.util.Processor;
+import com.intellij.util.containers.ContainerUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.concurrent.Callable;
+
+/**
+ * @author Nadya Zabrodina
+ */
+public class RepositoryUtil {
+
+ private static final Logger LOGGER = Logger.getInstance(RepositoryUtil.class);
+ private static final int IO_RETRIES = 3; // number of retries before fail if an IOException happens during file read.
+
+ public static void assertFileExists(File file, String message) {
+ if (!file.exists()) {
+ throw new RepoStateException(message);
+ }
+ }
+
+ /**
+ * Loads the file content.
+ * Tries 3 times, then a {@link RepoStateException} is thrown.
+ * Content is then trimmed and line separators get converted.
+ *
+ * @param file File to read.
+ * @return file content.
+ */
+ @NotNull
+ public static String tryLoadFile(@NotNull final File file) {
+ return tryOrThrow(new Callable<String>() {
+ @Override
+ public String call() throws Exception {
+ return StringUtil.convertLineSeparators(FileUtil.loadFile(file).trim());
+ }
+ }, file);
+ }
+
+ /**
+ * Tries to execute the given action.
+ * If an IOException happens, tries again up to 3 times, and then throws a {@link RepoStateException}.
+ * If an other exception happens, rethrows it as a {@link RepoStateException}.
+ * In the case of success returns the result of the task execution.
+ */
+ public static <T> T tryOrThrow(Callable<T> actionToTry, File fileToLoad) {
+ IOException cause = null;
+ for (int i = 0; i < IO_RETRIES; i++) {
+ try {
+ return actionToTry.call();
+ }
+ catch (IOException e) {
+ LOGGER.info("IOException while loading " + fileToLoad, e);
+ cause = e;
+ }
+ catch (Exception e) { // this shouldn't happen since only IOExceptions are thrown in clients.
+ throw new RepoStateException("Couldn't load file " + fileToLoad, e);
+ }
+ }
+ throw new RepoStateException("Couldn't load file " + fileToLoad, cause);
+ }
+
+ public static void visitVcsDirVfs(@NotNull VirtualFile gitDir, @NotNull Collection<String> subDirs) {
+ gitDir.getChildren();
+ for (String subdir : subDirs) {
+ VirtualFile dir = gitDir.findFileByRelativePath(subdir);
+ // process recursively, because we need to visit all branches under refs/heads and refs/remotes
+ visitAllChildrenRecursively(dir);
+ }
+ }
+
+ public static void visitAllChildrenRecursively(@Nullable VirtualFile dir) {
+ if (dir == null) {
+ return;
+ }
+ VfsUtil.processFilesRecursively(dir, new Processor<VirtualFile>() {
+ @Override
+ public boolean process(VirtualFile virtualFile) {
+ return true;
+ }
+ });
+ }
+
+ public static class Updater implements Consumer<Object> {
+ private final Repository myRepository;
+
+ public Updater(Repository repository) {
+ myRepository = repository;
+ }
+
+ @Override
+ public void consume(Object dummy) {
+ if (!Disposer.isDisposed(myRepository)) {
+ myRepository.update();
+ }
+ }
+ }
+
+ public static <T extends Repository> List<T> sortRepositories(@NotNull Collection<T> repositories) {
+ List<T> repos = ContainerUtil.filter(repositories, new Condition<T>() {
+ @Override
+ public boolean value(T t) {
+ return t.getRoot().isValid();
+ }
+ });
+ Collections.sort(repos, new Comparator<Repository>() {
+ @Override
+ public int compare(Repository o1, Repository o2) {
+ return o1.getPresentableUrl().compareTo(o2.getPresentableUrl());
+ }
+ });
+ return repos;
+ }
+}
diff --git a/platform/dvcs-impl/src/com/intellij/dvcs/ui/BranchActionGroupPopup.java b/platform/dvcs-impl/src/com/intellij/dvcs/ui/BranchActionGroupPopup.java
new file mode 100644
index 000000000000..44b4c8c5c3f0
--- /dev/null
+++ b/platform/dvcs-impl/src/com/intellij/dvcs/ui/BranchActionGroupPopup.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2000-2013 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 com.intellij.dvcs.ui;
+
+import com.intellij.openapi.actionSystem.ActionGroup;
+import com.intellij.openapi.actionSystem.AnAction;
+import com.intellij.openapi.actionSystem.impl.SimpleDataContext;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.popup.PopupStep;
+import com.intellij.openapi.util.Condition;
+import com.intellij.ui.ErrorLabel;
+import com.intellij.ui.JBColor;
+import com.intellij.ui.components.panels.OpaquePanel;
+import com.intellij.ui.popup.PopupFactoryImpl;
+import com.intellij.ui.popup.WizardPopup;
+import com.intellij.ui.popup.list.PopupListElementRenderer;
+import com.intellij.util.ui.UIUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import java.awt.*;
+
+public class BranchActionGroupPopup extends PopupFactoryImpl.ActionGroupPopup {
+ public BranchActionGroupPopup(@NotNull String title, @NotNull Project project,
+ @NotNull Condition<AnAction> preselectActionCondition, @NotNull ActionGroup actions) {
+ super(title, actions, SimpleDataContext.getProjectContext(project), false, false, true, false, null, -1,
+ preselectActionCondition, null);
+ }
+
+ @Override
+ protected WizardPopup createPopup(WizardPopup parent, PopupStep step, Object parentValue) {
+ WizardPopup popup = super.createPopup(parent, step, parentValue);
+ RootAction rootAction = getRootAction(parentValue);
+ if (rootAction != null) {
+ popup.setAdText((rootAction).getCaption());
+ }
+ return popup;
+ }
+
+ @Nullable
+ private static RootAction getRootAction(Object value) {
+ if (value instanceof PopupFactoryImpl.ActionItem) {
+ AnAction action = ((PopupFactoryImpl.ActionItem)value).getAction();
+ if (action instanceof RootAction) {
+ return (RootAction)action;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ protected ListCellRenderer getListElementRenderer() {
+ return new PopupListElementRenderer(this) {
+
+ private ErrorLabel myBranchLabel;
+
+ @Override
+ protected void customizeComponent(JList list, Object value, boolean isSelected) {
+ super.customizeComponent(list, value, isSelected);
+
+ RootAction rootAction = getRootAction(value);
+ if (rootAction != null) {
+ myBranchLabel.setVisible(true);
+ myBranchLabel.setText(String.format("[%s]", rootAction.getDisplayableBranchText()));
+
+ if (isSelected) {
+ setSelected(myBranchLabel);
+ }
+ else {
+ myBranchLabel.setBackground(getBackground());
+ myBranchLabel.setForeground(JBColor.GRAY); // different foreground than for other elements
+ }
+
+ adjustOpacity(myBranchLabel, isSelected);
+ }
+ else {
+ myBranchLabel.setVisible(false);
+ }
+ }
+
+ @Override
+ protected JComponent createItemComponent() {
+ myTextLabel = new ErrorLabel();
+ myTextLabel.setOpaque(true);
+ myTextLabel.setBorder(BorderFactory.createEmptyBorder(1, 1, 1, 1));
+
+ myBranchLabel = new ErrorLabel();
+ myBranchLabel.setOpaque(true);
+ myBranchLabel.setBorder(BorderFactory.createEmptyBorder(1, UIUtil.DEFAULT_HGAP, 1, 1));
+
+ JPanel compoundPanel = new OpaquePanel(new BorderLayout(), JBColor.WHITE);
+ compoundPanel.add(myTextLabel, BorderLayout.CENTER);
+ compoundPanel.add(myBranchLabel, BorderLayout.EAST);
+
+ return layoutComponent(compoundPanel);
+ }
+ };
+ }
+}
+
diff --git a/platform/dvcs-impl/src/com/intellij/dvcs/ui/CloneDvcsDialog.form b/platform/dvcs-impl/src/com/intellij/dvcs/ui/CloneDvcsDialog.form
new file mode 100644
index 000000000000..53d957676c8b
--- /dev/null
+++ b/platform/dvcs-impl/src/com/intellij/dvcs/ui/CloneDvcsDialog.form
@@ -0,0 +1,84 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="com.intellij.dvcs.ui.CloneDvcsDialog">
+ <grid id="27dc6" binding="myRootPanel" layout-manager="GridLayoutManager" row-count="4" column-count="4" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
+ <margin top="0" left="0" bottom="0" right="0"/>
+ <constraints>
+ <xy x="20" y="20" width="785" height="119"/>
+ </constraints>
+ <properties/>
+ <border type="none"/>
+ <children>
+ <component id="7e816" class="javax.swing.JLabel" binding="myRepositoryUrlLabel">
+ <constraints>
+ <grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+ </constraints>
+ <properties>
+ <labelFor value=""/>
+ <text value="&amp;Repository URL"/>
+ </properties>
+ </component>
+ <vspacer id="86a0f">
+ <constraints>
+ <grid row="3" column="0" row-span="1" col-span="1" vsize-policy="6" hsize-policy="1" anchor="0" fill="2" indent="0" use-parent-layout="false"/>
+ </constraints>
+ </vspacer>
+ <hspacer id="a0290">
+ <constraints>
+ <grid row="3" column="1" row-span="1" col-span="3" vsize-policy="1" hsize-policy="6" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
+ </constraints>
+ </hspacer>
+ <component id="b8c8e" class="com.intellij.ui.EditorComboBox" binding="myRepositoryURL" custom-create="true">
+ <constraints>
+ <grid row="0" column="1" row-span="1" col-span="2" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
+ <preferred-size width="150" height="-1"/>
+ </grid>
+ </constraints>
+ <properties/>
+ </component>
+ <component id="1422b" class="javax.swing.JLabel">
+ <constraints>
+ <grid row="1" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+ </constraints>
+ <properties>
+ <text resource-bundle="com/intellij/dvcs/ui/DvcsBundle" key="clone.parent.dir"/>
+ </properties>
+ </component>
+ <component id="876a6" class="com.intellij.openapi.ui.TextFieldWithBrowseButton" binding="myParentDirectory">
+ <constraints>
+ <grid row="1" column="1" row-span="1" col-span="3" vsize-policy="0" hsize-policy="6" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
+ </constraints>
+ <properties/>
+ </component>
+ <component id="2002c" class="javax.swing.JLabel">
+ <constraints>
+ <grid row="2" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+ </constraints>
+ <properties>
+ <labelFor value="4e988"/>
+ <text resource-bundle="com/intellij/dvcs/ui/DvcsBundle" key="clone.dir.name"/>
+ </properties>
+ </component>
+ <component id="fbda8" class="javax.swing.JButton" binding="myTestButton">
+ <constraints>
+ <grid row="0" column="3" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
+ </constraints>
+ <properties>
+ <text resource-bundle="com/intellij/dvcs/ui/DvcsBundle" key="clone.test"/>
+ </properties>
+ </component>
+ <component id="4e988" class="javax.swing.JTextField" binding="myDirectoryName">
+ <constraints>
+ <grid row="2" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
+ <preferred-size width="150" height="-1"/>
+ </grid>
+ </constraints>
+ <properties/>
+ </component>
+ <hspacer id="e781a">
+ <constraints>
+ <grid row="2" column="2" row-span="1" col-span="2" vsize-policy="1" hsize-policy="6" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
+ </constraints>
+ </hspacer>
+ </children>
+ </grid>
+</form>
diff --git a/platform/dvcs-impl/src/com/intellij/dvcs/ui/CloneDvcsDialog.java b/platform/dvcs-impl/src/com/intellij/dvcs/ui/CloneDvcsDialog.java
new file mode 100644
index 000000000000..8e06c99775e2
--- /dev/null
+++ b/platform/dvcs-impl/src/com/intellij/dvcs/ui/CloneDvcsDialog.java
@@ -0,0 +1,332 @@
+/*
+ * Copyright 2000-2013 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 com.intellij.dvcs.ui;
+
+import com.intellij.dvcs.DvcsRememberedInputs;
+import com.intellij.ide.impl.ProjectUtil;
+import com.intellij.openapi.fileChooser.FileChooserDescriptor;
+import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.*;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.ui.DocumentAdapter;
+import com.intellij.ui.EditorComboBox;
+import com.intellij.util.ArrayUtil;
+import org.jetbrains.annotations.NonNls;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import javax.swing.event.DocumentEvent;
+import javax.swing.event.DocumentListener;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.io.File;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.regex.Pattern;
+
+/**
+ * @author Nadya Zabrodina
+ */
+public abstract class CloneDvcsDialog extends DialogWrapper {
+ /**
+ * The pattern for SSH URL-s in form [user@]host:path
+ */
+ private static final Pattern SSH_URL_PATTERN;
+
+ static {
+ // TODO make real URL pattern
+ @NonNls final String ch = "[\\p{ASCII}&&[\\p{Graph}]&&[^@:/]]";
+ @NonNls final String host = ch + "+(?:\\." + ch + "+)*";
+ @NonNls final String path = "/?" + ch + "+(?:/" + ch + "+)*/?";
+ @NonNls final String all = "(?:" + ch + "+@)?" + host + ":" + path;
+ SSH_URL_PATTERN = Pattern.compile(all);
+ }
+
+ private JPanel myRootPanel;
+ private EditorComboBox myRepositoryURL;
+ private TextFieldWithBrowseButton myParentDirectory;
+ private JButton myTestButton; // test repository
+ private JTextField myDirectoryName;
+ private JLabel myRepositoryUrlLabel;
+
+ @NotNull private String myTestURL; // the repository URL at the time of the last test
+ @Nullable private Boolean myTestResult; // the test result of the last test or null if not tested
+ @NotNull private String myDefaultDirectoryName = "";
+ @NotNull protected final Project myProject;
+ @NotNull protected final String myVcsDirectoryName;
+
+ public CloneDvcsDialog(@NotNull Project project, @NotNull String displayName, @NotNull String vcsDirectoryName) {
+ super(project, true);
+ myProject = project;
+ myVcsDirectoryName = vcsDirectoryName;
+ init();
+ initListeners();
+ setTitle(DvcsBundle.getString("clone.title"));
+ myRepositoryUrlLabel.setText(DvcsBundle.message("clone.repository.url", displayName));
+ setOKButtonText(DvcsBundle.getString("clone.button"));
+ }
+
+ @NotNull
+ public String getSourceRepositoryURL() {
+ return getCurrentUrlText();
+ }
+
+ public String getParentDirectory() {
+ return myParentDirectory.getText();
+ }
+
+ public String getDirectoryName() {
+ return myDirectoryName.getText();
+ }
+
+ /**
+ * Init components
+ */
+ private void initListeners() {
+ FileChooserDescriptor fcd = FileChooserDescriptorFactory.createSingleFolderDescriptor();
+ fcd.setShowFileSystemRoots(true);
+ fcd.setTitle(DvcsBundle.getString("clone.destination.directory.title"));
+ fcd.setDescription(DvcsBundle.getString("clone.destination.directory.description"));
+ fcd.setHideIgnored(false);
+ myParentDirectory.addActionListener(
+ new ComponentWithBrowseButton.BrowseFolderActionListener<JTextField>(fcd.getTitle(), fcd.getDescription(), myParentDirectory,
+ myProject, fcd, TextComponentAccessor.TEXT_FIELD_WHOLE_TEXT) {
+ @Override
+ protected VirtualFile getInitialFile() {
+ // suggest project base directory only if nothing is typed in the component.
+ String text = getComponentText();
+ if (text.length() == 0) {
+ VirtualFile file = myProject.getBaseDir();
+ if (file != null) {
+ return file;
+ }
+ }
+ return super.getInitialFile();
+ }
+ }
+ );
+
+ final DocumentListener updateOkButtonListener = new DocumentAdapter() {
+ @Override
+ protected void textChanged(DocumentEvent e) {
+ updateButtons();
+ }
+ };
+ myParentDirectory.getChildComponent().getDocument().addDocumentListener(updateOkButtonListener);
+ String parentDir = getRememberedInputs().getCloneParentDir();
+ if (StringUtil.isEmptyOrSpaces(parentDir)) {
+ parentDir = ProjectUtil.getBaseDir();
+ }
+ myParentDirectory.setText(parentDir);
+
+ myDirectoryName.getDocument().addDocumentListener(updateOkButtonListener);
+
+ myTestButton.addActionListener(new ActionListener() {
+ public void actionPerformed(final ActionEvent e) {
+ test();
+ }
+ });
+
+ setOKActionEnabled(false);
+ myTestButton.setEnabled(false);
+ }
+
+ private void test() {
+ myTestURL = getCurrentUrlText();
+ boolean testResult = test(myTestURL);
+
+ if (testResult) {
+ Messages.showInfoMessage(myTestButton, DvcsBundle.message("clone.test.success.message", myTestURL),
+ DvcsBundle.getString("clone.test.connection.title"));
+ myTestResult = Boolean.TRUE;
+ }
+ else {
+ myTestResult = Boolean.FALSE;
+ }
+ updateButtons();
+ }
+
+ protected abstract boolean test(@NotNull String url);
+
+ @NotNull
+ protected abstract DvcsRememberedInputs getRememberedInputs();
+
+ /**
+ * Check fields and display error in the wrapper if there is a problem
+ */
+ private void updateButtons() {
+ if (!checkRepositoryURL()) {
+ return;
+ }
+ if (!checkDestination()) {
+ return;
+ }
+ setErrorText(null);
+ setOKActionEnabled(true);
+ }
+
+ /**
+ * Check destination directory and set appropriate error text if there are problems
+ *
+ * @return true if destination components are OK.
+ */
+ private boolean checkDestination() {
+ if (myParentDirectory.getText().length() == 0 || myDirectoryName.getText().length() == 0) {
+ setErrorText(null);
+ setOKActionEnabled(false);
+ return false;
+ }
+ File file = new File(myParentDirectory.getText(), myDirectoryName.getText());
+ if (file.exists()) {
+ setErrorText(DvcsBundle.message("clone.destination.exists.error", file));
+ setOKActionEnabled(false);
+ return false;
+ }
+ else if (!file.getParentFile().exists()) {
+ setErrorText(DvcsBundle.message("clone.parent.missing.error", file.getParent()));
+ setOKActionEnabled(false);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Check repository URL and set appropriate error text if there are problems
+ *
+ * @return true if repository URL is OK.
+ */
+ private boolean checkRepositoryURL() {
+ String repository = getCurrentUrlText();
+ if (repository.length() == 0) {
+ setErrorText(null);
+ setOKActionEnabled(false);
+ return false;
+ }
+ if (myTestResult != null && repository.equals(myTestURL)) {
+ if (!myTestResult.booleanValue()) {
+ setErrorText(DvcsBundle.getString("clone.test.failed.error"));
+ setOKActionEnabled(false);
+ return false;
+ }
+ else {
+ return true;
+ }
+ }
+ try {
+ if (new URI(repository).isAbsolute()) {
+ return true;
+ }
+ }
+ catch (URISyntaxException urlExp) {
+ // do nothing
+ }
+ // check if ssh url pattern
+ if (SSH_URL_PATTERN.matcher(repository).matches()) {
+ return true;
+ }
+ try {
+ File file = new File(repository);
+ if (file.exists()) {
+ if (!file.isDirectory()) {
+ setErrorText(DvcsBundle.getString("clone.url.is.not.directory.error"));
+ setOKActionEnabled(false);
+ }
+ return true;
+ }
+ }
+ catch (Exception fileExp) {
+ // do nothing
+ }
+ setErrorText(DvcsBundle.getString("clone.invalid.url"));
+ setOKActionEnabled(false);
+ return false;
+ }
+
+ @NotNull
+ private String getCurrentUrlText() {
+ return myRepositoryURL.getText().trim();
+ }
+
+ private void createUIComponents() {
+ myRepositoryURL = new EditorComboBox("");
+ final DvcsRememberedInputs rememberedInputs = getRememberedInputs();
+ myRepositoryURL.setHistory(ArrayUtil.toObjectArray(rememberedInputs.getVisitedUrls(), String.class));
+ myRepositoryURL.addDocumentListener(new com.intellij.openapi.editor.event.DocumentAdapter() {
+ @Override
+ public void documentChanged(com.intellij.openapi.editor.event.DocumentEvent e) {
+ // enable test button only if something is entered in repository URL
+ final String url = getCurrentUrlText();
+ myTestButton.setEnabled(url.length() != 0);
+ if (myDefaultDirectoryName.equals(myDirectoryName.getText()) || myDirectoryName.getText().length() == 0) {
+ // modify field if it was unmodified or blank
+ myDefaultDirectoryName = defaultDirectoryName(url, myVcsDirectoryName);
+ myDirectoryName.setText(myDefaultDirectoryName);
+ }
+ updateButtons();
+ }
+ });
+ }
+
+ public void prependToHistory(@NotNull final String item) {
+ myRepositoryURL.prependItem(item);
+ }
+
+ public void rememberSettings() {
+ final DvcsRememberedInputs rememberedInputs = getRememberedInputs();
+ rememberedInputs.addUrl(getSourceRepositoryURL());
+ rememberedInputs.setCloneParentDir(getParentDirectory());
+ }
+
+ /**
+ * Get default name for checked out directory
+ *
+ * @param url an URL to checkout
+ * @return a default repository name
+ */
+ @NotNull
+ private static String defaultDirectoryName(@NotNull final String url, @NotNull final String vcsDirName) {
+ String nonSystemName;
+ if (url.endsWith("/" + vcsDirName) || url.endsWith(File.separator + vcsDirName)) {
+ nonSystemName = url.substring(0, url.length() - vcsDirName.length() - 1);
+ }
+ else {
+ if (url.endsWith(vcsDirName)) {
+ nonSystemName = url.substring(0, url.length() - vcsDirName.length());
+ }
+ else {
+ nonSystemName = url;
+ }
+ }
+ int i = nonSystemName.lastIndexOf('/');
+ if (i == -1 && File.separatorChar != '/') {
+ i = nonSystemName.lastIndexOf(File.separatorChar);
+ }
+ return i >= 0 ? nonSystemName.substring(i + 1) : "";
+ }
+
+ @Nullable
+ @Override
+ public JComponent getPreferredFocusedComponent() {
+ return myRepositoryURL;
+ }
+
+ protected JComponent createCenterPanel() {
+ return myRootPanel;
+ }
+}
diff --git a/platform/dvcs-impl/src/com/intellij/dvcs/ui/DvcsBundle.java b/platform/dvcs-impl/src/com/intellij/dvcs/ui/DvcsBundle.java
new file mode 100644
index 000000000000..879def5e1802
--- /dev/null
+++ b/platform/dvcs-impl/src/com/intellij/dvcs/ui/DvcsBundle.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2000-2013 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 com.intellij.dvcs.ui;
+
+import com.intellij.CommonBundle;
+import org.jetbrains.annotations.NonNls;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.PropertyKey;
+
+import java.lang.ref.Reference;
+import java.lang.ref.SoftReference;
+import java.util.ResourceBundle;
+
+/**
+ * @author Nadya Zabrodina
+ */
+public class DvcsBundle {
+
+ public static String message(@NotNull @PropertyKey(resourceBundle = BUNDLE) String key, @NotNull Object... params) {
+ return CommonBundle.message(getBundle(), key, params);
+ }
+
+ private static Reference<ResourceBundle> ourBundle;
+ @NonNls
+ private static final String BUNDLE = "com.intellij.dvcs.ui.DvcsBundle";
+
+ private DvcsBundle() {
+ }
+
+ private static ResourceBundle getBundle() {
+ ResourceBundle bundle = com.intellij.reference.SoftReference.dereference(ourBundle);
+ if (bundle == null) {
+ bundle = ResourceBundle.getBundle(BUNDLE);
+ ourBundle = new SoftReference<ResourceBundle>(bundle);
+ }
+ return bundle;
+ }
+
+ public static String getString(@PropertyKey(resourceBundle = BUNDLE) final String key) {
+ return getBundle().getString(key);
+ }
+}
diff --git a/platform/dvcs-impl/src/com/intellij/dvcs/ui/DvcsBundle.properties b/platform/dvcs-impl/src/com/intellij/dvcs/ui/DvcsBundle.properties
new file mode 100644
index 000000000000..14822049588d
--- /dev/null
+++ b/platform/dvcs-impl/src/com/intellij/dvcs/ui/DvcsBundle.properties
@@ -0,0 +1,19 @@
+clone.button=Clone
+clone.destination.directory.description=Select a parent directory destination directory for clone
+clone.destination.directory.title=Parent Directory
+clone.destination.exists.error=The path {0} exists. Repository cannot be cloned to an existing directory.
+clone.dir.name=Directory &Name:
+clone.invalid.url=Repository URL is a malformed URL or non-existent directory.
+clone.parent.dir=&Parent Directory:
+clone.parent.missing.error=The parent path {0} must exist.
+clone.repository.url={0} &Repository URL:
+clone.test.failed.error=Repository test has failed.
+clone.test.success.message=<html>Connection to repository {0} has been successful.</html>
+clone.test.connection.title=Test Connection
+clone.test=&Test
+clone.testing=Testing repository {0}
+clone.title=Clone Repository
+clone.url.is.not.directory.error=Repository URL is not a directory.
+cloning.repository=Cloning source repository {0}
+commit.amend=Amend commit
+commit.amend.tooltip=<html>Merge this commit with the previous one</html>
diff --git a/platform/dvcs-impl/src/com/intellij/dvcs/ui/NewBranchAction.java b/platform/dvcs-impl/src/com/intellij/dvcs/ui/NewBranchAction.java
new file mode 100644
index 000000000000..92ba9515b5d6
--- /dev/null
+++ b/platform/dvcs-impl/src/com/intellij/dvcs/ui/NewBranchAction.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2000-2013 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 com.intellij.dvcs.ui;
+
+import com.intellij.dvcs.DvcsUtil;
+import com.intellij.dvcs.repo.Repository;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.project.DumbAwareAction;
+import com.intellij.openapi.project.Project;
+import com.intellij.util.IconUtil;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+public abstract class NewBranchAction<T extends Repository> extends DumbAwareAction {
+ protected final List<T> myRepositories;
+ protected Project myProject;
+
+ public NewBranchAction(@NotNull Project project, @NotNull List<T> repositories) {
+ super("New Branch", "Create and checkout new branch", IconUtil.getAddIcon());
+ myRepositories = repositories;
+ myProject = project;
+ }
+
+
+ @Override
+ public void update(AnActionEvent e) {
+ if (DvcsUtil.anyRepositoryIsFresh(myRepositories)) {
+ e.getPresentation().setEnabled(false);
+ e.getPresentation().setDescription("Checkout of a new branch is not possible before the first commit");
+ }
+ }
+
+ @Override
+ public abstract void actionPerformed(AnActionEvent e);
+}
diff --git a/platform/dvcs-impl/src/com/intellij/dvcs/ui/RootAction.java b/platform/dvcs-impl/src/com/intellij/dvcs/ui/RootAction.java
new file mode 100644
index 000000000000..93e49042648c
--- /dev/null
+++ b/platform/dvcs-impl/src/com/intellij/dvcs/ui/RootAction.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2000-2013 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 com.intellij.dvcs.ui;
+
+import com.intellij.dvcs.DvcsUtil;
+import com.intellij.dvcs.repo.Repository;
+import com.intellij.openapi.actionSystem.ActionGroup;
+import com.intellij.openapi.actionSystem.AnAction;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.util.PlatformIcons;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * The element of the branch popup which allows to show branches of the selected repository.
+ * It is available only in projects with multiple roots.
+ *
+ * @author Kirill Likhodedov
+ * @author Nadya Zabrodina
+ */
+public class RootAction<T extends Repository> extends ActionGroup {
+
+ @NotNull protected final T myRepository;
+ @NotNull private final ActionGroup myGroup;
+ @NotNull private final String myBranchText;
+
+ /**
+ * @param currentRepository Pass null in the case of common repositories - none repository will be highlighted then.
+ * @param actionsGroup
+ * @param branchText
+ */
+ public RootAction(@NotNull T repository, @Nullable T currentRepository, @NotNull ActionGroup actionsGroup, @NotNull String branchText) {
+ super("", true);
+ myRepository = repository;
+ myGroup = actionsGroup;
+ myBranchText = branchText;
+ if (repository.equals(currentRepository)) {
+ getTemplatePresentation().setIcon(PlatformIcons.CHECK_ICON);
+ }
+ getTemplatePresentation().setText(DvcsUtil.getShortRepositoryName(repository), false);
+ }
+
+ @NotNull
+ public String getCaption() {
+ return "Current branch in " + DvcsUtil.getShortRepositoryName(myRepository) + ": " + getDisplayableBranchText();
+ }
+
+ @NotNull
+ public String getDisplayableBranchText() {
+ return myBranchText;
+ }
+
+ @NotNull
+ @Override
+ public AnAction[] getChildren(@Nullable AnActionEvent e) {
+ return myGroup.getChildren(e);
+ }
+}
+
+
+
+
diff --git a/platform/dvcs-impl/src/com/intellij/dvcs/ui/VcsLogAction.java b/platform/dvcs-impl/src/com/intellij/dvcs/ui/VcsLogAction.java
new file mode 100644
index 000000000000..bb7f64f9b24c
--- /dev/null
+++ b/platform/dvcs-impl/src/com/intellij/dvcs/ui/VcsLogAction.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2000-2014 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 com.intellij.dvcs.ui;
+
+import com.intellij.dvcs.repo.Repository;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.actionSystem.CommonDataKeys;
+import com.intellij.openapi.project.DumbAwareAction;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.util.containers.MultiMap;
+import com.intellij.vcs.log.VcsFullCommitDetails;
+import com.intellij.vcs.log.VcsLog;
+import com.intellij.vcs.log.VcsLogDataKeys;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+public abstract class VcsLogAction<Repo extends Repository> extends DumbAwareAction {
+
+ @Override
+ public void actionPerformed(AnActionEvent e) {
+ Project project = e.getRequiredData(CommonDataKeys.PROJECT);
+ VcsLog log = e.getRequiredData(VcsLogDataKeys.VSC_LOG);
+ List<VcsFullCommitDetails> details = log.getSelectedDetails();
+ MultiMap<Repo, VcsFullCommitDetails> grouped = groupByRootWithCheck(project, details);
+ assert grouped != null;
+ actionPerformed(project, grouped);
+ }
+
+ @Override
+ public void update(AnActionEvent e) {
+ Project project = e.getProject();
+ VcsLog log = e.getData(VcsLogDataKeys.VSC_LOG);
+ if (project == null || log == null) {
+ e.getPresentation().setEnabledAndVisible(false);
+ return;
+ }
+
+ List<VcsFullCommitDetails> details = log.getSelectedDetails();
+ MultiMap<Repo, VcsFullCommitDetails> grouped = groupByRootWithCheck(project, details);
+ if (grouped == null) {
+ e.getPresentation().setEnabledAndVisible(false);
+ }
+ else {
+ e.getPresentation().setVisible(true);
+ e.getPresentation().setEnabled(!grouped.isEmpty() && isEnabled(grouped));
+ }
+ }
+
+ protected abstract void actionPerformed(@NotNull Project project, @NotNull MultiMap<Repo, VcsFullCommitDetails> grouped);
+
+ protected abstract boolean isEnabled(@NotNull MultiMap<Repo, VcsFullCommitDetails> grouped);
+
+ @Nullable
+ protected abstract Repo getRepositoryForRoot(@NotNull Project project, @NotNull VirtualFile root);
+
+ @Nullable
+ private MultiMap<Repo, VcsFullCommitDetails> groupByRootWithCheck(@NotNull Project project, @NotNull List<VcsFullCommitDetails> commits) {
+ MultiMap<Repo, VcsFullCommitDetails> map = MultiMap.create();
+ for (VcsFullCommitDetails commit : commits) {
+ Repo root = getRepositoryForRoot(project, commit.getRoot());
+ if (root == null) { // commit from some other VCS
+ return null;
+ }
+ map.putValue(root, commit);
+ }
+ return map;
+ }
+
+}
diff --git a/platform/dvcs-impl/src/com/intellij/dvcs/ui/VcsLogOneCommitPerRepoAction.java b/platform/dvcs-impl/src/com/intellij/dvcs/ui/VcsLogOneCommitPerRepoAction.java
new file mode 100644
index 000000000000..1b4be918a3e1
--- /dev/null
+++ b/platform/dvcs-impl/src/com/intellij/dvcs/ui/VcsLogOneCommitPerRepoAction.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2000-2014 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 com.intellij.dvcs.ui;
+
+import com.intellij.dvcs.repo.Repository;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Condition;
+import com.intellij.util.containers.ContainerUtil;
+import com.intellij.util.containers.MultiMap;
+import com.intellij.vcs.log.VcsFullCommitDetails;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Collection;
+import java.util.Map;
+
+public abstract class VcsLogOneCommitPerRepoAction<Repo extends Repository> extends VcsLogAction<Repo> {
+
+ @Override
+ protected void actionPerformed(@NotNull Project project, @NotNull MultiMap<Repo, VcsFullCommitDetails> grouped) {
+ Map<Repo, VcsFullCommitDetails> singleElementMap = convertToSingleElementMap(grouped);
+ assert singleElementMap != null;
+ actionPerformed(project, singleElementMap);
+ }
+
+ @Override
+ protected boolean isEnabled(@NotNull MultiMap<Repo, VcsFullCommitDetails> grouped) {
+ return allValuesAreSingletons(grouped);
+ }
+
+ protected abstract void actionPerformed(@NotNull Project project, @NotNull Map<Repo, VcsFullCommitDetails> commits);
+
+ private boolean allValuesAreSingletons(@NotNull MultiMap<Repo, VcsFullCommitDetails> grouped) {
+ return !ContainerUtil.exists(grouped.entrySet(), new Condition<Map.Entry<Repo, Collection<VcsFullCommitDetails>>>() {
+ @Override
+ public boolean value(Map.Entry<Repo, Collection<VcsFullCommitDetails>> entry) {
+ return entry.getValue().size() != 1;
+ }
+ });
+ }
+
+ @Nullable
+ private Map<Repo, VcsFullCommitDetails> convertToSingleElementMap(@NotNull MultiMap<Repo, VcsFullCommitDetails> groupedCommits) {
+ Map<Repo, VcsFullCommitDetails> map = ContainerUtil.newHashMap();
+ for (Map.Entry<Repo, Collection<VcsFullCommitDetails>> entry : groupedCommits.entrySet()) {
+ Collection<VcsFullCommitDetails> commits = entry.getValue();
+ if (commits.size() != 1) {
+ return null;
+ }
+ map.put(entry.getKey(), commits.iterator().next());
+ }
+ return map;
+ }
+
+
+}
diff --git a/platform/dvcs-impl/src/com/intellij/dvcs/ui/VcsLogSingleCommitAction.java b/platform/dvcs-impl/src/com/intellij/dvcs/ui/VcsLogSingleCommitAction.java
new file mode 100644
index 000000000000..4b43380ea9eb
--- /dev/null
+++ b/platform/dvcs-impl/src/com/intellij/dvcs/ui/VcsLogSingleCommitAction.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2000-2014 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 com.intellij.dvcs.ui;
+
+import com.intellij.dvcs.repo.Repository;
+import com.intellij.openapi.project.Project;
+import com.intellij.util.containers.MultiMap;
+import com.intellij.vcs.log.VcsFullCommitDetails;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Collection;
+import java.util.Map;
+
+public abstract class VcsLogSingleCommitAction<Repo extends Repository> extends VcsLogAction<Repo> {
+
+ @Override
+ protected boolean isEnabled(@NotNull MultiMap<Repo, VcsFullCommitDetails> grouped) {
+ return grouped.size() == 1;
+ }
+
+ @Override
+ protected void actionPerformed(@NotNull Project project, @NotNull MultiMap<Repo, VcsFullCommitDetails> grouped) {
+ assert grouped.size() == 1;
+ Map.Entry<Repo, Collection<VcsFullCommitDetails>> entry = grouped.entrySet().iterator().next();
+ Repo repository = entry.getKey();
+ Collection<VcsFullCommitDetails> commits = entry.getValue();
+ assert commits.size() == 1;
+ actionPerformed(repository, commits.iterator().next());
+ }
+
+ protected abstract void actionPerformed(@NotNull Repo repository, @NotNull VcsFullCommitDetails commit);
+
+}