diff --git a/platform/platform-api/src/com/intellij/ui/SimpleColoredComponent.java b/platform/platform-api/src/com/intellij/ui/SimpleColoredComponent.java index aa6a588d7416..dbfa86c74e91 100644 --- a/platform/platform-api/src/com/intellij/ui/SimpleColoredComponent.java +++ b/platform/platform-api/src/com/intellij/ui/SimpleColoredComponent.java @@ -18,6 +18,7 @@ package com.intellij.ui; import com.intellij.openapi.application.Application; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; +import com.intellij.util.containers.HashMap; import com.intellij.util.ui.UIUtil; import org.jetbrains.annotations.NotNull; @@ -25,6 +26,7 @@ import javax.swing.*; import javax.swing.border.Border; import java.awt.*; import java.util.ArrayList; +import java.util.Map; /** * This is high performance Swing component which represents an icon @@ -71,8 +73,7 @@ public class SimpleColoredComponent extends JComponent { private int myMainTextLastIndex = -1; - private int myAlignIndex; - private int myAlignWidth; + private final Map myAligns; private boolean myIconOpaque = true; @@ -84,6 +85,7 @@ public class SimpleColoredComponent extends JComponent { myIpad = new Insets(1, 2, 1, 2); myIconTextGap = 2; myBorder = new MyBorder(); + myAligns = new HashMap(10); setOpaque(true); } @@ -137,8 +139,8 @@ public class SimpleColoredComponent extends JComponent { } public synchronized void appendAlign(int alignWidth) { - myAlignIndex = myFragments.size()-1; - myAlignWidth = alignWidth; + final int alignIndex = myFragments.size()-1; + myAligns.put(alignIndex, alignWidth); } /** @@ -153,8 +155,7 @@ public class SimpleColoredComponent extends JComponent { myAttributes.clear(); myFragmentTags = null; myMainTextLastIndex = -1; - myAlignIndex = -1; - myAlignWidth = -1; + myAligns.clear(); } revalidateAndRepaint(); } @@ -280,8 +281,10 @@ public class SimpleColoredComponent extends JComponent { } final FontMetrics metrics = getFontMetrics(font); width += metrics.stringWidth(myFragments.get(i)); - if (i == myAlignIndex && width < myAlignWidth) { - width = myAlignWidth; + + final Integer fixedWidth = myAligns.get(i); + if (fixedWidth != null && width < fixedWidth.intValue()) { + width = fixedWidth.intValue(); } if (mainTextOnly && myMainTextLastIndex >= 0 && i == myMainTextLastIndex) break; @@ -330,8 +333,9 @@ public class SimpleColoredComponent extends JComponent { return i; } curX += curWidth; - if (i == myAlignIndex && curX < myAlignWidth) { - curX = myAlignWidth; + final Integer fixedWidth = myAligns.get(i); + if (fixedWidth != null && curX < fixedWidth.intValue()) { + curX = fixedWidth.intValue(); } } return -1; @@ -444,8 +448,9 @@ public class SimpleColoredComponent extends JComponent { xOffset += fragmentWidth; - if (i == myAlignIndex && xOffset < myAlignWidth) { - xOffset = myAlignWidth; + final Integer fixedWidth = myAligns.get(i); + if (fixedWidth != null && xOffset < fixedWidth.intValue()) { + xOffset = fixedWidth.intValue(); } } diff --git a/platform/util/src/com/intellij/util/OnOff.java b/platform/util/src/com/intellij/util/OnOff.java new file mode 100644 index 000000000000..32abe88cd161 --- /dev/null +++ b/platform/util/src/com/intellij/util/OnOff.java @@ -0,0 +1,22 @@ +/* + * Copyright 2000-2010 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.util; + +public interface OnOff { + boolean isOn(); + void on(); + void off(); +} diff --git a/plugins/svn4idea/src/org/jetbrains/idea/svn/RequestsMerger.java b/platform/vcs-api/src/com/intellij/openapi/vcs/RequestsMerger.java similarity index 97% rename from plugins/svn4idea/src/org/jetbrains/idea/svn/RequestsMerger.java rename to platform/vcs-api/src/com/intellij/openapi/vcs/RequestsMerger.java index 6498bef75e14..49038a5cae2b 100644 --- a/plugins/svn4idea/src/org/jetbrains/idea/svn/RequestsMerger.java +++ b/platform/vcs-api/src/com/intellij/openapi/vcs/RequestsMerger.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2009 JetBrains s.r.o. + * Copyright 2000-2010 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. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.jetbrains.idea.svn; +package com.intellij.openapi.vcs; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.progress.SomeQueue; @@ -39,7 +39,7 @@ import java.util.Map; */ @SomeQueue public class RequestsMerger { - private static final Logger LOG = Logger.getInstance("#org.jetbrains.idea.svn.RequestsMerger"); + private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.vcs.RequestsMerger"); private static final int ourDelay = 300; private final MyWorker myWorker; diff --git a/platform/vcs-impl/src/com/intellij/openapi/vcs/changes/committed/CommittedChangesTreeBrowser.java b/platform/vcs-impl/src/com/intellij/openapi/vcs/changes/committed/CommittedChangesTreeBrowser.java index 34f60221a7a0..c8baf7d4dc71 100644 --- a/platform/vcs-impl/src/com/intellij/openapi/vcs/changes/committed/CommittedChangesTreeBrowser.java +++ b/platform/vcs-impl/src/com/intellij/openapi/vcs/changes/committed/CommittedChangesTreeBrowser.java @@ -18,7 +18,6 @@ import com.intellij.openapi.keymap.KeymapManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.Splitter; import com.intellij.openapi.ui.SplitterProportionsData; -import com.intellij.openapi.ui.ThreeComponentsSplitter; import com.intellij.openapi.util.Comparing; import com.intellij.openapi.util.Pair; import com.intellij.openapi.vcs.AbstractVcs; @@ -67,8 +66,6 @@ import java.util.List; * @author yole */ public class CommittedChangesTreeBrowser extends JPanel implements TypeSafeDataProvider, Disposable, DecoratorManager { - private static final Border LEFT_BORDER = BorderFactory.createMatteBorder(1, 0, 0, 1, UIUtil.getBorderSeparatorColor()); - private static final Border MIDDLE_BORDER = BorderFactory.createMatteBorder(1, 1, 0, 1, UIUtil.getBorderSeparatorColor()); private static final Border RIGHT_BORDER = BorderFactory.createMatteBorder(1, 1, 0, 0, UIUtil.getBorderSeparatorColor()); private final Project myProject; @@ -275,7 +272,7 @@ public class CommittedChangesTreeBrowser extends JPanel implements TypeSafeDataP } } - private static List collectChanges(final List selectedChangeLists, final boolean withMovedTrees) { + public static List collectChanges(final List selectedChangeLists, final boolean withMovedTrees) { List result = new ArrayList(); Collections.sort(selectedChangeLists, new Comparator() { public int compare(final CommittedChangeList o1, final CommittedChangeList o2) { @@ -637,106 +634,6 @@ public class CommittedChangesTreeBrowser extends JPanel implements TypeSafeDataP } } - private static class WiseSplitter { - private final Runnable myRefresher; - private final Splitter myParentSplitter; - private final ThreeComponentsSplitter myInnerSplitter; - private final Map myInnerSplitterContents; - - private WiseSplitter(final Runnable refresher, final Splitter parentSplitter) { - myRefresher = refresher; - myParentSplitter = parentSplitter; - - myInnerSplitter = new ThreeComponentsSplitter(false); - myInnerSplitter.setHonorComponentsMinimumSize(true); - myInnerSplitterContents = new HashMap(); - updateBorders(); - } - - public boolean canAdd() { - return myInnerSplitterContents.size() <= 3; - } - - public void add(final String key, final JComponent comp) { - final int idx = myInnerSplitterContents.size(); - myInnerSplitterContents.put(key, idx); - if (idx == 0) { - myParentSplitter.setFirstComponent(myInnerSplitter); - if (myParentSplitter.getProportion() < 0.05f) { - myParentSplitter.setProportion(0.25f); - } - myInnerSplitter.setFirstComponent(comp); - myInnerSplitter.setFirstSize((int) (myParentSplitter.getSize().getWidth() * myParentSplitter.getProportion())); - } else if (idx == 1) { - final Dimension dimension = myInnerSplitter.getSize(); - final double width = dimension.getWidth() / 2; - myInnerSplitter.setInnerComponent(comp); - myInnerSplitter.setFirstSize((int) width); - } else { - final Dimension dimension = myInnerSplitter.getSize(); - final double width = dimension.getWidth() / 3; - myInnerSplitter.setLastComponent(comp); - myInnerSplitter.setFirstSize((int) width); - myInnerSplitter.setLastSize((int) width); - } - - updateBorders(); - - myRefresher.run(); - } - - private void updateBorders() { - boolean isEmpty = myInnerSplitterContents.size() == 0; - if (!isEmpty) { - setBorder(myInnerSplitter.getFirstComponent(), true); - setBorder(myInnerSplitter.getInnerComponent(), false); - } - setBorder(myParentSplitter.getSecondComponent(), isEmpty); - } - - private void setBorder(JComponent c, boolean leftMost) { - if (c instanceof JScrollPane) c.setBorder(leftMost ? LEFT_BORDER : MIDDLE_BORDER); - } - - public void remove(final String key) { - final Integer idx = myInnerSplitterContents.remove(key); - if (idx == null) { - return; - } - final Map tmp = new HashMap(); - for (Map.Entry entry : myInnerSplitterContents.entrySet()) { - if (entry.getValue() < idx) { - tmp.put(entry.getKey(), entry.getValue()); - } else { - tmp.put(entry.getKey(), entry.getValue() - 1); - } - } - myInnerSplitterContents.clear(); - myInnerSplitterContents.putAll(tmp); - - if (idx == 0) { - final JComponent inner = myInnerSplitter.getInnerComponent(); - myInnerSplitter.setInnerComponent(null); - myInnerSplitter.setFirstComponent(inner); - lastToInner(); - } else if (idx == 1) { - lastToInner(); - } else { - myInnerSplitter.setLastComponent(null); - } - - updateBorders(); - - myRefresher.run(); - } - - private void lastToInner() { - final JComponent last = myInnerSplitter.getLastComponent(); - myInnerSplitter.setLastComponent(null); - myInnerSplitter.setInnerComponent(last); - } - } - public interface CommittedChangesReloadListener { void itemsReloaded(); void emptyRefresh(); diff --git a/platform/vcs-impl/src/com/intellij/openapi/vcs/changes/committed/WiseSplitter.java b/platform/vcs-impl/src/com/intellij/openapi/vcs/changes/committed/WiseSplitter.java new file mode 100644 index 000000000000..845fffb6edea --- /dev/null +++ b/platform/vcs-impl/src/com/intellij/openapi/vcs/changes/committed/WiseSplitter.java @@ -0,0 +1,129 @@ +/* + * Copyright 2000-2010 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.openapi.vcs.changes.committed; + +import com.intellij.openapi.ui.Splitter; +import com.intellij.openapi.ui.ThreeComponentsSplitter; +import com.intellij.util.ui.UIUtil; + +import javax.swing.*; +import javax.swing.border.Border; +import java.awt.*; +import java.util.HashMap; +import java.util.Map; + +public class WiseSplitter { + private static final Border LEFT_BORDER = BorderFactory.createMatteBorder(1, 0, 0, 1, UIUtil.getBorderSeparatorColor()); + private static final Border MIDDLE_BORDER = BorderFactory.createMatteBorder(1, 1, 0, 1, UIUtil.getBorderSeparatorColor()); + + private final Runnable myRefresher; + private final Splitter myParentSplitter; + private final ThreeComponentsSplitter myInnerSplitter; + private final Map myInnerSplitterContents; + + public WiseSplitter(final Runnable refresher, final Splitter parentSplitter) { + myRefresher = refresher; + myParentSplitter = parentSplitter; + + myInnerSplitter = new ThreeComponentsSplitter(false); + myInnerSplitter.setHonorComponentsMinimumSize(true); + myInnerSplitterContents = new HashMap(); + updateBorders(); + } + + public boolean canAdd() { + return myInnerSplitterContents.size() <= 3; + } + + public void add(final String key, final JComponent comp) { + final int idx = myInnerSplitterContents.size(); + myInnerSplitterContents.put(key, idx); + if (idx == 0) { + myParentSplitter.setFirstComponent(myInnerSplitter); + if (myParentSplitter.getProportion() < 0.05f) { + myParentSplitter.setProportion(0.25f); + } + myInnerSplitter.setFirstComponent(comp); + myInnerSplitter.setFirstSize((int) (myParentSplitter.getSize().getWidth() * myParentSplitter.getProportion())); + } else if (idx == 1) { + final Dimension dimension = myInnerSplitter.getSize(); + final double width = dimension.getWidth() / 2; + myInnerSplitter.setInnerComponent(comp); + myInnerSplitter.setFirstSize((int) width); + } else { + final Dimension dimension = myInnerSplitter.getSize(); + final double width = dimension.getWidth() / 3; + myInnerSplitter.setLastComponent(comp); + myInnerSplitter.setFirstSize((int) width); + myInnerSplitter.setLastSize((int) width); + } + + updateBorders(); + + myRefresher.run(); + } + + private void updateBorders() { + boolean isEmpty = myInnerSplitterContents.size() == 0; + if (!isEmpty) { + setBorder(myInnerSplitter.getFirstComponent(), true); + setBorder(myInnerSplitter.getInnerComponent(), false); + } + setBorder(myParentSplitter.getSecondComponent(), isEmpty); + } + + private void setBorder(JComponent c, boolean leftMost) { + if (c instanceof JScrollPane) c.setBorder(leftMost ? LEFT_BORDER : MIDDLE_BORDER); + } + + public void remove(final String key) { + final Integer idx = myInnerSplitterContents.remove(key); + if (idx == null) { + return; + } + final Map tmp = new HashMap(); + for (Map.Entry entry : myInnerSplitterContents.entrySet()) { + if (entry.getValue() < idx) { + tmp.put(entry.getKey(), entry.getValue()); + } else { + tmp.put(entry.getKey(), entry.getValue() - 1); + } + } + myInnerSplitterContents.clear(); + myInnerSplitterContents.putAll(tmp); + + if (idx == 0) { + final JComponent inner = myInnerSplitter.getInnerComponent(); + myInnerSplitter.setInnerComponent(null); + myInnerSplitter.setFirstComponent(inner); + lastToInner(); + } else if (idx == 1) { + lastToInner(); + } else { + myInnerSplitter.setLastComponent(null); + } + + updateBorders(); + + myRefresher.run(); + } + + private void lastToInner() { + final JComponent last = myInnerSplitter.getLastComponent(); + myInnerSplitter.setLastComponent(null); + myInnerSplitter.setInnerComponent(last); + } +} diff --git a/plugins/git4idea/resources/icons/branch.png b/plugins/git4idea/resources/icons/branch.png new file mode 100644 index 000000000000..559574528dda Binary files /dev/null and b/plugins/git4idea/resources/icons/branch.png differ diff --git a/plugins/git4idea/resources/icons/filter.png b/plugins/git4idea/resources/icons/filter.png new file mode 100644 index 000000000000..0c002389c4ef Binary files /dev/null and b/plugins/git4idea/resources/icons/filter.png differ diff --git a/plugins/git4idea/resources/icons/gitlogtree.png b/plugins/git4idea/resources/icons/gitlogtree.png new file mode 100644 index 000000000000..38e77e532bb8 Binary files /dev/null and b/plugins/git4idea/resources/icons/gitlogtree.png differ diff --git a/plugins/git4idea/resources/icons/inclusion.png b/plugins/git4idea/resources/icons/inclusion.png new file mode 100644 index 000000000000..bb268fbd99a3 Binary files /dev/null and b/plugins/git4idea/resources/icons/inclusion.png differ diff --git a/plugins/git4idea/resources/icons/show.png b/plugins/git4idea/resources/icons/show.png new file mode 100644 index 000000000000..a8cdc7965e0a Binary files /dev/null and b/plugins/git4idea/resources/icons/show.png differ diff --git a/plugins/git4idea/resources/icons/tag.png b/plugins/git4idea/resources/icons/tag.png new file mode 100644 index 000000000000..2715b76dbeac Binary files /dev/null and b/plugins/git4idea/resources/icons/tag.png differ diff --git a/plugins/git4idea/resources/icons/user.png b/plugins/git4idea/resources/icons/user.png new file mode 100644 index 000000000000..9d77dc0be926 Binary files /dev/null and b/plugins/git4idea/resources/icons/user.png differ diff --git a/plugins/git4idea/src/META-INF/plugin.xml b/plugins/git4idea/src/META-INF/plugin.xml index 8a65cc2c7428..e685623094e4 100644 --- a/plugins/git4idea/src/META-INF/plugin.xml +++ b/plugins/git4idea/src/META-INF/plugin.xml @@ -108,4 +108,9 @@ - \ No newline at end of file + + + git4idea.history.browser.GitProjectLogManager + + + diff --git a/plugins/git4idea/src/git4idea/GitBranch.java b/plugins/git4idea/src/git4idea/GitBranch.java index 0650bbf8d509..e1acd2f96f1e 100644 --- a/plugins/git4idea/src/git4idea/GitBranch.java +++ b/plugins/git4idea/src/git4idea/GitBranch.java @@ -119,13 +119,14 @@ public class GitBranch extends GitReference { * @param remote if true remote branches are listed * @param local if true local branches are listed * @param branches the collection used to store branches + * @param containingCommit * @throws VcsException if there is a problem with running git */ public static void listAsStrings(final Project project, final VirtualFile root, final boolean remote, final boolean local, - final Collection branches) throws VcsException { + final Collection branches, @Nullable final String containingCommit) throws VcsException { if (!local && !remote) { // no need to run handler return; @@ -140,6 +141,10 @@ public class GitBranch extends GitReference { else if (remote) { handler.addParameters("-r"); } + if (containingCommit != null) { + handler.addParameters("--contains"); + handler.addParameters(containingCommit); + } StringScanner s = new StringScanner(handler.run()); while (s.hasMoreData()) { String line = s.line(); @@ -208,14 +213,14 @@ public class GitBranch extends GitReference { final Collection branches) throws VcsException { ArrayList temp = new ArrayList(); if (local) { - listAsStrings(project, root, false, true, temp); + listAsStrings(project, root, false, true, temp, null); for (String b : temp) { branches.add(new GitBranch(b, false, false)); } temp.clear(); } if (remote) { - listAsStrings(project, root, true, false, temp); + listAsStrings(project, root, true, false, temp, null); for (String b : temp) { branches.add(new GitBranch(b, false, true)); } diff --git a/plugins/git4idea/src/git4idea/GitTag.java b/plugins/git4idea/src/git4idea/GitTag.java index b8803d2cd8fa..acc607b96661 100644 --- a/plugins/git4idea/src/git4idea/GitTag.java +++ b/plugins/git4idea/src/git4idea/GitTag.java @@ -22,6 +22,7 @@ import git4idea.commands.GitCommand; import git4idea.commands.GitSimpleHandler; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.Collection; @@ -58,13 +59,19 @@ public class GitTag extends GitReference { * @param project the context * @param root the git root * @param tags the tag list + * @param containingCommit * @throws VcsException if there is a problem with running git */ - public static void listAsStrings(final Project project, final VirtualFile root, final Collection tags) throws VcsException { + public static void listAsStrings(final Project project, final VirtualFile root, final Collection tags, + @Nullable final String containingCommit) throws VcsException { GitSimpleHandler handler = new GitSimpleHandler(project, root, GitCommand.TAG); handler.setNoSSH(true); handler.setSilent(true); handler.addParameters("-l"); + if (containingCommit != null) { + handler.addParameters("--contains"); + handler.addParameters(containingCommit); + } for (String line : handler.run().split("\n")) { if (line.length() == 0) { continue; @@ -83,7 +90,7 @@ public class GitTag extends GitReference { */ public static void list(final Project project, final VirtualFile root, final Collection tags) throws VcsException { ArrayList temp = new ArrayList(); - listAsStrings(project, root, temp); + listAsStrings(project, root, temp, null); for (String t : temp) { tags.add(new GitTag(t)); } diff --git a/plugins/git4idea/src/git4idea/GitUtil.java b/plugins/git4idea/src/git4idea/GitUtil.java index dd940b09a1c6..505c7fd76191 100644 --- a/plugins/git4idea/src/git4idea/GitUtil.java +++ b/plugins/git4idea/src/git4idea/GitUtil.java @@ -674,8 +674,6 @@ public class GitUtil { final VirtualFile root, final Consumer parametersSpecifier, final Consumer consumer) throws VcsException { - final List rc = new ArrayList(); - GitSimpleHandler h = new GitSimpleHandler(project, root, GitCommand.LOG); h.setSilent(true); h.setNoSSH(true); diff --git a/plugins/git4idea/src/git4idea/changes/GitChangeUtils.java b/plugins/git4idea/src/git4idea/changes/GitChangeUtils.java index 318c34e7f81d..3a95c881c7b2 100644 --- a/plugins/git4idea/src/git4idea/changes/GitChangeUtils.java +++ b/plugins/git4idea/src/git4idea/changes/GitChangeUtils.java @@ -314,7 +314,7 @@ public class GitChangeUtils { } GitRevisionNumber thisRevision = new GitRevisionNumber(revisionNumber, commitDate); GitRevisionNumber parentRevision = parents.length > 0 ? loadRevision(project, root, parents[0]) : null; - long number = Long.parseLong(revisionNumber.substring(0, 15), 16) << 4 + Integer.parseInt(revisionNumber.substring(15, 16), 16); + long number = longForSHAHash(revisionNumber); if (parents.length <= 1) { // This is the first or normal commit with the single parent. // Just parse changes in this commit as returned by the show command. @@ -350,4 +350,8 @@ public class GitChangeUtils { return new CommittedChangeListImpl(commentSubject + "(" + revisionNumber + ")", fullComment, committerName, number, commitDate, changes); } + + public static long longForSHAHash(String revisionNumber) { + return Long.parseLong(revisionNumber.substring(0, 15), 16) << 4 + Integer.parseInt(revisionNumber.substring(15, 16), 16); + } } diff --git a/plugins/git4idea/src/git4idea/checkin/GitPushDialog.java b/plugins/git4idea/src/git4idea/checkin/GitPushDialog.java index e2a55198876b..19b97a65819d 100644 --- a/plugins/git4idea/src/git4idea/checkin/GitPushDialog.java +++ b/plugins/git4idea/src/git4idea/checkin/GitPushDialog.java @@ -369,8 +369,8 @@ public class GitPushDialog extends DialogWrapper { myBranchNames.clear(); myTagNames.clear(); try { - GitBranch.listAsStrings(myProject, getGitRoot(), false, true, myBranchNames); - GitTag.listAsStrings(myProject, getGitRoot(), myTagNames); + GitBranch.listAsStrings(myProject, getGitRoot(), false, true, myBranchNames, null); + GitTag.listAsStrings(myProject, getGitRoot(), myTagNames, null); } catch (VcsException ex) { LOG.warn("Exception in branch list: \n" + StringUtil.getThrowableText(ex)); diff --git a/plugins/git4idea/src/git4idea/checkout/GitCheckoutDialog.java b/plugins/git4idea/src/git4idea/checkout/GitCheckoutDialog.java index a091baa13131..17e11e757fed 100644 --- a/plugins/git4idea/src/git4idea/checkout/GitCheckoutDialog.java +++ b/plugins/git4idea/src/git4idea/checkout/GitCheckoutDialog.java @@ -240,14 +240,14 @@ public class GitCheckoutDialog extends DialogWrapper { try { List branchesAndTags = new ArrayList(); // get branches - GitBranch.listAsStrings(myProject, gitRoot(), true, true, branchesAndTags); + GitBranch.listAsStrings(myProject, gitRoot(), true, true, branchesAndTags, null); existingBranches.clear(); existingBranches.addAll(branchesAndTags); Collections.sort(branchesAndTags); // get tags if (myIncludeTagsCheckBox.isSelected()) { int mark = branchesAndTags.size(); - GitTag.listAsStrings(myProject, gitRoot(), branchesAndTags); + GitTag.listAsStrings(myProject, gitRoot(), branchesAndTags, null); Collections.sort(branchesAndTags.subList(mark, branchesAndTags.size())); } myBranchToCkeckout.removeAllItems(); diff --git a/plugins/git4idea/src/git4idea/history/GitHistoryUtils.java b/plugins/git4idea/src/git4idea/history/GitHistoryUtils.java index 722619cd4417..b032a0779157 100644 --- a/plugins/git4idea/src/git4idea/history/GitHistoryUtils.java +++ b/plugins/git4idea/src/git4idea/history/GitHistoryUtils.java @@ -18,6 +18,7 @@ package git4idea.history; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Key; +import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vcs.FilePath; import com.intellij.openapi.vcs.VcsException; import com.intellij.openapi.vcs.changes.Change; @@ -26,6 +27,7 @@ import com.intellij.openapi.vcs.diff.ItemLatestState; import com.intellij.openapi.vcs.history.VcsFileRevision; import com.intellij.openapi.vcs.history.VcsRevisionNumber; import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.util.ArrayUtil; import com.intellij.util.Consumer; import com.intellij.util.concurrency.Semaphore; import com.intellij.util.text.StringTokenizer; @@ -35,11 +37,11 @@ import git4idea.commands.GitCommand; import git4idea.commands.GitLineHandler; import git4idea.commands.GitLineHandlerAdapter; import git4idea.commands.GitSimpleHandler; +import git4idea.history.browser.GitCommit; +import git4idea.history.browser.SHAHash; import org.jetbrains.annotations.Nullable; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; +import java.util.*; /** * History utilities for Git @@ -277,6 +279,129 @@ public class GitHistoryUtils { return rc; } + public static List historyWithLinks(Project project, FilePath path, final Set allBranchesSet, final String... parameters) throws VcsException { + // adjust path using change manager + path = getLastCommitName(project, path); + final VirtualFile root = GitUtil.getGitRoot(path); + GitSimpleHandler h = new GitSimpleHandler(project, root, GitCommand.LOG); + h.setNoSSH(true); + h.setStdoutSuppressed(true); + // todo think of name+email linkage in a single field + h.addParameters(parameters); + /*h.addParameters("-M", "--follow", "--name-only", + "--pretty=format:%x03%H%x00%ct%x00%an%x00%ae%x00%cn%x00%ce%x00[%P]%x00[%d]%x00%s%n%n%b%x00", "--encoding=UTF-8");*/ + h.addParameters("--name-only", + "--pretty=format:%x03%H%x00%ct%x00%an%x00%ae%x00%cn%x00%ce%x00[%P]%x00[%d]%x00%s%n%n%b%x00", "--encoding=UTF-8"); + + h.endOptions(); + h.addRelativePaths(path); + String output = h.run(); + final List rc = new ArrayList(); + StringTokenizer tk = new StringTokenizer(output, "\u0003", false); + final String prefix = root.getPath() + "/"; + + while (tk.hasMoreTokens()) { + final String line = tk.nextToken(); + final StringTokenizer tk2 = new StringTokenizer(line, "\u0000\n", false); + //while (tk2.hasMoreTokens()) { + final String hash = tk2.nextToken("\u0000\n"); + final String dateString = tk2.nextToken("\u0000"); + final Date date = GitUtil.parseTimestamp(dateString); + final String authorName = tk2.nextToken("\u0000"); + final String authorEmail = tk2.nextToken("\u0000"); + final String committerName = tk2.nextToken("\u0000"); + final String committerEmail = tk2.nextToken("\u0000"); + // parent hashes + final String parents = removeSquareBraces(tk2.nextToken("\u0000")); + final Set parentsHashes; + if (! StringUtil.isEmptyOrSpaces(parents)) { + final String[] parentsSplit = parents.split(" "); // todo if parent = 000000 + parentsHashes = new HashSet(); + for (String s : parentsSplit) { + parentsHashes.add(new SHAHash(s)); + } + } else { + parentsHashes = Collections.emptySet(); + } + // decorate + final String decorate = tk2.nextToken("\u0000"); + final String[] refNames = parseRefNames(decorate); + final List tags = refNames.length > 0 ? new LinkedList() : Collections.emptyList(); + final List branches = refNames.length > 0 ? new LinkedList() : Collections.emptyList(); + for (String refName : refNames) { + if (allBranchesSet.contains(refName)) { + branches.add(refName); + } else { + tags.add(refName); + } + } + + final String message = tk2.nextToken("\u0000").trim(); + + final List pathsList = new LinkedList(); + tk2.reset("\u0000\n"); + while (tk2.hasMoreTokens()) { + final FilePath revisionPath = VcsUtil.getFilePathForDeletedFile(prefix + GitUtil.unescapePath(tk2.nextToken()), false); + pathsList.add(revisionPath); + } + // todo parse revisions... patches? + rc.add(new GitCommit(new SHAHash(hash), authorName, committerName, date, message, parentsHashes, pathsList, authorEmail, + committerEmail, tags, branches)); + //} + } + /*StringTokenizer tk2 = new StringTokenizer(output, "\u0000\n", false); + String prefix = root.getPath() + "/"; + while (tk.hasMoreTokens()) { + final String hash = tk.nextToken("\u0000\n"); + final Date date = GitUtil.parseTimestamp(tk.nextToken("\u0000")); + final String authorName = tk.nextToken("\u0000"); + final String committerName = tk.nextToken("\u0000"); + // parent hashes + final String parents = tk.nextToken("\u0000"); + final String[] parentsSplit = parents.split(" "); // todo if parent = 000000 + final Set parentsHashes = new HashSet(); + for (String s : parentsSplit) { + parentsHashes.add(new SHAHash(s)); + } + // decorate + final String decorate = tk.nextToken("\u0000"); + final int startSymb = decorate.indexOf("("); + int idxFrom = startSymb == -1 ? 0 : startSymb; + final int endSymb = decorate.indexOf(")"); + int idxTo = endSymb == -1 ? 0 : endSymb; + final String refs = decorate.substring(idxFrom, idxTo); + final String[] refNames = refs.split(", "); + + final String message = tk.nextToken("\u0000").trim(); + tk.nextToken("\u0000\u0001\u0000"); + //final FilePath revisionPath = VcsUtil.getFilePathForDeletedFile(prefix + GitUtil.unescapePath(tk.nextToken("\u0000\n")), false); + rc.add(new GitCommit(new SHAHash(hash), authorName, committerName, date, message, parentsHashes, Arrays.asList(refNames))); + }*/ + return rc; + } + + @Nullable + private static String removeSquareBraces(final String s) { + final int startSquare = s.indexOf("["); + final int endSquare = s.indexOf("]"); + if ((startSquare == -1) || (endSquare == -1)) return null; + return s.substring(startSquare + 1, endSquare); + } + + private static String[] parseRefNames(final String decorate) { + if (removeSquareBraces(decorate) == null) return ArrayUtil.EMPTY_STRING_ARRAY; + /*final int startSquare = decorate.indexOf("["); + final int endSquare = decorate.indexOf("]"); + if ((startSquare == -1) || (endSquare == -1)) return ArrayUtil.EMPTY_STRING_ARRAY;*/ + + final int startParentheses = decorate.indexOf("("); + final int endParentheses = decorate.indexOf(")"); + if ((startParentheses == -1) || (endParentheses == -1)) return ArrayUtil.EMPTY_STRING_ARRAY; + + final String refs = decorate.substring(startParentheses + 1, endParentheses); + return refs.split(", "); + } + /** * Get name of the file in the last commit. If file was renamed, returns the previous name. * diff --git a/plugins/git4idea/src/git4idea/history/browser/CaptionIcon.java b/plugins/git4idea/src/git4idea/history/browser/CaptionIcon.java new file mode 100644 index 000000000000..070477054d2b --- /dev/null +++ b/plugins/git4idea/src/git4idea/history/browser/CaptionIcon.java @@ -0,0 +1,71 @@ +/* + * Copyright 2000-2010 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.history.browser; + +import com.intellij.util.ui.UIUtil; + +import javax.swing.*; +import java.awt.*; +import java.awt.geom.Rectangle2D; + +public class CaptionIcon implements Icon { + private final Color myBgrnd; + private final Font myFont; + private final String myText; + + private final int myHeight; + private final int myWidth; + + public CaptionIcon(Color bgrnd, Font font, String text, final Component someComponent) { + myBgrnd = bgrnd; + myFont = font; + myText = text; + + final FontMetrics fm = someComponent.getFontMetrics(myFont); + final Rectangle2D bounds = fm.getStringBounds(text, someComponent.getGraphics()); + final double height = (text.equals(text.toLowerCase())) ? (bounds.getHeight() - fm.getMaxDescent()) : bounds.getHeight(); + myHeight = (int) height + 4; + myWidth = (int) bounds.getWidth() + 4; + } + + public int getIconHeight() { + return myHeight; + } + + public void paintIcon(Component c, Graphics g, int x, int y) { + final Graphics2D graphics2D = (Graphics2D)g; + final Paint oldPaint = graphics2D.getPaint(); + final Stroke oldStroke = graphics2D.getStroke(); + + graphics2D.setPaint(myBgrnd); + g.fillRoundRect(x + 2, y, myWidth, myHeight, 2, 2); + + graphics2D.setPaint(UIUtil.getTextAreaForeground()); + final BasicStroke stroke = new BasicStroke(1); + graphics2D.setStroke(stroke); + g.drawRoundRect(x + 2, y, myWidth, myHeight - 1, 2, 2); + + g.setColor(UIUtil.getTextAreaForeground()); + g.drawString(myText, x + 4, y + myHeight - 3); // -2 + + graphics2D.setPaint(oldPaint); + graphics2D.setStroke(oldStroke); + } + + public int getIconWidth() { + return myWidth + 4; + } +} diff --git a/plugins/git4idea/src/git4idea/history/browser/ChangesFilter.java b/plugins/git4idea/src/git4idea/history/browser/ChangesFilter.java new file mode 100644 index 000000000000..ffb02a76f686 --- /dev/null +++ b/plugins/git4idea/src/git4idea/history/browser/ChangesFilter.java @@ -0,0 +1,227 @@ +/* + * Copyright 2000-2010 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.history.browser; + +import org.jetbrains.annotations.Nullable; + +import java.util.*; +import java.util.regex.Pattern; + +public class ChangesFilter { + + public abstract static class Merger { + private final Collection myFilters; + private MemoryFilter myResult; + + protected Merger() { + myFilters = new LinkedList(); + } + + protected abstract boolean acceptImpl(MemoryFilter filter); + protected abstract MemoryFilter merge(final Collection filters); + + public boolean accept(final MemoryFilter filter) { + if (acceptImpl(filter)) { + myFilters.add(filter); + return true; + } + return false; + } + + @Nullable + public MemoryFilter getResult() { + if (myFilters.isEmpty()) return null; + return merge(myFilters); + } + } + + public static class UsersMerger extends Merger { + @Override + protected boolean acceptImpl(MemoryFilter filter) { + return filter instanceof Author || filter instanceof Committer; + } + + @Override + protected MemoryFilter merge(Collection filters) { + final CompositeUserMemoryFilter result = new CompositeUserMemoryFilter(); + for (MemoryFilter filter : filters) { + if (filter instanceof Author) { + result.addUser(((Author) filter).myRegexp); + } else if (filter instanceof Committer) { + result.addUser(((Committer) filter).myRegexp); + } + } + return result; + } + } + + public static List combineFilters(final Collection filters) { + final Merger[] mergers = {new UsersMerger()}; + if (filters.isEmpty()) return Collections.emptyList(); + + final List result = new LinkedList(); + for (Filter filter : filters) { + boolean taken = false; + for (Merger combiner : mergers) { + if (combiner.accept(filter)) { + taken = true; + break; + } + } + if (! taken) { + result.add(filter); + } + } + for (Merger combiner : mergers) { + final MemoryFilter combined = combiner.getResult(); + if (combined != null) { + result.add(combined); + } + } + return result; + } + + private static class CompositeUserMemoryFilter implements MemoryFilter { + private final Set myUsers; + + private CompositeUserMemoryFilter() { + myUsers = new HashSet(); + } + + public void addUser(final String name) { + myUsers.add(name); + } + + public boolean applyInMemory(GitCommit commit) { + return myUsers.contains(commit.getCommitter()) || myUsers.contains(commit.getAuthor()); + } + } + + public interface MemoryFilter { + boolean applyInMemory(final GitCommit commit); + } + + public interface Filter extends MemoryFilter { + void applyToCommandLine(final List sink); + } + + public static class Branch implements Filter { + // todo decode into hash + private Collection myCommitNames; + + public boolean applyInMemory(final GitCommit commit) { + for (String commitName : myCommitNames) { + //if (commit.getHash().startsWith(commitName) || ) + } + return false; + } + + public void applyToCommandLine(List sink) { + //To change body of implemented methods use File | Settings | File Templates. + } + } + + public static class Author implements Filter { + private final String myRegexp; + private Pattern myPattern; + + public Author(String regexp) { + myRegexp = regexp; + myPattern = Pattern.compile(myRegexp); + } + + public boolean applyInMemory(GitCommit commit) { + return myPattern.matcher(commit.getAuthor()).matches(); + } + + public void applyToCommandLine(List sink) { + sink.add("--author=" + myRegexp); + } + } + + public static class Committer implements Filter { + private final String myRegexp; + private Pattern myPattern; + + public Committer(String regexp) { + myRegexp = regexp; + myPattern = Pattern.compile(myRegexp); + } + + public boolean applyInMemory(GitCommit commit) { + return myPattern.matcher(commit.getCommitter()).matches(); + } + + public void applyToCommandLine(List sink) { + sink.add("--committer=" + myRegexp); + } + } + + public static class BeforeDate implements Filter { + private final Date myDate; + + public BeforeDate(final Date date) { + myDate = date; + } + + public boolean applyInMemory(GitCommit commit) { + return commit.getDate().before(myDate); + } + + public void applyToCommandLine(List sink) { + sink.add("--before=" + formatDate(myDate)); + } + } + + public static class AfterDate implements Filter { + private final Date myDate; + + public AfterDate(final Date date) { + myDate = date; + } + + public boolean applyInMemory(final GitCommit commit) { + return commit.getDate().after(myDate); + } + + public void applyToCommandLine(final List sink) { + sink.add("--after=" + formatDate(myDate)); + } + } + + private static String formatDate(final Date date) { + return date.toString(); + } + + public static class Comment implements Filter { + private final String myRegexp; + private Pattern myPattern; + + public Comment(final String regexp) { + myRegexp = regexp; + myPattern = Pattern.compile(myRegexp); + } + + public boolean applyInMemory(final GitCommit commit) { + return myPattern.matcher(commit.getDescription()).matches() || myPattern.matcher(commit.getCommitter()).matches() || + myPattern.matcher(commit.getAuthor()).matches(); + } + + public void applyToCommandLine(final List sink) { + sink.add("--grep=" + myRegexp); + } + } +} diff --git a/plugins/git4idea/src/git4idea/history/browser/ErrorsHolder.java b/plugins/git4idea/src/git4idea/history/browser/ErrorsHolder.java new file mode 100644 index 000000000000..fa3d867014ad --- /dev/null +++ b/plugins/git4idea/src/git4idea/history/browser/ErrorsHolder.java @@ -0,0 +1,22 @@ +/* + * Copyright 2000-2010 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.history.browser; + +public class ErrorsHolder { + public void register(String s) { + //To change body of created methods use File | Settings | File Templates. + } +} diff --git a/plugins/git4idea/src/git4idea/history/browser/FictiveTableCellRenderer.java b/plugins/git4idea/src/git4idea/history/browser/FictiveTableCellRenderer.java new file mode 100644 index 000000000000..22fa0900a742 --- /dev/null +++ b/plugins/git4idea/src/git4idea/history/browser/FictiveTableCellRenderer.java @@ -0,0 +1,171 @@ +/* + * Copyright 2000-2010 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.history.browser; + +import com.intellij.openapi.util.Pair; +import com.intellij.openapi.util.Trinity; +import com.intellij.openapi.vcs.changes.committed.CommittedChangesTreeBrowser; +import com.intellij.ui.ColoredListCellRenderer; +import com.intellij.ui.SimpleTextAttributes; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.awt.*; + +public abstract class FictiveTableCellRenderer extends ColoredListCellRenderer { + // for cases when our renderer is encapsulated + protected abstract int getParentWidth(JList list); + @Nullable + protected abstract Color getBgrndColor(JList list, Object value, int index, boolean selected, boolean hasFocus); + protected abstract Description getDescription(Object value); + protected abstract Trinity getMoreTag(); + protected abstract boolean willRender(Object value); + + protected abstract static class Description { + private final java.util.List> myPieces; + private final int myMiddleIdx; + + public Description(int middleIdx, java.util.List> pieces) { + myMiddleIdx = middleIdx; + myPieces = pieces; + } + + public int getMiddleIdx() { + return myMiddleIdx; + } + + public java.util.List> getPieces() { + return myPieces; + } + + public abstract String getMaxString(final int idx); + } + + @Override + protected void customizeCellRenderer(JList list, Object value, int index, boolean selected, boolean hasFocus) { + if (! willRender(value)) return; + final int parentWidth = getParentWidth(list); + final Color bgrndColor = getBgrndColor(list, value, index, selected, hasFocus); + if (bgrndColor != null) { + setBackground(bgrndColor); + } + + final Description description = getDescription(value); + + final MyFontHelper helper = new MyFontHelper(list); + + int fixedWidth = 0; + int fixedWidthBefore = 0; + int changeableWidth = 0; + final java.util.List> pieces = description.getPieces(); + final int middleIdx = description.getMiddleIdx(); + for (int i = 0; i < pieces.size(); i++) { + final Pair pair = pieces.get(i); + if (middleIdx == i) { + changeableWidth = helper.getWidth(pair); + } else { + final int width = helper.getWidth(pair); + + if (i < middleIdx) { + fixedWidthBefore += width; + append(pair.getFirst(), pair.getSecond()); + final String adj = description.getMaxString(i); + if (adj != null) { + final int adjWidth = helper.getWidth(new Pair(adj, pair.getSecond())); + appendAlign(adjWidth); + fixedWidth += adjWidth; + } else { + fixedWidth += width; + } + } else { + fixedWidth += width; + } + } + } + + final Pair changeable = pieces.get(middleIdx); + final int difference = parentWidth - fixedWidth; + if ((fixedWidth + changeableWidth) > parentWidth) { + // need zipping + if (fixedWidth < parentWidth) { + // todo this should be rewised + final Trinity tag = getMoreTag(); + final int more = helper.getWidth(new Pair(tag.getFirst(), tag.getSecond())); + if (more < difference) { + final String truncated = CommittedChangesTreeBrowser.CommittedChangeListRenderer + .truncateDescription(changeable.getFirst(), helper.getFontMetrics(changeable.getSecond()), difference - more); + + final int truncatedWidth = helper.getWidth(new Pair(truncated, changeable.getSecond())); + append(truncated, changeable.getSecond()); + append(tag.getFirst(), tag.getSecond(), tag.getThird()); + if (truncatedWidth > 0) { + appendAlign(difference - truncatedWidth); + } + } else { + appendAlign(difference); + } + } + } else { + append(changeable.getFirst(), changeable.getSecond()); + appendAlign(parentWidth - fixedWidth); + } + + for (int i = middleIdx + 1; i < pieces.size(); i++) { + final Pair pair = pieces.get(i); + append(pair.getFirst(), pair.getSecond()); + } + } + + private static class MyFontHelper { + private final FontMetrics myFontMetrics; + private final FontMetrics myBoldMetrics; + private final FontMetrics myItalicsMetrics; + private final FontMetrics myBoldItalicsMetrics; + + private MyFontHelper(final JList list) { + final Font font = list.getFont(); + myFontMetrics = list.getFontMetrics(font); + myBoldMetrics = list.getFontMetrics(font.deriveFont(Font.BOLD)); + myItalicsMetrics = list.getFontMetrics(font.deriveFont(Font.ITALIC)); + myBoldItalicsMetrics = list.getFontMetrics(font.deriveFont(Font.ITALIC | Font.BOLD)); + } + + public int getWidth(final Pair pair) { + final SimpleTextAttributes ta = pair.getSecond(); + FontMetrics fm; + fm = getFontMetrics(ta); + return fm.stringWidth(pair.getFirst()); + } + + public FontMetrics getFontMetrics(SimpleTextAttributes ta) { + FontMetrics fm; + if ((ta.getStyle() & SimpleTextAttributes.STYLE_BOLD) == SimpleTextAttributes.STYLE_BOLD) { + if ((ta.getStyle() & SimpleTextAttributes.STYLE_ITALIC) == SimpleTextAttributes.STYLE_ITALIC) { + fm = myBoldItalicsMetrics; + } else { + fm = myBoldMetrics; + } + } else { + if ((ta.getStyle() & SimpleTextAttributes.STYLE_ITALIC) == SimpleTextAttributes.STYLE_ITALIC) { + fm = myItalicsMetrics; + } else { + fm = myFontMetrics; + } + } + return fm; + } + } +} diff --git a/plugins/git4idea/src/git4idea/history/browser/GitCommit.java b/plugins/git4idea/src/git4idea/history/browser/GitCommit.java new file mode 100644 index 000000000000..ececa1da6461 --- /dev/null +++ b/plugins/git4idea/src/git4idea/history/browser/GitCommit.java @@ -0,0 +1,143 @@ +/* + * Copyright 2000-2010 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.history.browser; + +import com.intellij.openapi.vcs.FilePath; +import org.jetbrains.annotations.NotNull; + +import java.util.*; + +public class GitCommit { + @NotNull + private final SHAHash myHash; + private final String myAuthor; + private final String myCommitter; + private final String myDescription; + private final Date myDate; + + private final String myAuthorEmail; + private final String myComitterEmail; + + private final List myTags; + private final List myBranches; + + private final Set myParentsHashes; + private final Set myParentsLinks; + + private final List myPathsList; + + public GitCommit(@NotNull final SHAHash hash, final String author, final String committer, final Date date, final String description, + final Set parentsHashes, final List pathsList, + final String authorEmail, final String comitterEmail, List tags, List branches) { + myAuthor = author; + myCommitter = committer; + myDate = date; + myDescription = description; + myHash = hash; + myParentsHashes = parentsHashes; + myPathsList = pathsList; + myAuthorEmail = authorEmail; + myComitterEmail = comitterEmail; + myTags = tags; + myBranches = branches; + myParentsLinks = new HashSet(); + } + + public void addParentLink(final GitCommit commit) { + myParentsLinks.add(commit); + } + + public String getAuthor() { + return myAuthor; + } + + public String getCommitter() { + return myCommitter; + } + + public Date getDate() { + return myDate; + } + + public String getDescription() { + return myDescription; + } + + @NotNull + public SHAHash getHash() { + return myHash; + } + + // todo think of interface + public Set getParentsHashes() { + return myParentsHashes; + } + + // todo think of interface + public Set getParentsLinks() { + return myParentsLinks; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + GitCommit gitCommit = (GitCommit)o; + + if (!myHash.equals(gitCommit.myHash)) return false; + + return true; + } + + @Override + public int hashCode() { + return myHash.hashCode(); + } + + public List getBranches() { + return myBranches; + } + + public List getTags() { + return myTags; + } + + public void orderTags(final Comparator comparator) { + Collections.sort(myTags, comparator); + } + + public void orderBranches(final Comparator comparator) { + Collections.sort(myBranches, comparator); + } + + public String getAuthorEmail() { + return myAuthorEmail; + } + + public String getComitterEmail() { + return myComitterEmail; + } + + public List getPathsList() { + return myPathsList; + } + + @Override + public String toString() { + return myHash.getValue(); + } +} diff --git a/plugins/git4idea/src/git4idea/history/browser/GitLogTree.java b/plugins/git4idea/src/git4idea/history/browser/GitLogTree.java new file mode 100644 index 000000000000..ea50a9329be7 --- /dev/null +++ b/plugins/git4idea/src/git4idea/history/browser/GitLogTree.java @@ -0,0 +1,1475 @@ +/* + * Copyright 2000-2010 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.history.browser; + +import com.intellij.ide.projectView.PresentationData; +import com.intellij.ide.util.treeView.*; +import com.intellij.openapi.Disposable; +import com.intellij.openapi.actionSystem.*; +import com.intellij.openapi.application.ModalityState; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.Splitter; +import com.intellij.openapi.util.*; +import com.intellij.openapi.vcs.changes.Change; +import com.intellij.openapi.vcs.changes.committed.AbstractCalledLater; +import com.intellij.openapi.vcs.changes.committed.CommittedChangesTreeBrowser; +import com.intellij.openapi.vcs.changes.committed.RepositoryChangesBrowser; +import com.intellij.openapi.vcs.versionBrowser.CommittedChangeList; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.openapi.wm.IdeFocusManager; +import com.intellij.ui.ColoredListCellRenderer; +import com.intellij.ui.ListSpeedSearch; +import com.intellij.ui.PopupHandler; +import com.intellij.ui.SimpleTextAttributes; +import com.intellij.util.*; +import com.intellij.util.ui.UIUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeModel; +import java.awt.*; +import java.awt.event.KeyEvent; +import java.text.DateFormat; +import java.util.*; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +public class GitLogTree implements GitTreeViewI { + private final JPanel myPanel; + private JList myCommitsList; + private final MyCompoundCellRenderer myListCellRenderer; + private final ManageGitTreeView myController; + private final JLabel myStateLabel; + private boolean myRefreshInProgress; + + private final MyErrorRefresher myErrorRefresher; + private final Project myProject; + private Disposable myParentDisposable; + + private final Splitter myMainSplitter; + private final Splitter myFiltersSplitter; + + private JComponent myFiltersPane; + private JComponent myInheritancePane; + + private CommonOnOff myFiltersOnOff; + private CommonOnOff myInheritanceOnOff; + + private final List myInitWaiters; + + private final static DataKey MANAGER_KEY = DataKey.create("MANAGER_KEY"); + private final static DataKey FILTERS_ON_OFF = DataKey.create("FILTERS_ON_OFF"); + private final static DataKey INHERITANCE_ON_OFF = DataKey.create("INHERITANCE_ON_OFF"); + private Set myHighlightedIds; + private TravelTicket myTicket; + private Splitter myListWithDetails; + + private final Alarm myWaitAlarm; + private RepositoryChangesBrowser myRepositoryChangesBrowser; + + public GitLogTree(final Project project, final VirtualFile root) { + myProject = project; + myWaitAlarm = new Alarm(Alarm.ThreadToUse.SWING_THREAD); + myPanel = new MyPanel(new BorderLayout()); + myListCellRenderer = new MyCompoundCellRenderer(); + myErrorRefresher = new MyErrorRefresher(project); + myController = new GitTreeController(project, root, this); + myStateLabel = new JLabel(); + + myMainSplitter = new Splitter(false, 0.2f); + myFiltersSplitter = new Splitter(false, 0.5f); + myMainSplitter.setDividerWidth(3); + myFiltersSplitter.setDividerWidth(3); + + myInitWaiters = new LinkedList(); + myCommitsList = new JList(); + myController.init(); + } + + public JComponent getFocusTarget() { + return myCommitsList; + } + + public void setParentDisposable(Disposable parentDisposable) { + myParentDisposable = parentDisposable; + } + + private class MyPanel extends JPanel implements TypeSafeDataProvider { + private MyPanel() { + } + + private MyPanel(boolean isDoubleBuffered) { + super(isDoubleBuffered); + } + + private MyPanel(LayoutManager layout) { + super(layout); + } + + private MyPanel(LayoutManager layout, boolean isDoubleBuffered) { + super(layout, isDoubleBuffered); + } + + public void calcData(DataKey key, DataSink sink) { + if (MANAGER_KEY.equals(key)) { + sink.put(MANAGER_KEY, myController); + } else if (FILTERS_ON_OFF.equals(key)) { + sink.put(FILTERS_ON_OFF, myFiltersOnOff); + } else if (INHERITANCE_ON_OFF.equals(key)) { + sink.put(INHERITANCE_ON_OFF, myInheritanceOnOff); + } + } + } + + public void controllerReady() { + new AbstractCalledLater(myProject, ModalityState.NON_MODAL) { + public void run() { + for (Runnable initWaiter : myInitWaiters) { + initWaiter.run(); + } + } + }.callMe(); + } + + public void refreshView(@NotNull final List commitsToShow, final TravelTicket ticket) { + new AbstractCalledLater(myProject, ModalityState.NON_MODAL) { + public void run() { + final boolean wasSelected = myCommitsList.getSelectedIndices().length == 0; + myCommitsList.setListData(ArrayUtil.toObjectArray(commitsToShow)); + if ((! commitsToShow.isEmpty()) && (wasSelected || (! Comparing.equal(myTicket, ticket)))) { + myCommitsList.setSelectedIndex(0); + } + myTicket = ticket; + myCommitsList.revalidate(); + myCommitsList.repaint(); + + myListCellRenderer.clear(); + } + }.callMe(); + } + + public void showStatusMessage(@NotNull final String message) { + new AbstractCalledLater(myProject, ModalityState.NON_MODAL) { + public void run() { + myStateLabel.setText(message); + } + }.callMe(); + } + + public void refreshStarted() { + new AbstractCalledLater(myProject, ModalityState.NON_MODAL) { + public void run() { + myRefreshInProgress = true; + myStateLabel.setText("Refreshing..."); + } + }.callMe(); + } + + public void refreshFinished() { + new AbstractCalledLater(myProject, ModalityState.NON_MODAL) { + public void run() { + myRefreshInProgress = false; + myStateLabel.setText(""); + } + }.callMe(); + } + + public void initView() { + myHighlightedIds = Collections.emptySet(); + + final JPanel actionsPanel = new MyPanel(new BorderLayout()); + final DefaultActionGroup group = new DefaultActionGroup(); + final MyOpenCloseFilters openFilters = new MyOpenCloseFilters(); + group.add(openFilters); + + openFilters.registerCustomShortcutSet(new CustomShortcutSet(KeyStroke.getKeyStroke(KeyEvent.VK_F, KeyEvent.ALT_DOWN_MASK | KeyEvent.CTRL_DOWN_MASK)), myPanel); + + final MyOpenCloseInheritance openShow = new MyOpenCloseInheritance(); + group.add(openShow); + openShow.registerCustomShortcutSet(new CustomShortcutSet(KeyStroke.getKeyStroke(KeyEvent.VK_E, KeyEvent.ALT_DOWN_MASK | KeyEvent.CTRL_DOWN_MASK)), myPanel); + final MyPreviousAction previousAction = new MyPreviousAction(); + group.add(previousAction); + previousAction.registerCustomShortcutSet(new CustomShortcutSet(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, KeyEvent.ALT_DOWN_MASK | KeyEvent.CTRL_DOWN_MASK)), myPanel); + + final MyNextAction nextAction = new MyNextAction(); + group.add(nextAction); + nextAction.registerCustomShortcutSet(new CustomShortcutSet(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, KeyEvent.ALT_DOWN_MASK | KeyEvent.CTRL_DOWN_MASK)), myPanel); + final ActionManager am = ActionManager.getInstance(); + final ActionToolbar tb = am.createActionToolbar("GitLogTree", group, true); + group.add(new MyRefreshAction()); + + actionsPanel.add(tb.getComponent(), BorderLayout.WEST); + actionsPanel.add(myStateLabel, BorderLayout.CENTER); + + final ActionPopupMenu popup = am.createActionPopupMenu("GitLogTree", group); + myCommitsList.addMouseListener(new PopupHandler() { + @Override + public void invokePopup(Component comp, int x, int y) { + popup.getComponent().show(comp, x, y); + } + }); + + myPanel.add(actionsPanel, BorderLayout.NORTH); + + myCommitsList.setCellRenderer(myListCellRenderer); + + myMainSplitter.setFirstComponent(myFiltersSplitter); + final JScrollPane sp = new JScrollPane(myCommitsList, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + + myRepositoryChangesBrowser = new RepositoryChangesBrowser(myProject, Collections.emptyList(), Collections.emptyList(), null); + myRepositoryChangesBrowser.getDiffAction().registerCustomShortcutSet(CommonShortcuts.getDiff(), myCommitsList); + + myListWithDetails = new Splitter(false, 0.7f); + myListWithDetails.setFirstComponent(sp); + myListWithDetails.setSecondComponent(myRepositoryChangesBrowser); + myListWithDetails.setDividerWidth(2); + + myCommitsList.addListSelectionListener(new ListSelectionListener() { + public void valueChanged(ListSelectionEvent e) { + final Object[] selected = myCommitsList.getSelectedValues(); + myRepositoryChangesBrowser.setChangesToDisplay(Collections.emptyList()); + if (selected != null && selected.length > 0) { + myWaitAlarm.addRequest(new Runnable() { + public void run() { + final Object[] values = myCommitsList.getSelectedValues(); + if (Comparing.equal(selected, values)) { + final List hashes = new ArrayList(values.length); + for (Object value : values) { + hashes.add(((GitCommit) value).getHash()); + } + myController.getDetails(hashes); + } + } + }, 100); + } + } + }); + + myMainSplitter.setSecondComponent(myListWithDetails); + + final JPanel filtersPanel = new JPanel(new BorderLayout()); + + final MyFiltersTree filterControl = + new MyFiltersTree(myProject, myController.getFiltering(), myController, myParentDisposable, "Filter", null, myCommitsList); + filtersPanel.add(filterControl.getComponent(), BorderLayout.CENTER); + + myFiltersPane = filtersPanel; + final JPanel inheritancePanel = new JPanel(new BorderLayout()); + final MyFiltersTree showControl = + new MyFiltersTree(myProject, myController.getHighlighting(), myController, myParentDisposable, "Emphasize", null, myCommitsList); + inheritancePanel.add(showControl.getComponent(), BorderLayout.CENTER); + myInheritancePane = inheritancePanel; + + myFiltersOnOff = new CommonOnOff(myFiltersPane, true, filterControl.getTree()); + myInheritanceOnOff = new CommonOnOff(myInheritancePane, false, showControl.getTree()); + myFiltersOnOff.setPeer(myInheritanceOnOff); + myInheritanceOnOff.setPeer(myFiltersOnOff); + + myPanel.add(myMainSplitter, BorderLayout.CENTER); + + new ListSpeedSearch(myCommitsList, new Function() { + public String fun(Object o) { + if (o instanceof GitCommit) { + final GitCommit gc = (GitCommit)o; + return gc.getDescription() + gc.getHash() + gc.getCommitter(); + } + return null; + } + }); + } + + private class CommonOnOff implements OnOff { + private boolean myState; + private final JComponent myComponent; + private OnOff myPeer; + private final boolean myIsFirst; + private final JComponent myFocusTarget; + + protected CommonOnOff(JComponent component, boolean isFirst, JComponent focusTarget) { + myComponent = component; + myIsFirst = isFirst; + myFocusTarget = focusTarget; + } + + public void setPeer(final OnOff peer) { + myPeer = peer; + } + + public boolean isOn() { + return myState; + } + + public void on() { + if (isOn()) return; + + if (myIsFirst) { + myFiltersSplitter.setFirstComponent(myComponent); + } else { + myFiltersSplitter.setSecondComponent(myComponent); + } + if (! myPeer.isOn()) { + myMainSplitter.setFirstComponent(myFiltersSplitter); + myMainSplitter.doLayout(); + } + myFiltersSplitter.doLayout(); + SwingUtilities.invokeLater(new Runnable() { + public void run() { + IdeFocusManager.getInstance(myProject).requestFocus(myFocusTarget, true); + } + }); + myState = true; + } + + public void off() { + if (! isOn()) return; + + if (myIsFirst) { + myFiltersSplitter.setFirstComponent(null); + } else { + myFiltersSplitter.setSecondComponent(null); + } + myFiltersSplitter.doLayout(); + myState = false; + if (! myPeer.isOn()) { + myMainSplitter.setFirstComponent(null); + myMainSplitter.doLayout(); + } + } + } + + public JPanel getComponent() { + return myPanel; + } + + // todo details? + public void acceptError(final String text) { + myErrorRefresher.setText(text); + myErrorRefresher.callMe(); + } + + public void acceptHighlighted(final Set ids) { + new AbstractCalledLater(myProject, ModalityState.NON_MODAL) { + public void run() { + myHighlightedIds = ids; + myCommitsList.revalidate(); + myCommitsList.repaint(); + } + }.callMe(); + } + + public void clearHighlighted() { + new AbstractCalledLater(myProject, ModalityState.NON_MODAL) { + public void run() { + myHighlightedIds = Collections.emptySet(); + myCommitsList.revalidate(); + myCommitsList.repaint(); + } + }.callMe(); + } + + private class MyOpenCloseFilters extends ToggleAction { + private MyOpenCloseFilters() { + super("Open Filters", "Open Filters", IconLoader.getIcon("/icons/filter.png")); + } + + @Override + public boolean isSelected(AnActionEvent e) { + final OnOff onoff = myFiltersOnOff; + if (onoff == null) { + return false; + } + return onoff.isOn(); + } + + @Override + public void setSelected(AnActionEvent e, boolean state) { + final OnOff onoff = myFiltersOnOff; + if (onoff == null) { + return; + } + if (state) { + onoff.on(); + } else { + onoff.off(); + } + } + + @Override + public void update(AnActionEvent e) { + super.update(e); + final Presentation presentation = e.getPresentation(); + presentation.setEnabled(myController.isInitialized()); + final String text = (myFiltersOnOff == null) || myFiltersOnOff.isOn() ? "Close Filters" : "Open Filters"; + presentation.setText(text); + presentation.setDescription(text); + } + } + + private class MyOpenCloseInheritance extends ToggleAction { + private MyOpenCloseInheritance() { + super("Open Emphasize", "Open Emphasize", IconLoader.getIcon("/icons/show.png")); + } + + @Override + public boolean isSelected(AnActionEvent e) { + final OnOff onoff = myInheritanceOnOff; + if (onoff == null) { + return false; + } + return onoff.isOn(); + } + + @Override + public void setSelected(AnActionEvent e, boolean state) { + final OnOff onoff = myInheritanceOnOff; + if (onoff == null) { + return; + } + if (state) { + onoff.on(); + } else { + onoff.off(); + } + } + @Override + public void update(AnActionEvent e) { + super.update(e); + final Presentation presentation = e.getPresentation(); + presentation.setEnabled(myController.isInitialized()); + final String text = (myInheritanceOnOff == null) || myInheritanceOnOff.isOn() ? "Close Emphasize" : "Open Emphasize"; + presentation.setText(text); + presentation.setDescription(text); + } + } + + private class MyNextAction extends AnAction { + private MyNextAction() { + super("Next Page", "Next Page", IconLoader.getIcon("/actions/nextfile.png")); + } + + @Override + public void update(AnActionEvent e) { + super.update(e); + + final ManageGitTreeView manager = myController; + e.getPresentation().setEnabled((! myRefreshInProgress) && manager != null && manager.hasNext(myTicket)); + } + + @Override + public void actionPerformed(AnActionEvent e) { + final ManageGitTreeView manager = myController; + if (manager == null || (! manager.hasNext(myTicket))) { + return; + } + manager.next(myTicket); + } + } + + private class MyPreviousAction extends AnAction { + private MyPreviousAction() { + super("Previous Page", "Previous Page", IconLoader.getIcon("/actions/prevfile.png")); + } + + @Override + public void actionPerformed(AnActionEvent e) { + final ManageGitTreeView manager = myController; + if (manager == null || (! manager.hasPrevious(myTicket))) { + return; + } + manager.previous(myTicket); + } + + @Override + public void update(AnActionEvent e) { + super.update(e); + + final ManageGitTreeView manager = myController; + e.getPresentation().setEnabled((! myRefreshInProgress) && manager != null && manager.hasPrevious(myTicket)); + } + } + + private class MyRefreshAction extends AnAction { + private MyRefreshAction() { + super("Refresh", "Refresh", IconLoader.getIcon("/actions/sync.png")); + } + + @Override + public void actionPerformed(AnActionEvent e) { + myController.refresh(); + } + } + + private static class MyErrorRefresher extends AbstractCalledLater { + private final AtomicReference myText; + + private MyErrorRefresher(final Project project) { + super(project, ModalityState.NON_MODAL); + myText = new AtomicReference(); + } + + public void setText(final String value) { + myText.set(value); + } + + public void clear() { + // todo null? + myText.set(""); + } + + public void run() { + // todo + } + } + + private final static DateFormat ourDateFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT); + + private class MyCompoundCellRenderer implements ListCellRenderer { + private final JPanel myPanel; + private final MyTextPartRenderer myTextPartRenderer; + private final Map myTagMap; + private final Map myBranchMap; + + public void clear() { + myTagMap.clear(); + myBranchMap.clear(); + } + + private MyCompoundCellRenderer() { + myPanel = new JPanel(new GridBagLayout()); + myTextPartRenderer = new MyTextPartRenderer(); + myTagMap = new HashMap(); + myBranchMap = new HashMap(); + } + + public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { + if (value instanceof GitCommit) { + final GitCommit gc = (GitCommit)value; + + final List branches = gc.getBranches(); + final List tags = gc.getTags(); + if (branches.isEmpty() && tags.isEmpty()) { + myTextPartRenderer.setLeftPartWidth(0); + return myTextPartRenderer.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); + } + + final GridBagConstraints gb = + new GridBagConstraints(0, 0, 1, 1, 0, 0, GridBagConstraints.NORTHWEST, GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0); + gb.anchor = GridBagConstraints.WEST; + myPanel.removeAll(); + myPanel.setBackground(list.getBackground()); + int leftWidth = 0; + for (String branch : branches) { + Icon icon = myBranchMap.get(branch); + if (icon == null) { + icon = new CaptionIcon(Colors.ourBranch, list.getFont(), branch, list); + myBranchMap.put(branch, icon); + } + leftWidth += icon.getIconWidth(); + myPanel.add(new JLabel(icon), gb); + ++ gb.gridx; + } + for (String tag : tags) { + Icon icon = myTagMap.get(tag); + if (icon == null) { + icon = new CaptionIcon(Colors.ourTag, list.getFont(), tag, list); + myTagMap.put(tag, icon); + } + leftWidth += icon.getIconWidth(); + myPanel.add(new JLabel(icon), gb); + ++ gb.gridx; + } + myTextPartRenderer.setLeftPartWidth(leftWidth); + gb.anchor = GridBagConstraints.NORTHWEST; + gb.fill = GridBagConstraints.HORIZONTAL; + gb.weightx = 1; + myPanel.add(myTextPartRenderer.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus), gb); + return myPanel; + } + return null; + } + } + + private class MyTextPartRenderer extends FictiveTableCellRenderer { + private int myLeftPartWidth; + + private MyTextPartRenderer() { + myLeftPartWidth = 0; + } + + @Override + protected boolean willRender(Object value) { + return value instanceof GitCommit; + } + + @Nullable + @Override + protected Color getBgrndColor(JList list, Object value, int index, boolean selected, boolean hasFocus) { + if (value instanceof GitCommit) { + final GitCommit commit = (GitCommit) value; + if (selected) { + return UIUtil.getListSelectionBackground(); + } else { + if (myHighlightedIds.contains(commit.getHash())) { + return Colors.ourHighlighting; + } else { + return UIUtil.getListBackground(); + } + } + } + return null; + } + + @Override + protected int getParentWidth(JList list) { + return list.getParent().getWidth() - myLeftPartWidth; + } + + @Override + protected Description getDescription(Object value) { + final GitCommit node = (GitCommit)value; + final List> list = new ArrayList>(4); + list.add(new Pair(node.getHash().getValue().substring(0, 8) + " ", SimpleTextAttributes.REGULAR_BOLD_ATTRIBUTES)); + list.add(new Pair(node.getDescription() + " ", SimpleTextAttributes.REGULAR_ATTRIBUTES)); + list.add(new Pair(node.getAuthor() + ", ", SimpleTextAttributes.REGULAR_BOLD_ATTRIBUTES)); + // 7-8 + list.add(new Pair(ourDateFormat.format(node.getDate()), SimpleTextAttributes.REGULAR_ATTRIBUTES)); + return new Description(1, list) { + @Override + public String getMaxString(int idx) { + if (idx == 0) { + return "wwwwwwww"; + } + return null; + } + }; + } + + @Override + protected Trinity getMoreTag() { + //return new Trinity("todo", SimpleTextAttributes.LINK_ATTRIBUTES, new Object()); + return new Trinity(" ...", SimpleTextAttributes.REGULAR_ATTRIBUTES, new Object()); + } + + public void setLeftPartWidth(int leftPartWidth) { + myLeftPartWidth = leftPartWidth; + } + } + + private class MyColoredListCellRenderer extends ColoredListCellRenderer { + @Override + protected void customizeCellRenderer(JList list, Object value, int index, boolean selected, boolean hasFocus) { + if (value instanceof GitCommit) { + final GitCommit commit = (GitCommit) value; + + Color usualColor; + if (selected) { + usualColor = UIUtil.getListSelectionBackground(); + } else { + if (myHighlightedIds.contains(commit.getHash())) { + usualColor = UIUtil.getToolTipBackground().darker(); + } else { + usualColor = UIUtil.getListBackground(); + } + /*usualColor = myHighlightedIds.contains(commit.getHash()) ? UIUtil.getToolTipBackground() : UIUtil.getListBackground(); + if ((! selected) && (index % 3 == 0)) { + usualColor = getSecondStripeColor(usualColor); + }*/ + } + setBackground(usualColor); + + drawRefs(commit.getBranches(), Color.green); + drawRefs(commit.getTags(), Color.blue); + + append(" "); + appendAlign(60); + String descr = commit.getDescription(); + descr = (descr.length() > 50) ? descr.substring(0, 47) + "..." : descr; + append(descr + " "); + + append(commit.getAuthor(), SimpleTextAttributes.REGULAR_BOLD_ATTRIBUTES); + append(" " + commit.getDate()); + } else if (value instanceof String) { + append((String)value); + } + } + + private void drawRefs(final List referencies, final Color color) { + if (referencies != null && (! referencies.isEmpty())) { + final SimpleTextAttributes attrs = + new SimpleTextAttributes(SimpleTextAttributes.STYLE_BOLD | SimpleTextAttributes.STYLE_UNDERLINE, color); + for (String reference : referencies) { + append(reference, attrs); + append(" "); + } + } + } + } + + + private static Color getSecondStripeColor(final Color bg) { + int delta = - 15; + /*if (bg.getRed() < 25 || bg.getGreen() < 25 || bg.getBlue() < 25) { + delta = 15; + }*/ + return new Color(bg.getRed() + delta, bg.getGreen() + delta, bg.getBlue() + delta); + } + + /*private static class BranchFilterComponent { + private final ManageGitTreeView myController; + private final JPanel myPanel; + private final JList myInclude; + //private final JList myExclude; + private final List myIncludedData; + private int myTagsStartIdx; + + private BranchFilterComponent(final ManageGitTreeView controller, final List initWaiters) { + myController = controller; + + myPanel = new JPanel(new BorderLayout()); + myInclude = new JList(); + myIncludedData = new LinkedList(); + //myExclude = new JList(); + + initView(initWaiters); + } + + private void initView(final List initWaiters) { + //BorderFactory.createEmptyBorder(1,1,1,1) + final Border border = BorderFactory.createTitledBorder("Only branches/tags:"); + myPanel.setBorder(border); + myInclude.setListData(new Object[0]); + //myPanel.add(new JLabel("Only branches/tags:"), BorderLayout.NORTH); + + final DefaultActionGroup group = new DefaultActionGroup(); + final MyAddAction addAction = new MyAddAction(); + addAction.registerCustomShortcutSet(CommonShortcuts.INSERT, myPanel); + group.add(addAction); + final MyRemoveAction removeAction = new MyRemoveAction(); + removeAction.registerCustomShortcutSet(CommonShortcuts.DELETE, myPanel); + group.add(removeAction); + final ActionToolbar tb = ActionManager.getInstance().createActionToolbar("branchFilter", group, true); + myPanel.add(tb.getComponent(), BorderLayout.NORTH); + + //final JList selectedList = new JList(); + //selectedList.setPreferredSize(new Dimension()); + final JScrollPane listScroll = new JScrollPane(myInclude) { + @Override + public Dimension getPreferredSize() { + return super.getPreferredSize(); + } + }; + myPanel.add(listScroll, BorderLayout.CENTER); + + } + + public JPanel getPanel() { + return myPanel; + } + }*/ + + public void acceptDetails(List changeList) { + final List changes = CommittedChangesTreeBrowser.collectChanges(changeList, false); + + myListWithDetails.doLayout(); + myRepositoryChangesBrowser.setChangesToDisplay(changes); + myRepositoryChangesBrowser.repaint(); + } + + private static class MyFiltersTree { + private final JPanel myPanel; + private final Project myProject; + private final GitTreeFiltering myFiltering; + private final RepositoryCommonData myCommonData; + private final Disposable myParentDisposable; + private final String myTitle; + private MyTreeStructure myStructure; + private JTree myTree; + private AbstractTreeBuilder myAtb; + private final Icon myChildrenIcon; + private final JList myRefreshComponent; + + private MyFiltersTree(final Project project, final GitTreeFiltering filtering, final RepositoryCommonData commonData, + final Disposable parentDisposable, String title, Icon childrenIcon, final JList refreshComponent) { + myProject = project; + myFiltering = filtering; + myCommonData = commonData; + myParentDisposable = parentDisposable; + myTitle = title; + myChildrenIcon = childrenIcon; + myRefreshComponent = refreshComponent; + myPanel = new JPanel(new BorderLayout()); + initView(childrenIcon); + } + + public JComponent getComponent() { + return myPanel; + } + + public JComponent getTree() { + return myTree; + } + + private static class MyTreeStructure extends AbstractTreeStructure { + private final MyRootNode myRoot; + + private MyTreeStructure(final RepositoryCommonData commonData, final GitTreeFiltering filtering, final Project project, + final Icon childrenIcon) { + myRoot = new MyRootNode(project); + myRoot.addChild(new BranchHeader(commonData, filtering, project, myRoot, childrenIcon)); + myRoot.addChild(new TagHeader(commonData, filtering, project, myRoot, childrenIcon)); + myRoot.addChild(new UserHeader(commonData, filtering, project, myRoot, childrenIcon)); + } + + @Override + public void commit() { + } + @Override + public Object getRootElement() { + return myRoot; + } + @Override + public Object[] getChildElements(Object element) { + if (element instanceof MyAbstractNode) { + // todo remove convertion??? + final List children = ((MyAbstractNode)element).getChildren(); + return children.toArray(new Object[children.size()]); + } + throw new IllegalStateException(); + } + @Override + public Object getParentElement(Object element) { + if (element instanceof MyAbstractNode) { + return ((MyAbstractNode)element).getParent(); + } + throw new IllegalStateException(); + } + @NotNull + @Override + public NodeDescriptor createDescriptor(Object element, NodeDescriptor parentDescriptor) { + if (element instanceof MyAbstractNode) { + return ((MyAbstractNode)element).getDescriptor(); + } + throw new IllegalStateException(); + } + @Override + public boolean hasSomethingToCommit() { + return false; + } + } + + private void initView(Icon childrenIcon) { + myStructure = new MyTreeStructure(myCommonData, myFiltering, myProject, childrenIcon); + myTree = new JTree(); + + myAtb = new AbstractTreeBuilder(myTree, new DefaultTreeModel(new DefaultMutableTreeNode()), myStructure, null); + myAtb.initRootNode(); + Disposer.register(myParentDisposable, myAtb); + myTree.setDragEnabled(false); + myTree.setShowsRootHandles(true); + myTree.setRootVisible(false); + myTree.setCellRenderer(new NodeRenderer()); + + final DefaultActionGroup group = new DefaultActionGroup(); + final MyAddAction addAction = new MyAddAction(); + addAction.registerCustomShortcutSet(CommonShortcuts.INSERT, myPanel); + group.add(addAction); + final MyRemoveAction removeAction = new MyRemoveAction(); + removeAction.registerCustomShortcutSet(CommonShortcuts.DELETE, myPanel); + group.add(removeAction); + final ActionManager am = ActionManager.getInstance(); + final ActionPopupMenu actionPopupMenu = am.createActionPopupMenu("branchFilter", group); + + myTree.addMouseListener(new PopupHandler() { + @Override + public void invokePopup(Component comp, int x, int y) { + actionPopupMenu.getComponent().show(comp, x, y); + } + }); +// myPanel.add(tb.getComponent(), BorderLayout.NORTH); + final JLabel titleLabel = new JLabel(myTitle); + titleLabel.setFont(titleLabel.getFont().deriveFont(Font.BOLD)); + myPanel.add(titleLabel, BorderLayout.NORTH); + + myPanel.add(new JScrollPane(myTree), BorderLayout.CENTER); + + //tree.set + } + + /*@Nullable + private Object[] getSelectedNodes() { + //final TreePath[] paths = myTree.getSelectionPaths(); + final Set selectedElements = myAtb.getSelectedElements(); + if (selectedElements == null) return null; + final Object[] result = new Object[selectedElements.length]; + for (int i = 0; i < paths.length; i++) { + final TreePath path = paths[i]; + result[i] = ((DefaultMutableTreeNode) path.getLastPathComponent()).getUserObject(); + } + return result; + }*/ + + @Nullable + private Object getFirstSelected() { + final Set selected = myAtb.getSelectedElements(); + if (selected.isEmpty()) return null; + return selected.iterator().next(); + } + + private boolean nodesAreOfSameType() { + final Set selectedValues = myAtb.getSelectedElements(); + if (selectedValues.size() == 0) return false; + NodeType type = null; + for (Object selectedValue : selectedValues) { + if (! (selectedValue instanceof MyAbstractNode)) { + return false; + } + final MyAbstractNode node = (MyAbstractNode) selectedValue; + if ((type != null) && (! type.equals(node.getNodeType()))) { + return false; + } + type = node.getNodeType(); + } + return true; + } + + private class MyAddAction extends AnAction { + private final Consumer myAfterAction; + //private TreeState myState; + + private MyAddAction() { + super("Add to filter", "Add to filter", IconLoader.getIcon("/general/add.png")); + myAfterAction = new Consumer() { + public void consume(final Object object) { + myAtb.addSubtreeToUpdate((DefaultMutableTreeNode) myTree.getModel().getRoot(), new Runnable() { + public void run() { + myAtb.expand(object, null); + SwingUtilities.invokeLater(new Runnable() { + public void run() { + myRefreshComponent.revalidate(); + myRefreshComponent.repaint(); + } + }); + } + }); + } + }; + } + + @Override + public void actionPerformed(AnActionEvent e) { + if (! nodesAreOfSameType()) return; + final Object selectedValue = getFirstSelected(); + if (! ((MyAbstractNode) selectedValue).canInsert()) return; + + final MyAbstractNode node = (MyAbstractNode)selectedValue; + node.onInsert(e.getDataContext(), myAfterAction); + } + + @Override + public void update(AnActionEvent e) { + super.update(e); + e.getPresentation().setEnabled(nodesAreOfSameType() && ((MyAbstractNode) getFirstSelected()).canInsert()); + } + } + + private class MyRemoveAction extends AnAction { + private final Runnable myAfterAction; + + private MyRemoveAction() { + super("Remove from filter", "Remove from filter", IconLoader.getIcon("/general/remove.png")); + myAfterAction = new Runnable() { + public void run() { + myAtb.addSubtreeToUpdate((DefaultMutableTreeNode) myTree.getModel().getRoot()); + SwingUtilities.invokeLater(new Runnable() { + public void run() { + myRefreshComponent.revalidate(); + myRefreshComponent.repaint(); + } + }); + } + }; + } + + @Override + public void update(AnActionEvent e) { + super.update(e); + + e.getPresentation().setEnabled(canDelete()); + } + + private boolean canDelete() { + if (! nodesAreOfSameType()) return false; + final Set selectedElements = myAtb.getSelectedElements(); + for (Object object : selectedElements) { + if (! ((MyAbstractNode) object).canBeDeleted()) return false; + } + return true; + } + + @Override + public void actionPerformed(AnActionEvent e) { + if (! canDelete()) return; + + final Set selectedElements = myAtb.getSelectedElements(); + for (Object value : selectedElements) { + ((MyAbstractNode) value).onDelete(myAfterAction); + } + myAfterAction.run(); +// myAtb.addSubtreeToUpdate(myAtb.getRootNode()); + //myAtb.addSubtreeToUpdate((DefaultMutableTreeNode) myTree.getModel().getRoot()); + + /*((DefaultTreeModel) myTree.getModel()).reload(); + myTree.revalidate(); + myTree.repaint();*/ + } + } + + /*private static class MyFilterCellRenderer extends ColoredTreeCellRenderer { + @Override + public void customizeCellRenderer(JTree tree, + Object value, + boolean selected, + boolean expanded, + boolean leaf, + int row, + boolean hasFocus) { + if (value instanceof MyAbstractNode) { + final MyAbstractNode abstractNode = (MyAbstractNode) value; + abstractNode.renderItself(selected, expanded, leaf, row, hasFocus, this); + } + } + }*/ + + private static class BranchNode extends MyAbstractNode { + private BranchNode(RepositoryCommonData commonData, + GitTreeFiltering filtering, + Project project, BranchHeader branchHeader, + String text) { + super(false, commonData, filtering, project, Color.green, branchHeader, text, NodeType.branch, null); + } + + @Override + boolean canBeDeleted() { + return true; + } + @Override + NodeType getNodeType() { + return NodeType.branch; + } + @Override + boolean canInsert() { + return true; + } + @Override + void onInsert(DataContext dc, final Consumer after) { + final List list = myCommonData.getAllBranchesOrdered(); + + SelectGitBranchesPopup.showMe(new Consumer() { + public void consume(Object[] objects) { + for (Object o : objects) { + final String branchName = ((String) o); + + final BranchNode newNode = new BranchNode(myCommonData, myFiltering, myProject, getParent(), branchName); + getParent().addChild(newNode); + + // put into controller + myFiltering.addStartingPoint(branchName); + after.consume(getParent()); + } + } + }, list, false, dc); + } + + @Override + void onDelete(Runnable after) { + myFiltering.removeStartingPoint(getText()); + getParent().removeChild(this); + } + } + + private static class TagNode extends MyAbstractNode { + private TagNode(RepositoryCommonData commonData, GitTreeFiltering filtering, Project project, TagHeader tagHeader, String text, + final Icon icon) { + super(false, commonData, filtering, project, Color.blue, tagHeader, text, NodeType.tag, icon); + } + + @Override + boolean canBeDeleted() { + return true; + } + @Override + boolean canInsert() { + return true; + } + @Override + void onInsert(DataContext dc, final Consumer after) { + final List list = myCommonData.getAllTagsOrdered(); + + SelectGitBranchesPopup.showMe(new Consumer() { + public void consume(Object[] objects) { + for (Object o : objects) { + final String branchName = ((String) o); + + final TagNode newNode = new TagNode(myCommonData, myFiltering, myProject, myParent, branchName, myIcon); + getParent().addChild(newNode); + + // put into controller + myFiltering.addStartingPoint(branchName); + after.consume(getParent()); + } + } + }, list, true, dc); + } + + @Override + void onDelete(Runnable after) { + myFiltering.removeStartingPoint(getText()); + getParent().removeChild(this); + } + } + + private static class BranchHeader extends MyAbstractNode { + private final Icon myChildrenIcon; + + private BranchHeader(RepositoryCommonData commonData, + GitTreeFiltering filtering, + Project project, + MyRootNode parent, + Icon childrenIcon) { + super(true, commonData, filtering, project, null, parent, "Branches", NodeType.branchesHead, null); + myChildrenIcon = childrenIcon; + } + + @Override + boolean canBeDeleted() { + return false; + } + @Override + boolean canInsert() { + return true; + } + @Override + void onInsert(DataContext dc, final Consumer after) { + final List list = myCommonData.getAllBranchesOrdered(); + + SelectGitBranchesPopup.showMe(new Consumer() { + public void consume(Object[] objects) { + for (Object o : objects) { + final String branchName = ((String) o); + + final BranchNode newNode = new BranchNode(myCommonData, myFiltering, myProject, BranchHeader.this, branchName); + addChild(newNode); + + // put into controller + myFiltering.addStartingPoint(branchName); + + after.consume(BranchHeader.this); + } + } + }, list, false, dc); + } + + @Override + void onDelete(Runnable after) { + } + } + + private static class TagHeader extends MyAbstractNode { + private final Icon myChildrenIcon; + + private TagHeader(RepositoryCommonData commonData, GitTreeFiltering filtering, Project project, MyRootNode parent, Icon childrenIcon) { + super(true, commonData, filtering, project, null, parent, "Tags", NodeType.tagsHead, null); + myChildrenIcon = childrenIcon; + } + + @Override + boolean canBeDeleted() { + return false; + } + @Override + boolean canInsert() { + return true; + } + @Override + void onInsert(DataContext dc, final Consumer after) { + final List list = myCommonData.getAllTagsOrdered(); + + SelectGitBranchesPopup.showMe(new Consumer() { + public void consume(Object[] objects) { + for (Object o : objects) { + final String branchName = ((String) o); + + final TagNode newNode = new TagNode(myCommonData, myFiltering, myProject, TagHeader.this, branchName, myChildrenIcon); + addChild(newNode); + + // put into controller + myFiltering.addStartingPoint(branchName); + after.consume(TagHeader.this); + } + } + }, list, true, dc); + } + + @Override + void onDelete(Runnable after) { + } + } + + private static class UserNode extends MyAbstractNode { + private final ChangesFilter.Filter[] myFilters; + + private UserNode(RepositoryCommonData commonData, + GitTreeFiltering filtering, + Project project, + UserHeader userHeader, + String text, ChangesFilter.Filter[] filters, Icon icon) { + super(false, commonData, filtering, project, UIUtil.getTextAreaForeground(), userHeader, text, NodeType.user, icon); + myFilters = filters; + } + @Override + boolean canBeDeleted() { + return true; + } + @Override + boolean canInsert() { + return true; + } + + @Override + void onInsert(DataContext dc, final Consumer after) { + UsersPopup.showUsersPopup(myCommonData.getKnownUsers(), new Consumer() { + public void consume(String name) { + final ChangesFilter.Committer committer = new ChangesFilter.Committer(name); + myFiltering.addFilter(committer); + final ChangesFilter.Author author = new ChangesFilter.Author(name); + myFiltering.addFilter(author); + + final UserNode newNode = new UserNode(myCommonData, myFiltering, myProject, getParent(), name, + new ChangesFilter.Filter[] {committer, author}, myIcon); + getParent().addChild(newNode); + + after.consume(getParent()); + } + }, dc); + } + + @Override + void onDelete(Runnable after) { + for (ChangesFilter.Filter filter : myFilters) { + myFiltering.removeFilter(filter); + } + getParent().removeChild(this); + } + } + + private static class UserHeader extends MyAbstractNode { + private final Icon myChildrenIcon; + + private UserHeader(RepositoryCommonData commonData, GitTreeFiltering filtering, Project project, MyRootNode parent, Icon childrenIcon) { + super(true, commonData, filtering, project, null, parent, "Users", NodeType.userHead, null); + myChildrenIcon = childrenIcon; + } + + @Override + boolean canBeDeleted() { + return false; + } + @Override + boolean canInsert() { + return true; + } + @Override + void onInsert(DataContext dc, final Consumer after) { + UsersPopup.showUsersPopup(myCommonData.getKnownUsers(), new Consumer() { + public void consume(String name) { + final ChangesFilter.Committer committer = new ChangesFilter.Committer(name); + myFiltering.addFilter(committer); + final ChangesFilter.Author author = new ChangesFilter.Author(name); + myFiltering.addFilter(author); + + final UserNode newNode = new UserNode(myCommonData, myFiltering, myProject, UserHeader.this, name, + new ChangesFilter.Filter[] {committer, author}, myChildrenIcon); + addChild(newNode); + + after.consume(UserHeader.this); + } + }, dc); + } + + @Override + void onDelete(Runnable after) { + } + } + + private static abstract class MyAbstractNode { + protected final Project myProject; + protected final GitTreeFiltering myFiltering; + private final boolean myAllowsChildren; + protected final RepositoryCommonData myCommonData; + @Nullable + private final Color myColor; + + protected final Parent myParent; + private List myChildren; + + private final String myText; + private final NodeType myType; + @Nullable protected final Icon myIcon; + private final NodeDescriptor myDescriptor; + + protected MyAbstractNode(boolean allowsChildren, + RepositoryCommonData commonData, + GitTreeFiltering filtering, + Project project, @Nullable Color color, final Parent parent, final String text, NodeType type, + @Nullable final Icon icon) { + myAllowsChildren = allowsChildren; + myCommonData = commonData; + myFiltering = filtering; + myProject = project; + myColor = color; + myParent = parent; + myText = text; + myType = type; + myIcon = icon; + myChildren = myAllowsChildren ? new LinkedList() : Collections.emptyList(); + myDescriptor = new PresentableNodeDescriptor(myProject, myParent == null ? null : myParent.getDescriptor()) { + @Override + public PresentableNodeDescriptor getChildToHighlightAt(int index) { + return null; + } + @Override + protected void update(PresentationData presentation) { + presentation.setClosedIcon(icon); + presentation.setOpenIcon(icon); + presentation.setPresentableText(myText); + } + + @Override + public Object getElement() { + return MyAbstractNode.this; + } + }; + } + + public NodeDescriptor getDescriptor() { + return myDescriptor; + } + + void addChild(final Child child) { + assert myAllowsChildren; + myChildren.add(child); + } + + public Parent getParent() { + return myParent; + } + + boolean isAllowsChildren() { + return myAllowsChildren; + } + + public String getText() { + return myText; + } + + public Color getColor() { + return myColor; + } + + public List getChildren() { + return myChildren; + } + public void removeChild(final Child child) { + myChildren.remove(child); + } + + NodeType getNodeType() { + return myType; + } + + abstract boolean canInsert(); // peer or child + abstract boolean canBeDeleted(); //self + abstract void onInsert(DataContext dc, Consumer after); + abstract void onDelete(Runnable after); + //abstract void renderItself(boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus, final SimpleColoredComponent coloredComponent); + + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + MyAbstractNode that = (MyAbstractNode)o; + + if (myText != null ? !myText.equals(that.myText) : that.myText != null) return false; + if (myType != that.myType) return false; + + return true; + } + + @Override + public int hashCode() { + int result = myText != null ? myText.hashCode() : 0; + result = 31 * result + myType.hashCode(); + return result; + } + } + + private static class MyRootNode extends MyAbstractNode { + private MyRootNode(final Project project) { + super(true, null, null, project, null, null, "Root", NodeType.root, null); + } + @Override + boolean canBeDeleted() { + return false; + } + @Override + boolean canInsert() { + return false; + } + @Override + void onInsert(DataContext dc, Consumer after) { + } + @Override + void onDelete(Runnable after) { + } + } + + private static enum NodeType { + root, + branchesHead, + tagsHead, + branch, + tag, + userHead, + user, + before, + after, + selectedHeader, + selectedNode + } + } + + private interface Colors { + Color ourHighlighting = new Color(255, 255, 200); + Color ourBranch = new Color(226, 255, 174); + Color ourTag = new Color(193, 223, 247); + } +} diff --git a/plugins/git4idea/src/git4idea/history/browser/GitProjectLogManager.java b/plugins/git4idea/src/git4idea/history/browser/GitProjectLogManager.java new file mode 100644 index 000000000000..078447745694 --- /dev/null +++ b/plugins/git4idea/src/git4idea/history/browser/GitProjectLogManager.java @@ -0,0 +1,150 @@ +/* + * Copyright 2000-2010 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.history.browser; + +import com.intellij.openapi.components.ProjectComponent; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.project.DumbAwareRunnable; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.startup.StartupManager; +import com.intellij.openapi.vcs.ProjectLevelVcsManager; +import com.intellij.openapi.vcs.VcsException; +import com.intellij.openapi.vcs.VcsListener; +import com.intellij.openapi.vcs.changes.ui.ChangesViewContentManager; +import com.intellij.openapi.vfs.VfsUtil; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.ui.content.Content; +import com.intellij.ui.content.ContentFactory; +import com.intellij.util.containers.HashMap; +import git4idea.GitBranchesSearcher; +import git4idea.GitVcs; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; + +public class GitProjectLogManager implements ProjectComponent { + private final static Logger LOG = Logger.getInstance("#git4idea.history.browser.GitProjectLogManager"); + private final Project myProject; + private final ProjectLevelVcsManager myVcsManager; + + private final AtomicReference> myComponentsMap; + private VcsListener myListener; + + public GitProjectLogManager(final Project project, final ProjectLevelVcsManager vcsManager) { + myProject = project; + myVcsManager = vcsManager; + myComponentsMap = new AtomicReference>(new HashMap()); + myListener = new VcsListener() { + public void directoryMappingChanged() { + recalculateWindows(); + } + }; + } + + public static GitProjectLogManager getInstance(final Project project) { + return project.getComponent(GitProjectLogManager.class); + } + + public void projectClosed() { + myVcsManager.removeVcsListener(myListener); + } + + public void projectOpened() { + StartupManager.getInstance(myProject).registerPostStartupActivity(new DumbAwareRunnable() { + public void run() { + myVcsManager.addVcsListener(myListener); + recalculateWindows(); + } + }); + } + + private void recalculateWindows() { + final GitVcs vcs = GitVcs.getInstance(myProject); + final VirtualFile[] roots = myVcsManager.getRootsUnderVcs(vcs); + + final Map currentState = myComponentsMap.get(); + final Set currentKeys = new HashSet(currentState.keySet()); + currentKeys.removeAll(Arrays.asList(roots)); + + final Map newKeys = new HashMap(currentState); + + final ChangesViewContentManager cvcm = ChangesViewContentManager.getInstance(myProject); + + final VirtualFile baseDir = myProject.getBaseDir(); + final ContentFactory contentFactory = ContentFactory.SERVICE.getInstance(); + for (VirtualFile root : roots) { + if (! currentState.containsKey(root)) { + final GitLogTree tree = new GitLogTree(myProject, root); + tree.setParentDisposable(myProject); + tree.initView(); + final Content content = contentFactory.createContent(tree.getComponent(), getCaption(baseDir, root), false); + content.setCloseable(false); + cvcm.addContent(content); + newKeys.put(root, content); + } + } + + for (VirtualFile currentKey : currentKeys) { + final Content content = newKeys.remove(currentKey); + cvcm.removeContent(content); + } + + myComponentsMap.set(newKeys); + } + + private String getCaption(@Nullable VirtualFile baseDir, final VirtualFile root) { + String result = root.getPresentableUrl(); + if (baseDir != null) { + if (baseDir.equals(root)) { + result = ""; + } else { + final String variant = VfsUtil.getRelativePath(baseDir, root, '/'); + if ((variant != null) && (variant.length() < result.length())) { + result = variant; + } + } + } + + GitBranchesSearcher searcher = null; + try { + searcher = new GitBranchesSearcher(myProject, root, false); + } + catch (VcsException e) { + LOG.info(e); + } + + if (searcher != null) { + result += " (" + searcher.getLocal().getName() + ")"; + } + return "Log for: " + result; + } + + @NotNull + public String getComponentName() { + return "git4idea.history.browser.GitProjectLogManager"; + } + + public void initComponent() { + } + + public void disposeComponent() { + } +} diff --git a/plugins/git4idea/src/git4idea/history/browser/GitTreeController.java b/plugins/git4idea/src/git4idea/history/browser/GitTreeController.java new file mode 100644 index 000000000000..1395a1331096 --- /dev/null +++ b/plugins/git4idea/src/git4idea/history/browser/GitTreeController.java @@ -0,0 +1,547 @@ +/* + * Copyright 2000-2010 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.history.browser; + +import com.intellij.openapi.application.Application; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Pair; +import com.intellij.openapi.vcs.RequestsMerger; +import com.intellij.openapi.vcs.VcsException; +import com.intellij.openapi.vcs.changes.Change; +import com.intellij.openapi.vcs.versionBrowser.CommittedChangeList; +import com.intellij.openapi.vcs.versionBrowser.CommittedChangeListImpl; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.util.Alarm; +import com.intellij.util.Consumer; +import com.intellij.util.Processor; +import com.intellij.util.containers.SLRUCache; +import git4idea.changes.GitChangeUtils; +import git4idea.config.GitConfigUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; +import java.util.concurrent.atomic.AtomicReference; + +class GitTreeController implements ManageGitTreeView { + private final Project myProject; + private final VirtualFile myRoot; + private final GitTreeViewI myTreeView; + + private final LowLevelAccess myAccess; + private volatile boolean myInitialized; + + // guarded by lock + private final AtomicReference> myTags; + private final AtomicReference> myBranches; + private final AtomicReference> myUsers; + + private final MyFiltersStateHolder myFilterHolder; + private final MyFiltersStateHolder myHighlightingHolder; + + private final RequestsMerger myFilterRequestsMerger; + //private final RequestsMerger myHighlightingRequestsMerger; + + private final MyUpdateStateInterceptor myFiltering; + private final MyUpdateStateInterceptor myHighlighting; + private Alarm myAlarm; + private Portion myFiltered; + + private final SLRUCache myListsCache = new SLRUCache(16, 64) { + @NotNull + @Override + public CommittedChangeList createValue(SHAHash key) { + try { + return GitChangeUtils.getRevisionChanges(myProject, myRoot, key.getValue()); + } + catch (VcsException e) { + return new CommittedChangeListImpl(e.getMessage(), "", "", -1, null, Collections.emptyList()); + } + } + }; + + GitTreeController(final Project project, final VirtualFile root, final GitTreeViewI treeView) { + myProject = project; + myRoot = root; + myTreeView = treeView; + myAccess = new LowLevelAccessImpl(project, root); + + myFilterHolder = new MyFiltersStateHolder(); + myHighlightingHolder = new MyFiltersStateHolder(); + + myTags = new AtomicReference>(Collections.emptyList()); + myBranches = new AtomicReference>(Collections.emptyList()); + myUsers = new AtomicReference>(Collections.emptyList()); + + // todo !!!!! dispose + myAlarm = new Alarm(Alarm.ThreadToUse.OWN_THREAD, project); + myFilterRequestsMerger = new RequestsMerger(new Runnable() { + public void run() { + if (myFilterHolder.isDirty()) { + final Portion filtered = loadPortion(myFilterHolder.getStartingPoints(), myFilterHolder.getCurrentPoint(), null, + myFilterHolder.getFilters(), PageSizes.LOAD_SIZE); + if (filtered == null) return; + + final List commitList = filtered.getXFrom(0, PageSizes.VISIBLE_PAGE_SIZE); + myFiltered = filtered; + myTreeView.refreshView(commitList, new TravelTicket(filtered.isStartFound(), filtered.getLast().getDate())); + + myFilterHolder.setDirty(false); + } + + // highlighting + if (myHighlightingHolder.isNothingSelected()) { + myTreeView.clearHighlighted(); + return; + } + + myTreeView.acceptHighlighted(loadIdsToHighlight()); + myTreeView.refreshFinished(); + } + }, new Consumer() { + public void consume(Runnable runnable) { + myTreeView.refreshStarted(); + myAlarm.addRequest(runnable, 50); + } + }); + /*myHighlightingRequestsMerger = new RequestsMerger(new Runnable() { + public void run() { + if (myHighlightingHolder.isNothingSelected()) { + myTreeView.clearHighlighted(); + return; + } + + myTreeView.acceptHighlighted(loadIdsToHighlight()); + } + }, new Consumer() { + public void consume(Runnable runnable) { + myTreeView.refreshStarted(); + application.executeOnPooledThread(runnable); + myTreeView.refreshFinished(); + } + });*/ + + myFiltering = new MyUpdateStateInterceptor(myFilterRequestsMerger, myFilterHolder, null); + myHighlighting = new MyUpdateStateInterceptor(myFilterRequestsMerger, myHighlightingHolder, null); + myFilterHolder.setDirty(true); + } + + private Set loadIdsToHighlight() { + final Portion highlighted; + if (myFiltered == null) { + // todo rewrite when dates are introduced + highlighted = loadPortion(myHighlightingHolder.getStartingPoints(), myFilterHolder.getCurrentPoint(), + null, Collections.emptyList(), -1); + } else { + highlighted = loadPortion(myHighlightingHolder.getStartingPoints(), myFilterHolder.getCurrentPoint(), + myFiltered.getLast().getDate(), Collections.emptyList(), -1); + } + + final Collection filters = myHighlightingHolder.getFilters(); + final List combined = ChangesFilter.combineFilters(filters); + + final Set highlightedIds = new HashSet(); + highlighted.iterateFrom(0, new Processor() { + public boolean process(GitCommit gitCommit) { + for (ChangesFilter.MemoryFilter filter : combined) { + if (! filter.applyInMemory(gitCommit)) { + return false; + } + } + highlightedIds.add(gitCommit.getHash()); + return false; + } + }); + + return highlightedIds; + } + + // !!!! after point is included! (should be) + @Nullable + private Portion loadPortion(final List startingPoints, final Date beforePoint, final Date afterPoint, + final Collection filtersIn, int maxCnt) { + try { + final Collection filters = new LinkedList(filtersIn); + if (beforePoint != null) { + filters.add(new ChangesFilter.BeforeDate(new Date(beforePoint.getTime() - 1))); + } + if (afterPoint != null) { + filters.add(new ChangesFilter.AfterDate(afterPoint)); + } + + final Portion portion = new Portion(null); + myAccess.loadCommits(startingPoints, Collections.emptyList(), filters, portion, myBranches.get(), maxCnt); + return portion; + } + catch (VcsException e) { + myTreeView.acceptError(e.getMessage()); + return null; + } + } + + // todo not-so-good + /*@Nullable + private Portion loadPortion(@Nullable final ChangesFilter.Filter filter, final boolean useMaxCnt, final MyFiltersStateHolder... holders) { + try { + final Collection filters = new LinkedList(); + final List startingPoints = new LinkedList(); + + // continuation condition + final Date point = myFilterHolder.getCurrentPoint(); + if (point != null) { + filters.add(new ChangesFilter.BeforeDate(new Date(point.getTime() - 1))); + } + if (filter != null) { + filters.add(filter); + } + + for (MyFiltersStateHolder holder : holders) { + startingPoints.addAll(holder.getStartingPoints()); + filters.addAll(holder.getFilters()); + } + final Portion portion = new Portion(null); + + myAccess.loadCommits(startingPoints, Collections.emptyList(), filters, portion, myBranches.get(), useMaxCnt); + return portion; + } + catch (VcsException e) { + myTreeView.acceptError(e.getMessage()); + return null; + } + }*/ + + private String getStatusMessage() { + // todo + return "Showing"; + } + + private void loadTagsNBranches(final boolean loadBranches, final boolean loadTags, final boolean loadUsers) { + final List branches = new LinkedList(); + final List tags = new LinkedList(); + + try { + if (loadBranches) { + myAccess.loadAllBranches(branches); + Collections.sort(branches); + myBranches.set(branches); + } + if (loadTags) { + myAccess.loadAllTags(tags); + Collections.sort(tags); + myTags.set(tags); + } + + if (loadUsers) { + final List> value = GitConfigUtil.getAllValues(myProject, myRoot, "user.name"); + final String username = value.size() == 1 ? value.get(0).getSecond() : null; + + final Portion p = loadPortion(Collections.emptyList(), null, null, Collections.emptyList(), 500); + final Set users = new HashSet(); + p.iterateFrom(0, new Processor() { + public boolean process(GitCommit gitCommit) { + users.add(gitCommit.getAuthor()); + users.add(gitCommit.getCommitter()); + return false; + } + }); + final ArrayList usersList = new ArrayList(users); + Collections.sort(usersList); + if (username != null) { + usersList.remove(username); + usersList.add(0, username); + } + myUsers.set(usersList); + } + } + catch (VcsException e) { + myTreeView.acceptError(e.getMessage()); + } + } + + // no filter - or saved filter? -> then pass + public void init() { + assert ! myInitialized; + + myAlarm.addRequest(new Runnable() { + public void run() { + myFilterRequestsMerger.request(); + loadTagsNBranches(true, true, false); + myInitialized = true; + myTreeView.controllerReady(); + + myAlarm.addRequest(new Runnable() { + public void run() { + loadTagsNBranches(false, false, true); + } + }, 100); + } + }, 100); + } + + public boolean hasNext(final TravelTicket ticket) { + return (ticket == null) || (! ticket.isIsBottomReached()); + } + + public boolean hasPrevious(final TravelTicket ticket) { + return myFilterHolder.getCurrentPoint() != null; + } + + public void next(final TravelTicket ticket) { + myFilterHolder.addContinuationPoint(ticket.getLatestDate()); + myFilterRequestsMerger.request(); + } + + public void previous(final TravelTicket ticket) { + myFilterHolder.popContinuationPoint(); + myFilterRequestsMerger.request(); + } + + public void refresh() { + myFilterRequestsMerger.request(); + myAlarm.addRequest(new Runnable() { + public void run() { + // todo USERS ARE NOT REFRESHED now! to be fixed after loading them faster + loadTagsNBranches(true, true, false); + } + }, 100); + } + + public void getDetails(final Collection hashes) { + final Application application = ApplicationManager.getApplication(); + myAlarm.addRequest(new Runnable() { + public void run() { + final List loaded = new LinkedList(); + final Set requested = new HashSet(hashes.size()); + for (SHAHash hash : hashes) { + requested.add(GitChangeUtils.longForSHAHash(hash.getValue())); + + final CommittedChangeList changeList = myListsCache.get(hash); + if (requested.contains(changeList.getNumber())) { + loaded.add(changeList); + } + } + if (! loaded.isEmpty()) { + application.invokeLater(new Runnable() { + public void run() { + myTreeView.acceptDetails(loaded); + } + }); + } + } + }, 30); + } + + public GitTreeFiltering getFiltering() { + return myFiltering; + } + + public GitTreeFiltering getHighlighting() { + return myHighlighting; + } + + public List getKnownUsers() { + return new ArrayList(myUsers.get()); + } + + public List getAllBranchesOrdered() { + return new ArrayList(myBranches.get()); + } + + public List getAllTagsOrdered() { + return new ArrayList(myTags.get()); + } + + public boolean isInitialized() { + return myInitialized; + } + + private static class MyUpdateStateInterceptor implements GitTreeFiltering { + private final MyFiltersStateHolder myState; + @Nullable private final RequestsMerger mySecond; + private final RequestsMerger myRequestsMerger; + + protected MyUpdateStateInterceptor(RequestsMerger requestsMerger, MyFiltersStateHolder state, final @Nullable RequestsMerger mySecond) { + myRequestsMerger = requestsMerger; + myState = state; + this.mySecond = mySecond; + } + + private void requestRefresh() { + myRequestsMerger.request(); + if (mySecond != null) { + mySecond.request(); + } + } + + public void addFilter(ChangesFilter.Filter filter) { + myState.addFilter(filter); + requestRefresh(); + } + + public void removeFilter(ChangesFilter.Filter filter) { + myState.removeFilter(filter); + requestRefresh(); + } + + public void addStartingPoint(String ref) { + myState.addStartingPoint(ref); + requestRefresh(); + } + + public void removeStartingPoint(String ref) { + myState.removeStartingPoint(ref); + requestRefresh(); + } + + public void updateExcludePoints(List points) { + myState.updateExcludePoints(points); + requestRefresh(); + } + + public List getStartingPoints() { + return myState.getStartingPoints(); + } + + public List getExcludePoints() { + return myState.getExcludePoints(); + } + + public Collection getFilters() { + return myState.getFilters(); + } + } + + private static class MyFiltersStateHolder implements GitTreeFiltering { + private final Object myLock; + private final List myStartingPoints; + private boolean myDirty; + + private final List myContinuationPoints; + + @Nullable + private List myExcludePoints; + private final Collection myFilters; + + private MyFiltersStateHolder() { + myLock = new Object(); + myStartingPoints = new LinkedList(); + myFilters = new LinkedList(); + myContinuationPoints = new LinkedList(); + } + + public boolean isDirty() { + synchronized (myLock) { + return myDirty; + } + } + + public void setDirty(boolean dirty) { + synchronized (myLock) { + myDirty = dirty; + } + } + + @Nullable + public List getExcludePoints() { + synchronized (myLock) { + return myExcludePoints; + } + } + + // page starts + public void addContinuationPoint(final Date point) { + synchronized (myLock) { + if ((! myContinuationPoints.isEmpty()) && myContinuationPoints.get(myContinuationPoints.size() - 1).equals(point)) return; + myDirty = true; + myContinuationPoints.add(point); + } + } + + @Nullable + public Date popContinuationPoint() { + synchronized (myLock) { + myDirty = true; + return myContinuationPoints.isEmpty() ? null : myContinuationPoints.remove(myContinuationPoints.size() - 1); + } + } + + public Date getCurrentPoint() { + synchronized (myLock) { + return myContinuationPoints.isEmpty() ? null : myContinuationPoints.get(myContinuationPoints.size() - 1); + } + } + + public void addFilter(ChangesFilter.Filter filter) { + synchronized (myLock) { + myDirty = true; + myFilters.add(filter); + myContinuationPoints.clear(); + } + } + + public void removeFilter(ChangesFilter.Filter filter) { + synchronized (myLock) { + myDirty = true; + myFilters.remove(filter); + myContinuationPoints.clear(); + } + } + + public void addStartingPoint(String ref) { + synchronized (myLock) { + myDirty = true; + myStartingPoints.add(ref); + myContinuationPoints.clear(); + } + } + + public void removeStartingPoint(String ref) { + synchronized (myLock) { + myDirty = true; + myStartingPoints.remove(ref); + myContinuationPoints.clear(); + } + } + + public void updateExcludePoints(List points) { + synchronized (myLock) { + myDirty = true; + myExcludePoints = points; + myContinuationPoints.clear(); + } + } + + @Nullable + public List getStartingPoints() { + synchronized (myLock) { + return myStartingPoints; + } + } + + public Collection getFilters() { + synchronized (myLock) { + return myFilters; + } + } + + public boolean isNothingSelected() { + synchronized (myLock) { + return ((myExcludePoints == null) || (myExcludePoints.isEmpty())) && myFilters.isEmpty() && myStartingPoints.isEmpty(); + } + } + } +} diff --git a/plugins/git4idea/src/git4idea/history/browser/GitTreeFiltering.java b/plugins/git4idea/src/git4idea/history/browser/GitTreeFiltering.java new file mode 100644 index 000000000000..1d08396c4e44 --- /dev/null +++ b/plugins/git4idea/src/git4idea/history/browser/GitTreeFiltering.java @@ -0,0 +1,42 @@ +/* + * Copyright 2000-2010 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.history.browser; + +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; +import java.util.List; + +public interface GitTreeFiltering { +// void updateFilter(final Collection filters); +// void updateStartingPoints(final List points); + + void addFilter(final ChangesFilter.Filter filter); + void removeFilter(final ChangesFilter.Filter filter); + + void addStartingPoint(final String ref); + void removeStartingPoint(final String ref); + + void updateExcludePoints(final List points); + + // todo COPIES! + @Nullable + List getStartingPoints(); + @Nullable + List getExcludePoints(); + Collection getFilters(); + +} diff --git a/plugins/git4idea/src/git4idea/history/browser/GitTreeViewI.java b/plugins/git4idea/src/git4idea/history/browser/GitTreeViewI.java new file mode 100644 index 000000000000..f4db71050a8f --- /dev/null +++ b/plugins/git4idea/src/git4idea/history/browser/GitTreeViewI.java @@ -0,0 +1,38 @@ +/* + * Copyright 2000-2010 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.history.browser; + +import com.intellij.openapi.vcs.versionBrowser.CommittedChangeList; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Set; + +public interface GitTreeViewI { + void controllerReady(); + void refreshView(@NotNull final List commitsToShow, final TravelTicket ticket); + void showStatusMessage(@NotNull final String message); + + void refreshStarted(); + void refreshFinished(); + + // todo details? + void acceptError(String text); + void acceptHighlighted(final Set ids); + void clearHighlighted(); + + void acceptDetails(List changeList); +} diff --git a/plugins/git4idea/src/git4idea/history/browser/LazyWorker.java b/plugins/git4idea/src/git4idea/history/browser/LazyWorker.java new file mode 100644 index 000000000000..e6a270d47b5c --- /dev/null +++ b/plugins/git4idea/src/git4idea/history/browser/LazyWorker.java @@ -0,0 +1,20 @@ +/* + * Copyright 2000-2010 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.history.browser; + +public interface LazyWorker { + void accept(final Runnable work, final Runnable afterWork); +} diff --git a/plugins/git4idea/src/git4idea/history/browser/LowLevelAccess.java b/plugins/git4idea/src/git4idea/history/browser/LowLevelAccess.java new file mode 100644 index 000000000000..9764eb9ebd5e --- /dev/null +++ b/plugins/git4idea/src/git4idea/history/browser/LowLevelAccess.java @@ -0,0 +1,35 @@ +/* + * Copyright 2000-2010 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.history.browser; + +import com.intellij.openapi.vcs.VcsException; +import com.intellij.util.Consumer; + +import java.util.Collection; +import java.util.List; + +public interface LowLevelAccess { + GitCommit getCommitByHash(final SHAHash hash); + // todo define signature + void loadCommits(final List startingPoints, final List endPoints, final Collection filters, + final Consumer consumer, final Collection branches, int useMaxCnt) throws VcsException; + + Collection getBranchesWithCommit(final SHAHash hash) throws VcsException; + Collection getTagsWithCommit(final SHAHash hash) throws VcsException; + + void loadAllBranches(final List sink) throws VcsException; + void loadAllTags(final List sink) throws VcsException; +} diff --git a/plugins/git4idea/src/git4idea/history/browser/LowLevelAccessImpl.java b/plugins/git4idea/src/git4idea/history/browser/LowLevelAccessImpl.java new file mode 100644 index 000000000000..a166b4de1789 --- /dev/null +++ b/plugins/git4idea/src/git4idea/history/browser/LowLevelAccessImpl.java @@ -0,0 +1,101 @@ +/* + * Copyright 2000-2010 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.history.browser; + +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vcs.FilePathImpl; +import com.intellij.openapi.vcs.VcsException; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.util.Consumer; +import git4idea.GitBranch; +import git4idea.GitTag; +import git4idea.history.GitHistoryUtils; +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; + +public class LowLevelAccessImpl implements LowLevelAccess { + private final Project myProject; + private final VirtualFile myRoot; + + public LowLevelAccessImpl(final Project project, final VirtualFile root) { + myProject = project; + myRoot = root; + } + + public GitCommit getCommitByHash(SHAHash hash) { + return null; //To change body of implemented methods use File | Settings | File Templates. + } + + public void loadCommits(final @NotNull List startingPoints, @NotNull final List endPoints, + @NotNull final Collection filters, + @NotNull final Consumer consumer, final Collection branches, int useMaxCnt) + throws VcsException { + + final List parameters = new LinkedList(); + if (useMaxCnt > 0) { + parameters.add("--max-count=" + useMaxCnt); + } + + for (ChangesFilter.Filter filter : filters) { + filter.applyToCommandLine(parameters); + } + + if (! startingPoints.isEmpty()) { + for (String startingPoint : startingPoints) { + parameters.add(startingPoint); + } + } else { + parameters.add("--all"); + } + + for (String endPoint : endPoints) { + parameters.add("^" + endPoint); + } + + // todo can easily be done step-by-step + final List commits = + GitHistoryUtils.historyWithLinks(myProject, new FilePathImpl(myRoot), new HashSet(branches), parameters.toArray(new String[parameters.size()])); + for (GitCommit commit : commits) { + consumer.consume(commit); + } + } + + public Collection getBranchesWithCommit(final SHAHash hash) throws VcsException { + final List result = new LinkedList(); + GitBranch.listAsStrings(myProject, myRoot, false, true, result, hash.getValue()); + GitBranch.listAsStrings(myProject, myRoot, true, false, result, hash.getValue()); + return result; + } + + public Collection getTagsWithCommit(final SHAHash hash) throws VcsException { + final List result = new LinkedList(); + GitTag.listAsStrings(myProject, myRoot, result, hash.getValue()); + return result; + } + + public void loadAllBranches(List sink) throws VcsException { + GitBranch.listAsStrings(myProject, myRoot, true, false, sink, null); + GitBranch.listAsStrings(myProject, myRoot, false, true, sink, null); + } + + public void loadAllTags(List sink) throws VcsException { + GitTag.listAsStrings(myProject, myRoot, sink, null); + } +} diff --git a/plugins/git4idea/src/git4idea/history/browser/ManageGitTreeView.java b/plugins/git4idea/src/git4idea/history/browser/ManageGitTreeView.java new file mode 100644 index 000000000000..35f5aacee339 --- /dev/null +++ b/plugins/git4idea/src/git4idea/history/browser/ManageGitTreeView.java @@ -0,0 +1,37 @@ +/* + * Copyright 2000-2010 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.history.browser; + +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; + +interface ManageGitTreeView extends RepositoryCommonData { + void init(); + boolean hasNext(@Nullable final TravelTicket ticket); + boolean hasPrevious(@Nullable final TravelTicket ticket); + void next(@Nullable final TravelTicket ticket); + void previous(@Nullable final TravelTicket ticket); + + void refresh(); + + void getDetails(final Collection hashes); + + GitTreeFiltering getFiltering(); + GitTreeFiltering getHighlighting(); + + boolean isInitialized(); +} diff --git a/plugins/git4idea/src/git4idea/history/browser/Mediator.java b/plugins/git4idea/src/git4idea/history/browser/Mediator.java new file mode 100644 index 000000000000..0fed61f0ee05 --- /dev/null +++ b/plugins/git4idea/src/git4idea/history/browser/Mediator.java @@ -0,0 +1,22 @@ +/* + * Copyright 2000-2010 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.history.browser; + +import java.util.Collection; + +public interface Mediator { + Collection getOrLoadParents(final GitCommit commit); +} diff --git a/plugins/git4idea/src/git4idea/history/browser/PageSizes.java b/plugins/git4idea/src/git4idea/history/browser/PageSizes.java new file mode 100644 index 000000000000..157c54a4638a --- /dev/null +++ b/plugins/git4idea/src/git4idea/history/browser/PageSizes.java @@ -0,0 +1,21 @@ +/* + * Copyright 2000-2010 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.history.browser; + +public interface PageSizes { + int LOAD_SIZE = 50; + int VISIBLE_PAGE_SIZE = 50; +} diff --git a/plugins/git4idea/src/git4idea/history/browser/Portion.java b/plugins/git4idea/src/git4idea/history/browser/Portion.java new file mode 100644 index 000000000000..f105c93c97ed --- /dev/null +++ b/plugins/git4idea/src/git4idea/history/browser/Portion.java @@ -0,0 +1,151 @@ +/* + * Copyright 2000-2010 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.history.browser; + +import com.intellij.util.AsynchConsumer; +import com.intellij.util.Processor; +import com.intellij.util.containers.MultiMap; +import org.jetbrains.annotations.Nullable; + +import java.util.*; + +// todo introduce synchronization here? +public class Portion implements AsynchConsumer { + private final Map myNameToHash; + private final Map myHolder; + private boolean myStartFound; + + // ordered + private List myOrdered; + + private final boolean myChildrenWasSet; + + private final MultiMap myOrphanMap; + + private final Set myUsers; + + // parents, w/out loaded commits; theoretically, those in myOrphan map todo check + private final List myRoots; + // what was passed into log command + private final List myLeafs; + @Nullable private final List myStartingPoints; + + public Portion(@Nullable final List startingPoints) { + myStartingPoints = startingPoints; + myChildrenWasSet = startingPoints == null || startingPoints.isEmpty(); + + myNameToHash = new HashMap(); + myHolder = new HashMap(); + myOrdered = new LinkedList(); + + myRoots = new LinkedList(); + myLeafs = new LinkedList(); + + myOrphanMap = new MultiMap(); + myUsers = new HashSet(); + } + + public void finished() { + //To change body of implemented methods use File | Settings | File Templates. + } + + public Set getUsers() { + return myUsers; + } + + public void consume(final GitCommit commit) { + myUsers.add(commit.getAuthor()); + myUsers.add(commit.getCommitter()); + + myOrdered.add(commit); + myHolder.put(commit.getHash(), myOrdered.size() - 1); + final Collection orphans = myOrphanMap.get(commit.getHash()); + for (GitCommit orphan : orphans) { + orphan.addParentLink(commit); + } + + final List referencies = commit.getBranches(); + if (! referencies.isEmpty()) { + final SHAHash hash = commit.getHash(); + for (String reference : referencies) { + myNameToHash.put(reference, hash); + } + } + final List tags = commit.getTags(); + if (! tags.isEmpty()) { + final SHAHash hash = commit.getHash(); + for (String reference : tags) { + myNameToHash.put(reference, hash); + } + } + + final Set parentHashes = commit.getParentsHashes(); + if (parentHashes.isEmpty()) { + myStartFound = true; + } else { + for (SHAHash parentHash : parentHashes) { + final Integer idx = myHolder.get(parentHash); + if (idx != null) { + final GitCommit parent = myOrdered.get(idx); + commit.addParentLink(parent); + } else { + myOrphanMap.putValue(parentHash, commit); + } + } + } + } + + public boolean isStartFound() { + return myStartFound; + } + + @Nullable + public GitCommit getLast() { + return myOrdered.isEmpty() ? null : myOrdered.get(myOrdered.size() - 1); + } + + // todo make simplier + public List getXFrom(final int idx, final int num) { + final List result = new ArrayList(); + iterateFrom(idx, new Processor() { + public boolean process(GitCommit gitCommit) { + result.add(gitCommit); + return result.size() == num; + } + }); + return result; + } + + public void iterateFrom(final int idx, final Processor processor) { + if ((idx < 0) || (idx > (myOrdered.size() - 1))) return; + + for (int i = idx; i < myOrdered.size(); i++) { + final GitCommit commit = myOrdered.get(i); + if (processor.process(commit)) return; + } + } + + public SHAHash getHashForReference(final String reference) { + return myNameToHash.get(reference); + } + + @Nullable + public GitCommit getByHash(final String hash) { + final SHAHash shaHash = new SHAHash(hash); + final Integer idx = myHolder.get(shaHash); + return idx == null ? null : myOrdered.get(idx); + } +} diff --git a/plugins/git4idea/src/git4idea/history/browser/RecentLogsHolder.java b/plugins/git4idea/src/git4idea/history/browser/RecentLogsHolder.java new file mode 100644 index 000000000000..6f55a5844735 --- /dev/null +++ b/plugins/git4idea/src/git4idea/history/browser/RecentLogsHolder.java @@ -0,0 +1,25 @@ +/* + * Copyright 2000-2010 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.history.browser; + +// this will be passed to build UI +public interface RecentLogsHolder { + //boolean isReachable(final String childHash, final String parentHash); + // todo move out + boolean isReachable(final GitCommit commit, SHAHash parentHash); + SHAHash getHashByName(final String name); + GitCommit getByHash(final SHAHash hash); +} diff --git a/plugins/git4idea/src/git4idea/history/browser/RepositoryCommonData.java b/plugins/git4idea/src/git4idea/history/browser/RepositoryCommonData.java new file mode 100644 index 000000000000..80d67aded971 --- /dev/null +++ b/plugins/git4idea/src/git4idea/history/browser/RepositoryCommonData.java @@ -0,0 +1,24 @@ +/* + * Copyright 2000-2010 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.history.browser; + +import java.util.List; + +public interface RepositoryCommonData { + List getAllTagsOrdered(); + List getAllBranchesOrdered(); + List getKnownUsers(); +} diff --git a/plugins/git4idea/src/git4idea/history/browser/SHAHash.java b/plugins/git4idea/src/git4idea/history/browser/SHAHash.java new file mode 100644 index 000000000000..7522ae7bba3f --- /dev/null +++ b/plugins/git4idea/src/git4idea/history/browser/SHAHash.java @@ -0,0 +1,51 @@ +/* + * Copyright 2000-2010 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.history.browser; + +public class SHAHash { + private final String myValue; + + public SHAHash(final String value) { + myValue = value; + assert myValue.length() == 40 : myValue; + } + + public String getValue() { + return myValue; + } + + @Override + public String toString() { + return myValue; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + SHAHash shaHash = (SHAHash)o; + + if (!myValue.equals(shaHash.myValue)) return false; + + return true; + } + + @Override + public int hashCode() { + return myValue.hashCode(); + } +} diff --git a/plugins/git4idea/src/git4idea/history/browser/SelectGitBranchesPopup.java b/plugins/git4idea/src/git4idea/history/browser/SelectGitBranchesPopup.java new file mode 100644 index 000000000000..c97da159b0b4 --- /dev/null +++ b/plugins/git4idea/src/git4idea/history/browser/SelectGitBranchesPopup.java @@ -0,0 +1,67 @@ +/* + * Copyright 2000-2010 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.history.browser; + +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.ui.popup.JBPopupFactory; +import com.intellij.ui.ColoredListCellRenderer; +import com.intellij.ui.SimpleTextAttributes; +import com.intellij.util.ArrayUtil; +import com.intellij.util.Consumer; + +import javax.swing.*; +import java.awt.*; +import java.util.List; + +public class SelectGitBranchesPopup { + private SelectGitBranchesPopup() { + } + + public static void showMe(final Consumer selectedBranchConsumer, + final List branchesAndTags, + boolean tags, + DataContext dc) { + final JList list = new JList(); + list.setCellRenderer(new BranchListCellRenderer(tags)); + list.setListData(ArrayUtil.toObjectArray(branchesAndTags)); + JBPopupFactory.getInstance().createListPopupBuilder(list) + .setTitle("Select " + (tags ? "tags" : "branches")) + .setResizable(true) + .setDimensionServiceKey("Git.Select branches/tags") + .setItemChoosenCallback(new Runnable() { + public void run() { + selectedBranchConsumer.consume(list.getSelectedValues()); + } + }) + .createPopup().showInBestPositionFor(dc); + } + + private static class BranchListCellRenderer extends ColoredListCellRenderer { + private final boolean myTags; + + private BranchListCellRenderer(boolean tags) { + myTags = tags; + } + + @Override + protected void customizeCellRenderer(JList list, Object value, int index, boolean selected, boolean hasFocus) { + if (value instanceof String) { + final Color color = (myTags) ? Color.blue : Color.green; + append((String) value, new SimpleTextAttributes(SimpleTextAttributes.STYLE_PLAIN, color)); + } + } + } +} diff --git a/plugins/git4idea/src/git4idea/history/browser/SelfRemovingLabel.java b/plugins/git4idea/src/git4idea/history/browser/SelfRemovingLabel.java new file mode 100644 index 000000000000..6f6b23246d97 --- /dev/null +++ b/plugins/git4idea/src/git4idea/history/browser/SelfRemovingLabel.java @@ -0,0 +1,62 @@ +/* + * Copyright 2000-2010 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.history.browser; + +import com.intellij.openapi.util.SystemInfo; +import com.intellij.ui.IdeBorderFactory; + +import javax.swing.*; +import javax.swing.border.Border; +import javax.swing.border.CompoundBorder; +import java.awt.*; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; + +public class SelfRemovingLabel { + public static JPanel createComponent(final String text, final Icon icon, final JComponent parent, final Color textColor) { + final JLabel textLabel = new JLabel(text); + final JPanel panel = new JPanel(new BorderLayout()); + final JLabel label = new JLabel(icon); + label.setOpaque(true); + label.setBackground(textLabel.getBackground()); + textLabel.setOpaque(true); + textLabel.setForeground(textColor); + panel.add(label, BorderLayout.WEST); + panel.add(textLabel, BorderLayout.EAST); + //textLabel.setToolTipText(myDescription); + //label.setToolTipText(myDescription); + final Border originalBorder; + if (SystemInfo.isMac) { + originalBorder = BorderFactory.createLoweredBevelBorder(); + } + else { + originalBorder = textLabel.getBorder(); + } + + panel.setBorder(new CompoundBorder(IdeBorderFactory.createEmptyBorder(4, 0, 4, 0), originalBorder)); + + textLabel.setOpaque(true); + textLabel.setBorder(IdeBorderFactory.createEmptyBorder(0, 5, 0, 5)); + + label.addMouseListener(new MouseAdapter() { + public void mouseClicked(MouseEvent e) { + parent.remove(panel); + parent.repaint(); + } + }); + return panel; + } +} diff --git a/plugins/git4idea/src/git4idea/history/browser/TagBridge.java b/plugins/git4idea/src/git4idea/history/browser/TagBridge.java new file mode 100644 index 000000000000..6b5574f00406 --- /dev/null +++ b/plugins/git4idea/src/git4idea/history/browser/TagBridge.java @@ -0,0 +1,60 @@ +/* + * Copyright 2000-2010 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.history.browser; + +import com.intellij.openapi.vcs.VcsException; +import com.intellij.util.containers.MultiMap; + +import java.util.Collection; +import java.util.Set; + +// when you're not at first page, or when your filter moved tags out, you might still want to filter by them +public class TagBridge { + final Set myParentsInterestedIn; + private final LowLevelAccess myAccess; + + final MultiMap myTagsForHashes; + final MultiMap myHashesForTags; + + public TagBridge(final Set parentsInterestedIn, final LowLevelAccess access) { + myParentsInterestedIn = parentsInterestedIn; + myAccess = access; + myTagsForHashes = new MultiMap(); + myHashesForTags = new MultiMap(); + } + + public void load() throws VcsException { + for (SHAHash hash : myParentsInterestedIn) { + final Collection refs = myAccess.getBranchesWithCommit(hash); + refs.addAll(myAccess.getTagsWithCommit(hash)); + + myTagsForHashes.put(hash, refs); + for (String ref : refs) { + myHashesForTags.putValue(ref, hash); + } + } + } + + // todo +- + public MultiMap getHashesForTags() { + return myHashesForTags; + } + + // todo +- + public MultiMap getTagsForHashes() { + return myTagsForHashes; + } +} diff --git a/plugins/git4idea/src/git4idea/history/browser/TravelTicket.java b/plugins/git4idea/src/git4idea/history/browser/TravelTicket.java new file mode 100644 index 000000000000..9d181041e2ca --- /dev/null +++ b/plugins/git4idea/src/git4idea/history/browser/TravelTicket.java @@ -0,0 +1,53 @@ +/* + * Copyright 2000-2010 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.history.browser; + +import java.util.Date; + +public class TravelTicket { + private final boolean myIsBottomReached; + private final Date myLatestDate; + + public TravelTicket(boolean isBottomReached, Date latestDate) { + myIsBottomReached = isBottomReached; + myLatestDate = latestDate; + } + + public boolean isIsBottomReached() { + return myIsBottomReached; + } + + public Date getLatestDate() { + return myLatestDate; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + TravelTicket that = (TravelTicket)o; + + if (!myLatestDate.equals(that.myLatestDate)) return false; + + return true; + } + + @Override + public int hashCode() { + return myLatestDate.hashCode(); + } +} diff --git a/plugins/git4idea/src/git4idea/history/browser/UsersPopup.java b/plugins/git4idea/src/git4idea/history/browser/UsersPopup.java new file mode 100644 index 000000000000..b3117dba3195 --- /dev/null +++ b/plugins/git4idea/src/git4idea/history/browser/UsersPopup.java @@ -0,0 +1,50 @@ +/* + * Copyright 2000-2010 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.history.browser; + +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.ui.popup.JBPopupFactory; +import com.intellij.ui.ListSpeedSearch; +import com.intellij.util.ArrayUtil; +import com.intellij.util.Consumer; + +import javax.swing.*; +import java.util.List; + +public class UsersPopup { + private UsersPopup() { + } + + public static void showUsersPopup(final List knownUsers, final Consumer continuation, DataContext dc) { + // todo pattern to come + final JList list = new JList(); + list.setListData(ArrayUtil.toObjectArray(knownUsers)); + new ListSpeedSearch(list); + + JBPopupFactory.getInstance().createListPopupBuilder(list) + .setTitle("Select author or committer") + .setResizable(true) + .setDimensionServiceKey("Git.Select user") + .setItemChoosenCallback(new Runnable() { + public void run() { + if (list.getSelectedIndices().length > 0) { + continuation.consume((String) list.getSelectedValue()); + } + } + }) + .createPopup().showInBestPositionFor(dc); + } +} diff --git a/plugins/git4idea/src/git4idea/ui/GitUnstashDialog.java b/plugins/git4idea/src/git4idea/ui/GitUnstashDialog.java index c799d626a36f..2642f8dc3366 100644 --- a/plugins/git4idea/src/git4idea/ui/GitUnstashDialog.java +++ b/plugins/git4idea/src/git4idea/ui/GitUnstashDialog.java @@ -304,7 +304,7 @@ public class GitUnstashDialog extends DialogWrapper { } myBranches.clear(); try { - GitBranch.listAsStrings(myProject, getGitRoot(), false, true, myBranches); + GitBranch.listAsStrings(myProject, getGitRoot(), false, true, myBranches, null); } catch (VcsException e) { // ignore error diff --git a/plugins/svn4idea/src/org/jetbrains/idea/svn/SvnCopiesRefreshManager.java b/plugins/svn4idea/src/org/jetbrains/idea/svn/SvnCopiesRefreshManager.java index 8610983764e6..ca7548182660 100644 --- a/plugins/svn4idea/src/org/jetbrains/idea/svn/SvnCopiesRefreshManager.java +++ b/plugins/svn4idea/src/org/jetbrains/idea/svn/SvnCopiesRefreshManager.java @@ -22,6 +22,7 @@ import com.intellij.openapi.progress.ProcessCanceledException; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.project.Project; +import com.intellij.openapi.vcs.RequestsMerger; import com.intellij.util.Consumer; import com.intellij.util.concurrency.Semaphore; diff --git a/plugins/svn4idea/src/org/jetbrains/idea/svn/integrate/IntegratedSelectedOptionsDialog.form b/plugins/svn4idea/src/org/jetbrains/idea/svn/integrate/IntegratedSelectedOptionsDialog.form index 06b98cadf6eb..097918c56883 100644 --- a/plugins/svn4idea/src/org/jetbrains/idea/svn/integrate/IntegratedSelectedOptionsDialog.form +++ b/plugins/svn4idea/src/org/jetbrains/idea/svn/integrate/IntegratedSelectedOptionsDialog.form @@ -87,7 +87,7 @@ - + @@ -96,7 +96,27 @@ - + + + + + + + + + + + + + + + + + + + + +