353e43b637f07350a6f96af16f5d1040957f5a1b
[idea/community.git] / plugins / svn4idea / src / org / jetbrains / idea / svn / treeConflict / TreeConflictRefreshablePanel.java
1 /*
2  * Copyright 2000-2016 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 org.jetbrains.idea.svn.treeConflict;
17
18 import com.intellij.openapi.CompositeDisposable;
19 import com.intellij.openapi.Disposable;
20 import com.intellij.openapi.application.ApplicationManager;
21 import com.intellij.openapi.fileEditor.FileDocumentManager;
22 import com.intellij.openapi.progress.*;
23 import com.intellij.openapi.project.Project;
24 import com.intellij.openapi.ui.MessageType;
25 import com.intellij.openapi.ui.Messages;
26 import com.intellij.openapi.util.Comparing;
27 import com.intellij.openapi.util.Disposer;
28 import com.intellij.openapi.util.io.FileUtil;
29 import com.intellij.openapi.vcs.FilePath;
30 import com.intellij.openapi.vcs.VcsException;
31 import com.intellij.openapi.vcs.changes.Change;
32 import com.intellij.openapi.vcs.changes.ChangesUtil;
33 import com.intellij.openapi.vcs.history.*;
34 import com.intellij.openapi.vcs.ui.VcsBalloonProblemNotifier;
35 import com.intellij.ui.JBColor;
36 import com.intellij.ui.components.JBLoadingPanel;
37 import com.intellij.util.BeforeAfter;
38 import com.intellij.util.containers.Convertor;
39 import com.intellij.util.ui.JBUI;
40 import com.intellij.util.ui.UIUtil;
41 import com.intellij.util.ui.VcsBackgroundTask;
42 import com.intellij.vcsUtil.VcsUtil;
43 import gnu.trove.TLongArrayList;
44 import org.jetbrains.annotations.CalledInAwt;
45 import org.jetbrains.annotations.CalledInBackground;
46 import org.jetbrains.annotations.NotNull;
47 import org.jetbrains.annotations.Nullable;
48 import org.jetbrains.idea.svn.ConflictedSvnChange;
49 import org.jetbrains.idea.svn.SvnRevisionNumber;
50 import org.jetbrains.idea.svn.SvnVcs;
51 import org.jetbrains.idea.svn.conflict.ConflictAction;
52 import org.jetbrains.idea.svn.conflict.ConflictReason;
53 import org.jetbrains.idea.svn.conflict.ConflictVersion;
54 import org.jetbrains.idea.svn.conflict.TreeConflictDescription;
55 import org.jetbrains.idea.svn.history.SvnHistoryProvider;
56 import org.tmatesoft.svn.core.SVNException;
57 import org.tmatesoft.svn.core.wc.SVNRevision;
58
59 import javax.swing.*;
60 import java.awt.*;
61 import java.awt.event.ActionEvent;
62 import java.awt.event.ActionListener;
63 import java.util.Collections;
64 import java.util.List;
65
66 import static com.intellij.util.ObjectUtils.notNull;
67 import static org.jetbrains.idea.svn.history.SvnHistorySession.getCurrentCommittedRevision;
68
69 public class TreeConflictRefreshablePanel implements Disposable {
70
71   public static final String TITLE = "Resolve tree conflict";
72   private final ConflictedSvnChange myChange;
73   private final SvnVcs myVcs;
74   private SvnRevisionNumber myCommittedRevision;
75   private FilePath myPath;
76   private final CompositeDisposable myChildDisposables = new CompositeDisposable();
77   private final TLongArrayList myRightRevisionsList;
78   @NotNull private final String myLoadingTitle;
79   @NotNull private final JBLoadingPanel myDetailsPanel;
80   @NotNull private final BackgroundTaskQueue myQueue;
81
82   public TreeConflictRefreshablePanel(@NotNull Project project,
83                                       @NotNull String loadingTitle,
84                                       @NotNull BackgroundTaskQueue queue,
85                                       Change change) {
86     myVcs = SvnVcs.getInstance(project);
87     assert change instanceof ConflictedSvnChange;
88     myChange = (ConflictedSvnChange) change;
89     myPath = ChangesUtil.getFilePath(myChange);
90     myRightRevisionsList = new TLongArrayList();
91
92     myLoadingTitle = loadingTitle;
93     myQueue = queue;
94     myDetailsPanel = new JBLoadingPanel(new BorderLayout(), this);
95   }
96
97   public static boolean descriptionsEqual(TreeConflictDescription d1, TreeConflictDescription d2) {
98     if (d1.isPropertyConflict() != d2.isPropertyConflict()) return false;
99     if (d1.isTextConflict() != d2.isTextConflict()) return false;
100     if (d1.isTreeConflict() != d2.isTreeConflict()) return false;
101
102     if (! d1.getOperation().equals(d2.getOperation())) return false;
103     if (! d1.getConflictAction().equals(d2.getConflictAction())) return false;
104     if (! Comparing.equal(d1.getConflictReason(), d2.getConflictReason())) return false;
105     if (! Comparing.equal(d1.getPath(), d2.getPath())) return false;
106     if (! Comparing.equal(d1.getNodeKind(), d2.getNodeKind())) return false;
107     if (! compareConflictVersion(d1.getSourceLeftVersion(), d2.getSourceLeftVersion())) return false;
108     if (! compareConflictVersion(d1.getSourceRightVersion(), d2.getSourceRightVersion())) return false;
109     return true;
110   }
111
112   private static boolean compareConflictVersion(ConflictVersion v1, ConflictVersion v2) {
113     if (v1 == null && v2 == null) return true;
114     if (v1 == null || v2 == null) return false;
115     if (! v1.getKind().equals(v2.getKind())) return false;
116     if (! v1.getPath().equals(v2.getPath())) return false;
117     if (v1.getPegRevision() != v2.getPegRevision()) return false;
118     if (! Comparing.equal(v1.getRepositoryRoot(), v2.getRepositoryRoot())) return false;
119     return true;
120   }
121
122   @NotNull
123   public JPanel getPanel() {
124     return myDetailsPanel;
125   }
126
127   @CalledInBackground
128   private BeforeAfter<ConflictSidePresentation> processDescription(TreeConflictDescription description) throws VcsException {
129     if (description == null) return null;
130     if (myChange.getBeforeRevision() != null) {
131       myCommittedRevision = (SvnRevisionNumber)getCurrentCommittedRevision(myVcs, myChange.getBeforeRevision() != null ? myChange
132         .getBeforeRevision().getFile().getIOFile() : myPath.getIOFile());
133     }
134
135     ConflictSidePresentation leftSide;
136     ConflictSidePresentation rightSide;
137     if (isDifferentURLs(description)) {
138       leftSide = createSide(description.getSourceLeftVersion(), null, true);
139       rightSide = createSide(description.getSourceRightVersion(), null, false);
140     }
141     else { //only one side
142       leftSide = createSide(null, null, true);
143       rightSide = createSide(description.getSourceRightVersion(), getPegRevisionFromLeftSide(description), false);
144     }
145     leftSide.load();
146     rightSide.load();
147
148     return new BeforeAfter<>(leftSide, rightSide);
149   }
150
151   @Nullable
152   private SVNRevision getPegRevisionFromLeftSide(@NotNull TreeConflictDescription description) {
153     SVNRevision result = null;
154     if (description.getSourceLeftVersion() != null) {
155       long committed = description.getSourceLeftVersion().getPegRevision();
156       if (myCommittedRevision != null &&
157           myCommittedRevision.getRevision().getNumber() < committed &&
158           myCommittedRevision.getRevision().isValid()) {
159         committed = myCommittedRevision.getRevision().getNumber();
160       }
161       result = SVNRevision.create(committed);
162     }
163     return result;
164   }
165
166   private static boolean isDifferentURLs(TreeConflictDescription description) {
167     return description.getSourceLeftVersion() != null && description.getSourceRightVersion() != null &&
168                 ! Comparing.equal(description.getSourceLeftVersion().getPath(), description.getSourceRightVersion().getPath());
169   }
170
171   @NotNull
172   private ConflictSidePresentation createSide(@Nullable ConflictVersion version, @Nullable SVNRevision untilThisOther, boolean isLeft)
173     throws VcsException {
174     ConflictSidePresentation result = EmptyConflictSide.getInstance();
175     if (version != null &&
176         (myChange.getBeforeRevision() == null ||
177          myCommittedRevision == null ||
178          !isLeft ||
179          !myCommittedRevision.getRevision().isValid() ||
180          myCommittedRevision.getRevision().getNumber() != version.getPegRevision())) {
181       HistoryConflictSide side = new HistoryConflictSide(myVcs, version, untilThisOther);
182       if (untilThisOther != null && !isLeft) {
183         side.setListToReportLoaded(myRightRevisionsList);
184       }
185       result = side;
186     }
187     myChildDisposables.add(result);
188     return result;
189   }
190
191   @CalledInAwt
192   public void refresh() {
193     ApplicationManager.getApplication().assertIsDispatchThread();
194
195     myDetailsPanel.startLoading();
196     myQueue.run(new Loader(myVcs.getProject(), myLoadingTitle));
197   }
198
199   @CalledInAwt
200   protected JPanel dataToPresentation(BeforeAfter<BeforeAfter<ConflictSidePresentation>> data) {
201     final JPanel wrapper = new JPanel(new BorderLayout());
202     final JPanel main = new JPanel(new GridBagLayout());
203
204     final GridBagConstraints gb = new GridBagConstraints(0, 0, 1, 1, 1, 0, GridBagConstraints.NORTHWEST, GridBagConstraints.HORIZONTAL,
205                                                          JBUI.insets(1), 0, 0);
206     final String pathComment = myCommittedRevision == null ? "" :
207                                " (current: " +
208                                myChange.getBeforeRevision().getRevisionNumber().asString() +
209                                ", committed: " +
210                                myCommittedRevision.asString() +
211                                ")";
212     final JLabel name = new JLabel(myPath.getName() + pathComment);
213     name.setFont(name.getFont().deriveFont(Font.BOLD));
214     gb.insets.top = 5;
215     main.add(name, gb);
216     ++ gb.gridy;
217     gb.insets.top = 10;
218     appendDescription(myChange.getBeforeDescription(), main, gb, data.getBefore(), myPath.isDirectory());
219     appendDescription(myChange.getAfterDescription(), main, gb, data.getAfter(), myPath.isDirectory());
220     wrapper.add(main, BorderLayout.NORTH);
221     return wrapper;
222   }
223
224   private void appendDescription(TreeConflictDescription description,
225                                  JPanel main,
226                                  GridBagConstraints gb,
227                                  BeforeAfter<ConflictSidePresentation> ba, boolean directory) {
228     if (description == null) return;
229     JLabel descriptionLbl = new JLabel(description.toPresentableString());
230     descriptionLbl.setForeground(JBColor.RED);
231     main.add(descriptionLbl, gb);
232     ++ gb.gridy;
233     //buttons
234     gb.insets.top = 0;
235     addResolveButtons(description, main, gb);
236
237     addSide(main, gb, ba.getBefore(), description.getSourceLeftVersion(), "Left", directory);
238     addSide(main, gb, ba.getAfter(), description.getSourceRightVersion(), "Right", directory);
239   }
240
241   private void addResolveButtons(TreeConflictDescription description, JPanel main, GridBagConstraints gb) {
242     final FlowLayout flowLayout = new FlowLayout(FlowLayout.LEFT, 5, 5);
243     JPanel wrapper = new JPanel(flowLayout);
244     final JButton both = new JButton("Both");
245     final JButton merge = new JButton("Merge");
246     final JButton left = new JButton("Accept Yours");
247     final JButton right = new JButton("Accept Theirs");
248     enableAndSetListener(createBoth(description), both);
249     enableAndSetListener(createMerge(description), merge);
250     enableAndSetListener(createLeft(description), left);
251     enableAndSetListener(createRight(description), right);
252     //wrapper.add(both);
253     if (merge.isEnabled()) {
254       wrapper.add(merge);
255     }
256     wrapper.add(left);
257     wrapper.add(right);
258     gb.insets.left = -4;
259     main.add(wrapper, gb);
260     gb.insets.left = 1;
261     ++ gb.gridy;
262   }
263
264   private ActionListener createRight(final TreeConflictDescription description) {
265     return new ActionListener() {
266       @Override
267       public void actionPerformed(ActionEvent e) {
268         int ok = Messages.showOkCancelDialog(myVcs.getProject(), "Accept theirs for " + filePath(myPath) + "?",
269                                              TITLE, Messages.getQuestionIcon());
270         if (Messages.OK != ok) return;
271         FileDocumentManager.getInstance().saveAllDocuments();
272         final Paths paths = getPaths(description);
273         ProgressManager.getInstance().run(
274           new VcsBackgroundTask<TreeConflictDescription>(myVcs.getProject(), "Accepting theirs for: " + filePath(paths.myMainPath),
275                                                          PerformInBackgroundOption.ALWAYS_BACKGROUND,
276                                                          Collections.singletonList(description),
277                                                          true) {
278             @Override
279             protected void process(TreeConflictDescription d) throws VcsException {
280               new SvnTreeConflictResolver(myVcs, paths.myMainPath, paths.myAdditionalPath).resolveSelectTheirsFull();
281             }
282
283             @Override
284             public void onSuccess() {
285               super.onSuccess();
286               if (executedOk()) {
287                 VcsBalloonProblemNotifier.showOverChangesView(myProject, "Theirs accepted for " + filePath(paths.myMainPath), MessageType.INFO);
288               }
289             }
290           });
291       }
292     };
293   }
294
295   private Paths getPaths(final TreeConflictDescription description) {
296     FilePath mainPath;
297     FilePath additionalPath = null;
298     if (myChange.isMoved() || myChange.isRenamed()) {
299       if (ConflictAction.ADD.equals(description.getConflictAction())) {
300         mainPath = myChange.getAfterRevision().getFile();
301         additionalPath = myChange.getBeforeRevision().getFile();
302       } else {
303         mainPath = myChange.getBeforeRevision().getFile();
304         additionalPath = myChange.getAfterRevision().getFile();
305       }
306     } else {
307       mainPath = myChange.getBeforeRevision() != null ? myChange.getBeforeRevision().getFile() : myChange.getAfterRevision().getFile();
308     }
309     return new Paths(mainPath, additionalPath);
310   }
311
312   private static class Paths {
313     public final FilePath myMainPath;
314     public final FilePath myAdditionalPath;
315
316     private Paths(FilePath mainPath, FilePath additionalPath) {
317       myMainPath = mainPath;
318       myAdditionalPath = additionalPath;
319     }
320   }
321
322   private ActionListener createLeft(final TreeConflictDescription description) {
323     return new ActionListener() {
324       @Override
325       public void actionPerformed(ActionEvent e) {
326         int ok = Messages.showOkCancelDialog(myVcs.getProject(), "Accept yours for " + filePath(myPath) + "?",
327                                              TITLE, Messages.getQuestionIcon());
328         if (Messages.OK != ok) return;
329         FileDocumentManager.getInstance().saveAllDocuments();
330         final Paths paths = getPaths(description);
331         ProgressManager.getInstance().run(
332           new VcsBackgroundTask<TreeConflictDescription>(myVcs.getProject(), "Accepting yours for: " + filePath(paths.myMainPath),
333                                                          PerformInBackgroundOption.ALWAYS_BACKGROUND,
334                                                          Collections.singletonList(description),
335                                                          true) {
336             @Override
337             protected void process(TreeConflictDescription d) throws VcsException {
338               new SvnTreeConflictResolver(myVcs, paths.myMainPath, paths.myAdditionalPath).resolveSelectMineFull();
339             }
340
341             @Override
342             public void onSuccess() {
343               super.onSuccess();
344               if (executedOk()) {
345                 VcsBalloonProblemNotifier.showOverChangesView(myProject, "Yours accepted for " + filePath(paths.myMainPath), MessageType.INFO);
346               }
347             }
348           });
349       }
350     };
351   }
352
353   private ActionListener createMerge(final TreeConflictDescription description) {
354     if (isDifferentURLs(description)) {
355       return null;
356     }
357     // my edit, theirs move or delete
358     if (ConflictAction.EDIT.equals(description.getConflictAction()) && description.getSourceLeftVersion() != null &&
359         ConflictReason.DELETED.equals(description.getConflictReason()) && (myChange.isMoved() || myChange.isRenamed()) &&
360         myCommittedRevision != null) {
361       if (myPath.isDirectory() == description.getSourceRightVersion().isDirectory()) {
362         return createMergeTheirsForFile(description);
363       }
364     }
365     return null;
366   }
367
368   private ActionListener createMergeTheirsForFile(final TreeConflictDescription description) {
369     return new ActionListener() {
370       @Override
371       public void actionPerformed(ActionEvent e) {
372         new MergeFromTheirsResolver(myVcs, description, myChange, myCommittedRevision).execute();
373       }
374     };
375   }
376
377   @NotNull
378   public static String filePath(@NotNull FilePath newFilePath) {
379     return newFilePath.getName() + " (" + notNull(newFilePath.getParentPath()).getPath() + ")";
380   }
381
382   private static ActionListener createBoth(TreeConflictDescription description) {
383     return null;
384   }
385
386   private static void enableAndSetListener(final ActionListener al, final JButton b) {
387     if (al == null) {
388       b.setEnabled(false);
389     }
390     else {
391       b.addActionListener(al);
392     }
393   }
394
395   private void addSide(JPanel main,
396                        GridBagConstraints gb,
397                        ConflictSidePresentation before,
398                        ConflictVersion leftVersion, final String name, boolean directory) {
399     final String leftPresentation = leftVersion == null ? name + ": (" + (directory ? "directory" : "file") +
400       (myChange.getBeforeRevision() == null ? ") added" : ") unversioned") :
401                                     name + ": " + FileUtil.toSystemIndependentName(ConflictVersion.toPresentableString(leftVersion));
402     gb.insets.top = 10;
403     main.add(new JLabel(leftPresentation), gb);
404     ++ gb.gridy;
405     gb.insets.top = 0;
406
407     if (before != null) {
408       JPanel panel = before.createPanel();
409       if (panel != null) {
410         //gb.fill = GridBagConstraints.HORIZONTAL;
411         main.add(panel, gb);
412         //gb.fill = GridBagConstraints.NONE;
413         ++ gb.gridy;
414       }
415     }
416   }
417
418   @Override
419   public void dispose() {
420     Disposer.dispose(myChildDisposables);
421   }
422
423   private interface ConflictSidePresentation extends Disposable {
424     JPanel createPanel();
425
426     void load() throws VcsException;
427   }
428
429   private static class EmptyConflictSide implements ConflictSidePresentation {
430     private static final EmptyConflictSide ourInstance = new EmptyConflictSide();
431
432     public static EmptyConflictSide getInstance() {
433       return ourInstance;
434     }
435
436     @Override
437     public JPanel createPanel() {
438       return null;
439     }
440
441     @Override
442     public void dispose() {
443     }
444
445     @Override
446     public void load() {
447     }
448   }
449
450   private abstract static class AbstractConflictSide<T> implements ConflictSidePresentation, Convertor<T, VcsRevisionNumber> {
451     protected final Project myProject;
452     protected final ConflictVersion myVersion;
453
454     private AbstractConflictSide(Project project, ConflictVersion version) {
455       myProject = project;
456       myVersion = version;
457     }
458   }
459
460   private static class HistoryConflictSide extends AbstractConflictSide<VcsFileRevision> {
461     public static final int LIMIT = 10;
462     private final VcsAppendableHistoryPartnerAdapter mySessionAdapter;
463     private final SvnHistoryProvider myProvider;
464     private final FilePath myPath;
465     private final SvnVcs myVcs;
466     private final SVNRevision myPeg;
467     private FileHistoryPanelImpl myFileHistoryPanel;
468     private TLongArrayList myListToReportLoaded;
469
470     private HistoryConflictSide(SvnVcs vcs, ConflictVersion version, final SVNRevision peg) throws VcsException {
471       super(vcs.getProject(), version);
472       myVcs = vcs;
473       myPeg = peg;
474       try {
475         myPath = VcsUtil.getFilePathOnNonLocal(
476           version.getRepositoryRoot().appendPath(FileUtil.toSystemIndependentName(version.getPath()), true).toString(),
477           version.isDirectory());
478       }
479       catch (SVNException e) {
480         throw new VcsException(e);
481       }
482
483       mySessionAdapter = new VcsAppendableHistoryPartnerAdapter();
484       /*mySessionAdapter.reportCreatedEmptySession(new SvnHistorySession(myVcs, Collections.<VcsFileRevision>emptyList(),
485         myPath, SvnUtil.checkRepositoryVersion15(myVcs, version.getPath()), null, true));*/
486       myProvider = (SvnHistoryProvider) myVcs.getVcsHistoryProvider();
487     }
488
489     public void setListToReportLoaded(TLongArrayList listToReportLoaded) {
490       myListToReportLoaded = listToReportLoaded;
491     }
492
493     @Override
494     public VcsRevisionNumber convert(VcsFileRevision o) {
495       return o.getRevisionNumber();
496     }
497
498     @Override
499     public void load() throws VcsException {
500       SVNRevision from = SVNRevision.create(myVersion.getPegRevision());
501       myProvider.reportAppendableHistory(myPath, mySessionAdapter, from, myPeg, myPeg == null ? LIMIT : 0, myPeg, true);
502       VcsAbstractHistorySession session = mySessionAdapter.getSession();
503       if (myListToReportLoaded != null && session != null) {
504         List<VcsFileRevision> list = session.getRevisionList();
505         for (VcsFileRevision revision : list) {
506           myListToReportLoaded.add(((SvnRevisionNumber) revision.getRevisionNumber()).getRevision().getNumber());
507         }
508       }
509     }
510
511     @Override
512     public void dispose() {
513       if (myFileHistoryPanel != null) {
514         Disposer.dispose(myFileHistoryPanel);
515       }
516     }
517
518     @Override
519     public JPanel createPanel() {
520       VcsAbstractHistorySession session = mySessionAdapter.getSession();
521       if (session == null) return EmptyConflictSide.getInstance().createPanel();
522       List<VcsFileRevision> list = session.getRevisionList();
523       if (list.isEmpty()) {
524         return EmptyConflictSide.getInstance().createPanel();
525       }
526       VcsFileRevision last = null;
527       if (! list.isEmpty() && myPeg == null && list.size() == LIMIT ||
528           myPeg != null && myPeg.getNumber() > 0 &&
529           myPeg.equals(((SvnRevisionNumber) list.get(list.size() - 1).getRevisionNumber()).getRevision())) {
530         last = list.remove(list.size() - 1);
531       }
532       myFileHistoryPanel = new FileHistoryPanelImpl(myVcs, myPath, session, myProvider, null, new FileHistoryRefresherI() {
533         @Override
534         public void run(boolean isRefresh, boolean canUseCache) {
535           //we will not refresh
536         }
537
538         @Override
539         public boolean isFirstTime() {
540           return false;
541         }
542       }, true);
543       myFileHistoryPanel.setBottomRevisionForShowDiff(last);
544       myFileHistoryPanel.setBorder(BorderFactory.createLineBorder(UIUtil.getBorderColor()));
545       return myFileHistoryPanel;
546     }
547   }
548
549   private class Loader extends Task.Backgroundable {
550     private BeforeAfter<BeforeAfter<ConflictSidePresentation>> myData;
551     private VcsException myException;
552
553     private Loader(@Nullable Project project, @NotNull String title) {
554       super(project, title, false);
555     }
556
557     @Override
558     public void run(@NotNull ProgressIndicator indicator) {
559       try {
560         myData = new BeforeAfter<>(processDescription(myChange.getBeforeDescription()), processDescription(myChange.getAfterDescription()));
561       }
562       catch (VcsException e) {
563         myException = e;
564       }
565     }
566
567     @Override
568     public void onSuccess() {
569       if (myException != null) {
570         VcsBalloonProblemNotifier.showOverChangesView(myProject, myException.getMessage(), MessageType.ERROR);
571       }
572       else {
573         myDetailsPanel.add(dataToPresentation(myData));
574         myDetailsPanel.stopLoading();
575       }
576     }
577   }
578 }