diff options
Diffstat (limited to 'platform/dvcs-impl/src/com')
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="&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); + +} |