blob: 16904b673b5f3b9bcb786123178926b3ffa5ea76 [file] [log] [blame]
/*
* Copyright 2000-2014 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.rollback;
import com.intellij.openapi.util.Couple;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vcs.*;
import com.intellij.openapi.vcs.changes.*;
import com.intellij.openapi.vcs.rollback.DefaultRollbackEnvironment;
import com.intellij.openapi.vcs.rollback.RollbackProgressListener;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.Processor;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.idea.svn.*;
import org.jetbrains.idea.svn.api.Depth;
import org.jetbrains.idea.svn.api.EventAction;
import org.jetbrains.idea.svn.api.ProgressEvent;
import org.jetbrains.idea.svn.api.ProgressTracker;
import org.jetbrains.idea.svn.commandLine.SvnBindException;
import org.jetbrains.idea.svn.info.Info;
import org.jetbrains.idea.svn.properties.PropertiesMap;
import org.jetbrains.idea.svn.properties.PropertyConsumer;
import org.jetbrains.idea.svn.properties.PropertyData;
import org.tmatesoft.svn.core.SVNErrorCode;
import org.tmatesoft.svn.core.SVNException;
import org.tmatesoft.svn.core.SVNURL;
import org.tmatesoft.svn.core.wc.SVNRevision;
import org.tmatesoft.svn.core.wc2.SvnTarget;
import java.io.File;
import java.io.IOException;
import java.util.*;
/**
* @author yole
*/
public class SvnRollbackEnvironment extends DefaultRollbackEnvironment {
private final SvnVcs mySvnVcs;
public SvnRollbackEnvironment(SvnVcs svnVcs) {
mySvnVcs = svnVcs;
}
@Override
public String getRollbackOperationName() {
return SvnBundle.message("action.name.revert");
}
public void rollbackChanges(List<Change> changes, final List<VcsException> exceptions, @NotNull final RollbackProgressListener listener) {
listener.indeterminate();
final SvnChangeProvider changeProvider = (SvnChangeProvider) mySvnVcs.getChangeProvider();
final Collection<List<Change>> collections = SvnUtil.splitChangesIntoWc(mySvnVcs, changes);
for (List<Change> collection : collections) {
// to be more sure about nested changes, being or being not reverted
final List<Change> innerChanges = new ArrayList<Change>(collection);
Collections.sort(innerChanges, ChangesAfterPathComparator.getInstance());
//for (Change change : innerChanges) {
rollbackGroupForWc(innerChanges, exceptions, listener, changeProvider);
//}
}
}
private void rollbackGroupForWc(List<Change> changes,
final List<VcsException> exceptions,
final RollbackProgressListener listener,
SvnChangeProvider changeProvider) {
final UnversionedAndNotTouchedFilesGroupCollector collector = new UnversionedAndNotTouchedFilesGroupCollector();
final ChangesChecker checker = new ChangesChecker(changeProvider, collector);
checker.gather(changes);
exceptions.addAll(checker.getExceptions());
ProgressTracker revertHandler = new ProgressTracker() {
public void consume(ProgressEvent event) {
if (event.getAction() == EventAction.REVERT) {
final File file = event.getFile();
if (file != null) {
listener.accept(file);
}
}
if (event.getAction() == EventAction.FAILED_REVERT) {
exceptions.add(new VcsException("Revert failed"));
}
}
public void checkCancelled() {
listener.checkCanceled();
}
};
final List<CopiedAsideInfo> fromToModified = new ArrayList<CopiedAsideInfo>();
final Map<File, PropertiesMap> properties = ContainerUtil.newHashMap();
moveRenamesToTmp(exceptions, fromToModified, properties, collector);
// adds (deletes)
// deletes (adds)
// modifications
final Reverter reverter = new Reverter(mySvnVcs, revertHandler, exceptions);
reverter.revert(checker.getForAdds(), true);
reverter.revert(checker.getForDeletes(), true);
final List<File> edits = checker.getForEdits();
reverter.revert(edits.toArray(new File[edits.size()]), false);
moveGroup(exceptions, fromToModified, properties);
final List<Couple<File>> toBeDeleted = collector.getToBeDeleted();
for (Couple<File> pair : toBeDeleted) {
if (pair.getFirst().exists()) {
FileUtil.delete(pair.getSecond());
}
}
}
private void moveRenamesToTmp(List<VcsException> exceptions,
List<CopiedAsideInfo> fromToModified,
final Map<File, PropertiesMap> properties,
final UnversionedAndNotTouchedFilesGroupCollector collector) {
final Map<File, ThroughRenameInfo> fromTo = collector.getFromTo();
try {
final File tmp = FileUtil.createTempDirectory("forRename", "");
final PropertyConsumer handler = new PropertyConsumer() {
@Override
public void handleProperty(File path, PropertyData property) throws SVNException {
final ThroughRenameInfo info = collector.findToFile(new FilePathImpl(path, path.isDirectory()), null);
if (info != null) {
if (!properties.containsKey(info.getTo())) {
properties.put(info.getTo(), new PropertiesMap());
}
properties.get(info.getTo()).put(property.getName(), property.getValue());
}
}
@Override
public void handleProperty(SVNURL url, PropertyData property) throws SVNException {
}
@Override
public void handleProperty(long revision, PropertyData property) throws SVNException {
}
};
// copy also directories here - for moving with svn
// also, maybe still use just patching? -> well-tested thing, only deletion of folders might suffer
// todo: special case: addition + move. mark it
for (Map.Entry<File, ThroughRenameInfo> entry : fromTo.entrySet()) {
final File source = entry.getKey();
final ThroughRenameInfo info = entry.getValue();
if (info.isVersioned()) {
mySvnVcs.getFactory(source).createPropertyClient().list(SvnTarget.fromFile(source), SVNRevision.WORKING, Depth.EMPTY, handler);
}
if (source.isDirectory()) {
if (! FileUtil.filesEqual(info.getTo(), info.getFirstTo())) {
fromToModified.add(new CopiedAsideInfo(info.getParentImmediateReverted(), info.getTo(), info.getFirstTo(), null));
}
continue;
}
final File tmpFile = FileUtil.createTempFile(tmp, source.getName(), "", false);
tmpFile.mkdirs();
FileUtil.delete(tmpFile);
FileUtil.copy(source, tmpFile);
fromToModified.add(new CopiedAsideInfo(info.getParentImmediateReverted(), info.getTo(), info.getFirstTo(), tmpFile));
}
}
catch (IOException e) {
exceptions.add(new VcsException(e));
}
catch(VcsException e) {
exceptions.add(e);
}
}
private void moveGroup(final List<VcsException> exceptions,
List<CopiedAsideInfo> fromTo,
Map<File, PropertiesMap> properties) {
Collections.sort(fromTo, new Comparator<CopiedAsideInfo>() {
@Override
public int compare(CopiedAsideInfo o1, CopiedAsideInfo o2) {
return FileUtil.compareFiles(o1.getTo(), o2.getTo());
}
});
for (CopiedAsideInfo info : fromTo) {
if (info.getParentImmediateReverted().exists()) {
// parent successfully renamed/moved
try {
final File from = info.getFrom();
final File target = info.getTo();
if (from != null && ! FileUtil.filesEqual(from, target) && ! target.exists()) {
SvnFileSystemListener.moveFileWithSvn(mySvnVcs, from, target);
}
final File root = info.getTmpPlace();
if (root == null) continue;
if (! root.isDirectory()) {
if (target.exists()) {
FileUtil.copy(root, target);
} else {
FileUtil.rename(root, target);
}
} else {
FileUtil.processFilesRecursively(root, new Processor<File>() {
@Override
public boolean process(File file) {
if (file.isDirectory()) return true;
String relativePath = FileUtil.getRelativePath(root.getPath(), file.getPath(), File.separatorChar);
File newFile = new File(target, relativePath);
newFile.getParentFile().mkdirs();
try {
if (target.exists()) {
FileUtil.copy(file, newFile);
} else {
FileUtil.rename(file, newFile);
}
}
catch (IOException e) {
exceptions.add(new VcsException(e));
}
return true;
}
});
}
}
catch (IOException e) {
exceptions.add(new VcsException(e));
}
catch (VcsException e) {
exceptions.add(e);
}
}
}
applyProperties(properties, exceptions);
}
private void applyProperties(Map<File, PropertiesMap> propertiesMap, final List<VcsException> exceptions) {
for (Map.Entry<File, PropertiesMap> entry : propertiesMap.entrySet()) {
File file = entry.getKey();
try {
mySvnVcs.getFactory(file).createPropertyClient().setProperties(file, entry.getValue());
}
catch (VcsException e) {
exceptions.add(e);
}
}
}
private static class Reverter {
@NotNull private final SvnVcs myVcs;
private ProgressTracker myHandler;
private final List<VcsException> myExceptions;
private Reverter(@NotNull SvnVcs vcs, ProgressTracker handler, List<VcsException> exceptions) {
myVcs = vcs;
myHandler = handler;
myExceptions = exceptions;
}
public void revert(final File[] files, final boolean recursive) {
if (files.length == 0) return;
try {
// Files passed here are split into groups by root and working copy format - thus we could determine factory based on first file
myVcs.getFactory(files[0]).createRevertClient().revert(files, Depth.allOrEmpty(recursive), myHandler);
}
catch (VcsException e) {
processRevertError(e);
}
}
private void processRevertError(@NotNull VcsException e) {
if (e.getCause() instanceof SVNException) {
SVNException cause = (SVNException)e.getCause();
// skip errors on unversioned resources.
if (cause.getErrorMessage().getErrorCode() != SVNErrorCode.WC_NOT_DIRECTORY) {
myExceptions.add(e);
}
} else {
myExceptions.add(e);
}
}
}
public void rollbackMissingFileDeletion(List<FilePath> filePaths, final List<VcsException> exceptions,
final RollbackProgressListener listener) {
List<File> files = ChangesUtil.filePathsToFiles(filePaths);
for (File file : files) {
listener.accept(file);
try {
revertFileOrDir(file);
} catch (VcsException e) {
exceptions.add(e);
} catch (SVNException e) {
exceptions.add(new VcsException(e));
}
}
}
private void revertFileOrDir(File file) throws SVNException, VcsException {
Info info = mySvnVcs.getInfo(file);
if (info != null) {
if (info.isFile()) {
doRevert(file, false);
} else {
if (Info.SCHEDULE_ADD.equals(info.getSchedule())) {
doRevert(file, true);
} else {
boolean is17OrGreater = is17OrGreaterCopy(file, info);
if (is17OrGreater) {
doRevert(file, true);
} else {
// do update to restore missing directory.
mySvnVcs.getSvnKitManager().createUpdateClient().doUpdate(file, SVNRevision.HEAD, true);
}
}
}
} else {
throw new VcsException("Can not get 'svn info' for " + file.getPath());
}
}
private void doRevert(@NotNull File path, boolean recursive) throws VcsException {
mySvnVcs.getFactory(path).createRevertClient().revert(new File[]{path}, Depth.allOrFiles(recursive), null);
}
private boolean is17OrGreaterCopy(final File file, final Info info) throws VcsException {
final RootsToWorkingCopies copies = mySvnVcs.getRootsToWorkingCopies();
WorkingCopy copy = copies.getMatchingCopy(info.getURL());
if (copy == null) {
WorkingCopyFormat format = mySvnVcs.getWorkingCopyFormat(file);
return format.isOrGreater(WorkingCopyFormat.ONE_DOT_SEVEN);
} else {
return copy.is17Copy();
}
}
private static class UnversionedAndNotTouchedFilesGroupCollector extends EmptyChangelistBuilder {
private final List<Couple<File>> myToBeDeleted;
private final Map<File, ThroughRenameInfo> myFromTo;
// created by changes
private TreeMap<String, File> myRenames;
private Set<String> myAlsoReverted;
private UnversionedAndNotTouchedFilesGroupCollector() {
myFromTo = new HashMap<File, ThroughRenameInfo>();
myToBeDeleted = new ArrayList<Couple<File>>();
}
@Override
public void processUnversionedFile(final VirtualFile file) {
toFromTo(file);
}
private void markRename(@NotNull final File beforeFile, @NotNull final File afterFile) {
myToBeDeleted.add(Couple.of(beforeFile, afterFile));
}
public ThroughRenameInfo findToFile(@NotNull final FilePath file, @Nullable final File firstTo) {
final String path = FilePathsHelper.convertPath(file);
if (myAlsoReverted.contains(path)) return null;
final NavigableMap<String, File> head = myRenames.headMap(path, true);
if (head == null || head.isEmpty()) return null;
for (Map.Entry<String, File> entry : head.descendingMap().entrySet()) {
if (path.equals(entry.getKey())) return null;
if (path.startsWith(entry.getKey())) {
final String convertedBase = FileUtil.toSystemIndependentName(entry.getKey());
final String convertedChild = FileUtil.toSystemIndependentName(file.getPath());
final String relativePath = FileUtil.getRelativePath(convertedBase, convertedChild, '/');
assert relativePath != null;
return new ThroughRenameInfo(entry.getValue(), new File(entry.getValue(), relativePath), firstTo, file.getIOFile(), firstTo != null);
}
}
return null;
}
private void toFromTo(VirtualFile file) {
final FilePathImpl path = new FilePathImpl(file);
final ThroughRenameInfo info = findToFile(path, null);
if (info != null) {
myFromTo.put(path.getIOFile(), info);
}
}
private void processChangeImpl(final Change change) {
if (change.getAfterRevision() != null) {
final FilePath after = change.getAfterRevision().getFile();
final ThroughRenameInfo info = findToFile(after, change.getBeforeRevision() == null ? null : change.getBeforeRevision().getFile().getIOFile());
if (info != null) {
myFromTo.put(after.getIOFile(), info);
}
}
}
@Override
public void processChange(Change change, VcsKey vcsKey) {
processChangeImpl(change);
}
@Override
public void processChangeInList(Change change, @Nullable ChangeList changeList, VcsKey vcsKey) {
processChangeImpl(change);
}
@Override
public void processChangeInList(Change change, String changeListName, VcsKey vcsKey) {
processChangeImpl(change);
}
@Override
public void processIgnoredFile(VirtualFile file) {
// as with unversioned
toFromTo(file);
}
public List<Couple<File>> getToBeDeleted() {
return myToBeDeleted;
}
public Map<File, ThroughRenameInfo> getFromTo() {
return myFromTo;
}
public void setRenamesMap(TreeMap<String, File> renames) {
myRenames = renames;
}
public void setAlsoReverted(Set<String> alsoReverted) {
myAlsoReverted = alsoReverted;
}
}
private static class CopiedAsideInfo {
private final File myParentImmediateReverted;
private final File myTo;
private final File myFrom;
private final File myTmpPlace;
private CopiedAsideInfo(File parentImmediateReverted, File to, File from, File tmpPlace) {
myParentImmediateReverted = parentImmediateReverted;
myTo = to;
myFrom = from;
myTmpPlace = tmpPlace;
}
public File getParentImmediateReverted() {
return myParentImmediateReverted;
}
public File getTo() {
return myTo;
}
public File getFrom() {
return myFrom;
}
public File getTmpPlace() {
return myTmpPlace;
}
@Override
public String toString() {
return myFrom + " -> " + myTo;
}
}
private static class ThroughRenameInfo {
private final File myParentImmediateReverted;
private final File myTo;
private final File myFirstTo;
private final File myFrom;
private final boolean myVersioned;
private ThroughRenameInfo(File parentImmediateReverted, File to, File firstTo, File from, boolean versioned) {
myParentImmediateReverted = parentImmediateReverted;
myTo = to;
myFrom = from;
myVersioned = versioned;
myFirstTo = firstTo;
}
public File getFirstTo() {
return myFirstTo;
}
public boolean isVersioned() {
return myVersioned;
}
public File getParentImmediateReverted() {
return myParentImmediateReverted;
}
public File getTo() {
return myTo;
}
public File getFrom() {
return myFrom;
}
}
// both adds and deletes
private static abstract class SuperfluousRemover {
private final Set<File> myParentPaths;
private SuperfluousRemover() {
myParentPaths = new HashSet<File>();
}
@Nullable
protected abstract File accept(final Change change);
public void check(final File file) {
for (Iterator<File> iterator = myParentPaths.iterator(); iterator.hasNext();) {
final File parentPath = iterator.next();
if (VfsUtil.isAncestor(parentPath, file, true)) {
return;
} else if (VfsUtil.isAncestor(file, parentPath, true)) {
iterator.remove();
// remove others; dont check for 1st variant any more
for (; iterator.hasNext();) {
final File innerParentPath = iterator.next();
if (VfsUtil.isAncestor(file, innerParentPath, true)) {
iterator.remove();
}
}
// will be added in the end
}
}
myParentPaths.add(file);
}
public Set<File> getParentPaths() {
return myParentPaths;
}
}
private static class ChangesChecker {
private final SuperfluousRemover myForAdds;
private final SuperfluousRemover myForDeletes;
private final List<File> myForEdits;
private final SvnChangeProvider myChangeProvider;
private final UnversionedAndNotTouchedFilesGroupCollector myCollector;
private final List<VcsException> myExceptions;
private ChangesChecker(SvnChangeProvider changeProvider, UnversionedAndNotTouchedFilesGroupCollector collector) {
myChangeProvider = changeProvider;
myCollector = collector;
myForAdds = new SuperfluousRemover() {
@Nullable
@Override
protected File accept(Change change) {
final ContentRevision beforeRevision = change.getBeforeRevision();
final ContentRevision afterRevision = change.getAfterRevision();
if (beforeRevision == null || MoveRenameReplaceCheck.check(change)) {
return afterRevision.getFile().getIOFile();
}
return null;
}
};
myForDeletes = new SuperfluousRemover() {
@Nullable
@Override
protected File accept(Change change) {
final ContentRevision beforeRevision = change.getBeforeRevision();
final ContentRevision afterRevision = change.getAfterRevision();
if (afterRevision == null || MoveRenameReplaceCheck.check(change)) {
return beforeRevision.getFile().getIOFile();
}
return null;
}
};
myForEdits = new ArrayList<File>();
myExceptions = new ArrayList<VcsException>();
}
public void gather(final List<Change> changes) {
final TreeMap<String, File> renames = new TreeMap<String, File>();
final Set<String> alsoReverted = new HashSet<String>();
final Map<String, FilePath> files = new HashMap<String, FilePath>();
for (Change change : changes) {
final ContentRevision beforeRevision = change.getBeforeRevision();
final ContentRevision afterRevision = change.getAfterRevision();
final String key = afterRevision == null ? null : FilePathsHelper.convertWithLastSeparator(afterRevision.getFile());
if (MoveRenameReplaceCheck.check(change)) {
final File beforeFile = beforeRevision.getFile().getIOFile();
renames.put(key, beforeFile);
files.put(key, afterRevision.getFile());
myCollector.markRename(beforeFile, afterRevision.getFile().getIOFile());
} else if (afterRevision != null) {
alsoReverted.add(key);
}
}
if (! renames.isEmpty()) {
final ArrayList<String> paths = new ArrayList<String>(renames.keySet());
if (paths.size() > 1) {
FilterFilePathStrings.getInstance().doFilter(paths);
}
myCollector.setRenamesMap(renames);
myCollector.setAlsoReverted(alsoReverted);
for (String path : paths) {
try {
myChangeProvider.getChanges(files.get(path), true, myCollector);
}
catch (SVNException e) {
myExceptions.add(new VcsException(e));
}
catch (SvnBindException e) {
myExceptions.add(e);
}
}
}
for (Change change : changes) {
final ContentRevision afterRevision = change.getAfterRevision();
boolean checked = getAddDelete(myForAdds, change);
checked |= getAddDelete(myForDeletes, change);
if (! checked) {
myForEdits.add(afterRevision.getFile().getIOFile());
}
}
}
private boolean getAddDelete(final SuperfluousRemover superfluousRemover, final Change change) {
final File file = superfluousRemover.accept(change);
if (file != null) {
superfluousRemover.check(file);
return true;
}
return false;
}
public File[] getForAdds() {
return convert(myForAdds.getParentPaths());
}
public File[] getForDeletes() {
return convert(myForDeletes.getParentPaths());
}
private File[] convert(final Collection<File> paths) {
return paths.toArray(new File[paths.size()]);
}
public List<VcsException> getExceptions() {
return myExceptions;
}
public List<File> getForEdits() {
return myForEdits;
}
}
}