mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[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:
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user