From 7ac6e11cf70d97a425bc249cddcc06197cd9e9c5 Mon Sep 17 00:00:00 2001 From: Kirill Likhodedov Date: Mon, 10 Mar 2014 20:02:20 +0400 Subject: [PATCH] [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). --- plugins/git4idea/src/META-INF/plugin.xml | 4 + .../src/git4idea/branch/DeepComparator.java | 264 ++++++++++++++++++ .../git4idea/branch/DeepComparatorHolder.java | 59 ++++ .../git4idea/branch/DeepCompareAction.java | 109 ++++++++ .../src/git4idea/commands/GitCommand.java | 1 + 5 files changed, 437 insertions(+) create mode 100644 plugins/git4idea/src/git4idea/branch/DeepComparator.java create mode 100644 plugins/git4idea/src/git4idea/branch/DeepComparatorHolder.java create mode 100644 plugins/git4idea/src/git4idea/branch/DeepCompareAction.java 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");