| /* |
| * 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"); |
| } |
| } |
| } |
| } |
| |
| } |
| } |