blob: 41461ed623c665670b829536b7ccde13c616b8fa [file] [log] [blame]
/*
* Copyright 2000-2013 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.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vcs.*;
import com.intellij.openapi.vcs.actions.VcsContextFactory;
import com.intellij.openapi.vcs.changes.*;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.EventDispatcher;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.idea.svn.actions.CleanupWorker;
import org.jetbrains.idea.svn.api.Depth;
import org.jetbrains.idea.svn.commandLine.SvnBindException;
import org.jetbrains.idea.svn.commandLine.SvnExceptionWrapper;
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.internal.util.SVNPathUtil;
import org.tmatesoft.svn.core.wc.ISVNStatusFileProvider;
import java.io.File;
import java.util.*;
/**
* @author max
* @author yole
*/
public class SvnChangeProvider implements ChangeProvider {
private static final Logger LOG = Logger.getInstance("#org.jetbrains.idea.svn.SvnChangeProvider");
public static final String ourDefaultListName = VcsBundle.message("changes.default.changelist.name");
public static final String PROPERTY_LAYER = "Property";
private final SvnVcs myVcs;
private final VcsContextFactory myFactory;
private final SvnFileUrlMappingImpl mySvnFileUrlMapping;
public SvnChangeProvider(final SvnVcs vcs) {
myVcs = vcs;
myFactory = VcsContextFactory.SERVICE.getInstance();
mySvnFileUrlMapping = (SvnFileUrlMappingImpl) vcs.getSvnFileUrlMapping();
}
public void getChanges(final VcsDirtyScope dirtyScope, final ChangelistBuilder builder, ProgressIndicator progress,
final ChangeListManagerGate addGate) throws VcsException {
final SvnScopeZipper zipper = new SvnScopeZipper(dirtyScope);
zipper.run();
final Map<String, SvnScopeZipper.MyDirNonRecursive> nonRecursiveMap = zipper.getNonRecursiveDirs();
final ISVNStatusFileProvider fileProvider = createFileProvider(nonRecursiveMap);
try {
final SvnChangeProviderContext context = new SvnChangeProviderContext(myVcs, builder, progress);
final StatusWalkerPartner partner = new StatusWalkerPartner(myVcs, progress);
final NestedCopiesBuilder nestedCopiesBuilder = new NestedCopiesBuilder(myVcs, mySvnFileUrlMapping);
final EventDispatcher<StatusReceiver> statusReceiver = EventDispatcher.create(StatusReceiver.class);
statusReceiver.addListener(context);
statusReceiver.addListener(nestedCopiesBuilder);
final SvnRecursiveStatusWalker walker = new SvnRecursiveStatusWalker(myVcs, statusReceiver.getMulticaster(), partner);
for (FilePath path : zipper.getRecursiveDirs()) {
walker.go(path, Depth.INFINITY);
}
partner.setFileProvider(fileProvider);
for (SvnScopeZipper.MyDirNonRecursive item : nonRecursiveMap.values()) {
walker.go(item.getDir(), Depth.IMMEDIATES);
}
statusReceiver.getMulticaster().finish();
processCopiedAndDeleted(context, dirtyScope);
processUnsaved(dirtyScope, addGate, context);
final Set<NestedCopyInfo> nestedCopies = nestedCopiesBuilder.getCopies();
mySvnFileUrlMapping.acceptNestedData(nestedCopies);
putAdministrative17UnderVfsListener(nestedCopies);
} catch (SvnExceptionWrapper e) {
LOG.info(e);
throw new VcsException(e.getCause());
} catch (SVNException e) {
if (e.getCause() != null) {
throw new VcsException(e.getMessage() + " " + e.getCause().getMessage(), e);
}
throw new VcsException(e);
}
}
/**
* TODO: Currently could not find exact case when "file status is not correctly refreshed after external commit" that is covered by this
* TODO: code. So for now, checks for formats greater than 1.7 are not added here.
*/
private static void putAdministrative17UnderVfsListener(Set<NestedCopyInfo> pointInfos) {
if (! SvnVcs.ourListenToWcDb) return;
final LocalFileSystem lfs = LocalFileSystem.getInstance();
for (NestedCopyInfo info : pointInfos) {
if (WorkingCopyFormat.ONE_DOT_SEVEN.equals(info.getFormat()) && ! NestedCopyType.switched.equals(info.getType())) {
final VirtualFile root = info.getFile();
lfs.refreshIoFiles(Collections.singletonList(SvnUtil.getWcDb(new File(root.getPath()))), true, false, null);
}
}
}
private void processUnsaved(VcsDirtyScope dirtyScope, ChangeListManagerGate addGate, SvnChangeProviderContext context)
throws SVNException {
final FileDocumentManager fileDocumentManager = FileDocumentManager.getInstance();
final Document[] unsavedDocuments = fileDocumentManager.getUnsavedDocuments();
for (Document unsavedDocument : unsavedDocuments) {
final VirtualFile file = fileDocumentManager.getFile(unsavedDocument);
if (file != null && dirtyScope.belongsTo(new FilePathImpl(file)) && fileDocumentManager.isFileModified(file)) {
final FileStatus status = addGate.getStatus(file);
if (status == null || FileStatus.NOT_CHANGED.equals(status)) {
context.addModifiedNotSavedChange(file);
}
}
}
}
private ISVNStatusFileProvider createFileProvider(Map<String, SvnScopeZipper.MyDirNonRecursive> nonRecursiveMap) {
// translate into terms of File.getAbsolutePath()
final Map<String, Map> preparedMap = new HashMap<String, Map>();
for (SvnScopeZipper.MyDirNonRecursive item : nonRecursiveMap.values()) {
final Map result = new HashMap();
for (FilePath path : item.getChildrenList()) {
result.put(path.getName(), path.getIOFile());
}
preparedMap.put(item.getDir().getIOFile().getAbsolutePath(), result);
}
return new ISVNStatusFileProvider() {
public Map getChildrenFiles(File parent) {
return preparedMap.get(parent.getAbsolutePath());
}
};
}
private void processCopiedAndDeleted(final SvnChangeProviderContext context, final VcsDirtyScope dirtyScope) throws SVNException {
for(SvnChangedFile copiedFile: context.getCopiedFiles()) {
if (context.isCanceled()) {
throw new ProcessCanceledException();
}
processCopiedFile(copiedFile, context.getBuilder(), context, dirtyScope);
}
for(SvnChangedFile deletedFile: context.getDeletedFiles()) {
if (context.isCanceled()) {
throw new ProcessCanceledException();
}
context.processStatus(deletedFile.getFilePath(), deletedFile.getStatus());
}
}
public void getChanges(final FilePath path, final boolean recursive, final ChangelistBuilder builder)
throws SVNException, SvnBindException {
final SvnChangeProviderContext context = new SvnChangeProviderContext(myVcs, builder, null);
final StatusWalkerPartner partner = new StatusWalkerPartner(myVcs, ProgressManager.getInstance().getProgressIndicator());
final SvnRecursiveStatusWalker walker = new SvnRecursiveStatusWalker(myVcs, context, partner);
walker.go(path, recursive ? Depth.INFINITY : Depth.IMMEDIATES);
processCopiedAndDeleted(context, null);
}
private void processCopiedFile(SvnChangedFile copiedFile,
ChangelistBuilder builder,
SvnChangeProviderContext context, final VcsDirtyScope dirtyScope) throws SVNException {
boolean foundRename = false;
final Status copiedStatus = copiedFile.getStatus();
final String copyFromURL = copiedFile.getCopyFromURL();
final FilePath copiedToPath = copiedFile.getFilePath();
// if copy target is _deleted_, treat like deleted, not moved!
/*for (Iterator<SvnChangedFile> iterator = context.getDeletedFiles().iterator(); iterator.hasNext();) {
final SvnChangedFile deletedFile = iterator.next();
final FilePath deletedPath = deletedFile.getFilePath();
if (Comparing.equal(deletedPath, copiedToPath)) {
return;
}
}*/
final Set<SvnChangedFile> deletedToDelete = new HashSet<SvnChangedFile>();
for (Iterator<SvnChangedFile> iterator = context.getDeletedFiles().iterator(); iterator.hasNext();) {
SvnChangedFile deletedFile = iterator.next();
final Status deletedStatus = deletedFile.getStatus();
if ((deletedStatus != null) && (deletedStatus.getURL() != null) && Comparing.equal(copyFromURL, deletedStatus.getURL().toString())) {
final String clName = SvnUtil.getChangelistName(copiedFile.getStatus());
final Change newChange = context.createMovedChange(createBeforeRevision(deletedFile, true),
CurrentContentRevision.create(copiedFile.getFilePath()), copiedStatus,
deletedStatus);
applyMovedChange(copiedFile.getFilePath(), builder, dirtyScope, deletedToDelete, deletedFile, clName, newChange);
for(Iterator<SvnChangedFile> iterChild = context.getDeletedFiles().iterator(); iterChild.hasNext();) {
SvnChangedFile deletedChild = iterChild.next();
final Status childStatus = deletedChild.getStatus();
if (childStatus == null) {
continue;
}
final SVNURL childUrl = childStatus.getURL();
if (childUrl == null) {
continue;
}
final String childURL = childUrl.toDecodedString();
if (StringUtil.startsWithConcatenation(childURL, copyFromURL, "/")) {
String relativePath = childURL.substring(copyFromURL.length());
File newPath = new File(copiedFile.getFilePath().getIOFile(), relativePath);
FilePath newFilePath = myFactory.createFilePathOn(newPath);
if (!context.isDeleted(newFilePath)) {
final Change movedChange = context.createMovedChange(createBeforeRevision(deletedChild, true),
CurrentContentRevision.create(newFilePath),
context.getTreeConflictStatus(newPath), childStatus);
applyMovedChange(newFilePath, builder, dirtyScope, deletedToDelete, deletedChild, clName, movedChange);
}
}
}
foundRename = true;
break;
}
}
final List<SvnChangedFile> deletedFiles = context.getDeletedFiles();
for (SvnChangedFile file : deletedToDelete) {
deletedFiles.remove(file);
}
// handle the case when the deleted file wasn't included in the dirty scope - try searching for the local copy
// by building a relative url
if (!foundRename && copiedStatus.getURL() != null) {
File wcPath = guessWorkingCopyPath(copiedStatus.getFile(), copiedStatus.getURL(), copyFromURL);
Status status;
try {
status = myVcs.getFactory(wcPath).createStatusClient().doStatus(wcPath, false);
}
catch(SvnBindException ex) {
LOG.info(ex);
status = null;
}
if (status != null && status.is(StatusType.STATUS_DELETED)) {
final FilePath filePath = myFactory.createFilePathOnDeleted(wcPath, false);
final SvnContentRevision beforeRevision = SvnContentRevision.createBaseRevision(myVcs, filePath, status.getRevision());
final ContentRevision afterRevision = CurrentContentRevision.create(copiedFile.getFilePath());
builder.processChangeInList(context.createMovedChange(beforeRevision, afterRevision, copiedStatus, status),
SvnUtil.getChangelistName(status), SvnVcs.getKey());
foundRename = true;
}
}
if (!foundRename) {
// for debug
LOG.info("Rename not found for " + copiedFile.getFilePath().getPresentableUrl());
context.processStatus(copiedFile.getFilePath(), copiedStatus);
}
}
private void applyMovedChange(final FilePath oldPath,
ChangelistBuilder builder,
final VcsDirtyScope dirtyScope,
Set<SvnChangedFile> deletedToDelete, SvnChangedFile deletedFile, String clName, final Change newChange) {
final boolean isUnder = dirtyScope == null ? true : ApplicationManager.getApplication().runReadAction(new Computable<Boolean>() {
@Override
public Boolean compute() {
return ChangeListManagerImpl.isUnder(newChange, dirtyScope);
}
});
if (isUnder) {
builder.removeRegisteredChangeFor(oldPath);
builder.processChangeInList(newChange, clName, SvnVcs.getKey());
deletedToDelete.add(deletedFile);
}
}
private SvnContentRevision createBeforeRevision(final SvnChangedFile changedFile, final boolean forDeleted) {
return SvnContentRevision.createBaseRevision(myVcs,
forDeleted ? FilePathImpl.createForDeletedFile(changedFile.getStatus().getFile(),
changedFile.getFilePath().isDirectory()) :
changedFile.getFilePath(), changedFile.getStatus().getRevision());
}
private static File guessWorkingCopyPath(final File file, @NotNull final SVNURL url, final String copyFromURL) throws SVNException {
String copiedPath = url.getPath();
String copyFromPath = SVNURL.parseURIEncoded(copyFromURL).getPath();
String commonPathAncestor = SVNPathUtil.getCommonPathAncestor(copiedPath, copyFromPath);
int pathSegmentCount = SVNPathUtil.getSegmentsCount(copiedPath);
int ancestorSegmentCount = SVNPathUtil.getSegmentsCount(commonPathAncestor);
boolean startsWithSlash = file.getAbsolutePath().startsWith("/");
List<String> segments = StringUtil.split(file.getPath(), File.separator);
List<String> copyFromPathSegments = StringUtil.split(copyFromPath, "/");
List<String> resultSegments = new ArrayList<String>();
final int keepSegments = segments.size() - pathSegmentCount + ancestorSegmentCount;
for(int i=0; i< keepSegments; i++) {
resultSegments.add(segments.get(i));
}
for(int i=ancestorSegmentCount; i<copyFromPathSegments.size(); i++) {
resultSegments.add(copyFromPathSegments.get(i));
}
String result = StringUtil.join(resultSegments, "/");
if (startsWithSlash) {
result = "/" + result;
}
return new File(result);
}
public boolean isModifiedDocumentTrackingRequired() {
return true;
}
public void doCleanup(final List<VirtualFile> files) {
new CleanupWorker(VfsUtil.toVirtualFileArray(files), myVcs.getProject(), "action.Subversion.cleanup.progress.title").execute();
}
}