/*******************************************************************************
* Copyright (C) 2011, Dariusz Luksza <dariusz@luksza.org>
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Jens Baumgart <jens.baumgart@sap.com> - initial implementation in IndexDifCacheEntry
* Dariusz Luksza - extraction to separate class
* Andre Bossert <anb0s@anbos.de> - Cleaning up the DecoratableResourceAdapter
*******************************************************************************/
package org.eclipse.egit.core.internal.indexdiff;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceDelta;
import org.eclipse.core.resources.IResourceDeltaVisitor;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.egit.core.Activator;
import org.eclipse.egit.core.internal.util.ResourceUtil;
import org.eclipse.egit.core.project.GitProjectData;
import org.eclipse.egit.core.project.RepositoryMapping;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.Repository;
/**
* Git specific implementation of {@link IResourceDeltaVisitor} that ignores not
* interesting resources. Also collects list of paths and resources to update
*/
public class GitResourceDeltaVisitor implements IResourceDeltaVisitor {
/**
* Bit-mask describing interesting changes for IResourceChangeListener
* events
*/
private static int INTERESTING_CHANGES = IResourceDelta.CONTENT
| IResourceDelta.MOVED_FROM | IResourceDelta.MOVED_TO
| IResourceDelta.OPEN | IResourceDelta.REPLACED
| IResourceDelta.TYPE;
private final Repository repository;
private final Collection<String> filesToUpdate;
private final Collection<IResource> resourcesToUpdate;
private final Map<IProject, IPath> deletedProjects;
private boolean gitIgnoreChanged = false;
private boolean projectDeleted = false;
/**
* Constructs {@link GitResourceDeltaVisitor}
*
* @param repository
* which should be considered during visiting
* {@link IResourceDelta}s
*/
public GitResourceDeltaVisitor(Repository repository) {
this(repository, Collections.<IProject, IPath> emptyMap());
}
/**
* Constructs {@link GitResourceDeltaVisitor}
*
* @param repository
* which should be considered during visiting
* {@link IResourceDelta}s
* @param deletedProjects
* possibly empty map of projects that were removed from the
* workspace, with their (former) locations
*/
public GitResourceDeltaVisitor(Repository repository,
Map<IProject, IPath> deletedProjects) {
this.repository = repository;
filesToUpdate = new HashSet<String>();
resourcesToUpdate = new HashSet<IResource>();
this.deletedProjects = deletedProjects;
}
@Override
public boolean visit(IResourceDelta delta) throws CoreException {
final IResource resource = delta.getResource();
if (resource.getType() == IResource.ROOT) {
return true;
}
if (resource.getType() == IResource.PROJECT) {
if (delta.getKind() == IResourceDelta.REMOVED) {
IPath loc = deletedProjects.remove(resource);
if (loc != null) {
projectDeleted |= !loc.toFile().isDirectory();
}
return false;
}
// If the resource is not part of a project under
// Git revision control or from a different repository
if (!ResourceUtil.isSharedWithGit(resource)) {
// Ignore the change for project and its children
return false;
}
GitProjectData gitData = GitProjectData.get((IProject) resource);
if (gitData == null) {
return false;
}
RepositoryMapping mapping = gitData.getRepositoryMapping(resource);
if (mapping == null || !gitData.hasInnerRepositories()
&& mapping.getRepository() != repository) {
return false;
}
// continue with children
return true;
}
Repository repositoryOfResource = null;
if (resource.isLinked()) {
IPath location = resource.getLocation();
if (location == null) {
return false;
}
repositoryOfResource = ResourceUtil.getRepository(location);
// Ignore linked files, folders and their children, if they're not
// in the same repository
if (repository != repositoryOfResource) {
return false;
}
} else {
repositoryOfResource = ResourceUtil.getRepository(resource);
}
if (resource.getType() == IResource.FOLDER) {
GitProjectData gitData = GitProjectData.get(resource.getProject());
if (gitData == null) {
return false;
}
if (repositoryOfResource == null || !gitData.isProtected(resource)
&& repositoryOfResource != repository) {
return false;
}
if (delta.getKind() == IResourceDelta.ADDED) {
IPath repoRelativePath = ResourceUtil.getRepositoryRelativePath(
resource.getLocation(), repository);
if (repoRelativePath == null) {
return false;
}
if (!repoRelativePath.isEmpty()) {
String path = repoRelativePath.toPortableString() + "/"; //$NON-NLS-1$
if (isIgnoredInOldIndex(path)) {
return true; // keep going to catch .gitignore files.
}
filesToUpdate.add(path);
resourcesToUpdate.add(resource);
}
}
// continue with children
return true;
}
if (repositoryOfResource != repository) {
return false;
}
if (!isInteresting(delta)) {
return false;
}
if (resource.getName().equals(Constants.DOT_GIT_IGNORE)) {
gitIgnoreChanged = true;
return false;
}
IPath repoRelativePath = ResourceUtil
.getRepositoryRelativePath(resource.getLocation(), repository);
if (repoRelativePath == null) {
resourcesToUpdate.add(resource);
return true;
}
String path = repoRelativePath.toPortableString();
if (isIgnoredInOldIndex(path)) {
// This file is ignored in the old index, and ignore rules did not
// change: ignore the delta to avoid unnecessary index updates
return false;
}
filesToUpdate.add(path);
resourcesToUpdate.add(resource);
return true;
}
/**
* If the file has changed but not in a way that we care about (e.g. marker
* changes to files) then ignore
*
* @param delta
* @return true - if change is interesting
*/
static boolean isInteresting(IResourceDelta delta) {
if (delta.getKind() == IResourceDelta.CHANGED
&& (delta.getFlags() & INTERESTING_CHANGES) == 0) {
return false;
}
return true;
}
/**
* @param path
* the repository relative path of the resource to check
* @return whether the given path is ignored by the given
* {@link IndexDiffCacheEntry}
*/
private boolean isIgnoredInOldIndex(String path) {
if (gitIgnoreChanged) {
return false;
}
IndexDiffCacheEntry entry = null;
IndexDiffCache cache = Activator.getDefault().getIndexDiffCache();
if (cache != null) {
entry = cache.getIndexDiffCacheEntry(repository);
}
// fall back to processing all changes as long as there is no old index.
if (entry == null) {
return false;
}
IndexDiffData indexDiff = entry.getIndexDiff();
if (indexDiff == null) {
return false;
}
String p = path;
Set<String> ignored = indexDiff.getIgnoredNotInIndex();
while (p != null) {
if (ignored.contains(p)) {
return true;
}
p = skipLastSegment(p);
}
return false;
}
private String skipLastSegment(String path) {
int slashPos = path.lastIndexOf('/');
return slashPos == -1 ? null : path.substring(0, slashPos);
}
/**
* @return collection of files to update
*/
public Collection<IFile> getFileResourcesToUpdate() {
Collection<IFile> result = new ArrayList<IFile>();
for (IResource resource : resourcesToUpdate)
if (resource instanceof IFile)
result.add((IFile) resource);
return result;
}
/**
* @return collection of resources to update
*/
public Collection<IResource> getResourcesToUpdate() {
return resourcesToUpdate;
}
/**
* @return collection of files / folders to update. Folder paths end with /
*/
public Collection<String> getFilesToUpdate() {
return filesToUpdate;
}
/**
* @return {@code true} when content .gitignore file changed, {@code false}
* otherwise
*/
public boolean getGitIgnoreChanged() {
return gitIgnoreChanged;
}
/**
* Returns whether a project was deleted.
*
* @return {@code true} if a project in the repository was deleted,
* {@code false} otherwise
*/
public boolean isProjectDeleted() {
return projectDeleted;
}
}