/******************************************************************************* * 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) 2015, IBM Corporation (Dani Megert <daniel_megert@ch.ibm.com>) * Copyright (C) 2016, Thomas Wolf <thomas.wolf@paranor.ch> * Copyright (C) 2016, Andre Bossert <anb0s@anbos.de> * * 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.project; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.text.MessageFormat; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Properties; import java.util.Set; import org.eclipse.core.resources.IContainer; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.IResourceChangeEvent; import org.eclipse.core.resources.IResourceChangeListener; import org.eclipse.core.resources.IResourceDelta; import org.eclipse.core.resources.IResourceDeltaVisitor; import org.eclipse.core.resources.ResourcesPlugin; 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.QualifiedName; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.core.runtime.preferences.DefaultScope; import org.eclipse.core.runtime.preferences.IEclipsePreferences; import org.eclipse.core.runtime.preferences.InstanceScope; import org.eclipse.egit.core.Activator; import org.eclipse.egit.core.GitCorePreferences; import org.eclipse.egit.core.JobFamilies; import org.eclipse.egit.core.internal.CoreText; import org.eclipse.egit.core.internal.Utils; import org.eclipse.egit.core.internal.trace.GitTraceLocation; import org.eclipse.egit.core.internal.util.ResourceUtil; import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.annotations.Nullable; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.RepositoryCache; import org.eclipse.jgit.storage.file.FileRepositoryBuilder; import org.eclipse.jgit.storage.file.WindowCacheConfig; import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.FileUtils; import org.eclipse.osgi.util.NLS; import org.eclipse.team.core.RepositoryProvider; import org.eclipse.team.core.TeamException; /** * This class keeps information about how a project is mapped to * a Git repository. */ public class GitProjectData { private static final Map<IProject, GitProjectData> projectDataCache = new HashMap<IProject, GitProjectData>(); private static Set<RepositoryMappingChangeListener> repositoryChangeListeners = new HashSet<RepositoryMappingChangeListener>(); @SuppressWarnings("synthetic-access") private static final IResourceChangeListener rcl = new RCL(); private static class RCL implements IResourceChangeListener { @Override @SuppressWarnings("synthetic-access") public void resourceChanged(final IResourceChangeEvent event) { switch (event.getType()) { case IResourceChangeEvent.PRE_CLOSE: uncache((IProject) event.getResource()); break; case IResourceChangeEvent.PRE_DELETE: try { delete((IProject) event.getResource()); } catch (IOException e) { Activator.logError(e.getMessage(), e); } break; case IResourceChangeEvent.POST_CHANGE: update(event); break; default: break; } } } private static QualifiedName MAPPING_KEY = new QualifiedName( GitProjectData.class.getName(), "RepositoryMapping"); //$NON-NLS-1$ /** * Start listening for resource changes. */ public static void attachToWorkspace() { trace("attachToWorkspace - addResourceChangeListener"); //$NON-NLS-1$ ResourcesPlugin.getWorkspace().addResourceChangeListener( rcl, IResourceChangeEvent.POST_CHANGE | IResourceChangeEvent.PRE_CLOSE | IResourceChangeEvent.PRE_DELETE); } /** * Stop listening to resource changes */ public static void detachFromWorkspace() { trace("detachFromWorkspace - removeResourceChangeListener"); //$NON-NLS-1$ ResourcesPlugin.getWorkspace().removeResourceChangeListener(rcl); } /** * Register a new listener for repository modification events. * <p> * This is a no-op if <code>objectThatCares</code> has already been * registered. * </p> * * @param objectThatCares * the new listener to register. Must not be null. */ public static synchronized void addRepositoryChangeListener( final RepositoryMappingChangeListener objectThatCares) { if (objectThatCares == null) throw new NullPointerException(); repositoryChangeListeners.add(objectThatCares); } /** * Remove a registered {@link RepositoryMappingChangeListener} * * @param objectThatCares * The listener to remove */ public static synchronized void removeRepositoryChangeListener( final RepositoryMappingChangeListener objectThatCares) { repositoryChangeListeners.remove(objectThatCares); } /** * Notify registered {@link RepositoryMappingChangeListener}s of a change. * * @param which * the repository which has had changes occur within it. */ static void fireRepositoryChanged(final RepositoryMapping which) { Job job = new Job(CoreText.GitProjectData_repositoryChangedJobName) { @Override protected IStatus run(IProgressMonitor monitor) { RepositoryMappingChangeListener[] listeners = getRepositoryChangeListeners(); monitor.beginTask( CoreText.GitProjectData_repositoryChangedTaskName, listeners.length); for (RepositoryMappingChangeListener listener : listeners) { listener.repositoryChanged(which); monitor.worked(1); } monitor.done(); return Status.OK_STATUS; } @Override public boolean belongsTo(Object family) { if (JobFamilies.REPOSITORY_CHANGED.equals(family)) return true; return super.belongsTo(family); } }; job.schedule(); } /** * Get a copy of the current set of repository change listeners * <p> * The array has no references, so is safe for iteration and modification * * @return a copy of the current repository change listeners */ private static synchronized RepositoryMappingChangeListener[] getRepositoryChangeListeners() { return repositoryChangeListeners .toArray(new RepositoryMappingChangeListener[repositoryChangeListeners .size()]); } /** * @param p * @return {@link GitProjectData} for the specified project, or null if the * Git provider is not associated with the project or an exception * occurred */ @Nullable public synchronized static GitProjectData get(final @NonNull IProject p) { try { GitProjectData d = lookup(p); if (d == null && ResourceUtil.isSharedWithGit(p)) { d = new GitProjectData(p).load(); cache(p, d); } return d; } catch (IOException err) { Activator.logError(CoreText.GitProjectData_missing, err); return null; } } /** * Drop the Eclipse project from our association of projects/repositories * * @param p * Eclipse project * @throws IOException * if deletion of property files failed */ public static void delete(final IProject p) throws IOException { trace("delete(" + p.getName() + ")"); //$NON-NLS-1$ //$NON-NLS-2$ GitProjectData d = lookup(p); if (d == null) deletePropertyFiles(p); else d.deletePropertyFilesAndUncache(); } /** * Drop the Eclipse project from our association of projects/repositories * and remove all RepositoryMappings. * * @param p * to deconfigure * @throws IOException * if the property file cannot be removed. */ public static void deconfigure(final IProject p) throws IOException { trace("deconfigure(" + p.getName() + ")"); //$NON-NLS-1$ //$NON-NLS-2$ GitProjectData d = lookup(p); if (d == null) { deletePropertyFiles(p); } else { d.deletePropertyFilesAndUncache(); unmap(d); } } /** * Add the Eclipse project to our association of projects/repositories * * @param p * Eclipse project * @param d * {@link GitProjectData} associated with this project */ public static void add(final IProject p, final GitProjectData d) { trace("add(" + p.getName() + ")"); //$NON-NLS-1$ //$NON-NLS-2$ cache(p, d); } /** * Update mappings of EGit-managed projects in response to new DOT_GIT * repositories appearing. * * @param event * A {@link IResourceChangeEvent#POST_CHANGE} event */ public static void update(IResourceChangeEvent event) { // If the project is EGit-managed, let's see if this added any DOT_GIT // files or folders. If so, update the project's RepositoryMappings // and then mark as team private, if anything added. We won't get // deletions of DOT_GIT directories or files here, those are // "protected" and the GitMoveDeleteHook will prevent their deletion -- // the project has to be disconnected first. final Set<GitProjectData> modified = new HashSet<>(); try { event.getDelta().accept(new IResourceDeltaVisitor() { @Override public boolean visit(IResourceDelta delta) throws CoreException { IResource resource = delta.getResource(); int type = resource.getType(); if (type == IResource.ROOT) { return true; } else if (type == IResource.PROJECT) { return (delta.getKind() & (IResourceDelta.ADDED | IResourceDelta.CHANGED)) != 0 && ResourceUtil.isSharedWithGit(resource); } // Files & folders if ((delta.getKind() & (IResourceDelta.ADDED | IResourceDelta.CHANGED)) == 0 || resource.isLinked()) { return false; } IPath location = resource.getLocation(); if (location == null) { return false; } if (!Constants.DOT_GIT.equals(resource.getName())) { return type == IResource.FOLDER; } // A file or folder named .git File gitCandidate = location.toFile().getParentFile(); File git = new FileRepositoryBuilder() .addCeilingDirectory(gitCandidate) .findGitDir(gitCandidate).getGitDir(); if (git == null) { return false; } // Yes, indeed a valid git directory. GitProjectData data = get(resource.getProject()); if (data == null) { return false; } RepositoryMapping m = RepositoryMapping .create(resource.getParent(), git); // Is its working directory really here? If not, // a submodule folder may have been copied. try { Repository r = Activator.getDefault() .getRepositoryCache().lookupRepository(git); if (m != null && r != null && gitCandidate.equals(r.getWorkTree())) { if (data.map(m)) { data.mappings.put(m.getContainerPath(), m); modified.add(data); } } } catch (IOException e) { Activator.logError(e.getMessage(), e); } return false; } }); } catch (CoreException e) { Activator.logError(e.getMessage(), e); } finally { for (GitProjectData data : modified) { try { data.store(); } catch (CoreException e) { Activator.logError(e.getMessage(), e); } } } } static void trace(final String m) { // TODO is this the right location? if (GitTraceLocation.CORE.isActive()) GitTraceLocation.getTrace().trace( GitTraceLocation.CORE.getLocation(), "(GitProjectData) " + m); //$NON-NLS-1$ } private synchronized static void cache(final IProject p, final GitProjectData d) { projectDataCache.put(p, d); } private synchronized static void uncache(final IProject p) { if (projectDataCache.remove(p) != null) { trace("uncacheDataFor(" //$NON-NLS-1$ + p.getName() + ")"); //$NON-NLS-1$ } } private static void unmap(GitProjectData data) { for (RepositoryMapping m : data.mappings.values()) { IContainer c = m.getContainer(); if (c != null && c.isAccessible()) { try { c.setSessionProperty(MAPPING_KEY, null); // Team private members are re-set in // DisconnectProviderOperation } catch (CoreException e) { Activator.logWarning(MessageFormat.format( CoreText.GitProjectData_failedToUnmapRepoMapping, c.getFullPath()), e); } } } } private synchronized static GitProjectData lookup(final IProject p) { return projectDataCache.get(p); } /** * Update the settings for the global window cache of the workspace. */ public static void reconfigureWindowCache() { final WindowCacheConfig c = new WindowCacheConfig(); IEclipsePreferences d = DefaultScope.INSTANCE.getNode(Activator.getPluginId()); IEclipsePreferences p = InstanceScope.INSTANCE.getNode(Activator.getPluginId()); c.setPackedGitLimit(p.getInt(GitCorePreferences.core_packedGitLimit, d.getInt(GitCorePreferences.core_packedGitLimit, 0))); c.setPackedGitWindowSize(p.getInt(GitCorePreferences.core_packedGitWindowSize, d.getInt(GitCorePreferences.core_packedGitWindowSize, 0))); c.setPackedGitMMAP(p.getBoolean(GitCorePreferences.core_packedGitMMAP, d.getBoolean(GitCorePreferences.core_packedGitMMAP, false))); c.setDeltaBaseCacheLimit(p.getInt(GitCorePreferences.core_deltaBaseCacheLimit, d.getInt(GitCorePreferences.core_deltaBaseCacheLimit, 0))); c.setStreamFileThreshold(p.getInt(GitCorePreferences.core_streamFileThreshold, d.getInt(GitCorePreferences.core_streamFileThreshold, 0))); c.install(); } private final IProject project; private final Map<IPath, RepositoryMapping> mappings = new HashMap<>(); private final Set<IResource> protectedResources = new HashSet<IResource>(); /** * Construct a {@link GitProjectData} for the mapping * of a project. * * @param p Eclipse project */ public GitProjectData(final IProject p) { project = p; } /** * @return the Eclipse project mapped through this resource. */ public IProject getProject() { return project; } /** * Set repository mappings * * @param newMappings */ public void setRepositoryMappings(final Collection<RepositoryMapping> newMappings) { mappings.clear(); for (RepositoryMapping mapping : newMappings) { mappings.put(mapping.getContainerPath(), mapping); } remapAll(); } /** * Get repository mappings * * @return the repository mappings for a project */ public final Map<IPath, RepositoryMapping> getRepositoryMappings() { return mappings; } /** * Hide our private parts from the navigators other browsers. * * @throws CoreException */ public void markTeamPrivateResources() throws CoreException { for (final RepositoryMapping rm : mappings.values()) { final IContainer c = rm.getContainer(); if (c == null) continue; // Not fully mapped yet? final IResource dotGit = c.findMember(Constants.DOT_GIT); if (dotGit != null) { try { final Repository r = rm.getRepository(); final File dotGitDir = dotGit.getLocation().toFile() .getCanonicalFile(); // TODO: .git *files* with gitdir: "redirect" // TODO: check whether Repository.getDirectory() is // canonicalized! If not, this check will fail anyway. if (dotGitDir.equals(r.getDirectory())) { trace("teamPrivate " + dotGit); //$NON-NLS-1$ dotGit.setTeamPrivateMember(true); } } catch (IOException err) { throw new CoreException(Activator.error(CoreText.Error_CanonicalFile, err)); } } } } /** * Determines whether the project this instance belongs to has any inner * repositories like submodules or nested repositories. * * @return {@code true} if the project has inner repositories; {@code false} * otherwise. */ public boolean hasInnerRepositories() { return !protectedResources.isEmpty(); } /** * @param f * @return true if a resource is protected in this repository */ public boolean isProtected(final IResource f) { return protectedResources.contains(f); } /** * @param resource any workbench resource contained within this project. * @return the mapping for the specified project */ @Nullable public /* TODO static */ RepositoryMapping getRepositoryMapping( @Nullable IResource resource) { IResource r = resource; try { for (; r != null; r = r.getParent()) { final RepositoryMapping m; if (!r.isAccessible()) continue; m = (RepositoryMapping) r.getSessionProperty(MAPPING_KEY); if (m != null) return m; } } catch (CoreException err) { Activator.logError( CoreText.GitProjectData_failedFindingRepoMapping, err); } return null; } private void deletePropertyFilesAndUncache() throws IOException { deletePropertyFiles(getProject()); uncache(getProject()); } private static void deletePropertyFiles(IProject project) throws IOException { final File dir = propertyFile(project).getParentFile(); FileUtils.delete(dir, FileUtils.RECURSIVE); trace("deleteDataFor(" //$NON-NLS-1$ + project.getName() + ")"); //$NON-NLS-1$ } /** * Store information about the repository connection in the workspace * * @throws CoreException */ public void store() throws CoreException { final File dat = propertyFile(); final File tmp; boolean ok = false; try { trace("save " + dat); //$NON-NLS-1$ tmp = File.createTempFile( "gpd_", //$NON-NLS-1$ ".prop", //$NON-NLS-1$ dat.getParentFile()); final FileOutputStream o = new FileOutputStream(tmp); try { final Properties p = new Properties(); for (final RepositoryMapping repoMapping : mappings.values()) { repoMapping.store(p); } p.store(o, "GitProjectData"); //$NON-NLS-1$ ok = true; } finally { o.close(); if (!ok && tmp.exists()) { FileUtils.delete(tmp); } } if (dat.exists()) FileUtils.delete(dat); if (!tmp.renameTo(dat)) { if (tmp.exists()) FileUtils.delete(tmp); throw new CoreException( Activator.error(NLS.bind( CoreText.GitProjectData_saveFailed, dat), null)); } } catch (IOException ioe) { throw new CoreException(Activator.error( NLS.bind(CoreText.GitProjectData_saveFailed, dat), ioe)); } } private File propertyFile() { return propertyFile(getProject()); } private static File propertyFile(IProject project) { return new File(project.getWorkingLocation(Activator.getPluginId()) .toFile(), "GitProjectData.properties"); //$NON-NLS-1$ } private GitProjectData load() throws IOException { final File dat = propertyFile(); trace("load " + dat); //$NON-NLS-1$ final FileInputStream o = new FileInputStream(dat); try { final Properties p = new Properties(); p.load(o); mappings.clear(); for (final Object keyObj : p.keySet()) { final String key = keyObj.toString(); if (RepositoryMapping.isInitialKey(key)) { RepositoryMapping mapping = new RepositoryMapping(p, key); mappings.put(mapping.getContainerPath(), mapping); } } } finally { o.close(); } if (!remapAll()) { try { store(); } catch (CoreException e) { IStatus status = e.getStatus(); Activator.logError(status.getMessage(), status.getException()); } } return this; } private boolean remapAll() { protectedResources.clear(); boolean allMapped = true; Iterator<RepositoryMapping> iterator = mappings.values().iterator(); while (iterator.hasNext()) { RepositoryMapping m = iterator.next(); if (!map(m)) { iterator.remove(); allMapped = false; } } return allMapped; } private boolean map(final RepositoryMapping m) { final IResource r; final File git; final IResource dotGit; IContainer c = null; m.clear(); r = getProject().findMember(m.getContainerPath()); if (r instanceof IContainer) { c = (IContainer) r; } else if (r != null) { c = Utils.getAdapter(r, IContainer.class); } if (c == null) { logAndUnmapGoneMappedResource(m, null); return false; } m.setContainer(c); IPath absolutePath = m.getGitDirAbsolutePath(); if (absolutePath == null) { logAndUnmapGoneMappedResource(m, c); return false; } git = absolutePath.toFile(); if (!RepositoryCache.FileKey.isGitRepository(git, FS.DETECTED)) { logAndUnmapGoneMappedResource(m, c); return false; } try { m.setRepository(Activator.getDefault().getRepositoryCache() .lookupRepository(git)); } catch (IOException ioe) { logAndUnmapGoneMappedResource(m, c); return false; } trace("map " //$NON-NLS-1$ + c + " -> " //$NON-NLS-1$ + m.getRepository()); try { c.setSessionProperty(MAPPING_KEY, m); } catch (CoreException err) { Activator.logError( CoreText.GitProjectData_failedToCacheRepoMapping, err); } dotGit = c.findMember(Constants.DOT_GIT); if (dotGit != null) { protect(dotGit); } fireRepositoryChanged(m); return true; } private void logAndUnmapGoneMappedResource(final RepositoryMapping m, final IContainer c) { Activator.logError(MessageFormat.format( CoreText.GitProjectData_mappedResourceGone, m.toString()), new FileNotFoundException(m.getContainerPath().toString())); m.clear(); if (c instanceof IProject) { UnmapJob unmapJob = new UnmapJob((IProject) c); unmapJob.schedule(); } else if (c != null) { try { c.setSessionProperty(MAPPING_KEY, null); } catch (CoreException e) { Activator.logWarning(MessageFormat.format( CoreText.GitProjectData_failedToUnmapRepoMapping, c.getFullPath()), e); } } } private void protect(IResource resource) { IResource c = resource; try { c.setTeamPrivateMember(true); } catch (CoreException e) { Activator.logError(MessageFormat.format( CoreText.GitProjectData_FailedToMarkTeamPrivate, c.getFullPath()), e); } while (c != null && !c.equals(getProject())) { trace("protect " + c); //$NON-NLS-1$ protectedResources.add(c); c = c.getParent(); } } private static class UnmapJob extends Job { private final IProject project; private UnmapJob(IProject project) { super(MessageFormat.format(CoreText.GitProjectData_UnmapJobName, project.getName())); this.project = project; } @Override protected IStatus run(IProgressMonitor monitor) { try { RepositoryProvider.unmap(project); return Status.OK_STATUS; } catch (TeamException e) { return new Status(IStatus.ERROR, Activator.getPluginId(), MessageFormat.format( CoreText.GitProjectData_UnmappingGoneResourceFailed, project.getName()), e); } } } }