2 * Copyright 2000-2009 JetBrains s.r.o.
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
16 package org.jetbrains.idea.svn.history;
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.SVNRevision;
48 import org.tmatesoft.svn.core.wc2.SvnTarget;
49 import org.tmatesoft.svn.util.SVNLogType;
52 import javax.swing.table.TableCellRenderer;
54 import java.awt.event.MouseEvent;
55 import java.nio.charset.Charset;
56 import java.util.Collections;
57 import java.util.Date;
58 import java.util.List;
60 public class SvnHistoryProvider
61 implements VcsHistoryProvider, VcsCacheableHistorySessionFactory<Boolean, SvnHistorySession> {
62 private final SvnVcs myVcs;
64 public SvnHistoryProvider(SvnVcs vcs) {
69 public boolean supportsHistoryForDirectories() {
74 public DiffFromHistoryHandler getHistoryDiffHandler() {
79 public boolean canShowHistoryFor(@NotNull VirtualFile file) {
84 public VcsDependentHistoryComponents getUICustomization(final VcsHistorySession session, JComponent forShortcutRegistration) {
85 final ColumnInfo[] columns;
86 final Consumer<VcsFileRevision> listener;
87 final JComponent addComp;
88 if (((SvnHistorySession)session).isHaveMergeSources()) {
89 final MergeSourceColumnInfo mergeSourceColumn = new MergeSourceColumnInfo((SvnHistorySession)session);
90 columns = new ColumnInfo[]{new CopyFromColumnInfo(), mergeSourceColumn};
92 final JPanel panel = new JPanel(new BorderLayout());
94 final JTextArea field = new JTextArea();
95 field.setEditable(false);
96 field.setBackground(UIUtil.getComboBoxDisabledBackground());
97 field.setWrapStyleWord(true);
98 listener = new Consumer<VcsFileRevision>() {
100 public void consume(VcsFileRevision vcsFileRevision) {
101 field.setText(mergeSourceColumn.getText(vcsFileRevision));
105 final MergeSourceDetailsAction sourceAction = new MergeSourceDetailsAction();
106 sourceAction.registerSelf(forShortcutRegistration);
108 JPanel fieldPanel = new ToolbarDecorator() {
110 protected JComponent getComponent() {
115 protected void updateButtons() {
119 protected void installDnDSupport() {
123 protected boolean isModelEditable() {
127 .addExtraAction(AnActionButton.fromAction(sourceAction))
129 fieldPanel.setBorder(IdeBorderFactory.createBorder(SideBorder.LEFT | SideBorder.TOP));
131 panel.add(fieldPanel, BorderLayout.CENTER);
132 panel.add(new JLabel("Merge Sources:"), BorderLayout.NORTH);
136 columns = new ColumnInfo[]{new CopyFromColumnInfo()};
140 return new VcsDependentHistoryComponents(columns, listener, addComp);
144 public FilePath getUsedFilePath(SvnHistorySession session) {
145 return session.getCommittedPath();
149 public Boolean getAddinionallyCachedData(SvnHistorySession session) {
150 return session.isHaveMergeSources();
154 public SvnHistorySession createFromCachedData(Boolean aBoolean,
155 @NotNull List<VcsFileRevision> revisions,
156 @NotNull FilePath filePath,
157 VcsRevisionNumber currentRevision) {
158 return new SvnHistorySession(myVcs, revisions, filePath, aBoolean, currentRevision, false, ! filePath.isNonLocal());
163 public VcsHistorySession createSessionFor(final FilePath filePath) throws VcsException {
164 final VcsAppendableHistoryPartnerAdapter adapter = new VcsAppendableHistoryPartnerAdapter();
165 reportAppendableHistory(filePath, adapter);
168 return adapter.getSession();
172 public void reportAppendableHistory(FilePath path, final VcsAppendableHistorySessionPartner partner) throws VcsException {
173 // we need + 1 rows to be reported to further detect that number of rows exceeded the limit
174 reportAppendableHistory(path, partner, null, null, VcsConfiguration.getInstance(myVcs.getProject()).MAXIMUM_HISTORY_ROWS + 1, null, false);
177 public void reportAppendableHistory(FilePath path, final VcsAppendableHistorySessionPartner partner,
178 @Nullable final SVNRevision from, @Nullable final SVNRevision to, final int limit,
179 SVNRevision peg, final boolean forceBackwards) throws VcsException {
180 FilePath committedPath = path;
181 Change change = ChangeListManager.getInstance(myVcs.getProject()).getChange(path);
182 if (change != null) {
183 final ContentRevision beforeRevision = change.getBeforeRevision();
184 final ContentRevision afterRevision = change.getAfterRevision();
185 if (beforeRevision != null && afterRevision != null && !beforeRevision.getFile().equals(afterRevision.getFile()) &&
186 afterRevision.getFile().equals(path)) {
187 committedPath = beforeRevision.getFile();
189 // revision can be VcsRevisionNumber.NULL
190 if (peg == null && change.getBeforeRevision() != null && change.getBeforeRevision().getRevisionNumber() instanceof SvnRevisionNumber) {
191 peg = ((SvnRevisionNumber) change.getBeforeRevision().getRevisionNumber()).getRevision();
195 final boolean showMergeSources = SvnConfiguration.getInstance(myVcs.getProject()).isShowMergeSourcesInAnnotate();
196 final LogLoader logLoader;
197 if (path.isNonLocal()) {
198 logLoader = new RepositoryLoader(myVcs, committedPath, from, to, limit, peg, forceBackwards, showMergeSources);
201 logLoader = new LocalLoader(myVcs, committedPath, from, to, limit, peg, showMergeSources);
205 logLoader.preliminary();
207 catch (SVNCancelException e) {
208 throw new VcsException(e);
210 catch (SVNException e) {
211 throw new VcsException(e);
214 if (showMergeSources) {
215 logLoader.initSupports15();
218 final SvnHistorySession historySession =
219 new SvnHistorySession(myVcs, Collections.<VcsFileRevision>emptyList(), committedPath, showMergeSources && Boolean.TRUE.equals(logLoader.mySupport15), null, false,
220 ! path.isNonLocal());
222 final Ref<Boolean> sessionReported = new Ref<Boolean>();
223 final ProgressIndicator indicator = ProgressManager.getInstance().getProgressIndicator();
224 if (indicator != null) {
225 indicator.setText(SvnBundle.message("progress.text2.collecting.history", path.getName()));
227 final Consumer<VcsFileRevision> consumer = new Consumer<VcsFileRevision>() {
229 public void consume(VcsFileRevision vcsFileRevision) {
230 if (!Boolean.TRUE.equals(sessionReported.get())) {
231 partner.reportCreatedEmptySession(historySession);
232 sessionReported.set(true);
234 partner.acceptRevision(vcsFileRevision);
238 logLoader.setConsumer(consumer);
243 private static abstract class LogLoader {
244 protected final boolean myShowMergeSources;
245 protected String myUrl;
246 protected boolean mySupport15;
247 protected final SvnVcs myVcs;
248 protected final FilePath myFile;
249 protected final SVNRevision myFrom;
250 protected final SVNRevision myTo;
251 protected final int myLimit;
252 protected final SVNRevision myPeg;
253 protected Consumer<VcsFileRevision> myConsumer;
254 protected final ProgressIndicator myPI;
255 protected VcsException myException;
257 protected LogLoader(SvnVcs vcs, FilePath file, SVNRevision from, SVNRevision to, int limit, SVNRevision peg, boolean showMergeSources) {
264 myPI = ProgressManager.getInstance().getProgressIndicator();
265 myShowMergeSources = showMergeSources;
268 public void setConsumer(Consumer<VcsFileRevision> consumer) {
269 myConsumer = consumer;
272 protected void initSupports15() {
273 assert myUrl != null;
274 mySupport15 = SvnUtil.checkRepositoryVersion15(myVcs, myUrl);
277 public void check() throws VcsException {
278 if (myException != null) throw myException;
281 protected abstract void preliminary() throws SVNException;
283 protected abstract void load();
286 private static class LocalLoader extends LogLoader {
287 private SVNInfo myInfo;
289 private LocalLoader(SvnVcs vcs, FilePath file, SVNRevision from, SVNRevision to, int limit, SVNRevision peg, boolean showMergeSources) {
290 super(vcs, file, from, to, limit, peg, showMergeSources);
294 protected void preliminary() throws SVNException {
295 myInfo = myVcs.getInfo(myFile.getIOFile());
296 if (myInfo == null || myInfo.getRepositoryRootURL() == null) {
297 myException = new VcsException("File " + myFile.getPath() + " is not under version control");
300 if (myInfo.getURL() == null) {
301 myException = new VcsException("File " + myFile.getPath() + " is not under Subversion control");
304 myUrl = myInfo.getURL().toDecodedString();
308 protected void load() {
309 String relativeUrl = myUrl;
310 final SVNURL repoRootURL = myInfo.getRepositoryRootURL();
312 final String root = repoRootURL.toString();
313 if (myUrl != null && myUrl.startsWith(root)) {
314 relativeUrl = myUrl.substring(root.length());
317 myPI.setText2(SvnBundle.message("progress.text2.changes.establishing.connection", myUrl));
319 final SVNRevision pegRevision = myInfo.getRevision();
320 final SvnTarget target = SvnTarget.fromFile(myFile.getIOFile(), myPeg);
322 myVcs.getFactory(target).createHistoryClient().doLog(
324 myFrom == null ? SVNRevision.HEAD : myFrom,
325 myTo == null ? SVNRevision.create(1) : myTo,
326 false, true, myShowMergeSources && mySupport15, myLimit + 1, null,
327 new MyLogEntryHandler(myVcs, myUrl, pegRevision, relativeUrl,
328 createConsumerAdapter(myConsumer),
329 repoRootURL, myFile.getCharset()));
331 catch (SVNCancelException e) {
334 catch (SVNException e) {
335 myException = new VcsException(e);
337 catch (VcsException e) {
343 private static ThrowableConsumer<VcsFileRevision, SVNException> createConsumerAdapter(final Consumer<VcsFileRevision> consumer) {
344 return new ThrowableConsumer<VcsFileRevision, SVNException>() {
346 public void consume(VcsFileRevision revision) throws SVNException {
347 consumer.consume(revision);
352 private static class RepositoryLoader extends LogLoader {
353 private final boolean myForceBackwards;
355 private RepositoryLoader(SvnVcs vcs,
361 boolean forceBackwards, boolean showMergeSources) {
362 super(vcs, file, from, to, limit, peg, showMergeSources);
363 myForceBackwards = forceBackwards;
367 protected void preliminary() throws SVNException {
368 myUrl = myFile.getPath().replace('\\', '/');
372 protected void load() {
374 myPI.setText2(SvnBundle.message("progress.text2.changes.establishing.connection", myUrl));
378 if (myForceBackwards) {
379 SVNURL svnurl = SVNURL.parseURIEncoded(myUrl);
380 if (! existsNow(svnurl)) {
381 loadBackwards(svnurl);
386 final SVNURL svnurl = SVNURL.parseURIEncoded(myUrl);
387 SVNRevision operationalFrom = myFrom == null ? SVNRevision.HEAD : myFrom;
388 // TODO: try to rewrite without separately retrieving repository url by item url - as this command could require authentication
389 // TODO: and it is not "clear enough/easy to implement" with current design (for some cases) how to cache credentials (if in
390 // TODO: non-interactive mode)
391 final SVNURL rootURL = SvnUtil.getRepositoryRoot(myVcs, svnurl);
392 if (rootURL == null) {
393 throw new VcsException("Could not find repository root for URL: " + myUrl);
395 final String root = rootURL.toString();
396 String relativeUrl = myUrl;
397 if (myUrl.startsWith(root)) {
398 relativeUrl = myUrl.substring(root.length());
400 SvnTarget target = SvnTarget.fromURL(svnurl, myPeg == null ? myFrom : myPeg);
401 RepositoryLogEntryHandler handler =
402 new RepositoryLogEntryHandler(myVcs, myUrl, SVNRevision.UNDEFINED, relativeUrl, createConsumerAdapter(myConsumer), rootURL);
404 myVcs.getFactory(target).createHistoryClient()
405 .doLog(target, operationalFrom, myTo == null ? SVNRevision.create(1) : myTo, false, true, myShowMergeSources && mySupport15,
406 myLimit + 1, null, handler);
408 catch (SVNCancelException e) {
411 catch (SVNException e) {
412 myException = new VcsException(e);
414 catch (VcsException e) {
419 private void loadBackwards(SVNURL svnurl) throws SVNException, VcsException {
420 // this method is called when svnurl does not exist in latest repository revision - thus concrete old revision is used for "info"
421 // command to get repository url
422 SVNInfo info = myVcs.getInfo(svnurl, myPeg, myPeg);
423 final SVNURL rootURL = info != null ? info.getRepositoryRootURL() : null;
424 final String root = rootURL != null ? rootURL.toString() : "";
425 String relativeUrl = myUrl;
426 if (myUrl.startsWith(root)) {
427 relativeUrl = myUrl.substring(root.length());
430 final RepositoryLogEntryHandler repositoryLogEntryHandler =
431 new RepositoryLogEntryHandler(myVcs, myUrl, SVNRevision.UNDEFINED, relativeUrl,
432 new ThrowableConsumer<VcsFileRevision, SVNException>() {
434 public void consume(VcsFileRevision revision) throws SVNException {
435 myConsumer.consume(revision);
438 repositoryLogEntryHandler.setThrowCancelOnMeetPathCreation(true);
440 SvnTarget target = SvnTarget.fromURL(rootURL, myFrom);
441 myVcs.getFactory(target).createHistoryClient()
442 .doLog(target, myFrom, myTo == null ? SVNRevision.create(1) : myTo, false, true, myShowMergeSources && mySupport15, 1, null,
443 repositoryLogEntryHandler);
446 private boolean existsNow(SVNURL svnurl) {
449 info = myVcs.getInfo(svnurl, SVNRevision.HEAD, SVNRevision.HEAD);
451 catch (SVNException e) {
454 return info != null && info.getURL() != null && info.getRevision().isValid();
459 public String getHelpId() {
464 public AnAction[] getAdditionalActions(final Runnable refresher) {
465 return new AnAction[]{ ShowAllAffectedGenericAction.getInstance(), new MergeSourceDetailsAction(), new SvnEditCommitMessageFromFileHistoryAction()};
469 public boolean isDateOmittable() {
473 private static class MyLogEntryHandler implements ISVNLogEntryHandler {
474 private final ProgressIndicator myIndicator;
475 protected final SvnVcs myVcs;
476 protected final SvnPathThroughHistoryCorrection myLastPathCorrector;
477 private final Charset myCharset;
478 protected final ThrowableConsumer<VcsFileRevision, SVNException> myResult;
479 private final String myLastPath;
480 private VcsFileRevision myPrevious;
481 private final SVNRevision myPegRevision;
482 protected final String myUrl;
483 private final SvnMergeSourceTracker myTracker;
484 protected SVNURL myRepositoryRoot;
485 private boolean myThrowCancelOnMeetPathCreation;
487 public void setThrowCancelOnMeetPathCreation(boolean throwCancelOnMeetPathCreation) {
488 myThrowCancelOnMeetPathCreation = throwCancelOnMeetPathCreation;
491 public MyLogEntryHandler(SvnVcs vcs, final String url,
492 final SVNRevision pegRevision,
494 final ThrowableConsumer<VcsFileRevision, SVNException> result,
495 SVNURL repoRootURL, Charset charset)
496 throws SVNException, VcsException {
498 myLastPathCorrector = new SvnPathThroughHistoryCorrection(lastPath);
499 myLastPath = lastPath;
501 myIndicator = ProgressManager.getInstance().getProgressIndicator();
503 myPegRevision = pegRevision;
505 myRepositoryRoot = repoRootURL;
506 myTracker = new SvnMergeSourceTracker(new ThrowableConsumer<Pair<SVNLogEntry, Integer>, SVNException>() {
508 public void consume(final Pair<SVNLogEntry, Integer> svnLogEntryIntegerPair) throws SVNException {
509 final SVNLogEntry logEntry = svnLogEntryIntegerPair.getFirst();
511 if (myIndicator != null) {
512 if (myIndicator.isCanceled()) {
513 SVNErrorManager.cancel(SvnBundle.message("exception.text.update.operation.cancelled"), SVNLogType.DEFAULT);
515 myIndicator.setText2(SvnBundle.message("progress.text2.revision.processed", logEntry.getRevision()));
517 SVNLogEntryPath entryPath = null;
518 String copyPath = null;
519 final int mergeLevel = svnLogEntryIntegerPair.getSecond();
521 if (! myLastPathCorrector.isRoot()) {
522 myLastPathCorrector.handleLogEntry(logEntry);
523 entryPath = myLastPathCorrector.getDirectlyMentioned();
525 if (entryPath != null) {
526 copyPath = entryPath.getCopyPath();
528 // if there are no path with exact match, check whether parent or child paths had changed
529 // "entry path" is allowed to be null now; if it is null, last path would be taken for revision construction
531 // Separate SVNLogEntry is issued for each "merge source" revision. These "merge source" revisions are treated as child
532 // revisions of some other revision - this way we construct merge hierarchy.
533 // mergeLevel >= 0 indicates that we are currently processing some "merge source" revision. This "merge source" revision
534 // contains changes from some other branch - so checkForChildChanges() and checkForParentChanges() return "false".
535 // Because of this case we apply these methods only for non-"merge source" revisions - this means mergeLevel < 0.
536 // TODO: Do not apply path filtering even for log entries on the first level => just output of 'svn log' should be returned.
537 // TODO: Looks like there is no cases when we issue 'svn log' for some parent paths or some other cases where we need such
538 // TODO: filtering. Check user feedback on this.
539 // if (mergeLevel < 0 && !checkForChildChanges(logEntry) && !checkForParentChanges(logEntry)) return;
543 final SvnFileRevision revision = createRevision(logEntry, copyPath, entryPath);
544 if (mergeLevel >= 0) {
545 addToListByLevel((SvnFileRevision)myPrevious, revision, mergeLevel);
548 myResult.consume(revision);
549 myPrevious = revision;
551 if (myThrowCancelOnMeetPathCreation && myUrl.equals(revision.getURL()) && entryPath != null && entryPath.getType() == 'A') {
552 throw new SVNCancelException();
559 private boolean checkForParentChanges(SVNLogEntry logEntry) {
560 final String lastPathBefore = myLastPathCorrector.getBefore();
561 String path = SVNPathUtil.removeTail(lastPathBefore);
562 while (path.length() > 0) {
563 final SVNLogEntryPath entryPath = logEntry.getChangedPaths().get(path);
564 // A & D are checked since we are not interested in parent folders property changes, only in structure changes
565 // TODO: seems that R (replaced) should also be checked here
566 if (entryPath != null && (entryPath.getType() == 'A' || entryPath.getType() == 'D')) {
567 if (entryPath.getCopyPath() != null) {
572 path = SVNPathUtil.removeTail(path);
577 // TODO: this makes sense only for directories, but should always return true if something under the directory was changed in revision
578 // TODO: as svn will provide child changes in history for directory
579 private boolean checkForChildChanges(SVNLogEntry logEntry) {
580 final String lastPathBefore = myLastPathCorrector.getBefore();
581 for (String key : logEntry.getChangedPaths().keySet()) {
582 if (SVNPathUtil.isAncestor(lastPathBefore, key)) {
590 public void handleLogEntry(SVNLogEntry logEntry) throws SVNException {
591 myTracker.consume(logEntry);
594 private static void addToListByLevel(final SvnFileRevision revision, final SvnFileRevision revisionToAdd, final int level) {
599 revision.addMergeSource(revisionToAdd);
602 final List<SvnFileRevision> sources = revision.getMergeSources();
603 if (!sources.isEmpty()) {
604 addToListByLevel(sources.get(sources.size() - 1), revisionToAdd, level - 1);
608 protected SvnFileRevision createRevision(final SVNLogEntry logEntry, final String copyPath, SVNLogEntryPath entryPath) throws SVNException {
609 Date date = logEntry.getDate();
610 String author = logEntry.getAuthor();
611 String message = logEntry.getMessage();
612 SVNRevision rev = SVNRevision.create(logEntry.getRevision());
613 final SVNURL url = myRepositoryRoot.appendPath(myLastPath, true);
614 // final SVNURL url = entryPath != null ? myRepositoryRoot.appendPath(entryPath.getPath(), true) :
615 // myRepositoryRoot.appendPath(myLastPathCorrector.getBefore(), false);
616 return new SvnFileRevision(myVcs, myPegRevision, rev, url.toString(), author, date, message, copyPath, myCharset);
620 private static class RepositoryLogEntryHandler extends MyLogEntryHandler {
621 public RepositoryLogEntryHandler(final SvnVcs vcs, final String url,
622 final SVNRevision pegRevision,
624 final ThrowableConsumer<VcsFileRevision, SVNException> result,
626 throws VcsException, SVNException {
627 super(vcs, url, pegRevision, lastPath, result, repoRootURL, null);
631 protected SvnFileRevision createRevision(final SVNLogEntry logEntry, final String copyPath, SVNLogEntryPath entryPath)
632 throws SVNException {
633 final SVNURL url = entryPath == null ? myRepositoryRoot.appendPath(myLastPathCorrector.getBefore(), false) :
634 myRepositoryRoot.appendPath(entryPath.getPath(), true);
635 return new SvnFileRevision(myVcs, SVNRevision.UNDEFINED, logEntry, url.toString(), copyPath, null);
639 private static class RevisionMergeSourceInfo {
641 @NotNull private final VcsFileRevision revision;
643 private RevisionMergeSourceInfo(@NotNull VcsFileRevision revision) {
644 this.revision = revision;
648 public SvnFileRevision getRevision() {
649 return (SvnFileRevision)revision;
652 // will be used, for instance, while copying (to clipboard) data from table
654 public String toString() {
655 return toString(revision);
658 private static String toString(@Nullable VcsFileRevision value) {
659 if (!(value instanceof SvnFileRevision)) return "";
660 final SvnFileRevision revision = (SvnFileRevision)value;
661 final List<SvnFileRevision> mergeSources = revision.getMergeSources();
662 if (mergeSources.isEmpty()) {
665 final StringBuilder sb = new StringBuilder();
666 for (SvnFileRevision source : mergeSources) {
667 if (sb.length() != 0) {
670 sb.append(source.getRevisionNumber().asString());
671 if (!source.getMergeSources().isEmpty()) {
675 return sb.toString();
679 private class MergeSourceColumnInfo extends ColumnInfo<VcsFileRevision, RevisionMergeSourceInfo> {
680 private final MergeSourceRenderer myRenderer;
682 private MergeSourceColumnInfo(final SvnHistorySession session) {
683 super("Merge Sources");
684 myRenderer = new MergeSourceRenderer(session);
688 public TableCellRenderer getRenderer(final VcsFileRevision vcsFileRevision) {
693 public RevisionMergeSourceInfo valueOf(final VcsFileRevision vcsFileRevision) {
694 return vcsFileRevision != null ? new RevisionMergeSourceInfo(vcsFileRevision) : null;
697 public String getText(final VcsFileRevision vcsFileRevision) {
698 return myRenderer.getText(vcsFileRevision);
702 public int getAdditionalWidth() {
707 public String getPreferredStringValue() {
708 return "1234567, 1234567, 1234567";
712 private static final Object MERGE_SOURCE_DETAILS_TAG = new Object();
714 private class MergeSourceDetailsLinkListener extends TableLinkMouseListener {
715 private final VirtualFile myFile;
716 private final Object myTag;
718 private MergeSourceDetailsLinkListener(final Object tag, final VirtualFile file) {
724 public boolean onClick(@NotNull MouseEvent e, int clickCount) {
725 if (e.getButton() == 1 && !e.isPopupTrigger()) {
726 Object tag = getTagAt(e);
728 final SvnFileRevision revision = getSelectedRevision(e);
729 if (revision != null) {
730 SvnMergeSourceDetails.showMe(myVcs.getProject(), revision, myFile);
739 private SvnFileRevision getSelectedRevision(final MouseEvent e) {
740 JTable table = (JTable)e.getSource();
741 int row = table.rowAtPoint(e.getPoint());
742 int column = table.columnAtPoint(e.getPoint());
744 final Object value = table.getModel().getValueAt(row, column);
745 if (value instanceof RevisionMergeSourceInfo) {
746 return ((RevisionMergeSourceInfo)value).getRevision();
752 public void mouseMoved(MouseEvent e) {
753 JTable table = (JTable)e.getSource();
754 Object tag = getTagAt(e);
756 table.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
759 table.setCursor(Cursor.getDefaultCursor());
764 private class MergeSourceRenderer extends ColoredTableCellRenderer {
765 private MergeSourceDetailsLinkListener myListener;
766 private final VirtualFile myFile;
768 private MergeSourceRenderer(final SvnHistorySession session) {
769 myFile = session.getCommittedPath().getVirtualFile();
772 public String getText(final VcsFileRevision value) {
773 return RevisionMergeSourceInfo.toString(value);
777 protected void customizeCellRenderer(final JTable table,
779 final boolean selected,
780 final boolean hasFocus,
783 if (myListener == null) {
784 myListener = new MergeSourceDetailsLinkListener(MERGE_SOURCE_DETAILS_TAG, myFile);
785 myListener.installOn(table);
787 appendMergeSourceText(table, row, column, value instanceof RevisionMergeSourceInfo ? value.toString() : null);
790 private void appendMergeSourceText(JTable table, int row, int column, @Nullable String text) {
791 if (StringUtil.isEmpty(text)) {
792 append("", SimpleTextAttributes.REGULAR_ATTRIBUTES);
795 append(cutString(text, table.getCellRect(row, column, false).getWidth()), SimpleTextAttributes.REGULAR_ATTRIBUTES,
796 MERGE_SOURCE_DETAILS_TAG);
800 private String cutString(final String text, final double value) {
801 final FontMetrics m = getFontMetrics(getFont());
802 final Graphics g = getGraphics();
804 if (m.getStringBounds(text, g).getWidth() < value) return text;
806 final String dots = "...";
807 final double dotsWidth = m.getStringBounds(dots, g).getWidth();
808 if (dotsWidth >= value) {
812 for (int i = 1; i < text.length(); i++) {
813 if ((m.getStringBounds(text, 0, i, g).getWidth() + dotsWidth) >= value) {
814 if (i < 2) return dots;
815 return text.substring(0, i - 1) + dots;
822 private static class CopyFromColumnInfo extends ColumnInfo<VcsFileRevision, String> {
823 private final Icon myIcon = PlatformIcons.COPY_ICON;
824 private final ColoredTableCellRenderer myRenderer = new ColoredTableCellRenderer() {
826 protected void customizeCellRenderer(final JTable table,
828 final boolean selected,
829 final boolean hasFocus,
832 if (value instanceof String && ((String)value).length() > 0) {
834 setToolTipText(SvnBundle.message("copy.column.tooltip", value));
842 public CopyFromColumnInfo() {
843 super(SvnBundle.message("copy.column.title"));
847 public String valueOf(final VcsFileRevision o) {
848 return o instanceof SvnFileRevision ? ((SvnFileRevision)o).getCopyFromPath() : "";
852 public TableCellRenderer getRenderer(final VcsFileRevision vcsFileRevision) {
857 public String getMaxStringValue() {
858 return SvnBundle.message("copy.column.title");
862 public int getAdditionalWidth() {