blob: 1fc5f9ce075a58ac952e956fc1cef8835b93bc92 [file] [log] [blame]
/*
* 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;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.vcs.*;
import com.intellij.openapi.vcs.changes.*;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.idea.svn.actions.AbstractShowPropertiesDiffAction;
import org.jetbrains.idea.svn.branchConfig.SvnBranchConfigurationManager;
import org.jetbrains.idea.svn.info.Info;
import org.jetbrains.idea.svn.lock.Lock;
import org.jetbrains.idea.svn.status.Status;
import org.jetbrains.idea.svn.status.StatusType;
import org.tmatesoft.svn.core.SVNException;
import org.tmatesoft.svn.core.SVNURL;
import org.tmatesoft.svn.core.wc.SVNRevision;
import java.io.File;
import java.util.*;
class SvnChangeProviderContext implements StatusReceiver {
private static final Logger LOG = Logger.getInstance("org.jetbrains.idea.svn.SvnChangeProviderContext");
private final ChangelistBuilder myChangelistBuilder;
private List<SvnChangedFile> myCopiedFiles = null;
private final List<SvnChangedFile> myDeletedFiles = new ArrayList<SvnChangedFile>();
// for files moved in a subtree, which were the targets of merge (for instance).
private final Map<String, Status> myTreeConflicted;
private Map<FilePath, String> myCopyFromURLs = null;
private final SvnVcs myVcs;
private final SvnBranchConfigurationManager myBranchConfigurationManager;
private final List<File> filesToRefresh = ContainerUtil.newArrayList();
private final ProgressIndicator myProgress;
public SvnChangeProviderContext(SvnVcs vcs, final ChangelistBuilder changelistBuilder, final ProgressIndicator progress) {
myVcs = vcs;
myChangelistBuilder = changelistBuilder;
myProgress = progress;
myTreeConflicted = new HashMap<String, Status>();
myBranchConfigurationManager = SvnBranchConfigurationManager.getInstance(myVcs.getProject());
}
public void process(FilePath path, Status status) throws SVNException {
processStatusFirstPass(path, status);
}
public void processIgnored(VirtualFile vFile) {
myChangelistBuilder.processIgnoredFile(vFile);
}
public void processUnversioned(VirtualFile vFile) {
myChangelistBuilder.processUnversionedFile(vFile);
}
@Override
public void processCopyRoot(VirtualFile file, SVNURL url, WorkingCopyFormat format, SVNURL rootURL) {
}
@Override
public void bewareRoot(VirtualFile vf, SVNURL url) {
}
@Override
public void finish() {
LocalFileSystem.getInstance().refreshIoFiles(filesToRefresh, true, false, null);
}
public ChangelistBuilder getBuilder() {
return myChangelistBuilder;
}
public void reportTreeConflict(final Status status) {
myTreeConflicted.put(status.getFile().getAbsolutePath(), status);
}
@Nullable
public Status getTreeConflictStatus(final File file) {
return myTreeConflicted.get(file.getAbsolutePath());
}
@NotNull
public List<SvnChangedFile> getCopiedFiles() {
if (myCopiedFiles == null) {
return Collections.emptyList();
}
return myCopiedFiles;
}
public List<SvnChangedFile> getDeletedFiles() {
return myDeletedFiles;
}
public boolean isDeleted(final FilePath path) {
for (SvnChangedFile deletedFile : myDeletedFiles) {
if (Comparing.equal(path, deletedFile.getFilePath())) {
return true;
}
}
return false;
}
public boolean isCanceled() {
return (myProgress != null) && myProgress.isCanceled();
}
/**
* If the specified filepath or its parent was added with history, returns the URL of the copy source for this filepath.
*
* @param filePath the original filepath
* @return the copy source url, or null if the file isn't a copy of anything
*/
@Nullable
public String getParentCopyFromURL(FilePath filePath) {
if (myCopyFromURLs == null) {
return null;
}
StringBuilder relPathBuilder = new StringBuilder();
while (filePath != null) {
String copyFromURL = myCopyFromURLs.get(filePath);
if (copyFromURL != null) {
return copyFromURL + relPathBuilder.toString();
}
relPathBuilder.insert(0, "/" + filePath.getName());
filePath = filePath.getParentPath();
}
return null;
}
public void addCopiedFile(final FilePath filePath, final Status status, final String copyFromURL) {
if (myCopiedFiles == null) {
myCopiedFiles = new ArrayList<SvnChangedFile>();
}
myCopiedFiles.add(new SvnChangedFile(filePath, status, copyFromURL));
final String url = status.getCopyFromURL();
if (url != null) {
addCopyFromURL(filePath, url);
}
}
public void addCopyFromURL(final FilePath filePath, final String url) {
if (myCopyFromURLs == null) {
myCopyFromURLs = new HashMap<FilePath, String>();
}
myCopyFromURLs.put(filePath, url);
}
//
void processStatusFirstPass(final FilePath filePath, final Status status) throws SVNException {
if (status == null) {
// external to wc
return;
}
if (status.getRemoteLock() != null) {
final Lock lock = status.getRemoteLock();
myChangelistBuilder.processLogicallyLockedFolder(filePath.getVirtualFile(),
new LogicalLock(false, lock.getOwner(), lock.getComment(), lock.getCreationDate(),
lock.getExpirationDate()));
}
if (status.getLocalLock() != null) {
final Lock lock = status.getLocalLock();
myChangelistBuilder.processLogicallyLockedFolder(filePath.getVirtualFile(),
new LogicalLock(true, lock.getOwner(), lock.getComment(), lock.getCreationDate(),
lock.getExpirationDate()));
}
if (filePath.isDirectory() && status.isLocked()) {
myChangelistBuilder.processLockedFolder(filePath.getVirtualFile());
}
if ((status.is(StatusType.STATUS_ADDED) || StatusType.STATUS_MODIFIED.equals(status.getNodeStatus())) &&
status.getCopyFromURL() != null) {
addCopiedFile(filePath, status, status.getCopyFromURL());
}
else if (status.is(StatusType.STATUS_DELETED)) {
myDeletedFiles.add(new SvnChangedFile(filePath, status));
}
else {
String parentCopyFromURL = getParentCopyFromURL(filePath);
if (parentCopyFromURL != null) {
addCopiedFile(filePath, status, parentCopyFromURL);
}
else {
processStatus(filePath, status);
}
}
}
void processStatus(final FilePath filePath, final Status status) throws SVNException {
WorkingCopyFormat format = myVcs.getWorkingCopyFormat(filePath.getIOFile());
if (!WorkingCopyFormat.UNKNOWN.equals(format) && format.less(WorkingCopyFormat.ONE_DOT_SEVEN)) {
loadEntriesFile(filePath);
}
if (status != null) {
FileStatus fStatus = SvnStatusConvertor.convertStatus(status);
final StatusType statusType = status.getContentsStatus();
final StatusType propStatus = status.getPropertiesStatus();
if (status.is(StatusType.STATUS_UNVERSIONED, StatusType.UNKNOWN)) {
final VirtualFile file = filePath.getVirtualFile();
if (file != null) {
myChangelistBuilder.processUnversionedFile(file);
}
}
else if (status.is(StatusType.STATUS_ADDED)) {
myChangelistBuilder.processChangeInList(createChange(null, CurrentContentRevision.create(filePath), fStatus, status),
SvnUtil.getChangelistName(status), SvnVcs.getKey());
}
else if (status.is(StatusType.STATUS_CONFLICTED, StatusType.STATUS_MODIFIED, StatusType.STATUS_REPLACED) ||
propStatus == StatusType.STATUS_MODIFIED ||
propStatus == StatusType.STATUS_CONFLICTED) {
myChangelistBuilder.processChangeInList(
createChange(SvnContentRevision.createBaseRevision(myVcs, filePath, status), CurrentContentRevision.create(filePath), fStatus,
status), SvnUtil.getChangelistName(status), SvnVcs.getKey()
);
checkSwitched(filePath, myChangelistBuilder, status, fStatus);
}
else if (status.is(StatusType.STATUS_DELETED)) {
myChangelistBuilder.processChangeInList(
createChange(SvnContentRevision.createBaseRevision(myVcs, filePath, status), null, fStatus, status),
SvnUtil.getChangelistName(status), SvnVcs.getKey());
}
else if (status.is(StatusType.STATUS_MISSING)) {
myChangelistBuilder.processLocallyDeletedFile(createLocallyDeletedChange(filePath, status));
}
else if (status.is(StatusType.STATUS_IGNORED)) {
if (filePath.getVirtualFile() == null) {
filePath.hardRefresh();
}
if (filePath.getVirtualFile() == null) {
LOG.error("No virtual file for ignored file: " + filePath.getPresentableUrl() + ", isNonLocal: " + filePath.isNonLocal());
}
else if (!myVcs.isWcRoot(filePath)) {
myChangelistBuilder.processIgnoredFile(filePath.getVirtualFile());
}
}
else if (status.isCopied()) {
//
}
else if ((fStatus == FileStatus.NOT_CHANGED || fStatus == FileStatus.SWITCHED) && statusType != StatusType.STATUS_NONE) {
VirtualFile file = filePath.getVirtualFile();
if (file != null && FileDocumentManager.getInstance().isFileModified(file)) {
myChangelistBuilder.processChangeInList(
createChange(SvnContentRevision.createBaseRevision(myVcs, filePath, status), CurrentContentRevision.create(filePath),
FileStatus.MODIFIED, status), SvnUtil.getChangelistName(status), SvnVcs.getKey()
);
}
else if (status.getTreeConflict() != null) {
myChangelistBuilder.processChange(createChange(SvnContentRevision.createBaseRevision(myVcs, filePath, status),
CurrentContentRevision.create(filePath), FileStatus.MODIFIED, status),
SvnVcs.getKey());
}
checkSwitched(filePath, myChangelistBuilder, status, fStatus);
}
}
}
public void addModifiedNotSavedChange(final VirtualFile file) throws SVNException {
final FilePath filePath = new FilePathImpl(file);
final Info svnInfo = myVcs.getInfo(file);
if (svnInfo != null) {
final Status svnStatus = new Status();
svnStatus.setRevision(svnInfo.getRevision());
myChangelistBuilder.processChangeInList(
createChange(SvnContentRevision.createBaseRevision(myVcs, filePath, svnInfo.getRevision()), CurrentContentRevision.create(filePath),
FileStatus.MODIFIED, svnStatus), (String)null, SvnVcs.getKey()
);
}
}
private void checkSwitched(final FilePath filePath, final ChangelistBuilder builder, final Status status,
final FileStatus convertedStatus) {
if (status.isSwitched() || (convertedStatus == FileStatus.SWITCHED)) {
final VirtualFile virtualFile = filePath.getVirtualFile();
if (virtualFile == null) return;
final String switchUrl = status.getURL().toString();
final VirtualFile vcsRoot = ProjectLevelVcsManager.getInstance(myVcs.getProject()).getVcsRootFor(virtualFile);
if (vcsRoot != null) { // it will be null if we walked into an excluded directory
String baseUrl = null;
try {
baseUrl = myBranchConfigurationManager.get(vcsRoot).getBaseName(switchUrl);
}
catch (VcsException e) {
LOG.info(e);
}
builder.processSwitchedFile(virtualFile, baseUrl == null ? switchUrl : baseUrl, true);
}
}
}
/**
* Ensures that the contents of the 'entries' file is cached in the VFS, so that the VFS will send
* correct events when the 'entries' file is changed externally (to be received by SvnEntriesFileListener)
*
* @param filePath the path of a changed file.
*/
private void loadEntriesFile(final FilePath filePath) {
final FilePath parentPath = filePath.getParentPath();
if (parentPath == null) {
return;
}
refreshDotSvnAndEntries(parentPath);
if (filePath.isDirectory()) {
refreshDotSvnAndEntries(filePath);
}
}
private void refreshDotSvnAndEntries(FilePath filePath) {
final File svn = new File(filePath.getPath(), SvnUtil.SVN_ADMIN_DIR_NAME);
filesToRefresh.add(svn);
filesToRefresh.add(new File(svn, SvnUtil.ENTRIES_FILE_NAME));
}
// seems here we can only have a tree conflict; which can be marked on either path (?)
// .. ok try to merge states
Change createMovedChange(final ContentRevision before, final ContentRevision after, final Status copiedStatus,
final Status deletedStatus) throws SVNException {
// todo no convertion needed for the contents status?
final ConflictedSvnChange conflictedSvnChange =
new ConflictedSvnChange(before, after, ConflictState.mergeState(getState(copiedStatus), getState(deletedStatus)),
((copiedStatus != null) && (copiedStatus.getTreeConflict() != null)) ? after.getFile() : before.getFile());
if (deletedStatus != null) {
conflictedSvnChange.setBeforeDescription(deletedStatus.getTreeConflict());
}
if (copiedStatus != null) {
conflictedSvnChange.setAfterDescription(copiedStatus.getTreeConflict());
}
return patchWithPropertyChange(conflictedSvnChange, copiedStatus, deletedStatus);
}
private Change createChange(final ContentRevision before,
final ContentRevision after,
final FileStatus fStatus,
final Status svnStatus)
throws SVNException {
final ConflictedSvnChange conflictedSvnChange = new ConflictedSvnChange(before, after, correctContentsStatus(fStatus, svnStatus),
getState(svnStatus),
after == null ? before.getFile() : after.getFile());
if (svnStatus != null) {
if (StatusType.STATUS_DELETED.equals(svnStatus.getNodeStatus()) && !svnStatus.getRevision().isValid()) {
conflictedSvnChange.setIsPhantom(true);
}
conflictedSvnChange.setBeforeDescription(svnStatus.getTreeConflict());
}
return patchWithPropertyChange(conflictedSvnChange, svnStatus, null);
}
private FileStatus correctContentsStatus(final FileStatus fs, final Status svnStatus) throws SVNException {
//if (svnStatus.isSwitched()) return FileStatus.SWITCHED;
return fs;
//return SvnStatusConvertor.convertContentsStatus(svnStatus);
}
private LocallyDeletedChange createLocallyDeletedChange(@NotNull FilePath filePath, final Status status) {
return new SvnLocallyDeletedChange(filePath, getState(status));
}
private Change patchWithPropertyChange(final Change change, final Status svnStatus, final Status deletedStatus)
throws SVNException {
if (svnStatus == null) return change;
final StatusType propertiesStatus = svnStatus.getPropertiesStatus();
if (StatusType.STATUS_CONFLICTED.equals(propertiesStatus) || StatusType.CHANGED.equals(propertiesStatus) ||
StatusType.STATUS_ADDED.equals(propertiesStatus) || StatusType.STATUS_DELETED.equals(propertiesStatus) ||
StatusType.STATUS_MODIFIED.equals(propertiesStatus) || StatusType.STATUS_REPLACED.equals(propertiesStatus) ||
StatusType.MERGED.equals(propertiesStatus)) {
final FilePath path = ChangesUtil.getFilePath(change);
final File ioFile = path.getIOFile();
final File beforeFile = deletedStatus != null ? deletedStatus.getFile() : ioFile;
final String beforeList = StatusType.STATUS_ADDED.equals(propertiesStatus) && deletedStatus == null ? null :
AbstractShowPropertiesDiffAction.getPropertyList(myVcs, beforeFile, SVNRevision.BASE);
final String afterList = StatusType.STATUS_DELETED.equals(propertiesStatus) ? null :
AbstractShowPropertiesDiffAction.getPropertyList(myVcs, ioFile, SVNRevision.WORKING);
// TODO: There are cases when status output is like (on newly added file with some properties that is locally deleted)
// <entry path="some_path"> <wc-status item="missing" revision="-1" props="modified"> </wc-status> </entry>
// TODO: For such cases in current logic we'll have Change with before revision containing SVNRevision.UNDEFINED
// TODO: Analyze if this logic is OK or we should update flow somehow (for instance, to have null before revision)
final String beforeRevisionNu = change.getBeforeRevision() == null ? null : change.getBeforeRevision().getRevisionNumber().asString();
final String afterRevisionNu = change.getAfterRevision() == null ? null : change.getAfterRevision().getRevisionNumber().asString();
final Change propertyChange = new Change(beforeList == null ? null : new SimpleContentRevision(beforeList, path, beforeRevisionNu),
afterList == null ? null : new SimpleContentRevision(afterList, path, afterRevisionNu),
deletedStatus != null
? FileStatus.MODIFIED
: SvnStatusConvertor.convertPropertyStatus(propertiesStatus));
change.addAdditionalLayerElement(SvnChangeProvider.PROPERTY_LAYER, propertyChange);
}
return change;
}
private ConflictState getState(@Nullable final Status svnStatus) {
if (svnStatus == null) {
return ConflictState.none;
}
final StatusType propertiesStatus = svnStatus.getPropertiesStatus();
final boolean treeConflict = svnStatus.getTreeConflict() != null;
final boolean textConflict = StatusType.STATUS_CONFLICTED == svnStatus.getContentsStatus();
final boolean propertyConflict = StatusType.STATUS_CONFLICTED == propertiesStatus;
if (treeConflict) {
reportTreeConflict(svnStatus);
}
return ConflictState.getInstance(treeConflict, textConflict, propertyConflict);
}
}