blob: c8617dc5f34724ce529f8c395d0593a3ffd45e7a [file] [log] [blame]
/*
* Copyright 2000-2012 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.changes;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vcs.*;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.Consumer;
import com.intellij.util.PairProcessor;
import com.intellij.util.Processor;
import com.intellij.util.SmartList;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.Convertor;
import com.intellij.util.containers.MultiMap;
import gnu.trove.THashSet;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
/**
* @author max
* @author yole
*/
public class VcsDirtyScopeImpl extends VcsModifiableDirtyScope {
private final Map<VirtualFile, THashSet<FilePath>> myDirtyFiles = new HashMap<VirtualFile, THashSet<FilePath>>();
private final Map<VirtualFile, THashSet<FilePath>> myDirtyDirectoriesRecursively = new HashMap<VirtualFile, THashSet<FilePath>>();
private final Set<VirtualFile> myAffectedContentRoots = new THashSet<VirtualFile>();
private final Project myProject;
private final ProjectLevelVcsManager myVcsManager;
private final AbstractVcs myVcs;
private VcsDirtyScopeModifier myVcsDirtyScopeModifier;
private boolean myWasEverythingDirty;
public VcsDirtyScopeImpl(final AbstractVcs vcs, final Project project) {
myProject = project;
myVcs = vcs;
myVcsManager = ProjectLevelVcsManager.getInstance(project);
myWasEverythingDirty = false;
myVcsDirtyScopeModifier = new VcsDirtyScopeModifier() {
@Override
public Collection<VirtualFile> getAffectedVcsRoots() {
return Collections.unmodifiableCollection(myDirtyDirectoriesRecursively.keySet());
}
@Override
public Iterator<FilePath> getDirtyFilesIterator() {
if (myDirtyFiles.isEmpty()) {
return Collections.<FilePath>emptyList().iterator();
}
final ArrayList<Iterator<FilePath>> iteratorList = new ArrayList<Iterator<FilePath>>(myDirtyFiles.size());
for (THashSet<FilePath> paths : myDirtyFiles.values()) {
iteratorList.add(paths.iterator());
}
return ContainerUtil.concatIterators(iteratorList);
}
@NotNull
@Override
public Iterator<FilePath> getDirtyDirectoriesIterator(final VirtualFile root) {
final THashSet<FilePath> filePaths = myDirtyDirectoriesRecursively.get(root);
if (filePaths != null) {
return filePaths.iterator();
}
return ContainerUtil.emptyIterator();
}
@Override
public void recheckDirtyKeys() {
recheckMap(myDirtyDirectoriesRecursively);
recheckMap(myDirtyFiles);
}
private void recheckMap(Map<VirtualFile, THashSet<FilePath>> map) {
for (Iterator<THashSet<FilePath>> iterator = map.values().iterator(); iterator.hasNext();) {
final THashSet<FilePath> next = iterator.next();
if (next.isEmpty()) {
iterator.remove();
}
}
}
};
}
@Override
public Collection<VirtualFile> getAffectedContentRoots() {
return myAffectedContentRoots;
}
@Override
public Collection<VirtualFile> getAffectedContentRootsWithCheck() {
if (myVcs.allowsNestedRoots()) {
final ProjectLevelVcsManager vcsManager = ProjectLevelVcsManager.getInstance(myVcs.getProject());
final VirtualFile[] roots = vcsManager.getRootsUnderVcs(myVcs);
final Set<VirtualFile> result = new HashSet<VirtualFile>(myAffectedContentRoots);
for (VirtualFile root : roots) {
for (VirtualFile dir : myDirtyDirectoriesRecursively.keySet()) {
if (VfsUtilCore.isAncestor(dir, root, true)) {
result.add(root);
}
}
}
return new SmartList<VirtualFile>(result);
}
return myAffectedContentRoots;
}
@Override
public Project getProject() {
return myProject;
}
@Override
public AbstractVcs getVcs() {
return myVcs;
}
@Override
public Set<FilePath> getDirtyFiles() {
final THashSet<FilePath> result = new THashSet<FilePath>();
for (THashSet<FilePath> paths : myDirtyFiles.values()) {
result.addAll(paths);
}
for (THashSet<FilePath> paths : myDirtyFiles.values()) {
for (FilePath filePath : paths) {
VirtualFile vFile = filePath.getVirtualFile();
if (vFile != null && vFile.isValid() && vFile.isDirectory()) {
for(VirtualFile child: vFile.getChildren()) {
result.add(new FilePathImpl(child));
}
}
}
}
return result;
}
@Override
public Set<FilePath> getDirtyFilesNoExpand() {
final THashSet<FilePath> paths = new THashSet<FilePath>();
for (THashSet<FilePath> filePaths : myDirtyFiles.values()) {
paths.addAll(filePaths);
}
return paths;
}
@Override
public Set<FilePath> getRecursivelyDirtyDirectories() {
THashSet<FilePath> result = new THashSet<FilePath>();
for(THashSet<FilePath> dirsByRoot: myDirtyDirectoriesRecursively.values()) {
result.addAll(dirsByRoot);
}
return result;
}
@Override
public boolean isRecursivelyDirty(final VirtualFile vf) {
for(THashSet<FilePath> dirsByRoot: myDirtyDirectoriesRecursively.values()) {
for (FilePath dir : dirsByRoot) {
final VirtualFile dirVf = dir.getVirtualFile();
if (dirVf != null) {
if (VfsUtilCore.isAncestor(dirVf, vf, false)) {
return true;
}
}
}
}
return false;
}
private static class FileOrDir {
private final FilePath myPath;
private final boolean myRecursive;
private FileOrDir(FilePath path, boolean recursive) {
myPath = path;
myRecursive = recursive;
}
}
public void addDirtyData(final Collection<FilePath> dirs, final Collection<FilePath> files) {
ApplicationManager.getApplication().runReadAction(new Runnable() {
@Override
public void run() {
final HashSet<FilePath> newFiles = new HashSet<FilePath>(files);
newFiles.removeAll(dirs); // if the same dir is added recursively and not recursively, prefer recursive mark
final MultiMap<VirtualFile, FileOrDir> perRoot = MultiMap.createSet();
for (Map.Entry<VirtualFile, THashSet<FilePath>> entry : myDirtyDirectoriesRecursively.entrySet()) {
newFiles.removeAll(entry.getValue()); // if the same dir is added recursively and not recursively, prefer recursive mark
for (FilePath path : entry.getValue()) {
perRoot.putValue(entry.getKey(), new FileOrDir(path, true));
}
}
for (Map.Entry<VirtualFile, THashSet<FilePath>> entry : myDirtyFiles.entrySet()) {
for (FilePath path : entry.getValue()) {
perRoot.putValue(entry.getKey(), new FileOrDir(path, false));
}
}
for (FilePath dir : dirs) {
addFilePathToMap(perRoot, dir, true);
}
for (FilePath file : newFiles) {
addFilePathToMap(perRoot, file, false);
}
for (Map.Entry<VirtualFile, Collection<FileOrDir>> entry : perRoot.entrySet()) {
final Collection<FileOrDir> set = entry.getValue();
final Collection<FileOrDir> newCollection = FileUtil.removeAncestors(set, new Convertor<FileOrDir, String>() {
@Override
public String convert(FileOrDir o) {
return o.myPath.getPath();
}
}, new PairProcessor<FileOrDir, FileOrDir>() {
@Override
public boolean process(FileOrDir parent, FileOrDir child) {
if (parent.myRecursive) {
return true;
}
// if under non-recursive dirty dir, generally do not remove child with one exception...
if (child.myRecursive || child.myPath.isDirectory()) {
return false;
}
// only if dir non-recursively + non-recursive file child -> can be truncated to dir only
return Comparing.equal(child.myPath.getParentPath(), parent.myPath);
}
});
set.retainAll(newCollection);
}
myAffectedContentRoots.addAll(perRoot.keySet());
for (Map.Entry<VirtualFile, Collection<FileOrDir>> entry : perRoot.entrySet()) {
final VirtualFile root = entry.getKey();
final THashSet<FilePath> curFiles = new THashSet<FilePath>();
final THashSet<FilePath> curDirs = new THashSet<FilePath>();
final Collection<FileOrDir> value = entry.getValue();
for (FileOrDir fileOrDir : value) {
if (fileOrDir.myRecursive) {
curDirs.add(fileOrDir.myPath);
} else {
curFiles.add(fileOrDir.myPath);
}
}
// no clear is necessary since no root can disappear
// also, we replace contents, so here's no merging
if (! curDirs.isEmpty()) {
myDirtyDirectoriesRecursively.put(root, curDirs);
}
if (! curFiles.isEmpty()) {
myDirtyFiles.put(root, curFiles);
}
}
}
});
}
private void addFilePathToMap(MultiMap<VirtualFile, FileOrDir> perRoot, FilePath dir, final boolean recursively) {
final VirtualFile vcsRoot = myVcsManager.getVcsRootFor(dir);
if (vcsRoot != null) {
perRoot.putValue(vcsRoot, new FileOrDir(dir, recursively));
}
}
/**
* Add dirty directory recursively. If there are already dirty entries
* that are descendants or ancestors for the added directory, the contained
* entries are dropped from scope.
*
* @param newcomer a new directory to add
*/
@Override
public void addDirtyDirRecursively(final FilePath newcomer) {
ApplicationManager.getApplication().runReadAction(new Runnable() {
@Override
public void run() {
final VirtualFile vcsRoot = myVcsManager.getVcsRootFor(newcomer);
if (vcsRoot == null) return;
myAffectedContentRoots.add(vcsRoot);
for (Map.Entry<VirtualFile, THashSet<FilePath>> entry : myDirtyFiles.entrySet()) {
final VirtualFile groupRoot = entry.getKey();
if (groupRoot != null && VfsUtilCore.isAncestor(vcsRoot, groupRoot, false)) {
final THashSet<FilePath> files = entry.getValue();
if (files != null) {
for (Iterator<FilePath> it = files.iterator(); it.hasNext();) {
FilePath oldBoy = it.next();
if (oldBoy.isUnder(newcomer, false)) {
it.remove();
}
}
}
}
}
THashSet<FilePath> dirsByRoot = myDirtyDirectoriesRecursively.get(vcsRoot);
if (dirsByRoot == null) {
dirsByRoot = new THashSet<FilePath>();
myDirtyDirectoriesRecursively.put(vcsRoot, dirsByRoot);
}
else {
for (Iterator<FilePath> it = dirsByRoot.iterator(); it.hasNext();) {
FilePath oldBoy = it.next();
if (newcomer.isUnder(oldBoy, false)) {
return;
}
if (oldBoy.isUnder(newcomer, false)) {
it.remove();
}
}
}
dirsByRoot.add(newcomer);
}
});
}
/**
* Add dirty file to the scope. Note that file is not added if its ancestor was added as dirty recursively or if its parent is in already
* in the dirty scope. Also immediate non-directory children are removed from the set of dirty files.
*
* @param newcomer a file or directory added to the dirty scope.
*/
@Override
public void addDirtyFile(final FilePath newcomer) {
ApplicationManager.getApplication().runReadAction(new Runnable() {
@Override
public void run() {
final VirtualFile vcsRoot = myVcsManager.getVcsRootFor(newcomer);
myAffectedContentRoots.add(vcsRoot);
THashSet<FilePath> dirsByRoot = myDirtyDirectoriesRecursively.get(vcsRoot);
if (dirsByRoot != null) {
for (FilePath oldBoy : dirsByRoot) {
if (newcomer.isUnder(oldBoy, false)) {
return;
}
}
}
final THashSet<FilePath> dirtyFiles = myDirtyFiles.get(vcsRoot);
if (dirtyFiles == null) {
final THashSet<FilePath> set = new THashSet<FilePath>();
set.add(newcomer);
myDirtyFiles.put(vcsRoot, set);
} else {
if (newcomer.isDirectory()) {
for (Iterator<FilePath> iterator = dirtyFiles.iterator(); iterator.hasNext(); ) {
final FilePath oldBoy = iterator.next();
if (!oldBoy.isDirectory() && Comparing.equal(oldBoy.getVirtualFileParent(), newcomer.getVirtualFile())) {
iterator.remove();
}
}
} else if (!dirtyFiles.isEmpty()) {
VirtualFile parent = newcomer.getVirtualFileParent();
if (parent != null && dirtyFiles.contains(new FilePathImpl(parent))) {
return;
}
dirtyFiles.add(newcomer);
}
}
}
});
}
@Override
public void iterate(final Processor<FilePath> iterator) {
if (myProject.isDisposed()) return;
for (VirtualFile root : myAffectedContentRoots) {
THashSet<FilePath> dirsByRoot = myDirtyDirectoriesRecursively.get(root);
if (dirsByRoot != null) {
for (FilePath dir : dirsByRoot) {
final VirtualFile vFile = dir.getVirtualFile();
if (vFile != null && vFile.isValid()) {
myVcsManager.iterateVcsRoot(vFile, iterator);
}
}
}
}
for (VirtualFile root : myAffectedContentRoots) {
final THashSet<FilePath> files = myDirtyFiles.get(root);
if (files != null) {
for (FilePath file : files) {
iterator.process(file);
final VirtualFile vFile = file.getVirtualFile();
if (vFile != null && vFile.isValid() && vFile.isDirectory()) {
for (VirtualFile child : vFile.getChildren()) {
iterator.process(new FilePathImpl(child));
}
}
}
}
}
}
@Override
public void iterateExistingInsideScope(Processor<VirtualFile> processor) {
if (myProject.isDisposed()) return;
for (VirtualFile root : myAffectedContentRoots) {
THashSet<FilePath> dirsByRoot = myDirtyDirectoriesRecursively.get(root);
if (dirsByRoot != null) {
for (FilePath dir : dirsByRoot) {
final VirtualFile vFile = obtainVirtualFile(dir);
if (vFile != null && vFile.isValid()) {
myVcsManager.iterateVfUnderVcsRoot(vFile, processor);
}
}
}
}
for (VirtualFile root : myAffectedContentRoots) {
final THashSet<FilePath> files = myDirtyFiles.get(root);
if (files != null) {
for (FilePath file : files) {
VirtualFile vFile = obtainVirtualFile(file);
if (vFile != null && vFile.isValid()) {
processor.process(vFile);
if (vFile.isDirectory()) {
for (VirtualFile child : vFile.getChildren()) {
processor.process(child);
}
}
}
}
}
}
}
@Nullable
private static VirtualFile obtainVirtualFile(FilePath file) {
VirtualFile vFile = file.getVirtualFile();
return vFile == null ? VfsUtil.findFileByIoFile(file.getIOFile(), false) : vFile;
}
@Override
public boolean isEmpty() {
return myDirtyDirectoriesRecursively.isEmpty() && myDirtyFiles.isEmpty();
}
@Override
public boolean belongsTo(final FilePath path, final Consumer<AbstractVcs> vcsConsumer) {
return ApplicationManager.getApplication().runReadAction(new Computable<Boolean>() {
@Override
public Boolean compute() {
if (myProject.isDisposed()) return Boolean.FALSE;
final VcsRoot rootObject = myVcsManager.getVcsRootObjectFor(path);
if (vcsConsumer != null && rootObject != null) {
vcsConsumer.consume(rootObject.getVcs());
}
if (rootObject == null || rootObject.getVcs() != myVcs) {
return Boolean.FALSE;
}
final VirtualFile vcsRoot = rootObject.getPath();
if (vcsRoot != null) {
for (VirtualFile contentRoot : myAffectedContentRoots) {
// since we don't know exact dirty mechanics, maybe we have 3 nested mappings like:
// /root -> vcs1, /root/child -> vcs2, /root/child/inner -> vcs1, and we have file /root/child/inner/file,
// mapping is detected as vcs1 with root /root/child/inner, but we could possibly have in scope
// "affected root" -> /root with scope = /root recursively
if (VfsUtilCore.isAncestor(contentRoot, vcsRoot, false)) {
THashSet<FilePath> dirsByRoot = myDirtyDirectoriesRecursively.get(contentRoot);
if (dirsByRoot != null) {
for (FilePath filePath : dirsByRoot) {
if (path.isUnder(filePath, false)) return Boolean.TRUE;
}
}
}
}
}
if (!myDirtyFiles.isEmpty()) {
FilePath parent;
VirtualFile vParent = path.getVirtualFileParent();
if (vParent != null && vParent.isValid()) {
parent = new FilePathImpl(vParent);
}
else {
parent = FilePathImpl.create(path.getIOFile().getParentFile());
}
return isInDirtyFiles(path) || isInDirtyFiles(parent);
}
return Boolean.FALSE;
}
}).booleanValue();
}
private boolean isInDirtyFiles(final FilePath path) {
final VcsRoot rootObject = myVcsManager.getVcsRootObjectFor(path);
if (rootObject != null && myVcs.equals(rootObject.getVcs())) {
final THashSet<FilePath> files = myDirtyFiles.get(rootObject.getPath());
if (files != null && files.contains(path)) return true;
}
return false;
}
@Override
public boolean belongsTo(final FilePath path) {
return belongsTo(path, null);
}
@Override @NonNls
public String toString() {
@NonNls StringBuilder result = new StringBuilder("VcsDirtyScope[");
if (!myDirtyFiles.isEmpty()) {
result.append(" files=");
for (THashSet<FilePath> paths : myDirtyFiles.values()) {
for (FilePath file : paths) {
result.append(file).append(" ");
}
}
}
if (!myDirtyDirectoriesRecursively.isEmpty()) {
result.append(" dirs=");
for(THashSet<FilePath> dirsByRoot: myDirtyDirectoriesRecursively.values()) {
for(FilePath file: dirsByRoot) {
result.append(file).append(" ");
}
}
}
result.append("affected roots=");
for (VirtualFile contentRoot : myAffectedContentRoots) {
result.append(contentRoot.getPath()).append(" ");
}
result.append("affected roots DISCLOSED=");
for (VirtualFile contentRoot : getAffectedContentRootsWithCheck()) {
result.append(contentRoot.getPath()).append(" ");
}
result.append("]");
return result.toString();
}
@Override
public VcsDirtyScopeModifier getModifier() {
return myVcsDirtyScopeModifier;
}
@Override
public boolean wasEveryThingDirty() {
return myWasEverythingDirty;
}
@Override
public void setWasEverythingDirty(boolean wasEverythingDirty) {
myWasEverythingDirty = wasEverythingDirty;
}
}