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