[log] IDEA-118947 Implement special highlighting for cherry-pick

* Add DeepCompareAction as a toggle action to the vcs log toolbar.
* Clicking on the button starts 'git cherry' in background to
  get commits non picked to the current branch.
* Once completed, the log is highlighted so that picked commits
  appear in grey.
* If initially there is no branch filter or several branches selected,
  propose user to select a single branch, apply filters and start
  collecting git cherry.

* If branch filter changes, reset the highlighting.
* If the log refreshes, restart all procedure to make sure that
  highlighting is correct, say, after cherry-picking on of the "black"
  commits.
* But if current branch changes, reset the highlighting and don't
  restart the procedure.

* DeepComparatorHolder maintains a single DeepComparator instance
  per each VcsLogUi (i.e. each vcs log).
This commit is contained in:
Kirill Likhodedov
2014-03-10 20:05:04 +04:00
parent 881645aad5
commit 7ac6e11cf7
5 changed files with 437 additions and 0 deletions
+4
View File
@@ -107,6 +107,9 @@
<group id="Git.Log.Toolbar">
<reference id="Git.CherryPick"/>
<add-to-group group-id="Vcs.Log.Toolbar" />
<action id="Git.Log.DeepCompare" class="git4idea.branch.DeepCompareAction" text="Highlight non-picked commits"
icon="AllIcons.Diff.Diff"
description="Dims those commits which have equivalent changes in the current branch"/>
</group>
@@ -174,6 +177,7 @@
serviceImplementation="git4idea.branch.GitBrancherImpl"/>
<projectService serviceImplementation="git4idea.log.GitContentRevisionFactory" />
<projectService serviceImplementation="git4idea.branch.DeepComparatorHolder"/>
<applicationService serviceImplementation="git4idea.history.wholeTree.GitCommitsSequentialIndex"
serviceInterface="git4idea.history.wholeTree.GitCommitsSequentially"/>
@@ -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<GitRepository, GitBranch> repositoriesWithCurrentBranches = myTask.myRepositoriesWithCurrentBranches;
VcsLogDataProvider provider = myTask.myProvider;
stopAndUnhighlight();
// highlight again
Map<GitRepository, GitBranch> 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<GitRepository, GitBranch> repositories = getRepositories(myUi.getDataPack().getLogProviders(), branchToCompare);
if (repositories.isEmpty()) {
return;
}
myTask = new MyTask(myProject, myUi, repositories, dataProvider, branchToCompare);
myTask.queue();
}
@NotNull
private Map<GitRepository, GitBranch> getRepositories(@NotNull Map<VirtualFile, VcsLogProvider> providers,
@NotNull String branchToCompare) {
Map<GitRepository, GitBranch> 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<GitRepository, GitBranch> 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<GitRepository, GitBranch> 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<GitRepository, GitBranch> 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;
}
}
}
@@ -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<VcsLogUi, DeepComparator> 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();
}
}
@@ -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<String>() {
@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<String> consumer) {
ActionGroup actionGroup = BranchFilterPopupComponent.constructActionGroup(dataPack, null, new Function<String, AnAction>() {
@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);
}
}
@@ -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");