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