/*******************************************************************************
* Copyright (C) 2007, IBM Corporation and others
* Copyright (C) 2007, Dave Watson <dwatson@mimvista.com>
* Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
* Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
* Copyright (C) 2008, Google Inc.
* Copyright (C) 2008, Tor Arne Vestbø <torarnv@gmail.com>
* Copyright (C) 2011, Dariusz Luksza <dariusz@luksza.org>
* Copyright (C) 2011, Christian Halstrick <christian.halstrick@sap.com>
* Copyright (C) 2015, Thomas Wolf <thomas.wolf@paranor.ch>
*
* 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:
* Thomas Wolf <thomas.wolf@paranor.ch> - Factored out from DecoratableResourceAdapter
* and GitLightweightDecorator
*******************************************************************************/
package org.eclipse.egit.ui.internal.resources;
import java.io.File;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.HashSet;
import java.util.Set;
import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.egit.core.Activator;
import org.eclipse.egit.core.internal.indexdiff.IndexDiffCacheEntry;
import org.eclipse.egit.core.internal.indexdiff.IndexDiffData;
import org.eclipse.egit.core.internal.util.ResourceUtil;
import org.eclipse.egit.ui.internal.resources.IResourceState.StagingState;
import org.eclipse.jgit.annotations.NonNull;
import org.eclipse.jgit.annotations.Nullable;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.Repository;
/**
* Factory for creating {@link IResourceState}s.
*/
public class ResourceStateFactory {
/**
* {@link IResourceState} returned when no information can be retrieved. All
* boolean getters return {@code false}, and the
* {@link IResourceState.StagingState StagingState} is
* {@link IResourceState.StagingState#NOT_STAGED NOT_STAGED}.
*/
@NonNull
public static final IResourceState UNKNOWN_STATE = new ResourceState();
@NonNull
private static final ResourceStateFactory INSTANCE = new ResourceStateFactory();
/**
* Retrieves the singleton instance of the {@link ResourceStateFactory}.
*
* @return the factory singleton
*/
@NonNull
public static ResourceStateFactory getInstance() {
return INSTANCE;
}
/**
* Returns the {@link IndexDiffData} for a given {@link IResource}, provided
* the resource exists and belongs to a git-tracked project.
*
* @param resource
* context to get the repository to get the index diff data from
* @return the IndexDiffData, or {@code null} if none.
*/
@Nullable
public IndexDiffData getIndexDiffDataOrNull(@Nullable IResource resource) {
if (resource == null || resource.getType() == IResource.ROOT
|| !ResourceUtil.isSharedWithGit(resource)) {
return null;
}
Repository repository = ResourceUtil.getRepository(resource);
return getIndexDiffDataOrNull(repository);
}
/**
* Returns the {@link IndexDiffData} for a given {@link File}, provided the
* file is in a git repository working tree.
*
* @param file
* context to get the repository to get the index diff data from
* @return the IndexDiffData, or {@code null} if none.
*/
@Nullable
public IndexDiffData getIndexDiffDataOrNull(@Nullable File file) {
if (file == null) {
return null;
}
File absoluteFile = file.getAbsoluteFile();
IPath path = new org.eclipse.core.runtime.Path(absoluteFile.getPath());
Repository repository = ResourceUtil.getRepository(path);
return getIndexDiffDataOrNull(repository);
}
/**
* Returns the {@link IndexDiffData} for a given {@link Repository}.
*
* @param repository
* to get the index diff data from
* @return the IndexDiffData, or {@code null} if none.
*/
@Nullable
private IndexDiffData getIndexDiffDataOrNull(
@Nullable Repository repository) {
if (repository == null) {
return null;
} else if (repository.isBare()) {
// For bare repository just return empty data
return new IndexDiffData();
}
IndexDiffCacheEntry diffCacheEntry = Activator.getDefault()
.getIndexDiffCache().getIndexDiffCacheEntry(repository);
if (diffCacheEntry == null) {
return null;
}
return diffCacheEntry.getIndexDiff();
}
/**
* Determines the repository state of the given {@link IResource}.
*
* @param resource
* to get the state for
* @return the state, {@link #UNKNOWN_STATE} if none can be determined.
*/
@NonNull
public IResourceState get(@Nullable IResource resource) {
IndexDiffData indexDiffData = getIndexDiffDataOrNull(resource);
if (indexDiffData == null || resource == null) {
return UNKNOWN_STATE;
}
return get(indexDiffData, resource);
}
/**
* Determines the repository state of the given {@link File}.
*
* @param file
* to get the state for
* @return the state, {@link #UNKNOWN_STATE} if none can be determined.
*/
@NonNull
public IResourceState get(@Nullable File file) {
IndexDiffData indexDiffData = getIndexDiffDataOrNull(file);
if (indexDiffData == null || file == null) {
return UNKNOWN_STATE;
}
return get(indexDiffData, file);
}
/**
* Computes an {@link IResourceState} for the given {@link IResource} from
* the given {@link IndexDiffData}.
*
* @param indexDiffData
* to compute the state from
* @param resource
* to get the state of
* @return the state
*/
@NonNull
public IResourceState get(@NonNull IndexDiffData indexDiffData,
@NonNull IResource resource) {
IPath path = resource.getLocation();
if (path != null) {
return get(indexDiffData, new ResourceItem(resource));
}
return UNKNOWN_STATE;
}
/**
* Computes an {@link IResourceState} for the given {@link File} from the
* given {@link IndexDiffData}.
*
* @param indexDiffData
* to compute the state from
* @param file
* to get the state of
* @return the state
*/
@NonNull
public IResourceState get(@NonNull IndexDiffData indexDiffData,
@NonNull File file) {
return get(indexDiffData, new FileItem(file));
}
/**
* Computes an {@link IResourceState} for the given {@link FileSystemItem}
* from the given {@link IndexDiffData}.
*
* @param indexDiffData
* to compute the state from
* @param file
* to get the state of
* @return the state
*/
@NonNull
private IResourceState get(@NonNull IndexDiffData indexDiffData,
@NonNull FileSystemItem file) {
IPath path = file.getAbsolutePath();
if (path == null) {
return UNKNOWN_STATE;
}
Repository repository = file.getRepository();
if (repository == null || repository.isBare()) {
return UNKNOWN_STATE;
}
File workTree = repository.getWorkTree();
String repoRelativePath = path.makeRelativeTo(
new org.eclipse.core.runtime.Path(workTree.getAbsolutePath()))
.toString();
if (repoRelativePath.equals(path.toString())) {
// Could not be made relative.
return UNKNOWN_STATE;
}
ResourceState result = new ResourceState();
if (file.isContainer()) {
if (!repoRelativePath.endsWith("/")) { //$NON-NLS-1$
repoRelativePath += '/';
}
if (ResourceUtil.isSymbolicLink(repository, repoRelativePath)) {
// The Eclipse resource model handles a symlink to a folder like
// the container it refers to but git status handles the symlink
// source like a special file.
extractFileProperties(indexDiffData, repoRelativePath, result);
} else {
extractContainerProperties(indexDiffData, repoRelativePath,
file, result);
}
} else {
extractFileProperties(indexDiffData, repoRelativePath, result);
}
return result;
}
private void extractFileProperties(@NonNull IndexDiffData indexDiffData,
@NonNull String repoRelativePath, @NonNull ResourceState state) {
Set<String> ignoredFiles = indexDiffData.getIgnoredNotInIndex();
boolean ignored = ignoredFiles.contains(repoRelativePath)
|| containsPrefixPath(ignoredFiles, repoRelativePath);
state.setIgnored(ignored);
Set<String> untracked = indexDiffData.getUntracked();
state.setTracked(!ignored && !untracked.contains(repoRelativePath));
Set<String> added = indexDiffData.getAdded();
Set<String> removed = indexDiffData.getRemoved();
Set<String> changed = indexDiffData.getChanged();
if (added.contains(repoRelativePath)) {
state.setStagingState(StagingState.ADDED);
} else if (removed.contains(repoRelativePath)) {
state.setStagingState(StagingState.REMOVED);
} else if (changed.contains(repoRelativePath)) {
state.setStagingState(StagingState.MODIFIED);
} else {
state.setStagingState(StagingState.NOT_STAGED);
}
// conflicting
Set<String> conflicting = indexDiffData.getConflicting();
state.setConflicts(conflicting.contains(repoRelativePath));
// locally modified
Set<String> modified = indexDiffData.getModified();
state.setDirty(modified.contains(repoRelativePath));
// locally deleted
Set<String> missing = indexDiffData.getMissing();
state.setMissing(missing.contains(repoRelativePath));
Set<String> assumeUnchanged = indexDiffData.getAssumeUnchanged();
state.setAssumeUnchanged(assumeUnchanged.contains(repoRelativePath));
}
private void extractContainerProperties(
@NonNull IndexDiffData indexDiffData,
@NonNull String repoRelativePath, @NonNull FileSystemItem directory,
@NonNull ResourceState state) {
Set<String> ignoredFiles = indexDiffData.getIgnoredNotInIndex();
Set<String> untrackedFolders = indexDiffData.getUntrackedFolders();
boolean ignored = containsPrefixPath(ignoredFiles, repoRelativePath)
|| !directory.hasContainerAnyFiles();
state.setIgnored(ignored);
state.setTracked(!ignored
&& !containsPrefixPath(untrackedFolders, repoRelativePath));
// containers are marked as staged whenever file was added, removed or
// changed
Set<String> changed = new HashSet<>(indexDiffData.getChanged());
changed.addAll(indexDiffData.getAdded());
changed.addAll(indexDiffData.getRemoved());
if (containsPrefix(changed, repoRelativePath)) {
state.setStagingState(StagingState.MODIFIED);
} else {
state.setStagingState(StagingState.NOT_STAGED);
}
// conflicting
Set<String> conflicting = indexDiffData.getConflicting();
state.setConflicts(containsPrefix(conflicting, repoRelativePath));
// locally modified / untracked
Set<String> modified = indexDiffData.getModified();
Set<String> untracked = indexDiffData.getUntracked();
Set<String> missing = indexDiffData.getMissing();
state.setDirty(containsPrefix(modified, repoRelativePath)
|| containsPrefix(untracked, repoRelativePath)
|| containsPrefix(missing, repoRelativePath));
}
private boolean containsPrefix(Set<String> collection, String prefix) {
// when prefix is empty we are handling repository root, therefore we
// should return true whenever collection isn't empty
if (prefix.length() == 1 && !collection.isEmpty())
return true;
for (String path : collection)
if (path.startsWith(prefix))
return true;
return false;
}
private boolean containsPrefixPath(Set<String> collection, String path) {
for (String entry : collection) {
String entryPath;
if (entry.endsWith("/")) //$NON-NLS-1$
entryPath = entry;
else
entryPath = entry + "/"; //$NON-NLS-1$
if (path.startsWith(entryPath))
return true;
}
return false;
}
private interface FileSystemItem {
boolean hasContainerAnyFiles();
boolean isContainer();
@Nullable
IPath getAbsolutePath();
@Nullable
Repository getRepository();
}
private static class FileItem implements FileSystemItem {
@NonNull
private final File file;
public FileItem(@NonNull File file) {
this.file = file;
}
@Override
@NonNull
public IPath getAbsolutePath() {
return new org.eclipse.core.runtime.Path(file.getAbsolutePath());
}
@Override
public Repository getRepository() {
return ResourceUtil.getRepository(getAbsolutePath());
}
@Override
public boolean isContainer() {
return file.isDirectory();
}
@Override
public boolean hasContainerAnyFiles() {
if (!isContainer()) {
throw new IllegalArgumentException("Container expected"); //$NON-NLS-1$
}
try {
final boolean[] result = new boolean[] { false };
final Path dotGit = Paths.get(Constants.DOT_GIT);
Files.walkFileTree(file.toPath(), new FileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir,
BasicFileAttributes attrs) throws IOException {
if (dotGit.equals(dir.getFileName())) {
return FileVisitResult.SKIP_SUBTREE;
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path path,
BasicFileAttributes attrs) throws IOException {
if (!attrs.isDirectory()) {
result[0] = true;
return FileVisitResult.TERMINATE;
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path path,
IOException exc) throws IOException {
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir,
IOException exc) throws IOException {
return FileVisitResult.CONTINUE;
}
});
return result[0];
} catch (IOException e) {
// if can't get any info, treat as with file
return true;
}
}
}
private static class ResourceItem implements FileSystemItem {
@NonNull
private final IResource resource;
public ResourceItem(@NonNull IResource resource) {
this.resource = resource;
}
@Override
@Nullable
public IPath getAbsolutePath() {
return resource.getLocation();
}
@Override
public Repository getRepository() {
return ResourceUtil.getRepository(resource);
}
@Override
public boolean isContainer() {
return isContainer(resource);
}
@Override
public boolean hasContainerAnyFiles() {
return containsFiles(resource);
}
private boolean isContainer(IResource rsc) {
int type = rsc.getType();
return type == IResource.FOLDER || type == IResource.PROJECT
|| type == IResource.ROOT;
}
private boolean containsFiles(IResource rsc) {
if (rsc instanceof IContainer) {
IContainer container = (IContainer) rsc;
try {
return anyFile(container.members());
} catch (CoreException e) {
// if can't get any info, treat as with file
return true;
}
}
throw new IllegalArgumentException(
"Expected a container resource."); //$NON-NLS-1$
}
private boolean anyFile(IResource[] members) {
for (IResource member : members) {
if (member.getType() == IResource.FILE) {
return true;
} else if (isContainer(member) && containsFiles(member)) {
return true;
}
}
return false;
}
}
}