blob: 507c37fd4c026f7db29575a183a177f8373aad95 [file] [log] [blame]
/*******************************************************************************
* 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 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*******************************************************************************/
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.jgit.util.SystemReader;
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<>();
private static Set<RepositoryMappingChangeListener> repositoryChangeListeners = new HashSet<>();
@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.setUser(false);
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 && !r.isBare()
&& 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)));
if (SystemReader.getInstance().isWindows()) {
c.setPackedGitMMAP(false);
} else {
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<>();
/**
* 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);
}
}
}
}