| /* |
| * Copyright 2000-2009 JetBrains s.r.o. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.jetbrains.idea.svn.history; |
| |
| import com.intellij.openapi.actionSystem.AnAction; |
| import com.intellij.openapi.progress.ProgressIndicator; |
| import com.intellij.openapi.progress.ProgressManager; |
| import com.intellij.openapi.util.Pair; |
| import com.intellij.openapi.util.Ref; |
| import com.intellij.openapi.util.text.StringUtil; |
| import com.intellij.openapi.vcs.FilePath; |
| import com.intellij.openapi.vcs.VcsConfiguration; |
| import com.intellij.openapi.vcs.VcsException; |
| import com.intellij.openapi.vcs.annotate.ShowAllAffectedGenericAction; |
| import com.intellij.openapi.vcs.changes.Change; |
| import com.intellij.openapi.vcs.changes.ChangeListManager; |
| import com.intellij.openapi.vcs.changes.ContentRevision; |
| import com.intellij.openapi.vcs.changes.issueLinks.TableLinkMouseListener; |
| import com.intellij.openapi.vcs.history.*; |
| import com.intellij.openapi.vfs.VirtualFile; |
| import com.intellij.ui.*; |
| import com.intellij.util.Consumer; |
| import com.intellij.util.PlatformIcons; |
| import com.intellij.util.ThrowableConsumer; |
| import com.intellij.util.ui.ColumnInfo; |
| import com.intellij.util.ui.UIUtil; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| import org.jetbrains.idea.svn.*; |
| import org.jetbrains.idea.svn.commandLine.SvnBindException; |
| import org.jetbrains.idea.svn.info.Info; |
| import org.tmatesoft.svn.core.*; |
| import org.tmatesoft.svn.core.internal.util.SVNPathUtil; |
| import org.tmatesoft.svn.core.internal.wc.SVNErrorManager; |
| import org.tmatesoft.svn.core.wc.SVNRevision; |
| import org.tmatesoft.svn.core.wc2.SvnTarget; |
| import org.tmatesoft.svn.util.SVNLogType; |
| |
| import javax.swing.*; |
| import javax.swing.table.TableCellRenderer; |
| import java.awt.*; |
| import java.awt.event.MouseEvent; |
| import java.nio.charset.Charset; |
| import java.util.Collections; |
| import java.util.Date; |
| import java.util.List; |
| |
| public class SvnHistoryProvider |
| implements VcsHistoryProvider, VcsCacheableHistorySessionFactory<Boolean, SvnHistorySession> { |
| private final SvnVcs myVcs; |
| |
| public SvnHistoryProvider(SvnVcs vcs) { |
| myVcs = vcs; |
| } |
| |
| @Override |
| public boolean supportsHistoryForDirectories() { |
| return true; |
| } |
| |
| @Override |
| public DiffFromHistoryHandler getHistoryDiffHandler() { |
| return new SvnDiffFromHistoryHandler(myVcs); |
| } |
| |
| @Override |
| public boolean canShowHistoryFor(@NotNull VirtualFile file) { |
| return true; |
| } |
| |
| @Override |
| public VcsDependentHistoryComponents getUICustomization(final VcsHistorySession session, JComponent forShortcutRegistration) { |
| final ColumnInfo[] columns; |
| final Consumer<VcsFileRevision> listener; |
| final JComponent addComp; |
| if (((SvnHistorySession)session).isHaveMergeSources()) { |
| final MergeSourceColumnInfo mergeSourceColumn = new MergeSourceColumnInfo((SvnHistorySession)session); |
| columns = new ColumnInfo[]{new CopyFromColumnInfo(), mergeSourceColumn}; |
| |
| final JPanel panel = new JPanel(new BorderLayout()); |
| |
| final JTextArea field = new JTextArea(); |
| field.setEditable(false); |
| field.setBackground(UIUtil.getComboBoxDisabledBackground()); |
| field.setWrapStyleWord(true); |
| listener = new Consumer<VcsFileRevision>() { |
| @Override |
| public void consume(VcsFileRevision vcsFileRevision) { |
| field.setText(mergeSourceColumn.getText(vcsFileRevision)); |
| } |
| }; |
| |
| final MergeSourceDetailsAction sourceAction = new MergeSourceDetailsAction(); |
| sourceAction.registerSelf(forShortcutRegistration); |
| |
| JPanel fieldPanel = new ToolbarDecorator() { |
| @Override |
| protected JComponent getComponent() { |
| return field; |
| } |
| |
| @Override |
| protected void updateButtons() { |
| } |
| |
| @Override |
| protected void installDnDSupport() { |
| } |
| |
| @Override |
| protected boolean isModelEditable() { |
| return false; |
| } |
| }.initPosition() |
| .addExtraAction(AnActionButton.fromAction(sourceAction)) |
| .createPanel(); |
| fieldPanel.setBorder(IdeBorderFactory.createBorder(SideBorder.LEFT | SideBorder.TOP)); |
| |
| panel.add(fieldPanel, BorderLayout.CENTER); |
| panel.add(new JLabel("Merge Sources:"), BorderLayout.NORTH); |
| addComp = panel; |
| } |
| else { |
| columns = new ColumnInfo[]{new CopyFromColumnInfo()}; |
| addComp = null; |
| listener = null; |
| } |
| return new VcsDependentHistoryComponents(columns, listener, addComp); |
| } |
| |
| @Override |
| public FilePath getUsedFilePath(SvnHistorySession session) { |
| return session.getCommittedPath(); |
| } |
| |
| @Override |
| public Boolean getAddinionallyCachedData(SvnHistorySession session) { |
| return session.isHaveMergeSources(); |
| } |
| |
| @Override |
| public SvnHistorySession createFromCachedData(Boolean aBoolean, |
| @NotNull List<VcsFileRevision> revisions, |
| @NotNull FilePath filePath, |
| VcsRevisionNumber currentRevision) { |
| return new SvnHistorySession(myVcs, revisions, filePath, aBoolean, currentRevision, false, ! filePath.isNonLocal()); |
| } |
| |
| @Override |
| @Nullable |
| public VcsHistorySession createSessionFor(final FilePath filePath) throws VcsException { |
| final VcsAppendableHistoryPartnerAdapter adapter = new VcsAppendableHistoryPartnerAdapter(); |
| reportAppendableHistory(filePath, adapter); |
| adapter.check(); |
| |
| return adapter.getSession(); |
| } |
| |
| @Override |
| public void reportAppendableHistory(FilePath path, final VcsAppendableHistorySessionPartner partner) throws VcsException { |
| // we need + 1 rows to be reported to further detect that number of rows exceeded the limit |
| reportAppendableHistory(path, partner, null, null, VcsConfiguration.getInstance(myVcs.getProject()).MAXIMUM_HISTORY_ROWS + 1, null, false); |
| } |
| |
| public void reportAppendableHistory(FilePath path, final VcsAppendableHistorySessionPartner partner, |
| @Nullable final SVNRevision from, @Nullable final SVNRevision to, final int limit, |
| SVNRevision peg, final boolean forceBackwards) throws VcsException { |
| FilePath committedPath = path; |
| Change change = ChangeListManager.getInstance(myVcs.getProject()).getChange(path); |
| if (change != null) { |
| final ContentRevision beforeRevision = change.getBeforeRevision(); |
| final ContentRevision afterRevision = change.getAfterRevision(); |
| if (beforeRevision != null && afterRevision != null && !beforeRevision.getFile().equals(afterRevision.getFile()) && |
| afterRevision.getFile().equals(path)) { |
| committedPath = beforeRevision.getFile(); |
| } |
| // revision can be VcsRevisionNumber.NULL |
| if (peg == null && change.getBeforeRevision() != null && change.getBeforeRevision().getRevisionNumber() instanceof SvnRevisionNumber) { |
| peg = ((SvnRevisionNumber) change.getBeforeRevision().getRevisionNumber()).getRevision(); |
| } |
| } |
| |
| final boolean showMergeSources = SvnConfiguration.getInstance(myVcs.getProject()).isShowMergeSourcesInAnnotate(); |
| final LogLoader logLoader; |
| if (path.isNonLocal()) { |
| logLoader = new RepositoryLoader(myVcs, committedPath, from, to, limit, peg, forceBackwards, showMergeSources); |
| } |
| else { |
| logLoader = new LocalLoader(myVcs, committedPath, from, to, limit, peg, showMergeSources); |
| } |
| |
| try { |
| logLoader.preliminary(); |
| } |
| catch (SVNCancelException e) { |
| throw new VcsException(e); |
| } |
| catch (SVNException e) { |
| throw new VcsException(e); |
| } |
| logLoader.check(); |
| if (showMergeSources) { |
| logLoader.initSupports15(); |
| } |
| |
| final SvnHistorySession historySession = |
| new SvnHistorySession(myVcs, Collections.<VcsFileRevision>emptyList(), committedPath, showMergeSources && Boolean.TRUE.equals(logLoader.mySupport15), null, false, |
| ! path.isNonLocal()); |
| |
| final Ref<Boolean> sessionReported = new Ref<Boolean>(); |
| final ProgressIndicator indicator = ProgressManager.getInstance().getProgressIndicator(); |
| if (indicator != null) { |
| indicator.setText(SvnBundle.message("progress.text2.collecting.history", path.getName())); |
| } |
| final Consumer<VcsFileRevision> consumer = new Consumer<VcsFileRevision>() { |
| @Override |
| public void consume(VcsFileRevision vcsFileRevision) { |
| if (!Boolean.TRUE.equals(sessionReported.get())) { |
| partner.reportCreatedEmptySession(historySession); |
| sessionReported.set(true); |
| } |
| partner.acceptRevision(vcsFileRevision); |
| } |
| }; |
| |
| logLoader.setConsumer(consumer); |
| logLoader.load(); |
| logLoader.check(); |
| } |
| |
| private static abstract class LogLoader { |
| protected final boolean myShowMergeSources; |
| protected String myUrl; |
| protected boolean mySupport15; |
| protected final SvnVcs myVcs; |
| protected final FilePath myFile; |
| protected final SVNRevision myFrom; |
| protected final SVNRevision myTo; |
| protected final int myLimit; |
| protected final SVNRevision myPeg; |
| protected Consumer<VcsFileRevision> myConsumer; |
| protected final ProgressIndicator myPI; |
| protected VcsException myException; |
| |
| protected LogLoader(SvnVcs vcs, FilePath file, SVNRevision from, SVNRevision to, int limit, SVNRevision peg, boolean showMergeSources) { |
| myVcs = vcs; |
| myFile = file; |
| myFrom = from; |
| myTo = to; |
| myLimit = limit; |
| myPeg = peg; |
| myPI = ProgressManager.getInstance().getProgressIndicator(); |
| myShowMergeSources = showMergeSources; |
| } |
| |
| public void setConsumer(Consumer<VcsFileRevision> consumer) { |
| myConsumer = consumer; |
| } |
| |
| protected void initSupports15() { |
| assert myUrl != null; |
| mySupport15 = SvnUtil.checkRepositoryVersion15(myVcs, myUrl); |
| } |
| |
| public void check() throws VcsException { |
| if (myException != null) throw myException; |
| } |
| |
| protected abstract void preliminary() throws SVNException; |
| |
| protected abstract void load(); |
| } |
| |
| private static class LocalLoader extends LogLoader { |
| private Info myInfo; |
| |
| private LocalLoader(SvnVcs vcs, FilePath file, SVNRevision from, SVNRevision to, int limit, SVNRevision peg, boolean showMergeSources) { |
| super(vcs, file, from, to, limit, peg, showMergeSources); |
| } |
| |
| @Override |
| protected void preliminary() throws SVNException { |
| myInfo = myVcs.getInfo(myFile.getIOFile()); |
| if (myInfo == null || myInfo.getRepositoryRootURL() == null) { |
| myException = new VcsException("File " + myFile.getPath() + " is not under version control"); |
| return; |
| } |
| if (myInfo.getURL() == null) { |
| myException = new VcsException("File " + myFile.getPath() + " is not under Subversion control"); |
| return; |
| } |
| myUrl = myInfo.getURL().toDecodedString(); |
| } |
| |
| @Override |
| protected void load() { |
| String relativeUrl = myUrl; |
| final SVNURL repoRootURL = myInfo.getRepositoryRootURL(); |
| |
| final String root = repoRootURL.toString(); |
| if (myUrl != null && myUrl.startsWith(root)) { |
| relativeUrl = myUrl.substring(root.length()); |
| } |
| if (myPI != null) { |
| myPI.setText2(SvnBundle.message("progress.text2.changes.establishing.connection", myUrl)); |
| } |
| final SVNRevision pegRevision = myInfo.getRevision(); |
| final SvnTarget target = SvnTarget.fromFile(myFile.getIOFile(), myPeg); |
| try { |
| myVcs.getFactory(target).createHistoryClient().doLog( |
| target, |
| myFrom == null ? SVNRevision.HEAD : myFrom, |
| myTo == null ? SVNRevision.create(1) : myTo, |
| false, true, myShowMergeSources && mySupport15, myLimit + 1, null, |
| new MyLogEntryHandler(myVcs, myUrl, pegRevision, relativeUrl, |
| createConsumerAdapter(myConsumer), |
| repoRootURL, myFile.getCharset())); |
| } |
| catch (SVNCancelException e) { |
| // |
| } |
| catch (SVNException e) { |
| myException = new VcsException(e); |
| } |
| catch (VcsException e) { |
| myException = e; |
| } |
| } |
| } |
| |
| private static ThrowableConsumer<VcsFileRevision, SVNException> createConsumerAdapter(final Consumer<VcsFileRevision> consumer) { |
| return new ThrowableConsumer<VcsFileRevision, SVNException>() { |
| @Override |
| public void consume(VcsFileRevision revision) throws SVNException { |
| consumer.consume(revision); |
| } |
| }; |
| } |
| |
| private static class RepositoryLoader extends LogLoader { |
| private final boolean myForceBackwards; |
| |
| private RepositoryLoader(SvnVcs vcs, |
| FilePath file, |
| SVNRevision from, |
| SVNRevision to, |
| int limit, |
| SVNRevision peg, |
| boolean forceBackwards, boolean showMergeSources) { |
| super(vcs, file, from, to, limit, peg, showMergeSources); |
| myForceBackwards = forceBackwards; |
| } |
| |
| @Override |
| protected void preliminary() throws SVNException { |
| myUrl = myFile.getPath().replace('\\', '/'); |
| } |
| |
| @Override |
| protected void load() { |
| if (myPI != null) { |
| myPI.setText2(SvnBundle.message("progress.text2.changes.establishing.connection", myUrl)); |
| } |
| |
| try { |
| if (myForceBackwards) { |
| SVNURL svnurl = SVNURL.parseURIEncoded(myUrl); |
| if (! existsNow(svnurl)) { |
| loadBackwards(svnurl); |
| return; |
| } |
| } |
| |
| final SVNURL svnurl = SVNURL.parseURIEncoded(myUrl); |
| SVNRevision operationalFrom = myFrom == null ? SVNRevision.HEAD : myFrom; |
| // TODO: try to rewrite without separately retrieving repository url by item url - as this command could require authentication |
| // TODO: and it is not "clear enough/easy to implement" with current design (for some cases) how to cache credentials (if in |
| // TODO: non-interactive mode) |
| final SVNURL rootURL = SvnUtil.getRepositoryRoot(myVcs, svnurl); |
| if (rootURL == null) { |
| throw new VcsException("Could not find repository root for URL: " + myUrl); |
| } |
| final String root = rootURL.toString(); |
| String relativeUrl = myUrl; |
| if (myUrl.startsWith(root)) { |
| relativeUrl = myUrl.substring(root.length()); |
| } |
| SvnTarget target = SvnTarget.fromURL(svnurl, myPeg == null ? myFrom : myPeg); |
| RepositoryLogEntryHandler handler = |
| new RepositoryLogEntryHandler(myVcs, myUrl, SVNRevision.UNDEFINED, relativeUrl, createConsumerAdapter(myConsumer), rootURL); |
| |
| myVcs.getFactory(target).createHistoryClient() |
| .doLog(target, operationalFrom, myTo == null ? SVNRevision.create(1) : myTo, false, true, myShowMergeSources && mySupport15, |
| myLimit + 1, null, handler); |
| } |
| catch (SVNCancelException e) { |
| // |
| } |
| catch (SVNException e) { |
| myException = new VcsException(e); |
| } |
| catch (VcsException e) { |
| myException = e; |
| } |
| } |
| |
| private void loadBackwards(SVNURL svnurl) throws SVNException, VcsException { |
| // this method is called when svnurl does not exist in latest repository revision - thus concrete old revision is used for "info" |
| // command to get repository url |
| Info info = myVcs.getInfo(svnurl, myPeg, myPeg); |
| final SVNURL rootURL = info != null ? info.getRepositoryRootURL() : null; |
| final String root = rootURL != null ? rootURL.toString() : ""; |
| String relativeUrl = myUrl; |
| if (myUrl.startsWith(root)) { |
| relativeUrl = myUrl.substring(root.length()); |
| } |
| |
| final RepositoryLogEntryHandler repositoryLogEntryHandler = |
| new RepositoryLogEntryHandler(myVcs, myUrl, SVNRevision.UNDEFINED, relativeUrl, |
| new ThrowableConsumer<VcsFileRevision, SVNException>() { |
| @Override |
| public void consume(VcsFileRevision revision) throws SVNException { |
| myConsumer.consume(revision); |
| } |
| }, rootURL); |
| repositoryLogEntryHandler.setThrowCancelOnMeetPathCreation(true); |
| |
| SvnTarget target = SvnTarget.fromURL(rootURL, myFrom); |
| myVcs.getFactory(target).createHistoryClient() |
| .doLog(target, myFrom, myTo == null ? SVNRevision.create(1) : myTo, false, true, myShowMergeSources && mySupport15, 1, null, |
| repositoryLogEntryHandler); |
| } |
| |
| private boolean existsNow(SVNURL svnurl) { |
| final Info info; |
| try { |
| info = myVcs.getInfo(svnurl, SVNRevision.HEAD, SVNRevision.HEAD); |
| } |
| catch (SvnBindException e) { |
| return false; |
| } |
| return info != null && info.getURL() != null && info.getRevision().isValid(); |
| } |
| } |
| |
| @Override |
| public String getHelpId() { |
| return null; |
| } |
| |
| @Override |
| public AnAction[] getAdditionalActions(final Runnable refresher) { |
| return new AnAction[]{ ShowAllAffectedGenericAction.getInstance(), new MergeSourceDetailsAction(), new SvnEditCommitMessageFromFileHistoryAction()}; |
| } |
| |
| @Override |
| public boolean isDateOmittable() { |
| return false; |
| } |
| |
| private static class MyLogEntryHandler implements LogEntryConsumer { |
| private final ProgressIndicator myIndicator; |
| protected final SvnVcs myVcs; |
| protected final SvnPathThroughHistoryCorrection myLastPathCorrector; |
| private final Charset myCharset; |
| protected final ThrowableConsumer<VcsFileRevision, SVNException> myResult; |
| private final String myLastPath; |
| private VcsFileRevision myPrevious; |
| private final SVNRevision myPegRevision; |
| protected final String myUrl; |
| private final SvnMergeSourceTracker myTracker; |
| protected SVNURL myRepositoryRoot; |
| private boolean myThrowCancelOnMeetPathCreation; |
| |
| public void setThrowCancelOnMeetPathCreation(boolean throwCancelOnMeetPathCreation) { |
| myThrowCancelOnMeetPathCreation = throwCancelOnMeetPathCreation; |
| } |
| |
| public MyLogEntryHandler(SvnVcs vcs, final String url, |
| final SVNRevision pegRevision, |
| String lastPath, |
| final ThrowableConsumer<VcsFileRevision, SVNException> result, |
| SVNURL repoRootURL, Charset charset) |
| throws SVNException, VcsException { |
| myVcs = vcs; |
| myLastPathCorrector = new SvnPathThroughHistoryCorrection(lastPath); |
| myLastPath = lastPath; |
| myCharset = charset; |
| myIndicator = ProgressManager.getInstance().getProgressIndicator(); |
| myResult = result; |
| myPegRevision = pegRevision; |
| myUrl = url; |
| myRepositoryRoot = repoRootURL; |
| myTracker = new SvnMergeSourceTracker(new ThrowableConsumer<Pair<LogEntry, Integer>, SVNException>() { |
| @Override |
| public void consume(final Pair<LogEntry, Integer> svnLogEntryIntegerPair) throws SVNException { |
| final LogEntry logEntry = svnLogEntryIntegerPair.getFirst(); |
| |
| if (myIndicator != null) { |
| if (myIndicator.isCanceled()) { |
| SVNErrorManager.cancel(SvnBundle.message("exception.text.update.operation.cancelled"), SVNLogType.DEFAULT); |
| } |
| myIndicator.setText2(SvnBundle.message("progress.text2.revision.processed", logEntry.getRevision())); |
| } |
| LogEntryPath entryPath = null; |
| String copyPath = null; |
| final int mergeLevel = svnLogEntryIntegerPair.getSecond(); |
| |
| if (! myLastPathCorrector.isRoot()) { |
| myLastPathCorrector.consume(logEntry); |
| entryPath = myLastPathCorrector.getDirectlyMentioned(); |
| copyPath = null; |
| if (entryPath != null) { |
| copyPath = entryPath.getCopyPath(); |
| } else { |
| // if there are no path with exact match, check whether parent or child paths had changed |
| // "entry path" is allowed to be null now; if it is null, last path would be taken for revision construction |
| |
| // Separate LogEntry is issued for each "merge source" revision. These "merge source" revisions are treated as child |
| // revisions of some other revision - this way we construct merge hierarchy. |
| // mergeLevel >= 0 indicates that we are currently processing some "merge source" revision. This "merge source" revision |
| // contains changes from some other branch - so checkForChildChanges() and checkForParentChanges() return "false". |
| // Because of this case we apply these methods only for non-"merge source" revisions - this means mergeLevel < 0. |
| // TODO: Do not apply path filtering even for log entries on the first level => just output of 'svn log' should be returned. |
| // TODO: Looks like there is no cases when we issue 'svn log' for some parent paths or some other cases where we need such |
| // TODO: filtering. Check user feedback on this. |
| // if (mergeLevel < 0 && !checkForChildChanges(logEntry) && !checkForParentChanges(logEntry)) return; |
| } |
| } |
| |
| final SvnFileRevision revision = createRevision(logEntry, copyPath, entryPath); |
| if (mergeLevel >= 0) { |
| addToListByLevel((SvnFileRevision)myPrevious, revision, mergeLevel); |
| } |
| else { |
| myResult.consume(revision); |
| myPrevious = revision; |
| } |
| if (myThrowCancelOnMeetPathCreation && myUrl.equals(revision.getURL()) && entryPath != null && entryPath.getType() == 'A') { |
| throw new SVNCancelException(); |
| } |
| } |
| |
| }); |
| } |
| |
| private boolean checkForParentChanges(LogEntry logEntry) { |
| final String lastPathBefore = myLastPathCorrector.getBefore(); |
| String path = SVNPathUtil.removeTail(lastPathBefore); |
| while (path.length() > 0) { |
| final LogEntryPath entryPath = logEntry.getChangedPaths().get(path); |
| // A & D are checked since we are not interested in parent folders property changes, only in structure changes |
| // TODO: seems that R (replaced) should also be checked here |
| if (entryPath != null && (entryPath.getType() == 'A' || entryPath.getType() == 'D')) { |
| if (entryPath.getCopyPath() != null) { |
| return true; |
| } |
| break; |
| } |
| path = SVNPathUtil.removeTail(path); |
| } |
| return false; |
| } |
| |
| // TODO: this makes sense only for directories, but should always return true if something under the directory was changed in revision |
| // TODO: as svn will provide child changes in history for directory |
| private boolean checkForChildChanges(LogEntry logEntry) { |
| final String lastPathBefore = myLastPathCorrector.getBefore(); |
| for (String key : logEntry.getChangedPaths().keySet()) { |
| if (SVNPathUtil.isAncestor(lastPathBefore, key)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| @Override |
| public void consume(LogEntry logEntry) throws SVNException { |
| myTracker.consume(logEntry); |
| } |
| |
| private static void addToListByLevel(final SvnFileRevision revision, final SvnFileRevision revisionToAdd, final int level) { |
| if (level < 0) { |
| return; |
| } |
| if (level == 0) { |
| revision.addMergeSource(revisionToAdd); |
| return; |
| } |
| final List<SvnFileRevision> sources = revision.getMergeSources(); |
| if (!sources.isEmpty()) { |
| addToListByLevel(sources.get(sources.size() - 1), revisionToAdd, level - 1); |
| } |
| } |
| |
| protected SvnFileRevision createRevision(final LogEntry logEntry, final String copyPath, LogEntryPath entryPath) throws SVNException { |
| Date date = logEntry.getDate(); |
| String author = logEntry.getAuthor(); |
| String message = logEntry.getMessage(); |
| SVNRevision rev = SVNRevision.create(logEntry.getRevision()); |
| final SVNURL url = myRepositoryRoot.appendPath(myLastPath, true); |
| // final SVNURL url = entryPath != null ? myRepositoryRoot.appendPath(entryPath.getPath(), true) : |
| // myRepositoryRoot.appendPath(myLastPathCorrector.getBefore(), false); |
| return new SvnFileRevision(myVcs, myPegRevision, rev, url.toString(), author, date, message, copyPath); |
| } |
| } |
| |
| private static class RepositoryLogEntryHandler extends MyLogEntryHandler { |
| public RepositoryLogEntryHandler(final SvnVcs vcs, final String url, |
| final SVNRevision pegRevision, |
| String lastPath, |
| final ThrowableConsumer<VcsFileRevision, SVNException> result, |
| SVNURL repoRootURL) |
| throws VcsException, SVNException { |
| super(vcs, url, pegRevision, lastPath, result, repoRootURL, null); |
| } |
| |
| @Override |
| protected SvnFileRevision createRevision(final LogEntry logEntry, final String copyPath, LogEntryPath entryPath) |
| throws SVNException { |
| final SVNURL url = entryPath == null ? myRepositoryRoot.appendPath(myLastPathCorrector.getBefore(), false) : |
| myRepositoryRoot.appendPath(entryPath.getPath(), true); |
| return new SvnFileRevision(myVcs, SVNRevision.UNDEFINED, logEntry, url.toString(), copyPath); |
| } |
| } |
| |
| private static class RevisionMergeSourceInfo { |
| |
| @NotNull private final VcsFileRevision revision; |
| |
| private RevisionMergeSourceInfo(@NotNull VcsFileRevision revision) { |
| this.revision = revision; |
| } |
| |
| @NotNull |
| public SvnFileRevision getRevision() { |
| return (SvnFileRevision)revision; |
| } |
| |
| // will be used, for instance, while copying (to clipboard) data from table |
| @Override |
| public String toString() { |
| return toString(revision); |
| } |
| |
| private static String toString(@Nullable VcsFileRevision value) { |
| if (!(value instanceof SvnFileRevision)) return ""; |
| final SvnFileRevision revision = (SvnFileRevision)value; |
| final List<SvnFileRevision> mergeSources = revision.getMergeSources(); |
| if (mergeSources.isEmpty()) { |
| return ""; |
| } |
| final StringBuilder sb = new StringBuilder(); |
| for (SvnFileRevision source : mergeSources) { |
| if (sb.length() != 0) { |
| sb.append(", "); |
| } |
| sb.append(source.getRevisionNumber().asString()); |
| if (!source.getMergeSources().isEmpty()) { |
| sb.append("*"); |
| } |
| } |
| return sb.toString(); |
| } |
| } |
| |
| private class MergeSourceColumnInfo extends ColumnInfo<VcsFileRevision, RevisionMergeSourceInfo> { |
| private final MergeSourceRenderer myRenderer; |
| |
| private MergeSourceColumnInfo(final SvnHistorySession session) { |
| super("Merge Sources"); |
| myRenderer = new MergeSourceRenderer(session); |
| } |
| |
| @Override |
| public TableCellRenderer getRenderer(final VcsFileRevision vcsFileRevision) { |
| return myRenderer; |
| } |
| |
| @Override |
| public RevisionMergeSourceInfo valueOf(final VcsFileRevision vcsFileRevision) { |
| return vcsFileRevision != null ? new RevisionMergeSourceInfo(vcsFileRevision) : null; |
| } |
| |
| public String getText(final VcsFileRevision vcsFileRevision) { |
| return myRenderer.getText(vcsFileRevision); |
| } |
| |
| @Override |
| public int getAdditionalWidth() { |
| return 20; |
| } |
| |
| @Override |
| public String getPreferredStringValue() { |
| return "1234567, 1234567, 1234567"; |
| } |
| } |
| |
| private static final Object MERGE_SOURCE_DETAILS_TAG = new Object(); |
| |
| private class MergeSourceDetailsLinkListener extends TableLinkMouseListener { |
| private final VirtualFile myFile; |
| private final Object myTag; |
| |
| private MergeSourceDetailsLinkListener(final Object tag, final VirtualFile file) { |
| myTag = tag; |
| myFile = file; |
| } |
| |
| @Override |
| public boolean onClick(@NotNull MouseEvent e, int clickCount) { |
| if (e.getButton() == 1 && !e.isPopupTrigger()) { |
| Object tag = getTagAt(e); |
| if (tag == myTag) { |
| final SvnFileRevision revision = getSelectedRevision(e); |
| if (revision != null) { |
| SvnMergeSourceDetails.showMe(myVcs.getProject(), revision, myFile); |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| @Nullable |
| private SvnFileRevision getSelectedRevision(final MouseEvent e) { |
| JTable table = (JTable)e.getSource(); |
| int row = table.rowAtPoint(e.getPoint()); |
| int column = table.columnAtPoint(e.getPoint()); |
| |
| final Object value = table.getModel().getValueAt(row, column); |
| if (value instanceof RevisionMergeSourceInfo) { |
| return ((RevisionMergeSourceInfo)value).getRevision(); |
| } |
| return null; |
| } |
| |
| @Override |
| public void mouseMoved(MouseEvent e) { |
| JTable table = (JTable)e.getSource(); |
| Object tag = getTagAt(e); |
| if (tag == myTag) { |
| table.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); |
| } |
| else { |
| table.setCursor(Cursor.getDefaultCursor()); |
| } |
| } |
| } |
| |
| private class MergeSourceRenderer extends ColoredTableCellRenderer { |
| private MergeSourceDetailsLinkListener myListener; |
| private final VirtualFile myFile; |
| |
| private MergeSourceRenderer(final SvnHistorySession session) { |
| myFile = session.getCommittedPath().getVirtualFile(); |
| } |
| |
| public String getText(final VcsFileRevision value) { |
| return RevisionMergeSourceInfo.toString(value); |
| } |
| |
| @Override |
| protected void customizeCellRenderer(final JTable table, |
| final Object value, |
| final boolean selected, |
| final boolean hasFocus, |
| final int row, |
| final int column) { |
| if (myListener == null) { |
| myListener = new MergeSourceDetailsLinkListener(MERGE_SOURCE_DETAILS_TAG, myFile); |
| myListener.installOn(table); |
| } |
| appendMergeSourceText(table, row, column, value instanceof RevisionMergeSourceInfo ? value.toString() : null); |
| } |
| |
| private void appendMergeSourceText(JTable table, int row, int column, @Nullable String text) { |
| if (StringUtil.isEmpty(text)) { |
| append("", SimpleTextAttributes.REGULAR_ATTRIBUTES); |
| } |
| else { |
| append(cutString(text, table.getCellRect(row, column, false).getWidth()), SimpleTextAttributes.REGULAR_ATTRIBUTES, |
| MERGE_SOURCE_DETAILS_TAG); |
| } |
| } |
| |
| private String cutString(final String text, final double value) { |
| final FontMetrics m = getFontMetrics(getFont()); |
| final Graphics g = getGraphics(); |
| |
| if (m.getStringBounds(text, g).getWidth() < value) return text; |
| |
| final String dots = "..."; |
| final double dotsWidth = m.getStringBounds(dots, g).getWidth(); |
| if (dotsWidth >= value) { |
| return dots; |
| } |
| |
| for (int i = 1; i < text.length(); i++) { |
| if ((m.getStringBounds(text, 0, i, g).getWidth() + dotsWidth) >= value) { |
| if (i < 2) return dots; |
| return text.substring(0, i - 1) + dots; |
| } |
| } |
| return text; |
| } |
| } |
| |
| private static class CopyFromColumnInfo extends ColumnInfo<VcsFileRevision, String> { |
| private final Icon myIcon = PlatformIcons.COPY_ICON; |
| private final ColoredTableCellRenderer myRenderer = new ColoredTableCellRenderer() { |
| @Override |
| protected void customizeCellRenderer(final JTable table, |
| final Object value, |
| final boolean selected, |
| final boolean hasFocus, |
| final int row, |
| final int column) { |
| if (value instanceof String && ((String)value).length() > 0) { |
| setIcon(myIcon); |
| setToolTipText(SvnBundle.message("copy.column.tooltip", value)); |
| } |
| else { |
| setToolTipText(""); |
| } |
| } |
| }; |
| |
| public CopyFromColumnInfo() { |
| super(SvnBundle.message("copy.column.title")); |
| } |
| |
| @Override |
| public String valueOf(final VcsFileRevision o) { |
| return o instanceof SvnFileRevision ? ((SvnFileRevision)o).getCopyFromPath() : ""; |
| } |
| |
| @Override |
| public TableCellRenderer getRenderer(final VcsFileRevision vcsFileRevision) { |
| return myRenderer; |
| } |
| |
| @Override |
| public String getMaxStringValue() { |
| return SvnBundle.message("copy.column.title"); |
| } |
| |
| @Override |
| public int getAdditionalWidth() { |
| return 6; |
| } |
| } |
| } |