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.SVNLogClient;
48 import org.tmatesoft.svn.core.wc.SVNRevision;
49 import org.tmatesoft.svn.core.wc2.SvnTarget;
50 import org.tmatesoft.svn.util.SVNLogType;
53 import javax.swing.table.TableCellRenderer;
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;
61 public class SvnHistoryProvider
62 implements VcsHistoryProvider, VcsCacheableHistorySessionFactory<Boolean, SvnHistorySession> {
63 private final SvnVcs myVcs;
65 public SvnHistoryProvider(SvnVcs vcs) {
70 public boolean supportsHistoryForDirectories() {
75 public DiffFromHistoryHandler getHistoryDiffHandler() {
80 public boolean canShowHistoryFor(@NotNull VirtualFile file) {
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};
93 final JPanel panel = new JPanel(new BorderLayout());
95 final JTextArea field = new JTextArea();
96 field.setEditable(false);
97 field.setBackground(UIUtil.getComboBoxDisabledBackground());
98 field.setWrapStyleWord(true);
99 listener = new Consumer<VcsFileRevision>() {
101 public void consume(VcsFileRevision vcsFileRevision) {
102 field.setText(mergeSourceColumn.getText(vcsFileRevision));
106 final MergeSourceDetailsAction sourceAction = new MergeSourceDetailsAction();
107 sourceAction.registerSelf(forShortcutRegistration);
109 JPanel fieldPanel = new ToolbarDecorator() {
111 protected JComponent getComponent() {
116 protected void updateButtons() {
120 protected void installDnDSupport() {
124 protected boolean isModelEditable() {
128 .addExtraAction(AnActionButton.fromAction(sourceAction))
130 fieldPanel.setBorder(IdeBorderFactory.createBorder(SideBorder.LEFT | SideBorder.TOP));
132 panel.add(fieldPanel, BorderLayout.CENTER);
133 panel.add(new JLabel("Merge Sources:"), BorderLayout.NORTH);
137 columns = new ColumnInfo[]{new CopyFromColumnInfo()};
141 return new VcsDependentHistoryComponents(columns, listener, addComp);
145 public FilePath getUsedFilePath(SvnHistorySession session) {
146 return session.getCommittedPath();
150 public Boolean getAddinionallyCachedData(SvnHistorySession session) {
151 return session.isHaveMergeSources();
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());
164 public VcsHistorySession createSessionFor(final FilePath filePath) throws VcsException {
165 final VcsAppendableHistoryPartnerAdapter adapter = new VcsAppendableHistoryPartnerAdapter();
166 reportAppendableHistory(filePath, adapter);
169 return adapter.getSession();
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);
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();
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();
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);
202 logLoader = new LocalLoader(myVcs, committedPath, from, to, limit, peg, showMergeSources);
206 logLoader.preliminary();
208 catch (SVNCancelException e) {
209 throw new VcsException(e);
211 catch (SVNException e) {
212 throw new VcsException(e);
215 if (showMergeSources) {
216 logLoader.initSupports15();
219 final SvnHistorySession historySession =
220 new SvnHistorySession(myVcs, Collections.<VcsFileRevision>emptyList(), committedPath, showMergeSources && Boolean.TRUE.equals(logLoader.mySupport15), null, false,
221 ! path.isNonLocal());
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()));
228 final Consumer<VcsFileRevision> consumer = new Consumer<VcsFileRevision>() {
230 public void consume(VcsFileRevision vcsFileRevision) {
231 if (!Boolean.TRUE.equals(sessionReported.get())) {
232 partner.reportCreatedEmptySession(historySession);
233 sessionReported.set(true);
235 partner.acceptRevision(vcsFileRevision);
239 logLoader.setConsumer(consumer);
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;
258 protected LogLoader(SvnVcs vcs, FilePath file, SVNRevision from, SVNRevision to, int limit, SVNRevision peg, boolean showMergeSources) {
265 myPI = ProgressManager.getInstance().getProgressIndicator();
266 myShowMergeSources = showMergeSources;
269 public void setConsumer(Consumer<VcsFileRevision> consumer) {
270 myConsumer = consumer;
273 protected void initSupports15() {
274 assert myUrl != null;
275 mySupport15 = SvnUtil.checkRepositoryVersion15(myVcs, myUrl);
278 public void check() throws VcsException {
279 if (myException != null) throw myException;
282 protected abstract void preliminary() throws SVNException;
284 protected abstract void load();
287 private static class LocalLoader extends LogLoader {
288 private SVNInfo myInfo;
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);
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");
301 if (myInfo.getURL() == null) {
302 myException = new VcsException("File " + myFile.getPath() + " is not under Subversion control");
305 myUrl = myInfo.getURL().toDecodedString();
309 protected void load() {
310 String relativeUrl = myUrl;
311 final SVNURL repoRootURL = myInfo.getRepositoryRootURL();
313 final String root = repoRootURL.toString();
314 if (myUrl != null && myUrl.startsWith(root)) {
315 relativeUrl = myUrl.substring(root.length());
318 myPI.setText2(SvnBundle.message("progress.text2.changes.establishing.connection", myUrl));
320 final SVNRevision pegRevision = myInfo.getRevision();
321 final SvnTarget target = SvnTarget.fromFile(myFile.getIOFile(), myPeg);
323 myVcs.getFactory(target).createHistoryClient().doLog(
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()));
332 catch (SVNCancelException e) {
335 catch (SVNException e) {
336 myException = new VcsException(e);
338 catch (VcsException e) {
344 private static ThrowableConsumer<VcsFileRevision, SVNException> createConsumerAdapter(final Consumer<VcsFileRevision> consumer) {
345 return new ThrowableConsumer<VcsFileRevision, SVNException>() {
347 public void consume(VcsFileRevision revision) throws SVNException {
348 consumer.consume(revision);
353 private static class RepositoryLoader extends LogLoader {
354 private final boolean myForceBackwards;
356 private RepositoryLoader(SvnVcs vcs,
362 boolean forceBackwards, boolean showMergeSources) {
363 super(vcs, file, from, to, limit, peg, showMergeSources);
364 myForceBackwards = forceBackwards;
368 protected void preliminary() throws SVNException {
369 myUrl = myFile.getPath().replace('\\', '/');
373 protected void load() {
375 myPI.setText2(SvnBundle.message("progress.text2.changes.establishing.connection", myUrl));
379 if (myForceBackwards) {
380 SVNURL svnurl = SVNURL.parseURIEncoded(myUrl);
381 if (! existsNow(svnurl)) {
382 loadBackwards(svnurl);
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);
393 final String root = rootURL.toString();
394 String relativeUrl = myUrl;
395 if (myUrl.startsWith(root)) {
396 relativeUrl = myUrl.substring(root.length());
398 SvnTarget target = SvnTarget.fromURL(svnurl, myPeg == null ? myFrom : myPeg);
399 RepositoryLogEntryHandler handler =
400 new RepositoryLogEntryHandler(myVcs, myUrl, SVNRevision.UNDEFINED, relativeUrl, createConsumerAdapter(myConsumer), rootURL);
402 myVcs.getFactory(target).createHistoryClient()
403 .doLog(target, operationalFrom, myTo == null ? SVNRevision.create(1) : myTo, false, true, myShowMergeSources && mySupport15,
404 myLimit + 1, null, handler);
406 catch (SVNCancelException e) {
409 catch (SVNException e) {
410 myException = new VcsException(e);
412 catch (VcsException e) {
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());
425 // TODO: Update this call to myVcs.getFactory.createHistoryClient
426 SVNLogClient client = myVcs.createLogClient();
428 final RepositoryLogEntryHandler repositoryLogEntryHandler =
429 new RepositoryLogEntryHandler(myVcs, myUrl, SVNRevision.UNDEFINED, relativeUrl,
430 new ThrowableConsumer<VcsFileRevision, SVNException>() {
432 public void consume(VcsFileRevision revision) throws SVNException {
433 myConsumer.consume(revision);
434 throw new SVNCancelException(); // load only one revision
437 repositoryLogEntryHandler.setThrowCancelOnMeetPathCreation(true);
439 client.doLog(rootURL, new String[]{}, myFrom, myFrom, myTo == null ? SVNRevision.create(1) : myTo, false, true, myShowMergeSources && mySupport15, 0, null, repositoryLogEntryHandler);
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);
448 return info != null ? info.getRepositoryRootURL() : null;
451 private boolean existsNow(SVNURL svnurl) {
454 info = myVcs.getInfo(svnurl, SVNRevision.HEAD, SVNRevision.HEAD);
456 catch (SVNException e) {
459 return info != null && info.getURL() != null && info.getRevision().isValid();
464 public String getHelpId() {
469 public AnAction[] getAdditionalActions(final Runnable refresher) {
470 return new AnAction[]{ ShowAllAffectedGenericAction.getInstance(), new MergeSourceDetailsAction(), new SvnEditCommitMessageFromFileHistoryAction()};
474 public boolean isDateOmittable() {
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;
492 public void setThrowCancelOnMeetPathCreation(boolean throwCancelOnMeetPathCreation) {
493 myThrowCancelOnMeetPathCreation = throwCancelOnMeetPathCreation;
496 public MyLogEntryHandler(SvnVcs vcs, final String url,
497 final SVNRevision pegRevision,
499 final ThrowableConsumer<VcsFileRevision, SVNException> result,
500 SVNURL repoRootURL, Charset charset)
501 throws SVNException, VcsException {
503 myLastPathCorrector = new SvnPathThroughHistoryCorrection(lastPath);
504 myLastPath = lastPath;
506 myIndicator = ProgressManager.getInstance().getProgressIndicator();
508 myPegRevision = pegRevision;
510 myRepositoryRoot = repoRootURL;
511 myTracker = new SvnMergeSourceTracker(new ThrowableConsumer<Pair<SVNLogEntry, Integer>, SVNException>() {
513 public void consume(final Pair<SVNLogEntry, Integer> svnLogEntryIntegerPair) throws SVNException {
514 final SVNLogEntry logEntry = svnLogEntryIntegerPair.getFirst();
516 if (myIndicator != null) {
517 if (myIndicator.isCanceled()) {
518 SVNErrorManager.cancel(SvnBundle.message("exception.text.update.operation.cancelled"), SVNLogType.DEFAULT);
520 myIndicator.setText2(SvnBundle.message("progress.text2.revision.processed", logEntry.getRevision()));
522 SVNLogEntryPath entryPath = null;
523 String copyPath = null;
524 final int mergeLevel = svnLogEntryIntegerPair.getSecond();
526 if (! myLastPathCorrector.isRoot()) {
527 myLastPathCorrector.handleLogEntry(logEntry);
528 entryPath = myLastPathCorrector.getDirectlyMentioned();
530 if (entryPath != null) {
531 copyPath = entryPath.getCopyPath();
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
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;
548 final SvnFileRevision revision = createRevision(logEntry, copyPath, entryPath);
549 if (mergeLevel >= 0) {
550 addToListByLevel((SvnFileRevision)myPrevious, revision, mergeLevel);
553 myResult.consume(revision);
554 myPrevious = revision;
556 if (myThrowCancelOnMeetPathCreation && myUrl.equals(revision.getURL()) && entryPath != null && entryPath.getType() == 'A') {
557 throw new SVNCancelException();
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) {
577 path = SVNPathUtil.removeTail(path);
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)) {
595 public void handleLogEntry(SVNLogEntry logEntry) throws SVNException {
596 myTracker.consume(logEntry);
599 private static void addToListByLevel(final SvnFileRevision revision, final SvnFileRevision revisionToAdd, final int level) {
604 revision.addMergeSource(revisionToAdd);
607 final List<SvnFileRevision> sources = revision.getMergeSources();
608 if (!sources.isEmpty()) {
609 addToListByLevel(sources.get(sources.size() - 1), revisionToAdd, level - 1);
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);
625 private static class RepositoryLogEntryHandler extends MyLogEntryHandler {
626 public RepositoryLogEntryHandler(final SvnVcs vcs, final String url,
627 final SVNRevision pegRevision,
629 final ThrowableConsumer<VcsFileRevision, SVNException> result,
631 throws VcsException, SVNException {
632 super(vcs, url, pegRevision, lastPath, result, repoRootURL, null);
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);
644 private static class RevisionMergeSourceInfo {
646 @NotNull private final VcsFileRevision revision;
648 private RevisionMergeSourceInfo(@NotNull VcsFileRevision revision) {
649 this.revision = revision;
653 public SvnFileRevision getRevision() {
654 return (SvnFileRevision)revision;
657 // will be used, for instance, while copying (to clipboard) data from table
659 public String toString() {
660 return toString(revision);
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()) {
670 final StringBuilder sb = new StringBuilder();
671 for (SvnFileRevision source : mergeSources) {
672 if (sb.length() != 0) {
675 sb.append(source.getRevisionNumber().asString());
676 if (!source.getMergeSources().isEmpty()) {
680 return sb.toString();
684 private class MergeSourceColumnInfo extends ColumnInfo<VcsFileRevision, RevisionMergeSourceInfo> {
685 private final MergeSourceRenderer myRenderer;
687 private MergeSourceColumnInfo(final SvnHistorySession session) {
688 super("Merge Sources");
689 myRenderer = new MergeSourceRenderer(session);
693 public TableCellRenderer getRenderer(final VcsFileRevision vcsFileRevision) {
698 public RevisionMergeSourceInfo valueOf(final VcsFileRevision vcsFileRevision) {
699 return vcsFileRevision != null ? new RevisionMergeSourceInfo(vcsFileRevision) : null;
702 public String getText(final VcsFileRevision vcsFileRevision) {
703 return myRenderer.getText(vcsFileRevision);
707 public int getAdditionalWidth() {
712 public String getPreferredStringValue() {
713 return "1234567, 1234567, 1234567";
717 private static final Object MERGE_SOURCE_DETAILS_TAG = new Object();
719 private class MergeSourceDetailsLinkListener extends TableLinkMouseListener {
720 private final VirtualFile myFile;
721 private final Object myTag;
723 private MergeSourceDetailsLinkListener(final Object tag, final VirtualFile file) {
729 public boolean onClick(@NotNull MouseEvent e, int clickCount) {
730 if (e.getButton() == 1 && !e.isPopupTrigger()) {
731 Object tag = getTagAt(e);
733 final SvnFileRevision revision = getSelectedRevision(e);
734 if (revision != null) {
735 SvnMergeSourceDetails.showMe(myVcs.getProject(), revision, myFile);
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());
749 final Object value = table.getModel().getValueAt(row, column);
750 if (value instanceof RevisionMergeSourceInfo) {
751 return ((RevisionMergeSourceInfo)value).getRevision();
757 public void mouseMoved(MouseEvent e) {
758 JTable table = (JTable)e.getSource();
759 Object tag = getTagAt(e);
761 table.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
764 table.setCursor(Cursor.getDefaultCursor());
769 private class MergeSourceRenderer extends ColoredTableCellRenderer {
770 private MergeSourceDetailsLinkListener myListener;
771 private final VirtualFile myFile;
773 private MergeSourceRenderer(final SvnHistorySession session) {
774 myFile = session.getCommittedPath().getVirtualFile();
777 public String getText(final VcsFileRevision value) {
778 return RevisionMergeSourceInfo.toString(value);
782 protected void customizeCellRenderer(final JTable table,
784 final boolean selected,
785 final boolean hasFocus,
788 if (myListener == null) {
789 myListener = new MergeSourceDetailsLinkListener(MERGE_SOURCE_DETAILS_TAG, myFile);
790 myListener.installOn(table);
792 appendMergeSourceText(table, row, column, value instanceof RevisionMergeSourceInfo ? value.toString() : null);
795 private void appendMergeSourceText(JTable table, int row, int column, @Nullable String text) {
796 if (StringUtil.isEmpty(text)) {
797 append("", SimpleTextAttributes.REGULAR_ATTRIBUTES);
800 append(cutString(text, table.getCellRect(row, column, false).getWidth()), SimpleTextAttributes.REGULAR_ATTRIBUTES,
801 MERGE_SOURCE_DETAILS_TAG);
805 private String cutString(final String text, final double value) {
806 final FontMetrics m = getFontMetrics(getFont());
807 final Graphics g = getGraphics();
809 if (m.getStringBounds(text, g).getWidth() < value) return text;
811 final String dots = "...";
812 final double dotsWidth = m.getStringBounds(dots, g).getWidth();
813 if (dotsWidth >= value) {
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;
827 private static class CopyFromColumnInfo extends ColumnInfo<VcsFileRevision, String> {
828 private final Icon myIcon = PlatformIcons.COPY_ICON;
829 private final ColoredTableCellRenderer myRenderer = new ColoredTableCellRenderer() {
831 protected void customizeCellRenderer(final JTable table,
833 final boolean selected,
834 final boolean hasFocus,
837 if (value instanceof String && ((String)value).length() > 0) {
839 setToolTipText(SvnBundle.message("copy.column.tooltip", value));
847 public CopyFromColumnInfo() {
848 super(SvnBundle.message("copy.column.title"));
852 public String valueOf(final VcsFileRevision o) {
853 return o instanceof SvnFileRevision ? ((SvnFileRevision)o).getCopyFromPath() : "";
857 public TableCellRenderer getRenderer(final VcsFileRevision vcsFileRevision) {
862 public String getMaxStringValue() {
863 return SvnBundle.message("copy.column.title");
867 public int getAdditionalWidth() {