blob: 4712a080ad36ef0e72844c3e788d1923af879cbc [file] [log] [blame]
/*
* Copyright 2000-2012 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.actionSystem.DefaultActionGroup;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.vcs.*;
import com.intellij.openapi.vcs.changes.committed.DecoratorManager;
import com.intellij.openapi.vcs.changes.committed.VcsCommittedListsZipper;
import com.intellij.openapi.vcs.changes.committed.VcsCommittedViewAuxiliary;
import com.intellij.openapi.vcs.changes.committed.VcsConfigurationChangeListener;
import com.intellij.openapi.vcs.history.VcsRevisionNumber;
import com.intellij.openapi.vcs.versionBrowser.ChangeBrowserSettings;
import com.intellij.openapi.vcs.versionBrowser.ChangesBrowserSettingsEditor;
import com.intellij.openapi.vcs.versionBrowser.CommittedChangeList;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.AsynchConsumer;
import com.intellij.util.Consumer;
import com.intellij.util.PairConsumer;
import com.intellij.util.ThrowableConsumer;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.messages.MessageBusConnection;
import com.intellij.vcsUtil.VcsUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.idea.svn.SvnBundle;
import org.jetbrains.idea.svn.SvnUtil;
import org.jetbrains.idea.svn.SvnVcs;
import org.jetbrains.idea.svn.branchConfig.ConfigureBranchesAction;
import org.jetbrains.idea.svn.api.Depth;
import org.jetbrains.idea.svn.commandLine.SvnBindException;
import org.jetbrains.idea.svn.status.Status;
import org.jetbrains.idea.svn.status.StatusConsumer;
import org.jetbrains.idea.svn.status.StatusType;
import org.tmatesoft.svn.core.*;
import org.tmatesoft.svn.core.wc.SVNRevision;
import org.tmatesoft.svn.core.wc2.SvnTarget;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.File;
import java.io.IOException;
import java.util.*;
/**
* @author yole
*/
public class SvnCommittedChangesProvider implements CachingCommittedChangesProvider<SvnChangeList, ChangeBrowserSettings> {
private final static Logger LOG = Logger.getInstance(SvnCommittedChangesProvider.class);
private final Project myProject;
private final SvnVcs myVcs;
private final MessageBusConnection myConnection;
private MergeInfoUpdatesListener myMergeInfoUpdatesListener;
private final SvnCommittedListsZipper myZipper;
public final static int VERSION_WITH_COPY_PATHS_ADDED = 2;
public final static int VERSION_WITH_REPLACED_PATHS = 3;
public SvnCommittedChangesProvider(final Project project) {
myProject = project;
myVcs = SvnVcs.getInstance(myProject);
myZipper = new SvnCommittedListsZipper(myVcs);
myConnection = myProject.getMessageBus().connect();
myConnection.subscribe(VcsConfigurationChangeListener.BRANCHES_CHANGED_RESPONSE, new VcsConfigurationChangeListener.DetailedNotification() {
public void execute(final Project project, final VirtualFile vcsRoot, final List<CommittedChangeList> cachedList) {
ApplicationManager.getApplication().invokeLater(new Runnable() {
public void run() {
if (project.isDisposed()) {
return;
}
for (CommittedChangeList committedChangeList : cachedList) {
if ((committedChangeList instanceof SvnChangeList) &&
((vcsRoot == null) || (vcsRoot.equals(((SvnChangeList)committedChangeList).getVcsRoot())))) {
((SvnChangeList) committedChangeList).forceReloadCachedInfo(true);
}
}
}
});
}
});
}
@NotNull
public ChangeBrowserSettings createDefaultSettings() {
return new ChangeBrowserSettings();
}
public ChangesBrowserSettingsEditor<ChangeBrowserSettings> createFilterUI(final boolean showDateFilter) {
return new SvnVersionFilterComponent(showDateFilter);
}
@Nullable
public RepositoryLocation getLocationFor(final FilePath root) {
final String url = SvnUtil.getExactLocation(myVcs, root.getIOFile());
return url == null ? null : new SvnRepositoryLocation(url, root);
}
public RepositoryLocation getLocationFor(final FilePath root, final String repositoryPath) {
return repositoryPath == null ? getLocationFor(root) : new SvnRepositoryLocation(repositoryPath);
}
@Nullable
public VcsCommittedListsZipper getZipper() {
return myZipper;
}
public void loadCommittedChanges(ChangeBrowserSettings settings,
RepositoryLocation location,
int maxCount,
final AsynchConsumer<CommittedChangeList> consumer)
throws VcsException {
try {
final SvnRepositoryLocation svnLocation = (SvnRepositoryLocation) location;
final String repositoryRoot = getRepositoryRoot(svnLocation);
final ChangeBrowserSettings.Filter filter = settings.createFilter();
getCommittedChangesImpl(settings, svnLocation, maxCount, new Consumer<LogEntry>() {
public void consume(final LogEntry svnLogEntry) {
final SvnChangeList cl = new SvnChangeList(myVcs, svnLocation, svnLogEntry, repositoryRoot);
if (filter.accepts(cl)) {
consumer.consume(cl);
}
}
}, false, true);
}
finally {
consumer.finished();
}
}
public List<SvnChangeList> getCommittedChanges(ChangeBrowserSettings settings, final RepositoryLocation location, final int maxCount) throws VcsException {
final SvnRepositoryLocation svnLocation = (SvnRepositoryLocation) location;
final ArrayList<SvnChangeList> result = new ArrayList<SvnChangeList>();
final String repositoryRoot = getRepositoryRoot(svnLocation);
getCommittedChangesImpl(settings, svnLocation, maxCount, new Consumer<LogEntry>() {
public void consume(final LogEntry svnLogEntry) {
result.add(new SvnChangeList(myVcs, svnLocation, svnLogEntry, repositoryRoot));
}
}, false, true);
settings.filterChanges(result);
return result;
}
public void getCommittedChangesWithMergedRevisons(final ChangeBrowserSettings settings,
final RepositoryLocation location, final int maxCount,
final PairConsumer<SvnChangeList, LogHierarchyNode> finalConsumer)
throws VcsException {
final SvnRepositoryLocation svnLocation = (SvnRepositoryLocation) location;
final String repositoryRoot = getRepositoryRoot(svnLocation);
final MergeSourceHierarchyBuilder builder = new MergeSourceHierarchyBuilder(new Consumer<LogHierarchyNode>() {
public void consume(LogHierarchyNode node) {
finalConsumer.consume(new SvnChangeList(myVcs, svnLocation, node.getMe(), repositoryRoot), node);
}
});
final SvnMergeSourceTracker mergeSourceTracker = new SvnMergeSourceTracker(new ThrowableConsumer<Pair<LogEntry, Integer>, SVNException>() {
public void consume(Pair<LogEntry, Integer> svnLogEntryIntegerPair) throws SVNException {
builder.consume(svnLogEntryIntegerPair);
}
});
getCommittedChangesImpl(settings, svnLocation, maxCount, new Consumer<LogEntry>() {
public void consume(final LogEntry svnLogEntry) {
try {
mergeSourceTracker.consume(svnLogEntry);
}
catch (SVNException e) {
throw new RuntimeException(e);
// will not occur actually but anyway never eat them
}
}
}, true, false);
builder.finish();
}
private String getRepositoryRoot(@NotNull SvnRepositoryLocation svnLocation) throws VcsException {
// TODO: Additionally SvnRepositoryLocation could possibly be refactored to always contain FilePath (or similar local item)
// TODO: So here we could get repository url without performing remote svn command
SVNURL rootUrl = SvnUtil.getRepositoryRoot(myVcs, svnLocation.toSvnUrl());
if (rootUrl == null) {
throw new SvnBindException("Could not resolve repository root url for " + svnLocation);
}
return rootUrl.toDecodedString();
}
private void getCommittedChangesImpl(ChangeBrowserSettings settings, final SvnRepositoryLocation location,
final int maxCount, final Consumer<LogEntry> resultConsumer, final boolean includeMergedRevisions,
final boolean filterOutByDate) throws VcsException {
setCollectingChangesProgress(location);
String author = settings.getUserFilter();
Date dateFrom = settings.getDateAfterFilter();
Long changeFrom = settings.getChangeAfterFilter();
Date dateTo = settings.getDateBeforeFilter();
Long changeTo = settings.getChangeBeforeFilter();
SVNRevision revisionBefore = createRevision(dateTo, changeTo, SVNRevision.HEAD);
SVNRevision revisionAfter = createRevision(dateFrom, changeFrom, SVNRevision.create(1));
SvnTarget target = SvnTarget.fromURL(location.toSvnUrl(), revisionBefore);
myVcs.getFactory(target).createHistoryClient().doLog(target, revisionBefore, revisionAfter, settings.STOP_ON_COPY, true,
includeMergedRevisions, maxCount, null,
createLogHandler(resultConsumer, filterOutByDate, author));
}
@NotNull
private static SVNRevision createRevision(@Nullable Date date, @Nullable Long change, @NotNull SVNRevision defaultValue)
throws VcsException {
final SVNRevision result;
if (date != null) {
result = SVNRevision.create(date);
}
else if (change != null) {
result = SVNRevision.create(change.longValue());
}
else {
result = defaultValue;
}
return result;
}
@NotNull
private LogEntryConsumer createLogHandler(final Consumer<LogEntry> resultConsumer,
final boolean filterOutByDate,
final String author) {
return new LogEntryConsumer() {
@Override
public void consume(LogEntry logEntry) {
if (myProject.isDisposed()) throw new ProcessCanceledException();
ProgressManager.progress2(SvnBundle.message("progress.text2.processing.revision", logEntry.getRevision()));
if (filterOutByDate && logEntry.getDate() == null) {
// do not add lists without info - this situation is possible for lists where there are paths that user has no rights to observe
return;
}
if (author == null || author.equalsIgnoreCase(logEntry.getAuthor())) {
resultConsumer.consume(logEntry);
}
}
};
}
private static void setCollectingChangesProgress(@Nullable Object location) {
ProgressManager.progress(SvnBundle.message("progress.text.changes.collecting.changes"),
SvnBundle.message("progress.text2.changes.establishing.connection", location));
}
public ChangeListColumn[] getColumns() {
return new ChangeListColumn[] {
new ChangeListColumn.ChangeListNumberColumn(SvnBundle.message("revision.title")),
ChangeListColumn.NAME, ChangeListColumn.DATE, ChangeListColumn.DESCRIPTION
};
}
private void refreshMergeInfo(final RootsAndBranches action) {
if (myMergeInfoUpdatesListener == null) {
myMergeInfoUpdatesListener = new MergeInfoUpdatesListener(myProject, myConnection);
}
myMergeInfoUpdatesListener.addPanel(action);
}
@Nullable
public VcsCommittedViewAuxiliary createActions(final DecoratorManager manager, @Nullable final RepositoryLocation location) {
final RootsAndBranches rootsAndBranches = new RootsAndBranches(myProject, manager, location);
refreshMergeInfo(rootsAndBranches);
final DefaultActionGroup popup = new DefaultActionGroup(myVcs.getDisplayName(), true);
popup.add(rootsAndBranches.getIntegrateAction());
popup.add(rootsAndBranches.getUndoIntegrateAction());
popup.add(new ConfigureBranchesAction());
final ShowHideMergePanelAction action = new ShowHideMergePanelAction(manager, rootsAndBranches.getStrategy());
return new VcsCommittedViewAuxiliary(Collections.<AnAction>singletonList(popup), new Runnable() {
public void run() {
if (myMergeInfoUpdatesListener != null) {
myMergeInfoUpdatesListener.removePanel(rootsAndBranches);
rootsAndBranches.dispose();
}
}
}, Collections.<AnAction>singletonList(action));
}
public int getUnlimitedCountValue() {
return 0;
}
@Override
public Pair<SvnChangeList, FilePath> getOneList(final VirtualFile file, VcsRevisionNumber number) throws VcsException {
return new SingleCommittedListProvider(myVcs, file, number).run();
}
@Override
public RepositoryLocation getForNonLocal(VirtualFile file) {
final String url = file.getPresentableUrl();
return new SvnRepositoryLocation(FileUtil.toSystemIndependentName(url));
}
@Override
public boolean supportsIncomingChanges() {
return true;
}
public int getFormatVersion() {
return VERSION_WITH_REPLACED_PATHS;
}
public void writeChangeList(final DataOutput dataStream, final SvnChangeList list) throws IOException {
list.writeToStream(dataStream);
}
public SvnChangeList readChangeList(final RepositoryLocation location, final DataInput stream) throws IOException {
final int version = getFormatVersion();
return new SvnChangeList(myVcs, (SvnRepositoryLocation) location, stream,
VERSION_WITH_COPY_PATHS_ADDED <= version, VERSION_WITH_REPLACED_PATHS <= version);
}
public boolean isMaxCountSupported() {
return true;
}
@Nullable
public Collection<FilePath> getIncomingFiles(final RepositoryLocation location) throws VcsException {
FilePath root = null;
if (Registry.is("svn.use.incoming.optimization")) {
root = ((SvnRepositoryLocation)location).getRoot();
if (root == null) {
LOG.info("Working copy root is not provided for repository location " + location);
}
}
return root != null ? getIncomingFiles(root) : null;
}
@NotNull
private Collection<FilePath> getIncomingFiles(@NotNull FilePath root) throws SvnBindException {
// TODO: "svn diff -r BASE:HEAD --xml --summarize" command is also suitable here and outputs only necessary changed files,
// TODO: while "svn status -u" also outputs other files which could be not modified on server. But for svn 1.7 "--xml --summarize"
// TODO: could only be used with url targets - so we could not use "svn diff" here now for all cases (we could not use url with
// TODO: concrete revision as there could be mixed revision working copy).
final Set<FilePath> result = ContainerUtil.newHashSet();
File rootFile = root.getIOFile();
myVcs.getFactory(rootFile).createStatusClient()
.doStatus(rootFile, SVNRevision.UNDEFINED, Depth.INFINITY, true, false, false, false, new StatusConsumer() {
@Override
public void consume(Status status) throws SVNException {
File file = status.getFile();
boolean changedOnServer = isNotNone(status.getRemoteContentsStatus()) ||
isNotNone(status.getRemoteNodeStatus()) ||
isNotNone(status.getRemotePropertiesStatus());
if (file != null && changedOnServer) {
result.add(VcsUtil.getFilePath(file, file.isDirectory()));
}
}
}, null);
return result;
}
private static boolean isNotNone(@Nullable StatusType status) {
return status != null && !StatusType.STATUS_NONE.equals(status);
}
public boolean refreshCacheByNumber() {
return true;
}
public String getChangelistTitle() {
return SvnBundle.message("changes.browser.revision.term");
}
public boolean isChangeLocallyAvailable(FilePath filePath, @Nullable VcsRevisionNumber localRevision, VcsRevisionNumber changeRevision,
final SvnChangeList changeList) {
return localRevision != null && localRevision.compareTo(changeRevision) >= 0;
}
public boolean refreshIncomingWithCommitted() {
return true;
}
public void deactivate() {
myConnection.disconnect();
}
}