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