41be21de42ea43e803475139a402fe0953fbb38f
[idea/community.git] / plugins / git4idea / src / git4idea / history / GitDiffFromHistoryHandler.java
1 /*
2  * Copyright 2000-2012 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.history;
17
18 import com.intellij.openapi.actionSystem.*;
19 import com.intellij.openapi.actionSystem.impl.SimpleDataContext;
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.ui.DialogBuilder;
26 import com.intellij.openapi.ui.MessageType;
27 import com.intellij.openapi.ui.popup.JBPopupFactory;
28 import com.intellij.openapi.ui.popup.ListPopup;
29 import com.intellij.openapi.util.Pair;
30 import com.intellij.openapi.util.io.FileUtil;
31 import com.intellij.openapi.vcs.FilePath;
32 import com.intellij.openapi.vcs.VcsException;
33 import com.intellij.openapi.vcs.changes.Change;
34 import com.intellij.openapi.vcs.changes.ui.ChangesBrowser;
35 import com.intellij.openapi.vcs.history.*;
36 import com.intellij.openapi.vcs.ui.VcsBalloonProblemNotifier;
37 import com.intellij.openapi.vfs.VirtualFile;
38 import com.intellij.ui.awt.RelativePoint;
39 import com.intellij.util.ArrayUtil;
40 import com.intellij.util.Consumer;
41 import git4idea.GitFileRevision;
42 import git4idea.GitRevisionNumber;
43 import git4idea.GitUtil;
44 import git4idea.changes.GitChangeUtils;
45 import git4idea.commands.Git;
46 import git4idea.commands.GitCommandResult;
47 import git4idea.repo.GitRepository;
48 import git4idea.repo.GitRepositoryManager;
49 import org.jetbrains.annotations.NotNull;
50 import org.jetbrains.annotations.Nullable;
51
52 import java.awt.event.MouseEvent;
53 import java.util.ArrayList;
54 import java.util.Collection;
55 import java.util.List;
56 import java.util.concurrent.atomic.AtomicBoolean;
57
58 /**
59  * {@link DiffFromHistoryHandler#showDiff(FilePath, VcsFileRevision, VcsFileRevision) "Show Diff" for 2 revision} calls the common code.
60  * {@link DiffFromHistoryHandler#showDiff(AnActionEvent, FilePath, VcsFileRevision) "Show diff" for 1 revision}
61  * behaves differently for merge commits: for them it shown a popup displaying the parents of the selected commit. Selecting a parent
62  * from the popup shows the difference with this parent.
63  * If an ordinary (not merge) revision with 1 parent, it is the same as usual: just compare with the parent;
64  *
65  * @author Kirill Likhodedov
66  */
67 class GitDiffFromHistoryHandler implements DiffFromHistoryHandler {
68   
69   private static final Logger LOG = Logger.getInstance(GitDiffFromHistoryHandler.class);
70
71   @NotNull private final Project myProject;
72   @NotNull private final Git myGit;
73   @NotNull private final GitRepositoryManager myRepositoryManager;
74
75   GitDiffFromHistoryHandler(@NotNull Project project) {
76     myProject = project;
77     myGit = ServiceManager.getService(project, Git.class);
78     myRepositoryManager = GitUtil.getRepositoryManager(project);
79   }
80
81   @Override
82   public void showDiff(@NotNull AnActionEvent e, @NotNull FilePath filePath, @NotNull VcsFileRevision revision) {
83     GitFileRevision rev = (GitFileRevision)revision;
84     Collection<String> parents = rev.getParents();
85     if (parents.size() < 2) {
86       showDiffWithParent(revision, filePath, parents);
87     }
88     else { // merge 
89       showDiffForMergeCommit(e, filePath, rev, parents);
90     }
91   }
92
93   @Override
94   public void showDiff(@NotNull FilePath filePath, @NotNull VcsFileRevision revision1, @NotNull VcsFileRevision revision2) {
95     doShowDiff(filePath, revision1, revision2, true);
96   }
97
98   private void doShowDiff(@NotNull FilePath filePath, @NotNull VcsFileRevision revision1, @NotNull VcsFileRevision revision2,
99                           boolean autoSort) {
100     if (!filePath.isDirectory()) {
101       VcsHistoryUtil.showDifferencesInBackground(myProject, filePath, revision1, revision2, autoSort);
102     }
103     else if (revision2 instanceof CurrentRevision) {
104       GitFileRevision left = (GitFileRevision)revision1;
105       showDiffForDirectory(filePath, left.getHash(), null);
106     }
107     else {
108       GitFileRevision left = (GitFileRevision)revision1;
109       GitFileRevision right = (GitFileRevision)revision2;
110       if (autoSort) {
111         Pair<VcsFileRevision, VcsFileRevision> pair = VcsHistoryUtil.sortRevisions(revision1, revision2);
112         left = (GitFileRevision)pair.first;
113         right = (GitFileRevision)pair.second;
114       }
115       showDiffForDirectory(filePath, left.getHash(), right.getHash());
116     }
117   }
118
119   private void showDiffForDirectory(@NotNull final FilePath path, @NotNull final String hash1, @Nullable final String hash2) {
120     GitRepository repository = getRepository(path);
121     calculateDiffInBackground(repository, hash1, hash2, new Consumer<List<Change>>() {
122       @Override
123       public void consume(List<Change> changes) {
124         showDirDiffDialog(path, hash1, hash2, changes);
125       }
126     });
127   }
128
129   @NotNull
130   private GitRepository getRepository(@NotNull FilePath path) {
131     VirtualFile file = path.getVirtualFile();
132     LOG.assertTrue(file != null, "VirtualFile can't be null for " + path); // we clicked on a file and asked its history => VF must exist.
133     GitRepository repository = myRepositoryManager.getRepositoryForFile(file);
134     LOG.assertTrue(repository != null, "Repository is null for " + file);
135     return repository;
136   }
137
138   private void calculateDiffInBackground(@NotNull final GitRepository repository, final String hash1, @Nullable final String hash2,
139                                          final Consumer<List<Change>> successHandler) {
140     new Task.Backgroundable(myProject, "Comparing revisions...") {
141       private List<Change> myChanges;
142       @Override
143       public void run(@NotNull ProgressIndicator indicator) {
144         try {
145           myChanges = new ArrayList<Change>(GitChangeUtils.getDiff(repository.getProject(), repository.getRoot(), hash1, hash2));
146         }
147         catch (VcsException e) {
148           showError(e, "Error during requesting diff for directory");
149         }
150       }
151
152       @Override
153       public void onSuccess() {
154         successHandler.consume(myChanges);
155       }
156     }.queue();
157   }
158
159   private void showDirDiffDialog(@NotNull FilePath path, @NotNull String hash1, @Nullable String hash2, @NotNull List<Change> diff) {
160     DialogBuilder dialogBuilder = new DialogBuilder(myProject);
161     String title;
162     if (hash2 != null) {
163       title = String.format("Difference between %s and %s in %s", GitUtil.getShortHash(hash1), GitUtil.getShortHash(hash2), path.getName());
164     }
165     else {
166       title = String.format("Difference between %s and local version in %s", GitUtil.getShortHash(hash1), path.getName());
167     }
168     dialogBuilder.setTitle(title);
169     dialogBuilder.setActionDescriptors(new DialogBuilder.ActionDescriptor[] { new DialogBuilder.CloseDialogAction()});
170     final ChangesBrowser changesBrowser = new ChangesBrowser(myProject, null, diff, null, false, true,
171                                                              null, ChangesBrowser.MyUseCase.COMMITTED_CHANGES, null);
172     changesBrowser.setChangesToDisplay(diff);
173     dialogBuilder.setCenterPanel(changesBrowser);
174     dialogBuilder.show();
175   }
176
177   private void showDiffForMergeCommit(@NotNull final AnActionEvent event, @NotNull final FilePath filePath,
178                                       @NotNull final GitFileRevision rev, @NotNull final Collection<String> parents) {
179
180     final Consumer<Boolean> afterTouchCheck = new Consumer<Boolean>() {
181       @Override
182       public void consume(Boolean wasTouched) {
183         if (wasTouched) {
184           String message = filePath.getName() + " did not change in this merge commit";
185           VcsBalloonProblemNotifier.showOverVersionControlView(GitDiffFromHistoryHandler.this.myProject, message, MessageType.INFO);
186         }
187         showPopup(event, rev, filePath, parents);
188       }
189     };
190
191     if (filePath.isDirectory()) {        // for directories don't check if the file was modified in the merge commit
192       afterTouchCheck.consume(false);
193     }
194     else {
195       checkIfFileWasTouchedInBackground(filePath, rev, afterTouchCheck);
196     }
197   }
198
199   private void checkIfFileWasTouchedInBackground(@NotNull final FilePath filePath, @NotNull final GitFileRevision rev,
200                                                  @NotNull final Consumer<Boolean> afterTouchCheck) {
201     new Task.Backgroundable(myProject, "Loading changes...", false) {
202       private final AtomicBoolean fileTouched = new AtomicBoolean();
203
204       @Override public void run(@NotNull ProgressIndicator indicator) {
205         try {
206           fileTouched.set(wasFileTouched(rev, filePath));
207         }
208         catch (VcsException e) {
209           String logMessage = "Error happened while executing git show " + rev + ":" + filePath;
210           showError(e, logMessage);
211         }
212       }
213
214       @Override
215       public void onSuccess() {
216         afterTouchCheck.consume(fileTouched.get());
217       }
218     }.queue();
219   }
220
221   private void showError(VcsException e, String logMessage) {
222     LOG.info(logMessage, e);
223     VcsBalloonProblemNotifier.showOverVersionControlView(this.myProject, e.getMessage(), MessageType.ERROR);
224   }
225
226   private void showPopup(@NotNull AnActionEvent event, @NotNull GitFileRevision rev, @NotNull FilePath filePath,
227                          @NotNull Collection<String> parents) {
228     ActionGroup parentActions = createActionGroup(rev, filePath, parents);
229     DataContext dataContext = SimpleDataContext.getProjectContext(myProject);
230     ListPopup popup = JBPopupFactory.getInstance().createActionGroupPopup("Choose parent to compare", parentActions, dataContext,
231                                                                           JBPopupFactory.ActionSelectionAid.NUMBERING, true);
232     showPopupInBestPosition(popup, event, dataContext);
233   }
234
235   private static void showPopupInBestPosition(@NotNull ListPopup popup, @NotNull AnActionEvent event, @NotNull DataContext dataContext) {
236     if (event.getInputEvent() instanceof MouseEvent) {
237       if (!event.getPlace().equals(ActionPlaces.UPDATE_POPUP)) {
238         popup.show(new RelativePoint((MouseEvent)event.getInputEvent()));
239       }
240       else { // quick fix for invoking from the context menu: coordinates are calculated incorrectly there.
241         popup.showInBestPositionFor(dataContext);
242       }
243     }
244     else {
245       popup.showInBestPositionFor(dataContext);
246     }
247   }
248
249   @NotNull
250   private ActionGroup createActionGroup(@NotNull GitFileRevision rev, @NotNull FilePath filePath, @NotNull Collection<String> parents) {
251     Collection<AnAction> actions = new ArrayList<AnAction>(2);
252     for (String parent : parents) {
253       actions.add(createParentAction(rev, filePath, parent));
254     }
255     return new DefaultActionGroup(ArrayUtil.toObjectArray(actions, AnAction.class));
256   }
257
258   @NotNull
259   private AnAction createParentAction(@NotNull GitFileRevision rev, @NotNull FilePath filePath, @NotNull String parent) {
260     return new ShowDiffWithParentAction(filePath, rev, parent);
261   }
262
263   private void showDiffWithParent(@NotNull VcsFileRevision revision, @NotNull FilePath filePath, @NotNull Collection<String> parents) {
264     VcsFileRevision parentRevision;
265     if (parents.size() == 1) {
266       String parent = parents.iterator().next();
267       parentRevision = makeRevisionFromHash(filePath, parent);
268     }
269     else {
270       parentRevision = VcsFileRevision.NULL;
271     }
272     doShowDiff(filePath, parentRevision, revision, false);
273   }
274
275   @NotNull
276   private GitFileRevision makeRevisionFromHash(@NotNull FilePath filePath, @NotNull String hash) {
277     return new GitFileRevision(myProject, filePath, new GitRevisionNumber(hash), false);
278   }
279
280   private boolean wasFileTouched(@NotNull GitFileRevision rev, @NotNull FilePath path) throws VcsException {
281     GitRepository repository = getRepository(path);
282     GitCommandResult result = myGit.show(repository, rev + ":" + path);
283     if (result.success()) {
284       return isFilePresentInOutput(repository, path, result.getOutput());
285     }
286     throw new VcsException(result.getErrorOutputAsJoinedString());
287   }
288
289   private static boolean isFilePresentInOutput(@NotNull GitRepository repository, @NotNull FilePath path, @NotNull List<String> output) {
290     String relativePath = FileUtil.getRelativePath(repository.getRoot().getPath(), path.getPath(), '/');
291     for (String line : output) {
292       if (line.startsWith("---") || line.startsWith("+++")) {
293         if (line.contains(relativePath)) {
294           return true;
295         }
296       }
297     }
298     return false;
299   }
300
301   private class ShowDiffWithParentAction extends AnAction {
302
303     @NotNull private final FilePath myFilePath;
304     @NotNull private final GitFileRevision myRevision;
305     @NotNull private final String myParentRevision;
306
307     public ShowDiffWithParentAction(@NotNull FilePath filePath, @NotNull GitFileRevision rev, @NotNull String parent) {
308       super(GitUtil.getShortHash(parent));
309       myFilePath = filePath;
310       myRevision = rev;
311       myParentRevision = parent;
312     }
313
314     @Override
315     public void actionPerformed(AnActionEvent e) {
316       doShowDiff(myFilePath, makeRevisionFromHash(myFilePath, myParentRevision), myRevision, false);
317     }
318
319   }
320 }