[git] fix deep comparator keeping highlighting after branch filter unset
[idea/community.git] / plugins / git4idea / src / git4idea / branch / DeepComparator.java
1 /*
2 ` * Copyright 2000-2015 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package git4idea.branch;
17
18 import com.intellij.openapi.Disposable;
19 import com.intellij.openapi.application.ApplicationManager;
20 import com.intellij.openapi.components.ServiceManager;
21 import com.intellij.openapi.diagnostic.Logger;
22 import com.intellij.openapi.progress.ProgressIndicator;
23 import com.intellij.openapi.progress.Task;
24 import com.intellij.openapi.project.Project;
25 import com.intellij.openapi.util.Disposer;
26 import com.intellij.openapi.util.Key;
27 import com.intellij.openapi.vcs.VcsException;
28 import com.intellij.openapi.vcs.VcsNotifier;
29 import com.intellij.openapi.vfs.VirtualFile;
30 import com.intellij.util.containers.ContainerUtil;
31 import com.intellij.vcs.log.*;
32 import com.intellij.vcs.log.data.VcsLogData;
33 import com.intellij.vcs.log.impl.HashImpl;
34 import com.intellij.vcs.log.impl.VcsLogUtil;
35 import com.intellij.vcs.log.ui.MergeCommitsHighlighter;
36 import com.intellij.vcs.log.ui.VcsLogHighlighterFactory;
37 import git4idea.GitBranch;
38 import git4idea.commands.GitCommand;
39 import git4idea.commands.GitLineHandler;
40 import git4idea.commands.GitLineHandlerAdapter;
41 import git4idea.repo.GitRepository;
42 import git4idea.repo.GitRepositoryManager;
43 import org.jetbrains.annotations.NotNull;
44 import org.jetbrains.annotations.Nullable;
45
46 import java.util.Map;
47 import java.util.Set;
48
49 public class DeepComparator implements VcsLogHighlighter, Disposable {
50   private static final Logger LOG = Logger.getInstance(DeepComparator.class);
51
52   @NotNull private final Project myProject;
53   @NotNull private final GitRepositoryManager myRepositoryManager;
54   @NotNull private final VcsLogUi myUi;
55
56   @Nullable private MyTask myTask;
57   @Nullable private Set<CommitId> myNonPickedCommits;
58
59   public DeepComparator(@NotNull Project project, @NotNull GitRepositoryManager manager, @NotNull VcsLogUi ui, @NotNull Disposable parent) {
60     myProject = project;
61     myRepositoryManager = manager;
62     myUi = ui;
63     Disposer.register(parent, this);
64   }
65
66   public void highlightInBackground(@NotNull String branchToCompare, @NotNull VcsLogDataProvider dataProvider) {
67     if (myTask != null) {
68       LOG.error("Shouldn't be possible");
69       return;
70     }
71
72     Map<GitRepository, GitBranch> repositories = getRepositories(myUi.getDataPack().getLogProviders(), branchToCompare);
73     if (repositories.isEmpty()) {
74       removeHighlighting();
75       return;
76     }
77
78     myTask = new MyTask(myProject, repositories, dataProvider, branchToCompare);
79     myTask.queue();
80   }
81
82   @NotNull
83   private Map<GitRepository, GitBranch> getRepositories(@NotNull Map<VirtualFile, VcsLogProvider> providers,
84                                                         @NotNull String branchToCompare) {
85     Map<GitRepository, GitBranch> repos = ContainerUtil.newHashMap();
86     for (VirtualFile root : providers.keySet()) {
87       GitRepository repository = myRepositoryManager.getRepositoryForRoot(root);
88       if (repository == null || repository.getCurrentBranch() == null ||
89           repository.getBranches().findBranchByName(branchToCompare) == null) {
90         continue;
91       }
92       repos.put(repository, repository.getCurrentBranch());
93     }
94     return repos;
95   }
96
97   public void stopAndUnhighlight() {
98     stopTask();
99     removeHighlighting();
100   }
101
102   private void stopTask() {
103     if (myTask != null) {
104       myTask.cancel();
105       myTask = null;
106     }
107   }
108
109   private void removeHighlighting() {
110     ApplicationManager.getApplication().assertIsDispatchThread();
111     myNonPickedCommits = null;
112   }
113
114   @Override
115   public void dispose() {
116     stopAndUnhighlight();
117   }
118
119   public boolean hasHighlightingOrInProgress() {
120     return myTask != null;
121   }
122
123   public static DeepComparator getInstance(@NotNull Project project, @NotNull VcsLogUi logUi) {
124     return ServiceManager.getService(project, DeepComparatorHolder.class).getInstance(logUi);
125   }
126
127   @NotNull
128   @Override
129   public VcsLogHighlighter.VcsCommitStyle getStyle(@NotNull VcsShortCommitDetails commitDetails, boolean isSelected) {
130     if (myNonPickedCommits == null) return VcsCommitStyle.DEFAULT;
131     return VcsCommitStyleFactory.foreground(!myNonPickedCommits.contains(new CommitId(commitDetails.getId(), commitDetails.getRoot()))
132                                             ? MergeCommitsHighlighter.MERGE_COMMIT_FOREGROUND
133                                             : null);
134   }
135
136   @Override
137   public void update(@NotNull VcsLogDataPack dataPack, boolean refreshHappened) {
138     if (myTask == null) { // no task in progress => not interested in refresh events
139       return;
140     }
141
142     String comparedBranch = myTask.myComparedBranch;
143     VcsLogBranchFilter branchFilter = dataPack.getFilters().getBranchFilter();
144     if (branchFilter == null || !myTask.myComparedBranch.equals(VcsLogUtil.getSingleFilteredBranch(branchFilter, dataPack.getRefs()))) {
145       stopAndUnhighlight();
146       return;
147     }
148
149     if (refreshHappened) {
150       Map<GitRepository, GitBranch> repositoriesWithCurrentBranches = myTask.myRepositoriesWithCurrentBranches;
151       VcsLogDataProvider provider = myTask.myProvider;
152
153       stopTask();
154
155       // highlight again
156       Map<GitRepository, GitBranch> repositories = getRepositories(dataPack.getLogProviders(), comparedBranch);
157       if (repositories.equals(repositoriesWithCurrentBranches)) {
158         // but not if current branch changed
159         highlightInBackground(comparedBranch, provider);
160       }
161       else {
162         removeHighlighting();
163       }
164     }
165   }
166
167   public static class Factory implements VcsLogHighlighterFactory {
168     @NotNull private static final String ID = "CHERRY_PICKED_COMMITS";
169
170     @NotNull
171     @Override
172     public VcsLogHighlighter createHighlighter(@NotNull VcsLogData logDataManager, @NotNull VcsLogUi logUi) {
173       return getInstance(logDataManager.getProject(), logUi);
174     }
175
176     @NotNull
177     @Override
178     public String getId() {
179       return ID;
180     }
181
182     @NotNull
183     @Override
184     public String getTitle() {
185       return "Cherry Picked Commits";
186     }
187
188     @Override
189     public boolean showMenuItem() {
190       return false;
191     }
192   }
193
194   private class MyTask extends Task.Backgroundable {
195
196     @NotNull private final Project myProject;
197     @NotNull private final Map<GitRepository, GitBranch> myRepositoriesWithCurrentBranches;
198     @NotNull private final VcsLogDataProvider myProvider;
199     @NotNull private final String myComparedBranch;
200
201     @NotNull private final Set<CommitId> myCollectedNonPickedCommits = ContainerUtil.newHashSet();
202     @Nullable private VcsException myException;
203     private boolean myCancelled;
204
205     public MyTask(@NotNull Project project,
206                   @NotNull Map<GitRepository, GitBranch> repositoriesWithCurrentBranches,
207                   @NotNull VcsLogDataProvider dataProvider,
208                   @NotNull String branchToCompare) {
209       super(project, "Comparing Branches...");
210       myProject = project;
211       myRepositoriesWithCurrentBranches = repositoriesWithCurrentBranches;
212       myProvider = dataProvider;
213       myComparedBranch = branchToCompare;
214     }
215
216     @Override
217     public void run(@NotNull ProgressIndicator indicator) {
218       try {
219         for (Map.Entry<GitRepository, GitBranch> entry : myRepositoriesWithCurrentBranches.entrySet()) {
220           GitRepository repo = entry.getKey();
221           GitBranch currentBranch = entry.getValue();
222           myCollectedNonPickedCommits
223             .addAll(getNonPickedCommitsFromGit(myProject, repo.getRoot(), currentBranch.getName(), myComparedBranch));
224         }
225       }
226       catch (VcsException e) {
227         LOG.warn(e);
228         myException = e;
229       }
230     }
231
232     @Override
233     public void onSuccess() {
234       if (myCancelled) {
235         return;
236       }
237
238       removeHighlighting();
239
240       if (myException != null) {
241         VcsNotifier.getInstance(myProject).notifyError("Couldn't compare with branch " + myComparedBranch, myException.getMessage());
242         return;
243       }
244       myNonPickedCommits = myCollectedNonPickedCommits;
245     }
246
247     public void cancel() {
248       myCancelled = true;
249     }
250
251     @NotNull
252     private Set<CommitId> getNonPickedCommitsFromGit(@NotNull Project project,
253                                                      @NotNull final VirtualFile root,
254                                                      @NotNull String currentBranch,
255                                                      @NotNull String comparedBranch) throws VcsException {
256       GitLineHandler handler = new GitLineHandler(project, root, GitCommand.CHERRY);
257       handler.addParameters(currentBranch, comparedBranch); // upstream - current branch; head - compared branch
258
259       final Set<CommitId> pickedCommits = ContainerUtil.newHashSet();
260       handler.addLineListener(new GitLineHandlerAdapter() {
261         @Override
262         public void onLineAvailable(String line, Key outputType) {
263           // + 645caac042ff7fb1a5e3f7d348f00e9ceea5c317
264           // - c3b9b90f6c26affd7e597ebf65db96de8f7e5860
265           if (line.startsWith("+")) {
266             try {
267               line = line.substring(2).trim();
268               int firstSpace = line.indexOf(' ');
269               if (firstSpace > 0) {
270                 line = line.substring(0, firstSpace); // safety-check: take just the first word for sure
271               }
272               Hash hash = HashImpl.build(line);
273               pickedCommits.add(new CommitId(hash, root));
274             }
275             catch (Exception e) {
276               LOG.error("Couldn't parse line [" + line + "]");
277             }
278           }
279         }
280       });
281       handler.runInCurrentThread(null);
282       return pickedCommits;
283     }
284   }
285 }