blob: 6cc4b7c1b200e95ef08e73fda5d1094c9f6b7f78 [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 com.intellij.openapi.vcs;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.command.CommandAdapter;
import com.intellij.openapi.command.CommandEvent;
import com.intellij.openapi.command.CommandProcessor;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vcs.actions.VcsContextFactory;
import com.intellij.openapi.vcs.changes.ChangeListManager;
import com.intellij.openapi.vcs.changes.VcsDirtyScopeManager;
import com.intellij.openapi.vfs.*;
import com.intellij.openapi.vfs.newvfs.NewVirtualFile;
import com.intellij.util.SmartList;
import com.intellij.vcsUtil.VcsUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.util.*;
/**
* @author yole
*/
public abstract class VcsVFSListener implements Disposable {
private final VcsDirtyScopeManager myDirtyScopeManager;
private final ProjectLevelVcsManager myVcsManager;
private final VcsFileListenerContextHelper myVcsFileListenerContextHelper;
protected static class MovedFileInfo {
public final String myOldPath;
public String myNewPath;
private final VirtualFile myFile;
protected MovedFileInfo(VirtualFile file, final String newPath) {
myOldPath = file.getPath();
myNewPath = newPath;
myFile = file;
}
}
protected final Project myProject;
protected final AbstractVcs myVcs;
protected final ChangeListManager myChangeListManager;
protected final VcsShowConfirmationOption myAddOption;
protected final VcsShowConfirmationOption myRemoveOption;
protected final List<VirtualFile> myAddedFiles = new ArrayList<VirtualFile>();
protected final Map<VirtualFile, VirtualFile> myCopyFromMap = new HashMap<VirtualFile, VirtualFile>();
protected final List<VcsException> myExceptions = new SmartList<VcsException>();
protected final List<FilePath> myDeletedFiles = new ArrayList<FilePath>();
protected final List<FilePath> myDeletedWithoutConfirmFiles = new ArrayList<FilePath>();
protected final List<MovedFileInfo> myMovedFiles = new ArrayList<MovedFileInfo>();
protected final List<VirtualFile> myDirtyFiles = new ArrayList<VirtualFile>();
protected enum VcsDeleteType {SILENT, CONFIRM, IGNORE}
protected VcsVFSListener(@NotNull Project project, @NotNull AbstractVcs vcs) {
myProject = project;
myVcs = vcs;
myChangeListManager = ChangeListManager.getInstance(project);
myDirtyScopeManager = VcsDirtyScopeManager.getInstance(myProject);
final MyVirtualFileAdapter myVFSListener = new MyVirtualFileAdapter();
final MyCommandAdapter myCommandListener = new MyCommandAdapter();
myVcsManager = ProjectLevelVcsManager.getInstance(project);
myAddOption = myVcsManager.getStandardConfirmation(VcsConfiguration.StandardConfirmation.ADD, vcs);
myRemoveOption = myVcsManager.getStandardConfirmation(VcsConfiguration.StandardConfirmation.REMOVE, vcs);
VirtualFileManager.getInstance().addVirtualFileListener(myVFSListener, this);
CommandProcessor.getInstance().addCommandListener(myCommandListener, this);
myVcsFileListenerContextHelper = VcsFileListenerContextHelper.getInstance(myProject);
}
@Override
public void dispose() {
}
protected boolean isEventIgnored(final VirtualFileEvent event, boolean putInDirty) {
if (event.isFromRefresh()) return true;
boolean vcsIgnored = !isUnderMyVcs(event.getFile());
if (vcsIgnored) {
myDirtyFiles.add(event.getFile());
}
return vcsIgnored;
}
private boolean isUnderMyVcs(VirtualFile file) {
return myVcsManager.getVcsFor(file) == myVcs &&
myVcsManager.isFileInContent(file) &&
!myChangeListManager.isIgnoredFile(file);
}
protected void executeAdd() {
final List<VirtualFile> addedFiles = acquireAddedFiles();
for (Iterator<VirtualFile> iterator = addedFiles.iterator(); iterator.hasNext(); ) {
VirtualFile file = iterator.next();
if (myVcsFileListenerContextHelper.isAdditionIgnored(file)) {
iterator.remove();
}
}
final Map<VirtualFile, VirtualFile> copyFromMap = acquireCopiedFiles();
if (! addedFiles.isEmpty()) {
executeAdd(addedFiles, copyFromMap);
}
}
/**
* @return get map of copied files and clear the map
*/
protected Map<VirtualFile, VirtualFile> acquireCopiedFiles() {
final Map<VirtualFile, VirtualFile> copyFromMap = new HashMap<VirtualFile, VirtualFile>(myCopyFromMap);
myCopyFromMap.clear();
return copyFromMap;
}
/**
* @return get list of added files and clear previous list
*/
protected List<VirtualFile> acquireAddedFiles() {
final List<VirtualFile> addedFiles = new ArrayList<VirtualFile>(myAddedFiles);
myAddedFiles.clear();
return addedFiles;
}
/**
* Execute add that performs adding from specific collections
*
* @param addedFiles the added files
* @param copyFromMap the copied files
*/
protected void executeAdd(List<VirtualFile> addedFiles, Map<VirtualFile, VirtualFile> copyFromMap) {
if (myAddOption.getValue() == VcsShowConfirmationOption.Value.DO_NOTHING_SILENTLY) return;
if (myAddOption.getValue() == VcsShowConfirmationOption.Value.DO_ACTION_SILENTLY) {
performAdding(addedFiles, copyFromMap);
}
else {
final AbstractVcsHelper helper = AbstractVcsHelper.getInstance(myProject);
// TODO[yole]: nice and clean description label
Collection<VirtualFile> filesToProcess = helper.selectFilesToProcess(addedFiles, getAddTitle(), null,
getSingleFileAddTitle(), getSingleFileAddPromptTemplate(),
myAddOption);
if (filesToProcess != null) {
performAdding(new ArrayList<VirtualFile>(filesToProcess), copyFromMap);
}
}
}
private void addFileToDelete(VirtualFile file) {
if (file.isDirectory() && file instanceof NewVirtualFile && !isDirectoryVersioningSupported()) {
for (VirtualFile child : ((NewVirtualFile)file).getCachedChildren()) {
addFileToDelete(child);
}
}
else {
final VcsDeleteType type = needConfirmDeletion(file);
final FilePath filePath =
VcsContextFactory.SERVICE.getInstance().createFilePathOnDeleted(new File(file.getPath()), file.isDirectory());
if (type == VcsDeleteType.CONFIRM) {
myDeletedFiles.add(filePath);
}
else if (type == VcsDeleteType.SILENT) {
myDeletedWithoutConfirmFiles.add(filePath);
}
}
}
protected void executeDelete() {
final List<FilePath> filesToDelete = new ArrayList<FilePath>(myDeletedWithoutConfirmFiles);
final List<FilePath> deletedFiles = new ArrayList<FilePath>(myDeletedFiles);
myDeletedWithoutConfirmFiles.clear();
myDeletedFiles.clear();
for (Iterator<FilePath> iterator = filesToDelete.iterator(); iterator.hasNext(); ) {
FilePath file = iterator.next();
if (myVcsFileListenerContextHelper.isDeletionIgnored(file)) {
iterator.remove();
}
}
for (Iterator<FilePath> iterator = deletedFiles.iterator(); iterator.hasNext(); ) {
FilePath file = iterator.next();
if (myVcsFileListenerContextHelper.isDeletionIgnored(file)) {
iterator.remove();
}
}
if (deletedFiles.isEmpty() &&filesToDelete.isEmpty()) return;
if (myRemoveOption.getValue() != VcsShowConfirmationOption.Value.DO_NOTHING_SILENTLY) {
if (myRemoveOption.getValue() == VcsShowConfirmationOption.Value.DO_ACTION_SILENTLY || deletedFiles.isEmpty()) {
filesToDelete.addAll(deletedFiles);
}
else {
Collection<FilePath> filePaths = selectFilePathsToDelete(deletedFiles);
if (filePaths != null) {
filesToDelete.addAll(filePaths);
}
}
}
performDeletion(filesToDelete);
}
/**
* Select file paths to delete
*
* @param deletedFiles deleted files set
* @return selected files or null (that is considered as empty file set)
*/
@Nullable
protected Collection<FilePath> selectFilePathsToDelete(final List<FilePath> deletedFiles) {
AbstractVcsHelper helper = AbstractVcsHelper.getInstance(myProject);
return helper.selectFilePathsToProcess(deletedFiles, getDeleteTitle(), null, getSingleFileDeleteTitle(),
getSingleFileDeletePromptTemplate(), myRemoveOption);
}
protected void beforeContentsChange(VirtualFileEvent event, VirtualFile file) {
}
protected void fileAdded(VirtualFileEvent event, VirtualFile file) {
if (!isEventIgnored(event, true) && !myChangeListManager.isIgnoredFile(file) &&
(isDirectoryVersioningSupported() || !file.isDirectory())) {
myAddedFiles.add(event.getFile());
}
}
private void addFileToMove(final VirtualFile file, final String newParentPath, final String newName) {
if (file.isDirectory() && !file.is(VFileProperty.SYMLINK) && !isDirectoryVersioningSupported()) {
@SuppressWarnings("UnsafeVfsRecursion") VirtualFile[] children = file.getChildren();
if (children != null) {
for (VirtualFile child : children) {
addFileToMove(child, newParentPath + "/" + newName, child.getName());
}
}
}
else {
processMovedFile(file, newParentPath, newName);
}
}
protected boolean filterOutUnknownFiles() {
return true;
}
protected void processMovedFile(VirtualFile file, String newParentPath, String newName) {
final FileStatus status = FileStatusManager.getInstance(myProject).getStatus(file);
if (status == FileStatus.IGNORED) {
if (file.getParent() != null) {
myDirtyFiles.add(file.getParent());
myDirtyFiles.add(file); // will be at new path
}
}
if (!(filterOutUnknownFiles() && status == FileStatus.UNKNOWN) && status != FileStatus.IGNORED) {
final String newPath = newParentPath + "/" + newName;
boolean foundExistingInfo = false;
for (MovedFileInfo info : myMovedFiles) {
if (Comparing.equal(info.myFile, file)) {
info.myNewPath = newPath;
foundExistingInfo = true;
break;
}
}
if (!foundExistingInfo) {
myMovedFiles.add(new MovedFileInfo(file, newPath));
}
}
}
private void executeMoveRename() {
final List<MovedFileInfo> movedFiles = new ArrayList<MovedFileInfo>(myMovedFiles);
myMovedFiles.clear();
performMoveRename(movedFiles);
}
protected VcsDeleteType needConfirmDeletion(final VirtualFile file) {
return VcsDeleteType.CONFIRM;
}
protected abstract String getAddTitle();
protected abstract String getSingleFileAddTitle();
protected abstract String getSingleFileAddPromptTemplate();
protected abstract void performAdding(final Collection<VirtualFile> addedFiles, final Map<VirtualFile, VirtualFile> copyFromMap)
;
protected abstract String getDeleteTitle();
protected abstract String getSingleFileDeleteTitle();
protected abstract String getSingleFileDeletePromptTemplate();
protected abstract void performDeletion(List<FilePath> filesToDelete);
protected abstract void performMoveRename(List<MovedFileInfo> movedFiles);
protected abstract boolean isDirectoryVersioningSupported();
private class MyVirtualFileAdapter extends VirtualFileAdapter {
@Override
public void fileCreated(@NotNull final VirtualFileEvent event) {
VirtualFile file = event.getFile();
if (isUnderMyVcs(file)) {
fileAdded(event, file);
}
}
@Override
public void fileCopied(@NotNull final VirtualFileCopyEvent event) {
if (isEventIgnored(event, true) || myChangeListManager.isIgnoredFile(event.getFile())) return;
final AbstractVcs oldVcs = ProjectLevelVcsManager.getInstance(myProject).getVcsFor(event.getOriginalFile());
if (oldVcs == myVcs) {
final VirtualFile parent = event.getFile().getParent();
if (parent != null) {
myAddedFiles.add(event.getFile());
myCopyFromMap.put(event.getFile(), event.getOriginalFile());
}
}
else {
myAddedFiles.add(event.getFile());
}
}
@Override
public void beforeFileDeletion(@NotNull final VirtualFileEvent event) {
final VirtualFile file = event.getFile();
if (isEventIgnored(event, true)) {
return;
}
if (!myChangeListManager.isIgnoredFile(file)) {
addFileToDelete(file);
return;
}
// files are ignored, directories are handled recursively
if (event.getFile().isDirectory()) {
final List<VirtualFile> list = new LinkedList<VirtualFile>();
VcsUtil.collectFiles(file, list, true, isDirectoryVersioningSupported());
for (VirtualFile child : list) {
if (!myChangeListManager.isIgnoredFile(child)) {
addFileToDelete(child);
}
}
}
}
@Override
public void beforeFileMovement(@NotNull final VirtualFileMoveEvent event) {
if (isEventIgnored(event, true)) return;
final VirtualFile file = event.getFile();
final AbstractVcs newVcs = ProjectLevelVcsManager.getInstance(myProject).getVcsFor(event.getNewParent());
if (newVcs == myVcs) {
addFileToMove(file, event.getNewParent().getPath(), file.getName());
}
else {
addFileToDelete(event.getFile());
}
}
@Override
public void fileMoved(@NotNull final VirtualFileMoveEvent event) {
if (isEventIgnored(event, true)) return;
final AbstractVcs oldVcs = ProjectLevelVcsManager.getInstance(myProject).getVcsFor(event.getOldParent());
if (oldVcs != myVcs) {
myAddedFiles.add(event.getFile());
}
}
@Override
public void beforePropertyChange(@NotNull final VirtualFilePropertyEvent event) {
if (!isEventIgnored(event, false) && event.getPropertyName().equalsIgnoreCase(VirtualFile.PROP_NAME)) {
String oldName = (String)event.getOldValue();
String newName = (String)event.getNewValue();
// in order to force a reparse of a file, the rename event can be fired with old name equal to new name -
// such events needn't be handled by the VCS
if (!Comparing.equal(oldName, newName)) {
final VirtualFile file = event.getFile();
final VirtualFile parent = file.getParent();
if (parent != null) {
addFileToMove(file, parent.getPath(), newName);
}
}
}
}
@Override
public void beforeContentsChange(@NotNull VirtualFileEvent event) {
VirtualFile file = event.getFile();
assert !file.isDirectory();
if (isUnderMyVcs(file)) {
VcsVFSListener.this.beforeContentsChange(event, file);
}
}
}
private class MyCommandAdapter extends CommandAdapter {
private int myCommandLevel;
@Override
public void commandStarted(final CommandEvent event) {
if (myProject != event.getProject()) return;
myCommandLevel++;
}
private void checkMovedAddedSourceBack() {
if (myAddedFiles.isEmpty() || myMovedFiles.isEmpty()) return;
final Map<String, VirtualFile> addedPaths = new HashMap<String, VirtualFile>(myAddedFiles.size());
for (VirtualFile file : myAddedFiles) {
addedPaths.put(file.getPath(), file);
}
for (Iterator<MovedFileInfo> iterator = myMovedFiles.iterator(); iterator.hasNext();) {
final MovedFileInfo movedFile = iterator.next();
if (addedPaths.containsKey(movedFile.myOldPath)) {
iterator.remove();
final VirtualFile oldAdded = addedPaths.get(movedFile.myOldPath);
myAddedFiles.remove(oldAdded);
myAddedFiles.add(movedFile.myFile);
myCopyFromMap.put(oldAdded, movedFile.myFile);
}
}
}
// If a file is scheduled for deletion, and at the same time for copying or addition, don't delete it.
// It happens during Overwrite command or undo of overwrite.
private void doNotDeleteAddedCopiedOrMovedFiles() {
Collection<String> copiedAddedMoved = new ArrayList<String>();
for (VirtualFile file : myCopyFromMap.keySet()) {
copiedAddedMoved.add(file.getPath());
}
for (VirtualFile file : myAddedFiles) {
copiedAddedMoved.add(file.getPath());
}
for (MovedFileInfo movedFileInfo : myMovedFiles) {
copiedAddedMoved.add(movedFileInfo.myNewPath);
}
for (Iterator<FilePath> iterator = myDeletedFiles.iterator(); iterator.hasNext(); ) {
if (copiedAddedMoved.contains(FileUtil.toSystemIndependentName(iterator.next().getPath()))) {
iterator.remove();
}
}
for (Iterator<FilePath> iterator = myDeletedWithoutConfirmFiles.iterator(); iterator.hasNext(); ) {
if (copiedAddedMoved.contains(FileUtil.toSystemIndependentName(iterator.next().getPath()))) {
iterator.remove();
}
}
}
@Override
public void commandFinished(final CommandEvent event) {
if (myProject != event.getProject()) return;
myCommandLevel--;
if (myCommandLevel == 0) {
if (!myAddedFiles.isEmpty() || !myDeletedFiles.isEmpty() || !myDeletedWithoutConfirmFiles.isEmpty() || !myMovedFiles.isEmpty() ||
! myDirtyFiles.isEmpty()) {
// avoid reentering commandFinished handler - saving the documents may cause a "before file deletion" event firing,
// which will cause closing the text editor, which will itself run a command that will be caught by this listener
myCommandLevel++;
try {
FileDocumentManager.getInstance().saveAllDocuments();
}
finally {
myCommandLevel--;
}
doNotDeleteAddedCopiedOrMovedFiles();
checkMovedAddedSourceBack();
if (!myAddedFiles.isEmpty()) {
executeAdd();
myAddedFiles.clear();
}
if (!myDeletedFiles.isEmpty() || !myDeletedWithoutConfirmFiles.isEmpty()) {
executeDelete();
myDeletedFiles.clear();
myDeletedWithoutConfirmFiles.clear();
}
if (!myMovedFiles.isEmpty()) {
executeMoveRename();
myMovedFiles.clear();
}
if (! myDirtyFiles.isEmpty()) {
final List<VirtualFile> files = new ArrayList<VirtualFile>();
final List<VirtualFile> dirs = new ArrayList<VirtualFile>();
for (VirtualFile dirtyFile : myDirtyFiles) {
if (dirtyFile != null) {
if (dirtyFile.isDirectory()) {
dirs.add(dirtyFile);
} else {
files.add(dirtyFile);
}
}
}
myDirtyScopeManager.filesDirty(files, dirs);
myDirtyFiles.clear();
}
if (! myExceptions.isEmpty()) {
AbstractVcsHelper.getInstance(myProject).showErrors(myExceptions, myVcs.getDisplayName() + " operations errors");
}
}
}
}
}
}