7aed03546ce8a0eb12cb4b7faee6912624bd53d3
[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     if (refreshHappened) {
143       // collect data
144       String comparedBranch = myTask.myComparedBranch;
145       Map<GitRepository, GitBranch> repositoriesWithCurrentBranches = myTask.myRepositoriesWithCurrentBranches;
146       VcsLogDataProvider provider = myTask.myProvider;
147
148       stopTask();
149
150       // highlight again
151       Map<GitRepository, GitBranch> repositories = getRepositories(dataPack.getLogProviders(), comparedBranch);
152       if (repositories.equals(repositoriesWithCurrentBranches)) { // but not if current branch changed
153         highlightInBackground(comparedBranch, provider);
154       }
155     }
156     else {
157       VcsLogBranchFilter branchFilter = dataPack.getFilters().getBranchFilter();
158       if (branchFilter == null || !myTask.myComparedBranch.equals(VcsLogUtil.getSingleFilteredBranch(branchFilter, dataPack.getRefs()))) {
159         stopAndUnhighlight();
160       }
161     }
162   }
163
164   public static class Factory implements VcsLogHighlighterFactory {
165     @NotNull private static final String ID = "CHERRY_PICKED_COMMITS";
166
167     @NotNull
168     @Override
169     public VcsLogHighlighter createHighlighter(@NotNull VcsLogData logDataManager, @NotNull VcsLogUi logUi) {
170       return getInstance(logDataManager.getProject(), logUi);
171     }
172
173     @NotNull
174     @Override
175     public String getId() {
176       return ID;
177     }
178
179     @NotNull
180     @Override
181     public String getTitle() {
182       return "Cherry Picked Commits";
183     }
184
185     @Override
186     public boolean showMenuItem() {
187       return false;
188     }
189   }
190
191   private class MyTask extends Task.Backgroundable {
192
193     @NotNull private final Project myProject;
194     @NotNull private final Map<GitRepository, GitBranch> myRepositoriesWithCurrentBranches;
195     @NotNull private final VcsLogDataProvider myProvider;
196     @NotNull private final String myComparedBranch;
197
198     @NotNull private final Set<CommitId> myCollectedNonPickedCommits = ContainerUtil.newHashSet();
199     @Nullable private VcsException myException;
200     private boolean myCancelled;
201
202     public MyTask(@NotNull Project project,
203                   @NotNull Map<GitRepository, GitBranch> repositoriesWithCurrentBranches,
204                   @NotNull VcsLogDataProvider dataProvider,
205                   @NotNull String branchToCompare) {
206       super(project, "Comparing Branches...");
207       myProject = project;
208       myRepositoriesWithCurrentBranches = repositoriesWithCurrentBranches;
209       myProvider = dataProvider;
210       myComparedBranch = branchToCompare;
211     }
212
213     @Override
214     public void run(@NotNull ProgressIndicator indicator) {
215       try {
216         for (Map.Entry<GitRepository, GitBranch> entry : myRepositoriesWithCurrentBranches.entrySet()) {
217           GitRepository repo = entry.getKey();
218           GitBranch currentBranch = entry.getValue();
219           myCollectedNonPickedCommits
220             .addAll(getNonPickedCommitsFromGit(myProject, repo.getRoot(), currentBranch.getName(), myComparedBranch));
221         }
222       }
223       catch (VcsException e) {
224         LOG.warn(e);
225         myException = e;
226       }
227     }
228
229     @Override
230     public void onSuccess() {
231       if (myCancelled) {
232         return;
233       }
234
235       removeHighlighting();
236
237       if (myException != null) {
238         VcsNotifier.getInstance(myProject).notifyError("Couldn't compare with branch " + myComparedBranch, myException.getMessage());
239         return;
240       }
241       myNonPickedCommits = myCollectedNonPickedCommits;
242     }
243
244     public void cancel() {
245       myCancelled = true;
246     }
247
248     @NotNull
249     private Set<CommitId> getNonPickedCommitsFromGit(@NotNull Project project,
250                                                      @NotNull final VirtualFile root,
251                                                      @NotNull String currentBranch,
252                                                      @NotNull String comparedBranch) throws VcsException {
253       GitLineHandler handler = new GitLineHandler(project, root, GitCommand.CHERRY);
254       handler.addParameters(currentBranch, comparedBranch); // upstream - current branch; head - compared branch
255
256       final Set<CommitId> pickedCommits = ContainerUtil.newHashSet();
257       handler.addLineListener(new GitLineHandlerAdapter() {
258         @Override
259         public void onLineAvailable(String line, Key outputType) {
260           // + 645caac042ff7fb1a5e3f7d348f00e9ceea5c317
261           // - c3b9b90f6c26affd7e597ebf65db96de8f7e5860
262           if (line.startsWith("+")) {
263             try {
264               line = line.substring(2).trim();
265               int firstSpace = line.indexOf(' ');
266               if (firstSpace > 0) {
267                 line = line.substring(0, firstSpace); // safety-check: take just the first word for sure
268               }
269               Hash hash = HashImpl.build(line);
270               pickedCommits.add(new CommitId(hash, root));
271             }
272             catch (Exception e) {
273               LOG.error("Couldn't parse line [" + line + "]");
274             }
275           }
276         }
277       });
278       handler.runInCurrentThread(null);
279       return pickedCommits;
280     }
281
282   }
283 }