diff --git a/plugins/git4idea/src/META-INF/plugin.xml b/plugins/git4idea/src/META-INF/plugin.xml
index 3e36a5824540..de5eb2c8faa9 100644
--- a/plugins/git4idea/src/META-INF/plugin.xml
+++ b/plugins/git4idea/src/META-INF/plugin.xml
@@ -107,6 +107,9 @@
+
@@ -174,6 +177,7 @@
serviceImplementation="git4idea.branch.GitBrancherImpl"/>
+
diff --git a/plugins/git4idea/src/git4idea/branch/DeepComparator.java b/plugins/git4idea/src/git4idea/branch/DeepComparator.java
new file mode 100644
index 000000000000..1b46c6735f84
--- /dev/null
+++ b/plugins/git4idea/src/git4idea/branch/DeepComparator.java
@@ -0,0 +1,264 @@
+/*
+ * Copyright 2000-2014 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package git4idea.branch;
+
+import com.intellij.openapi.Disposable;
+import com.intellij.openapi.components.ServiceManager;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.progress.ProgressIndicator;
+import com.intellij.openapi.progress.Task;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Disposer;
+import com.intellij.openapi.util.Key;
+import com.intellij.openapi.vcs.VcsException;
+import com.intellij.openapi.vcs.VcsNotifier;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.ui.JBColor;
+import com.intellij.util.containers.ContainerUtil;
+import com.intellij.vcs.log.*;
+import com.intellij.vcs.log.data.DataPack;
+import com.intellij.vcs.log.data.VcsLogDataHolder;
+import com.intellij.vcs.log.data.VcsLogRefreshListener;
+import com.intellij.vcs.log.impl.HashImpl;
+import git4idea.GitBranch;
+import git4idea.commands.GitCommand;
+import git4idea.commands.GitLineHandler;
+import git4idea.commands.GitLineHandlerAdapter;
+import git4idea.repo.GitRepository;
+import git4idea.repo.GitRepositoryManager;
+import gnu.trove.TIntHashSet;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.awt.*;
+import java.util.Map;
+
+public class DeepComparator implements Disposable {
+
+ private static final Logger LOG = Logger.getInstance(DeepComparator.class);
+
+ @NotNull private final Project myProject;
+ @NotNull private final GitRepositoryManager myRepositoryManager;
+ @NotNull private final VcsLogUi myUi;
+ @NotNull private final VcsLogFilterChangeListener myFilterChangeListener;
+
+ @Nullable private VcsLogHighlighter myHighlighter;
+ @Nullable private MyTask myTask;
+
+ @NotNull
+ public static DeepComparator getInstance(@NotNull Project project, @NotNull VcsLogUi ui) {
+ DeepComparatorHolder holder = ServiceManager.getService(project, DeepComparatorHolder.class);
+ return holder.getInstance(ui);
+ }
+
+ DeepComparator(@NotNull Project project, @NotNull GitRepositoryManager manager, @NotNull VcsLogUi ui, @NotNull Disposable parent) {
+ myProject = project;
+ myRepositoryManager = manager;
+ myUi = ui;
+ Disposer.register(parent, this);
+
+ myFilterChangeListener = new VcsLogFilterChangeListener() {
+ @Override
+ public void filtersPossiblyChanged() {
+ if (myTask == null) { // no task in progress => not interested in filter changes
+ return;
+ }
+
+ VcsLogBranchFilter branchFilter = myUi.getFilterUi().getFilters().getBranchFilter();
+ if (branchFilter == null ||
+ branchFilter.getBranchNames().size() != 1 ||
+ !branchFilter.getBranchNames().iterator().next().equals(myTask.myComparedBranch)) {
+ stopAndUnhighlight();
+ }
+ }
+ };
+ myUi.addFilterChangeListener(myFilterChangeListener);
+
+ project.getMessageBus().connect(project).subscribe(VcsLogDataHolder.REFRESH_COMPLETED, new VcsLogRefreshListener() {
+ @Override
+ public void refresh(@NotNull DataPack dataPack) {
+ if (myTask == null) { // no task in progress => not interested in refresh events
+ return;
+ }
+
+ // collect data
+ String comparedBranch = myTask.myComparedBranch;
+ Map repositoriesWithCurrentBranches = myTask.myRepositoriesWithCurrentBranches;
+ VcsLogDataProvider provider = myTask.myProvider;
+
+ stopAndUnhighlight();
+
+ // highlight again
+ Map repositories = getRepositories(myUi.getDataPack().getLogProviders(), comparedBranch);
+ if (repositories.equals(repositoriesWithCurrentBranches)) { // but not if current branch changed
+ highlightInBackground(comparedBranch, provider);
+ }
+ }
+ });
+ }
+
+ public void highlightInBackground(@NotNull String branchToCompare, @NotNull VcsLogDataProvider dataProvider) {
+ if (myTask != null) {
+ LOG.error("Shouldn't be possible");
+ return;
+ }
+
+ Map repositories = getRepositories(myUi.getDataPack().getLogProviders(), branchToCompare);
+ if (repositories.isEmpty()) {
+ return;
+ }
+
+ myTask = new MyTask(myProject, myUi, repositories, dataProvider, branchToCompare);
+ myTask.queue();
+ }
+
+ @NotNull
+ private Map getRepositories(@NotNull Map providers,
+ @NotNull String branchToCompare) {
+ Map repos = ContainerUtil.newHashMap();
+ for (VirtualFile root : providers.keySet()) {
+ GitRepository repository = myRepositoryManager.getRepositoryForRoot(root);
+ if (repository == null || repository.getCurrentBranch() == null ||
+ repository.getBranches().findBranchByName(branchToCompare) == null) {
+ continue;
+ }
+ repos.put(repository, repository.getCurrentBranch());
+ }
+ return repos;
+ }
+
+ public void stopAndUnhighlight() {
+ if (myTask != null) {
+ myTask.cancel();
+ myTask = null;
+ }
+ if (myHighlighter != null) {
+ myUi.removeHighlighter(myHighlighter);
+ }
+ }
+
+ @Override
+ public void dispose() {
+ stopAndUnhighlight();
+ myUi.removeFilterChangeListener(myFilterChangeListener);
+ }
+
+ public boolean hasHighlightingOrInProgress() {
+ return myTask != null;
+ }
+
+ private class MyTask extends Task.Backgroundable {
+
+ @NotNull private final Project myProject;
+ @NotNull private final VcsLogUi myUi;
+ @NotNull private final Map myRepositoriesWithCurrentBranches;
+ @NotNull private final VcsLogDataProvider myProvider;
+ @NotNull private final String myComparedBranch;
+
+ @NotNull private final TIntHashSet myNonPickedCommits = new TIntHashSet();
+ @Nullable private VcsException myException;
+ private boolean myCancelled;
+
+ public MyTask(@NotNull Project project, @NotNull VcsLogUi ui, @NotNull Map repositoriesWithCurrentBranches,
+ @NotNull VcsLogDataProvider dataProvider, @NotNull String branchToCompare) {
+ super(project, "Comparing branches...");
+ myProject = project;
+ myUi = ui;
+ myRepositoriesWithCurrentBranches = repositoriesWithCurrentBranches;
+ myProvider = dataProvider;
+ myComparedBranch = branchToCompare;
+ }
+
+ @Override
+ public void run(@NotNull ProgressIndicator indicator) {
+ try {
+ for (Map.Entry entry : myRepositoriesWithCurrentBranches.entrySet()) {
+ GitRepository repo = entry.getKey();
+ GitBranch currentBranch = entry.getValue();
+ myNonPickedCommits.addAll(getNonPickedCommitsFromGit(myProject, repo.getRoot(), myProvider,
+ currentBranch.getName(), myComparedBranch).toArray());
+ }
+ }
+ catch (VcsException e) {
+ LOG.warn(e);
+ myException = e;
+ }
+ }
+
+ @Override
+ public void onSuccess() {
+ if (myCancelled) {
+ return;
+ }
+
+ if (myException != null) {
+ VcsNotifier.getInstance(myProject).notifyError("Couldn't compare with branch " + myComparedBranch, myException.getMessage());
+ return;
+ }
+
+ if (myHighlighter != null) {
+ myUi.removeHighlighter(myHighlighter);
+ }
+ myHighlighter = new VcsLogHighlighter() {
+ @Nullable
+ @Override
+ public Color getForeground(int commitIndex, boolean isSelected) {
+ return !myNonPickedCommits.contains(commitIndex) ? JBColor.GRAY : null;
+ }
+ };
+ myUi.addHighlighter(myHighlighter);
+ }
+
+ public void cancel() {
+ myCancelled = true;
+ }
+
+ @NotNull
+ private TIntHashSet getNonPickedCommitsFromGit(@NotNull Project project, @NotNull VirtualFile root,
+ @NotNull final VcsLogDataProvider dataProvider,
+ @NotNull String currentBranch, @NotNull String comparedBranch) throws VcsException {
+ GitLineHandler handler = new GitLineHandler(project, root, GitCommand.CHERRY);
+ handler.addParameters(currentBranch, comparedBranch); // upstream - current branch; head - compared branch
+
+ final TIntHashSet pickedCommits = new TIntHashSet();
+ handler.addLineListener(new GitLineHandlerAdapter() {
+ @Override
+ public void onLineAvailable(String line, Key outputType) {
+ // + 645caac042ff7fb1a5e3f7d348f00e9ceea5c317
+ // - c3b9b90f6c26affd7e597ebf65db96de8f7e5860
+ if (line.startsWith("+")) {
+ try {
+ line = line.substring(2).trim();
+ int firstSpace = line.indexOf(' ');
+ if (firstSpace > 0) {
+ line = line.substring(0, firstSpace); // safety-check: take just the first word for sure
+ }
+ Hash hash = HashImpl.build(line);
+ pickedCommits.add(dataProvider.getCommitIndex(hash));
+ }
+ catch (Exception e) {
+ LOG.error("Couldn't parse line [" + line + "]");
+ }
+ }
+ }
+ });
+ handler.runInCurrentThread(null);
+ return pickedCommits;
+ }
+
+ }
+
+}
diff --git a/plugins/git4idea/src/git4idea/branch/DeepComparatorHolder.java b/plugins/git4idea/src/git4idea/branch/DeepComparatorHolder.java
new file mode 100644
index 000000000000..fdab43700b50
--- /dev/null
+++ b/plugins/git4idea/src/git4idea/branch/DeepComparatorHolder.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2000-2014 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package git4idea.branch;
+
+import com.intellij.openapi.Disposable;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Disposer;
+import com.intellij.util.containers.ContainerUtil;
+import com.intellij.vcs.log.VcsLogUi;
+import git4idea.repo.GitRepositoryManager;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Map;
+
+public class DeepComparatorHolder implements Disposable {
+
+ @NotNull private final Project myProject;
+ @NotNull private final GitRepositoryManager myRepositoryManager;
+
+ @NotNull private final Map myComparators;
+
+ // initialized by pico-container
+ @SuppressWarnings("UnusedDeclaration")
+ private DeepComparatorHolder(@NotNull Project project, @NotNull GitRepositoryManager repositoryManager) {
+ myProject = project;
+ myRepositoryManager = repositoryManager;
+ myComparators = ContainerUtil.newHashMap();
+ Disposer.register(project, this);
+ }
+
+ @NotNull
+ public DeepComparator getInstance(@NotNull VcsLogUi ui) {
+ DeepComparator comparator = myComparators.get(ui);
+ if (comparator == null) {
+ comparator = new DeepComparator(myProject, myRepositoryManager, ui, this);
+ myComparators.put(ui, comparator);
+ }
+ return comparator;
+ }
+
+ @Override
+ public void dispose() {
+ myComparators.clear();
+ }
+
+}
diff --git a/plugins/git4idea/src/git4idea/branch/DeepCompareAction.java b/plugins/git4idea/src/git4idea/branch/DeepCompareAction.java
new file mode 100644
index 000000000000..ddb6f24042c6
--- /dev/null
+++ b/plugins/git4idea/src/git4idea/branch/DeepCompareAction.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2000-2014 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package git4idea.branch;
+
+import com.intellij.openapi.actionSystem.*;
+import com.intellij.openapi.project.DumbAware;
+import com.intellij.openapi.project.DumbAwareAction;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.popup.JBPopupFactory;
+import com.intellij.openapi.ui.popup.ListPopup;
+import com.intellij.ui.awt.RelativePoint;
+import com.intellij.util.Consumer;
+import com.intellij.util.Function;
+import com.intellij.vcs.log.*;
+import com.intellij.vcs.log.data.VcsLogBranchFilterImpl;
+import com.intellij.vcs.log.ui.filter.BranchFilterPopupComponent;
+import org.jetbrains.annotations.NotNull;
+
+import java.awt.event.InputEvent;
+import java.awt.event.MouseEvent;
+import java.util.Collections;
+
+public class DeepCompareAction extends ToggleAction implements DumbAware {
+
+ @Override
+ public boolean isSelected(AnActionEvent e) {
+ Project project = e.getData(CommonDataKeys.PROJECT);
+ VcsLogUi ui = e.getData(VcsLogDataKeys.VCS_LOG_UI);
+ if (project == null || ui == null) {
+ return false;
+ }
+ return DeepComparator.getInstance(project, ui).hasHighlightingOrInProgress();
+ }
+
+ @Override
+ public void setSelected(AnActionEvent e, boolean selected) {
+ Project project = e.getData(CommonDataKeys.PROJECT);
+ final VcsLogUi ui = e.getData(VcsLogDataKeys.VCS_LOG_UI);
+ final VcsLogDataProvider dataProvider = e.getData(VcsLogDataKeys.VCS_LOG_DATA_PROVIDER);
+ if (project == null || ui == null || dataProvider == null) {
+ return;
+ }
+ final DeepComparator dc = DeepComparator.getInstance(project, ui);
+ if (selected) {
+ VcsLogBranchFilter branchFilter = ui.getFilterUi().getFilters().getBranchFilter();
+ if (branchFilter == null || branchFilter.getBranchNames().size() != 1) {
+ selectBranchAndPerformAction(ui.getDataPack(), e, new Consumer() {
+ @Override
+ public void consume(String selectedBranch) {
+ ui.getFilterUi().setFilter(new VcsLogBranchFilterImpl(ui.getDataPack().getRefs().getBranches(),
+ Collections.singleton(selectedBranch)));
+ dc.highlightInBackground(selectedBranch, dataProvider);
+ }
+ });
+ return;
+ }
+ String branchToCompare = branchFilter.getBranchNames().iterator().next();
+ dc.highlightInBackground(branchToCompare, dataProvider);
+ }
+ else {
+ dc.stopAndUnhighlight();
+ }
+ }
+
+ private static void selectBranchAndPerformAction(@NotNull VcsLogDataPack dataPack, @NotNull AnActionEvent event,
+ @NotNull final Consumer consumer) {
+ ActionGroup actionGroup = BranchFilterPopupComponent.constructActionGroup(dataPack, null, new Function() {
+ @Override
+ public AnAction fun(final String s) {
+ return new DumbAwareAction(s) {
+ @Override
+ public void actionPerformed(AnActionEvent e) {
+ consumer.consume(s);
+ }
+ };
+ }
+ });
+ ListPopup popup = JBPopupFactory.getInstance().createActionGroupPopup("Select branch to compare", actionGroup, event.getDataContext(),
+ false, false, false, null, -1, null);
+ InputEvent inputEvent = event.getInputEvent();
+ if (inputEvent instanceof MouseEvent) {
+ popup.show(new RelativePoint((MouseEvent)inputEvent));
+ }
+ else {
+ popup.showInBestPositionFor(event.getDataContext());
+ }
+ }
+
+ @Override
+ public void update(AnActionEvent e) {
+ super.update(e);
+ VcsLogUi ui = e.getData(VcsLogDataKeys.VCS_LOG_UI);
+ e.getPresentation().setEnabledAndVisible(ui != null);
+ }
+
+}
diff --git a/plugins/git4idea/src/git4idea/commands/GitCommand.java b/plugins/git4idea/src/git4idea/commands/GitCommand.java
index 226d663beb1f..27beb50100c1 100644
--- a/plugins/git4idea/src/git4idea/commands/GitCommand.java
+++ b/plugins/git4idea/src/git4idea/commands/GitCommand.java
@@ -41,6 +41,7 @@ public class GitCommand {
public static final GitCommand CHECK_ATTR = read("check-attr");
public static final GitCommand COMMIT = write("commit");
public static final GitCommand CONFIG = read("config");
+ public static final GitCommand CHERRY = read("cherry");
public static final GitCommand CHERRY_PICK = write("cherry-pick");
public static final GitCommand CLONE = write("clone");
public static final GitCommand DIFF = read("diff");