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