/* * 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 myCopiedFiles = null; private final List myDeletedFiles = new ArrayList(); // for files moved in a subtree, which were the targets of merge (for instance). private final Map myTreeConflicted; private Map myCopyFromURLs = null; private final SvnVcs myVcs; private final SvnBranchConfigurationManager myBranchConfigurationManager; private final List 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(); 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 getCopiedFiles() { if (myCopiedFiles == null) { return Collections.emptyList(); } return myCopiedFiles; } public List 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(); } 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(); } 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) // // 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); } }