| /* |
| * Copyright 2000-2011 JetBrains s.r.o. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package git4idea.push; |
| |
| import com.intellij.dvcs.DvcsUtil; |
| 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.issueLinks.IssueLinkHtmlRenderer; |
| import com.intellij.openapi.vcs.changes.ui.ChangesBrowser; |
| import com.intellij.ui.*; |
| import com.intellij.util.ArrayUtil; |
| import com.intellij.util.Consumer; |
| import com.intellij.util.text.DateFormatUtil; |
| import com.intellij.util.ui.UIUtil; |
| import com.intellij.util.ui.tree.TreeUtil; |
| import git4idea.GitBranch; |
| import git4idea.GitCommit; |
| import git4idea.GitUtil; |
| import git4idea.repo.GitRepository; |
| import org.jetbrains.annotations.NotNull; |
| |
| import javax.swing.*; |
| import javax.swing.event.TreeSelectionEvent; |
| import javax.swing.event.TreeSelectionListener; |
| import javax.swing.tree.*; |
| import java.awt.*; |
| import java.awt.event.MouseEvent; |
| import java.util.*; |
| import java.util.List; |
| import java.util.concurrent.locks.ReentrantReadWriteLock; |
| |
| /** |
| * The list of commits from multiple repositories and branches, with diff panel at the right. |
| * |
| * @author Kirill Likhodedov |
| */ |
| class GitPushLog extends JPanel implements TypeSafeDataProvider { |
| |
| private final Project myProject; |
| private final Collection<GitRepository> myAllRepositories; |
| private final ChangesBrowser myChangesBrowser; |
| private final CheckboxTree myTree; |
| private final DefaultTreeModel myTreeModel; |
| private final CheckedTreeNode myRootNode; |
| private final ReentrantReadWriteLock TREE_CONSTRUCTION_LOCK = new ReentrantReadWriteLock(); |
| private boolean myTreeWasConstructed; |
| private final MyTreeCellRenderer myTreeCellRenderer; |
| |
| GitPushLog(@NotNull Project project, @NotNull Collection<GitRepository> repositories, @NotNull final Consumer<Boolean> checkboxListener) { |
| myProject = project; |
| myAllRepositories = repositories; |
| |
| myRootNode = new CheckedTreeNode(null); |
| myRootNode.add(new DefaultMutableTreeNode(new FakeCommit())); |
| |
| myTreeModel = new DefaultTreeModel(myRootNode); |
| myTreeCellRenderer = new MyTreeCellRenderer(); |
| myTree = new CheckboxTree(myTreeCellRenderer, myRootNode) { |
| @Override |
| protected void onNodeStateChanged(CheckedTreeNode node) { |
| Object userObject = node.getUserObject(); |
| if (userObject instanceof GitRepository) { |
| checkboxListener.consume(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 ""; |
| } |
| Object userObject = ((DefaultMutableTreeNode)node).getUserObject(); |
| if (userObject instanceof GitCommit) { |
| GitCommit commit = (GitCommit)userObject; |
| return getHashString(commit) + " " + getDateString(commit) + " by " + commit.getAuthorName() + "\n\n" + |
| IssueLinkHtmlRenderer.formatTextWithLinks(myProject, commit.getFullMessage()); |
| } |
| return ""; |
| } |
| }; |
| myTree.setRootVisible(false); |
| TreeUtil.expandAll(myTree); |
| |
| myTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); |
| myTree.addTreeSelectionListener(new TreeSelectionListener() { |
| @Override |
| public void valueChanged(TreeSelectionEvent e) { |
| DefaultMutableTreeNode node = (DefaultMutableTreeNode) myTree.getLastSelectedPathComponent(); |
| if (node != null) { |
| Object nodeInfo = node.getUserObject(); |
| if (nodeInfo instanceof GitCommit) { |
| myChangesBrowser.getViewer().setEmptyText("No differences"); |
| myChangesBrowser.setChangesToDisplay(new ArrayList<Change>(((GitCommit)nodeInfo).getChanges())); |
| return; |
| } |
| } |
| setDefaultEmptyText(); |
| myChangesBrowser.setChangesToDisplay(Collections.<Change>emptyList()); |
| } |
| }); |
| 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 GitCommit) { |
| sink.put(key, ArrayUtil.toObjectArray(((GitCommit)object).getChanges(), Change.class)); |
| } |
| } |
| } |
| |
| @NotNull |
| public JComponent getPreferredFocusComponent() { |
| return myTree; |
| } |
| |
| void setCommits(@NotNull GitCommitsByRepoAndBranch commits) { |
| try { |
| TREE_CONSTRUCTION_LOCK.writeLock().lock(); |
| myRootNode.removeAllChildren(); |
| createNodes(commits); |
| myTreeModel.nodeStructureChanged(myRootNode); |
| myTree.setModel(myTreeModel); // TODO: why doesn't it repaint otherwise? |
| TreeUtil.expandAll(myTree); |
| selectFirstCommit(); |
| collapseEmptyRepoNodes(commits); |
| myTreeWasConstructed = true; |
| } |
| finally { |
| TREE_CONSTRUCTION_LOCK.writeLock().unlock(); |
| } |
| } |
| |
| private void selectFirstCommit() { |
| DefaultMutableTreeNode firstLeaf = myRootNode.getFirstLeaf(); |
| if (firstLeaf == null) { |
| return; |
| } |
| |
| Enumeration enumeration = myRootNode.depthFirstEnumeration(); |
| DefaultMutableTreeNode node = null; |
| while (enumeration.hasMoreElements()) { |
| node = (DefaultMutableTreeNode) enumeration.nextElement(); |
| if (node.isLeaf() && node.getUserObject() instanceof GitCommit) { |
| break; |
| } |
| } |
| if (node == null) { |
| node = firstLeaf; |
| } |
| myTree.setSelectionPath(new TreePath(node.getPath())); |
| } |
| |
| private void collapseEmptyRepoNodes(GitCommitsByRepoAndBranch commits) { |
| Enumeration enumeration = myRootNode.breadthFirstEnumeration(); |
| while (enumeration.hasMoreElements()) { |
| DefaultMutableTreeNode node = (DefaultMutableTreeNode) enumeration.nextElement(); |
| Object userObject = node.getUserObject(); |
| if (userObject == null) { |
| // root object => seeking further |
| } |
| else if (userObject instanceof GitRepository) { |
| if (commits.get((GitRepository)userObject).isEmpty()) { |
| myTree.collapsePath(new TreePath(node.getPath())); |
| } |
| } |
| else { |
| // we're looking to the breadthFirstEnumeration => all repository nodes have already been enumerated |
| return; |
| } |
| } |
| } |
| |
| private void createNodes(@NotNull GitCommitsByRepoAndBranch commits) { |
| for (GitRepository repository : sortRepositories(commits)) { |
| GitCommitsByBranch commitsByBranch = commits.get(repository); |
| createRepoNode(repository, commitsByBranch, myRootNode); |
| } |
| } |
| |
| @NotNull |
| private static List<GitRepository> sortRepositories(@NotNull final GitCommitsByRepoAndBranch commits) { |
| List<GitRepository> repos = new ArrayList<GitRepository>(commits.getRepositories()); |
| Collections.sort(repos, new Comparator<GitRepository>() { |
| @Override public int compare(GitRepository r1, GitRepository r2) { |
| // empty repositories - to the end |
| if (commits.get(r1).isEmpty() && !commits.get(r2).isEmpty()) { |
| return 1; |
| } |
| if (commits.get(r2).isEmpty() && !commits.get(r1).isEmpty()) { |
| return -1; |
| } |
| return r1.getPresentableUrl().compareTo(r2.getPresentableUrl()); |
| } |
| }); |
| return repos; |
| } |
| |
| /** |
| * Creates the node with subnodes for a repository and adds it to the rootNode. |
| * If there is only one repo in the project, doesn't create a node for the repository, and adds subnodes directly to the rootNode. |
| */ |
| private void createRepoNode(@NotNull GitRepository repository, @NotNull GitCommitsByBranch commitsByBranch, |
| @NotNull DefaultMutableTreeNode rootNode) { |
| DefaultMutableTreeNode parentNode; |
| if (GitUtil.justOneGitRepository(myProject)) { |
| parentNode = rootNode; |
| } else { |
| parentNode = new CheckedTreeNode(repository); |
| if (commitsByBranch.isEmpty()) { |
| ((CheckedTreeNode)parentNode).setChecked(false); |
| } |
| rootNode.add(parentNode); |
| } |
| |
| for (GitBranch branch : sortBranches(commitsByBranch.getBranches())) { |
| DefaultMutableTreeNode branchNode = createBranchNode(branch, commitsByBranch.get(branch)); |
| parentNode.add(branchNode); |
| } |
| } |
| |
| private static List<GitBranch> sortBranches(@NotNull Collection<GitBranch> branches) { |
| List<GitBranch> sortedBranches = new ArrayList<GitBranch>(branches); |
| Collections.sort(sortedBranches, new Comparator<GitBranch>() { |
| @Override public int compare(GitBranch o1, GitBranch o2) { |
| return o1.getName().compareTo(o2.getName()); |
| } |
| }); |
| return sortedBranches; |
| } |
| |
| private static DefaultMutableTreeNode createBranchNode(@NotNull GitBranch branch, @NotNull GitPushBranchInfo branchInfo) { |
| DefaultMutableTreeNode branchNode = new DefaultMutableTreeNode(branchInfo); |
| for (GitCommit commit : branchInfo.getCommits()) { |
| branchNode.add(new DefaultMutableTreeNode(commit)); |
| } |
| if (branchInfo.isNewBranchCreated()) { |
| branchNode.add(new DefaultMutableTreeNode(new MoreCommitsToShow())); |
| } |
| return branchNode; |
| } |
| |
| void displayError(String message) { |
| DefaultMutableTreeNode titleNode = new DefaultMutableTreeNode("Error: couldn't collect commits to be pushed"); |
| DefaultMutableTreeNode detailNode = new DefaultMutableTreeNode(message); |
| myRootNode.add(titleNode); |
| myRootNode.add(detailNode); |
| myTreeModel.reload(myRootNode); |
| TreeUtil.expandAll(myTree); |
| repaint(); |
| } |
| |
| /** |
| * @return repositories selected (via checkboxes) to be pushed. |
| */ |
| Collection<GitRepository> getSelectedRepositories() { |
| if (myAllRepositories.size() == 1) { |
| return myAllRepositories; |
| } |
| |
| try { |
| TREE_CONSTRUCTION_LOCK.readLock().lock(); // wait for tree to be constructed |
| if (!myTreeWasConstructed) { |
| return myAllRepositories; |
| } |
| else { |
| Collection<GitRepository> selectedRepositories = new ArrayList<GitRepository>(myAllRepositories.size()); |
| if (myRootNode.getChildCount() == 0) { // the method is requested before tree construction began => returning all repos. |
| return myAllRepositories; |
| } |
| |
| for (int i = 0; i < myRootNode.getChildCount(); i++) { |
| TreeNode child = myRootNode.getChildAt(i); |
| if (child instanceof CheckedTreeNode) { |
| CheckedTreeNode node = (CheckedTreeNode)child; |
| if (node.isChecked()) { |
| if (node.getUserObject() instanceof GitRepository) { |
| selectedRepositories.add((GitRepository)node.getUserObject()); |
| } |
| } |
| } |
| } |
| return selectedRepositories; |
| } |
| } |
| finally { |
| TREE_CONSTRUCTION_LOCK.readLock().unlock(); |
| } |
| } |
| |
| @NotNull |
| private static String getDateString(@NotNull GitCommit commit) { |
| return DateFormatUtil.formatPrettyDateTime(commit.getAuthorTime()) + " "; |
| } |
| |
| @NotNull |
| private static String getHashString(@NotNull GitCommit commit) { |
| return GitUtil.getShortHash(commit.getHash().toString()); |
| } |
| |
| 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) { |
| Object userObject; |
| if (value instanceof CheckedTreeNode) { |
| userObject = ((CheckedTreeNode)value).getUserObject(); |
| } else if (value instanceof DefaultMutableTreeNode) { |
| userObject = ((DefaultMutableTreeNode)value).getUserObject(); |
| } else { |
| return; |
| } |
| |
| ColoredTreeCellRenderer renderer = getTextRenderer(); |
| if (userObject instanceof GitCommit) { |
| GitCommit commit = (GitCommit)userObject; |
| renderer.append(commit.getSubject(), new SimpleTextAttributes(SimpleTextAttributes.STYLE_SMALLER, getTextRenderer().getForeground())); |
| renderer.setToolTipText(getHashString(commit) + " " + getDateString(commit)); |
| } |
| else if (userObject instanceof GitRepository) { |
| String repositoryPath = DvcsUtil.getShortRepositoryName((GitRepository)userObject); |
| renderer.append(repositoryPath, SimpleTextAttributes.GRAY_ATTRIBUTES); |
| } |
| else if (userObject instanceof GitPushBranchInfo) { |
| GitPushBranchInfo branchInfo = (GitPushBranchInfo) userObject; |
| GitBranch fromBranch = branchInfo.getSourceBranch(); |
| GitBranch dest = branchInfo.getDestBranch(); |
| |
| GitPushBranchInfo.Type type = branchInfo.getType(); |
| final String showingRecentCommits = ", showing " + GitPusher.RECENT_COMMITS_NUMBER + " recent commits"; |
| String text = fromBranch.getName(); |
| SimpleTextAttributes attrs = SimpleTextAttributes.REGULAR_ATTRIBUTES; |
| String additionalText = ""; |
| switch (type) { |
| case STANDARD: |
| text += " -> " + dest.getName(); |
| if (branchInfo.getCommits().isEmpty()) { |
| additionalText = " nothing to push"; |
| } |
| break; |
| case NEW_BRANCH: |
| text += " -> +" + dest.getName(); |
| attrs = SimpleTextAttributes.REGULAR_BOLD_ATTRIBUTES; |
| additionalText = " new branch will be created" + showingRecentCommits; |
| break; |
| case NO_TRACKED_OR_TARGET: |
| attrs = SimpleTextAttributes.REGULAR_BOLD_ATTRIBUTES; |
| additionalText = " no tracked branch. Use checkbox below to push branch to manually specified" + showingRecentCommits; |
| break; |
| } |
| renderer.append(text, attrs); |
| renderer.append(additionalText, new SimpleTextAttributes(SimpleTextAttributes.STYLE_SMALLER, UIUtil.getInactiveTextColor())); |
| } |
| else if (userObject instanceof FakeCommit) { |
| int spaces = 6 + 15 + 3 + 30; |
| String s = String.format("%" + spaces + "s", " "); |
| renderer.append(s, new SimpleTextAttributes(SimpleTextAttributes.STYLE_PLAIN, renderer.getBackground())); |
| } |
| else if (userObject instanceof MoreCommitsToShow) { |
| renderer.append("..."); |
| } |
| else { |
| renderer.append(userObject == null ? "" : userObject.toString()); |
| } |
| } |
| } |
| |
| private static class FakeCommit { |
| } |
| |
| private static class MoreCommitsToShow { |
| } |
| } |