931fcc6a5977192a334f4ba660fda0930ff52025
[idea/community.git] / plugins / svn4idea / src / org / jetbrains / idea / svn / history / SvnHistoryProvider.java
1 /*
2  * Copyright 2000-2009 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.history;
17
18 import com.intellij.openapi.actionSystem.AnAction;
19 import com.intellij.openapi.progress.ProgressIndicator;
20 import com.intellij.openapi.progress.ProgressManager;
21 import com.intellij.openapi.util.Pair;
22 import com.intellij.openapi.util.Ref;
23 import com.intellij.openapi.util.text.StringUtil;
24 import com.intellij.openapi.vcs.FilePath;
25 import com.intellij.openapi.vcs.VcsConfiguration;
26 import com.intellij.openapi.vcs.VcsException;
27 import com.intellij.openapi.vcs.annotate.ShowAllAffectedGenericAction;
28 import com.intellij.openapi.vcs.changes.Change;
29 import com.intellij.openapi.vcs.changes.ChangeListManager;
30 import com.intellij.openapi.vcs.changes.ContentRevision;
31 import com.intellij.openapi.vcs.changes.issueLinks.TableLinkMouseListener;
32 import com.intellij.openapi.vcs.history.*;
33 import com.intellij.openapi.vfs.VirtualFile;
34 import com.intellij.ui.*;
35 import com.intellij.util.Consumer;
36 import com.intellij.util.PlatformIcons;
37 import com.intellij.util.ThrowableConsumer;
38 import com.intellij.util.ui.ColumnInfo;
39 import com.intellij.util.ui.UIUtil;
40 import org.jetbrains.annotations.NotNull;
41 import org.jetbrains.annotations.Nullable;
42 import org.jetbrains.idea.svn.*;
43 import org.tmatesoft.svn.core.*;
44 import org.tmatesoft.svn.core.internal.util.SVNPathUtil;
45 import org.tmatesoft.svn.core.internal.wc.SVNErrorManager;
46 import org.tmatesoft.svn.core.wc.SVNInfo;
47 import org.tmatesoft.svn.core.wc.SVNLogClient;
48 import org.tmatesoft.svn.core.wc.SVNRevision;
49 import org.tmatesoft.svn.core.wc2.SvnTarget;
50 import org.tmatesoft.svn.util.SVNLogType;
51
52 import javax.swing.*;
53 import javax.swing.table.TableCellRenderer;
54 import java.awt.*;
55 import java.awt.event.MouseEvent;
56 import java.nio.charset.Charset;
57 import java.util.Collections;
58 import java.util.Date;
59 import java.util.List;
60
61 public class SvnHistoryProvider
62   implements VcsHistoryProvider, VcsCacheableHistorySessionFactory<Boolean, SvnHistorySession> {
63   private final SvnVcs myVcs;
64
65   public SvnHistoryProvider(SvnVcs vcs) {
66     myVcs = vcs;
67   }
68
69   @Override
70   public boolean supportsHistoryForDirectories() {
71     return true;
72   }
73
74   @Override
75   public DiffFromHistoryHandler getHistoryDiffHandler() {
76     return null;
77   }
78
79   @Override
80   public boolean canShowHistoryFor(@NotNull VirtualFile file) {
81     return true;
82   }
83
84   @Override
85   public VcsDependentHistoryComponents getUICustomization(final VcsHistorySession session, JComponent forShortcutRegistration) {
86     final ColumnInfo[] columns;
87     final Consumer<VcsFileRevision> listener;
88     final JComponent addComp;
89     if (((SvnHistorySession)session).isHaveMergeSources()) {
90       final MergeSourceColumnInfo mergeSourceColumn = new MergeSourceColumnInfo((SvnHistorySession)session);
91       columns = new ColumnInfo[]{new CopyFromColumnInfo(), mergeSourceColumn};
92
93       final JPanel panel = new JPanel(new BorderLayout());
94
95       final JTextArea field = new JTextArea();
96       field.setEditable(false);
97       field.setBackground(UIUtil.getComboBoxDisabledBackground());
98       field.setWrapStyleWord(true);
99       listener = new Consumer<VcsFileRevision>() {
100         @Override
101         public void consume(VcsFileRevision vcsFileRevision) {
102           field.setText(mergeSourceColumn.getText(vcsFileRevision));
103         }
104       };
105
106       final MergeSourceDetailsAction sourceAction = new MergeSourceDetailsAction();
107       sourceAction.registerSelf(forShortcutRegistration);
108
109       JPanel fieldPanel = new ToolbarDecorator() {
110         @Override
111         protected JComponent getComponent() {
112           return field;
113         }
114
115         @Override
116         protected void updateButtons() {
117         }
118
119         @Override
120         protected void installDnDSupport() {
121         }
122
123         @Override
124         protected boolean isModelEditable() {
125           return false;
126         }
127       }.initPosition()
128         .addExtraAction(AnActionButton.fromAction(sourceAction))
129         .createPanel();
130       fieldPanel.setBorder(IdeBorderFactory.createBorder(SideBorder.LEFT | SideBorder.TOP));
131
132       panel.add(fieldPanel, BorderLayout.CENTER);
133       panel.add(new JLabel("Merge Sources:"), BorderLayout.NORTH);
134       addComp = panel;
135     }
136     else {
137       columns = new ColumnInfo[]{new CopyFromColumnInfo()};
138       addComp = null;
139       listener = null;
140     }
141     return new VcsDependentHistoryComponents(columns, listener, addComp);
142   }
143
144   @Override
145   public FilePath getUsedFilePath(SvnHistorySession session) {
146     return session.getCommittedPath();
147   }
148
149   @Override
150   public Boolean getAddinionallyCachedData(SvnHistorySession session) {
151     return session.isHaveMergeSources();
152   }
153
154   @Override
155   public SvnHistorySession createFromCachedData(Boolean aBoolean,
156                                                @NotNull List<VcsFileRevision> revisions,
157                                                @NotNull FilePath filePath,
158                                                VcsRevisionNumber currentRevision) {
159     return new SvnHistorySession(myVcs, revisions, filePath, aBoolean, currentRevision, false, ! filePath.isNonLocal());
160   }
161
162   @Override
163   @Nullable
164   public VcsHistorySession createSessionFor(final FilePath filePath) throws VcsException {
165     final VcsAppendableHistoryPartnerAdapter adapter = new VcsAppendableHistoryPartnerAdapter();
166     reportAppendableHistory(filePath, adapter);
167     adapter.check();
168
169     return adapter.getSession();
170   }
171
172   @Override
173   public void reportAppendableHistory(FilePath path, final VcsAppendableHistorySessionPartner partner) throws VcsException {
174     // we need + 1 rows to be reported to further detect that number of rows exceeded the limit
175     reportAppendableHistory(path, partner, null, null, VcsConfiguration.getInstance(myVcs.getProject()).MAXIMUM_HISTORY_ROWS + 1, null, false);
176   }
177
178   public void reportAppendableHistory(FilePath path, final VcsAppendableHistorySessionPartner partner,
179                                       @Nullable final SVNRevision from, @Nullable final SVNRevision to, final int limit,
180                                       SVNRevision peg, final boolean forceBackwards) throws VcsException {
181     FilePath committedPath = path;
182     Change change = ChangeListManager.getInstance(myVcs.getProject()).getChange(path);
183     if (change != null) {
184       final ContentRevision beforeRevision = change.getBeforeRevision();
185       final ContentRevision afterRevision = change.getAfterRevision();
186       if (beforeRevision != null && afterRevision != null && !beforeRevision.getFile().equals(afterRevision.getFile()) &&
187           afterRevision.getFile().equals(path)) {
188         committedPath = beforeRevision.getFile();
189       }
190       // revision can be VcsRevisionNumber.NULL
191       if (peg == null && change.getBeforeRevision() != null && change.getBeforeRevision().getRevisionNumber() instanceof SvnRevisionNumber) {
192         peg = ((SvnRevisionNumber) change.getBeforeRevision().getRevisionNumber()).getRevision();
193       }
194     }
195
196     final boolean showMergeSources = SvnConfiguration.getInstance(myVcs.getProject()).isShowMergeSourcesInAnnotate();
197     final LogLoader logLoader;
198     if (path.isNonLocal()) {
199       logLoader = new RepositoryLoader(myVcs, committedPath, from, to, limit, peg, forceBackwards, showMergeSources);
200     }
201     else {
202       logLoader = new LocalLoader(myVcs, committedPath, from, to, limit, peg, showMergeSources);
203     }
204
205     try {
206       logLoader.preliminary();
207     }
208     catch (SVNCancelException e) {
209       throw new VcsException(e);
210     }
211     catch (SVNException e) {
212       throw new VcsException(e);
213     }
214     logLoader.check();
215     if (showMergeSources) {
216       logLoader.initSupports15();
217     }
218
219     final SvnHistorySession historySession =
220       new SvnHistorySession(myVcs, Collections.<VcsFileRevision>emptyList(), committedPath, showMergeSources && Boolean.TRUE.equals(logLoader.mySupport15), null, false,
221                             ! path.isNonLocal());
222
223     final Ref<Boolean> sessionReported = new Ref<Boolean>();
224     final ProgressIndicator indicator = ProgressManager.getInstance().getProgressIndicator();
225     if (indicator != null) {
226       indicator.setText(SvnBundle.message("progress.text2.collecting.history", path.getName()));
227     }
228     final Consumer<VcsFileRevision> consumer = new Consumer<VcsFileRevision>() {
229       @Override
230       public void consume(VcsFileRevision vcsFileRevision) {
231         if (!Boolean.TRUE.equals(sessionReported.get())) {
232           partner.reportCreatedEmptySession(historySession);
233           sessionReported.set(true);
234         }
235         partner.acceptRevision(vcsFileRevision);
236       }
237     };
238
239     logLoader.setConsumer(consumer);
240     logLoader.load();
241     logLoader.check();
242   }
243
244   private static abstract class LogLoader {
245     protected final boolean myShowMergeSources;
246     protected String myUrl;
247     protected boolean mySupport15;
248     protected final SvnVcs myVcs;
249     protected final FilePath myFile;
250     protected final SVNRevision myFrom;
251     protected final SVNRevision myTo;
252     protected final int myLimit;
253     protected final SVNRevision myPeg;
254     protected Consumer<VcsFileRevision> myConsumer;
255     protected final ProgressIndicator myPI;
256     protected VcsException myException;
257
258     protected LogLoader(SvnVcs vcs, FilePath file, SVNRevision from, SVNRevision to, int limit, SVNRevision peg, boolean showMergeSources) {
259       myVcs = vcs;
260       myFile = file;
261       myFrom = from;
262       myTo = to;
263       myLimit = limit;
264       myPeg = peg;
265       myPI = ProgressManager.getInstance().getProgressIndicator();
266       myShowMergeSources = showMergeSources;
267     }
268
269     public void setConsumer(Consumer<VcsFileRevision> consumer) {
270       myConsumer = consumer;
271     }
272
273     protected void initSupports15() {
274       assert myUrl != null;
275       mySupport15 = SvnUtil.checkRepositoryVersion15(myVcs, myUrl);
276     }
277
278     public void check() throws VcsException {
279       if (myException != null) throw myException;
280     }
281
282     protected abstract void preliminary() throws SVNException;
283
284     protected abstract void load();
285   }
286
287   private static class LocalLoader extends LogLoader {
288     private SVNInfo myInfo;
289
290     private LocalLoader(SvnVcs vcs, FilePath file, SVNRevision from, SVNRevision to, int limit, SVNRevision peg, boolean showMergeSources) {
291       super(vcs, file, from, to, limit, peg, showMergeSources);
292     }
293
294     @Override
295     protected void preliminary() throws SVNException {
296       myInfo = myVcs.getInfo(myFile.getIOFile());
297       if (myInfo == null || myInfo.getRepositoryRootURL() == null) {
298         myException = new VcsException("File " + myFile.getPath() + " is not under version control");
299         return;
300       }
301       if (myInfo.getURL() == null) {
302         myException = new VcsException("File " + myFile.getPath() + " is not under Subversion control");
303         return;
304       }
305       myUrl = myInfo.getURL().toDecodedString();
306     }
307
308     @Override
309     protected void load() {
310       String relativeUrl = myUrl;
311       final SVNURL repoRootURL = myInfo.getRepositoryRootURL();
312
313       final String root = repoRootURL.toString();
314       if (myUrl != null && myUrl.startsWith(root)) {
315         relativeUrl = myUrl.substring(root.length());
316       }
317       if (myPI != null) {
318         myPI.setText2(SvnBundle.message("progress.text2.changes.establishing.connection", myUrl));
319       }
320       final SVNRevision pegRevision = myInfo.getRevision();
321       final SvnTarget target = SvnTarget.fromFile(myFile.getIOFile(), myPeg);
322       try {
323         myVcs.getFactory(target).createHistoryClient().doLog(
324           target,
325           myFrom == null ? SVNRevision.HEAD : myFrom,
326           myTo == null ? SVNRevision.create(1) : myTo,
327           false, true, myShowMergeSources && mySupport15, myLimit + 1, null,
328           new MyLogEntryHandler(myVcs, myUrl, pegRevision, relativeUrl,
329                                 createConsumerAdapter(myConsumer),
330                                 repoRootURL, myFile.getCharset()));
331       }
332       catch (SVNCancelException e) {
333         //
334       }
335       catch (SVNException e) {
336         myException = new VcsException(e);
337       }
338       catch (VcsException e) {
339         myException = e;
340       }
341     }
342   }
343
344   private static ThrowableConsumer<VcsFileRevision, SVNException> createConsumerAdapter(final Consumer<VcsFileRevision> consumer) {
345     return new ThrowableConsumer<VcsFileRevision, SVNException>() {
346       @Override
347       public void consume(VcsFileRevision revision) throws SVNException {
348         consumer.consume(revision);
349       }
350     };
351   }
352
353   private static class RepositoryLoader extends LogLoader {
354     private final boolean myForceBackwards;
355
356     private RepositoryLoader(SvnVcs vcs,
357                              FilePath file,
358                              SVNRevision from,
359                              SVNRevision to,
360                              int limit,
361                              SVNRevision peg,
362                              boolean forceBackwards, boolean showMergeSources) {
363       super(vcs, file, from, to, limit, peg, showMergeSources);
364       myForceBackwards = forceBackwards;
365     }
366
367     @Override
368     protected void preliminary() throws SVNException {
369       myUrl = myFile.getPath().replace('\\', '/');
370     }
371
372     @Override
373     protected void load() {
374       if (myPI != null) {
375         myPI.setText2(SvnBundle.message("progress.text2.changes.establishing.connection", myUrl));
376       }
377
378       try {
379         if (myForceBackwards) {
380           SVNURL svnurl = SVNURL.parseURIEncoded(myUrl);
381           if (! existsNow(svnurl)) {
382             loadBackwards(svnurl);
383             return;
384           }
385         }
386
387         final SVNURL svnurl = SVNURL.parseURIEncoded(myUrl);
388         SVNRevision operationalFrom = myFrom == null ? SVNRevision.HEAD : myFrom;
389         final SVNURL rootURL = getRepositoryRoot(svnurl, myFrom);
390         if (rootURL == null) {
391           throw new VcsException("Could not find repository root for URL: " + myUrl);
392         }
393         final String root = rootURL.toString();
394         String relativeUrl = myUrl;
395         if (myUrl.startsWith(root)) {
396           relativeUrl = myUrl.substring(root.length());
397         }
398         SvnTarget target = SvnTarget.fromURL(svnurl, myPeg == null ? myFrom : myPeg);
399         RepositoryLogEntryHandler handler =
400           new RepositoryLogEntryHandler(myVcs, myUrl, SVNRevision.UNDEFINED, relativeUrl, createConsumerAdapter(myConsumer), rootURL);
401
402         myVcs.getFactory(target).createHistoryClient()
403           .doLog(target, operationalFrom, myTo == null ? SVNRevision.create(1) : myTo, false, true, myShowMergeSources && mySupport15,
404                  myLimit + 1, null, handler);
405       }
406       catch (SVNCancelException e) {
407         //
408       }
409       catch (SVNException e) {
410         myException = new VcsException(e);
411       }
412       catch (VcsException e) {
413         myException = e;
414       }
415     }
416
417     private void loadBackwards(SVNURL svnurl) throws SVNException, VcsException {
418         final SVNURL rootURL = getRepositoryRoot(svnurl, myFrom);
419         final String root = rootURL.toString();
420         String relativeUrl = myUrl;
421         if (myUrl.startsWith(root)) {
422           relativeUrl = myUrl.substring(root.length());
423         }
424
425       // TODO: Update this call to myVcs.getFactory.createHistoryClient
426         SVNLogClient client = myVcs.createLogClient();
427
428         final RepositoryLogEntryHandler repositoryLogEntryHandler =
429           new RepositoryLogEntryHandler(myVcs, myUrl, SVNRevision.UNDEFINED, relativeUrl,
430                                         new ThrowableConsumer<VcsFileRevision, SVNException>() {
431                                           @Override
432                                           public void consume(VcsFileRevision revision) throws SVNException {
433                                             myConsumer.consume(revision);
434                                             throw new SVNCancelException(); // load only one revision
435                                           }
436                                         }, rootURL);
437         repositoryLogEntryHandler.setThrowCancelOnMeetPathCreation(true);
438
439         client.doLog(rootURL, new String[]{}, myFrom, myFrom, myTo == null ? SVNRevision.create(1) : myTo, false, true, myShowMergeSources && mySupport15, 0, null, repositoryLogEntryHandler);
440     }
441
442     // TODO: try to rewrite without separately retrieving repository url by item url - as this command could require authentication
443     // TODO: and it is not "clear enough/easy to implement" with current design (for some cases) how to cache credentials (if in
444     // TODO: non-interactive mode)
445     private SVNURL getRepositoryRoot(SVNURL svnurl, SVNRevision operationalFrom) throws SVNException {
446       SVNInfo info = myVcs.getInfo(svnurl, SVNRevision.HEAD);
447
448       return info != null ? info.getRepositoryRootURL() : null;
449     }
450
451     private boolean existsNow(SVNURL svnurl) {
452       final SVNInfo info;
453       try {
454         info = myVcs.getInfo(svnurl, SVNRevision.HEAD, SVNRevision.HEAD);
455       }
456       catch (SVNException e) {
457         return false;
458       }
459       return info != null && info.getURL() != null && info.getRevision().isValid();
460     }
461   }
462
463   @Override
464   public String getHelpId() {
465     return null;
466   }
467
468   @Override
469   public AnAction[] getAdditionalActions(final Runnable refresher) {
470     return new AnAction[]{ ShowAllAffectedGenericAction.getInstance(), new MergeSourceDetailsAction(), new SvnEditCommitMessageFromFileHistoryAction()};
471   }
472
473   @Override
474   public boolean isDateOmittable() {
475     return false;
476   }
477
478   private static class MyLogEntryHandler implements ISVNLogEntryHandler {
479     private final ProgressIndicator myIndicator;
480     protected final SvnVcs myVcs;
481     protected final SvnPathThroughHistoryCorrection myLastPathCorrector;
482     private final Charset myCharset;
483     protected final ThrowableConsumer<VcsFileRevision, SVNException> myResult;
484     private final String myLastPath;
485     private VcsFileRevision myPrevious;
486     private final SVNRevision myPegRevision;
487     protected final String myUrl;
488     private final SvnMergeSourceTracker myTracker;
489     protected SVNURL myRepositoryRoot;
490     private boolean myThrowCancelOnMeetPathCreation;
491
492     public void setThrowCancelOnMeetPathCreation(boolean throwCancelOnMeetPathCreation) {
493       myThrowCancelOnMeetPathCreation = throwCancelOnMeetPathCreation;
494     }
495
496     public MyLogEntryHandler(SvnVcs vcs, final String url,
497                              final SVNRevision pegRevision,
498                              String lastPath,
499                              final ThrowableConsumer<VcsFileRevision, SVNException> result,
500                              SVNURL repoRootURL, Charset charset)
501       throws SVNException, VcsException {
502       myVcs = vcs;
503       myLastPathCorrector = new SvnPathThroughHistoryCorrection(lastPath);
504       myLastPath = lastPath;
505       myCharset = charset;
506       myIndicator = ProgressManager.getInstance().getProgressIndicator();
507       myResult = result;
508       myPegRevision = pegRevision;
509       myUrl = url;
510       myRepositoryRoot = repoRootURL;
511       myTracker = new SvnMergeSourceTracker(new ThrowableConsumer<Pair<SVNLogEntry, Integer>, SVNException>() {
512         @Override
513         public void consume(final Pair<SVNLogEntry, Integer> svnLogEntryIntegerPair) throws SVNException {
514           final SVNLogEntry logEntry = svnLogEntryIntegerPair.getFirst();
515
516           if (myIndicator != null) {
517             if (myIndicator.isCanceled()) {
518               SVNErrorManager.cancel(SvnBundle.message("exception.text.update.operation.cancelled"), SVNLogType.DEFAULT);
519             }
520             myIndicator.setText2(SvnBundle.message("progress.text2.revision.processed", logEntry.getRevision()));
521           }
522           SVNLogEntryPath entryPath = null;
523           String copyPath = null;
524           final int mergeLevel = svnLogEntryIntegerPair.getSecond();
525
526           if (! myLastPathCorrector.isRoot()) {
527             myLastPathCorrector.handleLogEntry(logEntry);
528             entryPath = myLastPathCorrector.getDirectlyMentioned();
529             copyPath = null;
530             if (entryPath != null) {
531               copyPath = entryPath.getCopyPath();
532             } else {
533               // if there are no path with exact match, check whether parent or child paths had changed
534               // "entry path" is allowed to be null now; if it is null, last path would be taken for revision construction
535
536               // Separate SVNLogEntry is issued for each "merge source" revision. These "merge source" revisions are treated as child
537               // revisions of some other revision - this way we construct merge hierarchy.
538               // mergeLevel >= 0 indicates that we are currently processing some "merge source" revision. This "merge source" revision
539               // contains changes from some other branch - so checkForChildChanges() and checkForParentChanges() return "false".
540               // Because of this case we apply these methods only for non-"merge source" revisions - this means mergeLevel < 0.
541               // TODO: Do not apply path filtering even for log entries on the first level => just output of 'svn log' should be returned.
542               // TODO: Looks like there is no cases when we issue 'svn log' for some parent paths or some other cases where we need such
543               // TODO: filtering. Check user feedback on this.
544 //              if (mergeLevel < 0 && !checkForChildChanges(logEntry) && !checkForParentChanges(logEntry)) return;
545             }
546           }
547
548           final SvnFileRevision revision = createRevision(logEntry, copyPath, entryPath);
549           if (mergeLevel >= 0) {
550             addToListByLevel((SvnFileRevision)myPrevious, revision, mergeLevel);
551           }
552           else {
553             myResult.consume(revision);
554             myPrevious = revision;
555           }
556           if (myThrowCancelOnMeetPathCreation && myUrl.equals(revision.getURL()) && entryPath != null && entryPath.getType() == 'A') {
557             throw new SVNCancelException();
558           }
559         }
560
561       });
562     }
563
564     private boolean checkForParentChanges(SVNLogEntry logEntry) {
565       final String lastPathBefore = myLastPathCorrector.getBefore();
566       String path = SVNPathUtil.removeTail(lastPathBefore);
567       while (path.length() > 0) {
568         final SVNLogEntryPath entryPath = logEntry.getChangedPaths().get(path);
569         // A & D are checked since we are not interested in parent folders property changes, only in structure changes
570         // TODO: seems that R (replaced) should also be checked here
571         if (entryPath != null && (entryPath.getType() == 'A' || entryPath.getType() == 'D')) {
572           if (entryPath.getCopyPath() != null) {
573             return true;
574           }
575           break;
576         }
577         path = SVNPathUtil.removeTail(path);
578       }
579       return false;
580     }
581
582     // TODO: this makes sense only for directories, but should always return true if something under the directory was changed in revision
583     // TODO: as svn will provide child changes in history for directory
584     private boolean checkForChildChanges(SVNLogEntry logEntry) {
585       final String lastPathBefore = myLastPathCorrector.getBefore();
586       for (String key : logEntry.getChangedPaths().keySet()) {
587         if (SVNPathUtil.isAncestor(lastPathBefore, key)) {
588           return true;
589         }
590       }
591       return false;
592     }
593
594     @Override
595     public void handleLogEntry(SVNLogEntry logEntry) throws SVNException {
596       myTracker.consume(logEntry);
597     }
598
599     private static void addToListByLevel(final SvnFileRevision revision, final SvnFileRevision revisionToAdd, final int level) {
600       if (level < 0) {
601         return;
602       }
603       if (level == 0) {
604         revision.addMergeSource(revisionToAdd);
605         return;
606       }
607       final List<SvnFileRevision> sources = revision.getMergeSources();
608       if (!sources.isEmpty()) {
609         addToListByLevel(sources.get(sources.size() - 1), revisionToAdd, level - 1);
610       }
611     }
612
613     protected SvnFileRevision createRevision(final SVNLogEntry logEntry, final String copyPath, SVNLogEntryPath entryPath) throws SVNException {
614       Date date = logEntry.getDate();
615       String author = logEntry.getAuthor();
616       String message = logEntry.getMessage();
617       SVNRevision rev = SVNRevision.create(logEntry.getRevision());
618       final SVNURL url = myRepositoryRoot.appendPath(myLastPath, true);
619 //      final SVNURL url = entryPath != null ? myRepositoryRoot.appendPath(entryPath.getPath(), true) :
620 //                         myRepositoryRoot.appendPath(myLastPathCorrector.getBefore(), false);
621       return new SvnFileRevision(myVcs, myPegRevision, rev, url.toString(), author, date, message, copyPath, myCharset);
622     }
623   }
624
625   private static class RepositoryLogEntryHandler extends MyLogEntryHandler {
626     public RepositoryLogEntryHandler(final SvnVcs vcs, final String url,
627                                      final SVNRevision pegRevision,
628                                      String lastPath,
629                                      final ThrowableConsumer<VcsFileRevision, SVNException> result,
630                                      SVNURL repoRootURL)
631       throws VcsException, SVNException {
632       super(vcs, url, pegRevision, lastPath, result, repoRootURL, null);
633     }
634
635     @Override
636     protected SvnFileRevision createRevision(final SVNLogEntry logEntry, final String copyPath, SVNLogEntryPath entryPath)
637       throws SVNException {
638       final SVNURL url = entryPath == null ? myRepositoryRoot.appendPath(myLastPathCorrector.getBefore(), false) :
639                          myRepositoryRoot.appendPath(entryPath.getPath(), true);
640       return new SvnFileRevision(myVcs, SVNRevision.UNDEFINED, logEntry, url.toString(), copyPath, null);
641     }
642   }
643
644   private static class RevisionMergeSourceInfo {
645
646     @NotNull private final VcsFileRevision revision;
647
648     private RevisionMergeSourceInfo(@NotNull VcsFileRevision revision) {
649       this.revision = revision;
650     }
651
652     @NotNull
653     public SvnFileRevision getRevision() {
654       return (SvnFileRevision)revision;
655     }
656
657     // will be used, for instance, while copying (to clipboard) data from table
658     @Override
659     public String toString() {
660       return toString(revision);
661     }
662
663     private static String toString(@Nullable VcsFileRevision value) {
664       if (!(value instanceof SvnFileRevision)) return "";
665       final SvnFileRevision revision = (SvnFileRevision)value;
666       final List<SvnFileRevision> mergeSources = revision.getMergeSources();
667       if (mergeSources.isEmpty()) {
668         return "";
669       }
670       final StringBuilder sb = new StringBuilder();
671       for (SvnFileRevision source : mergeSources) {
672         if (sb.length() != 0) {
673           sb.append(", ");
674         }
675         sb.append(source.getRevisionNumber().asString());
676         if (!source.getMergeSources().isEmpty()) {
677           sb.append("*");
678         }
679       }
680       return sb.toString();
681     }
682   }
683
684   private class MergeSourceColumnInfo extends ColumnInfo<VcsFileRevision, RevisionMergeSourceInfo> {
685     private final MergeSourceRenderer myRenderer;
686
687     private MergeSourceColumnInfo(final SvnHistorySession session) {
688       super("Merge Sources");
689       myRenderer = new MergeSourceRenderer(session);
690     }
691
692     @Override
693     public TableCellRenderer getRenderer(final VcsFileRevision vcsFileRevision) {
694       return myRenderer;
695     }
696
697     @Override
698     public RevisionMergeSourceInfo valueOf(final VcsFileRevision vcsFileRevision) {
699       return vcsFileRevision != null ? new RevisionMergeSourceInfo(vcsFileRevision) : null;
700     }
701
702     public String getText(final VcsFileRevision vcsFileRevision) {
703       return myRenderer.getText(vcsFileRevision);
704     }
705
706     @Override
707     public int getAdditionalWidth() {
708       return 20;
709     }
710
711     @Override
712     public String getPreferredStringValue() {
713       return "1234567, 1234567, 1234567";
714     }
715   }
716
717   private static final Object MERGE_SOURCE_DETAILS_TAG = new Object();
718
719   private class MergeSourceDetailsLinkListener extends TableLinkMouseListener {
720     private final VirtualFile myFile;
721     private final Object myTag;
722
723     private MergeSourceDetailsLinkListener(final Object tag, final VirtualFile file) {
724       myTag = tag;
725       myFile = file;
726     }
727
728     @Override
729     public boolean onClick(@NotNull MouseEvent e, int clickCount) {
730       if (e.getButton() == 1 && !e.isPopupTrigger()) {
731         Object tag = getTagAt(e);
732         if (tag == myTag) {
733           final SvnFileRevision revision = getSelectedRevision(e);
734           if (revision != null) {
735             SvnMergeSourceDetails.showMe(myVcs.getProject(), revision, myFile);
736             return true;
737           }
738         }
739       }
740       return false;
741     }
742
743     @Nullable
744     private SvnFileRevision getSelectedRevision(final MouseEvent e) {
745       JTable table = (JTable)e.getSource();
746       int row = table.rowAtPoint(e.getPoint());
747       int column = table.columnAtPoint(e.getPoint());
748
749       final Object value = table.getModel().getValueAt(row, column);
750       if (value instanceof RevisionMergeSourceInfo) {
751         return ((RevisionMergeSourceInfo)value).getRevision();
752       }
753       return null;
754     }
755
756     @Override
757     public void mouseMoved(MouseEvent e) {
758       JTable table = (JTable)e.getSource();
759       Object tag = getTagAt(e);
760       if (tag == myTag) {
761         table.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
762       }
763       else {
764         table.setCursor(Cursor.getDefaultCursor());
765       }
766     }
767   }
768
769   private class MergeSourceRenderer extends ColoredTableCellRenderer {
770     private MergeSourceDetailsLinkListener myListener;
771     private final VirtualFile myFile;
772
773     private MergeSourceRenderer(final SvnHistorySession session) {
774       myFile = session.getCommittedPath().getVirtualFile();
775     }
776
777     public String getText(final VcsFileRevision value) {
778       return RevisionMergeSourceInfo.toString(value);
779     }
780
781     @Override
782     protected void customizeCellRenderer(final JTable table,
783                                          final Object value,
784                                          final boolean selected,
785                                          final boolean hasFocus,
786                                          final int row,
787                                          final int column) {
788       if (myListener == null) {
789         myListener = new MergeSourceDetailsLinkListener(MERGE_SOURCE_DETAILS_TAG, myFile);
790         myListener.installOn(table);
791       }
792       appendMergeSourceText(table, row, column, value instanceof RevisionMergeSourceInfo ? value.toString() : null);
793     }
794
795     private void appendMergeSourceText(JTable table, int row, int column, @Nullable String text) {
796       if (StringUtil.isEmpty(text)) {
797         append("", SimpleTextAttributes.REGULAR_ATTRIBUTES);
798       }
799       else {
800         append(cutString(text, table.getCellRect(row, column, false).getWidth()), SimpleTextAttributes.REGULAR_ATTRIBUTES,
801                MERGE_SOURCE_DETAILS_TAG);
802       }
803     }
804
805     private String cutString(final String text, final double value) {
806       final FontMetrics m = getFontMetrics(getFont());
807       final Graphics g = getGraphics();
808
809       if (m.getStringBounds(text, g).getWidth() < value) return text;
810
811       final String dots = "...";
812       final double dotsWidth = m.getStringBounds(dots, g).getWidth();
813       if (dotsWidth >= value) {
814         return dots;
815       }
816
817       for (int i = 1; i < text.length(); i++) {
818         if ((m.getStringBounds(text, 0, i, g).getWidth() + dotsWidth) >= value) {
819           if (i < 2) return dots;
820           return text.substring(0, i - 1) + dots;
821         }
822       }
823       return text;
824     }
825   }
826
827   private static class CopyFromColumnInfo extends ColumnInfo<VcsFileRevision, String> {
828     private final Icon myIcon = PlatformIcons.COPY_ICON;
829     private final ColoredTableCellRenderer myRenderer = new ColoredTableCellRenderer() {
830       @Override
831       protected void customizeCellRenderer(final JTable table,
832                                            final Object value,
833                                            final boolean selected,
834                                            final boolean hasFocus,
835                                            final int row,
836                                            final int column) {
837         if (value instanceof String && ((String)value).length() > 0) {
838           setIcon(myIcon);
839           setToolTipText(SvnBundle.message("copy.column.tooltip", value));
840         }
841         else {
842           setToolTipText("");
843         }
844       }
845     };
846
847     public CopyFromColumnInfo() {
848       super(SvnBundle.message("copy.column.title"));
849     }
850
851     @Override
852     public String valueOf(final VcsFileRevision o) {
853       return o instanceof SvnFileRevision ? ((SvnFileRevision)o).getCopyFromPath() : "";
854     }
855
856     @Override
857     public TableCellRenderer getRenderer(final VcsFileRevision vcsFileRevision) {
858       return myRenderer;
859     }
860
861     @Override
862     public String getMaxStringValue() {
863       return SvnBundle.message("copy.column.title");
864     }
865
866     @Override
867     public int getAdditionalWidth() {
868       return 6;
869     }
870   }
871 }