[git] Compare with Branch for directories
[idea/community.git] / plugins / git4idea / src / git4idea / actions / GitCompareWithBranchAction.java
1 /*
2  * Copyright 2000-2010 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.actions;
17
18 import com.intellij.notification.NotificationType;
19 import com.intellij.openapi.actionSystem.AnActionEvent;
20 import com.intellij.openapi.actionSystem.PlatformDataKeys;
21 import com.intellij.openapi.actionSystem.Presentation;
22 import com.intellij.openapi.diagnostic.Logger;
23 import com.intellij.openapi.project.DumbAwareAction;
24 import com.intellij.openapi.project.Project;
25 import com.intellij.openapi.ui.popup.JBPopupFactory;
26 import com.intellij.openapi.vcs.FilePath;
27 import com.intellij.openapi.vcs.FilePathImpl;
28 import com.intellij.openapi.vcs.VcsException;
29 import com.intellij.openapi.vcs.history.CurrentRevision;
30 import com.intellij.openapi.vcs.history.VcsFileRevision;
31 import com.intellij.openapi.vfs.VirtualFile;
32 import com.intellij.ui.components.JBList;
33 import git4idea.GitBranch;
34 import git4idea.GitFileRevision;
35 import git4idea.GitRevisionNumber;
36 import git4idea.GitUtil;
37 import git4idea.history.GitDiffFromHistoryHandler;
38 import git4idea.history.GitHistoryUtils;
39 import git4idea.repo.GitRepository;
40 import git4idea.repo.GitRepositoryManager;
41 import git4idea.util.GitUIUtil;
42 import org.jetbrains.annotations.NotNull;
43
44 import java.util.ArrayList;
45 import java.util.Arrays;
46 import java.util.Collections;
47 import java.util.List;
48
49 /**
50  * Compares selected file with itself in another branch.
51  * @author Kirill Likhodedov
52  */
53 public class GitCompareWithBranchAction extends DumbAwareAction {
54
55   private static final Logger LOG = Logger.getInstance(GitCompareWithBranchAction.class.getName());
56
57   @Override
58   public void actionPerformed(final AnActionEvent event) {
59     final Project project = event.getProject();
60     assert project != null;
61
62     final VirtualFile file = getAffectedFile(event);
63
64     GitRepositoryManager manager = GitUtil.getRepositoryManager(project);
65     GitRepository repository = manager.getRepositoryForFile(file);
66     assert repository != null;
67
68     GitBranch currentBranch = repository.getCurrentBranch();
69     final String head;
70     if (currentBranch == null) {
71       String currentRevision = repository.getCurrentRevision();
72       LOG.assertTrue(currentRevision != null,
73                      "Current revision is null for " + repository + ". Compare with branch shouldn't be available for fresh repository");
74       head = GitUtil.getShortHash(currentRevision);
75     }
76     else {
77       head = currentBranch.getName();
78     }
79     final List<String> branchNames = getBranchNamesExceptCurrent(repository);
80
81     // prepare and invoke popup
82     final JBList list = new JBList(branchNames);
83
84     JBPopupFactory.getInstance()
85       .createListPopupBuilder(list)
86       .setTitle("Select branch to compare")
87       .setItemChoosenCallback(new OnBranchChooseRunnable(project, file, head, list))
88       .setAutoselectOnMouseMove(true)
89       .createPopup()
90       .showInBestPositionFor(event.getDataContext());
91   }
92
93   private static List<String> getBranchNamesExceptCurrent(GitRepository repository) {
94     List<GitBranch> localBranches = new ArrayList<GitBranch>(repository.getBranches().getLocalBranches());
95     Collections.sort(localBranches);
96     List<GitBranch> remoteBranches = new ArrayList<GitBranch>(repository.getBranches().getRemoteBranches());
97     Collections.sort(remoteBranches);
98     
99     if (repository.isOnBranch()) {
100       localBranches.remove(repository.getCurrentBranch());
101     }
102     
103     final List<String> branchNames = new ArrayList<String>();
104     for (GitBranch branch : localBranches) {
105       branchNames.add(branch.getName());
106     }
107     for (GitBranch branch : remoteBranches) {
108       branchNames.add(branch.getName());
109     }
110     return branchNames;
111   }
112
113   private static VirtualFile getAffectedFile(AnActionEvent event) {
114     final VirtualFile[] vFiles = event.getData(PlatformDataKeys.VIRTUAL_FILE_ARRAY);
115     assert vFiles != null && vFiles.length == 1 && vFiles[0] != null : "Illegal virtual files selected: " + Arrays.toString(vFiles);
116     return vFiles[0];
117   }
118
119   @Override
120   public void update(AnActionEvent e) {
121     super.update(e);
122     Presentation presentation = e.getPresentation();
123     Project project = e.getProject();
124     if (project == null) {
125       presentation.setEnabled(false);
126       presentation.setVisible(false);
127       return;
128     }
129
130     VirtualFile[] vFiles = e.getData(PlatformDataKeys.VIRTUAL_FILE_ARRAY);
131     if (vFiles == null || vFiles.length != 1 || vFiles[0] == null) { // only 1 file for now
132       presentation.setEnabled(false);
133       presentation.setVisible(true);
134       return;
135     }
136
137     GitRepositoryManager manager = GitUtil.getRepositoryManager(project);
138
139     GitRepository repository = manager.getRepositoryForFile(vFiles[0]);
140     if (repository == null || repository.isFresh() || noBranchesToCompare(repository)) {
141       presentation.setEnabled(false);
142       presentation.setVisible(true);
143       return;
144     }
145
146     presentation.setEnabled(true);
147     presentation.setVisible(true);
148   }
149
150   private static boolean noBranchesToCompare(@NotNull GitRepository repository) {
151     int locals = repository.getBranches().getLocalBranches().size();
152     boolean haveRemotes = !repository.getBranches().getRemoteBranches().isEmpty();
153     if (repository.isOnBranch()) {  // there are other branches to compare
154       return locals < 2 && !haveRemotes;
155     }
156     return locals == 0 && !haveRemotes; // there are at least 1 branch to compare
157   }
158
159   private static class OnBranchChooseRunnable implements Runnable {
160     private final Project myProject;
161     private final VirtualFile myFile;
162     private final String myHead;
163     private final JBList myList;
164
165     public OnBranchChooseRunnable(Project project, VirtualFile file, String head, JBList list) {
166       myProject = project;
167       myFile = file;
168       myHead = head;
169       myList = list;
170     }
171
172     @Override
173     public void run() {
174       String branchToCompare = myList.getSelectedValue().toString();
175       try {
176         showDiffWithBranch(myProject, myFile, myHead, branchToCompare);
177       }
178       catch (VcsException e) {
179         if (e.getMessage().contains("exists on disk, but not in")) {
180           fileDoesntExistInBranchError(myProject, myFile, branchToCompare);
181         } else {
182           GitUIUtil.notifyError(myProject, "Couldn't compare with branch",
183                                 String.format("Couldn't compare file [%s] with selected branch [%s]", myFile, myList.getSelectedValue()),
184                                 false, e);
185         }
186       }
187     }
188
189     private static void showDiffWithBranch(@NotNull Project project, @NotNull VirtualFile file, @NotNull String head,
190                                            @NotNull String branchToCompare) throws VcsException {
191       final FilePath filePath = new FilePathImpl(file);
192       // we could use something like GitRepository#getCurrentRevision here,
193       // but this way we can easily identify if the file is available in the branch
194       final GitRevisionNumber currentRevisionNumber = (GitRevisionNumber)GitHistoryUtils.getCurrentRevision(project, filePath, head);
195       final GitRevisionNumber compareRevisionNumber =
196         (GitRevisionNumber)GitHistoryUtils.getCurrentRevision(project, filePath, branchToCompare);
197
198       if (compareRevisionNumber == null) {
199         fileDoesntExistInBranchError(project, file, branchToCompare);
200         return;
201       }
202       LOG.assertTrue(currentRevisionNumber != null,
203                      String.format("Current revision number is null for file [%s] and branch [%s]", filePath, head));
204
205       // constructing the revision with human readable name (will work for files comparison however).
206       final VcsFileRevision compareRevision =
207         new GitFileRevision(project, filePath, new GitRevisionNumber(branchToCompare, compareRevisionNumber.getTimestamp()), false);
208       CurrentRevision currentRevision = new CurrentRevision(file, new GitRevisionNumber(head, currentRevisionNumber.getTimestamp()));
209       new GitDiffFromHistoryHandler(project).showDiff(new FilePathImpl(file), compareRevision, currentRevision);
210     }
211
212     private static void fileDoesntExistInBranchError(Project project, VirtualFile file, String branchToCompare) {
213       GitUIUtil.notifyMessage(project, "File doesn't exist in branch",
214                               String.format("File <code>%s</code> doesn't exist in branch <code>%s</code>", file.getPresentableUrl(), branchToCompare),
215                               NotificationType.WARNING, true, null);
216     }
217   }
218
219 }