/******************************************************************************* * Copyright (C) 2011, Jens Baumgart <jens.baumgart@sap.com> * Copyright (C) 2012, 2013 Robin Stocker <robin@nibor.org> * Copyright (C) 2012, 2015 Laurent Goubet <laurent.goubet@obeo.fr> * Copyright (C) 2012, Gunnar Wagenknecht <gunnar@wagenknecht.org> * Copyright (C) 2016, 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 *******************************************************************************/ package org.eclipse.egit.core.internal.util; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.net.URI; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; import org.eclipse.core.filesystem.URIUtil; import org.eclipse.core.resources.IContainer; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.IWorkspaceRoot; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.resources.mapping.IModelProviderDescriptor; import org.eclipse.core.resources.mapping.ModelProvider; import org.eclipse.core.resources.mapping.ResourceMapping; import org.eclipse.core.resources.mapping.ResourceMappingContext; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.core.runtime.Path; import org.eclipse.core.runtime.QualifiedName; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.egit.core.Activator; import org.eclipse.egit.core.GitProvider; import org.eclipse.egit.core.RepositoryCache; import org.eclipse.egit.core.internal.CoreText; import org.eclipse.egit.core.internal.indexdiff.IndexDiffCacheEntry; import org.eclipse.egit.core.internal.indexdiff.IndexDiffData; import org.eclipse.egit.core.project.RepositoryMapping; import org.eclipse.egit.core.synchronize.IgnoreInGitSynchronizations; import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.annotations.Nullable; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.util.FS; import org.eclipse.team.core.RepositoryProvider; /** * Resource utilities * */ public class ResourceUtil { // The id used to associate a provider with a project, see // TeamPlugin.PROVIDER_PROP_KEY private final static QualifiedName PROVIDER_PROP_KEY = new QualifiedName( "org.eclipse.team.core", "repository"); //$NON-NLS-1$ //$NON-NLS-2$ // Our own session property to cache the provider ID of the configured // repository provider. private final static QualifiedName PROVIDER_ID = new QualifiedName( "org.eclipse.egit.core", "repositoryProviderID"); //$NON-NLS-1$ //$NON-NLS-2$ // Value for PROVIDER_ID to mark unshared projects. private final static Object PROJECT_IS_UNSHARED = new Object(); /** * Return the corresponding resource if it exists and has the Git repository * provider. * <p> * The returned file will be relative to the most nested non-closed * Git-managed project. * * @param location * the path to check * @param innerMost * check if there are multiple candidates in the workspace and * return innermost resource. <b>Note</b>, this check is * expensive and should not be used in performance critical code. * @return the resources, or null */ @Nullable public static IResource getResourceForLocation(@NonNull IPath location, boolean innerMost) { IFile file = getFileForLocation(location, innerMost); if (file != null) { return file; } return getContainerForLocation(location, innerMost); } /** * Return the corresponding file if it exists and has the Git repository * provider. * <p> * If checkNested argument is true, the returned file will be relative to * the most nested non-closed Git-managed project. * * @param location * @param innerMost * check if there are multiple candidates in the workspace and * return innermost resource. <b>Note</b>, this check is * expensive and should not be used in performance critical code. * @return the file, or null */ @Nullable public static IFile getFileForLocation(@NonNull IPath location, boolean innerMost) { IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot(); IFile file = root.getFileForLocation(location); if (file == null) { return null; } if (!innerMost && isValid(file)) { return file; } URI uri = URIUtil.toURI(location); IFile file2 = getFileForLocationURI(root, uri); if (file2 == null && isValid(file)) { return file; } return file2; } /** * sort out closed, linked or not shared resources * * @param resource * @return true if the resource is shared with git, not a link and * accessible in Eclipse */ private static boolean isValid(@NonNull IResource resource) { return resource.isAccessible() && !resource.isLinked(IResource.CHECK_ANCESTORS) && isSharedWithGit(resource); } /** * @param resource * non null * @return true if the project is configured with git provider */ public static boolean isSharedWithGit(@NonNull IResource resource) { IProject project = resource.getProject(); if (project == null || !project.isAccessible()) { return false; } try { // Look for an existing provider GitProvider provider = lookupProviderProp(project); if (provider != null || MappingJob.isKnownGitProject(project)) { // Is mapped or in the process of being mapped. return true; } else if (isMarkedAsNotSharedWithGit(project)) { return false; } // There isn't one so check the persistent property String existingID = project .getPersistentProperty(PROVIDER_PROP_KEY); if (existingID == null) { markAsUnshared(project); } else { markAsShared(project, existingID); } boolean isGitProvider = GitProvider.ID.equals(existingID); if (isGitProvider) { MappingJob.initProviderAsynchronously(project); } return isGitProvider; } catch (CoreException e) { Activator.getDefault().getLog().log(e.getStatus()); return false; } } private static class MappingJob extends Job { private final static MappingJob INSTANCE = new MappingJob(); public static void initProviderAsynchronously( @NonNull IProject project) { synchronized (INSTANCE.projects) { if (!INSTANCE.projects.add(project)) { return; } } INSTANCE.schedule(); } public static boolean isKnownGitProject(@NonNull IProject project) { synchronized (INSTANCE.projects) { return INSTANCE.projects.contains(project); } } HashSet<IProject> projects = new LinkedHashSet<>(); public MappingJob() { super(CoreText.ResourceUtil_mapProjectJob); setSystem(true); setUser(false); } @Override protected IStatus run(IProgressMonitor monitor) { HashSet<IProject> work; synchronized (projects) { work = new LinkedHashSet<>(projects); } for (IProject project : work) { if (monitor.isCanceled()) { break; } // this will instantiate and map the provider (can lock!) RepositoryProvider.getProvider(project, GitProvider.ID); } synchronized (projects) { if (monitor.isCanceled()) { projects.clear(); } else { projects.removeAll(work); } if (!projects.isEmpty()) { schedule(); } } return Status.OK_STATUS; } } /** * Returns git provider if associated with the given project or * <code>null</code> if the project is not associated with a provider or the * provider is not fully loaded yet. To check if the git provider is * associated with the project, use {@link #isSharedWithGit(IResource)}. * * @param project * the project to query for a provider * @return the repository provider or null */ @Nullable final public static GitProvider getGitProvider(@NonNull IProject project) { if (!project.isAccessible()) { return null; } try { // Look for an existing provider GitProvider provider = lookupProviderProp(project); if (provider != null) { return provider; } if (MappingJob.isKnownGitProject(project) || isMarkedAsNotSharedWithGit(project)) { // Is in the process of being mapped, but isn't mapped yet, or // isn't shared with us at all. return null; } String existingID = project .getPersistentProperty(PROVIDER_PROP_KEY); if (existingID == null) { markAsUnshared(project); } else { markAsShared(project, existingID); boolean isGitProvider = GitProvider.ID.equals(existingID); if (isGitProvider) { MappingJob.initProviderAsynchronously(project); } } // not loaded yet, but we can't load it because it will use locks // or not a GitProvider return null; } catch (CoreException e) { Activator.getDefault().getLog().log(e.getStatus()); } return null; } /* * Return the provider mapped to project, or null if none; */ @Nullable private static GitProvider lookupProviderProp(IProject project) throws CoreException { Object provider = project.getSessionProperty(PROVIDER_PROP_KEY); if (provider != null) { if (provider instanceof RepositoryProvider) { markAsShared(project, ((RepositoryProvider) provider).getID()); if (provider instanceof GitProvider) { return (GitProvider) provider; } } else { // Must be the RepositoryProvider's NOT_MAPPED marker markAsUnshared(project); } } return null; } /** * Sets the session property {@link #PROVIDER_ID} to * {@link #PROJECT_IS_UNSHARED} to indicate that the project is not shared * at all. * * @param project * to mark */ private static void markAsUnshared(@NonNull IProject project) { try { project.setSessionProperty(PROVIDER_ID, PROJECT_IS_UNSHARED); } catch (CoreException e) { // Ignore since this is "only" an optimization } } /** * Sets the session property {@link #PROVIDER_ID} to the given * {@code providerId}, or removes the property if the id is {@code null}. * * @param project * to mark * @param providerId * Id of the {@link RepositoryProvider} associated with the * project, if known, or {@code null} otherwise. */ private static void markAsShared(@NonNull IProject project, @Nullable String providerId) { try { project.setSessionProperty(PROVIDER_ID, providerId); } catch (CoreException e) { // Ignore since this is "only" an optimization } } /** * Tests the session property {@link #PROVIDER_ID}. * * @param project * to test * @return {@code true} is the project is marked as known to be unshared */ private static boolean isMarkedAsNotSharedWithGit( @NonNull IProject project) { try { Object property = project.getSessionProperty(PROVIDER_ID); if (property == PROJECT_IS_UNSHARED) { return true; } else if (property instanceof String && !GitProvider.ID.equals(property)) { return true; } } catch (CoreException e) { // Ignore and fall through } return false; } /** * Return the corresponding container if it exists and has the Git * repository provider. * <p> * The returned container will be relative to the most nested non-closed * Git-managed project. * * @param location * @param innerMost * check if there are multiple candidates in the workspace and * return innermost resource. <b>Note</b>, this check is * expensive and should not be used in performance critical code. * @return the container, or null */ @Nullable public static IContainer getContainerForLocation(@NonNull IPath location, boolean innerMost) { IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot(); IContainer dir = root.getContainerForLocation(location); if (dir == null) { return null; } if (!innerMost && isValid(dir)) { return dir; } URI uri = URIUtil.toURI(location); IContainer dir2 = getContainerForLocationURI(root, uri); if (dir2 == null && isValid(dir)) { return dir; } return dir2; } /** * Get the {@link IFile} corresponding to the arguments if it exists and has * the Git repository provider. * <p> * The returned file will be relative to the most nested non-closed * Git-managed project. * * @param repository * the repository of the file * @param repoRelativePath * the repository-relative path of the file to search for * @param innerMost * check if there are multiple candidates in the workspace and * return innermost resource. <b>Note</b>, this check is * expensive and should not be used in performance critical code. * @return the IFile corresponding to this path, or null */ @Nullable public static IFile getFileForLocation(@NonNull Repository repository, @NonNull String repoRelativePath, boolean innerMost) { IPath path = new Path(repository.getWorkTree().getAbsolutePath()).append(repoRelativePath); return getFileForLocation(path, innerMost); } /** * Get the {@link IContainer} corresponding to the arguments, using * {@link IWorkspaceRoot#getContainerForLocation(org.eclipse.core.runtime.IPath)} * . * * @param repository * the repository * @param repoRelativePath * the repository-relative path of the container to search for * @return the IContainer corresponding to this path, or null */ @Nullable public static IContainer getContainerForLocation( @NonNull Repository repository, @NonNull String repoRelativePath) { IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot(); IPath path = new Path(repository.getWorkTree().getAbsolutePath()).append(repoRelativePath); return root.getContainerForLocation(path); } /** * Checks if the path relative to the given repository refers to a symbolic * link * * @param repository * the repository of the file * @param repoRelativePath * the repository-relative path of the file to search for * @return {@code true} if the path in the given repository refers to a * symbolic link */ public static boolean isSymbolicLink(@NonNull Repository repository, @NonNull String repoRelativePath) { try { File f = new Path(repository.getWorkTree().getAbsolutePath()) .append((repoRelativePath)).toFile(); return FS.DETECTED.isSymLink(f); } catch (IOException e) { return false; } } /** * Returns a resource handle for this path in the workspace. Note that * neither the resource nor the result need exist in the workspace : this * may return inexistant or otherwise non-accessible IResources. * * @param path * Path for which we need a resource handle. * @return The resource handle for the given path in the workspace. */ @NonNull public static IResource getResourceHandleForLocation(@NonNull IPath path) { final IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace() .getRoot(); final IResource resource; if (path.segmentCount() > 1) resource = workspaceRoot.getFile(path); else resource = workspaceRoot.getProject(path.toString()); return resource; } /** * The method splits the given resources by their repository. For each * occurring repository a list is built containing the repository relative * paths of the related resources. * <p> * When one of the passed resources corresponds to the working directory, * <code>""</code> will be returned as part of the collection. * * @param resources * @return a map containing a list of repository relative paths for each * occurring repository */ public static Map<Repository, Collection<String>> splitResourcesByRepository( Collection<IResource> resources) { Map<Repository, Collection<String>> result = new HashMap<Repository, Collection<String>>(); for (IResource resource : resources) { RepositoryMapping repositoryMapping = RepositoryMapping .getMapping(resource); if (repositoryMapping == null) continue; String path = repositoryMapping.getRepoRelativePath(resource); addPathToMap(repositoryMapping.getRepository(), path, result); } return result; } /** * @see #splitResourcesByRepository(Collection) * @param resources * @return a map containing a list of repository relative paths for each * occurring repository */ public static Map<Repository, Collection<String>> splitResourcesByRepository( IResource[] resources) { return splitResourcesByRepository(Arrays.asList(resources)); } /** * The method splits the given paths by their repository. For each occurring * repository a list is built containing the repository relative paths of * the related resources. * <p> * When one of the passed paths corresponds to the working directory, * <code>""</code> will be returned as part of the collection. * * @param paths * @return a map containing a list of repository relative paths for each * occurring repository */ public static Map<Repository, Collection<String>> splitPathsByRepository( Collection<IPath> paths) { RepositoryCache repositoryCache = Activator.getDefault() .getRepositoryCache(); Map<Repository, Collection<String>> result = new HashMap<Repository, Collection<String>>(); for (IPath path : paths) { Repository repository = repositoryCache.getRepository(path); if (repository != null) { IPath repoPath = new Path(repository.getWorkTree() .getAbsolutePath()); IPath repoRelativePath = path.makeRelativeTo(repoPath); addPathToMap(repository, repoRelativePath.toString(), result); } } return result; } /** * Determine if given resource is imported into workspace or not * * @param resource * @return {@code true} when given resource is not imported into workspace, * {@code false} otherwise */ public static boolean isNonWorkspace(@NonNull IResource resource) { return resource.getLocation() == null; } private static IFile getFileForLocationURI(@NonNull IWorkspaceRoot root, @NonNull URI uri) { IFile[] files = root.findFilesForLocationURI(uri); return getExistingMappedResourceWithShortestPath(files); } private static IContainer getContainerForLocationURI(IWorkspaceRoot root, @NonNull URI uri) { IContainer[] containers = root.findContainersForLocationURI(uri); return getExistingMappedResourceWithShortestPath(containers); } private static <T extends IResource> T getExistingMappedResourceWithShortestPath( T[] resources) { int shortestPathSegmentCount = Integer.MAX_VALUE; T shortestPath = null; for (T resource : resources) { if (!resource.exists()) { continue; } if (!isSharedWithGit(resource)) { continue; } IPath fullPath = resource.getFullPath(); int segmentCount = fullPath.segmentCount(); if (segmentCount < shortestPathSegmentCount) { shortestPath = resource; shortestPathSegmentCount = segmentCount; } } return shortestPath; } private static void addPathToMap(@NonNull Repository repository, @Nullable String path, Map<Repository, Collection<String>> result) { if (path != null) { Collection<String> resourcesList = result.get(repository); if (resourcesList == null) { resourcesList = new ArrayList<String>(); result.put(repository, resourcesList); } resourcesList.add(path); } } /** * This will query all model providers for those that are enabled on the * given resource and list all mappings available for that resource. * * @param resource * The resource for which we need the associated resource * mappings. * @param context * Context from which remote content could be retrieved. * @return All mappings available for that file. */ public static ResourceMapping[] getResourceMappings( @NonNull IResource resource, ResourceMappingContext context) { final IModelProviderDescriptor[] modelDescriptors = ModelProvider .getModelProviderDescriptors(); final Set<ResourceMapping> mappings = new LinkedHashSet<ResourceMapping>(); for (IModelProviderDescriptor candidate : modelDescriptors) { try { final IResource[] resources = candidate .getMatchingResources(new IResource[] { resource, }); if (resources.length > 0) { // get mappings from model provider if there are matching resources final ModelProvider model = candidate.getModelProvider(); IgnoreInGitSynchronizations adapter = model .getAdapter(IgnoreInGitSynchronizations.class); if (adapter != null) { continue; } final ResourceMapping[] modelMappings = model.getMappings( resource, context, new NullProgressMonitor()); for (ResourceMapping mapping : modelMappings) mappings.add(mapping); } } catch (CoreException e) { Activator.logError(e.getMessage(), e); } } return mappings.toArray(new ResourceMapping[mappings.size()]); } /** * Save local history. * * @param repository */ public static void saveLocalHistory(@NonNull Repository repository) { IndexDiffCacheEntry indexDiffCacheEntry = org.eclipse.egit.core.Activator .getDefault().getIndexDiffCache() .getIndexDiffCacheEntry(repository); if (indexDiffCacheEntry == null) { return; } IndexDiffData indexDiffData = indexDiffCacheEntry.getIndexDiff(); if (indexDiffData != null) { Collection<IResource> changedResources = indexDiffData .getChangedResources(); for (IResource changedResource : changedResources) { if (changedResource instanceof IFile && changedResource.exists()) { try { ResourceUtil.saveLocalHistory(changedResource); } catch (CoreException e) { // Ignore error. Failure to save local history must // not interfere with the operation. Activator.logError(MessageFormat.format( CoreText.ResourceUtil_SaveLocalHistoryFailed, changedResource), e); } } } } } private static void saveLocalHistory(@NonNull IResource resource) throws CoreException { if (!resource.isSynchronized(IResource.DEPTH_ZERO)) resource.refreshLocal(IResource.DEPTH_ZERO, null); // Dummy update to force save for local history. ((IFile) resource).appendContents( new ByteArrayInputStream(new byte[0]), IResource.KEEP_HISTORY, null); } /** * Determines the repository containing the resource. * * @param resource * to get the repository for * @return the {@link Repository}, or {@code null} if none found. */ @Nullable public static Repository getRepository(@NonNull IResource resource) { RepositoryMapping mapping = RepositoryMapping.getMapping(resource); if (mapping != null) { return mapping.getRepository(); } return Activator.getDefault().getRepositoryCache() .getRepository(resource); } /** * Determines the repository containing the given {@link IPath}. * * @param path * to get the repository for * @return the {@link Repository}, or {@code null} if none found. */ @Nullable public static Repository getRepository(@NonNull IPath path) { return Activator.getDefault().getRepositoryCache().getRepository(path); } /** * Makes a given path relative to the working directory of the given * repository. If the repository is bare or the path is {@code null} or is * not in that working directory, {@code null} is returned. Returns an empty * path if the given path <em>is</em> the working directory. * * @param path * to make relative * @param repository * to make the path relative to * @return the repository-relative path, or {@code null} if the path is not * inside the repository's working directory. */ @Nullable public static IPath getRepositoryRelativePath(@Nullable IPath path, @NonNull Repository repository) { if (path == null || repository.isBare()) { return null; } java.nio.file.Path workingDirectory = repository.getWorkTree().toPath(); java.nio.file.Path toRelativize = path.toFile().toPath(); if (toRelativize.startsWith(workingDirectory)) { int n = workingDirectory.getNameCount(); int m = toRelativize.getNameCount(); if (n == m) { return new Path(""); //$NON-NLS-1$ } return Path.fromOSString(toRelativize.subpath(n, m).toString()); } return null; } }